Ë
    ~›øi¢[  ã                  óì  — d Z ddlmZ ddlZddlmZ ddlmZ ddlm	Z	m
Z
 ddlZddlmZ ddlmZ  ee«      Zd2d	„Zd3d
„Ze G d„ d«      «       Zd4d„ZdddœZ	 d5ddœ	 	 	 	 	 	 	 d6d„Z	 d5dddœ	 	 	 	 	 	 	 	 	 d7d„Zd8d„Zd8d„Zd8d„Zd8d„Zd9d„Z	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d:d„Zd;d„Zd;d„Z d;d„Z!d<d„Z"d<d „Z#d!Z$d"Z%d#Z&d$Z'd%Z(d&Z)d'Z*d=d>d(„Z+d5d?d)„Z,d@dAd*„Z-dBdAd+„Z.dBdAd,„Z/dBdAd-„Z0dBdAd.„Z1dCd/„Z2dDd0„Z3dEd1„Z4y)Fu~  
services/peoplehub_api.py
==========================
Defensive HTTP client for the People Hub upstream APIs.

  POST {PEOPLEHUB_BASE_URL}/login/by-accesskey/{access_key}
  POST {PEOPLEHUB_BASE_URL}/roleplays/by-accesskey/{access_key}
  POST {PEOPLEHUB_BASE_URL}/codingassessment/by-accesskey/{access_key}
  POST {PEOPLEHUB_BASE_URL}/coding/submit
  POST {PEOPLEHUB_ASSESS_BASE_URL}/test/gsonTestResponse.jsp?accesskey=â€¦
  POST {PEOPLEHUB_ASSESS_BASE_URL}/test/updateAssessmentScoreApi.jsp?â€¦

All endpoints are POST per upstream contract â€” calling them via GET
returns HTTP 405. The assess base URL is *derived* from PEOPLEHUB_BASE_URL
so deployments never hardcode hostnames.

Notes
â”€â”€â”€â”€â”€
â€¢ Response shapes are treated as opaque JSON. Extractors below read fields
  with safe fallbacks so the rest of the platform never KeyErrors.
â€¢ All calls include a configurable timeout + retry budget.
â€¢ The full response is preserved verbatim so the frontend can store it in
  localStorage exactly as the spec requires.
â€¢ No URL or credential is ever logged in cleartext at INFO level â€” only
  the path and host are logged for audit.
é    )ÚannotationsN)Ú	dataclass)ÚAny)ÚquoteÚurlparse)Úsettings)Ú
get_loggerc                 óü   — t         j                  xs dj                  d«      } | syd| v r| j                  dd«      S t	        | «      }|j
                  r(|j                  r|j
                  › d|j                  › d�S | S )aå  
    Derive the eReKruit-assess base URL from PEOPLEHUB_BASE_URL.

    The two upstream surfaces live on the same host but on different
    application contexts:
        api    base:  https://<host>/eReKruit/api
        assess base:  https://<host>/eReKruit-assess

    Operators only configure PEOPLEHUB_BASE_URL; we derive the assess
    base by replacing the /eReKruit/api context with /eReKruit-assess.
    Falls back to a same-host /eReKruit-assess for any non-standard base.
    Ú Ú/z/eReKruit/apiz/eReKruit-assessú://)r   ÚPEOPLEHUB_BASE_URLÚrstripÚreplacer   ÚschemeÚnetloc)ÚbaseÚparseds     úq/Users/priyanka/Documents/AI_Agent_Assessment/pepole_hub_candidate/people_hub_candidate/services/peoplehub_api.pyÚ_assess_base_urlr   ,   sv   € ô ×'Ñ'Ò-¨2×5Ñ5°cÓ:€DÙØØ˜$ÑØ�|‰|˜OÐ-?Ó@Ð@Ü�d‹^€FØ‡}‚}˜ŸšØ—-‘-�  F§M¡M ?Ð2BÐCÐCØ€Kó    c                ó†   — 	 t        | «      }|j                  › d|j                  › |j                  › �S # t        $ r Y yw xY w)z=Strip query string and credentials from a URL before logging.r   z<url>)r   r   r   ÚpathÚ	Exception)ÚurlÚps     r   Ú_safe_log_urlr   D   sC   € ðÜ�S‹MˆØ—(‘(�˜3˜qŸx™x˜j¨¯©¨Ð1Ð1øÜò Ùðús   ‚14 ´	A ¿A c                  óZ   — e Zd ZU ded<   ded<   ded<   ded<   d	Zd
ed<   dZded<   dd„Zy	)Ú	ApiResultÚboolÚokÚintÚstatus_coder   ÚdataÚstrÚrawNz
str | NoneÚerrorr   r   c                óv   — | j                   | j                  | j                  | j                  | j                  dœS )N©r!   r#   r$   r'   r   r)   )Úselfs    r   Úto_dictzApiResult.to_dictX   s1   € àŸ7™7Ø×+Ñ+ØŸ9™9ØŸ:™:ØŸ8™8ñ
ð 	
r   )ÚreturnÚdict)Ú__name__Ú
__module__Ú__qualname__Ú__annotations__r'   r   r+   © r   r   r   r   O   s/   … àƒHØÓØ
ƒIØ	ƒHØ€Eˆ:ÓØ€CˆƒMô
r   r   c                ó�   — t         j                  xs dj                  d«      }|› d| j                  d«      › dt	        |d¬«      › �S )Nr   r   ©Úsafe)r   r   r   Úlstripr   )r   Ú
access_keyr   s      r   Ú_urlr8   d   sF   € Ü×'Ñ'Ò-¨2×5Ñ5°cÓ:€DØˆV�1�T—[‘[ Ó%Ð& a¬¨j¸rÔ(BÐ'CÐDÐDr   zapplication/jsonzPeopleHub-Candidate-Portal/1.0)ÚAcceptz
User-AgentF©Úbarec          	     óÈ  — t        j                  t        j                  d¬«      }	 t        j                  |t        j
                  dt        ¬«      5 }|r|j                  | «      }n|j                  | |xs i ¬«      }ddd«       j                  }	 |j                  «       }|j                  rt        d|j                  ||d| «      S t        d|j                  ||d|j                  › �| «      S # 1 sw Y   ŒsxY w# t         j                  $ r t        dddd	d
| «      cY S t         j                  $ r t        dddd	d| «      cY S w xY w# t        j                  $ r |}Y Œ½w xY w)uñ  Single POST attempt â€” wrapped by `_post_with_retry`.

    `bare=True` sends a body-less POST (no Content-Type, zero-length body).
    The People Hub assess JSPs (`updateAssessmentScoreApi.jsp` and
    `gsonTestResponse.jsp`) accept this form â€” and only this form â€” and
    reject `application/json` requests with a 500 because their internal
    parser tries to read form parameters. Postman's default "POST with
    no body" mode also produces a bare POST, which is why it works there.
    ç      $@©ÚconnectT©ÚtimeoutÚverifyÚfollow_redirectsÚheaders)ÚjsonNFr   r   úupstream timeoutúupstream network errorúupstream returned HTTP ©ÚhttpxÚTimeoutr   ÚPEOPLEHUB_TIMEOUTÚClientÚPEOPLEHUB_VERIFY_TLSÚ_DEFAULT_HEADERSÚpostÚTimeoutExceptionr   ÚRequestErrorÚtextrE   ÚJSONDecodeErrorÚ
is_successr#   )r   Úbodyr;   rA   ÚclientÚrespr&   r   s           r   Ú_postrY   o   sZ  € ô �m‰mœH×6Ñ6ÀÔE€GðLÜ�\‰\ØÜ×0Ñ0Ø!Ü$õ	
ð
 Ùð —{‘{ 3Ó'‘à—{‘{ 3¨dªj°b�{Ó:�÷
ð$ �)‰)€CðØ—‘“ˆð ‡‚Ü˜˜t×/Ñ/°¸¸dÀCÓHÐHô Øˆt×Ñ ¨Ø
! $×"2Ñ"2Ð!3Ð4°cóð ÷=
ð 
ûô ×!Ñ!ò FÜ˜  4¨Ð-?ÀÓEÒEÜ×Ñò Lä˜  4¨Ð-EÀsÓKÒKðLûô ×Ñò ØŠðúsA   §+C< Á,C0Á>C< ÂE	 Ã0C9Ã5C< Ã<#EÄ!"EÅEÅ	E!Å E!)Úmax_retriesr;   c               óÌ  — |€t         j                  }t        dt        |«      dz   «      }d}t	        d|dz   «      D ]¥  }t        | ||¬«      }|}|j                  r|c S |j                  dk(  xs |j                  dk\  }|r||k\  r0t        j                  dt        | «      |j                  ||«       |c S t        j                  dt        | «      |j                  ||«       Œ§ |S )uG  Retry transient failures (timeouts, 5xx) up to N times.

    `bare=True` sends a body-less POST every attempt (used for the assess
    JSP endpoints â€” see `_post.bare` for why).

    `max_retries` defaults to `settings.PEOPLEHUB_RETRIES`. Pass 0 to
    disable retries. Connect failures and 5xx are treated as transient.
    Né   r:   r   éô  õ%   [PeopleHub] %s â†’ %s (attempt %d/%d)õ2   [PeopleHub] %s â†’ %s (attempt %d/%d) â€” retrying)r   ÚPEOPLEHUB_RETRIESÚmaxr"   ÚrangerY   r!   r#   ÚlogÚwarningr   r'   Úinfo)	r   rV   rZ   r;   ÚattemptsÚlastÚiÚresultÚ	transients	            r   Ú_post_with_retryrk   ¥   sÝ   € ð ÐÜ×0Ñ0ˆÜ�1”c˜+Ó&¨Ñ*Ó+€HØ!€Dä�1�h ‘lÖ#ˆÜ�s˜D tÔ,ˆØˆà�9Š9ØŠMà×'Ñ'¨1Ñ,ÒL°&×2DÑ2DÈÑ2Kˆ	Ù˜A šMÜ�K‰KØ7Ü˜cÓ" F§L¡L°!°Xôð ŠMä�‰Ø@Ü˜#Ó §¡¨a°õ	
ð $ð( €Kr   c                ón   — t        d| «      }t        j                  dt        |«      «       t	        |«      S )z%POST /login/by-accesskey/{access_key}zlogin/by-accesskeyu   [PeopleHub] login â†’ %s©r8   rc   re   r   rk   ©r7   r   s     r   Úloginro   Ò   s.   € ä
Ð# ZÓ
0€CÜ‡H�HÐ'¬°sÓ);Ô<Ü˜CÓ Ð r   c                ón   — t        d| «      }t        j                  dt        |«      «       t	        |«      S )z)POST /roleplays/by-accesskey/{access_key}zroleplays/by-accesskeyu   [PeopleHub] roleplays â†’ %srm   rn   s     r   Ú	roleplaysrq   Ù   s.   € ä
Ð'¨Ó
4€CÜ‡H�HÐ+¬]¸3Ó-?Ô@Ü˜CÓ Ð r   c                ón   — t        d| «      }t        j                  dt        |«      «       t	        |«      S )z0POST /codingassessment/by-accesskey/{access_key}zcodingassessment/by-accesskeyu   [PeopleHub] coding â†’ %srm   rn   s     r   Úcoding_assessmentrs   à   s.   € ä
Ð.°
Ó
;€CÜ‡H�HÐ(¬-¸Ó*<Ô=Ü˜CÓ Ð r   c                ó¶   — t        «       }|st        dddddd«      S |› dt        | d¬«      › �}t        j	                  dt        |«      «       t        |d	¬
«      S )uf  
    POST {assess_base}/test/gsonTestResponse.jsp?accesskey={access_key}

    Sent as a *bare* POST (no body, no Content-Type) â€” the JSP rejects
    application/json with a 500 because its internal parser tries to read
    form parameters from the body. GET also returns 405. Postman's default
    "POST with no body" call matches our request exactly.
    Fr   Nr   ú$PEOPLEHUB_BASE_URL is not configuredz%/test/gsonTestResponse.jsp?accesskey=r4   u!   [PeopleHub] get_assessment â†’ %sTr:   )r   r   r   rc   re   r   rk   )r7   r   r   s      r   Úget_assessmentrv   ç   sj   € ô Ó€DÙÜ˜  4¨Ð-SÐUWÓXÐXàˆ&ð Ü˜J¨RÔ0Ð1ð	3ð ô ‡H�HÐ0´-ÀÓ2DÔEÜ˜C dÔ+Ð+r   c           	     óö   — t        «       }|st        dddddd«      S |› dt        t        | «      d¬«      › dt        t        |«      d¬«      › �}t        j                  d	t        |«      «       t        |d
¬«      S )u  POST {assess_base}/test/updateAssessmentScoreApi.jsp?accesskey=â€¦&testid=â€¦

    Sent as a *bare* POST (no body, no Content-Type). Sending JSON here
    causes the JSP to throw 500. With a bare POST it returns:
        {"status":"success","accesskey":"â€¦","testid":N,"attempt_id":â€¦,
         "totalScore":â€¦,"resultStatus":â€¦,"message":"Test submitted successfully"}

    This call is the *final* submit only â€” it must NOT be invoked once per
    question. Per-question saves go through `update_assessment_answer`.
    Fr   Nr   ru   z-/test/updateAssessmentScoreApi.jsp?accesskey=r4   z&testid=u*   [PeopleHub] submit_assessment_score â†’ %sTr:   )r   r   r   r%   rc   re   r   rk   )r7   Útestidr   r   s       r   Úsubmit_assessment_scorery   û   s�   € ô Ó€DÙÜ˜  4¨Ð-SÐUWÓXÐXàˆ&ð ÜœC 
›O°"Ô5Ð6Ø
”5œ˜V›¨2Ô.Ð/ð	1ð ô
 ‡H�HÐ9¼=ÈÓ;MÔNÜ˜C dÔ+Ð+r   c           	     ó4  — t        «       }|st        dddddd«      S |› d�}t        j                  dt	        |«      «       t        | «      t        |«      t        |«      t        |«      t        |«      |€dn
t        |«      t        |«      dœ}	t        ||	«      S )	uÛ  POST {assess_base}/test/updateAssessmentApi.jsp

    Per-question save called on every Next click. The upstream JSP expects
    its parameters as application/x-www-form-urlencoded â€” sending JSON
    causes a 500. We pass them as form fields via httpx's `data=` kwarg.

    Expected response (loose contract â€” extracted defensively by callers):
        {
          "status": "success",
          "message": "",
          "answerUpdated": true,
          "timerUpdated": false,
          "isFinalSectionSubmit": false,
          "data": {
            "counter": "2865",
            "sectionid": "1",
            "questionid": "1551",
            "case_questionid": "0",
            "question_flag": "0"
          }
        }
    Fr   Nr   ru   z/test/updateAssessmentApi.jspu+   [PeopleHub] update_assessment_answer â†’ %s)Ú	accesskeyÚ	sectionidÚ
questionidÚquestion_flagÚcounterÚAnswerrx   )r   r   rc   re   r   r%   Ú_post_form_with_retry)
r7   r|   r}   r~   r   Úanswerrx   r   r   Úforms
             r   Úupdate_assessment_answerr„     s˜   € ô@ Ó€DÙÜ˜  4¨Ð-SÐUWÓXÐXàˆFÐ/Ð
0€CÜ‡H�HÐ:¼MÈ#Ó<NÔOô ˜Z›Ü˜Y›Ü˜Z›Ü˜]Ó+Ü˜W›Ø%˜~™´3°v³;Ü˜V›ñ€Dô !  dÓ+Ð+r   c                ó*  — t         j                  xs dj                  d«      }|st        dddddd«      S |› d�}t        j                  dt        |«      «       t        |xs i «      }t        | «      |d	<   |j                  d
d«       t        ||«      S )uÝ  POST {PEOPLEHUB_BASE_URL}/coding/submit

    Submits a single coding answer to the upstream People Hub service.

    Body shape (per upstream contract â€” matches the Postman sample):
        {
          "kind":        "coding_submit",
          "access_key":  "<KEY>",
          "received_at": "<ISO-8601>",
          "request": {
            "access_key":    "<KEY>",
            "candidate_id":  "<id>",
            "question_id":   <int>,
            "question_text": "...",
            "language":      "python|java|...",
            "code":          "<source>",
            "submitted_at":  "<ISO-8601>"
          }
        }

    Response body (loose):
        {
          "status":  true,
          "message": "Coding submission saved successfully",
          "data": {
            "id": <int>, "kind": "coding_submit", "accessKey": "...",
            "receivedAt": "...", "candidateId": "...", "questionId": <int>,
            "questionText": "...", ...
          }
        }
    r   r   Fr   Nru   z/coding/submitu    [PeopleHub] submit_coding â†’ %sr7   ÚkindÚcoding_submit©r   r   r   r   rc   re   r   r-   r%   Ú
setdefaultrk   ©r7   Úpayloadr   r   rV   s        r   Úsubmit_codingrŒ   E  s“   € ô@ ×'Ñ'Ò-¨2×5Ñ5°cÓ:€DÙÜ˜  4¨Ð-SÐUWÓXÐXØˆF�.Ð
!€CÜ‡H�HÐ/´¸sÓ1CÔDô
 �’˜2Ó€DÜ˜Z›€DˆÑØ‡O�O�F˜OÔ,Ü˜C Ó&Ð&r   c                ó*  — t         j                  xs dj                  d«      }|st        dddddd«      S |› d�}t        j                  dt        |«      «       t        |xs i «      }t        | «      |d	<   |j                  d
d«       t        ||«      S )a-  POST {PEOPLEHUB_BASE_URL}/roleplay/save

    Accepts a JSON body in the form documented by the upstream contract:
        {
          "kind": "roleplay",
          "access_key": "<KEY>",
          "received_at": "<ISO-8601>",
          "request": {
            "scenario": { "id": "...", "title": "..." },
            "session_id": "...",
            "duration_seconds": 0,
            "turns_completed": 0,
            "mode": "voice|video|text",
            "end_reason": "completed|time_expired|abandoned"
          }
        }

    Response body (loose):
        { "status": "success", "message": "...", "saved": true }

    The access_key is included in the body per the upstream sample, but
    we also pass it as a query string for backward compatibility with
    older builds of the upstream service.
    r   r   Fr   Nru   z/roleplay/saveu    [PeopleHub] save_roleplay â†’ %sr7   r†   Úroleplayrˆ   rŠ   s        r   Úsave_roleplayr�   t  s’   € ô2 ×'Ñ'Ò-¨2×5Ñ5°cÓ:€DÙÜ˜  4¨Ð-SÐUWÓXÐXØˆF�.Ð
!€CÜ‡H�HÐ/´¸sÓ1CÔDô �’˜2Ó€DÜ˜Z›€DˆÑØ‡O�O�F˜JÔ'Ü˜C Ó&Ð&r   c                ó*  — t         j                  xs dj                  d«      }|st        dddddd«      S |› d�}t        j                  dt        |«      «       t        |xs i «      }t        | «      |d	<   |j                  d
d«       t        ||«      S )aU  POST {PEOPLEHUB_BASE_URL}/roleplay/report

    Called once the role play report has been generated on the candidate
    side. The body mirrors the upstream contract:
        {
          "kind":        "roleplay",
          "access_key":  "<KEY>",
          "received_at": "<ISO-8601>",
          "request": {
            "scenario": { "id": "...", "title": "...", "description": "...",
                          "context": "...", "category": "...",
                          "cat_label": "...", "learner_role": "...",
                          "learner_emoji": "...", "ai_character": "...",
                          "ai_emoji": "...", "ai_personality": "...",
                          "difficulty": "...", "turns": 8 },
            "session_id": "...",
            "duration_seconds": 0,
            "turns_completed": 0,
            "mode": "voice|video|text",
            "end_reason": "completed|time_expired|abandoned",
            "report": { ... full generated report payload ... }
          }
        }

    Response body (loose):
        { "status": "success", "message": "...", "saved": true }
    r   r   Fr   Nru   z/roleplay/reportu'   [PeopleHub] save_roleplay_report â†’ %sr7   r†   rŽ   rˆ   rŠ   s        r   Úsave_roleplay_reportr‘   š  s“   € ô8 ×'Ñ'Ò-¨2×5Ñ5°cÓ:€DÙÜ˜  4¨Ð-SÐUWÓXÐXØˆFÐ"Ð
#€CÜ‡H�HÐ6¼ÀcÓ8JÔKô �’˜2Ó€DÜ˜Z›€DˆÑØ‡O�O�F˜JÔ'Ü˜C Ó&Ð&r   c           	     ó˜  — t        j                  t        j                  d¬«      }	 t        j                  |t        j
                  dt        ¬«      5 }|j                  | |¬«      }ddd«       j                  }	 |j                  «       }|j                  rt        d|j                  ||d| «      S t        d|j                  ||d|j                  › �| «      S # 1 sw Y   ŒsxY w# t         j                  $ r t        dddd	d
| «      cY S t         j                  $ r t        dddd	d| «      cY S w xY w# t        j                  $ r |}Y Œ½w xY w)z.Single attempt POST with form-urlencoded body.r=   r>   Tr@   )r$   NFr   r   rF   rG   rH   rI   )r   rƒ   rA   rW   rX   r&   r   s          r   Ú
_post_formr“   Ã  s?  € ä�m‰mœH×6Ñ6ÀÔE€GðLÜ�\‰\ØÜ×0Ñ0Ø!Ü$õ	
ð
 Ø—;‘;˜s¨�;Ó.ˆD÷
ð �)‰)€CðØ—‘“ˆð ‡‚Ü˜˜t×/Ñ/°¸¸dÀCÓHÐHÜØˆt×Ñ ¨Ø
! $×"2Ñ"2Ð!3Ð4°cóð ÷)
ð 
ûô ×!Ñ!ò FÜ˜  4¨Ð-?ÀÓEÒEÜ×Ñò LÜ˜  4¨Ð-EÀsÓKÒKðLûô ×Ñò ØŠðúsA   §+C$ ÁCÁ&C$ Á;D1 ÃC!ÃC$ Ã$#D.Ä	"D.Ä-D.Ä1E	ÅE	c                óÀ  — t        dt        t        j                  «      dz   «      }d }t	        d|dz   «      D ]£  }t        | |«      }|}|j                  r|c S |j                  dk(  xs |j                  dk\  }|r||k\  r0t        j                  dt        | «      |j                  ||«       |c S t        j                  dt        | «      |j                  ||«       Œ¥ |S )Nr\   r   r]   r^   r_   )ra   r"   r   r`   rb   r“   r!   r#   rc   rd   r   r'   re   )r   rƒ   rf   rg   rh   ri   rj   s          r   r�   r�   á  sÎ   € Ü�1”cœ(×4Ñ4Ó5¸Ñ9Ó:€HØ!€DÜ�1�h ‘lÖ#ˆÜ˜C Ó&ˆØˆØ�9Š9ØŠMØ×'Ñ'¨1Ñ,ÒL°&×2DÑ2DÈÑ2Kˆ	Ù˜A šMÜ�K‰KØ7Ü˜cÓ" F§L¡L°!°Xôð ŠMÜ�‰Ø@Ü˜#Ó §¡¨a°õ	
ð $ð  €Kr   )	Úcurrent_stageÚcurrentStageÚstageÚinterview_stageÚinterviewStageÚstage_noÚstageNoÚactive_stageÚactiveStage)	ÚnameÚcandidate_nameÚcandidateNameÚ	full_nameÚfullNameÚdisplay_nameÚdisplayNameÚ
first_nameÚ	firstName)ÚemailÚcandidate_emailÚcandidateEmailÚemail_idÚemailId)ÚidÚcandidate_idÚcandidateIdÚparticipant_idÚparticipantIdÚuser_idÚuserIdÚuuid)ÚphoneÚmobileÚphone_numberÚphoneNumberÚcontactÚcontact_numberÚcontactNumber)Úposition_appliedÚpositionAppliedÚpositionÚrole_appliedÚroleAppliedÚjob_roleÚjobRole)Ú	skill_setÚskillSetÚskillsÚ
skill_listÚ	skillListc                ó  — |dkD  s| €yt        | t        «      rH|D ]  }|| v sŒ| |   dvsŒ| |   c S  | j                  «       D ]  }t        |||dz   «      }|dvsŒ|c S  yt        | t        «      r| rt        | d   ||dz   «      S y)z;Recursively look for any of the given keys in nested dicts.é   N)Nr   r\   r   )Ú
isinstancer-   ÚvaluesÚ_walk_for_keysÚlist)r$   ÚkeysÚdepthÚkÚvÚfounds         r   rË   rË     s�   € àˆq‚y�D�LØÜ�$œÔÛˆAØ�DŠy˜T !™W¨JÒ6Ø˜A‘w’ð ð —‘–ˆAÜ" 1 d¨E°A©IÓ6ˆEØ˜JÒ&Ø’ð ð ô 
�Dœ$Ô	¡DÜ˜d 1™g t¨U°Q©YÓ7Ð7Ør   c                ó¼   — t        | t        «      }|€|S 	 t        |«      j                  «       }|s|S t	        |«      }|dk\  r|S |S # t
        t        f$ r |cY S w xY w)u‡  Pull current_stage from anywhere in the response.

    Returns the integer stage if the API explicitly provided one (>=1).
    Returns None when the value is missing, empty, zero, or non-numeric â€”
    the frontend then routes the candidate to the "no interview scheduled"
    page rather than fabricating a stage.

    Pass an explicit `fallback` if a caller actually wants a default.
    r\   )rË   Ú_STAGE_KEYSr%   Ústripr"   Ú
ValueErrorÚ	TypeError)r‹   Úfallbackr&   ÚsÚns        r   Úextract_current_stagerÚ     sj   € ô ˜¤+Ó
.€CØ
€{ØˆðÜ�‹H�N‰NÓˆÙØˆOÜ�‹FˆØ˜’FˆqÐ( Ð(øÜœ	Ð"ò ØŠðús   –A ³A ÁA ÁAÁAc                ó\   — t        | t        «      }|rt        |«      j                  «       S |S ©N)rË   Ú
_NAME_KEYSr%   rÔ   ©r‹   Údefaultr&   s      r   Úextract_candidate_namerà   5  s&   € Ü
˜¤*Ó
-€CÙ"Œ3ˆs‹8�>‰>ÓÐ/¨Ð/r   c                ó\   — t        | t        «      }|rt        |«      j                  «       S |S rÜ   )rË   Ú_EMAIL_KEYSr%   rÔ   rÞ   s      r   Úextract_candidate_emailrã   :  ó&   € Ü
˜¤+Ó
.€CÙ"Œ3ˆs‹8�>‰>ÓÐ/¨Ð/r   c                ó\   — t        | t        «      }|rt        |«      j                  «       S |S rÜ   )rË   Ú_ID_KEYSr%   rÔ   rÞ   s      r   Úextract_candidate_idrç   ?  s&   € Ü
˜¤(Ó
+€CÙ"Œ3ˆs‹8�>‰>ÓÐ/¨Ð/r   c                ó\   — t        | t        «      }|rt        |«      j                  «       S |S rÜ   )rË   Ú_PHONE_KEYSr%   rÔ   rÞ   s      r   Úextract_candidate_phonerê   D  rä   r   c                ó\   — t        | t        «      }|rt        |«      j                  «       S |S rÜ   )rË   Ú_POSITION_KEYSr%   rÔ   rÞ   s      r   Úextract_position_appliedrí   I  s&   € Ü
˜¤.Ó
1€CÙ"Œ3ˆs‹8�>‰>ÓÐ/¨Ð/r   c                ó0  — t        | t        «      }|€g S t        |t        «      r|D �cg c]  }t	        |«      ‘Œ c}S t        |t        «      r>|j                  d«      D �cg c]#  }|j                  «       sŒ|j                  «       ‘Œ% c}S g S c c}w c c}w )NÚ,)rË   Ú_SKILLS_KEYSrÉ   rÌ   r%   ÚsplitrÔ   )r‹   r&   rØ   s      r   Úextract_skill_setrò   N  s}   € Ü
˜¤,Ó
/€CØ
€{Øˆ	Ü�#”tÔÙ #Ó$¡˜1”�A• Ñ$Ð$Ü�#”sÔØ#&§9¡9¨S¤>Ó?¡>˜a°Q·W±WµY�—‘•	 >Ñ?Ð?Ø€Iùò %ùâ?s   ©BÁ!BÁ7Bc           	     ó”   — t        | «      t        | «      t        | «      t        | «      t	        | «      t        | «      t        | «      dœS )zÏSingle-stop dict the frontend can show on profile / toolbar.

    `current_stage` is None when the API has no stage assigned for this
    candidate; the frontend treats that as "no interview scheduled".
    )r¬   rž   r§   r´   r»   rÂ   r•   )rç   rà   rã   rê   rí   rò   rÚ   )r‹   s    r   Úextract_candidate_summaryrô   Y  sF   € ô 1°Ó9Ü2°7Ó;Ü3°GÓ<Ü3°GÓ<Ü4°WÓ=Ü-¨gÓ6Ü1°'Ó:ñð r   c           
     óX  — | €yt        | t        t        t        f«      rt	        | «      dk(  ryt        | t        «      rhdD ]c  }|| v sŒt        | |   t        t        t        f«      sŒ't	        | |   «      dk(  sŒ9| j                  «       D �cg c]  }||ddddfvsŒ|‘Œ }}|rŒc y yc c}w )	zATrue if payload is None / empty list / empty dict / empty string.Tr   )r$   Úitemsrq   ÚresultsrÌ   ÚstatusÚmessager!   ÚsuccessF)rÉ   rÌ   r-   r%   ÚlenrÍ   )r‹   rÏ   ÚkkÚothers       r   Úis_empty_responserþ   j  s¬   € à€ØÜ�'œD¤$¬Ð,Ô-´#°g³,À!Ò2CØä�'œ4Ô ÛBˆAØ�GŠ|¤
¨7°1©:¼¼dÄCÐ7HÕ IÌcÐRYÐZ[ÑR\ËoÐabÓNbà&-§l¡l¤nÓl¡n ¸À1ÀhÐPYÐ[_ÐajÐBkÒ8kš n�ÐlÚÙð Cð ùò ms   ÂB'ÂB')r,   r%   )r   r%   r,   r%   )r   r%   r7   r%   r,   r%   rÜ   )r   r%   rV   údict | Noner;   r    r,   r   )
r   r%   rV   rÿ   rZ   ú
int | Noner;   r    r,   r   )r7   r%   r,   r   )r7   r%   rx   r   r,   r   )r7   r%   r|   r   r}   r   r~   r   r   r   r‚   r%   rx   r   r,   r   )r7   r%   r‹   r-   r,   r   )r   r%   rƒ   r-   r,   r   )r   )r$   r   rÍ   ztuple[str, ...]rÎ   r"   r,   r   )r‹   r   r×   r   r,   r   )Ú	Candidate)r‹   r   rß   r%   r,   r%   )r   )r‹   r   r,   z	list[str])r‹   r   r,   r-   )r‹   r   r,   r    )5Ú__doc__Ú
__future__r   rE   Údataclassesr   Útypingr   Úurllib.parser   r   rJ   Úconfigr   Úutils.loggerr	   r.   rc   r   r   r   r8   rO   rY   rk   ro   rq   rs   rv   ry   r„   rŒ   r�   r‘   r“   r�   rÓ   rÝ   râ   ræ   ré   rì   rð   rË   rÚ   rà   rã   rç   rê   rí   rò   rô   rþ   r2   r   r   Ú<module>r	     sÑ  ðñõ4 #ã Ý !Ý ß (ã å Ý #á�Ó€ó
ó0ð ÷
ð 
ó ð
ó(Eð !Ø2ñÐ ð ð3ð ñ	3Ø	ð3à
ð3ð ð	3ð
 ó3ðp ð(ð #Øñ(Ø	ð(à
ð(ð ð	(ð
 ð(ð ó(óZ!ó!ó!ó,ó(,ð.0,àð0,ð ð0,ð ð	0,ð
 ð0,ð ð0,ð ð0,ð ð0,ð ó0,óf,'ó^#'óL&'óRó<ð4€ð€
ð T€ð,€ð2€ð8€àM€ôô"ô.0ô
0ô
0ô
0ô
0ó
óô"r   