
    4MjJO                         d Z ddlZddlZddlmZmZ ddlZ ej                  d      ZdZ ej                  d      Z
dedefd	Zd
edee   fdZd
edee   fdZdedz  fdZdefdZ G d d      Zy)u:  
clickup_client.py — ClickUp V2 API client for the Winston quote pipeline (DVI-815).

Handles:
  - Task discovery filtered by date (date_updated, date_created, or due_date)
  - File attachment upload to a task
  - Custom field value set (Quote Amount write-back)
  - Attachment existence check for deduplication
    N)datetimetimezoneztogen.clickup_clientzhttps://api.clickup.com/api/v2z (\d{2}-\d{4}[A-Z0-9]*(?:_v\d+)?)tagreturnc                 P    t        j                  dd| t         j                        S )u   Strip trailing version suffix (v1–v99, case-insensitive) from a tag.

    Mirrors PoC strip_version_suffix: 'xv2' → 'x', 'SHv1' → 'SH'.
    z	v\d{1,2}$ )flags)resub
IGNORECASE)r   s    %/var/www/html/togen/clickup_client.pystrip_version_suffixr      s    
 66,Cr}}==    taskc                     | j                  d      xs g D cg c]H  }|j                  d      xs dj                         r#|j                  d      xs dj                         J c}S c c}w )zTag names on THIS task only (not subtasks). Used for DEBUG logging so
    the raw ClickUp `tags` field is visible (DVI-994 point 2).tagsnamer   )getstrip)r   ts     r   _raw_tag_namesr   "   sd     hhv&,"P!%%-2E21L1L1N UU6] b'') P P Ps   AA(c                    g }| j                  d      xs g D ]9  }|j                  d      xs dj                         }|s)|j                  |       ; | j                  d      xs g D ]  }|j                  t	        |              t               }g }|D ])  }||vs|j                  |       |j                  |       + |S )u  Collect tag names from a task and all its subtasks recursively.

    Mirrors PoC _collect_tags: deduplicates, preserves order.
    Tags are returned as-is (not version-stripped) so callers can log
    the raw value and strip on demand.

    NOTE (DVI-994): the ClickUp list/team task endpoints return subtasks as
    FLAT sibling entries, not nested under a parent's ``subtasks`` key, so this
    recursion only sees subtasks that a prior per-task detail fetch nested in
    (see :meth:`ClickUpClient._nest_subtask_tags`). The quote-type tags
    (``xv1``/``shv2``/…) live on those subtasks, so without nesting this returns
    ``[]`` for every parent quote task.
    r   r   r   subtasks)r   r   appendextend_collect_tagssetadd)r   r   r   r   r   seenoutr   s           r   r   r   )   s     DXXf# f#**,KK xx
#)r (M#&'( UDC d?HHSMJJsO Jr   c                     | dv ry	 t        j                  t        |       dz  t        j                        j                         S # t        t        t        f$ r Y yw xY w)zConvert a ClickUp epoch-millisecond timestamp (str|int) to an ISO-8601
    UTC string, or None when absent/unparseable. DVI-972 (diagnostics only).)Nr   r   0N  )tz)	r   fromtimestampintr   utc	isoformat
ValueError	TypeErrorOSError)mss    r   
_ms_to_isor-   H   sV     
%%c"gnFPPRR	7+ s   >A AAcfc                 L   t        | t              sy| j                  d      }|y| j                  dd      }| j                  d      xs i }	 |dk(  rZ|j                  d      xs g }|D ]>  }|j                  d      |k(  s|j                  d	      |k(  s,|j                  d
|      c S  |S |dk(  rt        |t              rr|j                  d      xs g D ci c]4  }|j                  d	      |j                  d|j                  d
d            6 }}|D cg c]  }|j                  ||       c}S |dv rFt        |t              r6|D 	cg c]*  }	|	j                  d      xs |	j                  d      xs d, c}	S |S c c}w c c}w c c}	w # t        t
        f$ r |cY S w xY w)zReturn a human-readable value for a ClickUp custom field, resolving
    drop_down/labels ids to their option names. DVI-972 (diagnostics only).Nvaluetyper   type_config	drop_downoptions
orderindexidr   labelslabel)usersusernameemail)
isinstancedictr   listAttributeErrorr*   )
r.   r0   cf_typecfgr4   optoby_idvus
             r   _resolve_cf_valuerG   S   s    b$FF7OE}ffVR G
&&

%2Ck!ggi(.BG 277<(E1SWWT]e5K7765112 Lh:eT#:"wwy17R: UU4[!%%vr1B"CC :E :-23EIIaO33j Zt%<INOAQUU:&>!%%.>B>OO L:3OI& sU   AF F *F -,F 9F F F1F 4F /F
;F  F F#"F#c                      e Zd ZdZdefdZddddeded	ed
edee   f
dZ	dedefdZ
deeef   dedefdZdeeef   dedefdZdeeef   defdZdedee   fdZdee   ddfdZdedee   deddfdZdededz  fdZddddeded	ed
edee   f
d Z	 d)ded!ed"ed#edef
d$Zded!edefd%Zded&ed'edefd(Zy)*ClickUpClientz9Minimal ClickUp V2 client for the Winston quote pipeline.api_keyc                 R    |st        d      || _        |dd| _        d|i| _        y )NzClickUp API key is requiredzapplication/json)AuthorizationzContent-TyperL   )r)   _key_json_headers_bare_headers)selfrJ   s     r   __init__zClickUpClient.__init__r   s7    :;;	$.
 .w7r   date_updatedT)
date_fieldinclude_closedlist_idtarget_daterS   rT   r   c                @   t        j                  |      }t        t        |j                  |j                  |j
                  dddt        j                        j                         dz        }t        t        |j                  |j                  |j
                  dddt        j                        j                         dz        }g }d}		 | d|dz
  | d	|dz   d
dd|	i}
|rd|
d<   	 t        j                  t         d| d| j                  |
d      }|j                   dk7  r1t        j#                  d|j                   |||j$                  dd        nN|j'                         }|j                  dg       }|j)                  |       |j                  dd      s|sn|	dz  }	t        j+                  dt-        |      |||       | j/                  |       |D cg c]  }| j1                  |       c}S # t        $ r t        j                  d|	       Y sw xY wc c}w )zReturn normalized quote-task dicts for tasks matching target_date.

        target_date : 'YYYY-MM-DD'
        date_field  : 'date_updated' | 'date_created' | 'due_date'
        r   tzinfor#      ;   T_gt   _ltr   truepagerT   z/list//task   headersparamstimeoutz/ClickUp get_quote_tasks request error (page %d)   z1ClickUp get_tasks failed (%s) list=%s date=%s: %sN,  tasks	last_pagez6ClickUp: %d task(s) found for list=%s date=%s field=%s)r   fromisoformatr&   yearmonthdayr   r'   	timestamprequestsr   _CLICKUP_BASErN   	Exceptionlog	exceptionstatus_codeerrortextjsonr   infolen_nest_subtask_tags_parse_task)rP   rU   rV   rS   rT   dstart_msend_ms	raw_tasksr`   re   rdatabatchr   s                  r   get_quote_taskszClickUpClient.get_quote_tasks   s    "";/QVVQWWaeeQ1X\\JTTVY]]
 QVVQWWaeeRRMWWY\``
 !#	,c"HqL,c"FQJF	)F +1'(	LL$oVG9E: ..!	 }}#		GMMFF4CL 668DHHWb)EU#xxT*%AIDI L 	D	N	
 		*-67  #77=  OQUV< 8s   )-G6 H6HHr   c                    |j                  dd      }t        j                  |      }|r|j                  d      n|j	                         }|j                  dg       D ci c]#  }|j                  dd      j                         |% }}| j                  |ddd      }| j                  |dd	d
      }| j                  |ddd      }	|	s)|j                  dg       }
|
r|
d   j                  dd      }	|j                  d      xs i }t        |t              r|j                  dd      nd}| j                  |dd	d
      }|j                  d      xs g D cg c]8  }|j                  d      xs |j                  d      xs dj	                         : }
}|
D cg c]  }|s|	 }
}t        |      }||j                  dd      |||	||j                  dd      |||
t        |j                  d            t        |j                  d            t        |j                  d            | j                  |j                  dg             |dS c c}w c c}w c c}w )z@Normalize a raw ClickUp task into the fields the pipeline needs.r   r   r]   custom_fieldsr;   	recipientzcustomer emailamountzquote amounttotalsalespersonownerz	sales rep	assigneesr   statusr:   r6   urldate_createdrR   due_date)quote_numberclickup_task_id	task_namerecipient_emailowner_emailquote_amount_field_idr   r   amount_valuer   r   rR   r   r   r   )r   	_QUOTE_REsearchgroupr   lower_cf_str_cf_idr<   r=   	_cf_valuer   r-   _custom_fields_readable)rP   r   r   mr   r.   
cf_by_namer   r   r   r   
status_objstatus_namer   ar   s                   r   r|   zClickUpClient._parse_task   s>   xx#T"%&qwwqzDJJL 6:XXor5R'
/1BFF62$$&*'

 '
 ,,z7KIYZ $J.RY Zll:}g{Sb1I'l..w; XXh'-2
6@T6RjnnXr2XZ~~j(NGT hh{+1r
 UU:6!%%.6B==?
	 
 !*/1QQ/	/ T" )#xxb1.&%:88E2&!("&txx'?@&txx'?@"488J#78!99$((?TV:WX#
 	
9'
$
 0s   (I	=IIIfieldskeywordsc                     |D ]I  }|j                         D ]4  \  }}||v s|j                  d      }|st        |t              s0|c c S  K y)zGReturn value of the first custom field whose name contains any keyword.r0   r   )itemsr   r<   str)rP   r   r   kwr   r.   rE   s          r   r   zClickUpClient._cf_str   sT     	!B"LLN !b:wAZ3/ 	!	! r   c                 v    |D ]4  }|j                         D ]  \  }}||v s|j                  dd      c c S  6 y)zDReturn id of the first custom field whose name contains any keyword.r6   r   )r   r   rP   r   r   r   r   r.   s         r   r   zClickUpClient._cf_id	  sG     	,B"LLN ,b:66$++,	, r   c                 h    |D ]-  }|j                         D ]  \  }}||v st        |      c c S  / y)zGReturn the resolved value of the first custom field matching a keyword.N)r   rG   r   s         r   r   zClickUpClient._cf_value  sA     	1B"LLN 1b:,R001	1 r   r   c                     g }|xs g D ]9  }t        |      }|ddg fv r|j                  |j                  dd      |d       ; |S )zDVI-972: normalize all custom fields to [{name, value}] with human-
        readable values (drop_down/labels resolved), skipping empty values.Nr   r   )r   r0   )rG   r   r   )rP   r   r    r.   vals        r   r   z%ClickUpClient._custom_fields_readable  s]     %2 	CB#B'CtRn$JJvr 2SAB		C
 
r   r   Nc                    |D ch c]&  }|j                  d      s|j                  dd      ( }}|D ]  }|j                  dd      }t        |      }|rt        j                  d||       :|r| j	                  |      nd}|xs i j                  d      xs g }|r ||d<   |D ]  }| j                  ||d        t        d|j                  dg       i      }	t        j                  d|t        |      |	        yc c}w )	a  Mutate raw_tasks so each task carries its full nested subtask tree.

        ANY-DEPTH recursive fetch (DVI-999 / PoC fetch_subtask_names): for tasks
        with no own tags, calls GET /task/{id}?include_subtasks=true (nests one
        level), then recursively fetches each subtask's detail so _collect_tags
        can reach tags at any depth. Uses a visited set seeded with all top-level
        task IDs to avoid cycles; depth capped at 10.
        r6   r   z6ClickUp task %s: own tags=%s (no subtask fetch needed)Nr   r]   )depthz?ClickUp task %s: own_tags=[] nested_subtasks=%d subtask_tags=%s)r   r   rs   debugget_task_detail_fetch_subtask_treer   rz   )
rP   r   r   visitedt_idowndetailnestedr   sub_tagss
             r   r{   z ClickUpClient._nest_subtask_tags(  s	    7@O155;QUU4_OO 	A55r?D #C		L# 37T))$/TFl''
39rF &*! DC,,S',CD$j!%%
B2G%HIHIIQc&k8!	 Ps
   C?C?subtaskr   r   c                 P   |dk\  ry|j                  dd      }|r||v ry|j                  |       | j                  |      }|sy|j                  d      xs |j                  d      xs g |d<   |j                  d      xs g }||d<   |D ]  }| j                  |||dz           y)a  Recursively fetch and attach full detail (tags + children) for a subtask.

        PoC parity (DVI-999): each level of subtasks may itself have subtasks
        carrying the quote-type tags, so we drill down until depth>=10 or the
        visited set closes the cycle.
        
   Nr6   r   r   r   r]   )r   r   r   r   )rP   r   r   r   sub_idr   childrenchilds           r   r   z!ClickUpClient._fetch_subtask_treeG  s     B;T2&7*F%%f- **V,IF0CIr::j)/R&
 	@E$$UGUQY?	@r   task_idc                 N   |sy	 t        j                  t         d| | j                  ddid      }|j                  dk7  r0t
        j                  d	|j                  ||j                  dd
        y|j                         S # t        $ r t
        j                  d|       Y yw xY w)zGET a single task with nested subtasks (`include_subtasks=true`).

        Returns the raw task dict or None on error. Unlike the list/team task
        endpoints, this nests subtasks under the `subtasks` key.
        N/task/include_subtasksr_   rb   rc   z-ClickUp get_task_detail request error task=%srg   z/ClickUp get_task_detail failed (%s) task=%s: %srh   )rp   r   rq   rN   rr   rs   rt   ru   rv   rw   rx   )rP   r   r   s      r   r   zClickUpClient.get_task_detail]  s     		 /y1***F3	A ==CIIAwt vvx  	MMI7S	s   .B B$#B$team_idc                   t        j                  |      }t        t        |j                  |j                  |j
                  dddt        j                        j                         dz        }t        t        |j                  |j                  |j
                  dddt        j                        j                         dz        }g }d}		 | d|dz
  | d	|dz   d
dd|	i}
|rd|
d<   	 t        j                  t         d| d| j                  |
d      }|j                   dk7  r1t        j#                  d|j                   |||j$                  dd        nN|j'                         }|j                  dg       }|j)                  |       |j                  dd      s|sn|	dz  }	|D cg c]*  }t*        j-                  |j                  dd            s)|, }}t        j/                  dt1        |      |||t1        |             | j3                  |       |D cg c]  }| j5                  |       c}S # t        $ r t        j                  d|	       Y w xY wc c}w c c}w )u  Return normalized quote-task dicts queried from a whole team/workspace.

        Uses GET /team/{team_id}/task — matches the PoC (outlook_auto) approach
        when no specific list_id is configured. Filters to tasks whose names
        contain a recognizable quote number pattern to exclude unrelated workspace tasks.

        target_date : 'YYYY-MM-DD'
        date_field  : 'due_date' | 'date_updated' | 'date_created'
        r   rX   r#   rZ   r[   Tr\   r]   r^   r   r_   r`   rT   z/team/ra   rb   rc   z7ClickUp get_quote_tasks_by_team request error (page %d)rg   z9ClickUp get_tasks_by_team failed (%s) team=%s date=%s: %sNrh   ri   rj   r   r   zUClickUp: %d task(s) found for team=%s date=%s field=%s (%d total before quote filter))r   rk   r&   rl   rm   rn   r   r'   ro   rp   r   rq   rN   rr   rs   rt   ru   rv   rw   rx   r   r   r   ry   rz   r{   r|   )rP   r   rV   rS   rT   r}   r~   r   r   r`   re   r   r   r   r   quote_taskss                   r   get_quote_tasks_by_teamz%ClickUpClient.get_quote_tasks_by_teamw  sI   " "";/QVVQWWaeeQ1X\\JTTVY]]
 QVVQWWaeeRRMWWY\``
 !#	,c"HqL,c"FQJF	)F +1'(	LL$oVG9E: ..!	 }}#		OMMFF4CL 668DHHWb)EU#xxT*%AIDI P #,SQy/?/?fb@Q/RqSSc	N	
 	,-89  #99E  WY]^. T :s$   )-H5 (*III5IIfilenamecontentcontent_typec                 0   	 t        j                  t         d| d| j                  d|||fid      }|j                  dvr0t
        j                  d	|j                  ||j                  d
d        yy# t        $ r t
        j                  d|       Y yw xY w)z9Attach a file to a ClickUp task. Returns True on success.r   z/attachment
attachment<   )rd   filesrf   z!ClickUp attach_file error task=%sFrg      z+ClickUp attach_file failed (%s) task=%s: %sNrh   T)
rp   postrq   rO   rr   rs   rt   ru   rv   rw   )rP   r   r   r   r   r   s         r   attach_filezClickUpClient.attach_file  s    		 /y<**#h%FG	A ==
*II=t	   	MM=wG	s   2A3 3BBc                 0   	 t        j                  t         d| | j                  d      }|j                  dk(  r4|j                         j                  dg       }t        fd|D              S 	 y# t        $ r t        j                  d|       Y yw xY w)	zKReturn True if an attachment with this filename already exists on the task.r   rb   )rd   rf   rg   attachmentsc              3   r   K   | ].  }|j                  d       k(  xs |j                  d      k(   0 yw)title	file_nameN)r   ).0r   r   s     r   	<genexpr>z2ClickUpClient.attachment_exists.<locals>.<genexpr>  s<       EE'Nh.P!%%2D2PPs   47z'ClickUp attachment_exists error task=%sF)
rp   r   rq   rN   ru   rx   anyrr   rs   rt   )rP   r   r   r   r   s     `  r   attachment_existszClickUpClient.attachment_exists  s    	N /y1**A
 }}#ffhll="= (   $   	NMMCWM	Ns   A-A3 3BBfield_idr0   c           	      2   	 t        j                  t         d| d| | j                  d|id      }|j                  dvr1t
        j                  d	|j                  |||j                  d
d        yy# t        $ r t
        j                  d||       Y yw xY w)z<Set a custom field value on a task. Returns True on success.r   z/field/r0   rb   )rd   rx   rf   z/ClickUp set_custom_field error task=%s field=%sFr   z9ClickUp set_custom_field failed (%s) task=%s field=%s: %sNrh   T)
rp   r   rq   rN   rr   rs   rt   ru   rv   rw   )rP   r   r   r0   r   s        r   set_custom_fieldzClickUpClient.set_custom_field  s    	 /yzB**u%	A ==
*IIKt   	MMA7H 		s   1A3 3 BB)zapplication/pdf)__name__
__module____qualname____doc__r   rQ   boolr>   r=   r   r|   r   r   r   r   r{   r   r&   r   r   r   bytesr   r   objectr    r   r   rI   rI   o   s   C8 8& )#F8F8 F8
 F8 F8 
dF8P5
 5
 5
nd39o # # T#t)_   S$Y C 	T 	d4j 	DJ 4 >@4 @#c( @3 @SW @,s td{ > )#N:N: N:
 N: N: 
dN:r .  	
  
:   , s 6 d r   rI   )r   loggingr
   r   r   rp   	getLoggerrs   rq   compiler   r   r   r=   r>   r   r   r-   rG   rI   r   r   r   <module>r      s     	 ' g./0 BJJ:;	>c >c >P P$s) P c >cDj $ 8g gr   