ó
    £»j¾ ã                   óè  • S r SSKrSSKJr  SSKrSSKrSSKrSSKrSSKr	SSK
Jr  SSKJr  \R                  " S5      r\R                  " S5      r\R                  " S	\R"                  5      r\R                  " S
5      r\R                  " S5      r1 SkrS rS rSr\S-   r\R                  " S5      r\R                  " S5      rS rSLS jrS rS rS r S r!Sr"Sr#Sr$Sr%S r&\R                  " S\R"                  5      r'S r(S  r)S! r*S" r+/ r,S# r-S$ r.\R                  " S%5      r/S& r0S' r1SMS( jr2SNS) jr3S* r4\R                  " S+5      r5S, r6S- r7\R                  " S.5      r8\9" / S/QS05       V Vs0 s H  u  pURu                  5       U _M     snn r;S1 r<S2 r=S3 r>S4r?SOS5 jr@S6rAS7rBS8rCS9rDS:rES;rF " S< S=\G5      rHS> rIS? rJS@ rK\E4SA jrLSB rMSCrNSD rOSE rPSPSF jrQSG rRSHrSSI rTSLSJ jrUSOSK jrVgs  snn f )Qa  
Carwow reader.

Reads the logged in Carwow dealer stock list at dealers.carwow.co.uk and turns
each card into a Car. Carwow shows more on the card than Motorway: registration,
make, model and derivative, year, mileage, fuel, transmission, service history,
reserve, CAP value, condition grade and distance, plus the listing state.

Auction only. Steven never buys a buy it now listing, so any card whose state is
not an auction state is discarded here, on both platforms.

Nothing here bids. It reads the screen only.
é    N)Úfinance_linesé   )ÚCar)Úbrowserz\b[A-Z]{2}\d{2}\s?[A-Z]{3}\bz^(19[89]\d|20[0-4]\d)$z^(\d+)\s*miles? away$u   Â£\s*([\d,]+)z^\d{1,3}(?:,\d{3})+$|^\d{3,6}$>   ÚphevÚdieselÚhybridÚpetrolÚelectricúpetrol hybridúplug-in hybridc                 ó    • U c  g [        U 5      R                  SS5      R                  5       n U R                  5       (       a  [	        U 5      $ S $ )NÚ,Ú )ÚstrÚreplaceÚstripÚisdigitÚint)Úss    Ú8/Users/stevendouglas/BidBrain/bidbrain/readers/carwow.pyÚ_numr   "   s@   € Ø�yØÜˆA‹�‰�s˜BÓ×%Ñ%Ó'€AØ—Y‘Y—[‘[Œ3ˆq‹6Ð* dÐ*ó    c                 ó:   • SU =(       d    SR                  5       ;   $ )NÚauctionr   )Úlower)Ústates    r   Ú_is_auctionr   )   s   € ð ˜Ÿ "×+Ñ+Ó-Ñ-Ð-r   z:https://dealers.carwow.co.uk/dealers/listings/filtered/wonz?won_date=90&collection_date=0z Ended:\s*(\d{2})/(\d{2})/(\d{4})úSold forc                 óò  • / n[         R                  " SU 5      SS  GHÐ  nUR                  SS5      S   n[         R                  " SU5      n[         R                  " SSU5      R                  S5       Vs/ s H=  oUR	                  5       (       d  M  [
        R                  " U5      R	                  5       PM?     nnS	n/ nSn	Sn
S	nSn[        U5       GH‹  u  pÕ[        R                  U5      (       a~  US
-   [        U5      :  al  [         R                  " SXmS-      5      (       aK  XmS
-      R	                  5       R                  5       [        ;   a   [        XmS-      R                  SS	5      5      n[         R                  " SU5      nU(       a¹  [!        S U 5       5      (       a¢  US-   [        U5      :  a�  XmS-      nUSS R#                  5       (       aq  US;  ak  S	R%                  UR                  5       5      nU/nUS
-   [        U5      :  a7  [         R                  " SXmS
-      5      (       a  UR'                  XmS
-      5        US:X  aº  US-   [        U5      :  a¨  [(        R                  XmS-      5      nU(       a*  [        UR+                  S5      R                  SS	5      5      n	US
-   [        U5      :  aK  [(        R                  XmS
-      5      nU(       a*  [        UR+                  S5      R                  SS	5      5      n
[,        R                  U5      nU(       d  GMT  UR+                  S5       SUR+                  S
5       SUR+                  S5       3nGMŽ     U(       d  GMP  U	c	  US:X  d  GM\  UR'                  SU(       a  UR+                  S5      OUUSR%                  U5      UXšUUU(       a+  [.        R0                  S   S   SUR+                  S5       3-   OS	S.
5        GMÓ     U$ s  snf )zÓParse the won listings page into bought cars. Each card carries the reg,
name, Sold for price, the price including fees and VAT, CAP clean and the
sale end date. Voided cards are cancelled sales and are skipped.zdata-listing-state="é   NÚ"r   zhref="/dealers/listings/(\d+)"ú<[^>]+>Ú
r   r   z\d{1,3}(?:,\d{3})*r   z[A-Z0-9]{2,4}\s?[A-Z0-9]{1,4}c              3   ó@   #   • U  H  oR                  5       v •  M     g 7f©N)r   )Ú.0Úchs     r   Ú	<genexpr>Úparse_won.<locals>.<genexpr>e   s   é € Ð2²¨"Ÿ™Ÿ˜²ùs   ‚)ÚCopiedz\d\.\dr   é   Ú-ÚvoidedÚCarwowÚ ÚcarwowÚbase_urlú/dealers/listings/)
ÚplatformÚvehicle_refÚregÚnameÚstatusÚpriceÚtotal_priceÚbought_dateÚmileageÚlisting_url)ÚreÚsplitÚsearchÚsubr   ÚhtmllibÚunescapeÚ	enumerateÚYEARÚmatchÚlenÚ	fullmatchr   ÚFUELSr   r   ÚanyÚisupperÚjoinÚappendÚMONEYÚgroupÚ	_ENDED_REr   ÚSITES)Ú	page_htmlÚoutÚblockr   ÚlidÚlÚlinesr6   Ú
name_partsÚsold_forÚtotalÚendedr<   ÚjÚmÚ	cand_nameÚm2s                    r   Ú	parse_wonr`   :   sg  € ð €CÜ—’Ð1°9Ó=¸a¸bÔAˆØ—‘˜C Ó# AÑ&ˆô
 �iŠiÐ9¸5ÓAˆô Ÿ&š& ¨T°5Ó9×?Ñ?ÀÔEóTÚE�QÏÉÏó -”×!Ò! !Ó$×*Ñ*Ö,ÙEð 	ð TàˆØˆ
ØˆØˆØˆØˆÜ˜e×$‰DˆAô �z‰z˜!�}‰}  Q¡¬¨U«Ó!3¼¿ºÐEZÐ\aÐfgÑbgÑ\h×8iÑ8iØ !™e™×*Ñ*Ó,×2Ñ2Ó4¼Ó=Ü˜e¨¡E™l×2Ñ2°3¸Ó;Ó<�ô —’Ð=¸qÓAˆAÞ”SÑ2±Ó2×2Ñ2°q¸1±u¼sÀ5»zÓ7IØ! a¡%™L�	Ø˜R˜a�=×(Ñ(×*Ñ*¨yÀÓ/KØŸ'™' !§'¡'£)Ó,�CØ"+ �Jð ˜1‘uœs 5›zÓ)¬b¯hªh°yÀ%ÈAÉÁ,×.OÑ.OØ"×)Ñ)¨%°A±©,Ô7Ø�J‹ 1 q¡5¬3¨u«:Ó#5Ü—L‘L ¨1¡u¡Ó.�ÞÜ" 1§7¡7¨1£:×#5Ñ#5°c¸2Ó#>Ó?�HØ�q‘5œ3˜u›:Ó%ÜŸ™ e°©E¡lÓ3�BÞÜ # B§H¡H¨Q£K×$7Ñ$7¸¸RÓ$@Ó A˜Ü× Ñ  Ó#ˆAß‰qØŸ7™7 1›:˜, a¨¯©°«
 |°1°Q·W±W¸Q³Z°LÐA“ñG %÷L ‰3�HÑ(¨E°XÖ,=Ø�J‰J HÎS¸S¿Y¹YÀq¼\ÐVYØ"¨C¯H©H°ZÓ,@ÈEØ!)ÐPUØ#*æPSô )0¯©°hÑ(?À
Ñ(KØ*<¸S¿Y¹YÀq»\¸NÐ(Kò)LØY[ñ]÷ ^ñ{ BðF €JùòkTs   Á6O4Â(O4c                 óÀ  • [         R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       nUR	                  [
        SSS9   UR                  SSS	9  UR                  S5        UR                  SS5      nU H  n UR                  SU 3S5        M     SnSnXv:  a?  UR                  SS5      nU[        U5      :¼  a  OUR                  S5        US-  nXv:  a  M?  [        UR                  5       5      UR                  5         $ ! [         a    [        S
5      ef = f! [         a     M´  f = f! UR                  5         f = f)a
  Read the won listings screen: the cars Steven has bought on Carwow,
over the widest 90 day window the page itself offers. Fails loudly if
the screen does not render. Read only, never acts.

Each won card is its own <turbo-frame>, lazy loaded: only the ones
inside the initial viewport actually fetch their real content when the
page first loads, every other one sits as an empty shell until
scrolled into view (found live 2026-08-24, Mark: "im only showing 3
records for cars won in carwow", the real total was 32 that day, only
3 of which had ever rendered). Scrolling each frame's own element into
view triggers Turbo's own client side fetch, the exact same real
rendering process a human browsing the page would cause, rather than
reimplementing turbo-frame fetching by hand.r1   ©Úheadlessr   ÚdomcontentloadedéÈ¯  ©Ú
wait_untilÚtimeoutz[data-listing-state]i0u  ©rh   z‡Carwow won listings screen did not render. The page may have changed or the login may have expired. Run: python3 login.py carwow manualéè  z turbo-frame[id^='listing_card_']zels => els.map(e => e.id)Ú#ze => e.scrollIntoView()é N  zFels => els.filter(e => e.querySelector('[data-listing-state]')).lengthéô  )r   Úopen_reader_contextÚpagesÚnew_pageÚgotoÚWON_URL_WIDEÚwait_for_selectorÚ	ExceptionÚRuntimeErrorÚwait_for_timeoutÚeval_on_selector_allÚeval_on_selectorrG   r`   ÚcontentÚclose)	Ú
playwrightrc   ÚctxÚpageÚ	frame_idsÚfidÚdeadlineÚwaitedÚloadeds	            r   Úread_wonrƒ   …   sk  € ô ×
%Ò
% j°(ÀXÑ
N€Cð"Ø"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆØ�	‰	”,Ð+=Àuˆ	ÑMð	^Ø×"Ñ"Ð#9À5Ð"ÑIð
 	×Ñ˜dÔ#Ø×-Ñ-Ø.Ð0KóMˆ	ãˆCðØ×%Ñ%¨¨#¨ iÐ1JÖKñ ð ˆØˆØÓØ×.Ñ.Ø2ØXóZˆFð œ˜Y›Ó'ØØ×!Ñ! #Ô&Ø�c‰MˆFð Õô ˜Ÿ™›Ó(à�	‰	�øô; ó 	^Üð]ó^ð ^ð	^ûô ó Úðûð& 	�	‰	�úsO   ˜AE ÁD! Á.(E ÂD:Â,A
E Ã8E Ä!D7Ä7E Ä:
EÅE ÅEÅE ÅEc                 óŒ   • SR                  U 5      nSU;   d  SU;   a  gSU;   a  SU;   d  SU;   a  gSU;   a  SU;   d  S	U;   a  g
g)Nr0   zno shzno service historyÚnoneÚfullÚshÚfshÚpartÚserviceÚpartialr   )rL   )Úlines_lowerÚtexts     r   Ú_service_historyrŽ   ¹   sR   € Ø�8‰8�KÓ €DØ�$ƒÐ.°$Ó6ØØ�ƒ~˜$ $›,¨%°4«-ØØ�ƒ~˜4 4›<¨9¸Ó+<ØØr   c                 ó¤   • [         R                  " SU =(       d    SR                  5       5      nU(       a  [        UR	                  S5      5      $ S$ )aì  The previous owners count out of the line Carwow prints under "Former
keepers", or None when the line does not start with one.

It used to be a bare "2". On about 2026-09-15 Carwow put a link beside
it for its new Ownership history panel, so the line reads "2 view
details", and the plain isdigit() check this replaced left owners unset
on every Carwow car. A missing owners count fails the hard gate, so the
whole platform disappeared from the shortlist for four days with nothing
on the page saying why. Only a count at the very front of the line is
taken, so a line that changes shape again leaves owners unset and the
car is held rather than priced on a guess (golden rule 4). Pure string
work, no browser needed, so it is unit tested directly.z(\d+)(?!\S)r   r!   N)r>   rF   r   r   rO   )Úliner]   s     r   Úformer_keepersr‘   Ä   s:   € ô 	�Š� $§*¨"×!3Ñ!3Ó!5Ó6€AÞŒ3ˆq�w‰w�q‹z‹?Ð) TÐ)r   c                 óŽ   • [         R                  " SU 5      nU Vs/ s H  o"R                  S5      (       d  M  UPM     sn$ s  snf )Nz(?=data-listing-id=")zdata-listing-id=)r>   r?   Ú
startswith)rR   ÚblocksÚbs      r   Ú_card_blocksr–   Õ   s5   € Ü�XŠXÐ.°	Ó:€FÙÓB’v�!§¡Ð.@×!A�A‘vÑBÐBùÒBs
   œA¹Ac                 ó`  • / n[        U 5       GHþ  n[        R                  " SU5      nU(       a  UR                  S5      OSn[        R                  " SU5      nU(       a  UR                  S5      OSn[        U5      (       d  Mx  Sn[        R                  " SU5      nU(       ak   [        R                  " [        R                  " UR                  S5      5      5      nU(       a*  [        U[        5      (       a  US   R                  SS5      n[        R                  " SS	U[        R                  S
9n	[        R                  " SS	U	[        R                  S
9n	[        R                  " SS[        R                  " U	5      5      R!                  S5       V
s/ s H)  oªR#                  5       (       d  M  U
R#                  5       PM+     nn
U V
s/ s H  oªR%                  5       PM     nn
SnU HB  n
[&        R	                  U
5      nU(       d  M!  UR                  S5      R)                  S	S5      n  O   S nU H;  n
[*        R                  U
5      nU(       d  M!  [-        UR                  S5      5      n  O   S n[/        U5       HA  u  nn
U
R%                  5       S:X  d  M  US-   [1        U5      :  d  M0  [-        UUS-      5      n  O   S n[/        U5       H€  u  nn
SU
R%                  5       ;   d  M  [3        U[5        US-   [1        U5      5      5       H>  n[6        R	                  UU   5      nU(       d  M$  [-        UR                  S5      5      n  O     O   S n[/        U5       H“  u  nn
U
R%                  5       nSU;   d  M  SU;   a  S nOl[6        R	                  U
5      =(       d/    US-   [1        U5      :  a  [6        R	                  UUS-      5      OS nU(       a  [-        UR                  S5      5      n  O   SnU H  n
U
R%                  5       [8        ;   d  M  U
n  O   SnU HA  n
U
R#                  5       R%                  5       nUS;   d  M)  US:X  a  SOU
R#                  5       n  O   S nS n[/        U5       H/  u  nn
[:        R                  U
5      (       d  M"  [-        U
5      nUn  O   S=n=nnUbF  US:¼  a@  UUS-
     nUUS-
     nUR!                  5       nU(       a  US   nS	R=                  USS  5      nS n [/        U5       HY  u  nn
[>        R                  U
5      (       d  M"  [-        U
5      U:w  d  M3  UU:w  d  M;  [-        U
5      n!U!(       d  MO  U!S:¼  d  MW  U!n   O   [A        U5      n"[C        S,0 SU_SU_SU_SU_SU_SU _SU_SU_SU_S U_S!U"_S"UU(       a  S	U-   OS-   R#                  5       _S#U_S$U_S%U_S&[D        RF                  S'   S(   S)-   U-   _S*S+_6n#UU#l$        UU#l%        UU#l&        URO                  U#5        GM     U$ ! [         a    Sn GNf = fs  sn
f s  sn
f )-Núdata-listing-id="(\d+)"r!   r   zdata-listing-state="([^"]+)"zlazy-photos-value="([^"]+)"r   Úurlú<svg[^>]*>.*?</svg>r0   ©Úflagsú<script[^>]*>.*?</script>r#   r$   Úgradezreserve pricer,   Úcapzno cap)ÚmanualÚ	automaticÚautoÚcvtzsemi-automaticzsemi automaticr¢   Ú	Automaticr   éd   r6   ÚmakeÚmodelÚ
derivativeÚyearr<   ÚreserveÚ	cap_cleanÚdistance_milesÚservice_historyÚengineÚfuelÚtransmissionÚ	photo_urlr=   r1   r2   r3   Úsourcer/   © )(r–   r>   rF   rO   r@   r   ÚjsonÚloadsrB   rC   Ú
isinstanceÚlistÚgetrt   rA   ÚSr?   r   r   ÚPLATEr   ÚDISTANCEr   rD   rG   ÚrangeÚminrN   rI   rE   rL   ÚPURE_NUMrŽ   r   r   rQ   Ú_listing_idÚ_stateÚ_fuelrM   )$rR   ÚcarsrT   ÚmidÚ
listing_idr   ÚphotoÚmphÚarrr•   rV   rW   rŒ   r6   r]   Údistancerž   Úirª   r\   ÚmmrŸ   Úlowr¯   r°   r©   Úyear_idxr¦   r§   r¨   Ú
make_modelÚtoksr<   Úvr‡   Úcars$                                       r   Úparse_listingrÑ   Ú   sÂ  € Ø€DÜ˜i×(ˆÜ�hŠhÐ1°5Ó9ˆÞ%(�S—Y‘Y˜q”\¨bˆ
Ü—	’	Ð9¸5ÓAˆÞ"'�—‘˜A”¨Rˆô ˜5×!Ñ!Ùð ˆÜ�iŠiÐ6¸Ó>ˆÞðÜ—j’j¤×!1Ò!1°#·)±)¸A³,Ó!?Ó@�Þœ: c¬4×0Ñ0Ø ™FŸJ™J u¨bÓ1�Eô
 �FŠFÐ)¨3°¼R¿T¹TÑBˆÜ�FŠFÐ/°°a¼r¿t¹tÑDˆÜ$&§F¢F¨:°t¼W×=MÒ=MÈaÓ=PÓ$Q×$WÑ$WÐX\Ô$]ÓkÒ$]˜q×ahÑah×aj“�—‘–Ñ$]ˆÐkÙ*/Ó0ª% Q—w‘w–y©%ˆÐ0àˆÛˆAÜ—‘˜Q“ˆAßˆqØ—g‘g˜a“j×(Ñ(¨¨bÓ1�Ùñ	 ð ˆÛˆAÜ—‘˜qÓ!ˆAßˆqÜ §¡¨£
Ó+�Ùñ	 ð ˆÜ˜eÖ$‰DˆAˆqØ�w‰w‹y˜GÕ#¨¨A©´°E³
Õ(:Ü˜U 1 q¡5™\Ó*�Ùñ %ð
 ˆÜ˜eÖ$‰DˆAˆqØ !§'¡'£)Õ+Ü˜q¤# a¨!¡e¬S°«ZÓ"8Ö9�AÜŸ™ e¨A¡hÓ/�Bß�rÜ"& r§x¡x°£{Ó"3˜Ùñ	 :ñ
 ñ %ð ˆÜ˜eÖ$‰DˆAˆqØ—'‘'“)ˆCØ˜�|Ø˜s“?Ø‘CäŸ™ a›×hÈ1ÈqÉ5ÔSVÐW\ÓS]ÓK]¬U¯\©\¸%ÀÀAÁ¹,Ô-GÐcg�BÞÜ" 2§8¡8¨A£;Ó/˜Ùñ %ð ˆÛˆAØ�w‰w‹yœEÕ!Ø�Ùñ ð
 ˆÛˆAØ—'‘'“)—/‘/Ó#ˆCØÐ`Õ`Ø.1°V«m™{ÀÇÁÃ�Ùñ	 ð ˆØˆÜ˜eÖ$‰DˆAˆqÜ�z‰z˜!�}‹}Ü˜A“w�Ø�Ùñ	 %ð %'Ð&ˆÐ&ˆu�zØÑ H°£MØ˜x¨!™|Ñ,ˆJØ˜x¨!™|Ñ,ˆJØ×#Ñ#Ó%ˆDÞØ˜A‘w�ØŸ™  a b Ó*�ð ˆÜ˜eÖ$‰DˆAˆqÜ�~‰~˜a× Ó ¤T¨!£W°¥_¸!¸x½-Ü˜“G�ß�1˜˜c�Ø�GÙñ %ô ˜kÓ*ˆäò 
Ùð
áð
ñ ð
ñ "ð	
ñ
 ð
ñ ð
ñ ð
ñ ð
ñ ð
ñ $ð
ñ ð
ð ¶ #¨¢*¸"Ñ=×DÑDÔFð
ñ ð
ñ &ð
ñ ð
ô   Ÿ™ hÑ/°
Ñ;Ð>RÑRÐU_Ò_ð!
ñ" ð#
ˆð& %ˆŒØˆŒ
ØˆŒ	Ø�‰�C×ñK )ðN €Køôk ó Ø“ðüò lùÚ0s%   Â'A*XÆX&Æ4X&ÇX+ØX#Ø"X#)ÚyesÚoutstandingÚfinancedÚtrue)Únor…   Úclearznot outstandingúno outstanding financeÚfalse)zrecorded withÚtermÚ	agreementÚmonthszfinance companyÚlender)ÚpcpÚhpzhire purchaseÚleasezpcp financezconditional salec                 óò   • / n[        U 5       H`  u  p#UR                  5       R                  5       S:X  d  M)  UR                  XS-   US-     Vs/ s H  oDR                  5       PM     sn5        Mb     U$ s  snf )z¤Every run of lines that belongs to a MotorCheck section. Carwow puts
one in the page's own tab strip as well, so each heading is taken and
the caller decides. Pure.Ú
motorcheckr!   é<   )rD   r   r   rM   )rW   rS   rÉ   ÚrawrV   s        r   Ú_motorcheck_blocksrå   r  sl   € ð €CÜ˜EÖ"‰ˆØ�9‰9‹;×ÑÓ ,Õ.Ø�J‰J¨5°Q±°q¸2±vÑ+>Ó?Ò+> aŸ™ž	Ñ+>Ñ?Ö@ñ #ð €Jùò @s   ÁA4
z6^(.*?[A-Za-z].*?)\s*\(\s*\d[\d,.]*\s*m(?:iles?)?\s*\)$c                 óD  • [        U 5       H‘  u  pUR                  5       R                  5       S:w  a  M)  XS-   US-     HZ  n[        R	                  UR                  5       5      nU(       d  M/  UR                  S5      R                  S5      =(       d    Ss  s  $    M“     g)a
  The town the car is at, from the Location section of a Carwow car's
own page. The list card only says "134 miles away", so without this a
Carwow car showed a distance and no place (SA18AJX, Steven 2026-09-29).
Seen live 2026-09-29: "Location", "Location", "Elton, Bury (134 m)",
then "Exact location provided after sale is confirmed". Only a line
shaped like a place followed by the miles in brackets is taken, so the
tab strip's own "Location" word never reads as a town. None when the
section is not there. Pure, tested.Úlocationr!   é   z ,N)rD   r   r   Ú_TOWN_WITH_MILESrF   rO   )rW   rÉ   rä   Únxtr]   s        r   Úlocation_from_linesrë   €  s„   € ô ˜EÖ"‰ˆØ�9‰9‹;×ÑÓ *Ó,ÙØ˜Q™˜q 1™uÓ%ˆCÜ ×&Ñ& s§y¡y£{Ó3ˆAßˆqØ—w‘w˜q“z×'Ñ'¨Ó-×5°Ô5ó &ñ #ð r   c           	      ó|  • [         R                  " SU =(       d    S[         R                  5      nU(       d  g[        R                  " [         R
                  " SSUR                  S5      5      5      n[         R
                  " SSU5      n[         R
                  " SSU[         R                  S9n/ nUR                  S5       Hf  nS	R                  UR                  5       5      R                  S
5      nU(       d  M:  UR                  UR                  S5      (       a  UOUS-   5        Mh     S	R                  U5      $ )a   The seller's notes from a Carwow car page (Steven 2026-09-29, FY16BHD:
"Clutch pedal squeaks, rattle coming from the airbag area" and BidBrain
never said). Seen live the same day: a div.listing__seller-notes-text
holding one paragraph that starts "Seller notes: " and keeps the
seller's own line breaks; a long one is cut short on screen behind a
View more button, so it is read from the markup, not the visible text.
Each line ends in a full stop so Dealer OS can put one per line. "" when
there are none. Pure.z3class="listing__seller-notes-text"[^>]*>(.*?)</div>r   z	<br\s*/?>r$   r!   r#   z^\s*seller notes\s*:?\s*r›   r0   z ,;)Ú.Ú!Ú?rí   )r>   r@   r¹   rB   rC   rA   rO   ÚIr?   rL   ÚrstriprM   Úendswith)Úhtmlr]   r�   rS   r�   s        r   Úseller_notes_from_htmlrô   “  så   € ô 	�	Š	ÐHÈ$Ï*ÐRTÔVX×VZÑVZÓ[€AÞØÜ×ÒœBŸFšF <°°q·w±w¸q³zÓBÓC€DÜ�6Š6�*˜b $Ó'€DÜ�6Š6Ð-¨r°4¼r¿t¹tÑD€DØ
€CØ—
‘
˜4Ö ˆØ�x‰x˜Ÿ
™
›Ó%×,Ñ,¨UÓ3ˆßˆ4Ø�J‰J˜tŸ}™}¨_×=Ñ=‘tÀ4È#Á:ÖNñ !ð �8‰8�C‹=Ðr   c                 ó`  • [        U 5       H  n[        U5      nUc  M  Us  $    [        U 5       Hû  u  p4UR                  5       R	                  5       nSU;   a    gSU;   d  M3  UR                  SS5      S   R                  S5      nXcS-   [        U 5      :  a#  XS-      R                  5       R	                  5       OS4 Hh  nU(       d  M  U[        ;   d  UR                  S	5      (       a      gU[        ;   d.  UR                  S
5      (       d  UR                  S5      (       d  Mg      g   Mý     g)aB  Whether the car has outstanding finance, read where Carwow actually
says it: the MotorCheck section (2026-09-10, Steven: "Are you getting
the finance info from the correct place on a carwow listing?"). There
every check reads Clear when it is clear, Finance among them; a car
with finance still owing has that row pulled out instead and given the
agreement, the lender, the type and the term.

Only the MotorCheck section answers. The page also has a Finance
heading over Carwow's own marketing about savings and monthly
payments, which says nothing about the car and used to read as no
finance (EP16DAA, 2026-09-09). An "Outstanding finance" label is
still honoured if Carwow ever writes one. Anything else leaves the
question unanswered rather than guessing, because a wrong "no
finance" is the costly way to be wrong. Pure, tested.NrØ   Fzoutstanding financeÚfinancer!   z :?r   zno rÒ   ô   Â£T)
rå   Ú_finance_in_motorcheckrD   r   r   r?   rG   Ú_FIN_NOr“   Ú_FIN_YES)rW   rT   ÚanswerrÉ   rä   rV   ÚtailÚcands           r   Úfinance_from_linesrþ   ª  s  € ô $ EÖ*ˆÜ'¨Ó.ˆØÓØŠMñ +ô ˜EÖ"‰ˆØ�I‰I‹K×ÑÓˆØ# qÓ(ÙØ  AÕ%Ø—7‘7˜9 aÓ(¨Ñ+×1Ñ1°%Ó8ˆDØÀAÁÌÈEË
Ó@R ¨!¡e¡× 2Ñ 2Ó 4× :Ñ :Ô <ÐXZÓ\�ÞÙØœ7“? d§o¡o°e×&<Ñ&<Ú Øœ8Ó# t§¡°u×'=Ñ'=ÀÇÁÐQU×AVÓAVÚó ]ñ #ð r   c                 ó`  ^• [        U 5       Hž  u  pUR                  5       R                  5       S;  a  M)  XS-   US-     Hg  nUR                  5       R                  5       mT(       d  M*  TS;   a      gT[        ;   d  [	        U4S j[
         5       5      (       a      gTS;   d  Mf      g	   M      g	)
zíThe Finance row inside one MotorCheck section: Clear means nothing
owing, an agreement's own words mean there is. Whichever comes first
after the row decides, so the details of a later check can never be
read as this one's. Pure, tested.)rö   zfinance:r!   é   )r×   r…   z
no financeFc              3   ó,   >#   • U  H	  oT;   v •  M     g 7fr&   r³   )r'   ÚwrË   s     €r   r)   Ú)_finance_in_motorcheck.<locals>.<genexpr>Û  s   øé € Ð-QÒ@P¸1°3®hÒ@Pùs   ƒT)	úplate transferÚstolenzinsurance write-offÚsalvagezformer business vehiclezimported & exportedzimported and exportedr<   z	write-offN)rD   r   r   Ú_AGREEMENT_TYPESrJ   Ú_AGREEMENT_WORDS)rT   rÉ   rV   rê   rË   s       @r   rø   rø   Í  s    ø€ ô
 ˜%Ö ‰ˆØ�7‰7‹9�?‰?ÓÐ$;Ó;ÙØ˜Q™˜q 2™vÓ&ˆCØ—)‘)“+×#Ñ#Ó%ˆCÞÙØÐ5Ó5ÚØÔ&Ó&¬#Ô-QÕ@PÓ-Q×*QÑ*QÚàð /õ /ò ó 'ñ !ð  r   c                 ó4  • [         (       a  g[         R                  S5         [        R                  R	                  [        R                  R	                  [        R                  R	                  [        R                  R                  [        5      5      5      5      n[        R                  R                  USS5      n[        R                  " USS9  U  Vs/ s H/  nSUR                  5       ;   d  SUR                  5       ;   d  M-  UPM1     nn[        U 5      n[        [        R                  R                  US5      S	S
S9 nUR                  SUR                  =(       d    UR                   SUSS < S35        [        U5       H:  u  p‰UR                  SU SU	 V
s/ s H  oª(       d  M  U
PM     sn
SS < S35        M<     UR                  SUR                   < S35        SSS5        gs  snf s  sn
f ! , (       d  f       g= f! ["         a     gf = f)z{Once per run, the page lines that mention finance, to
data/logs/carwow_detail.log, so the wording Carwow uses is on record.NTÚdataÚlogs)Úexist_okrö   Úhpizcarwow_detail.logr  zutf-8)ÚencodingzCarwow finance lines for ú: é   r$   zMotorCheck block é   z	read as: )Ú_finance_note_writtenrM   ÚosÚpathÚdirnameÚabspathÚ__file__rL   Úmakedirsr   rå   ÚopenÚwriter6   r=   rD   rö   rt   )rW   rÐ   Úrootr  rV   Úhitsr”   ÚfÚnr•   Úxs              r   Ú_finance_note_oncer   è  s˜  € ÷ ÒØÜ× Ñ  Ô&ðÜ�w‰w�‰œrŸw™wŸ™¬r¯w©w¯©¼r¿w¹w¿¹ÌxÓ?XÓ/YÓZÓ[ˆÜ�w‰w�|‰|˜D &¨&Ó1ˆÜ
�Š�D 4Ò(Ù ÓQš5�a I°·±³Ó$:¸eÀqÇwÁwÃyÑ>P—™5ˆÐQÜ# EÓ*ˆÜ”"—'‘'—,‘,˜tÐ%8Ó9¸3ÈÒQÐUVØ�G‰GÐ/°·±×0J¸3¿?¹?Ð/KÈ2ÈdÐSVÐTVÈiÉ]ÐZ\Ð]Ô^Ü! &Ö)‘�Ø—‘Ð+¨A¨3¨b¹QÓ1DºQ¸Ã!·!¹QÑ1DÀSÀbÐ1IÑ0LÈBÐOÖPñ *à�G‰G�i §¡™¨bÐ1Ô2÷	 RÐQùò Rùò
 2E÷ RÕQûô
 ó Ùðúsa   £B4H
 Ã,G/ÄG/Ä6H
 ÅAG9Æ 
G4Æ.G4Æ42G9Ç&H
 Ç/H
 Ç4G9Ç9
HÈH
 ÈH
 È

HÈHc                 óf  ^• UR                   (       d  U$ U R                  UR                   SSS9   U R                  SSS9  U R	                  S5      R                  S5       Vs/ s H)  o"R                  5       (       d  M  UR                  5       PM+     snmU4S	 jnU" S
5      nU(       aO  [        R                  U5      nU(       a3  [        UR                  S5      5      nXal        UR                  c  Xal        [        U" S5      5      nUb  Xql        UR                  c  [!        T5      Ul        [#        TU5        UR$                  (       d  ['        T5      =(       d    SUl        U" S5      nUb&  UR                  5       R)                  5       S;   Ul        U" S5      n	U	(       aP   SSKn
U
R,                  R/                  U	R                  5       S5      R1                  5       R3                  5       Ul        U" S5      nU(       a  SUR)                  5       ;  a  X±l        U" S5      nU(       a  SUR)                  5       ;  a  XÁl        U" S5      nU" S5      nU=(       d    SR                  5       R)                  5       S:X  d*  U(       Ga  UR                  5       R)                  5       S:w  aô  SR=                  [?        UR@                  =(       d    S5      RC                  5       R                  5       5      n[E        T5       Hœ  u  nnUR)                  5       S:X  d  M  SR=                  TUUS-    5      RC                  5       n[F        RH                  " SU5       H@  nSR=                  UR                  S5      R                  5       5      nUU:w  d  M9  UUl%          O     O    U RM                  5       nURN                  (       d  [Q        U5      Ul'        URR                  (       d  [U        U5      SS Ul)        U$ ! [         a     GNÆf = fs  snf ! [6         a     GN&f = f! [         a    Sn N|f = f) a	  Open a Carwow car's own page and read what the list card does not show:
previous owners (Carwow calls them former keepers) and VAT qualifying.
Reads the rendered page, leaving fields unset if they cannot be read so the
brain can hold the car back rather than guess.rd   re   rf   Únetworkidleé'  ri   Úbodyr$   c                 óÊ   >• [        T5       HS  u  pUR                  5       U R                  5       :X  d  M)  US-   [        T5      :  d  M=  TUS-      R                  5       s  $    g )Nr!   )rD   r   rG   r   )ÚlabelrÉ   rV   rW   s      €r   Úvalue_afterÚ'enrich_from_detail.<locals>.value_after  sS   ø€ Ü˜eÖ$‰DˆAØ�w‰w‹y˜EŸK™K›MÕ)¨a°!©e´c¸%³jÕ.@Ø˜Q ™U‘|×)Ñ)Ó+Ò+ñ %ð r   z	CAP cleanr!   NzFormer keepersr   zVAT qualifying)rÒ   ÚyrÕ   zStart of current keeperr   z%d %b %YzMechanical faultszstarts and drives normallyzWarning lightszno warning lightszPrivate platezPlate transferrÒ   r×   r  r0   r  z6\b([A-Z]{2}\d{2}\s?[A-Z]{3}|[A-Z]\d{1,3}\s?[A-Z]{3})\b)+r=   rq   Úwait_for_load_statert   Ú
inner_textr?   r   rN   r@   r   rO   r«   rª   r‘   Úownersrö   rþ   r   rç   rë   r   Úvat_qualifyingÚdatetimeÚstrptimeÚdateÚ	isoformatÚkeeper_startÚ
ValueErrorÚ
mech_issueÚwarning_lightsrL   r   r6   ÚupperrD   r>   ÚfinditerÚselling_vrmry   Únotesrô   Ú
photo_urlsÚ_gallery_photos_from_html)r}   rÐ   rV   r'  Úcap_liner]   Ú	cap_valuer,  ÚvatÚksÚ_dtÚmechÚwlÚppÚptr6   rÉ   Úsegrý   ró   rW   s                       @r   Úenrich_from_detailrF  ý  sg  ø€ ð
 �?�?Øˆ
Ø‡I�Iˆc�o‰oÐ*<Àe€IÑLðØ× Ñ  ¸Ð Ñ>ð !%§¡°Ó 7× =Ñ =¸dÔ CÓQÒ C˜1ÇwÁwÇy‹YˆQ�W‰WŽYÑ CÑQ€Eõñ ˜;Ó'€HÞÜ�L‰L˜Ó"ˆÞÜ˜QŸW™W Q›ZÓ(ˆIØ%ŒMð
 �{‰{Ñ"Ø'”ô ™KÐ(8Ó9Ó:€FØÑØŒ
Ø
‡{�{ÑÜ(¨Ó/ˆŒÜ�u˜cÔ"à�<�<Ü*¨5Ó1×7°RˆŒá
Ð&Ó
'€CØ
�Ø ŸY™Y›[×.Ñ.Ó0Ð4HÑHˆÔñ 
Ð.Ó	/€BÞ	ð	Û"Ø"Ÿ|™|×4Ñ4°R·X±X³ZÀÓL×QÑQÓS×]Ñ]Ó_ˆCÔñ Ð*Ó+€DÞÐ,°D·J±J³LÓ@ØŒÙ	Ð%Ó	&€BÞ	Ð!¨¯©«Ó3ØÔñ
 
�_Ó	%€BÙ	Ð%Ó	&€BØ
�ˆb×ÑÓ×ÑÓ! UÓ*¯r°b·h±h³j×6FÑ6FÓ6HÈGÓ6SØ�g‰g”c˜#Ÿ'™'Ÿ- RÓ(×.Ñ.Ó0×6Ñ6Ó8Ó9ˆÜ˜eÖ$‰DˆAˆqØ�w‰w‹yÐ,Õ,Ø—h‘h˜u Q q¨2¡v˜Ó/×5Ñ5Ó7�ÜŸšÐ%^Ð`cÖd�AØŸ7™7 1§7¡7¨1£:×#3Ñ#3Ó#5Ó6�DØ˜s•{Ø*.˜œÙñ	 eñ
 ñ %ð"Ø�|‰|‹~ˆð �9�9Ü*¨4Ó0ˆŒ	Ø�>�>Ü2°4Ó8¸¸!Ð<ˆŒà€JøôA ó ÚðüâQøôb ó 	Úð	ûôL ó ØŠðúsB   ±O: Á#PÁ?PÆ(AP Î#P! Ï:
PÐPÐ
PÐPÐ!P0Ð/P0z3https://carwow-sell-my-car\.imgix\.net/[A-Za-z0-9]+c                 óÖ   • 0 n[         R                  U 5       H6  nUR                  UR                  S5      UR                  S5      S-   5        M8     [	        UR                  5       5      $ )z¸The full listing gallery from a car's own detail page HTML, deduped
to one URL per distinct photo. A standalone function purely for
testing, enrich_from_detail is the only real caller.r   z0?w=900&auto=compress,format&q=60&cs=srgb&fit=max)Ú_GALLERY_PHOTO_REr7  Ú
setdefaultrO   r·   Úvalues)ró   ÚseenÚgms      r   r;  r;  m  sS   € ð €DÜ×(Ñ(¨Ö.ˆØ�‰˜Ÿ™ › R§X¡X¨a£[Ð3eÑ%eÖfñ /ä�—‘“ÓÐr   c                 ó:   • U R                  S5      (       d  gSU ;   $ )aœ  True when a Carwow page load has landed on a login page instead of the
real stock list. Two real redirect shapes seen: Carwow's OWN login page,
same host as the stock list (dealers.carwow.co.uk/dealers/login, see
browser.SITES["carwow"]["login_url"], easy to miss with a host only
check since it is not a different host at all), and an SSO style
redirect to a different host entirely (auth.carwow.co.uk), the one
actually observed live 2026-08-25 when this was first caught. Checking
only the host would have silently missed the first shape, exactly the
class of bug this function exists to prevent. Pure string check, no
browser needed, so it is unit tested directly.zhttps://dealers.carwow.co.ukTz/dealers/login)r“   )r™   s    r   Ú_looks_logged_outrN  w  s"   € ð �>‰>Ð8×9Ñ9ØØ˜sÑ"Ð"r   c                 ó  • [         R                  S   S   nSU;   a  SOSn/ n[        5       n[        5       n[        SUS-   5       Hù  n	U R	                  U U SU	 3SSS	9   U R                  S
U	S:X  a  SOSS9  U R                  5       n
[        [        R                  " SU
5      5      nX¸-
  nU(       d    OŒX‹-  n[        U
5       HB  n[        USS5      nU(       d  M  Xç;  d  M   UR                  U5        UR!                  U5        MD     U(       a  U" U	[#        U5      5        U(       d  Mè  [#        U5      U:¼  d  Mù    O   U(       a  USU $ U$ ! [         a*    [        U R                  5      (       a  [        S5      e   MA  f = f)aú  Read the whole filtered Carwow stock, page by page (Carwow paginates
with &page=N), from an already open, already logged in page. Accumulates
auction cars across pages, stopping when a page shows no new listings.
Auction only: non auction states (for example second chance quotes) are
filtered out by parse_listing, which can legitimately leave zero cars
outside the auction window. The caller owns the page and its context, so
the daily run can reuse the same page afterward for detail enrichment
without opening a second session (the same shape as Auction4Cars'
read_all_pages). on_page(n, total_so_far), if given, is called after each
page, so a slow multi page read (up to 25 page loads) can show live
progress instead of looking stuck for minutes at a time.r1   Ú	stock_urlrï   Ú&r!   úpage=rd   re   rf   z[data-listing-id]rl   é@  ri   zmCarwow did not show the stock list, the login may have expired. Run: python3 login.py carwow, then try again.r˜   r¿   r   N)r   rQ   Úsetr¼   rq   rs   rt   rN  r™   ru   ry   r>   ÚfindallrÑ   ÚgetattrÚaddrM   rG   )r}   ÚlimitÚ	max_pagesÚon_pageÚbaseÚseprÂ   Ú	seen_carsÚseen_rawr  ró   Úraw_idsÚnew_rawÚcrU   s                  r   Úread_all_pagesrb  ‡  s}  € ô �=‰=˜Ñ" ;Ñ/€DØ˜“‰# #€CØ€DÜ“€IÜ‹u€HÜ�1�i !‘mÖ$ˆØ�	‰	�T�F˜3˜%˜u Q CÐ(Ð5GÐQVˆ	ÑWð	Ø×"Ñ"Ð#6ÈÈaËÁÐUYÐ"ÑZð �|‰|‹~ˆÜ”b—j’jÐ!;¸TÓBÓCˆØÑ$ˆÞÙØÑˆÜ˜tÖ$ˆAÜ˜!˜]¨BÓ/ˆCßˆs�sÕ+Ø—‘˜cÔ"Ø—‘˜A–ñ	 %ö
 Ù�A”s˜4“yÔ!ßˆ5”S˜“Y %Õ'Ùñ= %ö> !ˆ4��ˆ<Ð* dÐ*øô7 ó 	ô ! §¡×*Ñ*Ü"ðMóð ó ð	ús   Á"EÅ/FÆFc                 óî   • [         R                  " U SSS9n UR                  (       a  UR                  S   OUR                  5       n[	        XAUS9UR                  5         $ ! UR                  5         f = f)a  Standalone convenience: opens its own context, reads every page of the
filtered stock, and closes. The daily run does not use this, it manages
its own context so the same page can be reused for owner/VAT detail
enrichment afterward, see daily_run._read_carwow.r1   Trb   r   )rX  rY  )r   rn   ro   rp   rb  rz   )r{   rX  rY  r|   r}   s        r   Ú	read_liverd  º  sU   € ô
 ×
%Ò
% j°(ÀTÑ
J€CðØ"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆÜ˜d¸9ÑEà�	‰	�øˆ�	‰	�ús   ˜9A" Á"A4c                 ó  • U Hv  n U R                  U5      nUS-   [        U 5      :¼  a  M)  [        R	                  XS-      5      nU(       d  ML  [        UR                  S5      R                  SS5      5      s  $    g! [         a     Mˆ  f = f)a¸  The pound figure on the line straight after the first of labels that
is actually on the page, or None if none of them are there at all (a
purchase still mid document review has no breakdown yet, never
guessed). More than one label matters for "Vehicle price" specifically
(Mark 2026-08-24, "i want all the cars from the last 90 days added",
a real live batch capture found most real purchases silently getting
no breakdown at all): once a sale reaches "collected" (fully paid and
completed), Carwow's own page relabels it "Final vehicle price" and
adds a real payment date, invoice and proof of purchase download
links alongside the same figures, confirmed live against a real
completed purchase.r!   r   r   N)Úindexr3  rG   rN   r@   r   rO   r   )rW   Úlabelsr&  rÉ   r]   s        r   Ú_purchase_moneyrh  Ò  sˆ   € ó ˆð	Ø—‘˜EÓ"ˆAð ˆq‰5”C˜“JÓÙÜ�L‰L˜ 1™u™Ó&ˆßˆ1Ü�q—w‘w˜q“z×)Ñ)¨#¨rÓ2Ó3Ò3ñ ð øô ó 	Úð	ús   ‡A>Á>
BÂBu   Sale agreed for\s*Â£\s*([\d,]+)c                 óº  • U R                  [        R                  S   S    SU S3SSS9  U R                  S5         U R	                  S	S
S9  U R                  S5      n[        R                  " S[        U5      [        R                  " U5      5        [!        U5      $ ! [
         a±     U R                  [        R                  S   S    SU 3SSS9  U R                  S5        [        R                  U R                  S5      5      nO! [
         a    Sn Of = fU(       d  0 s $ [        UR                  S5      R                  SS5      5      SSSSS.s $ f = f)uÙ  Visits a won car's own /documents page and reads its real payment
breakdown: Vehicle price, Carwow Buyer's fee, VAT, Transport fee, VAT,
Total you pay (confirmed live against a real purchase, the same
figures also shown on the plain listing page, this page is visited
anyway for the document bundle below so no extra page load). Returns
a dict shaped like motorway.read_purchase_breakdown's own (winning_bid,
motorway_fee, transport_fee, vat_total, protect_fee), reusing the same
generic payment columns every platform's purchase already shares
(bidbrain/db.py's _PURCHASE_PAYMENT_FIELDS): motorway_fee stands in for
Carwow's own Buyer's fee, protect_fee is always None, Carwow has no
equivalent add on.

Falls back to the plain listing page's own "Sale agreed for Â£X"
headline (Mark 2026-08-24, "i want all the cars from the last 90 days
added": a real live batch found the itemised breakdown itself
genuinely collapses from view on Carwow's own /documents page once a
purchase is old enough, roughly two months in one real case, the fees
are no longer shown anywhere, only the plain listing page's own
headline figure survives). This fallback only ever fills winning_bid,
never guesses a fee breakdown that is not actually there any more.r1   r2   r3   ú
/documentsr"  re   rf   é°  ztext=Vehicle pricerS  ri   r$  Nr!   r   r   )Úwinning_bidÚmotorway_feeÚtransport_feeÚ	vat_totalÚprotect_fee)rq   r   rQ   rv   rs   rt   Ú_SALE_AGREED_REr@   r+  r   rO   r   r   Úfinance_lines_noter   Úfinance_from_purchase_textÚ_parse_purchase_breakdown)r}   rÄ   r]   r�   s       r   Úread_purchase_breakdownru  î  sa  € ð* 	‡I�I”—‘˜xÑ(¨Ñ4Ð5Ð5GÈ
À|ÐS]Ð^Ø&°ð ñ 7à×Ñ˜$ÔðOØ×ÑÐ3¸TÐÑBð �?‰?˜6Ó"€DÜ×$Ò$ X¬s°:«Ä×@hÒ@hÐimÓ@nÔoÜ$ TÓ*Ð*øô ó Oð	Ø�I‰IœŸ™ xÑ0°Ñ<Ð=Ð=OÐPZÈ|Ð\Ø!.¸ð ñ ?à×!Ñ! $Ô'Ü×&Ñ& t§¡°vÓ'>Ó?‰AøÜó 	ØŠAð	úæØŠIÜ" 1§7¡7¨1£:×#5Ñ#5°c¸2Ó#>Ó?ÐQUØ!%°DÈñOò 	OðOús<   ¾B Â
EÂ*AD
Ä	EÄ
DÄEÄDÄEÄ'0EÅEc           	      ó†  • U =(       d    SR                  5        Vs/ s H)  oR                  5       (       d  M  UR                  5       PM+     nn[        USS5      nUc  0 $ [        US5      n[        US5      n/ n[        U5       H}  u  pqUS:X  d  M  US-   [	        U5      :  d  M!  [
        R                  X'S-      5      nU(       d  MD  UR                  [        UR                  S5      R                  S	S5      5      5        M     [        U5      n	[        R                  " U 5      n
X4XY=(       d    SSU
S
   U
S   S.$ s  snf )a  Pure text parsing, pulled out of read_purchase_breakdown so it can be
tested without a browser, the same split already used for
motorway.py's own _money_after/_vat_total_from_lines. {} when the
breakdown is not on the page at all (a purchase still mid document
review).r   zVehicle pricezFinal vehicle priceNzCarwow Buyer's feezTransport feez	VAT (20%)r!   r   Úfinance_settlementÚnegative_equity)rl  rm  rn  ro  rp  rw  rx  )Ú
splitlinesr   rh  rD   rG   rN   r@   rM   r   rO   r   Úsumr   rs  )r�   rV   rW   Úvehicle_priceÚ	buyer_feern  Úvat_amountsrÉ   r]   ro  Úfins              r   rt  rt    s(  € ð "&§¨× 7Ñ 7Ô 9ÓGÒ 9˜1¿W¹W¿Y‹YˆQ�W‰WŽYÑ 9€EÐGô $ E¨?Ð<QÓR€MØÑØˆ	Ü Ð';Ó<€IÜ# E¨?Ó;€Mð €KÜ˜%Ö ‰ˆØ�Õ  A¡¬¨E«
Õ 2Ü—‘˜U q¡5™\Ó*ˆAßˆqØ×"Ñ"¤3 q§w¡w¨q£z×'9Ñ'9¸#¸rÓ'BÓ#CÖDñ	 !ô
 �KÓ €IÜ
×
2Ò
2°4Ó
8€CØ(Ø*×9JÀdØà"%Ð&:Ñ";ÐPSÐTeÑPfñ	hð hùò' Hs
   œD>¸D>zJCollection date\s*\n?\s*(\d{1,2})(?:st|nd|rd|th)\s+([A-Za-z]{3})\s+(\d{4}))ÚJanÚFebÚMarÚAprÚMayÚJunÚJulÚAugÚSepÚOctÚNovÚDecr!   c                 óÖ  • U R                  [        R                  S   S    SU 3SSS9  U R                  S5         U R	                  S5      n[        R                  U5      nU(       d  g	[        UR                  S
5      5      [        R                  UR                  S5      R                  5       5      UR                  S5      penU(       d  g	U SUS SUS 3$ ! [
         a     g	f = f)a”  The date Carwow's own transport collects, or is due to collect, the
car from the seller, wired up the same way as motorway.
read_collection_date (Mark 2026-08-26: "navigate to the won, then go in
to the listing and theres a card with collection confrimed and
collection date wire this up the same way as MW"). Read straight off
the won car's own plain listing page (the same page
read_purchase_gallery already visits, a real expandable "Collection
confirmed" card with a Collection date row, confirmed live against
three real purchases at different sale stages, quote_accepted and
collection_arranged both already carry a real scheduled date here).

Unlike Motorway, Carwow only ever shows the one "Collection date"
label (never a separate past tense "Collected" one), an ordinal date
("28th Aug 2026"), so there is no confirmed-vs-scheduled state to
choose between here, just the one figure to read, and it can move (a
reschedule) so this always takes a fresh value when read, see
db._PURCHASE_REFRESHABLE_FIELDS, same as Motorway's own.

Returns a "YYYY-MM-DD" string, or None when this listing carries no
such card at all (a still very early stage sale), never guessed.r1   r2   r3   r"  re   rf   rk  r$  Nr!   r   r,   r-   Ú02d)rq   r   rQ   rv   r+  rt   Ú_COLLECTION_DATE_REr@   r   rO   Ú_MONTHSr¸   r   )r}   rÄ   r�   r]   ÚdayÚmonr©   s          r   Úread_collection_dater‘  ?  sÞ   € ð* 	‡I�I”—‘˜xÑ(¨Ñ4Ð5Ð5GÈ
À|ÐTØ&°ð ñ 7à×Ñ˜$ÔðØ�‰˜vÓ&ˆô 	×"Ñ" 4Ó(€AÞØÜ˜Ÿ™ ›“_¤g§k¡k°!·'±'¸!³*×2BÑ2BÓ2DÓ&EÀqÇwÁwÈqÃzˆd€CÞØØˆV�1�S˜�I˜Q˜s 3˜iÐ(Ð(øô ó Ùðús   ½C Ã
C(Ã'C(c                 ó.   • [         R                  " XUSS9$ )a!  Carwow's own photos, saved as real files of ours (2026-09-10,
Steven: "yes fix the carwow photos"). Unlike Motorway's these urls are
not signed and do not expire on a clock, but they do point at Carwow's
own imgix host: a listing taken down, or hotlinking blocked, and every
car on the Purchases page loses its photos, with nothing on our side to
put back. Saved here, uploaded once to Dealer OS's own store before a
push (dealer_os_files), so Dealer OS shows a link of ours that keeps
working. Carwow's photo ids carry no extension, hence .jpg.z.jpg)Úext)r   Údownload_media)r}   ÚurlsÚout_dirs      r   Údownload_purchase_mediar—  d  s   € ô ×!Ò! $¨g¸6ÑBÐBr   c                 óÔ   • U R                  [        R                  S   S    SU 3SSS9  U R                  S5         U R	                  5       n[        U5      $ ! [
         a    / s $ f = f)zòThe full photo gallery from a won car's own listing page (not
/documents, which carries no photos at all, checked live). Reuses
_gallery_photos_from_html unchanged, the exact same host and pattern
already proven for the stock listing gallery.r1   r2   r3   r"  re   rf   rk  )rq   r   rQ   rv   ry   rt   r;  )r}   rÄ   ró   s      r   Úread_purchase_galleryr™  p  sx   € ð
 	‡I�I”—‘˜xÑ(¨Ñ4Ð5Ð5GÈ
À|ÐTØ&°ð ñ 7à×Ñ˜$ÔðØ�|‰|‹~ˆô % TÓ*Ð*øô ó ØŠ	ðús   ½A ÁA'Á&A')Úservice_record_Úv5c_logbook_c                 óì  • U R                  [        R                  S   S    SU S3SSS9  U R                  S5        U R	                  S	S
5      n[        S U 5       S5      nU(       d  / $ U R                  R                  R                  USS9nUR                  S:w  a  [        SUR                   S35      eU=(       d    [        R                  " SS9n[        R                  R                  US5      n[!        US5       nUR#                  UR%                  5       5        SSS5        / n[&        R(                  " U5       n	U	R+                  5        H^  n
U
R-                  [.        5      (       d  M  U	R1                  X¢5        UR3                  [        R                  R                  X*5      5        M`     SSS5        [        R4                  " U5        U$ ! , (       d  f       N¹= f! , (       d  f       N7= f)a[  Downloads the won car's own document bundle (a zip, "Download all"
on /documents) and extracts only the service record and V5C logbook
files, matching the exact real filenames Carwow's own bundle uses
(confirmed live: service_record_1_....jpg, v5c_logbook_1_....jpg,
alongside dashboard_photo, driving_licence and others, deliberately
never touched). The zip's own download link is a signed, short lived
S3 url, so this is deliberately read fresh and used immediately
(download then push in the same tight loop), never stored for later,
the same lesson already learned the hard way for Motorway's own V5
and service history urls (see CLAUDE.md). Returns a list of local
file paths ready for dealerkit_write.upload_local_document; [] if the
bundle is not ready yet (a purchase still mid document review) or has
no wanted files. Caller owns cleaning up out_dir.r1   r2   r3   rj  r"  re   rf   rk  za[href]zels => els.map(e => e.href)c              3   ó6   #   • U  H  nS U;   d  M  Uv •  M     g7f)zdocument_bundle.zipNr³   )r'   Úhs     r   r)   Ú*read_purchase_documents.<locals>.<genexpr>™  s   é € ÐCšu˜!Ð(=ÀÑ(B—A‘Ašuùs   ‚
�	Ni`ê  ri   éÈ   z3Carwow: fetching the document bundle returned HTTP rí   Úbidbrain_cw_docs_)Úprefixz
bundle.zipÚwb)rq   r   rQ   rv   rw   ÚnextÚcontextÚrequestr¸   r8   ru   ÚtempfileÚmkdtempr  r  rL   r  r  r$  ÚzipfileÚZipFileÚnamelistr“   Ú_WANTED_DOCUMENT_PREFIXESÚextractrM   Úremove)r}   rÄ   r–  ÚhrefsÚzip_urlÚrespÚzip_pathr  ÚpathsÚzfr7   s              r   Úread_purchase_documentsrµ  ‡  s�  € ð 	‡I�I”—‘˜xÑ(¨Ñ4Ð5Ð5GÈ
À|ÐS]Ð^Ø&°ð ñ 7à×Ñ˜$ÔØ×%Ñ% iÐ1NÓO€EÜÑC™uÓCÀTÓJ€GÞØˆ	Ø�<‰<×Ñ×#Ñ# G°UÐ#Ð;€DØ‡{�{�cÓÜÐPÐQU×Q\ÑQ\ÐP]Ð]^Ð_Ó`Ð`Ø×Eœ×)Ò)Ð1DÑE€GÜ�w‰w�|‰|˜G \Ó2€HÜ	ˆh˜Ô	 Ø	�‰�—	‘	“Ô÷ 
à€EÜ	�Š˜Ô	" bØ—K‘K–MˆDØ�‰Ô8×9Ó9Ø—
‘
˜4Ô)Ø—‘œRŸW™WŸ\™\¨'Ó8Ö9ñ "÷ 
#ô
 ‡I‚IˆhÔØ€L÷ 
Õ	ú÷ 
#Õ	"ús   Ä GÅ,G%Å1AG%Ç
G"Ç%
G3zlisting-bookmark-componentz@[data-action="click->listing-bookmark-component#toggleBookmark"]z5[data-listing-bookmark-component-target="activeIcon"]z(listing-bookmark-component__icon--hiddenr   aá  
([id, ident]) => {
  const frame = document.querySelector('turbo-frame#listing-bookmark-' + id);
  const root = frame || document;
  let el = root.querySelector('[data-controller~="' + ident + '"]');
  if (!el && frame && frame.matches('[data-controller~="' + ident + '"]')) el = frame;
  if (!el) return false;
  const app = window.Stimulus;
  if (!app || !app.getControllerForElementAndIdentifier) return false;
  return !!app.getControllerForElementAndIdentifier(el, ident);
}
c                   ó0   ^ • \ rS rSrSrSU 4S jjrSrU =r$ )ÚBookmarkNotConfirmediü  a.  Carwow took the click, answered its usual success, and the page
still reads the way it did before. `sent` says whether a toggle
request genuinely reached Carwow, which decides whether trying the
same car again is worth anything or is just flipping it back and
forth (see daily_run.watchlist_sync_pass).c                 ó0   >• [         TU ]  U5        X l        g r&   )ÚsuperÚ__init__Úsent)ÚselfÚmessager»  Ú	__class__s      €r   rº  ÚBookmarkNotConfirmed.__init__  s   ø€ Ü‰Ñ˜Ô!Ø�	r   ©r»  )F)Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__rº  Ú__static_attributes__Ú__classcell__)r¾  s   @r   r·  r·  ü  s   ø† ñ2÷õ r   r·  c                 óö   • [         R                  " S[         R                  " [        U5      5      -  U =(       d    S[         R                  5      nU(       a"  UR                  S5      UR                  S5      4$ S$ )am  This car's own shortlist panel out of a page Carwow served, as
(opening tag, what is inside it). (None, None) when the page does
not carry that car's panel at all. Its own function because the
panel's markup is the one thing that says how Carwow marks a car as
shortlisted, and a failure now reports it rather than leaving the
next session to guess (golden rule 5).zF(<turbo-frame[^>]*\bid="listing-bookmark-%s"[^>]*>)(.*?)</turbo-frame>r   r!   r   )NN)r>   r@   Úescaper   r¹   rO   )ró   rÄ   r]   s      r   Úbookmark_framerÊ    sZ   € ô 	�	Š	Ð[Ü—I’Iœc *›oÓ.ñ/Ø04·
¸¼B¿D¹Dó	B€Aæ'(ˆA�G‰G�A‹J˜Ÿ™ ›
Ð#Ð:¨lÐ:r   c                 ó  • [        X5      u  p#U(       d  g[        R                  " SSU=(       d    S[        R                  S9n[        R                  " SSX#-   5      R	                  5       nUSS [        U5      S:”  a  S	-   $ S-   $ )
z»The panel above squashed onto one readable line for a log or an
error, empty when there is no panel. Long markup is cut, the point
is the tags and their classes, not the svg paths inside.r   rš   z<svg/>r›   ú\s+r0   Ni�  z ...)rÊ  r>   rA   r¹   r   rG   )ró   rÄ   Úopen_tagÚinnerr�   s        r   Úbookmark_panel_wordsrÏ    st   € ô % TÓ6�O€HÞØÜ�FŠFÐ)¨8°U·[¸bÌÏÉÑM€EÜ�6Š6�&˜#˜xÑ/Ó0×6Ñ6Ó8€DØ��ˆ:¤3 t£9¨s£?˜Ñ;Ð;¸Ñ;Ð;r   c                 ó¤   • [         R                  " S[         R                  " U5      -  U =(       d    S5      nU(       a  UR                  S5      $ S$ )Nú\b%s="([^"]*)"r   r!   )r>   r@   rÉ  rO   )Útagr7   r]   s      r   Ú	_tag_attrrÓ     s9   € Ü
�	Š	Ð#¤b§i¢i°£oÑ5°s·y¸bÓA€AÞˆ1�7‰7�1‹:Ð" Ð"r   c           	      óª  ^• [        X5      u  p4Uc  / $ U=(       d    SnS[        US5      =(       d    S-   /n[        R                  " SU5       Hh  m[        TR	                  S5      S5      R                  5       n[        S U 5       5      (       d  ME  UR                  S	S
R                  U5      -   5        Mj     [        R                  " SU5       HK  mUR                  TR	                  S5      S-   [        TR	                  S5      S5      =(       d    S-   5        MM     [        R                  " SU5       H0  mUR                  SS
R                  U4S jS 5       5      -   5        M2     [        R                  " SU5       Hb  m[        TR	                  S5      S5      nU(       d  M'  US:w  d  M/  UR                  SU-   S-   [        TR	                  S5      S5      -   5        Md     S H¸  n/ n[        R                  " S[        R                  " U5      -  U5       HR  n	U	R	                  S5      (       d  M  U	R	                  S5      U;  d  M2  UR                  U	R	                  S5      5        MT     U(       d  M’  UR                  US-   SR                  U5      -   5        Mº     [        R                  " SS
U[        R                  S9n
[        R                  " SSU
5      n
[        R                  " SS
[        R                  " S S
U
5      5      R                  5       n
U
(       a  UR                  S!U
-   5        U Vs/ s H  o»SU PM	     sn$ s  snf )"ao  Every part of one car's shortlist panel that could say whether
Carwow has that car shortlisted, as short separate lines fit for a
log: the classes on the frame, on the panel and on each marked icon,
the form's own action and method, its fields, any label or wording,
and the words the panel shows. Empty list when the page does not
carry that car's panel.

Why it exists (2026-09-20): the click does reach Carwow now, and
Steven's own shortlist screen proves it lands, yet the page read back
still says the car is not shortlisted. No session away from the Mac
can open a real Carwow listing, so the only honest way to fix the
reading is to have the Mac report a starred car's panel beside an
unstarred one's and build the check on the difference, rather than on
a guess (golden rule 4). Each line is kept short because a log line
is cut at 400 characters on its way to Dealer OS.Nr   zframe classes = Úclassz(none)z<(?:div|span|button|a)\b[^>]*>r   c              3   óp   #   • U  H,  o[         :H  =(       d    UR                  [         S -   5      v •  M.     g7f)z--N)Ú_BOOKMARK_IDENTr“   )r'   r  s     r   r)   Ú!bookmark_marks.<locals>.<genexpr><  s,   é € Ð[ÒUZÐPQ”OÑ#×K q§|¡|´OÀdÑ4JÓ'KÔKÒUZùs   ‚46zpanel classes = r0   z><[^>]*\bdata-listing-bookmark-component-target="([^"]+)"[^>]*>r!   z classes = z<form\b[^>]*>zform = c              3   óª   >#   • U  HH  n[        TR                  S 5      U5      (       d  M%  US-   [        TR                  S 5      U5      -   v •  MJ     g7f)r   Ú=N)rÓ  rO   )r'   ÚkrÒ  s     €r   r)   rØ  A  sF   øé € ð (GÚ1Z¨AÜ+4°S·Y±Y¸q³\À1×+Eó )M¨¨C©´)¸C¿I¹IÀa»LÈ!Ó2LÖ(LÚ1Zùs
   ƒ$A«(A)ÚactionÚmethodzdata-turbo-methodz<input\b[^>]*>r7   Úauthenticity_tokenzinput z = Úvalue)z
aria-labelÚtitlezaria-pressedz0data-listing-bookmark-component-bookmarked-valuerÑ  z | rš   r›   z="[^"]*"rÚ  rÌ  r#   zwords = )rÊ  rÓ  r>   r7  rO   r?   rJ   rM   rL   rÉ  rA   r¹   r   )ró   rÄ   rX  rÍ  rÎ  rS   Únamesr7   rK  r]   Úwordsr�   rÒ  s               @r   Úbookmark_marksrã  %  s¨  ø€ ô  % TÓ6�O€HØÑØˆ	Ø�K�R€EØ¤¨8°WÓ!=×!IÀÑJÐ
K€CÜ�{Š{Ð<¸eÖDˆÜ˜#Ÿ)™) A›,¨Ó0×6Ñ6Ó8ˆÜÑ[ÑUZÓ[×[Ó[Ø�J‰JÐ)¨C¯H©H°U«OÑ;Ö<ñ Eô �{Š{Ð\Ð^cÖdˆØ�
‰
�3—9‘9˜Q“< -Ñ/´9¸S¿Y¹YÀq»\È7Ó3S×3_ÐW_Ñ`Öañ eä�{Š{Ð+¨UÖ3ˆØ�
‰
�9˜sŸx™xô (GÙ1Zó(Gó  Gñ Gö 	Hñ 4ô �{Š{Ð,¨eÖ4ˆÜ˜Ÿ™ 1› vÓ.ˆ÷ ˆ4�DÐ0Õ0Ø�J‰J�x $‘¨Ñ.´¸3¿9¹9ÀQ»<ÈÓ1QÑQÖRñ 5ó lˆØˆÜ—’Ð.´·²¸4³Ñ@À%ÖHˆAØ�w‰w�q�z‹z˜aŸg™g a›j°Õ4Ø—‘˜AŸG™G A›JÖ'ñ I÷ ˆ4Ø�J‰J�t˜e‘| e§j¡j°Ó&6Ñ6Ö7ñ lô �FŠFÐ)¨3°¼R¿T¹TÑB€Eô �FŠF�;  UÓ+€EÜ�FŠF�6˜3¤§¢ z°3¸Ó >Ó?×EÑEÓG€EÞØ�
‰
�: Ñ%Ô&Ù%(Ó)¢S˜T��%‹L¡SÑ)Ð)ùÒ)s   Ì?Mc                 óî   • [        X5      u  p#Uc  g[        R                  " SU5      nU(       d  g[        R                  " SUR                  S5      5      nU(       d  g[        UR                  S5      ;  $ )a�  Is this car on Carwow's own shortlist, read out of a page Carwow
just served? Carwow renders both hearts and hides one with
listing-bookmark-component__icon--hidden, so the active heart without
that class means bookmarked. Scoped to this listing's own turbo
frame, never any other card's. None when the frame or the heart is
not in the html at all, which is "cannot tell", never False.Nz?<[^>]*data-listing-bookmark-component-target="activeIcon"[^>]*>zclass="([^"]*)"r   r!   )rÊ  r>   r@   rO   Ú_BOOKMARK_HIDDEN_CLS)ró   rÄ   Ú_openrÎ  ÚiconÚclss         r   Úbookmarked_in_htmlré  \  sb   € ô " $Ó3�L€EØ�}ØÜ�9Š9ÐWÐY^Ó_€DÞØÜ
�)Š)Ð&¨¯
©
°1«Ó
6€CÞØÜ s§y¡y°£|Ñ3Ð3r   z@https://dealers.carwow.co.uk/dealers/listings/filtered/shortlistc                 óÐ   • [        5       n[        U =(       d    S5       HC  n[        R                  " SU5      nU(       d  M#  UR	                  UR                  S5      5        ME     U$ )a�  Every listing id on Carwow's own shortlisted vehicles page, read
off the same listing cards every other Carwow list is read from
(_card_blocks and data-listing-id). A car is on this page if, and
only if, Carwow has it shortlisted, so this is the real answer to
"is this car starred", and it is the very page Steven looks at.
Pure string work, no browser, so it is unit tested directly.r   r˜   r!   )rT  r–   r>   rF   rW  rO   )ró   rS   rT   r]   s       r   Úshortlisted_ids_in_htmlrë  r  sO   € ô ‹%€CÜ˜dŸj bÖ)ˆÜ�HŠHÐ/°Ó7ˆßˆ1Ø�G‰G�A—G‘G˜A“JÖñ *ð €Jr   c                 ó   • SU =(       d    S;   $ )a*  Could that page be read at all? True when it carries at least one
listing card, whether or not any car is on it. Carwow's own
shortlist page is built from the on sale list's grid and the same
listing-card-component (read from its stylesheet source, which needs
no login), so a page with no card markup anywhere is a page whose
shape has moved, not an empty shortlist. The difference matters:
answering "nothing is shortlisted" to a page that could not be read
would turn every starred car off. Pure string work, no browser, so
it is unit tested directly.zlisting-card-componentr   r³   )ró   s    r   Úshortlist_page_readablerí  �  s   € ð $¨¯
°Ñ3Ð3r   c                 óÞ  • [        5       Sp2[        SUS-   5       GH,  nS[        ;   a  SOSnUS:X  a  [        O[         U SU 3n U R                  R	                  USSS.SS	9nUR                  (       d  [        SUR                   S35      e[        UR                  =(       d    U5      (       a  [        S5      eSUR                  =(       d    U;  a  [        SUR                   S35      eUR                  5       n	[        U	5      n
US:X  a"  U
(       d  [        U	5      (       d  [        S5      eUS-  nX¢-
  (       d    U$ X*-  nGM/     U$ ! [
         a  n[        S
U S35      eSnAff = f)aý  Ask Carwow, right now, which cars are on the shortlist, through
the logged in session with the browser's cache cut out. Every page of
it, since a long shortlist paginates the same way the stock list does.

This replaces reading a heart on the car's own page (2026-09-20).
That was read live on the Mac for a car Steven had confirmed WAS on
his shortlist and one that was not, and the two pages came back
identical to the character: the listing page renders the unstarred
heart for every car whatever the truth, which is why every Carwow
star has been reported as a failure since the sync was built.

Raises rather than guessing when the page cannot be read, so a dead
login can never read as an empty shortlist and turn every starred
car off (golden rules 4 and 5).r   r!   rï   rQ  rR  úno-cache©zCache-ControlÚPragmare   ©Úheadersrh   z+Carwow's shortlist page could not be read (z).Nz!Carwow's shortlist page answered rí   zSCarwow's shortlist page went to a login screen. Run: python3 login.py carwow manualz/filtered/shortlistz/Carwow sent the shortlist page somewhere else (zrCarwow's shortlist page carried no cars and no listing cards at all, so it could not be read. Nothing was changed.)rT  r¼   ÚSHORTLIST_URLr¦  r¸   rt   ru   Úokr8   rN  r™   r�   rë  rí  )r}   rY  ÚidsÚ
pages_readr  r\  r™   r±  Úeró   Úfounds              r   Úshortlisted_idsrú  Ž  s†  € ô “e˜QˆÜ�1�i !‘m×$ˆØœMÓ)‰c¨sˆØ  A›v�m¬m¨_¸S¸EÀÀqÀcÐ+Jˆð	TØ—<‘<×#Ñ# CÀ:Ø<Fñ2HØQVð $ð XˆDð �w�wÜÐ!BÀ4Ç;Á;À-ÈqÐQÓRÐRÜ˜TŸX™XŸ_¨×-Ñ-Üð  Eó Fð Fð
 !¨¯©¯°SÓ9ÜÐ!PÐQU×QYÑQYÐPZÐZ\Ð]Ó^Ð^Ø�y‰y‹{ˆÜ'¨Ó-ˆð �‹6ž%Ô(?À×(EÑ(EÜð  ]ó ^ð ^à�a‰ˆ
Ø�{Øà€Jð 	‰‹ñ? %ð@ €Jøô5 ó 	TÜÐ!LÈQÈCÈrÐRÓSÐSûð	Tús   ÁEÅ
E,ÅE'Å'E,c                 ó€  ^ ^^^• [         R                  S   S    SU 3mSU 3n[        U5      [        T 5      ;   [	        U5      :X  a  gT R                  TSSS9   T R                  [        S	S
9  T R                  U5      R                  5       S:„  nU(       a  T R                  U5      OT mU4S jnU U4S jnSnU(       d  SnO% T R                  [        [        U5      [        /S	S9  [	        U5      (       + n/ mU4S jn	T R                  SU	5         TR                  [        5      R                   R#                  5         [%        S5       H*  n
T R'                  S5        T(       d  M  U" 5       U:X  d  M*    O   T R)                  SU	5        Un[%        S5       H2  n
T R'                  S5        [        U5      [        T 5      ;   nX²:X  d  M2    g   U(       d  SnO+U(       d  SnO!T(       d  SnOSSR+                  T5      -   S-   n[-        U" 5       U5      nU(       d   [-        T R/                  5       U5      n[1        SU SU S U S!U S"3	U(       a  S#U 3OS-   [	        T5      S$9e! [         a    [        SU S35      ef = f! [         a    Sn GN©f = f! T R)                  SU	5        f = f! [         a    Sn N„f = f)%a&  Add or remove one car from Carwow's own shortlist (bookmark), on an
already open page.

Whether the car is starred is asked of Carwow's own shortlisted
vehicles page, the very page Steven looks at, both before the click
and to confirm it (shortlisted_ids). The car's own listing page is
never asked: read live on the Mac 2026-09-20, it renders exactly the
same panel for a car that is on the shortlist and one that is not.

So a car already in the wanted state is never clicked at all, and
otherwise this opens the car's page, waits for Carwow's own star
button to be genuinely live (see this module's note above, a click
before that does nothing whatsoever), clicks it once, and waits for
the shortlist page to agree. Returns True once it does, raises
loudly on anything else, saying which part fell over.r1   r2   r3   zturbo-frame#listing-bookmark-Trd   re   rf   rl   ri   zCarwow listing z‚ did not show its own shortlist button. The page may have changed, the listing may no longer exist, or the login may have expired.r   c                  ó„   >• TR                  [        5      R                  R                  S5      =(       d    Sn [        U ;  $ )NrÕ  r   )ÚlocatorÚ_BOOKMARK_ACTIVE_SELÚfirstÚget_attributerå  )rè  Úwheres    €r   Ú	dom_stateÚ set_watchlist.<locals>.dom_stateè  s4   ø€ Ø�m‰mÔ0Ó1×7Ñ7×EÑEÀgÓN×TÐRTˆÜ#¨3Ñ.Ð.r   c                  ó¨   >•  TR                   R                  TSSS.SS9n U R                  (       a  U R                  5       $ S$ ! [         a     gf = f)z±The listing as Carwow itself serves it right now. A real
request through the logged in session, so the browser's own cache
cannot answer it. Empty when the request did not land.rï  rð  re   rò  r   )r¦  r¸   rõ  r�   rt   )r±  r}   r™   s    €€r   Úserved_pageÚ"set_watchlist.<locals>.served_pageì  s[   ø€ ð	Ø—<‘<×#Ñ# CÀ:Ø<Fñ2HØQVð $ð XˆDà"&§'§'�4—9‘9“;Ð1¨rÐ1øÜó 	Ùð	ús   ƒ>A ÁA Á
AÁAF)Úargrh   c                 ó¢   >• SU R                   =(       d    S;   a5  TR                  U R                  R                   SU R                   35        g g )Nz/listing_bookmarks/r   r0   )r™   rM   r¦  rÝ  r8   )r±  Úposteds    €r   Ú_watchÚset_watchlist.<locals>._watch  s>   ø€ Ø  T§X¡X§^°Ó4Ø�M‰M˜TŸ\™\×0Ñ0Ð1°°4·;±;°-Ð@ÕAð 5r   Úresponseé   rm   é   rj   z2this car's own shortlist panel was not on the pagezRCarwow's own star button never came alive on the page, so nothing could be clickedz the click sent nothing to CarwowzCarwow answered z, z and still shows it the old wayr   z still shows bookmarked=z after trying to set it to r  rí   z Carwow's own panel: rÀ  )r   rQ   r   rú  Úboolrq   rs   Ú_BOOKMARK_BTN_SELrt   ru   rý  ÚcountÚwait_for_functionÚ_BOOKMARK_READY_JSr×  Úonrÿ  Úclickr¼   rv   Úremove_listenerrL   rÏ  ry   r·  )r}   rÄ   ÚwatchedÚ	frame_selÚscopedr  r  ÚreadyÚcurrentr
  Ú_Ú	confirmedÚwhyÚpanel_afterr	  r™   r  s   `             @@@r   Úset_watchlistr   Á  s×  û€ ô  �]‰]˜8Ñ$ ZÑ0Ð1Ð1CÀJÀ<Ð
P€CØ/°
¨|Ð<€Iô 	ˆJ‹œ?¨4Ó0Ñ0´T¸'³]ÓBØØ‡I�IˆcÐ0¸%€IÑ@ð'Ø×ÑÔ0¸%ÐÑ@ð �\‰\˜)Ó$×*Ñ*Ó,¨qÑ0€FÞ'-ˆD�L‰L˜Ô#°4€Eõ/ö	ð €EÞØ‰ð	Ø×"Ñ"Ô#5¼CÀ
»OÌ_Ð;]Ø+0ð #ñ 2ô �w“-Ô€GØ€FõBð 	‡G�GˆJ˜Ôð
1Ø�‰Ô'Ó(×.Ñ.×4Ñ4Ô6ô �r–ˆAØ×!Ñ! #Ô&ßˆv™)›+¨Õ0Ùñ ð
 	×Ñ˜Z¨Ô0ð
 €IÜ�1ŽXˆØ×Ñ˜dÔ#Ü˜
“O¤°tÓ'<Ñ<ˆ	ØÕÙñ	 ö ØB‰ÞØb‰ÞØ0‰à  4§9¡9¨VÓ#4Ñ4Ð7XÑXˆô '¡{£}°jÓA€KÞð	Ü.¨t¯|©|«~¸zÓJˆKô Ø
˜*˜Ð%=¸i¸[ð I%Ø%, I¨R°¨u°Að	7æ4?Ð" ; -Ñ0ÀRñ	Iô �&‹\ñ	ð øôw ó 'ÜØ˜j˜\ð *&ð &ó'ð 	'ð'ûôB ó 	Ø‹Eð	ûð, 	×Ñ˜Z¨Õ0ûôD ó 	ØŠKð	úsI   ÁI' Ã$J ÄAJ Å*	J Å7J ÈJ. É'JÊJÊJÊJ+Ê.J=Ê<J=z^https://dealers.carwow.co.uk/dealers/listings/filtered/offers?bids_and_offers_type=under_offerc           	      ór  • [         R                  " SU 5      nU(       a  UR                  S5      OSn[         R                  " SSU [         R                  S9n[         R                  " SSU[         R                  S9n[         R                  " SS	[
        R                  " U5      5      R                  S	5       Vs/ s H)  oDR                  5       (       d  M  UR                  5       PM+     nnSnU HB  n[        R                  U5      nU(       d  M!  UR                  S
5      R                  SS5      n  O   U(       d  gSn[        U5       HI  u  p”[        R                  U5      (       d  M!  U	S:¼  d  M)  XYS-
     S-   XYS-
     -   R                  5       n  O   Sn
[        U5       H›  u  p”SUR                  5       ;   d  SUR                  5       ;   d  M/  U	S-
  X™S-   4 HY  nS
Us=::  a  [        U5      :  d  M  O  M  [         R                  X[   5      nU(       d  M?  [#        UR                  S5      5      n
  O   U
c  M›    O   U
cA  U H;  n[         R                  U5      nU(       d  M!  [#        UR                  S5      5      n
  O   SXhU
SUU(       a  [$        R&                  S   S    SU 3S.$ SS.$ s  snf )zÕOne card from the Under offer tab, reusing _card_blocks' own
splitting (the same real listing-card-component every other Carwow
card on this site uses). Returns None for a card with no readable
reg, never guessed.r˜   r!   r   rš   r0   r›   r�   r#   r$   r   Nr   ÚbidÚofferr/   r1   r2   r3   )r4   r6   r7   Úmax_bidÚnoted_atr5   r=   )r>   rF   rO   rA   r¹   rB   rC   r?   r   rº   r@   r   rD   rE   r   rG   rN   r   r   rQ   )rT   rÃ   rÄ   r•   rV   rW   r6   r]   r7   rÉ   r$  r\   rÊ   s                r   Ú_parse_under_offer_cardr&  R  sX  € ô
 �(Š(Ð-¨uÓ
5€CÞ!$�—‘˜1”¨"€JÜ
�ŠÐ% s¨E¼¿¹Ñ>€AÜ
�ŠÐ+¨S°!¼2¿4¹4Ñ@€AÜ "§¢ z°4¼×9IÒ9IÈ!Ó9LÓ M× SÑ SÐTXÔ YÓgÒ Y˜1×]dÑ]d×]f‹YˆQ�W‰WŽYÑ Y€EÐgà
€CÛˆÜ�L‰L˜‹Oˆßˆ1Ø—'‘'˜!“*×$Ñ$ S¨"Ó-ˆCÙñ	 ö
 Øð
 €DÜ˜%Ö ‰ˆÜ�:‰:�a�=‹=˜Q !�VØ˜a™%‘L 3Ñ&¨°1©u©Ñ5×<Ñ<Ó>ˆDÙñ !ð €GÜ˜%Ö ‰ˆØ�A—G‘G“IÓ ¨A¯G©G«IÕ!5Ø˜!‘e˜Q A¡Ó&�Ø˜Õ&œC ›J×&Ñ&ÜŸ™ e¡hÓ/�Bß�rÜ"& r§x¡x°£{Ó"3˜Ùñ 'ð Ó"Ùñ !ð �ÛˆAÜ—‘˜a“ˆBßˆrÜ˜rŸx™x¨›{Ó+�Ùñ	 ð  SÀ7Ø zÞakœ'Ÿ-™-¨Ñ1°*Ñ=Ð>Ð>PÐQ[ÐP\Ð]ñð ð rtñð ùòU hs   Â9J4ÃJ4c                 ó  • [         R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       nUR	                  [
        SSS9  UR                  S5        [        UR                  5      (       a  [        S5      eUR                  5       nUR                  5         / n[        U5       H(  n[        U5      nU(       d  M  UR                  U5        M*     U$ ! UR                  5         f = f)	a�  Read Bids & offers > Under offer: every real bid Carwow currently
shows as waiting on the seller, or where the reserve has not been met
yet, so never a confirmed purchase, never read from the won screen at
all. Read only, never acts. An empty result is the real, current,
confirmed state of this account (see this module's own note above),
never treated as a failure the way a changed page would be.r1   rb   r   r"  re   rf   iÜ  zwCarwow did not show the Bids & offers screen, the login may have expired. Run: python3 login.py carwow, then try again.)r   rn   ro   rp   rq   ÚUNDER_OFFER_URLrv   rN  r™   ru   ry   rz   r–   r&  rM   )r{   rc   r|   r}   ró   rS   rT   Úparseds           r   Úread_under_offerr*  Œ  sÕ   € ô ×
%Ò
% j°(ÀXÑ
N€Cð
Ø"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆØ�	‰	”/¨mÀUˆ	ÑKØ×Ñ˜dÔ#Ü˜TŸX™X×&Ñ&ÜðIóJð Jð �|‰|‹~ˆà�	‰	ŒØ
€CÜ˜dÖ#ˆÜ(¨Ó/ˆßˆ6Ø�J‰J�vÖñ $ð €Jøð 	�	‰	�ús   ˜BC. Ã.D c                 ó¤  • SSK Jn  U R                  USSS9  [        U R                  =(       d    S5      (       a  UR                  SU5      $  U R                  SS	S
9   U R                  S5      n U R                  5       n[        R                  " SU=(       d    S5      nU(       aL  [        XVR                  S5      5      nUR                  SUU=(       d    SUR                  S5      -   S-   /5        UR                  XE5      nUR!                  5       US'   X(S'   U$ ! [         a     NÌf = f! [         a    Sn NÌf = f! [         a    Sn NÍf = f)züThe shortlist deep read of one Carwow car page (2026-09-17): its
words and its gallery, handed to bidbrain.deep_read.from_carwow for
the push's deep block. A login page instead of the car is an answer
with an error, never blank. Reads only, never acts.r   )Ú	deep_readrd   re   rf   r   z8Carwow login needed. Run: python3 login.py carwow manualr"  r#  ri   r$  z/dealers/listings/(\d+)r!   zcarwow shortlist panelzno shortlist panel for listing z in the pageÚread_atÚasked_at)Úbidbrainr,  rq   rN  r™   Úfailedr*  rt   r+  ry   r>   r@   rã  rO   Ú	log_notesÚfrom_carwowÚ	now_stamp)	r}   r=   r.  Ú_deeprâ  ró   rÃ   ÚmarksÚds	            r   r,  r,  §  sD  € õ
 ,Ø‡I�IˆkÐ&8À%€IÑHÜ˜Ÿ™Ÿ R×(Ñ(Ø�|‰|ÐVÐX`ÓaÐaðØ× Ñ  ¸Ð Ñ>ðØ—‘ Ó'ˆðØ�|‰|‹~ˆô �)Š)Ð.°×0A¸rÓ
B€CÞ
Ü˜t§Y¡Y¨q£\Ó2ˆØ�‰Ð0°+Ø×dÐ"CÀcÇiÁiÐPQÃlÑ"RÐUcÑ"cÐ!dô	fà×Ñ˜%Ó&€AØ—?‘?Ó$€A€i�LØ€j�MØ€Høô+ ó Ùðûô ó ØŠðûô ó ØŠðús6   ÁD ÁD. Á1E  Ä
D+Ä*D+Ä.D=Ä<D=Å EÅE)T)Né   N)Nr7  r&   )r7  )WrÅ  r>   Úbidbrain.readersr   r  r´   r©  r§  ró   rB   Úpricingr   r   r   Úcompilerº   rE   rð   r»   rN   r¾   rI   r   r   ÚWON_URLrr   rP   Ú_SOLD_FOR_REr`   rƒ   rŽ   r‘   r–   rÑ   rú   rù   r  r  rå   ré   rë   rô   rþ   rø   r  r   rF  rH  r;  rN  rb  rd  rh  rq  ru  rt  r�  rD   r   rŽ  r‘  r—  r™  r¬  rµ  r×  r  rþ  rå  Ú_BOOKMARK_MARK_CHARSr  ru   r·  rÊ  rÏ  rÓ  rã  ré  rô  rë  rí  rú  r   r(  r&  r*  r,  )rÉ   r]   s   00r   Ú<module>r>     sŒ  ðñó 
å *Û 	Û Û Û Û Ý Ý à
�
Š
Ð2Ó3€Ø	‡z‚zÐ+Ó,€Ø�:Š:Ð.°·±Ó5€Ø
�
Š
Ð#Ó$€Ø�:Š:Ð7Ó8€Ú]€ò+ò.ð G€ð Ð9Ñ9€Ø�JŠJÐ:Ó;€	Ø�zŠz˜+Ó&€òHôV1òhò*ò"Cò
IðX 6€Ø
W€ð aÐ Ø]Ð òð —:’:ÐWÐY[×Y]ÑY]Ó^Ð òò&ò. òFð0 Ð òò*jðZ —J’JÐUÓVÐ òò#ô 0+ôf
ò0ð2 —*’*Ð?Ó@€ò(+òVhð@ —j’jØQóSÐ á$-ÚXÐZ[ô%]ô ^ò %]™D˜Aˆ1�7‰7‹9�aŠ<ñ %]ò ^€ò")òJ	Cò+ð( @Ð ô#ðz /€ØVÐ ØNÐ ØAÐ ð Ð ðÐ ô	˜<ô 	ò	;ò	<ò#ð
 ,@ô 4*òn4ð& S€òò
4ô0òf{ðZ8€ò7ôtõ6 ùóY^s   Å4G.