Ë
    ÝÄ'j+(  ã                   óp  — d Z ddlZddlZddlmZ ddlmZ defd„Zdefd„Zde	fd	„Z
de	fd
„Zde	fd„Zde	fd„Zde	fd„Zde	fd„Zde	defd„Zdededz  fd„Zde	dedz  fd„Zde	de	dedz  fd„Zde	de	defd„Z	 	 	 	 	 	 d$dedede	de	de	de	dz  de	deded ed!e	defd"„Z	 	 d%dede	de	de	de	d!e	defd#„Zy)&uÒ  
Azure DevOps REST API client (api-version=7.1).

Handles:
- Monthly iteration (classification node) â€” auto-creates if missing
- Monthly User Story ("Maintenance :: Month Year") â€” auto-creates if missing
- Task creation under the User Story, linked to Zammad ticket
- Idempotent: checks for existing task by ticket number before creating

Auth: HTTP Basic with PAT (Personal Access Token).
URL: Supports both hosted (dev.azure.com) and self-hosted (DEVOPS_URL).
é    N)Údatetimeé   )ÚsettingsÚreturnc                  ó–   — t        j                  dt        j                  › �j	                  «       «      j                  «       } d| › �ddœS )z.Basic auth headers for standard JSON requests.ú:úBasic zapplication/json©ÚAuthorizationzContent-Type©Úbase64Ú	b64encoder   Ú
devops_patÚencodeÚdecode©Útokens    ú-/var/www/html/zammad-automation/app/devops.pyÚ_authr      sI   € ä×Ñ˜q¤×!4Ñ!4Ð 5Ð6×=Ñ=Ó?Ó@×GÑGÓI€Eà! % Ð)Ø*ñð ó    c                  ó–   — t        j                  dt        j                  › �j	                  «       «      j                  «       } d| › �ddœS )zEBasic auth headers for JSON Patch requests (work item create/update).r   r	   zapplication/json-patch+jsonr
   r   r   s    r   Ú_patch_headersr      sI   € ä×Ñ˜q¤×!4Ñ!4Ð 5Ð6×=Ñ=Ó?Ó@×GÑGÓI€Eà! % Ð)Ø5ñð r   c                  óf   — t         j                  j                  d«      › dt         j                  › �S )z(Base URL including org: https://host/orgú/)r   Ú
devops_urlÚrstripÚ
devops_org© r   r   Ú_rootr   &   s,   € ä×!Ñ!×(Ñ(¨Ó-Ð.¨a´×0CÑ0CÐ/DÐEÐEr   c                  ó<   — t        «       › dt        j                  › �S )z!Base URL including org + project.r   )r   r   Údevops_projectr   r   r   Ú_baser"   +   s   € ä‹gˆY�aœ×/Ñ/Ð0Ð1Ð1r   c                  ó^   — t        «       › dt        j                  › dt        j                  › �S )zCBase URL including org + project + team (for team-scoped settings).r   )r   r   r!   Údevops_teamr   r   r   Ú
_team_baser%   0   s)   € ä‹gˆY�aœ×/Ñ/Ð0°´(×2FÑ2FÐ1GÐHÐHr   c                  óˆ   — t        j                  «       } d| j                  d«      j                  «       › d| j                  › �S )z.Current month iteration name, e.g. M_JUN_2026.ÚM_z%bÚ_)r   ÚnowÚstrftimeÚupperÚyear©r)   s    r   Úiteration_namer.   5   s7   € ä
�,‰,‹.€CØ�—‘˜TÓ"×(Ñ(Ó*Ð+¨1¨S¯X©X¨JÐ7Ð7r   c                  óR   — t        j                  «       } d| j                  d«      › �S )z@Current month User Story title, e.g. 'Maintenance :: June 2026'.zMaintenance :: z%B %Y)r   r)   r*   r-   s    r   Ústory_titler0   ;   s$   € ä
�,‰,‹.€CØ˜SŸ\™\¨'Ó2Ð3Ð4Ð4r   c               ƒ   ó¦  K  — t        «       } d}t        j                  d¬«      4 ƒd{  –—† }|j                  t	        «       › d| › d|› �t        «       ¬«      ƒ d{  –—† }|j                  dk(  r|j                  «       }nS|j                  t	        «       › d	|› �t        «       d
| i¬«      ƒ d{  –—† }|j                  «        |j                  «       }|d   }|j                  t        «       › d|› �t        «       d|i¬«      ƒ d{  –—†  ddd«      ƒd{  –—†  t        j                  › d| › �S 7 �Œ7 Œ×7 Œ†7 Œ07 Œ"# 1 ƒd{  –—†7  sw Y   Œ2xY w­w)z�Ensure monthly iteration classification node exists and is assigned to the team.
    Returns full iteration path like 'ProjectName\M_JUN_2026'.z7.1é   ©ÚtimeoutNú*/_apis/wit/classificationNodes/iterations/z?api-version=©ÚheaderséÈ   z6/_apis/wit/classificationNodes/iterations?api-version=Úname©r7   ÚjsonÚidz0/_apis/work/teamsettings/iterations?api-version=ú\)r.   ÚhttpxÚAsyncClientÚgetr"   r   Ústatus_coder;   ÚpostÚraise_for_statusr%   r   r!   )r9   ÚapiÚclientÚrÚnodeÚr2Úiter_ids          r   Úget_or_create_iterationrJ   A   sd  è ø€ ô Ó€DØ
€Cä× Ñ ¨Ô,÷ 
ð 
°à—*‘*Ü‹wˆiÐAÀ$ÀÀ}ÐUXÐTYÐZÜ“Gð ó 
÷ 
ˆð �=‰=˜CÒØ—6‘6“8‰Dð —{‘{Ü“7�)ÐQÐRUÐQVÐWÜ›Ø˜d�^ð #ó ÷ ˆBð
 ×ÑÔ!Ø—7‘7“9ˆDð �t‘*ˆØ�k‰kÜ‹|ˆnÐLÈSÈEÐRÜ“GØ˜�ð ó 
÷ 	
ð 	
÷)
÷ 
ô4 ×%Ñ%Ð& b¨¨Ð/Ð/ð5
ùð
øðøð	
øð)
ø÷ 
÷ 
ñ 
üs|   ‚'E©D1ªE­/D<ÁD4ÁAD<Â/D6Â0AD<ÄD8ÄD<ÄEÄD:ÄEÄ4D<Ä6D<Ä8D<Ä:EÄ<EÅEÅEÅ
Er9   c              ƒ   ó   K  — t        j                  d¬«      4 ƒd{  –—† }|j                  t        «       › d| › d�t	        «       ¬«      ƒ d{  –—† }|j
                  dk(  cddd«      ƒd{  –—†  S 7 ŒU7 Œ&7 Œ	# 1 ƒd{  –—†7  sw Y   yxY w­w)zPReturn True if iteration classification node exists (used by web UI validation).r2   r3   Nr5   ú?api-version=7.1r6   r8   )r>   r?   r@   r"   r   rA   )r9   rE   rF   s      r   Úcheck_iteration_existsrM   d   sŠ   è ø€ ä× Ñ ¨Ô,÷ $ð $°Ø—*‘*Ü‹wˆiÐAÀ$ÀÐGWÐXÜ“Gð ó 
÷ 
ˆð �}‰} Ñ#÷$÷ $ò $øð
øð$ø÷ $÷ $ñ $üsV   ‚B�A3žB¡-A9ÁA5ÁA9Á!BÁ-A7Á.BÁ5A9Á7BÁ9BÁ?BÂ BÂBÚitem_idc              ƒ   óˆ  K  — t        j                  d¬«      4 ƒd{  –—† }|j                  t        «       › d| › d�t	        «       ¬«      ƒ d{  –—† }|j
                  dk(  r	 ddd«      ƒd{  –—†  y|j                  «        |j                  «       cddd«      ƒd{  –—†  S 7 Œ‡7 ŒX7 Œ:7 Œ# 1 ƒd{  –—†7  sw Y   yxY w­w)z9Fetch a work item by ID. Returns None if not found (404).r2   r3   Nú/_apis/wit/workitems/rL   r6   i”  )r>   r?   r@   r"   r   rA   rC   r;   )rN   rE   rF   s      r   Úget_work_itemrQ   n   s¹   è ø€ ä× Ñ ¨Ô,÷ ð °Ø—*‘*Ü‹wˆiÐ,¨W¨IÐ5EÐFÜ“Gð ó 
÷ 
ˆð �=‰=˜CÒØ÷÷ ð ð 	
×ÑÔØ�v‰v‹x÷÷ ò øð
øðøð ø÷ ÷ ñ üst   ‚C�B%žC¡-B-ÁB'ÁB-Á#CÁ.B)Á/CÁ4B-ÂCÂB+Â CÂ'B-Â)CÂ+CÂ-B?Â3B6Â4B?Â;CÚticket_numberc              ƒ   óô  K  — dd| › d�i}t        j                  d¬«      4 ƒd{  –—† }|j                  t        «       › d�t	        «       |¬«      ƒ d{  –—† }|j                  «        |j                  «       j                  d	g «      }|s	 ddd«      ƒd{  –—†  y|d
   d   }t        «       › dt        j                  › d|› �}||dœcddd«      ƒd{  –—†  S 7 Œµ7 Œˆ7 ŒG7 Œ# 1 ƒd{  –—†7  sw Y   yxY w­w)zqSearch for an existing Task with this ticket number in the title.
    Returns {id, url} if found, None otherwise.Úqueryz‰SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.WorkItemType] = 'Task' AND [System.Title] CONTAINS '#z" ' AND [System.State] <> 'Removed'r2   r3   Nú/_apis/wit/wiql?api-version=7.1r:   Ú	workItemsr   r<   r   ú/_workitems/edit/©r<   Úurl)r>   r?   rB   r"   r   rC   r;   r@   r   r   r!   )rR   ÚwiqlrE   rF   ÚitemsÚtask_idÚtask_urls          r   Úfind_existing_taskr^   {   s  è ø€ ð 	ð-ð .;¨Oð <.ð.ð€Dô × Ñ ¨Ô,÷ 0ð 0°Ø—+‘+Ü‹wˆiÐ6Ð7Ü“GØð ó 
÷ 
ˆð
 	
×ÑÔØ—‘“—‘˜[¨"Ó-ˆÙØ÷0÷ 0ð 0ð ˜‘(˜4‘.ˆÜ“g�Y˜a¤× 7Ñ 7Ð8Ð8IÈ'ÈÐSˆØ hÑ/÷0÷ 0ò 0øð
øð0øð 0ø÷ 0÷ 0ñ 0üst   ‚#C8¥C¦C8©+C#ÁCÁ7C#ÂC8ÂCÂC8Â,C#Ã	C8ÃC!ÃC8ÃC#ÃC8Ã!C8Ã#C5Ã)C,Ã*C5Ã1C8ÚtitleÚ	iter_pathc              ƒ   óŒ  K  — dd| › d|› d�i}t        j                  d¬«      4 ƒd{  –—† }|j                  t        «       › d�t	        «       |¬	«      ƒ d{  –—† }|j                  «        |j                  «       j                  d
g «      }|r|d   d   ndcddd«      ƒd{  –—†  S 7 Œ€7 ŒS7 Œ	# 1 ƒd{  –—†7  sw Y   yxY w­w)zsSearch for a User Story with exact title in the given iteration.
    Returns work item ID if found, None otherwise.rT   z‡SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.WorkItemType] = 'User Story' AND [System.Title] = 'z ' AND [System.IterationPath] = 'z!' AND [System.State] <> 'Removed'r2   r3   NrU   r:   rV   r   r<   )r>   r?   rB   r"   r   rC   r;   r@   )r_   r`   rZ   rE   rF   r[   s         r   Úfind_user_storyrb   –   sÙ   è ø€ ð 	ð%ð &+ Gð ,-Ø-6¨Kð 8.ð	.ð	€Dô × Ñ ¨Ô,÷ 1ð 1°Ø—+‘+Ü‹wˆiÐ6Ð7Ü“GØð ó 
÷ 
ˆð
 	
×ÑÔØ—‘“—‘˜[¨"Ó-ˆÙ!&ˆu�Q‰x˜Š~¨D÷1÷ 1ò 1øð
øð1ø÷ 1÷ 1ñ 1üsV   ‚&C¨B)©C¬+B/ÁB+Á?B/ÂCÂ#B-Â$CÂ+B/Â-CÂ/CÂ5B8Â6CÂ=Cc              ƒ   óp  K  — dd| dœdd|dœddddœddddœg}t        j                  d	¬
«      4 ƒd{  –—† }|j                  t        «       › d�t	        «       |¬«      ƒ d{  –—† }|j                  «        |j                  «       d   cddd«      ƒd{  –—†  S 7 Œg7 Œ:7 Œ	# 1 ƒd{  –—†7  sw Y   yxY w­w)z2Create a new User Story work item. Returns its ID.Úaddú/fields/System.Title©ÚopÚpathÚvalueú/fields/System.IterationPathz/fields/System.WorkItemTypez
User Storyú/fields/System.StateÚActiver2   r3   Nz0/_apis/wit/workitems/$User Story?api-version=7.1r:   r<   )r>   r?   rB   r"   r   rC   r;   )r_   r`   ÚpatchrE   rF   s        r   Úcreate_user_storyrn   ®   sË   è ø€ ð Ð4ÀuÑMØÐ<ÀyÑQØÐ;À|ÑTØÐ4ÀxÑPð	€Eô × Ñ ¨Ô,÷ ð °Ø—+‘+Ü‹wˆiÐGÐHÜ"Ó$Øð ó 
÷ 
ˆð
 	
×ÑÔØ�v‰v‹x˜‰~÷÷ ò øð
øðø÷ ÷ ñ üsV   ‚1B6³B´B6·+B!Á"BÁ#&B!Â	B6ÂBÂB6ÂB!ÂB6Â!B3Â'B*Â(B3Â/B6Ústory_idÚ	ticket_idÚticket_titleÚ
ticket_urlÚactivityÚoriginal_estimateÚremaining_workÚexpert_estimateÚtagsc           
   ƒ   ó   K  — |€t        «       ƒ d{  –—† }d|› d|› d|› d|› d�	}ddd	|› d
|› �dœdd|dœdd|dœddddœdddt        «       › d| › �dœdœg}|dd|dœdd|dœdd|dœdd|	dœgz  }|
r|j                  dd|
dœ«       t        j                  d¬«      4 ƒd{  –—† }|j                  t        «       › d�t        «       |¬«      ƒ d{  –—† }|j                  «        |j                  «       }|d   }t        «       › dt        j                  › d|› �}||d œcddd«      ƒd{  –—†  S 7 �Œ7 Œ‘7 Œd7 Œ# 1 ƒd{  –—†7  sw Y   yxY w­w)!záCreate a Task work item linked to a parent User Story.

    Title format: '#<ticket_number> <ticket_title>'
    Custom.ExpertEstimate maps to a custom field in this DevOps instance.
    Returns {id, url} of created task.
    Nz<b>Zammad Ticket:</b> #z<br><b>Title:</b> z<br><b>Link:</b> <a href="z">z</a>rd   re   ú#ú rf   z/fields/System.Descriptionrj   rk   ÚNewz/relations/-z"System.LinkTypes.Hierarchy-ReverserP   )ÚrelrY   z&/fields/Microsoft.VSTS.Common.Activityz2/fields/Microsoft.VSTS.Scheduling.OriginalEstimatez//fields/Microsoft.VSTS.Scheduling.RemainingWorkz/fields/Custom.ExpertEstimatez/fields/System.Tagsr2   r3   z*/_apis/wit/workitems/$Task?api-version=7.1r:   r<   r   rW   rX   )rJ   r   Úappendr>   r?   rB   r"   r   rC   r;   r   r!   )ro   rp   rR   rq   rr   r`   rs   rt   ru   rv   rw   Údescriptionrm   rE   rF   Úitemr\   r]   s                     r   Úcreate_taskr€   À   sÖ  è ø€ ð& ÐÜ1Ó3×3ˆ	ð " - ð 1Ø%˜ð '!Ø!+ ¨B¨z¨l¸$ð	@ð ð Ð4Ø�m�_ A l ^Ð4ñ	6àÐ:ÐZeÑfØÐ<ÐZcÑdØÐ4ÐZ_Ñ`à˜nØ7Ü“g�YÐ3°H°:Ð>ñ8
ñ 	ð€Eð 
ØÐFÐ]eÑfØÐRÐ]nÑoØÐOÐ]kÑlØÐ=Ð^mÑnð	ñ €Eñ Ø�‰˜EÐ+@È4ÑPÔQä× Ñ ¨Ô,÷ 
0ð 
0°Ø—+‘+Ü‹wˆiÐAÐBÜ"Ó$Øð ó 
÷ 
ˆð
 	
×ÑÔØ�v‰v‹xˆØ�t‘*ˆÜ“g�Y˜a¤× 7Ñ 7Ð8Ð8IÈ'ÈÐSˆØ hÑ/÷
0÷ 
0ñ 
0ð= 4ùð<
0øð
øð
0ø÷ 
0÷ 
0ñ 
0üsk   ‚E’D0“BEÂ!D3Â"EÂ%+D9ÃD5ÃAD9ÄEÄ*D7Ä+EÄ3EÄ5D9Ä7EÄ9EÄ?EÅ EÅEc           
   ƒ   ó>  K  — t        |«      ƒ d{  –—† }|rd|d   |d   ddœS t        «       ƒ d{  –—† }t        «       }t        ||«      ƒ d{  –—† }	|	€t	        ||«      ƒ d{  –—† }	t        |	| ||||||¬«      ƒ d{  –—† }
|	|
d   |
d   ddœS 7 Œ�7 Œb7 ŒF7 Œ27 Œ­w)a  Idempotent task creation flow:
      1. Return existing task if found (no duplicate)
      2. Ensure monthly iteration exists
      3. Ensure monthly User Story exists
      4. Create and return new Task
    Returns {story_id, task_id, task_url, created: bool}.Nr<   rY   F)ro   r\   r]   Úcreated)r`   rs   rw   T)r^   rJ   r0   rb   rn   r€   )rp   rR   rq   rr   rs   rw   Úexistingr`   Ús_titlero   Útasks              r   Úensure_and_creater†   ÿ   s¿   è ø€ ô (¨Ó6×6€HÙØ ¨X°d©^ÈÐRWÉÐejÑkÐkä-Ó/×/€IÜ“€Gä$ W¨iÓ8×8€HØÐÜ*¨7°IÓ>×>ˆäØ�)˜]¨L¸*ØØØô	÷ €Dð !¨T°$©ZÀTÈ%Á[Ð]aÑbÐbð# 7øð 0øð 9øà>øðúsU   ‚B‘B’ B²B³BÁBÁBÁ&BÁ'BÂBÂBÂBÂBÂBÂB)NÚDevelopmentç      à?rˆ   g      Ð?Ú )r‡   r‰   )Ú__doc__r   r>   r   Úconfigr   Údictr   r   Ústrr   r"   r%   r.   r0   rJ   ÚboolrM   ÚintrQ   r^   rb   rn   Úfloatr€   r†   r   r   r   ú<module>r‘      sß  ðñó Û Ý Ý ðˆtó ð˜ó ðFˆsó Fð
2ˆsó 2ð
I�Có Ið
8˜ó 8ð5�Só 5ð 0 só  0ðF$ sð $¨tó $ð
 ð 
¨°©ó 
ð0¨Cð 0°D¸4±Kó 0ð61 ð 1°ð 1¸¸t¹ó 1ð0 3ð °3ð ¸3ó ð0 !Ø!Ø"ØØ!Øñ<0Øð<0àð<0ð ð<0ð ð	<0ð
 ð<0ð �T‰zð<0ð ð<0ð ð<0ð ð<0ð ð<0ð ð<0ð 
ó<0ðH "ØñcØðcàðcð ðcð ð	cð
 ðcð ðcð 
ôcr   