Ë
    ÿ�j™"  ã                   ó  — d Z ddlZddlZddlmZmZmZmZ ddlm	Z	 ddl
mZmZ ddlmZ ddlmZ ddlmZmZmZmZmZmZ dd	lmZ dd
lmZmZ  ej:                  e«      Z eddg¬«      Z de!de"de#fd„Z$e jK                  d«       ed«      fde"fd„«       Z&e jK                  d«       ed«      fde"fd„«       Z'e jQ                  d«       ed«      fde!fd„«       Z)e jU                  d«       ed«      fde!de!de!fd„«       Z+y)z-Connect and manage product ingestion sources.é    N)Ú	APIRouterÚBodyÚHTTPExceptionÚQuery)Ú	start_job)Úget_integrationÚset_integration_status)Úsettings©Úbuild_catalog)Ú
KIND_CRAWLÚKIND_HTTP_APIÚKIND_SHOPIFYÚget_sourcesÚmark_source_statusÚupsert_source)Újobs)ÚUnsafeUrlErrorÚassert_safe_urlz/sourcesÚsources)ÚprefixÚtagsÚkindÚconfigÚreturnc                 ó`   — g }| t         k(  r"|j                  d«      s|j                  d«       |S )aO  What must still be supplied before this source's products price correctly.

    A source with no currency still ingests -- its products are simply flagged
    incomplete and excluded from serving, which from the API looks identical
    to an empty catalogue. This is the surfaced prompt that replaces guessing:
    the merchant can see exactly what's missing instead of silently getting
    nothing recommended.

    KIND_CRAWL is excluded here even though its config can also lack a
    currency: fetch_products() resolves it at crawl time via an LLM
    inference over page context (see crawl.py's _infer_currency) when no
    page's own markup states one, so there is nothing for the merchant to
    supply. KIND_HTTP_API has no such fallback -- an API's JSON has no
    equivalent of schema.org markup to infer from, so it still needs it.
    Úcurrency)r   ÚgetÚappend)r   r   Úneedss      ú./var/www/html/strategist-ai/app/api/sources.pyÚ_needsr"      s-   € ð  €EØŒ}Ò V§Z¡Z°
Ô%;Ø�‰�ZÔ Ø€Ló    z/http.Úpayloadc           	   ƒ   óš  K  — | j                  d«      }| j                  d«      }|r|st        dd¬«      ‚	 t        |«       t        j                  |«      j                  }|| j                  d«      xs d	| j                  d
«      xs ddddddœ| j                  d«      xs i | j                  d«      | j                  d«      | j                  d«      xs ddidœ}| j                  d«      rd| d   ini }t        |t        |||«       t        |ddt        t        |«      dœS # t        $ r}t        dt	        |«      ¬«      ‚d}~ww xY w­w)a  Connect a custom product API.

    Only a base URL and credentials are required. The field map and
    url_template are inferred by the pipeline on first sync -- a merchant
    should not have to describe their own API's shape. Currency is not
    inferred or assumed from anything else the tenant has connected (a wrong
    guess there silently mis-prices a whole catalog); it is either supplied
    here or reported back via "needs" so the merchant knows to set it. This
    endpoint never refuses a connection for any of their absence.
    Ú	tenant_idÚbase_urlé¦  z#tenant_id and base_url are required©Ústatus_codeÚdetailé�  NÚrecords_pathz
$.productsÚ
paginationÚoffsetÚlimitÚskipé   z$.total)ÚstyleÚlimit_paramÚoffset_paramÚ	page_sizeÚ
total_pathÚfieldsÚurl_templater   Úauthr3   Únone)r'   r-   r.   r8   r9   r   r:   Úapi_keyÚactiveÚpending_first_sync)r   Úexternal_refÚstatusÚmappingr    )r   r   r   r   ÚstrÚhttpxÚURLÚhostr   r   r"   )r$   r&   r'   Úexr?   r   Úcredentialss          r!   Úcreate_http_sourcerH   -   sP  è ø€ ð —‘˜KÓ(€IØ�{‰{˜:Ó&€HÙ™HÜ¨Ø#HôJð 	Jð=Ü˜Ô!ô —9‘9˜XÓ&×+Ñ+€LàØŸ™ NÓ3ÒC°|Ø—k‘k ,Ó/ò NØ¨gØ"°À9ñ4Nð
 —+‘+˜hÓ'Ò-¨2ØŸ™ NÓ3Ø—K‘K 
Ó+Ø—‘˜FÓ#Ò8¨°Ð'8ñ€Fð 6=·[±[ÀÔ5K�9˜g iÑ0Ñ1ÐQS€Kä�)œ]¨L¸&À+ÔNÜ!°<ØÐ+?ÜœM¨6Ó2ñ4ð 4øô) ò =Ü¨´C¸³GÔ<Ð<ûð=üs)   ‚4E·D$ ÁC"EÄ$	EÄ-EÅEÅEz/websitec              ƒ   ón  K  — | j                  d«      }| j                  d«      xs | j                  d«      }|r|st        dd¬«      ‚	 t        |«       t        j                  |«      j                  }|| j                  d	«      | j                  d
«      xs t        j                  | j                  d«      dœ}t        |t        ||i «       t        |ddt        t        |«      dœ}| j                  dd«      r$dd„}	 t        |d|«      ƒ d{  –—† |d<   d|d<   |S |S # t        $ r}t        dt	        |«      ¬«      ‚d}~ww xY w7 Œ7# t        j                  $ r}|j                   |d<   d|d<   Y d}~|S d}~wt        $ r1}t"        j%                  d||j&                  «       d|d<   Y d}~|S d}~ww xY w­w)a[  Connect the merchant's own website, read by crawling it.

    The third way in, for a merchant with neither a spreadsheet nor an API: they
    give us a page on their site and we read the products off it. Prices come
    from the page's schema.org markup where it has any -- data the merchant
    already publishes for search engines, so it is exact rather than inferred.
    A page without it still yields a product, flagged as having no price.

    currency does not need to be supplied here: if no page states one in its
    own markup, fetch_products() infers it from page context (domain, locale,
    shipping/tax text) with a dedicated LLM call at crawl time, rather than
    asking the caller for it up front.

    Saving the connection also starts a build (sync -> enrich -> pair) for the
    whole tenant, not just this source -- pairing has to see every product to
    find cross-product relationships, so there is no such thing as pairing
    scoped to one newly added source. Every OTHER already-connected source
    still gets its normal 24h re-crawl throttle in that build; this one is
    always crawled fresh since it has never been read before. Set
    "auto_build": false to skip this and connect without triggering anything
    (e.g. a caller adding several sources in one batch that wants a single
    build at the end instead of one per source).
    r&   Úurlr'   r(   ztenant_id and url are requiredr)   r,   Nr   Ú	max_pagesÚmin_interval_hours)rJ   r   rK   rL   r=   Úpending_first_crawl)r   r?   r@   Úproductsr    Ú
auto_buildTc                 ó   — t        | |¬«      S )N)Úprogressr   )ÚtenantrQ   s     r!   Úrunz"create_website_source.<locals>.run‘   s   € Ü  °(Ô;Ð;r#   ÚbuildÚjob_idÚqueuedÚ
job_statusÚ#already_running_without_this_sourcez2Could not auto-start build after connecting %s: %sÚnot_started)N)r   r   r   r   rB   rC   rD   rE   r
   ÚCRAWL_MAX_PAGESr   r   r"   r   r   ÚJobAlreadyRunningrU   ÚloggerÚwarningr+   )	r$   r&   rJ   rF   r?   r   ÚresponserS   Úrunnings	            r!   Úcreate_website_sourcer`   Z   s¯  è ø€ ð2 —‘˜KÓ(€IØ
�+‰+�eÓ
Ò
7 §¡¨JÓ 7€CÙ™CÜ¨Ø#CôEð 	Eð=Ü˜Ôô —9‘9˜S“>×&Ñ&€LàØ—K‘K 
Ó+Ø—[‘[ Ó-ÒI´×1IÑ1Ið &Ÿk™kÐ*>Ó?ñ€Fô �)œZ¨°v¸rÔBä"°LØ"Ð0EÜ¤
¨FÓ3ñ5€Hð ‡{�{�< Ô&ó	<ð	3Ü'0°¸GÀSÓ'I×!IˆH�XÑØ%-ˆH�\Ñ"ð €Oˆ8€OøôQ ò =Ü¨´C¸³GÔ<Ð<ûð=úð4 "Jùä×%Ñ%ò 	Kð
 ")§¡ˆH�XÑØ%JˆH�\Õ"ð €Oûô ò 	3Ü�N‰NÐOØ'¨¯©ô4à%2ˆH�\Õ"à€Oûð	3üsy   ‚AF5Á
D ÁB(F5Ã>E ÄEÄE ÄF5Ä	EÄ(D>Ä>EÅF5ÅE ÅF2ÅE5Å/F5Å5F2Æ&F-Æ'F5Æ-F2Æ2F5Ú r&   c              ƒ   óÚ   K  — t        | «      D �cg c].  }|d   |d   |d   |d   |d   |d   t        |d   |d   «      dœ‘Œ0 }}t        | t        «      }|d|r|d   id
œS d	id
œS c c}w ­w)z6Every source for a tenant. Never includes credentials.r   r?   r   r@   Úconnected_atÚlast_synced_at)r   r?   r   r@   rc   rd   r    ÚshopifyN)r   Úplatform_integration)r   r"   r   r   )r&   Úsr   Úintegrations       r!   Úlist_sourcesri   ¦   s´   è ø€ ô ˜YÓ'öð
 ð	 �6‘¨A¨nÑ,=Ø�X‘;¨!¨H©+Ø˜>Ñ*¸aÐ@PÑ>QÜ˜˜6™ A h¡KÓ0ó	2ð€Gð ô " )¬\Ó:€KØà±K˜; xÑ0ð%KñLð LàEIð%KñLð Lùòùs   ‚A+�3A&Á(A+z/{kind}/{external_ref}r?   c              ƒ   óÜ   K  — t        |«      D �cg c]  }|d   | k(  r
|d   |k(  r|‘Œ }}|st        dd¬«      ‚t        | |d«       | t        k(  rt	        |t        d«       | |ddœS c c}w ­w)	Nr   r?   i”  zNo such connected sourcer)   ÚrevokedÚINACTIVE)r   r?   r@   )r   r   r   r   r	   )r   r?   r&   rg   Úmatchess        r!   Údelete_sourcern   ·   sˆ   è ø€ ä% iÓ0ö K�QØ�F‘)˜tÒ#¨¨.Ñ(9¸\Ò(Iò ð K€Gð KáÜ¨Ð4NÔOÐOä�t˜\¨9Ô5ØŒ|Òô
 	˜y¬,¸
ÔCØ¨,À)ÑLÐLùòKùs   ‚A,�A'©AA,),Ú__doc__ÚloggingrC   Úfastapir   r   r   r   Úapp.api.catalogr   Ú"app.services.integrations.platformr   r	   Úapp.core.configr
   Úapp.services.catalog.buildr   Úapp.services.catalog.sourcesr   r   r   r   r   r   Úapp.services.infrar   Úapp.services.infra.ssrfr   r   Ú	getLoggerÚ__name__r\   ÚrouterrB   ÚdictÚlistr"   ÚpostrH   r`   r   ri   Údeletern   © r#   r!   ú<module>r�      s3  ðÙ 3Û ã ß 9Ó 9å %ß VÝ $Ý 4÷÷ õ $ß Cà	ˆ×	Ñ	˜8Ó	$€á	˜*¨I¨;Ô	7€ð�ð ˜dð  tó ð, ‡�ˆWÓÙ-1°#«Yñ )4 dò )4ó ð)4ðX ‡�ˆZÓÙ04°S³	ñ H¨ò Hó ðHðV ‡�ˆBƒÙ(-¨c«
ñ L #ò Ló ðLð  ‡�Ð'Ó(ÙGLÈSÃzñ M˜cð M°ð MÀò Mó )ñMr#   