ó
    £»j¹¯ ã                   óà  • S r SSKrSSKrSSKJr  \R
                  " S5      rS 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  SS	KJr  \R
                  " S
5      r\R
                  " S5      r\R
                  " S5      r\R
                  " S\R,                  5      r\R
                  " S5      r1 Skr1 SkrS r\R
                  " S\R,                  5      rS rS rS rSr S r!SSSSSS.r"SSS S!S".r#S# r$S$ r%S%r&S&r'S'r(S( r)S) r*S* r+S+ r,S, r-/ r.S}S- jr/S. r0S/r1S0r2\R
                  " S15      r3S2r4\R
                  " S3\R,                  5      r5\R
                  " S45      r6\7" / S5QS65       V Vs0 s H  u  pURq                  5       U _M     snn r9S~S7 jr:S8 r;SS9 jr<SS: jr=S; r>S< r?\R
                  " S=5      r@S> rAS? rB\R
                  " S@5      rCSA rD\R
                  " SB\RŠ                  5      rFSC rG\R
                  " SD\RŠ                  5      rHSE rISFrJSG rKSH rLSIrM\R
                  " SJ5      rN\R
                  " SK5      rOSL rPSM rQSN rRS€SO jrSSPrTSQ rUSR rVS�SS jrWSTrXSU rYS€SV jrZ\R
                  " SW\R,                  5      r@\R
                  " SX5      r[\R
                  " SY\R,                  5      r\SZ r]S[ r^S\ r_S] r`\R
                  " S^\R,                  5      raS_ rbS` rc\R
                  " Sa5      rdSb reSc rf\R
                  " Sd\R,                  5      rgSe rhSf riSg rjSh rkSi rlSjrmSk rnSl roSm rpSn rqSo rrS‚Sp jrs0 rtSq ruSr rvSs rwSƒSt jrxSu ryS„Sv jrzS…Sw jr{Sx r|S~Sy jr}Szr~S{ rS~S| jr€gs  snn f )†ai  
Motorway reader.

Reads the logged in dealer stock list at pro.motorway.co.uk/vehicles and turns
each card into a Car. The list card carries registration, make, model and
derivative, year, mileage, fuel, transmission, distance, condition grade,
reserve price, the photo and the listing link.

Some fields are not on the list card and live on each car's own page: CAP Clean,
previous owners, service history and VAT status, and the exact engine. Those are
fetched by visiting the car's page, but only for cars worth the look, so the run
stays quick. See read_live below.

Nothing here bids. It reads the screen only.
é    N)Úfinance_linesz[A-Z0-9]{1,4}\s?[A-Z0-9]{1,4}c                 óH  • U =(       d    SR                  5       n[        R                  U5      (       d  gSR                  UR	                  5       5      nS[        U5      s=:*  =(       a    S:*  Os  =(       a+    [        S U 5       5      =(       a    [        S U 5       5      $ )NÚ Fé   é   c              3   ó@   #   • U  H  oR                  5       v •  M     g 7f©N)Úisdigit©Ú.0Úcs     Ú:/Users/stevendouglas/BidBrain/bidbrain/readers/motorway.pyÚ	<genexpr>Ú#looks_like_plate.<locals>.<genexpr>%   s   é € Ð&AºD°q§y¡y§{ {ºDùó   ‚c              3   ó@   #   • U  H  oR                  5       v •  M     g 7fr	   )Úisalphar   s     r   r   r   %   s   é € ÐIdÒ_cÐZ[Ï)É)Ï+È+Ò_cùr   )ÚstripÚ	_PLATE_REÚ	fullmatchÚjoinÚsplitÚlenÚany)ÚtextÚtÚbares      r   Úlooks_like_plater       st   € Ø	��×ÑÓ€AÜ×Ñ˜q×!Ñ!ØØ�7‰7�1—7‘7“9Ó€DØ”�D“	×Ó˜QÔ×d¤3Ñ&A¹DÓ&AÓ#A×dÄcÑIdÑ_cÓIdÓFdÐdó    r   )ÚCar)Úbrowser)Úis_platez\b[A-Z]{2}\d{2}\s?[A-Z]{3}\bz\b(19[89]\d|20[0-4]\d)\bz^([\d,]+)\s*mi$z^(\d+)\s*mi away$u   Â£\s*([\d,]+)>   ÚdieselÚhybridÚpetrolÚelectricúpetrol hybridúplug-in hybrid>   ÚcvtÚmanualÚ	automaticúsemi automaticúsemi-automaticc                 ó    • U c  g [        U 5      R                  SS5      R                  5       n U R                  5       (       a  [	        U 5      $ S $ )NÚ,r   )ÚstrÚreplacer   r
   Úint)Úss    r   Ú_numr4   7   s@   € Ø�yØÜˆA‹�‰�s˜BÓ×%Ñ%Ó'€AØ—Y‘Y—[‘[Œ3ˆq‹6Ð* dÐ*r   z'<a\b[^>]*?href="(/vehicles/\d+)"[^>]*?>c                 ó:  • [        [        R                  U 5      5      n/ n[        U5       Hk  u  p4UR	                  5       nUS-   [        U5      :  a  XS-      R                  5       O
[        U 5      nUR                  UR                  S5      XU 45        Mm     U$ )z Yield (listing_path, block_html) for each vehicle card, the link taken
from that card's own wrapping anchor so a card is never paired with the
next card's link.é   )	ÚlistÚ
_ANCHOR_REÚfinditerÚ	enumerateÚendr   ÚstartÚappendÚgroup)Ú	page_htmlÚanchorsÚoutÚiÚmr<   r;   s          r   Ú_card_blocksrD   E   sˆ   € ô ”:×&Ñ& yÓ1Ó2€GØ
€CÜ˜'Ö"‰ˆØ—‘“ˆØ()¨A©´°G³Ó(<ˆg˜!‘e‰n×"Ñ"Ô$Ä#ÀiÃ.ˆØ�
‰
�A—G‘G˜A“J 	°Ð 4Ð5Ö6ñ #ð €Jr   c                 ó$  • [         R                  " SSU 5      n[        R                  " U5      n[         R                  " SSU5      nUR	                  S5       Vs/ s H)  o"R                  5       (       d  M  UR                  5       PM+     sn$ s  snf )u±   The card's text as short lines: one per tag, and (2026-09-08) one
per bullet too, since a card now writes its facts on one line as
"54,080 mi â€¢ Diesel â€¢ Manual â€¢ Grade 3".z<[^>]+>Ú
u   \s*[â€¢Â·|]\s*)ÚreÚsubÚhtmllibÚunescaper   r   )Úblockr   Úls      r   Ú_text_linesrM   R   sj   € ô �6Š6�*˜d EÓ*€DÜ×Ò˜DÓ!€DÜ�6Š6Ð# T¨4Ó0€DØ#Ÿz™z¨$Ô/Ó=Ò/˜!·7±7·9‹IˆA�G‰GŽIÑ/Ñ=Ð=ùÒ=s   ÁBÁ6Bc                 óˆ  • / n[        U 5       GH&  u  p#U(       d  M  UR                  SS5      S   n[        R                  " SU5      n[        R                  " SU5      nU(       a%  [        R
                  " UR                  S5      5      OSn[        U5      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S	nU Hy  n
[        R                  U
5      nU(       a  Uc  [        UR                  S5      5      n[        R                  U
5      nU(       d  MZ  Ub  M_  [        UR                  S5      5      nM{     S	n[        U5       Hˆ  u  nn
U
R                  5       S
:X  a%  US-   [!        U5      :  a  [        UUS-      5      n  OM[        R                  " SU
[        R"                  5      nU(       d  Mn  [%        UR                  S5      5      n  O   S	n[        U5       H€  u  nn
SU
R                  5       ;   d  M  ['        U[)        US-   [!        U5      5      5       H>  n[*        R                  UU   5      nU(       d  M$  [        UR                  S5      5      n  O     O   S=n=nnS	nU(       aÙ  [,        R                  U5      nU(       ah  [        UR                  S5      5      nUS	UR/                  5        R1                  5       nUUR3                  5       S	 R1                  5       nU=(       d    UnOUnUR5                  5       nU(       a<  US   n[!        U5      S:”  a  US   n[!        U5      S:”  a  SR7                  USS	 5      nSnU H  n
U
R                  5       [8        ;   d  M  U
n  O   Sn U H  n
U
R                  5       [:        ;   d  M  U
n   O   [=        U	UUUUUUUUUU U(       a  UR                  S5      OS[>        R@                  S   S   U-   SU(       a  UOUS9n!UU!l!        UU!l"        URG                  U!5        GM)     [I        U5      $ )z�Turn the saved or live stock page HTML into a list of Car objects with
the fields the list card provides. Detail only fields stay None for now.Ú/r6   éÿÿÿÿz<img[^>]*\balt="([^"]+)"z<img[^>]*\bsrc="([^"]+)"r   r   Ú NÚgradezgrade\s*(\d)$zreserve priceé   r   ÚmotorwayÚbase_urlÚMotorway)ÚregÚmakeÚmodelÚ
derivativeÚyearÚmileagerR   ÚreserveÚdistance_milesÚfuelÚtransmissionÚ	photo_urlÚlisting_urlÚsourceÚengine)%rD   ÚrsplitrG   ÚsearchrI   rJ   r>   rM   ÚPLATEr1   ÚMILEAGEÚmatchr4   ÚDISTANCEr:   Úlowerr   ÚIr2   ÚrangeÚminÚMONEYÚYEARr<   r   r;   r   r   ÚFUELSÚ	GEARBOXESr    r!   ÚSITESÚ_vidÚ_fuelr=   Úmerge_card_copies)"r?   ÚcarsÚlisting_pathrK   ÚvidÚaltÚsrcÚalt_textÚlinesrW   rL   Úmmr\   ÚdistanceÚmiÚdirR   rB   Úmgr]   ÚjÚmorX   rY   rZ   r[   ÚymÚbeforeÚafterÚalt_text_nodateÚtoksr_   r`   Úcars"                                     r   Úparse_listingr‹   \   s¿  € ð €DÜ+¨I×6ÑˆæÙØ×!Ñ! # qÓ)¨"Ñ-ˆô �iŠiÐ3°UÓ;ˆÜ�iŠiÐ3°UÓ;ˆÞ58”7×#Ò# C§I¡I¨a£LÔ1¸bˆä˜EÓ"ˆàˆÛˆAÜ—‘˜a“ˆBßˆrØ—h‘h˜q“k×)Ñ)¨#¨rÓ2�Ùñ	 ð ˆØˆÛˆAÜ—‘˜qÓ!ˆBÞ�g‘oÜ˜rŸx™x¨›{Ó+�Ü—‘ Ó"ˆBßˆr�hÓ&Ü §¡¨£Ó,’ñ ð ˆÜ˜eÖ$‰DˆAˆqØ�w‰w‹y˜GÓ#¨¨A©´°E³
Ó(:Ü˜U 1 q¡5™\Ó*�ÙÜ—’Ð*¨A¬r¯t©tÓ4ˆBßˆrÜ˜BŸH™H Q›KÓ(�Ùñ %ð ˆÜ˜eÖ$‰DˆAˆqØ !§'¡'£)Õ+Ü˜q¤# a¨!¡e¬S°«ZÓ"8Ö9�AÜŸ™ e¨A¡hÓ/�Bß�rÜ"& r§x¡x°£{Ó"3˜Ùñ	 :ñ
 ñ %ð %'Ð&ˆÐ&ˆu�zØˆÞÜ—‘˜XÓ&ˆBÞÜ˜BŸH™H Q›KÓ(�ð
 " , B§H¡H£JÐ/×5Ñ5Ó7�Ø  §¡£ Ð+×1Ñ1Ó3�Ø"(§/¨E‘à"*�Ø"×(Ñ(Ó*ˆDÞØ˜A‘w�Ü�t“9˜q“=Ø  ™G�EÜ�t“9˜q“=Ø!$§¡¨$¨q¨r¨(Ó!3�JàˆÛˆAØ�w‰w‹yœEÕ!Ø�Ùñ ð ˆÛˆAØ�w‰w‹yœIÕ%Ø �Ùñ ô ØØØØ!ØØØØØ#ØØ%Þ'*�s—y‘y ”|°ÜŸ™ jÑ1°*Ñ=ÀÑLØÞ"&‘J¨Jñ
ˆð$ ˆŒØˆŒ	Ø�‰�C×ñk  7ôn ˜TÓ"Ð"r   )rW   rX   rY   rZ   r[   r\   rR   r]   r^   ra   r_   r`   rd   c           
      óè  • 0 n/ nU  HÏ  n[        USS5      =(       d    UR                  nUR                  U5      nUc  X1U'   UR                  U5        MN  [         H>  n[        XVS5      (       a  M  [        X6S5      (       d  M)  [        XV[        X65      5        M@     [        USS5      (       a  Mª  [        USS5      (       d  M¾  UR                  Ul        MÑ     U Vs/ s H  oqU   PM	     sn$ s  snf )aã  One car per vehicle id. Since 2026-09-08 a Motorway card wraps its
picture and its title in separate links to the same vehicle, so the
anchor based split read each car two or three times, each copy with
part of the facts (the plate on one, the mileage on another), and the
list carried 83 "cars" for 40 odd real ones, most of the copies with
no plate and so held back. The copies are folded into one car, the
first copy's facts kept and the gaps filled from the others. Pure,
tested.rt   Nru   r   )Úgetattrrb   Úgetr=   Ú_CARD_FIELDSÚsetattrru   )rw   Úby_idÚorderrŠ   ry   ÚfirstÚfÚvs           r   rv   rv   à   sÎ   € ð €EØ€EÛˆÜ�c˜6 4Ó(×;¨C¯O©OˆØ—	‘	˜#“ˆØ‰=Ø�#‰JØ�L‰L˜ÔÙßˆAÜ˜5 T×*Ó*¬w°s¸t×/DÓ/DÜ˜¤'¨#£/Ö2ñ ô �u˜g r×*Ó*¬w°s¸GÀR×/HÓ/HØŸ)™)ˆEŽKñ ñ $Ó$še˜�!ŒH™eÑ$Ð$ùÒ$s   ÃC/)ÚreservePriceÚreserve_pricer]   ÚpriceReserveÚminimumPriceÚstartingPrice)r\   ÚodometerÚodometerReading)r[   ÚyearOfManufactureÚregistrationYearÚmanufactureYear)ÚnumericGraderR   ÚconditionGrade)ÚdistanceMilesr   ÚdistanceFromDealer)r]   r\   r[   rR   r^   )r_   ÚfuelTypeÚ	fuel_type)r`   ÚtransmissionTypeÚgearbox)ÚbodyTypeÚ	bodyStyleÚ	body_type)ÚtownÚcityÚpostTownÚlocalityÚdisplayLocationÚpostcodeÚoutwardCode)r_   r`   rª   Úlocationc                 ó  • [        U [        5      (       a  g[        U [        5      (       a  U R                  5       =(       d    S$ [        U [        5      (       a.  S H(  n[        U R                  U5      5      nU(       d  M&  Us  $    g)zA short piece of text out of a page data value: a string, or a
small dict's displayValue, value, name or label. None otherwise.N)ÚdisplayValueÚvalueÚnameÚlabel)Ú
isinstanceÚboolr0   r   ÚdictÚ_text_inrŽ   )r•   Úkr   s      r   r»   r»     sj   € ô �!”T×ÑØÜ�!”S×ÑØ�w‰w‹y× ˜DÐ Ü�!”T×ÑÛ;ˆAÜ˜Ÿ™˜q›Ó"ˆAßˆqØ’ñ <ð r   c                 ó$  • / nU R                   (       dš  [        [        US5      5      =(       d1    [        [        US5      5      =(       d    [        [        US5      5      nU(       aF  [        U5      (       a6  UR	                  SS5      R                  5       U l         UR                  S5        U R                  (       a-  [        R                  U R                  =(       d    S5      (       a3  [        [        US5      5      nU(       a  X@l        UR                  S5        U R                  (       a-  [        R                  U R                  =(       d    S5      (       a3  [        [        US	5      5      nU(       a  XPl
        UR                  S
5        U$ )aÊ  The car's registration, make and model from its own page data
(mainInfo.prettyVrm, make, model) where the card gave none: since
2026-09-08 the card text carries no plate and its caption can start
with the year, and a car with no plate cannot be valued by Cazana or
Glass's, so 90 of 91 held Motorway cars that day were held for nothing
but a missing plate. A year where the make should be counts as none.
Pure over the data, tested. Returns the names filled.Ú	prettyVrmÚvrmÚ
sellingVrmrQ   r   zreg from prettyVrmrX   zmake from makerY   zmodel from model)rW   r»   Ú_find_firstr"   r1   Úupperr=   rX   rp   r   rY   )rŠ   ÚdataÚfilledr¿   rX   rY   s         r   Úfill_identity_from_datarÅ     s  € ð €FØ�7�7ô œ D¨+Ó6Ó7÷ <¼8ÄKÐPTÐV[ÓD\Ó;]÷ <Üœ; t¨\Ó:Ó;ð 	æ”8˜C—=‘=Ø—k‘k # rÓ*×0Ñ0Ó2ˆCŒGØ�M‰MÐ.Ô/Ø�8�8”t—~‘~ c§h¡h§n°"×5Ñ5Üœ D¨&Ó1Ó2ˆÞØŒHØ�M‰MÐ*Ô+Ø�9�9œŸ™ s§y¡y§°B×7Ñ7Üœ T¨7Ó3Ó4ˆÞØŒIØ�M‰MÐ,Ô-Ø€Mr   )	Ú	onFinanceÚfinanceAgreementsCountÚhasOutstandingFinanceÚoutstandingFinanceÚfinanceOutstandingÚ
isFinancedÚ
hasFinanceÚoutstanding_financeÚfinance)ÚyesÚtrueÚoutstandingÚfinancedÚy)ÚnoÚfalseÚnoneÚclearznot outstandingz
no financeÚnc                 óx  • [        U [        5      (       a  U $ [        U [        [        45      (       a  U S:„  $ [        U [        5      (       a5  U R                  5       R                  5       nU[        ;   a  gU[        ;   a  gg[        U [        5      (       a$  S H  nX ;   d  M
  [        X   5      nUc  M  Us  $    g)zÀTrue, False or None from one page data value: a bool, a yes/no
word, a number (an amount outstanding), or a small dict carrying one
of those under outstanding, hasOutstanding, status or value.r   FTN)ÚhasOutstandingrÑ   ÚisOutstandingÚstatusrµ   Úamount)r¸   r¹   r2   Úfloatr0   r   rk   Ú_NOÚ_YESrº   Ú_finance_value)r•   r3   r¼   Úrs       r   rá   rá   J  sž   € ô �!”T×ÑØˆÜ�!”cœ5�\×"Ñ"Ø�1‰uˆÜ�!”S×ÑØ�G‰G‹I�O‰OÓˆØ”‹8ØØ”‹9ØØÜ�!”T×ÑÛ`ˆAØ�vÜ" 1¡4Ó(�Ø“=Ø’Hñ	 að
 r   c                 ób   • [          H%  n[        X5      nUc  M  [        U5      nUc  M#  Us  $    g)z´Whether the car has outstanding finance, as the page data says it
(Steven 2026-09-09: "make the answer come from the auction house").
None when the page does not say. Pure, tested.N)ÚFINANCE_KEYSrÁ   rá   )rÃ   Úkeyr•   râ   s       r   Úfinance_from_dataræ   b  s9   € ÷ ˆÜ˜Ó"ˆØ‰9ÙÜ˜1ÓˆØ‹=ØŠHñ ð r   c                 ó®  • / n[        US5      [        US5      S.n[        R                  5        H¡  u  pE[        XS5      (       a  M  U Hƒ  nSn[	        UR                  U5      [        5      (       a  [        [        X4   U5      5      nUc  [        [        X5      5      nU(       d  M`  [        XU5        UR                  U SU 35          MŸ     M£     U$ )a  Fill the text facts a card did not give (fuel, gearbox, body, the
seller's town) from the car's own page data, under the names in
TEXT_FACT_KEYS, only where the car has none (pure over the data,
tested). The seller's own block is searched first for the town, so a
dealership address elsewhere in the page never reads as the car's,
and the specification block first for the body, which it spells the
way the filters expect ("Hatchback", "SUV") where the gallery block
carries a lower case copy. Returns the names filled.ÚsellerÚspecification)r²   rª   Nú from )
rÁ   ÚTEXT_FACT_KEYSÚitemsr�   r¸   rŽ   rº   r»   r�   r=   )rŠ   rÃ   rÄ   r“   ÚfieldÚkeysrå   r•   s           r   Úfill_text_facts_from_datarï   p  sÃ   € ð €FÜ$ T¨8Ó4Ä;ÈtÐUdÓCeÑf€EÜ%×+Ñ+Ö-‰ˆÜ�3˜t×$Ñ$ÙÛˆCØˆAÜ˜%Ÿ)™) EÓ*¬D×1Ñ1Üœ[¨©°sÓ;Ó<�Ø‰yÜœ[¨Ó3Ó4�ßˆqÜ˜ AÔ&Ø—‘   v¨c¨UÐ3Ô4Úó ñ .ð €Mr   c                 óž  • [        U [        5      (       a  g[        U [        [        45      (       a  [        U 5      $ [        U [        5      (       aJ  [
        R                  " SU 5      nU(       a*  [        UR                  S5      R                  SS5      5      $ S$ [        U [        5      (       a  S H  nX ;   d  M
  [        X   5      s  $    g)z€A whole number out of a value Motorway's page data might hold: a
number, a numeric string, or a small dict with value or amount.Nú\d[\d,]*r   r/   r   )rµ   rÝ   ÚcountÚmiles)r¸   r¹   r2   rÞ   r0   rG   rf   r>   r1   rº   Ú
_number_in)r•   rC   r¼   s      r   rô   rô   ‹  s    € ô �!”T×ÑØÜ�!”cœ5�\×"Ñ"Ü�1‹vˆÜ�!”S×ÑÜ�IŠI�k 1Ó%ˆÞ34Œs�1—7‘7˜1“:×%Ñ% c¨2Ó.Ó/Ð>¸$Ð>Ü�!”T×ÑÛ6ˆAØ�vÜ! !¡$Ó'Ò'ñ 7ð r   c           	      ó  • / n[         R                  5        Hr  u  p4[        XS5      b  M  U HX  n[        [	        X5      5      nUc  M  US:”  d  M$  [        XUS:X  a  [        U5      OU5        UR                  U SU 35          Mp     Mt     U$ )zìFill the list level facts a card did not give (reserve, mileage,
year, grade, distance) from the car's own page data, under the names
in LIST_FACT_KEYS, only where the car has none (pure over the data,
tested). Returns the names filled.Nr   r^   rê   )ÚLIST_FACT_KEYSrì   r�   rô   rÁ   r�   rÞ   r=   )rŠ   rÃ   rÄ   rí   rî   rå   rØ   s          r   Úfill_list_facts_from_datar÷   œ  s‹   € ð
 €FÜ%×+Ñ+Ö-‰ˆÜ�3˜tÓ$Ñ0ÙÛˆCÜœ; tÓ1Ó2ˆAØ‹}  Q¥Ü˜°Ð9IÓ0I¤E¨!¤HÈqÔQØ—‘   v¨c¨UÐ3Ô4Úó ñ .ð €Mr   c                 óX	  ^^^• [         (       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  SmS[        R                  R                  5       R                  SS	9 S
UR                  =(       d    UR                   3/mUUU4S jmT" U S5        TR                  SU(       a  SR                  U5      OS-   5        TR                  SUR                   SUR                   SUR                    SUR"                   SUR$                   3
5        TR                  SUR&                  < SUR(                  < SUR*                  < SUR,                  < SUR.                  < 3
5        TR                  SUR                  < SUR0                  < SUR2                  < SUR4                  < SUR                   < 3
5        TR                  SUR6                  < 35        TR                  S[9        U 5      < 35         Ub  UOSnUR;                  S 5       Vs/ s HC  n[<        R>                  " S!U[<        R@                  5      (       d  M0  URC                  5       SS" PME     nnTR                  S#[E        U5      < S$3U(       a  S%R                  USS& 5      OS'-   5        S)SK$n
[K        U S*5      nTR                  Ub  S+U
RM                  U5      SS, -   OS-5        S. H<  n[K        X5      nTR                  U S/3Ub  U
RM                  U5      SS0 OS'-   5        M>     S1 H<  n[K        X5      nUc  M  TR                  S2U S3U
RM                  U5      SS4  35        M>     [O        [        R                  R                  US55      S6S7S89 nURQ                  S R                  T5      S -   5        SSS5        gs  snf ! [F         a  n	TR                  S(U	 35         Sn	A	GN>Sn	A	ff = f! , (       d  f       g= f! [F         a     gf = f)9a
  Once per run, write every path in one car's page data whose name
says reserve, price, grade, mileage, year, distance or owner, with its
plain value, to data/logs/motorway_detail.log, so the names Motorway
really uses are on record and the next fix needs no guessing.NTrÃ   Úlogs©Úexist_ok)r]   ÚpricerR   r\   r[   r   ÚownerÚauctionÚbuyingÚsaler_   r`   r§   Úbodyr«   r¬   r°   r²   rè   ÚservicerÎ   ÚhpirÑ   ÚsettleÚcapÚ	valuationÚcleanÚretailÚguidezMotorway detail keys Úseconds©Útimespecz for c           	      óæ  >^• [        T5      S:”  a  g [        U [        5      (       a�  U R                  5        Hz  u  mnU(       a  U ST 3OTn[        U[        [        45      (       a
  T" X#5        M;  [        U4S jT 5       5      (       d  MW  TR                  SU S[        U5      S S  35        M|     g [        U [        5      (       a&  [        U S S 5       H  u  pBT" X! SU S	35        M     g g )
Ni  Ú.c              3   óH   >#   • U  H  oTR                  5       ;   v •  M     g 7fr	   ©rk   )r   Úwr¼   s     €r   r   Ú2_detail_keys_once.<locals>.walk.<locals>.<genexpr>É  s   øé € Ð;²U° !§'¡'£)ž^²Uùó   ƒ"ú  z = é<   é   Ú[Ú])	r   r¸   rº   rì   r7   r   r=   r0   r:   )	ÚoÚpathr•   ÚprB   r¼   r}   ÚwalkÚwordss	        @€€€r   r  Ú_detail_keys_once.<locals>.walkÁ  sÑ   ù€ Ü�5‹z˜CÓØÜ˜!œT×"Ñ"ØŸG™GžI‘D�A�qÞ)-˜4˜&  ! ™°1�AÜ! !¤d¬D \×2Ñ2Ù˜Qž
ÜÔ;±UÓ;×;Ó;ØŸ™ r¨!¨¨C´°A³°s¸°¨}Ð%=Ö>ò &ô ˜Aœt×$Ñ$Ü% a¨¨ eÖ,‘D�AÙ˜˜f A a S¨˜NÖ+ò -ð %r   r   zfilled: ú, Únothingzcar now: reserve z grade z year z	 mileage z
 distance zcar text: fuel z transmission z body z
 location z	 service zcar identity: reg z make z model z derivative zcar finance: zcap clean from page data: rF   z\bcap\béx   zcap clean from page words: z; lines naming CAP: z || é   rÖ   zpage words not read: r   Úbiddingzbidding block: éÜ  zbidding block: none)r²   ré   ÚmainInfoz block: i°  )r–   r]   r˜   r™   rš   rü   Ú
guidePriceÚaskingPricezfirst ú: éÈ   zmotorway_detail.logr  úutf-8©Úencoding))Ú_detail_keys_writtenr=   Úosr  ÚdirnameÚabspathÚ__file__r   ÚmakedirsÚdatetimeÚnowÚ	isoformatrW   rb   r]   rR   r[   r\   r^   r_   r`   rª   r²   Úservice_historyrX   rY   rZ   rÎ   Úfind_cap_cleanr   rG   rf   rl   r   Úcap_clean_in_textÚ	ExceptionÚjsonrÁ   ÚdumpsÚopenÚwrite)rÃ   rŠ   rÄ   Úpage_text_for_logÚrootrù   r  rL   ÚhitsÚeÚ_jsonr#  r¶   rK   rå   r•   r”   r}   r  r  s                    @@@r   Ú_detail_keys_oncerC  ±  sg  ú€ ÷
 ÒØÜ×Ñ Ô%ð1Ü�w‰w�‰œrŸw™wŸ™¬r¯w©w¯©¼r¿w¹w¿¹ÌxÓ?XÓ/YÓZÓ[ˆÜ�w‰w�|‰|˜D &¨&Ó1ˆÜ
�Š�D 4Ò(ðlˆð )¬×):Ñ):×)>Ñ)>Ó)@×)JÑ)JÐT]Ð)JÐ)^Ð(_Ð_dÐeh×elÑel×eÐps×pÑpð  eAð  Bð  Cˆ÷	,ñ 	ˆT�2Œð 	�‰�Z¾ 4§9¡9¨VÔ#4ÀIÑNÔOØ�‰Ð(¨¯©¨°W¸S¿Y¹Y¸KÀvÈcÏhÉhÈZÐW`Ðad×alÑalÐ`mÐmwÐx{÷  yKñ  yKð  xLð  Mô  	NØ�‰� s§x¡x¡l°.À×AQÑAQÑ@TÐTZÐ[^×[hÑ[hÑZkÐkuÐvy÷  wCñ  wCñ  vFð  FOð  PS÷  Pcñ  Pcñ  Ofð  gô  	hØ�‰Ð)¨#¯'©'©°F¸3¿8¹8¹,ÀgÈcÏiÉiÉ]ÐZfÐgj×guÑguÑfxÐx~ð  @C÷  @Hñ  @Hñ  Kð  Lô  	MØ�‰�} S§[¡[¡OÐ4Ô5Ø�‰Ð1´.ÀÓ2FÑ1IÐJÔKð	6Ø(9Ñ(EÑ$È2ˆDØ-1¯Z©Z¸Ô-=Ó`Ò-=¨ÄÇÂÈ:ÐWXÔZ\×Z^ÑZ^×A_“O�A—G‘G“I˜d˜s“OÑ-=ˆDÐ`Ø�L‰LÐ6Ô7HÈÓ7NÑ6QÐQeÐfö  EIÐjp×juÑjuÐvzÐ{~Ð|~Ðvô  kAð  OUñ  Vô  Wó 	Ü˜d IÓ.ˆØ�‰ÈÑH[Ð&¨¯©°WÓ)=¸e¸tÐ)DÒDÐavÔwÛ=ˆDÜ Ó+ˆEØ�L‰L˜D˜6 Ð*È5ÑK\¨e¯k©k¸%Ó.@ÀÀ$Ñ.GÐbhÑiÖjñ >ó FˆCÜ˜DÓ&ˆAØ‹}Ø—‘˜v c U¨"¨U¯[©[¸«^¸D¸SÐ-AÐ,BÐCÖDñ Fô ”"—'‘'—,‘,˜tÐ%:Ó;¸SÈ7ÒSÐWXØ�G‰G�D—I‘I˜eÓ$ tÑ+Ô,÷ TÐSùò aøäó 	6Ø�L‰LÐ0°°Ð4×5Ò5ûð	6ú÷ TÕSûäó Ùðúsz   ¦I;R Ê"Q Ê</QË/QÌ?Q ÍBR ÏAR Ð-$RÑR ÑQ Ñ
RÑ)RÑ=R ÒRÒR Ò
RÒR ÒR Ò
R)Ò(R)c                 óô   • [        U [        5      (       a3  X;   a  X   $ U R                  5        H  n[        X!5      nUc  M  Us  $    g[        U [        5      (       a  U  H  n[        X!5      nUc  M  Us  $    g)zAFind the first value for a key anywhere in a nested dict or list.N)r¸   rº   ÚvaluesrÁ   r7   )Úobjrå   r•   Úfounds       r   rÁ   rÁ   í  sx   € ä�#”t×ÑØ‹:Ø‘8ˆOØ—‘–ˆAÜ Ó'ˆEØÓ Ø’ñ ð ô 
�Cœ×	Ñ	ÛˆAÜ Ó'ˆEØÓ Ø’ñ ð r   z5https://pro.motorway.co.uk/account/purchases/completezhhttps://pro.motorway.co.uk/account/purchases/cancelled?dateRangeType=ALL&order=DESC&sortBy=dateBid&page=u   Â£([\d,]+)(?:\.(\d{2}))?)zAwaiting confirmationzReviewing documentszChecks completezDelivery requestedÚ	DeliveredÚCompletez2^(\d+)\s+(min|minute|hour|day|week|month)s?\s+ago$z%^(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})$)ÚJanÚFebÚMarÚAprÚMayÚJunÚJulÚAugÚSepÚOctÚNovÚDecr6   c                 ó$  • U (       d  gU=(       d    [         R                  R                  5       n[        R	                  U R                  5       5      nU(       ak  [        UR                  S5      5      UR                  S5      R                  5       pCSSSSSSS.U   nU[         R                  " X5-  S9-
  R                  5       $ [        R	                  U R                  5       5      nU(       a’  [        UR                  S5      5      [        R                  UR                  S5      R                  5       5      [        UR                  S	5      5      p‡nU(       a&   [         R                  " X‡U5      R                  5       $ g! [         a     gf = f)
aè  Motorway's purchases screen gives the date won as either a relative
phrase ("8 hours ago", or "57 mins ago" for a car won within the last
hour, caught live 2026-08-27 on LR68GCU: Motorway abbreviates minutes
as "mins", which _WON_AGO_RE originally never matched at all, so a
freshly won car's bought_date silently stayed blank and dropped off
the purchases page's own default June-onward date filter, "we have
won LR68 GCU on MW ... its not pulled this through", even though the
row itself had genuinely already synced) or an absolute one ("18 Jun
2026"), never both. Turns either into a plain YYYY-MM-DD string, the
same shape Carwow's own bought_date already uses. Returns "" rather
than guessing if the text does not match either known form.r   r6   r   r   é   é   )rn   ÚminuteÚhourÚdayÚweekÚmonth)ÚdaysrS   )r3  ÚdateÚtodayÚ_WON_AGO_REri   r   r2   r>   rk   Ú	timedeltar5  Ú_WON_ABS_REÚ_MONTHSrŽ   Ú
ValueError)	r   r`  rC   rØ   ÚunitÚdays_per_unitr[  Úmonr[   s	            r   Ú_parse_won_dateri    s+  € ö ØØ×*”X—]‘]×(Ñ(Ó*€EÜ×Ñ˜$Ÿ*™*›,Ó'€AÞÜ�a—g‘g˜a“j“/ 1§7¡7¨1£:×#3Ñ#3Ó#5ˆ4Ø !¨Q¸À!ÈQÐY[Ñ\Ð]aÑbˆØœ×*Ò*°Ñ0AÑBÑB×MÑMÓOÐOÜ×Ñ˜$Ÿ*™*›,Ó'€AÞÜ˜QŸW™W Q›Z›¬'¯+©+°a·g±g¸a³j×6FÑ6FÓ6HÓ*IÌ3ÈqÏwÉwÐWXËzË?�$ˆÞðÜ—}’} T°Ó4×>Ñ>Ó@Ð@ð øô ó Ùðús   Å$F Æ
FÆFc           
      óÊ  ^^• U R                  5       =(       d    SR                  S5       Vs/ s H)  oR                  5       (       d  M  UR                  5       PM+     nnU(       d  gU R                  S5      =(       d    SnUR	                  SS5      S   nSnSnSmSnSn[        U5       GHR  u  n	mU(       d„  [        T5      (       at  SR                  TR                  5       5      nU	S-   n
U
[        U5      :  a.  X*   [        ;   a"  U
S-  n
U
[        U5      :  a  X*   [        ;   a  M"  U
[        U5      :  a  X*   nUc\  TR                  S5      (       aF  [        R                  T5      nU(       a*  [        UR                  S5      R                  S	S5      5      n[         R                  T5      (       d  ["        R                  T5      (       a  TnT[        ;   a  Tm[%        U4S
 jS 5       5      (       d  GMP  TmGMU     U[        ;   a  Sn[%        U4S jS 5       5      nU(       a5  Uc  U(       a+  SXEUTUU['        U5      [(        R*                  S   S   U-   S.	$ gs  snf )a	  One datagrid row from the Purchases > Complete screen (reg, name,
status, price paid, date won) into the same dict shape read_purchases
and search_purchase both return. Returns None if the row has nothing
useful (no reg, or no price and not a cancelled/voided one).r   rF   NÚhrefrO   r6   rP   ô   Â£r/   c              3   óH   >#   • U  H  oTR                  5       ;   v •  M     g 7fr	   r  )r   r   rL   s     €r   r   Ú&_parse_purchase_row.<locals>.<genexpr>Z  s   øé € ÐGÒ'F !�A—G‘G“IŽ~Ò'Fùr  )ÚcancelÚvoidÚ	withdrawnc              3   óH   >#   • U  H  oTR                  5       ;   v •  M     g 7fr	   r  )r   r   rÜ   s     €r   r   rn  b  s   øé € ÐQÒ1P¨A˜Ÿ™›Ö'Ò1Pùr  rV   rT   rU   )	ÚplatformÚvehicle_refrW   r¶   rÜ   rü   Úwon_textÚbought_daterb   )Ú
inner_textr   r   Úget_attributere   r:   r   r   r   Ú_PURCHASE_STATUS_WORDSÚ
startswithÚ	_PRICE_REri   r2   r>   r1   ra  rc  r   ri  r!   rs   )ÚrowrL   r}   rk  ry   rW   r¶   rü   ru  rƒ   r¼   Úm2Ú	cancelledrÜ   s    `           @r   Ú_parse_purchase_rowr  1  s  ù€ ð
 "%§¡Ó!1×!7°R× >Ñ >¸tÔ DÓRÒ D˜1ÏÉÏ	‹YˆQ�W‰WŽYÑ D€EÐRÞØØ×Ñ˜VÓ$×*¨€DØ
�+‰+�c˜1Ó
˜bÑ
!€CØ
€CØ€DØ€FØ€EØ€HÜ˜%× ‰ˆˆ1ÞÔ'¨×*Ñ*Ø—'‘'˜!Ÿ'™'›)Ó$ˆCð �A‘ˆAØ”c˜%“j“. U¡XÔ1GÓ%GØ�Q‘�ð ”c˜%“j“. U¡XÔ1GÕ%Gà”3�u“:‹~Ø‘x�Ø‰=˜QŸ\™\¨$×/Ñ/Ü—‘ Ó#ˆBÞÜ˜BŸH™H Q›K×/Ñ/°°RÓ8Ó9�Ü×Ñ˜Q×Ñ¤;×#4Ñ#4°Q×#7Ñ#7ØˆHØÔ&Ó&ØˆFô
 ÔGÑ'FÓG×GÔGØ‹Fñ7 !ð@ Ô%Ó%ØˆÜÔQÑ1PÓQÓQ€Iö �Ñ!¦YØ&°sØ¨¸Ø$´_ÀXÓ5NÜ&Ÿ}™}¨ZÑ8¸ÑDÀtÑKñMð 	Mð ùòi Ss   ­I Á	I c                 ó6  • [         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SS	9  UR                  S5       Vs/ s H  n[        U5      =n(       d  M  UPM     nnU(       d  [        S5      eUUR                  5         $ ! [         a    [        S
5      ef = f! [         a     N~f = fs  snf ! UR                  5         f = f)aý  Read the purchases list (the screen behind Purchases in the top nav):
every car Steven has bought, newest first. Each datagrid row carries the
reg, name, status, price paid and a relative date won. Returns a list of
dicts. Fails loudly if the screen does not render. Read only, never acts.

Only ever shows the first 25 rows without paging (real pagination exists,
"1-25 of N" with next/prev arrows, confirmed live 2026-08-23 to reach the
account's full history, 754 rows on this account alone, going back well
before this project's own tracking began), not implemented here since
the daily purchases run only ever needs today's newest buys. For an
older, already known reg, use search_purchase instead, far cheaper than
paging through hundreds of rows to find one.rT   ©Úheadlessr   ÚdomcontentloadedéÈ¯  ©Ú
wait_untilÚtimeoutú!a[role="row"][href^="/vehicles/"]é0u  ©r‡  ú‘Motorway purchases screen did not render its rows. The page may have changed or the login may have expired. Run: python3 login.py motorway manualuá   () => {
                    const rows = document.querySelectorAll('a[role="row"][href^="/vehicles/"]');
                    return rows.length > 0 && Array.from(rows).every(r => r.innerText.includes('Â£'));
                }é˜:  zdMotorway purchases screen rendered but no rows were read. The layout may have changed, not guessing.)r!   Úopen_reader_contextÚpagesÚnew_pageÚgotoÚPURCHASES_URLÚwait_for_selectorr9  ÚRuntimeErrorÚwait_for_functionÚquery_selector_allr  Úclose)Ú
playwrightr‚  ÚctxÚpager|  râ   rA   s          r   Úread_purchasesrš  m  s-  € ô ×
%Ò
% j°*ÀxÑ
P€Cð!Ø"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆØ�	‰	”-Ð,>Èˆ	ÑNð	9Ø×"Ñ"Ð#FÐPUÐ"ÑVð		Ø×"Ñ"ðð ð #ñ ð !×3Ñ3Ð4WÔXó 3ÒX�SÜ+¨CÓ0Ð0�1Ó0÷ ÑXˆð 3æÜð  Ló Mð Màà�	‰	�øô9 ó 	9Üð8ó9ð 9ð	9ûô& ó 	Ùð	üò3øð 	�	‰	�úsT   ˜AD ÁC Á/C1 Á?D ÂDÂ,DÂ2D ÃC.Ã.D Ã1
C>Ã;D Ã=C>Ã>D ÄD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      nUR                  5         UR                  U5        UR                  S5        UR                  S5      nU H_  n[        U5      nU(       d  M  US   SR                  UR!                  5       5      R#                  5       :X  d  MM  Us  UR%                  5         $     UR%                  5         g! [         a    [        S
5      ef = f! UR%                  5         f = f)a:  Find one specific already known registration anywhere in the
account's full purchase history (not just the newest 25), via the real
"Search in Complete" box on the Purchases > Complete screen, confirmed
live 2026-08-23 to reach a purchase from February on an account whose
plain listing only ever shows the newest 25. Returns the same dict
shape as one row of read_purchases, or None if that reg has never been
bought on this account (a genuine miss, not a read failure, so this
does not raise for a not-found reg the way read_purchases raises for a
wholly broken page).rT   r�  r   rƒ  r„  r…  rˆ  r‰  rŠ  r‹  zSearch in CompleteéÐ  rW   r   N)r!   r�  rŽ  r�  r�  r‘  r’  r9  r“  Úget_by_placeholderÚclickÚfillÚwait_for_timeoutr•  r  r   r   rÂ   r–  )	r—  rW   r‚  r˜  r™  ÚboxÚrowsr|  Úparseds	            r   Úsearch_purchaser¤  Ÿ  s4  € ô ×
%Ò
% j°*ÀxÑ
P€CðØ"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆØ�	‰	”-Ð,>Èˆ	ÑNð	9Ø×"Ñ"Ð#FÐPUÐ"ÑVð ×%Ñ%Ð&:Ó;ˆØ�	‰	ŒØ�‰�ŒØ×Ñ˜dÔ#Ø×&Ñ&Ð'JÓKˆÛˆCÜ(¨Ó-ˆFßˆv˜& ™-¨2¯7©7°3·9±9³;Ó+?×+EÑ+EÓ+GÕGØ‘ð 	�	‰	�ñ ð à�	‰	�øô! ó 	9Üð8ó9ð 9ð	9ûð  	�	‰	�ús7   ˜AE ÁD9 Á.A)E Ã2E ÄE Ä%E Ä9EÅE ÅE$c                 óè  • [        U 5       Hã  u  p#X1:w  a  M  US-   [        U 5      :¼  d  XS-      R                  S5      (       d  M;  [        R                  " SXS-      5      nU(       d  M`  [        UR                  S5      R                  SS5      5      nSnUS-   [        U 5      :  a4  [        R                  " SXS-      5      (       a  [        XS-      SS	 5      n[        XVS
-  -   S5      s  $    g	)uö   The pound value on the line right after an exact label match, Motorway's
purchase summary always splits a figure across two lines, "Â£X,XXX" then
".XX" for the pence. Returns a float, or None if the label or its value
is not found, never guessed.r6   rl  õ
   Â£([\d,]+)r/   r   r   r   ú\.\d{2}Néd   ©
r:   r   rz  rG   ri   r2   r>   r1   r   Úround)r}   r·   rB   rL   rC   ÚpoundsÚpences          r   Ú_money_afterr­  Â  sÜ   € ô
 ˜%Ö ‰ˆØ‹:ÙØˆq‰5”C˜“JÓ e°©E¡l×&=Ñ&=¸d×&CÑ&CÙÜ�HŠH�] E¨a©%¡LÓ1ˆÞÙÜ�Q—W‘W˜Q“Z×'Ñ'¨¨RÓ0Ó1ˆØˆØˆq‰5”3�u“:Ó¤"§,¢,¨z¸5ÀQÁ¹<×"HÑ"HÜ˜ !™e™ Q RÐ(Ó)ˆEÜ�V c™kÑ)¨1Ó-Ò-ñ !ð r   c                 ó  • Sn[        U 5       Hç  u  p#US:w  a  M  US-   [        U 5      :¼  d  XS-      R                  S5      (       d  M<  [        R                  " SXS-      5      nU(       d  Ma  [        UR                  S5      R                  SS5      5      nSnUS	-   [        U 5      :  a4  [        R                  " S
XS	-      5      (       a  [        XS	-      SS 5      nU=(       d    SU-   US-  -   nMé     Ub  [        US	5      $ S$ )aX  Motorway's purchase summary shows a separate "VAT (20%)" line under
each of the Motorway fee, transport fee and Motorway Protect fee (when
present), never a single combined figure. Sums every one found. None if
none are found at all (never 0 by default, which would look like a real
reading of no VAT rather than a page that could not be read).Nz	VAT (20%)r6   rl  r¦  r/   r   r   r   r§  r¨  r©  )r}   ÚtotalrB   rL   rC   r«  r¬  s          r   Ú_vat_total_from_linesr°  ×  sú   € ð €EÜ˜%Ö ‰ˆØ�ÓÙØˆq‰5”C˜“JÓ e°©E¡l×&=Ñ&=¸d×&CÑ&CÙÜ�HŠH�] E¨a©%¡LÓ1ˆÞÙÜ�Q—W‘W˜Q“Z×'Ñ'¨¨RÓ0Ó1ˆØˆØˆq‰5”3�u“:Ó¤"§,¢,¨z¸5ÀQÁ¹<×"HÑ"HÜ˜ !™e™ Q RÐ(Ó)ˆEØ—˜!˜vÑ%¨°©Ñ3Šñ !ð $Ñ/Œ5�˜‹?Ð9°TÐ9r   z^([\d,]+)\s*miles$c                 ó¦   • U  HK  n[         R                  U5      nU(       d  M!  [        UR                  S5      R	                  SS5      5      s  $    g)z¥The vehicle summary at the top of the purchase summary page carries
the mileage as its own line, "72,054 miles". Returns an int, or None if
not found, never guessed.r6   r/   r   N)Ú_MILEAGE_REri   r2   r>   r1   )r}   rL   rC   s      r   Ú_mileage_from_linesr³  ñ  sH   € ó ˆÜ×Ñ˜aÓ ˆßˆ1Ü�q—w‘w˜q“z×)Ñ)¨#¨rÓ2Ó3Ò3ñ ð r   c           
      óB  • U R                  [        R                  S   S    SU 3SSS9   U R                  SSS	9  U R                  S5        U R                  S5      n[        R                  " SU[        R                  " U5      5        SU;   a  g
UR                  S5       Vs/ s H)  o3R                  5       (       d  M  UR                  5       PM+     nn[        US5      nSn[        US5      nUc  [        US5      nUS
LnUb  Uc  g
UU[        US5      [        US5      [        US5      [        U5      U[        U5      S.[        R                  " U5      R                  5        VV	s0 s H  u  p‰US;   d  M  X‰_M     sn	nE$ ! [         a     g
f = fs  snf s  sn	nf )aü  The full payment breakdown behind one purchased car (Mark 2026-08-21:
"if you click the price it shows winning bid, fee, transport, vat total"),
read from its own purchase summary page: the winning bid (Motorway calls
this "Vehicle price"), the Motorway fee, the transport fee, any Motorway
Protect fee, the VAT total across all three, the grand total, and the
mileage (already on the same page, so a live Cazana lookup for the
retail estimate does not need a separate detail page visit).

There are three states, not two (found live 2026-08-21 after Mark said
the data was there when he checked by hand and this was still returning
None): no breakdown at all yet ("Payment details will be available once
all seller documents have been reviewed", still returns None here,
nothing to read); an "Estimated breakdown" while the car is bought but
not yet paid for (same field labels, but the grand total is "Total to
pay", not "Total paid", and Motorway says outright "the final breakdown
will be updated post payment", so this is not treated as permanent, see
"estimated" in the returned dict and db.record_purchases's own handling
of it); and the final confirmed breakdown ("Total paid"). Returns a
dict either way once real figures exist, or None if truly nothing is
there yet.rT   rU   z/purchase-summary/rƒ  r„  r…  ztext=Vehicle priceé N  rŠ  Néô  r  zwill be available oncerF   zVehicle priceFz
Total paidzTotal to payzMotorway feezTransport feezMotorway Protect fee)Úwinning_bidÚ	estimatedÚmotorway_feeÚtransport_feeÚprotect_feeÚ	vat_totalÚtotal_pricer\   )Úfinance_settlementÚnegative_equity)r�  r!   rs   r’  r9  r   rw  r   Úfinance_lines_noteÚfinance_from_purchase_textr   r   r­  r°  r³  rì   )
r™  rt  r   rL   r}   Úbidr¸  r¯  r¼   r•   s
             r   Úread_purchase_breakdownrÃ  ü  s´  € ð* 	‡I�I”—‘˜zÑ*¨:Ñ6Ð7Ð7IÈ+ÈÐWØ+°Uð ñ <ðØ×ÑÐ3¸UÐÑCð 	×Ñ˜#ÔØ�?‰?˜6Ó"€DÜ×$Ò$ Z°¼m×>fÒ>fÐgkÓ>lÔmØ 4Ó'ØØ $§
¡
¨4Ô 0Ó>Ò 0˜1·G±G·I‹YˆQ�W‰WŽYÑ 0€EÐ>Ü
�u˜oÓ
.€CØ€IÜ˜ Ó-€EØ�}Ü˜U NÓ3ˆØ Ð%ˆ	Ø
�{�e‘mØàØÜ$ U¨NÓ;Ü% e¨_Ó=Ü# EÐ+AÓBÜ*¨5Ó1ØÜ& uÓ-ñô *×DÒDÀTÓJ×PÑPÔRô  FÒR‘D�AÐVWð  \Eñ  WE‹4ˆ1Š4ÑRò  Fðð øô! ó Ùðüò ?ùó( Fs)   ¬F Â$FÃ FÅ+FÅ;FÆ
FÆFúOhttps://[A-Za-z0-9._\-]*imgix[A-Za-z0-9._/\-]*exterior[A-Za-z0-9._/\-]*?\.jpe?gc                 ó¨   •  U R                  5       n[        R                  U5      nU(       a  UR	                  S5      S-   $ S$ ! [         a     gf = f)ag  A small photo for the purchases report (Mark 2026-08-21: "add an image
to the line"), read from the same purchase summary page read_purchase_
breakdown already opened, no extra page visit needed. Returns "" if no
exterior photo is found (a still pending purchase, or a page that failed
to load), never guessed from one of the document images on the same
page.r   r   z?w=300&auto=format,compress)Úcontentr9  Ú_PURCHASE_PHOTO_RErf   r>   )r™  ÚhtmlrC   s      r   Úread_purchase_photorÉ  E  sS   € ðØ�|‰|‹~ˆô 	×!Ñ! $Ó'€AÞ;<ˆA�G‰G�A‹JÐ6Ñ6ÐDÀ"ÐDøô ó Ùðús   ‚A Á
AÁAzo<img\b[^>]*\bsrc="(https://motorway-service-history-photos\.imgix\.net/[^"]+\.(?:jpe?g|png|heic)\?[^"]+)"[^>]*>c                 óª  •  U R                  5       n0 n[        R                  U5       Hn  n[        R
                  " UR                  S5      5      nUR                  S5      u  pVnSU;   =(       d    SU;   nXR;  d  X%   S   (       d  M`  U(       a  Mi  XH4X%'   Mp     UR                  5        V	s/ s H  o™S   PM	     sn	$ ! [         a    / s $ f = fs  sn	f )a|  Every service history document photo for the car (Mark 2026-08-21:
"grab the service history images and store in the database for
purchased cars"), read from the same purchase summary page
read_purchase_breakdown already opened, no extra page visit needed.
Each real photo appears in more than one <img> tag, at least one
cropped thumbnail and one full resolution version, so this dedupes to
one URL per distinct photo by its path before the query string,
preferring the full resolution version when both are present. Returns
[] if none are found, a still pending purchase or a car with no
service history photos on file, never guessed.r6   Ú?úw=úh=r   )	rÆ  r9  Ú_SERVICE_HISTORY_IMG_REr9   rI   rJ   r>   Ú	partitionrE  ©
r™  rÈ  ÚbestrC   ÚurlÚbaseÚ_ÚqueryÚhas_cropr•   s
             r   Úread_service_history_photosr×  j  sÀ   € ðØ�|‰|‹~ˆð €DÜ$×-Ñ-¨dÖ3ˆÜ×Ò˜qŸw™w q›zÓ*ˆØŸ™ sÓ+‰ˆ�Ø˜5‘=×1 D¨E¡MˆØÓ ¡
¨1§¡·h°hØ˜ˆD‹Jñ 4ð Ÿ+™+œ-Ó(š-�QˆaŒD™-Ñ(Ð(øô ó ØŠ	ðüò )ó   ‚B> Â-CÂ>CÃCze<img\b[^>]*\bsrc="(https://mw-documents\.imgix\.net/[^"]*/v5-[^"]+\.(?:jpe?g|png|heic)\?[^"]+)"[^>]*>c                 óª  •  U R                  5       n0 n[        R                  U5       Hn  n[        R
                  " UR                  S5      5      nUR                  S5      u  pVnSU;   =(       d    SU;   nXR;  d  X%   S   (       d  M`  U(       a  Mi  XH4X%'   Mp     UR                  5        V	s/ s H  o™S   PM	     sn	$ ! [         a    / s $ f = fs  sn	f )a  Every V5C document photo for the car, read from the same purchase
summary page read_purchase_breakdown already opened, no extra page
visit needed. Returns [] if none are found, a still pending purchase
or a car whose V5 has not been scanned yet, never guessed.r6   rË  rÌ  rÍ  r   )	rÆ  r9  Ú
_V5_IMG_REr9   rI   rJ   r>   rÏ  rE  rÐ  s
             r   Úread_v5_photosrÛ  �  sÀ   € ð
Ø�|‰|‹~ˆð €DÜ× Ñ  Ö&ˆÜ×Ò˜qŸw™w q›zÓ*ˆØŸ™ sÓ+‰ˆ�Ø˜5‘=×1 D¨E¡MˆØÓ ¡
¨1§¡·h°hØ˜ˆD‹Jñ 'ð Ÿ+™+œ-Ó(š-�QˆaŒD™-Ñ(Ð(øô ó ØŠ	ðüò )rØ  z?https://delivery.motorway.co.uk/inspection-report/{vehicle_ref}c                 óz  • UR                   nUR                  S:w  d  SU;  a  SU;  a  U R                  5         g U R                  5       nUR	                  5       n[        U5      S:  a  U R                  US9  g[        R                  " SS9u  pV [        R                  " XT5        [        R                  " U5        US	-   n[        R                  " S
SSSSSSSSUSU/[        R                  [        R                  SS9  [        R                  R!                  U5      (       aV  [        R                  R#                  U5      S:”  a3  [%        US5       nU R                  X8R'                  5       S9  SSS5        OU R                  US9  XfS	-   4 H  n	 [        R(                  " U	5        M     g! , (       d  f       N3= f! [*         a     M=  f = f! XfS	-   4 H+  n	 [        R(                  " U	5        M  ! [*         a     M)  f = f   f = f! [,         a%     U R                  5          g! [,         a      gf = ff = f)aë  Route handler for download_delivery_report_pdf: recompresses every
real image on the report through macOS's own sips (no extra Python
dependency, already used elsewhere in this project for the same
reason), never by rewriting the request. Rewriting a signed imgix url
(an appended w=/q=/auto= after its own s= signature token) was tried
first and silently broke every image on mw-vehicle-inspection-prod.imgix.net
specifically (a real, signed CDN, invalidated by any extra param,
caught live 2026-08-23 as "Image Error" placeholders, a false looking
file size win since the real photos were not there any more, not
genuinely smaller). Fetching the real response first and only touching
the bytes handed back to the page sidesteps signing entirely.Úimagez	imgix.netzamazonaws.comNr‰  )Úresponseú.jpg)Úsuffixz_out.jpgÚsipsz-sÚformatÚjpegÚformatOptionsÚ45z-ZÚ700z--outF)ÚstdoutÚstderrÚcheckr   Úrb)rÞ  r  )rÒ  Úresource_typeÚ	continue_Úfetchr  r   ÚfulfillÚtempfileÚmkstempr.  r=  r–  Ú
subprocessÚrunÚDEVNULLr  ÚexistsÚgetsizer<  ÚreadÚremoveÚOSErrorr9  )
ÚrouteÚrequestrÒ  Úrespr  Úfd_inÚpath_inÚpath_outr”   r  s
             r   Ú_compress_report_imagesrÿ  ³  sí  € ð �+‰+€CØ×Ñ Ó'¨K¸sÓ,BÀÐ^aÓGaØ�‰ÔØðØ�{‰{‹}ˆØ�y‰y‹{ˆÜˆt‹9�uÓØ�M‰M 4ˆMÑ(ØÜ!×)Ò)°Ñ8‰ˆð	Ü�HŠH�UÔ!Ü�HŠH�UŒOØ Ñ+ˆHÜ�NŠNØ˜˜x¨°°ÈØ�u˜g w°ð:ä!×)Ñ)´*×2DÑ2DÈEòSô �w‰w�~‰~˜h×'Ñ'¬B¯G©G¯O©O¸HÓ,EÈÓ,IÜ˜( DÔ)¨QØ—M‘M¨4·f±f³h�MÑ?÷ *Ð)ð —‘ t�Ñ,à¨Ñ3Ó4�ðÜ—I’I˜a–Lò 5÷ *Õ)ûô ó Úðûð ¨Ñ3Ó4�ðÜ—I’I˜a–LøÜó Úðúò 5ûô
 ó ð	Ø�O‰OÖøÜó 	Úð	úðús¨   »>H Á:H ÂCG ÅF/Å2G Æ
	H ÆG Æ*H Æ/
F=Æ9G Ç 
GÇ
H ÇGÇH Ç
HÇG4Ç2HÇ4
H	Ç>HÈH	ÈHÈH È
H:ÈH(È(
H6È2H:È5H6È6H:c                 óˆ  • U R                  S[        5         U R                  [        R	                  US9SSS9  U R                  S5        SU R                  5       =(       d    S;  a   U R                  S[        5        g	U R                  US
SS9   U R                  S[        5        g! U R                  S[        5        f = f)aè  Motorway's own post delivery "Vehicle report" (condition and damage,
delivery photos, a real delivery timestamp), a genuine HTML page, not a
native PDF, at DELIVERY_REPORT_URL, only real for a purchase Motorway's
own transport actually delivered (2026-08-23, Mark: "add this to DK as
a PDF document"). Rendered to a real PDF via headless Chrome, images
recompressed through _compress_report_images on the way through so the
result is a genuine few MB rather than the roughly 20 to 25MB an
unmodified render came out at live (32 real inspection and delivery
photos at full CDN resolution), proven live to stay legible at this
setting. Returns True if a report exists and was saved, False if this
vehicle_ref genuinely has none (a self collected purchase, or one
Motorway's own delivery has not completed), never guessed either way,
checked by looking for the page's own "Vehicle report" heading rather
than trusting a 200 status alone (the site returns 200 with a generic
shell for an unknown id too).z**/*©rt  Únetworkidleé`ê  r…  r$  zVehicle reportr   FÚA4T)r  râ  Úprint_background)	rù  rÿ  r�  ÚDELIVERY_REPORT_URLrâ  r   rÆ  ÚunrouteÚpdf)r™  rt  Úout_paths      r   Údownload_delivery_report_pdfr
  ä  sª   € ð  	‡J�JˆvÔ.Ô/ð	6Ø�	‰	Ô%×,Ñ,¸Ð,ÐEØ*°Eð 	ñ 	;à×Ñ˜dÔ#Ø D§L¡L£N×$8°bÓ9Øð 	�‰�VÔ4Õ5ð 	�‰�h t¸dˆÑCØà�‰�VÔ4Õ5øˆ�‰�VÔ4Õ5ús   ˜AB) Â B) Â)Cz1https://pro.motorway.co.uk/delivery/{vehicle_ref}z%Collected\s*\n?\s*(\d{2}/\d{2}/\d{4})z+Collection date\s*\n?\s*(\d{2}/\d{2}/\d{4})c                 óV  • U R                  [        R                  US9SSS9  U R                  S5        U R	                  S5      n[
        R                  U5      =(       d    [        R                  U5      nU(       d  gUR                  S5      R                  S	5      u  pEnU S
U S
U 3$ )a~  The date Motorway's own transport collects, or is due to collect,
the car from the seller, read from Motorway's own delivery timeline at
DELIVERY_TIMELINE_URL (Booking created, Allocated to WDC, Collection
date/Collected, Delivered, each a plain label then a DD/MM/YYYY line,
confirmed live against Mark's own screen). Deliberately NOT the same
as the "Delivered date" on the purchase summary page, or the
inspection report's own delivery timestamp, both read for other
purposes elsewhere in this project, both real but wrong for this one.

Prefers the real, already-happened "Collected" date; falls back to
the scheduled "Collection date" for a purchase still awaiting pickup
(2026-08-26, Mark: "we need to pull in the due in date from motorway
... delivery details, as collection date", found live: the purchases
page's own "Due in" column was reading blank for exactly these still
pending cars, since the original build only ever looked for the
confirmed past tense state). This does mean the same reg can
legitimately return an earlier, tentative date on one read and a
later, confirmed one after collection actually happens, or after a
reschedule, callers that store this should refresh rather than lock
it in for good once first read, see db._PURCHASE_REFRESHABLE_FIELDS.

Returns a "YYYY-MM-DD" string, or None if this purchase has no such
timeline at all (a self collected purchase), never guessed.r  Úloadr„  r…  r$  r  Nr6   rO   Ú-)
r�  ÚDELIVERY_TIMELINE_URLrâ  r   rw  Ú_COLLECTED_RErf   Ú_COLLECTION_DATE_REr>   r   )r™  rt  r   rC   Údr„   rÓ   s          r   Úread_collection_dater    s    € ð0 	‡I�IÔ#×*Ñ*°{Ð*ÐCØ¨ð ñ 0à×Ñ˜$ÔØ�?‰?˜6Ó"€DÜ×Ñ˜TÓ"×FÔ&9×&@Ñ&@ÀÓ&F€AÞØØ�w‰w�q‹z×Ñ Ó$�H€Aˆ1ØˆS��"��Q�q�cˆ?Ðr   c                 óØ   •  U R                  [        R                  S   S    SU 3SSS9  U R                  SSS	S
9  U R	                  SS5      n[        U5      $ ! [
         a    / s $ f = f)a™  The full exterior gallery for a purchased car (Mark 2026-08-21:
"apply this to the purchases page", the same hidden behind the main
photo, viewable in a modal treatment already built for the daily
shortlist). Unlike read_purchase_photo and read_service_history_photos,
this genuinely needs its own extra page visit: the purchase summary
page only ever carries the single exterior shot already used for
photo_url, the full gallery lives on the car's own original listing
page instead (/vehicles/{vehicle_ref}), the exact page
enrich_from_detail already reads for the daily shortlist, reusing the
same _gallery_photos_from_text. That original listing often stays
reachable for a while after a car is bought (proven live on a real
purchase), but not forever, cars do eventually come off the platform,
so this returns [] rather than guessing once it is gone, same as
every other "not available any more" case in this project.rT   rU   ú
/vehicles/rƒ  r‰  r…  ú#__NEXT_DATA__ÚattachedrŒ  ©Ústater‡  úel => el.textContent)r�  r!   rs   r’  Úeval_on_selectorr9  Ú_gallery_photos_from_text)r™  rt  Úblobs      r   Úread_purchase_galleryr  3  s‡   € ðØ�	‰	”W—]‘] :Ñ.¨zÑ:Ð;¸:ÀkÀ]ÐSØ/¸ð 	ñ 	@à×ÑÐ/°zÈ5ÐÑQØ×$Ñ$Ð%5Ð7MÓNˆô % TÓ*Ð*øô ó ØŠ	ðús   ‚AA ÁA)Á(A)c                 ó0   • [         R                  " XU5      $ )aß  Download a list of Motorway's own image urls (exterior photos,
service history and V5 scans, all served from the mw-documents/mw-
photos imgix hosts as short lived SIGNED urls, a real expires=... in
the query string) to real local files, the same reasoning already
proven for Carwow's own document bundle: read fresh and save
immediately, never store the url itself for later use, it goes dead
within about an hour (confirmed live 2026-08-26, Mark: "the motorway
v5 images have a '?' ... they dont load", a stored AY66WFW V5 url
captured 2026-08-23 returned a real 404 from a plain fetch three
days later, matching this urls own expires= timestamp exactly).
Keeps the real source filename, the path segment before the query
string (docs-service-history-.../v5-...), so a later DealerKit push
or the purchases page's own document type detection can still tell
what it is, the same convention dealerkit_write._download_temp
already uses for a url it downloads. Returns local file paths,
skipping (never raising on) any one url that fails to fetch, since
one dead link should never lose every other real photo captured in
the same pass. The saving itself is browser.download_media, shared
with Carwow since 2026-09-10; this keeps Motorway's own name.)r!   Údownload_media)r™  ÚurlsÚout_dirs      r   Údownload_purchase_mediar"  L  s   € ô( ×!Ò! $¨gÓ6Ð6r   c           	      óð  • / n[         R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       n[	        SUS-   5       GHK  nUR                  [        [        U5      -   SSS9   UR                  SS	S
9  UR                  S5        SnUR                  S5       Hå  nUR                  5       =(       d    SR                  S5       V	s/ s H)  o™R                  5       (       d  M  U	R                  5       PM+     n
n	SnSn[!        U
5       HO  u  pÙ[#        U	5      (       d  M  SR%                  U	R                  5       5      nUS-   ['        U
5      :  a  X­S-      n  O   U(       d  MÊ  UR)                  SUUSS.5        US-  nMç     US:X  d  GML    O   UR+                  5         U$ ! [         a    US:X  a  [        S5      e   M1  f = fs  sn	f ! UR+                  5         f = f)a  Read Motorway's Purchases > Cancelled list: cars bought where the sale was
then cancelled. Steven never wants these back, so the daily run suppresses
them. Returns dicts with reg, name and status. Newest first; max_pages limits
how deep to read (1 for the daily top up of new cancellations, more for a one
off backfill of the whole history). Fails loudly if page one does not render.rT   r�  r   r6   rƒ  r„  r…  ú[role="row"]rµ  rŠ  z�Motorway cancelled list did not render its rows. The page may have changed or the login may have expired. Run: python3 login.py motorway manualéX  r   rF   rV   Ú	Cancelled)rs  rW   r¶   rÜ   )r!   r�  rŽ  r�  rm   r�  ÚCANCELLED_URLr0   r’  r9  r“  r   r•  rw  r   r   r:   r   r   r   r=   r–  )r—  Ú	max_pagesr‚  rA   r˜  r™  rØ   Ú	page_regsr|  rL   r}   rW   r¶   rƒ   s                 r   Úread_cancelledr*  c  sÓ  € ð €CÜ
×
%Ò
% j°*ÀxÑ
P€Cð Ø"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆÜ�q˜) a™-×(ˆAØ�I‰I”m¤c¨!£fÑ,Ð9KÐUZˆIÑ[ðØ×&Ñ& ~¸uÐ&ÑEð ×!Ñ! #Ô&ØˆIØ×.Ñ.¨~Ö>�Ø-0¯^©^Ó-=×-CÀ×,JÑ,JÈ4Ô,PÓ^Ò,P q×T[ÑT[×T]›˜Ÿ™žÑ,P�Ð^Ø�Ø�Ü% eÖ,‘D�AÜ'¨×*Ó*Ø Ÿg™g a§g¡g£iÓ0˜Ø˜q™5¤3 u£:Ó-Ø#(¨Q©¡<˜DÙñ -÷ �3Ø—J‘J¨J¸sØ(,¸ñ Eô Fà ‘N’Iñ ?ð ˜AŽ~Ùñ9 )ð< 	�	‰	ŒØ€Jøô7 ó Ø˜“6Ü&ð@óAð Aó ðüò _øð  	�	‰	�úsV   šA$G# Á?F;ÂAG# Ã GÃ<GÄ$G# Ä8AG# Å=#G# Æ%G# Æ;GÇG# ÇGÇG# Ç#G5z7https://pro.motorway.co.uk/account/bids/didnt-win?page=c           
      óè  • U =(       d    SR                  S5       Vs/ s H)  o"R                  5       (       d  M  UR                  5       PM+     nnSnSnSnSnSn[        U5       GHQ  u  p’U(       dJ  [        U5      (       a:  SR	                  UR                  5       5      nU	S-   [        U5      :  a  X9S-      nMW  UR                  S5      (       aH  [        R                  U5      n
U
(       a*  [        U
R                  S5      R                  SS5      5      nMµ  US:X  aT  U	S:”  aN  [        R                  X9S-
     5      n
U
(       a*  [        U
R                  S5      R                  SS5      5      nGM  [        R                  U5      (       d  [        R                  U5      (       d  GMF  [        U5      nGMT     U(       d  gS	XEXgUUU(       a  [         R"                  S
   S    SU 3S.$ SS.$ s  snf )uí  One datagrid row from Bids & offers > Didn't win (reg, name, the
real Sold for price the car won at, our own bid, the date we bid) into
a plain dict, or None if the row has nothing useful. Pure text parsing,
pulled out so it can be tested without a browser, the same split
already used for _parse_purchase_row and Carwow's own
_parse_purchase_breakdown. Real captured shape (2026-08-24):
"X99 KDX\nMercedes A 180 D AMG Line Premium Auto\nSold for Â£7,457\n
Â£7,456\nYour max bid\n20 Aug 2026".r   rF   Nr6   zSold forr/   úYour max bidr   rV   rT   rU   r  )rs  rW   r¶   Úsold_forÚour_bidÚdate_bidrt  rb   )r   r   r:   r   r   r   rz  ro   rf   r2   r>   r1   rc  ri   ra  ri  r!   rs   )r   rt  rL   r}   rW   r¶   r-  r.  r/  rƒ   rC   s              r   Ú_parse_didnt_win_rowr0  ™  s©  € ð "&§¨× 2Ñ 2°4Ô 8ÓFÒ 8˜1¿G¹G¿I‹YˆQ�W‰WŽYÑ 8€EÐFØ
€CØ€DØ€HØ€GØ€HÜ˜%× ‰ˆÞÔ'¨×*Ñ*Ø—'‘'˜!Ÿ'™'›)Ó$ˆCØ�1‰u”s˜5“zÓ!Ø ™U‘|�ÙØ�<‰<˜
×#Ñ#Ü—‘˜Q“ˆAÞÜ˜qŸw™w q›z×1Ñ1°#°rÓ:Ó;�ÙØ�Ó 1 q£5Ü—‘˜U q¡5™\Ó*ˆAÞÜ˜aŸg™g a›j×0Ñ0°°bÓ9Ó:�ÚÜ×Ñ˜Q×Ñ¤;×#4Ñ#4°Q×#7Ô#7Ü& qÓ)‹Hñ# !ö$ Øà sØ¸hØ"Þ\gœ'Ÿ-™-¨
Ñ3°JÑ?Ð@À
È;È-ÐXñ	ð ð npñ	ð ùò5 Gs
   �G/¹G/c                 ó�   • / nU  H;  nUb$  [        UR                  S5      5      U;   a  US4s  $ UR                  U5        M=     US4$ )a�  The rows before the first one already on file. The Didn't win list
is newest first, so everything after a known car is older and on file
too: a daily sync only needs the new cars at the top (Steven
2026-09-09: "all you need to look for is new cars that have been added
to didnt win, there is no need to go through every car already in the
list"). Returns (new rows, whether a known car was reached). Pure,
tested.rt  TF)r0   rŽ   r=   )r¢  ÚknownrA   râ   s       r   Únew_until_knownr3  Ä  sO   € ð €CÛˆØÑ¤ Q§U¡U¨=Ó%9Ó!:¸eÓ!CØ˜�9ÒØ�
‰
�1Žñ ð �ˆ:Ðr   c                 ó&  • / n[         R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       n[	        SUS-   5       Hì  nU(       a  U" U[        U5      5        UR                  [        [        U5      -   SSS9   UR                  SS	S
9  UR                  S5        / n	UR                  S5       HF  n
[        U
R                  5       U
R!                  S5      5      nU(       d  M5  U	R#                  U5        MH     U	(       d    O+[%        X“5      u  pÍUR'                  U5        U(       d  Mì    O   UR)                  5         U$ ! [         a    US:X  a  [        S5      e   M1  f = f! UR)                  5         f = f)aß  Read Bids & offers > Didn't win: every real auction we bid on and
lost, with the real price it actually sold for. max_pages limits how
deep to read (25 rows a page). With `known` (the vehicle refs already
on file) the read stops at the first car already on file, so a daily
sync reads only the new cars at the top; without it (a backfill) it
reads every page it is given. Fails loudly if page one does not
render; a later empty page just means the end of the list, not a
failure.rT   r�  r   r6   rƒ  r„  r…  r$  rµ  rŠ  z�Motorway didn't win list did not render its rows. The page may have changed or the login may have expired. Run: python3 login.py motorway manualr%  Úid)r!   r�  rŽ  r�  rm   r   r�  ÚDIDNT_WIN_URLr0   r’  r9  r“  r   r•  r0  rw  rx  r=   r3  Úextendr–  )r—  r(  r‚  r2  Úon_pagerA   r˜  r™  rØ   Ú	page_rowsr|  r£  ÚfreshÚ	hit_knowns                 r   Úread_didnt_winr<  Ô  sk  € ð €CÜ
×
%Ò
% j°*ÀxÑ
P€CðØ"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆÜ�q˜) a™-Ö(ˆAÞÙ˜œ3˜s›8Ô$Ø�I‰I”m¤c¨!£fÑ,Ð9KÐUZˆIÑ[ðØ×&Ñ& ~¸uÐ&ÑEð ×!Ñ! #Ô&ØˆIØ×.Ñ.¨~Ö>�Ü-¨c¯n©nÓ.>À×@QÑ@QÐRVÓ@WÓX�ß�6Ø×$Ñ$ VÖ,ñ ?ö ÙÜ.¨yÓ@ÑˆEØ�J‰J�uÔßˆyÙñ1 )ð4 	�	‰	ŒØ€Jøô+ ó Ø˜“6Ü&ð@óAð Aó ðûð( 	�	‰	�ús>   šA<E> ÂEÂ'AE> Ä AE> ÅE> ÅE;Å6E> Å:E;Å;E> Å>Fzehttps://pro.motorway.co.uk/account/bids/under-offer?dateRangeType=ALL&order=DESC&sortBy=dateBid&page=c           	      ó  • U =(       d    SR                  S5       Vs/ s H)  o"R                  5       (       d  M  UR                  5       PM+     nnSnSnSnSn[        U5       Hð  u  p‚U(       dJ  [        U5      (       a:  SR	                  UR                  5       5      nUS-   [        U5      :  a  X8S-      nMV  US:X  aS  US:”  aM  [        R                  X8S-
     5      n	U	(       a*  [        U	R                  S5      R                  SS5      5      nM¯  [        R                  U5      (       d  [        R                  U5      (       d  Må  [        U5      nMò     U(       d  gSXEXgUU(       a  [        R                   S	   S
    SU 3S.$ SS.$ s  snf )u  One datagrid row from Bids & offers > Under offer (Mark 2026-08-27:
"on MW got to bids and offers then under offer, we have 2 cars there
currently"): a real bid Motorway currently shows as the accepted,
leading one, not yet a confirmed win, so it is never in the purchases
table at all, a genuinely earlier stage than anything read from there.
Pure text parsing, the same shape as _parse_didnt_win_row (real
captured 2026-08-27: "LR68 GCU\nBMW X2 xDrive 20d M Sport X Auto\n
Under offer\nÂ£9,822\nYour max bid\n2 hours ago").r   rF   Nr6   r,  r   r/   rV   rT   rU   r  )rs  rW   r¶   Úmax_bidÚnoted_atrt  rb   )r   r   r:   r   r   r   ro   rf   r2   r>   r1   rc  ri   ra  ri  r!   rs   )
r   rt  rL   r}   rW   r¶   r>  r?  rƒ   rC   s
             r   Ú_parse_under_offer_rowr@    sa  € ð "&§¨× 2Ñ 2°4Ô 8ÓFÒ 8˜1¿G¹G¿I‹YˆQ�W‰WŽYÑ 8€EÐFØ
€CØ€DØ€GØ€HÜ˜%Ö ‰ˆÞÔ'¨×*Ñ*Ø—'‘'˜!Ÿ'™'›)Ó$ˆCØ�1‰u”s˜5“zÓ!Ø ™U‘|�ÙØ�Ó 1 q£5Ü—‘˜U q¡5™\Ó*ˆAÞÜ˜aŸg™g a›j×0Ñ0°°bÓ9Ó:�ÙÜ×Ñ˜Q×Ñ¤;×#4Ñ#4°Q×#7Ó#7Ü& qÓ)ŠHñ !ö Øà sØØ"Þ\gœ'Ÿ-™-¨
Ñ3°JÑ?Ð@À
È;È-ÐXñ	ð ð npñ	ð ùò) Gs
   �F
¹F
c                 ó®  • / n[         R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       n[	        SUS-   5       H°  nUR                  [        [        U5      -   SSS9   UR                  SS	S
9  UR                  S5        SnUR                  S5       HK  n[        UR                  5       UR                  S5      5      n	U	(       d  M5  UR!                  U	5        US-  nMM     US:X  d  M°    O   UR#                  5         U$ ! [         a    US:X  a  [        S5      e   M1  f = f! UR#                  5         f = f)aŠ  Read Bids & offers > Under offer: every real bid Motorway currently
shows as accepted and leading, not yet a confirmed purchase, so never
read from the purchases table (Mark 2026-08-27: "add a under offer
section to the main nav for cars that are won but bids not accepts").
max_pages limits how deep to read, a real list is usually short since
it only ever holds bids still genuinely in play. Fails loudly if page
one does not render, the same accepted limitation read_didnt_win
already carries: a genuinely empty account looks the same as a
changed page, real reconnaissance never happened to catch this
account in that state to tell the two apart.rT   r�  r   r6   rƒ  r„  r…  r$  rµ  rŠ  zˆMotorway under offer list did not render. The page may have changed or the login may have expired. Run: python3 login.py motorway manualr%  r5  )r!   r�  rŽ  r�  rm   r�  ÚUNDER_OFFER_URLr0   r’  r9  r“  r   r•  r@  rw  rx  r=   r–  )
r—  r(  r‚  rA   r˜  r™  rØ   r9  r|  r£  s
             r   Úread_under_offerrC  '  sD  € ð €CÜ
×
%Ò
% j°*ÀxÑ
P€CðØ"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆÜ�q˜) a™-Ö(ˆAØ�I‰I”o¬¨A«Ñ.Ð;MÐW\ˆIÑ]ðØ×&Ñ& ~¸uÐ&ÑEð ×!Ñ! #Ô&ØˆIØ×.Ñ.¨~Ö>�Ü/°·±Ó0@À#×BSÑBSÐTXÓBYÓZ�ß�6Ø—J‘J˜vÔ&Ø ‘N’Iñ	 ?ð
 ˜A�~Ùñ' )ð* 	�	‰	ŒØ€Jøô% ó Ø˜“6Ü&ð@óAð Aó ðûð" 	�	‰	�ús=   šA#E Á>DÂAE Ã'E Ä	E ÄD?Ä:E Ä>D?Ä?E ÅEz([\d,]+)\s*mileszTransmission\n(.+?)\nzEngine size\n([\d,]+)\s*ccc                 ó¤   • [         R                  U =(       d    S5      nU(       a*  [        UR                  S5      R	                  SS5      5      $ S$ )zÃThe plain "63,900 miles" line on a vehicle's own page (real, still
viewable long after a sale, checked live 2026-08-24), or None if not
found, never guessed. Pure parsing, pulled out for testing.r   r6   r/   N)r²  rf   r2   r>   r1   ©r   rC   s     r   Ú_parse_mileagerF  T  s?   € ô 	×Ñ˜4Ÿ: 2Ó&€AÞ/0Œ3ˆq�w‰w�q‹z×!Ñ! # rÓ*Ó+Ð:°dÐ:r   c                 óŽ   • [         R                  U =(       d    S5      nU(       a  UR                  S5      R                  5       $ S$ )aI  The real "Transmission\nAutomatic" (or Manual, Semi-automatic...)
line in the page's own Vehicle details block (checked live 2026-08-25
against real Manual and Automatic examples, both spelled exactly as
CompMatch itself expects), a genuine confirmed value straight off the
platform, never guessed. None if the line is not there.r   r6   N)Ú_TRANSMISSION_RErf   r>   r   rE  s     r   Ú_parse_transmissionrI  \  s6   € ô 	×Ñ §
¨Ó+€AÞ!"ˆ1�7‰7�1‹:×ÑÓÐ,¨Ð,r   c                 ó°   • [         R                  U =(       d    S5      nU(       d  g[        UR                  S5      R	                  SS5      5      nUS-  S $ )a  The real "Engine size\n1461 cc" line, converted to the same "X.X"
litres text bid_suggest.engine_litres() already parses everywhere
else (real cc figures round to the nearest tenth of a litre the way
a car is normally marketed, e.g. 1461cc reads as 1.5). None if the
line is not there.r   Nr6   r/   éè  z.1f)Ú_ENGINE_CC_RErf   r2   r>   r1   )r   rC   Úccs      r   Ú_parse_engine_ccrN  f  sK   € ô 	×Ñ˜TŸZ RÓ(€AÞØÜ	ˆQ�W‰W�Q‹Z×Ñ  RÓ(Ó	)€BØ�4‰i˜ˆ_Ðr   c                 óª   • U R                  USSS9  U R                  S5        U R                  S5      n[        U5      [	        U5      [        U5      S.$ )ab  Opens a vehicle's own page (a real reg's Didn't win or Purchases
listing, still viewable indefinitely, proven live 2026-08-24 on a
car sold weeks earlier) and reads its real mileage, transmission and
engine size in one visit, for the suggested bid's own retail
enrichment pass (Mark: "% of cazana retail... should improve the
accuracy", then 2026-08-25: "add the transmission and year, engine
cc, grade, from the scrape for better matching" and, once
transmission became a hard gate, "any way to increase the amount of
cars you can suggest bids on"). Neither the Didn't win list nor the
purchases screens carry any of these themselves, but the vehicle's
own page's real "Vehicle details" block does (checked live), the
same genuine confirmed source as a fresh daily read, not a guess or
a badge inferred from a listing name. Takes an already open page (a
shared session across many cars, the same "never a fresh session per
car" lesson already learned for Glass's and DealerKit), not its own
context. Returns {"mileage", "transmission", "engine"}, any of which
is None, never guessed, when its own line is not on the page.rƒ  r„  r…  r$  r  )r\   r`   rd   )r�  r   rw  rF  rI  rN  )r™  rb   r   s      r   Úread_vehicle_detailsrP  s  sV   € ð$ 	‡I�IˆkÐ&8À%€IÑHØ×Ñ˜$ÔØ�?‰?˜6Ó"€DÜ% dÓ+Ô=PÐQUÓ=VÜ& tÓ,ñ.ð .r   z"^(parcel shelf included|op\s*:.*)$c                 ó  • / n[        U S5      n[        U[        5      (       a#  [        UR	                  S5      =(       d    S5      OSn[
        R                  " SU5       Vs/ s H)  oDR                  5       (       d  M  UR                  5       PM+     nnU(       aý  US   R                  S5       Vs/ s HS  ofR                  5       (       d  M  [        R                  UR                  5       5      (       a  MC  UR                  5       PMU     nnU(       a#  UR                  SSR                  U5      -   5        US	S
  HX  nSR                  UR                  5       5      nU(       d  M+  [        R                  U5      (       a  MG  UR                  U5        MZ     [        U S5      n[        U[        5      (       að  SR                  [        UR	                  S5      =(       d2    UR	                  S5      =(       d    UR	                  S5      =(       d    S5      R                  5       5      n	SR                  [        UR	                  S5      =(       d    S5      R                  5       5      n
U	(       d  U
(       a2  UR                  SU
(       a  U	(       a  U
 SU	 3O
U	=(       d    U
-   5        / nU H@  nUR                  S5      nUR                  UR                  S5      (       a  UOUS-   5        MB     SR                  U5      $ s  snf s  snf )a{  What the seller wrote about the car, from a Motorway car page's data
(Steven 2026-09-29: seller notes put him off a car BidBrain never told
him about). Seen live the same day: conditionAndDamage.additionalDetails
opens with the ticked extras on one line ("Rips, Tears or Burns, Parcel
shelf included"), then any free text in paragraphs after a blank line;
an "OP: <plate>" line is the original plate, not a note. relistDetails
says why a car is back in the sale ("Seller Readiness", then why). The
parcel shelf is left out, it says nothing about the car. One string with
each note ending in a full stop, or "" when there is none. Pure.ÚconditionAndDamageÚadditionalDetailsr   z\n\s*\nr   r/   zSeller ticked: r  r6   NrQ   ÚrelistDetailsÚinfoÚdescriptionÚreasonr¶   z
Relisted, r(  z ,;)r  Ú!rË  r  )rÁ   r¸   rº   r0   rŽ   rG   r   r   Ú_NOT_A_NOTEri   r=   r   ÚrstripÚendswith)rÃ   ÚnotesÚcdÚextrar  Úparasr   ÚtickedÚrdÚwhyr¶   rA   rØ   s                r   Úseller_notesrc  �  s'  € ð €EÜ	�TÐ/Ó	0€BÜ6@ÀÄT×6JÑ6JŒC�—‘Ð*Ó+×1¨rÔ2ÐPR€EÜ "§¢¨°UÔ ;ÓIÒ ;˜1¿w¹w¿y‹YˆQ�W‰WŽYÑ ;€EÐIÞØ%*¨1¡X§^¡^°CÔ%8ÓkÒ%8 ¿G¹G¿I“)Ìk×N_ÑN_Ð`a×`gÑ`gÓ`i×Nj“)�!—'‘'–)Ñ%8ˆÐkÞØ�L‰LÐ*¨T¯Y©Y°vÓ->Ñ>Ô?Ø�q�r“ˆAØ—‘˜Ÿ™›Ó#ˆAßˆqœ×*Ñ*¨1×-Ó-Ø—‘˜Q–ñ ô 
�T˜?Ó	+€BÜ�"”d×ÑØ�h‰h”s˜2Ÿ6™6 &›>×\¨R¯V©V°MÓ-B×\ÀbÇfÁfÈXÓFV×\ÐZ\Ó]×cÑcÓeÓfˆØ�x‰xœ˜BŸF™F 6›N×0¨bÓ1×7Ñ7Ó9Ó:ˆÞ–$Ø�L‰L˜¾TÆc¨D¨6°°C°5Ñ)9ÈsÏ{ÐVZÑ[Ô\Ø
€CÛˆØ�H‰H�U‹OˆØ�
‰
˜Ÿ
™
 ?×3Ñ3‘1¸¸S¹ÖAñ ð �8‰8�C‹=Ðùò' Jùâks   Á"LÁ>LÂ1LÃ&LÃ7Lc                 ó”  • SSK nU R                  UR                  SSS9   U R                  SSSS	9  U R                  SS
5      n UR                  U5      n[        US5      n[        R                  " S[        U=(       d    S5      R                  5       5      nU(       a<  UR                  S5       SUR                  S5       SUR                  S5       3Ul        [        US5      n[        U[        5      (       a|  UR!                  S5      (       a(  [        UR!                  S5      =(       d    S5      Ul        UR!                  S5      (       a(  [        UR!                  S5      =(       d    S5      Ul        UR&                  (       d  [)        U5      Ul        [        US5      n[        U5      R                  5       R+                  5       S:X  a#  [        [        US5      =(       d    S5      Ul        [        US5      n	SR/                  [        U	=(       d    S5      R1                  5       R3                  5       5      n	SR/                  [        UR4                  =(       d    S5      R1                  5       R3                  5       5      n
U	(       a  Xš:w  a  X‘l        UR8                  (       dU  [        R:                  " SU5      nU(       d  [        R:                  " SU5      nU(       a  UR                  S5      S-   Ul        UR<                  (       d  [?        U5      SS Ul        U$ ! [         a    Us $ f = f! [         a    Us $ f = f) ag  Open a car's own page and read the fields the new gate rules and the
private plate fallback need, plus the photo while we are there: keeper
start date, known mechanical and electrical issues, warning lights, and
the selling VRM (the original plate when the seller keeps a private one).
Reads the page's embedded data. Missing fields stay empty, never guessed.r   Nrƒ  r„  r…  r  r  rŒ  r  r  ÚkeeperStartDatez(\d{2})/(\d{2})/(\d{4})r   rS   r  r   r6   ÚelectricalAndMechanicalIssuesÚhasMechanicsProblemsÚmechanicsProblemsDescúdeclared by sellerÚhasElectricsProblemsÚelectricsProblemsDescÚhasWarningLightsrÏ   ÚwarningLightsDescÚshowingrÀ   rÄ  z6https://[A-Za-z0-9._\-]*imgix[A-Za-z0-9._/\-]*?\.jpe?gz?w=640&auto=format,compress) r:  r�  rb   r’  r9  r  ÚloadsrÁ   rG   r   r0   r   r>   Úkeeper_startr¸   rº   rŽ   Ú
mech_issueÚelectrical_issuer\  rc  rk   Úwarning_lightsr   rÂ   r   rW   Úselling_vrmra   rf   Ú
photo_urlsr  )r™  rŠ   rB  r  rÃ   ÚksrC   ÚissuesÚhwlÚsvrW   s              r   Úgate_detailsrz  ²  sš  € ó Ø‡I�Iˆc�o‰oÐ*<Àe€IÑLðØ×ÑÐ/°zÈ5ÐÑQð × Ñ Ð!1Ð3IÓJ€DðØ�{‰{˜4Ó ˆô
 
�TÐ,Ó	-€BÜ
�ŠÐ/´°R·X¸2³×1DÑ1DÓ1FÓG€AÞØŸg™g a›j˜\¨¨1¯7©7°1«:¨,°a¸¿¹À»
°|ÐDˆÔô ˜Ð>Ó?€FÜ�&œ$×ÑØ�:‰:Ð,×-Ñ-Ü  §¡Ð,CÓ!D×!\ÐH\Ó]ˆCŒNØ�:‰:Ð,×-Ñ-Ü#& v§z¡zÐ2IÓ'J×'bÐNbÓ#cˆCÔ ð �9�9Ü  Ó&ˆŒ	ô �dÐ.Ó
/€CÜ
ˆ3ƒx‡~�~Ó×ÑÓ 5Ó(Ü ¤¨TÐ3FÓ!G×!TÈ9ÓUˆÔô 
�T˜<Ó	(€BØ	�‰”�R—X˜2“×$Ñ$Ó&×,Ñ,Ó.Ó	/€BØ
�'‰'”#�c—g‘g—m Ó$×*Ñ*Ó,×2Ñ2Ó4Ó
5€CÞ	ˆb‹iØŒð �=�=Ü�IŠIÐhÐjnÓoˆÞÜ—	’	ÐSÐUYÓZˆAÞØŸG™G A›JÐ)FÑFˆCŒMð �>�>Ü2°4Ó8¸¸!Ð<ˆŒà€Jøôy ó ØŠ
ðûô
 ó ØŠ
ðús#   ¡L& ÁL8 Ì&L5Ì4L5Ì8MÍMzLhttps://motorway-photos[A-Za-z0-9.\-]*\.imgix\.net/[A-Za-z0-9._/\-]*?\.jpe?gc                 óÖ   • 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 the detail page's __NEXT_DATA__ blob,
deduped to one URL per distinct photo. A standalone function purely for
testing, enrich_from_detail is the only real caller.r   z?w=900&auto=format,compress)Ú_GALLERY_PHOTO_REr9   Ú
setdefaultr>   r7   rE  )r   ÚseenÚgms      r   r  r  ÿ  sS   € ð €DÜ×(Ñ(¨Ö.ˆØ�‰˜Ÿ™ › R§X¡X¨a£[Ð3PÑ%PÖQñ /ä�—‘“ÓÐr   c                 óV   ^^^• U4S jm/ mUUU4S jmT" U / 5        T(       a  TS   $ S$ )u2  CAP clean out of a car's page data, by name (2026-09-14, Steven:
"its showing cap retail instead of cap clean"). Until now the read took
whichever price entry sat next to priceSource CAP in the raw page text,
which is Motorway's own CAP based price for the car, not CAP clean.
Only a key that names CAP and clean counts ("capClean", "capCleanValue",
cap: {clean: ...}), a plain number, a "Â£5,195" string or a {value} dict,
and never one that names retail. None when the page does not carry it:
an honest gap the card shows as "not read", never the wrong figure.c                 óª  >• [        U [        5      (       a  g [        U [        [        45      (       a  U S:”  a  [        U 5      $ S $ [        U [        5      (       aJ  [
        R                  " SU 5      nU(       a*  [        UR                  S5      R                  SS5      5      $ S $ [        U [        5      (       a  S H  nX ;   d  M
  T" X   5      s  $    g )Nr   rñ   r/   r   )rµ   rÝ   rü   )
r¸   r¹   r2   rÞ   r0   rG   rf   r>   r1   rº   )r•   rC   r¼   Únums      €r   r‚  Úfind_cap_clean.<locals>.num  sª   ø€ Ü�aœ×ÑØÜ�aœ#œu˜×&Ñ&Ø ›U”3�q“6Ð,¨Ð,Ü�aœ×ÑÜ—	’	˜+ qÓ)ˆAÞ78”3�q—w‘w˜q“z×)Ñ)¨#¨rÓ2Ó3ÐB¸dÐBÜ�aœ×ÑÛ1�Ø•6Ù˜q™t›9Ò$ñ 2ð r   c                 óÎ  >• [        U [        5      (       a¥  U R                  5        H�  u  p#XR                  5       /-   nSR	                  U5      nSU;   a<  SU;   a6  SUR                  5       ;  a"  T" U5      nU(       a  TR                  U5        Mk  [        U[        [        45      (       d  Mˆ  T	" X45        M’     g [        U [        5      (       a  U S S  H  nT	" X15        M     g g )NrQ   r  r  r  é   )r¸   rº   rì   rk   r   r=   r7   )
r  r  r¼   r•   r  ÚjoinedrØ   rG  r‚  r  s
          €€€r   r  Úfind_cap_clean.<locals>.walk   s·   ø€ Ü�aœ×ÑØŸ™ž	‘�ØŸG™G›I˜;Ñ&�ØŸ™ !›�Ø˜F“? w°&Ó'8¸XÈQÏWÉWËYÓ=VÙ˜A›�AÞØŸ™ QœÙ Ü˜a¤$¬ ×.Ó.Ù˜–Jò "ô ˜œ4× Ñ Ø�s˜“V�Ù�Q–ò ð !r   r   N© )rÃ   rG  r‚  r  s    @@@r   r7  r7  	  s0   ú€ õð €E÷ñ 	ˆˆr„NÞˆ5�‰8Ð& $Ð&r   z\bcap\s*clean\bc           
      ó  • [        U =(       d    S5      R                  S5       Vs/ s H)  oR                  5       (       d  M  UR                  5       PM+     nn[        U5       Hœ  u  p1[        R                  U5      (       d  M!  SUR                  5       ;  d  M7  [        U[        US-   [        U5      5      5       H?  n[        R                  X$   5      nU(       d  M#  [        UR                  S5      5      s  s  $    Mž     gs  snf )z÷CAP clean out of a page's visible text (2026-09-15): the money on the
line that says "CAP clean", or on one of the two lines after it, the
way Carwow's car page is read. None when no such label is on the
page. A label naming retail is never taken.r   rF   r  rS   r6   N)r0   r   r   r:   Ú_CAP_CLEAN_LABELrf   rk   rm   rn   r   ro   r4   r>   )r   rL   r}   rB   rƒ   rC   s         r   r8  r8  6  s½   € ô
 !$ D§J¨B£× 5Ñ 5°dÔ ;ÓIÒ ;˜1¿w¹w¿y‹YˆQ�W‰WŽYÑ ;€EÐIÜ˜%Ö ‰ˆÜ×"Ñ" 1×%Ó%¨(¸!¿'¹'»)Õ*CÜ˜1œc ! a¡%¬¨U«Ó4Ö5�Ü—L‘L ¡Ó*�ß�1Ü §¡¨£
Ó+Ô+ó 6ñ !ð ùò Js   ¦DÁDc           	      ó¦  • SSK nUR                  =(       d'    [        R                  S   S   S-   [	        USS5      -   nU R                  USS	S
9   U R                  SSSS9  U R                  SS5      n UR                  U5      n[        US5      n[        U[        5      (       a.  [        UR                  S5      [        5      (       a
  US   Ul        [!        X5      nU[#        X5      -  nU[%        X5      -  nUR&                  c.  [)        U5      Ul        UR&                  b  UR+                  S5         U R-                  S5      n[/        XQXx5        [        US5      n	[        US5      n
U	(       d  U
(       a’  Sn U	(       a  [1        [        U	5      S-  S5       OSnSR3                  S UR4                  U[7        U
=(       d    S5      4 5       5      R9                  5       Ul        [7        U
=(       d    UR<                  5      Ul        [        US5      n[        U[6        5      (       a  U(       a  XÁl        [A        U5      nU(       d   [C        U R-                  S5      5      nU(       a  XÑl"        U$ ! [         a    Us $ f = f! [         a    Us $ f = f! [         a    Sn GNaf = f! [         a    Sn GNf = f! [         a    Sn Ngf = f)a  Open a car's own page and read the fields the list card does not show:
previous owners, exact engine size and fuel, service history and CAP. Reads
the page's embedded data, not guessed HTML. Leaves a field as is if it
cannot be read, so the brain can hold the car back rather than guess.r   NrT   rU   r  rt   r   rƒ  r„  r…  r  r  rŒ  r  r  ÚpreviousOwnersrò   zfinance from page datar  Ú
engineSizer_   rK  r6   rQ   c              3   ó6   #   • U  H  o(       d  M  Uv •  M     g 7fr	   rˆ  ©r   Úxs     r   r   Ú%enrich_from_detail.<locals>.<genexpr>x  s   é € ÐXÒ)R AÓVWŸa™aÒ)Rùó   ‚
�	ÚserviceHistory)#r:  rb   r!   rs   r�   r�  r’  r9  r  ro  rÁ   r¸   rº   rŽ   r2   ÚownersrÅ   r÷   rï   rÎ   ræ   r=   rw  rC  rª  r   rZ   r0   r   rd   ru   r6  r7  r8  Ú	cap_clean)r™  rŠ   rB  rÒ  r  rÃ   r”  rÄ   Ú_page_wordsÚengr_   ÚlitresÚshr  s                 r   Úenrich_from_detailrš  E  s•  € ó
 Ø
�/‰/×
nœgŸm™m¨JÑ7¸
ÑCÀlÑRÔU\Ð]`ÐbhÐjlÓUmÑm€CØ‡I�IˆcÐ0¸%€IÑ@ðà×ÑÐ/°zÈ5ÐÑQð × Ñ Ð!1Ð3IÓJ€DðØ�{‰{˜4Ó ˆô ˜Ð/Ó0€FÜ�&œ$×Ñ¤J¨v¯z©z¸'Ó/BÄC×$HÑ$HØ˜G‘_ˆŒ
ô % SÓ/€FØ
Ô'¨Ó2Ñ2€FØ
Ô'¨Ó2Ñ2€FØ
‡{�{ÑÜ'¨Ó-ˆŒØ�;‰;Ñ"Ø�M‰MÐ2Ô3ðØ—o‘o fÓ-ˆô �d Ô5ä
�d˜LÓ
)€CÜ�t˜VÓ$€DÞ
Ždð ˆð	Þ58œœc #›h t™m¨QÓ/Ñ0¸bˆFð —X‘XÑX¨#¯.©.¸&Ä#ÀdÇjÈbÃ/Ñ)RÓXÓX×^Ñ^Ó`ˆŒ
Ü˜×) §	¡	Ó*ˆŒ	ä	�TÐ+Ó	,€BÜ�"”c×ÑžrØ Ôô
 ˜Ó
€CÞð	Ü# D§O¡O°FÓ$;Ó<ˆCö ØŒà€Jøô{ ó ØŠ
ðûô
 ó ØŠ
ðûô* ó Ø‹ðûô ó 	Ø‹Fð	ûô$ ó 	ØŠCð	úsZ   ÁI7 Á5J	 Ä8J Å>"J. ÉK É7JÊJÊ	JÊJÊJ+Ê*J+Ê.J>Ê=J>ËKËKc                 óè   • U (       d  g [         R                  " S[        U 5      5      nU(       d  g UR                  S5      R	                  SS5      nUR                  5       (       a  [        U5      $ S $ )Nz[\d,]+r   r/   r   )rG   rf   r0   r>   r1   r
   r2   )r3   rC   rØ   s      r   Ú_moneyrœ  �  sU   € ÞØÜ
�	Š	�)œS ›VÓ$€AÞØØ	�‰�‹
×Ñ˜3 Ó#€AØ—Y‘Y—[‘[Œ3ˆq‹6Ð* dÐ*r   c           	      ón  • Sn[         R                  " SS[        U=(       d    S5      5      nUR                  5       (       a(  [	        U5      S:”  a  [        [	        U5      S-  S5       nSR                  S U[        U =(       d    S5      [        U=(       d    S5      4 5       5      R                  5       $ )zÄBuild the fullest engine description for the ban check: the litres from
the engine size, the model and trim text (which carries family names like
TSI, TFSI, EcoBoost, PureTech, DCI), and the fuel.r   z\Dr   rK  r6   rQ   c              3   ó6   #   • U  H  o(       d  M  Uv •  M     g 7fr	   rˆ  r�  s     r   r   Ú#_engine_from_csv.<locals>.<genexpr>¢  s   é € ÐPÒJ˜!Ëa—A‘AÒJùr’  )rG   rH   r0   r
   r2   rª  r   r   )rY   Úengine_sizer_   r˜  rM  s        r   Ú_engine_from_csvr¡  š  s‡   € ð €FÜ	�Š��rœ3˜{×0¨bÓ1Ó	2€BØ	‡z�z‡|�|œ˜B› !›Üœ#˜b›' D™.¨!Ó,Ð-ˆØ�8‰8ÑP ¬¨E¯K°RÓ(8¼#¸d¿jÀb»/ÑJÓPÓP×VÑVÓXÐXr   c                 ó¦   • [        U 5       VVs/ s H&  u  pSU=(       d    SR                  5       ;   d  M$  UPM(     nnnU(       a  US   $ S$ s  snnf )zºWhich of the Download buttons on screen to press once the export
menu is open: the last one whose words say Download (the opener is
the first), or None when there is none (pure, tested).Údownloadr   rP   N)r:   rk   )ÚlabelsrB   r   r@  s       r   Úpick_download_buttonr¥  ¥  sJ   € ô $ FÔ+ÔOÒ+‘$�!¨z¸a¿gÀ2¿_¹_Ó=NÑ/N�AÑ+€DÑOÞˆ4�‰8Ð% Ð%ùó Ps
   �#A¶A)r–  úgot itÚdismissÚokÚokayÚcontinueÚacceptúnot nowÚlaterú	no thanksÚskipÚdoneÚ
understoodô   Ã—r�  c                 óÂ   • [        U 5       HP  u  pU=(       d    SR                  5       R                  5       nU[        ;   d  UR	                  S5      (       d  MN  Us  $    g)zwWhich button in a pop up closes it: the first whose words are one
of the usual closing words (pure, tested), else None.r   )r–  r¦  r§  r®  r¬  N)r:   r   rk   ÚDISMISS_WORDSrz  )r¤  rB   r   r  s       r   Úpick_dismiss_buttonrµ  ±  sO   € ô ˜&Ö!‰ˆØ�W�"�O‰OÓ×#Ñ#Ó%ˆØ”Ó §¡Ð.d×!eÓ!eØŠHñ "ð r   c           
      óD  • / n[        S5       GHd  nU R                  S5       Vs/ s H  o3R                  5       (       d  M  UPM     nnU(       d    GO$U GH  nUR                  5       =(       d    SR	                  5       R                  SS5      SS nUR                  S5      n[        U Vs/ s H3  owR                  5       =(       d    UR                  S	5      =(       d    SPM5     sn5      nUb;   Xh   R                  S
S9  UR                  SU< S35        U R                  S5        M×  U R                  R                  S5        U R                  S5        UR                  SU< 35        GM     GMg     SR                  U5      $ s  snf s  snf ! [         a     Nrf = f)zƒClose every open pop up on the page by its own closing button, or
Escape when it has none. Returns a short note of what was closed.rS   z[role=dialog], dialog[open]r   rF   rQ   Nr!  Úbuttonz
aria-labelé¸  rŠ  zclosed z with its own buttonr¶  ÚEscapezpressed Escape on ú; )rm   r•  Ú
is_visiblerw  r   r1   rµ  rx  rž  r=   r   r9  ÚkeyboardÚpressr   )	r™  r\  rÔ  r  Údialogsr   ÚbtnsÚbÚwhichs	            r   Údismiss_dialogsrÂ  »  ss  € ð €EÜ�1�XˆØ"×5Ñ5Ð6SÔTÓgÒT˜×XdÑXd×Xf—1ÑTˆÐgÞÚÜˆAØ—L‘L“N×( b×/Ñ/Ó1×9Ñ9¸$ÀÓDÀTÀcÐJˆDØ×'Ñ'¨Ó1ˆDÜ'ÑjnÓ(oÒjnÐef¯,©,«.×*_¸A¿O¹OÈLÓ<Y×*_Ð]_Ò*_ÑjnÑ(oÓpˆEØÑ ðØ‘K×%Ñ%¨dÐ%Ñ3Ø—L‘L 7¨4©(Ð2FÐ!GÔHØ×)Ñ)¨#Ô.Ùð �M‰M×Ñ Ô)Ø×!Ñ! #Ô&Ø�L‰LÐ-¨d©XÐ6×7ô ñ	 ð& �9‰9�UÓÐùò% hùò )pøô !ó Ùðús#   ¥FÁFÂ-:FÃ28FÆ
FÆFc           	      óÔ  ^ • U 4S jnU" 5       SL a  g/ nSU 4S j4SU 4S j4SU 4S	 j4S
U 4S j44 HT  u  p4 U" 5         T R                  S5        UR                  U5        U" 5       SLa  U" 5       (       a  SU 3s  $ SU S3s  $ MV     SSR                  U5      -   $ ! [         a>  nUR                  U S[        U5      R	                  5       S   SS  35         SnAM¯  SnAff = f)a  Tick the Filtered vehicles radio on Motorway's export panel and say
how it went: "ticked", "already ticked", or what was tried when it
would not take. Tries the real radio input by its label first, then a
role=radio element, then the words themselves, and reads the tick back
after each.c                  ó”  >•  TR                  S5      n U R                  5       (       a#  [        U R                  R	                  5       5      $   TR                  SSS9nUR                  5       (       a5  UR                  R                  S5      =(       d    SR                  5       S:H  $  g ! [
         a     Nif = f! [
         a     g f = f)NúFiltered vehiclesú[role=radio]©Úhas_textúaria-checkedr   rÐ   )	Úget_by_labelrò   r¹   r“   Ú
is_checkedr9  Úlocatorrx  rk   )ÚinpÚelr™  s     €r   r  Ú%tick_filtered_vehicles.<locals>.stateÛ  sÁ   ø€ ð	Ø×#Ñ#Ð$7Ó8ˆCØ�y‰y�{‰{Ü˜CŸI™I×0Ñ0Ó2Ó3Ð3ð ð	Ø—‘˜nÐ7J�ÐKˆBØ�x‰x�z‰zØŸ™×.Ñ.¨~Ó>×DÀ"×KÑKÓMÐQWÑWÐWð ð øô ó 	Ùð	ûô ó 	ØØð	ús%   ƒAB* ÁAB: Â*
B7Â6B7Â:
CÃCTzalready tickedz	its inputc                  óT   >• T R                  S5      R                  R                  SS9$ )NrÅ  r¸  rŠ  )rÊ  r“   ré  ©r™  s   €r   Ú<lambda>Ú(tick_filtered_vehicles.<locals>.<lambda>í  s&   ø€ ˜d×/Ñ/Ð0CÓD×JÑJ×PÑPÐY]ÐPÑ^r   z	its radioc                  óR   >• T R                  SSS9R                  R                  SS9$ )NrÆ  rÅ  rÇ  r¸  rŠ  )rÌ  r“   rž  rÑ  s   €r   rÒ  rÓ  î  s(   ø€ ˜dŸl™l¨>ÐDW˜lÐX×^Ñ^×dÑdÐmqÐdÑrr   z	its wordsc                  ó$   >• T R                  SSS9$ )Nútext=Filtered vehiclesr¸  rŠ  ©rž  rÑ  s   €r   rÒ  rÓ  ï  s   ø€ ˜dŸj™jÐ)AÈ4˜jÑPr   zthe circle before its wordsc                  óX   >• T R                  S5      R                  R                  S5      $ )NrÖ  z‘el => { const p = el.closest('label') || el.parentElement; const c = p && (p.querySelector('input') || p.firstElementChild); (c || el).click(); })rÌ  r“   ÚevaluaterÑ  s   €r   rÒ  rÓ  ð  s'   ø€ °·±Ð=UÓ0V×0\Ñ0\×0eÑ0eð `ô1ar   é�  Fz
ticked by úpressed z, tick unreadablez	 failed: r   Nr  zwould not tick: rº  )r   r=   r9  r0   Ú
splitlinesr   )r™  r  Útriedr¶   ÚactionrA  s   `     r   Útick_filtered_vehiclesrß  Õ  s  ø€ õñ ƒw�$‚ØØ€Eà	Ô^Ð_Ø	ÔrÐsØ	ÔPÐQØ	&ô )að 	bó	‰ˆð	JÙŒHØ×!Ñ! #Ô&Ø�L‰L˜ÔÙ‹w˜eÒ#Ù.3¯g©g˜ D 6Ð*Ò]¸XÀdÀVÐK\Ð;]Ò]ñ $ñð  §	¡	¨%Ó 0Ñ0Ð0øô ó 	JØ�L‰L˜D˜6 ¬3¨q«6×+<Ñ+<Ó+>¸qÑ+AÀ#À2Ð+FÐ*GÐH×IÒIûð	Jús   ¹ABÁ?BÂ
C'Â)3C"Ã"C'c                 óº   •  U R                  S5      (       a  gU R                  S5      (       a  g[        U R                  S5      5      S:„  $ ! [         a     gf = f)ztWhether Download's own menu is showing: a menu item, or the
'Filtered vehicles' choice, or a second Download button.z,[role=menu], [role=menuitem], [role=listbox]TrÖ  úbutton:has-text("Download")r6   F)Úquery_selectorr   r•  r9  rÑ  s    r   Úexport_menu_openrã  þ  s_   € ðØ×ÑÐM×NÑNØØ×ÑÐ7×8Ñ8ØÜ�4×*Ñ*Ð+HÓIÓJÈQÑNÐNøÜó Ùðús   ‚A ™A °A Á
AÁAc                 óR  ^• U=(       d    SR                  5       nU=(       d    SR                  5       nU =(       d    SR                  5       mSU;   d  SU;   d  SU;   d  SU;   a  gSU;   d  SU;   a  g[        U4S	 jS
 5       5      (       a  [        U4S jS 5       5      (       d  gg)zßWhether a web answer seen during the export is the CSV itself, or
the link to it (pure, tested): a csv content type, an attachment, or
an address that says csv, export or download and is not a page
script, style or picture.r   útext/csvúapplication/csvúapplication/vnd.ms-excelúoctet-streamTÚ
attachmentú.csvc              3   ó,   >#   • U  H	  oT;   v •  M     g 7fr	   rˆ  )r   r  Úus     €r   r   Ú*looks_like_csv_response.<locals>.<genexpr>  s   øé € Ð
9Ò8�a�Ž6Ò8ùs   ƒ)ÚcsvÚexportr£  c              3   ód   >#   • U  H%  nTR                  U5      =(       d    US -   T;   v •  M'     g7f)rË  N)r[  )r   r�  rì  s     €r   r   rí    s>   øé € ð  Fiò  nhÐhiÀaÇjÁjÐQRÃm×FcÐWXÐ[^ÑW^ÐbcÑWcÔFcò  nhùs   ƒ-0)z.jsz.cssz.pngrß  z.svgz.woffz.woff2F)rk   r   )rÒ  Úcontent_typeÚdispositionÚctr]  rì  s        @r   Úlooks_like_csv_responserô    s°   ø€ ð
 ×
˜"×	#Ñ	#Ó	%€BØ
×
˜×	"Ñ	"Ó	$€BØ	��×ÑÓ€AØ�RÓÐ,°Ó2Ð6PÐTVÓ6VÐZhÐlnÓZnØØ�rÓ˜V r›\ØÜ
Ô
9Ñ8Ó
9×9Ñ9Ä#ô  Fiñ  nhó  Fi÷  Ciñ  CiØØr   c                 ó2  ^^^^^• SSK n/ m/ mU4S jmUU4S jnUU4S jmUR                  ST5        UR                  ST5        U R                  SU5         U" 5         UR                  5       US	-  -   nUR                  5       U:  Ga-  T(       aá  TS    UR                  ST5        UR                  ST5        U R                  SU5        Ub¢  T H›  n	 U	R                  n
UR                  S
U
R                   SU
R                  SS  S[        U
R                  5      SS < 35         UR                  U	R                  SS  SU	R                   SU	R                  5       SS < 35        M�     $ $ T(       Ga  [        S T 5       5      (       aþ  UR                  5       [        T5      S-   :”  aÞ    UR                  ST5        UR                  ST5        U R                  SU5        Ub¢  T H›  n	 U	R                  n
UR                  S
U
R                   SU
R                  SS  S[        U
R                  5      SS < 35         UR                  U	R                  SS  SU	R                   SU	R                  5       SS < 35        M�     ggUR                  S5        UR                  5       U:  a  GM-    UR                  ST5        UR                  ST5        U R                  SU5        Ub¢  T H›  n	 U	R                  n
UR                  S
U
R                   SU
R                  SS  S[        U
R                  5      SS < 35         UR                  U	R                  SS  SU	R                   SU	R                  5       SS < 35        M�     gg! [         a     GNÖf = f! [         a   nUR                  SU S35         SnAGN¢SnAff = f! [         a=  nUR                  U	R                  SS  SU	R                   SU S35         SnAGMD  SnAff = f! [         a     GNAf = f! [         a   nUR                  SU S35         SnAGNSnAff = f! [         a=  nUR                  U	R                  SS  SU	R                   SU S35         SnAGM¯  SnAff = f! [         a     GNÃf = f! [         a   nUR                  SU S35         SnAGN�SnAff = f! [         a=  nUR                  U	R                  SS  SU	R                   SU S35         SnAGM1  SnAff = f!  UR                  ST5        UR                  ST5        U R                  SU5        O! [         a     Of = fUGb  T GH  n	 U	R                  n
UR                  S
U
R                   SU
R                  SS  S[        U
R                  5      SS < 35        O,! [         a  nUR                  SU S35         SnAOSnAff = f UR                  U	R                  SS  SU	R                   SU	R                  5       SS < 35        MË  ! [         a=  nUR                  U	R                  SS  SU	R                   SU S35         SnAGM  SnAff = f   f f = f)a‰  Press, then wait up to timeout_ms for a file to arrive on this page
or on any page the site opens meanwhile. Returns the Download, or
None. seen (a list) is filled with every web answer during the wait
that looks like the CSV or its link (url, status, content type,
disposition), read by the caller when no browser download comes
(2026-09-08: Motorway stopped handing the file to the browser).r   Nc                 ó(   >• TR                  U 5        g r	   )r=   )r  Úgots    €r   Úon_downloadÚ'_wait_for_download.<locals>.on_download&  s   ø€ Ø�
‰
�1�r   c                 óN   >• U R                  ST5        U R                  ST5        g )Nr£  rÞ  )Úon)r  rø  Úon_responses    €€r   r8  Ú#_wait_for_download.<locals>.on_page(  s   ø€ Ø	�‰ˆZ˜Ô%Ø	�‰ˆZ˜Õ%r   c                 ó8  >•  U R                   nUR                  SS5      UR                  SS5      p2TbV  [        U R                  X#5      (       a:  TR	                  U R                  U R
                  X#45        TR	                  U 5        g g g ! [         a     g f = f)Núcontent-typer   zcontent-disposition)ÚheadersrŽ   rô  rÒ  r=   rÜ   r9  )râ   Úhró  r]  Úanswersr~  s       €€r   rü  Ú'_wait_for_download.<locals>.on_response+  sŠ   ø€ ð	Ø—	‘	ˆAØ—U‘U˜>¨2Ó.°·±Ð6KÈRÓ0P�ØÑÔ$;¸A¿E¹EÀ2×$JÑ$JØ—‘˜QŸU™U A§H¡H¨bÐ5Ô6Ø—‘˜qÕ!ð %KÐøô ó 	Ùð	ús   ƒBB Â
BÂBr£  rÞ  r™  g     @�@zasked rQ   r!  z with rÚ  zasked: not read (Ú)z HTTP r(  r%  z: body not read (c              3   ó@   #   • U  H  u  pp4[        X#U5      v •  M     g 7fr	   )Úcsv_answer_ready)r   rÔ  r3   ró  r]  s        r   r   Ú%_wait_for_download.<locals>.<genexpr>C  s   é € ÐQÊD¹L¸AÀ"Ô,¨Q°B×7Ð7ÊDùr   r  éú   )Útimerû  Úremove_listenerr9  rú  r=   ÚmethodrÒ  r0   Ú	post_datarÜ   r   r   Úseen_atr   )r˜  r™  rž  Ú
timeout_msr~  Úbodiesr	  r8  Údeadlinerâ   ÚreqrA  r  r÷  rø  rü  s       `       @@@@r   Ú_wait_for_downloadr    s[  ü€ ó Ø
€CØ€Gõö&öð 	‡G�GˆJ˜Ô$Ø‡G�GˆJ˜Ô$Ø‡F�Fˆ6�7Ôð%YÙŒØ—9‘9“; ¨fÑ!4Ñ4ˆØ�i‰i‹k˜HÔ$ÞØ˜1‘vð	Ø× Ñ  ¨[Ô9Ø× Ñ  ¨[Ô9Ø×Ñ ¨Ô0ð ÑÛ�ð<ØŸ)™)�CØ—M‘M F¨3¯:©:¨,°a¸¿¹ÀÀ¸°ÀfÌSÐQT×Q^ÑQ^ÓM_Ð`dÐadÐMeÑLhÐ"iÔjðYØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÀÀAÇFÁFÃHÈTÈcÀNÑCUÐ"VÖWò ð ÷ œÑQÉDÓQ×QÑQÐVZ×V_ÑV_ÓVaÔdkÐlpÓdqÐtuÑduÓVuØð	Ø× Ñ  ¨[Ô9Ø× Ñ  ¨[Ô9Ø×Ñ ¨Ô0ð ÑÛ�ð<ØŸ)™)�CØ—M‘M F¨3¯:©:¨,°a¸¿¹ÀÀ¸°ÀfÌSÐQT×Q^ÑQ^ÓM_Ð`dÐadÐMeÑLhÐ"iÔjðYØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÀÀAÇFÁFÃHÈTÈcÀNÑCUÐ"VÖWò ð ð ×!Ñ! #Ô&ð �i‰i‹k˜HÖ$ð ð	Ø× Ñ  ¨[Ô9Ø× Ñ  ¨[Ô9Ø×Ñ ¨Ô0ð ÑÛ�ð<ØŸ)™)�CØ—M‘M F¨3¯:©:¨,°a¸¿¹ÀÀ¸°ÀfÌSÐQT×Q^ÑQ^ÓM_Ð`dÐadÐMeÑLhÐ"iÔjðYØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÀÀAÇFÁFÃHÈTÈcÀNÑCUÐ"VÖWò ð øô ó 	Úð	ûô !ó <Ø—M‘MÐ$5°a°S¸Ð":×;Ò;ûð<ûô !ó YØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÐARÐSTÐRUÐUVÐ"W×XÓXûðYûô ó 	Úð	ûô !ó <Ø—M‘MÐ$5°a°S¸Ð":×;Ò;ûð<ûô !ó YØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÐARÐSTÐRUÐUVÐ"W×XÓXûðYûô ó 	Úð	ûô !ó <Ø—M‘MÐ$5°a°S¸Ð":×;Ò;ûð<ûô !ó YØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÐARÐSTÐRUÐUVÐ"W×XÓXûðYûð'	Ø× Ñ  ¨[Ô9Ø× Ñ  ¨[Ô9Ø×Ñ ¨Õ0øÜó 	Ùð	úð ÒÜ�ð<ØŸ)™)�CØ—M‘M F¨3¯:©:¨,°a¸¿¹ÀÀ¸°ÀfÌSÐQT×Q^ÑQ^ÓM_Ð`dÐadÐMeÑLhÐ"iÕjøÜ ó <Ø—M‘MÐ$5°a°S¸Ð":×;Ñ;ûð<úðYØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÀÀAÇFÁFÃHÈTÈcÀNÑCUÐ"VÖWøÜ ó YØ—M‘M Q§U¡U¨4¨C [ M°¸¿¹°zÐARÐSTÐRUÐUVÐ"W×XÓXûðYúò ð ús^  Á=T. Â6NÃAN'Ä+AOÅ3?T. Æ46P Ç3AP/ÉAQÊ%T. Ê:6R& Ë9AR7ÍAS$Î
N$Î#N$Î'
OÎ1OÏOÏ
PÏ1PÐPÐ
P,Ð+P,Ð/
QÐ9QÑQÑ
R#Ñ&1RÒR#Ò&
R4Ò3R4Ò7
S!ÓSÓS!Ó$
T+Ó.1T&Ô&T+Ô.ZÔ06U'Õ&ZÕ'
U4Õ1ZÕ3U4Õ4ZÖAW×Z×
X 	×!W;	×6Z×;X 	Ø ZØAYÙZÙ
Z	Ù1Z
	ÚZÚ
Z	ÚZc                 óÔ   • U=(       d    SR                  5       nU=(       d    SR                  5       nSU;   d  SU;   d  SU;   d  SU;   a  gSU;   d  SU;   a  gU S	:H  =(       a    S
U;  $ )a²  Whether a CSV looking answer is the file itself or a link to it,
rather than Motorway saying the file is still being prepared (pure,
tested). Since 8 September 2026 pressing Download makes the page call
generate_vehicle_list_csv (200, an empty JSON string) and then poll
get_vehicle_list_csv, which answers 202 JSON until the file is ready.
Neither JSON answer is the file; only a csv type, an attachment or a
200 that is not JSON is.r   rå  ræ  rç  rè  Tré  rê  r)  r:  r  )rÜ   rñ  rò  ró  r]  s        r   r  r  b  st   € ð ×
˜"×	#Ñ	#Ó	%€BØ
×
˜×	"Ñ	"Ó	$€BØ�RÓÐ,°Ó2Ð6PÐTVÓ6VÐZhÐlnÓZnØØ�rÓ˜V r›\ØØ�S‰=×-˜V¨2Ñ-Ð-r   c                 ó  • / nS Hv  n[        U 5       Hd  u  p4pVSU=(       d    SR                  5       ;   =(       d    SU=(       d    SR                  5       ;  nXr:X  d  ML  X1;  d  MS  UR                  U5        Mf     Mx     U$ )z®The addresses worth fetching for the file, best first: the download
call (get_vehicle_list_csv) before the generate call, newest first
within each, no repeats (pure, tested).©TFÚget_vehicle_list_csvr   Úgenerate)Úreversedrk   r=   )r~  rA   Úpick_getrÒ  rÜ   ró  r]  Úis_gets           r   Úcsv_urls_in_orderr  s  ss   € ð €CÛ!ˆÜ#+¨D¦>ÑˆC˜Ø+°·	°r×/@Ñ/@Ó/BÑB×kÀjÐY\×YbÐ`b×XiÑXiÓXkÑFkˆFØÕ! c¥nØ—
‘
˜3–ó $2ñ "ð
 €Jr   c                 ót   • SSK n[        U 5      nU[        ;  a  UR                  5       [        U'   [        U   $ )z@When the first CSV looking answer was seen, remembered per list.r   N)r	  r5  Ú_seen_clock)r~  r	  rå   s      r   r  r  €  s1   € ãÜ
ˆT‹(€CØ
”+ÓØŸ9™9›;Œ�CÑÜ�sÑÐr   c                 ó  • SSK n[        S[        X4-  5      5      n[        U5       GHû  nSUR	                  5       ;   a  UOSn	/ n
[        U	5       GH“  n U R                  R                  USS9nUR                  5       nUR                  R                  S5      =(       d    SnUSS	 R                  S
SS9nU
R                  [        UR                  5      5        UbÃ  US:  d  UR                  S;  a­  UR                  R                  S5      =(       dF    UR                  R                  S5      =(       d$    UR                  R                  S5      =(       d    SnUR                  SUS-    SUSS  SUR                   SUSS  SUSS < U(       a  SU-   OS 35        UR                  (       aÛ  SU;   aÕ  SU;   d  [        U5      S:”  aÀ  UR!                  5       R#                  S5      (       dœ  [%        US5       nUR'                  U5        SSS5        U(       a  SUS-   U-   S 3OSnUb*  UR                  S!USS  S"3SR)                  U
5      -   5        S#[        U5       S$USS%  S&UR                   S'U=(       d    S( S)U 3
s  s  $ UR                  S;   d+  S*UR	                  5       ;   a4  UR!                  5       SS S+;   a  US-   U	:  a  UR+                  U5        GM”    O   Uc  GMÇ  U
(       d  GMÑ  UR                  S!USS  S"3SR)                  U
5      -   5        GMþ     g! , (       d  f       GN!= f! [,         a<  nUb-  UR                  SUS-    SUSS  S,[        U5      SS-  S)35         SnA  M“  SnAff = f).a3  Fetch the CSV the page was given, through the same signed in
browser, and save it to out. The download call is asked first and,
while Motorway answers 202 (still preparing the file, 2026-09-15),
asked again every pause_s seconds for up to wait_s. Returns a note, or
None when nothing fetched reads as a CSV.r   Nr6   r  r  rŠ  rÿ  r   rœ  ú	utf-8-sigr1   )ÚerrorsrS   )éÊ   r¶  zx-request-idzx-amzn-requestidzcf-rayzpoll z of éF   z: HTTP rQ   é(   r!  z id r/   rF   r)  Ú<Úwbz after zs of Motorway preparing itzpoll answers for r(  zfetched z bytes from éZ   z (HTTP r  zno content typer  r:  )Ú{r  Ú"z: not answered (éP   )r	  Úmaxr2   r  rk   rm   rú  rŽ   r  r   Údecoder=   r0   rÜ   r¨  r   Úlstriprz  r<  r=  r   Úsleepr9  )r˜  r~  rA   Úwait_sÚpause_sÚlogr	  ÚtriesrÒ  ÚpollsÚstatusesrB   râ   r  ró  ÚheadÚridr”   ÚwaitedrA  s                       r   Úfetch_csv_responser7  ‰  sH  € ó Ü�”3�vÑ'Ó(Ó)€EÜ  ×&ˆØ/°3·9±9³;Ó>‘ÀAˆØˆÜ�u—ˆAðØ—K‘K—O‘O C°�OÐ7�Ø—v‘v“x�Ø—i‘i—m‘m NÓ3×9°r�Ø˜E˜T�{×)Ñ)¨+¸iÐ)ÐH�Ø—‘¤ A§H¡H£Ô.Ø‘?¨¨A«°·±ÀÓ1KØŸ)™)Ÿ-™-¨Ó7×}¸1¿9¹9¿=¹=ÐI[Ó;\×}Ð`a×`iÑ`i×`mÑ`mÐnvÓ`w×}Ð{}�CØ—J‘J  q¨1¡u g¨T°#°c°r°(°¸7À1Ç8Á8À*ÈAÈbÐQTÐRTÈgÈYÐVWÐX\Ð]aÐ^aÐXbÑWeÖx{ÐgmÐpsÒgsð  BDð  fEð   Fô  GØ—4—4˜C 4›K¨T°T«\¼SÀ»YÈ»_ÐVZ×VaÑVaÓVc×VnÑVnÐor×VsÑVsÜ˜c 4œ¨AØŸ™ œ÷ )æXY˜w¨¨A©°Ñ'8Ð&9Ð9SÑTÐ_a�FØ‘ØŸ
™
Ð%6°s¸3¸B°x°jÀÐ#CÀcÇhÁhÈxÓFXÑ#XÔYØ%¤c¨$£i [°¸SÀÀ"¸X¸JÀgÈaÏhÉhÈZÐWYÐZ\×ZqÐ`qÐYrÐrsÐtzÐs{Ð|Ô|ð —8‘8˜zÓ)¨f¸¿¹»
Ó.BÀtÇ{Á{Ã}ÐUWÐVWÐGXÐ\kÓGkØ˜1‘u˜u“}ØŸ
™
 7Ô+ÚÙñ1 ð: Œ?Ÿx™xØ�J‰JÐ*¨3¨s°¨8¨*°BÐ7¸#¿(¹(À8Ó:LÑL×MñC 'ðD ÷) )žûô ó Ø‘?Ø—J‘J  q¨1¡u g¨T°#°c°r°(°Ð;KÌCÐPQËFÐSVÐTVÈKÈ=ÐXYÐZÔ[Þûðús9   ÁF'L:Ç<L(ÈA9L:ÊAL:Ì(
L7Ì2L:Ì:
N Í0M;Í;N c           	      óè  • SSK n[        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  [        R                  R                  USU S35      n U R                  =(       d    S	S
S.nSSUS   [        US   S5      S.n U R                  S5      n	U	(       a  U	R                  5       OSn
U
(       a\  [        S[        U
S   5      S-
  5      n[        S[        U
S   5      S-
  5      nX¼[        SUS   U-
  5      [        SUS   U-
  5      S.nU R                  SSUSS9nUR!                  U5      R#                  S5      nSU S[$        R$                  R'                  5       R)                  SS9 S U 3S!U S"[+        U5       S#3/n[-        US$S%S&9 nUR/                  S'R                  U5      S'-   5        [1        S[+        U5      S(5       H  nUR/                  UUUS(-    S'-   5        M     SSS5        g! [         a     Nóf = f! , (       d  f       g= f! [         aH  n[-        US$S%S&9 nUR/                  S)U S'35        SSS5         SnAg! , (       d  f        SnAg= fSnAff = f)*ad  A small picture of the screen around the Download button, as base64
JPEG text in data/logs/motorway_<tag>.log, 400 characters a line, so
Dealer OS's diagnostics (text log tails only, 300 lines each) carry it
and it can be looked at from anywhere. Clipped to the Download
button's neighbourhood and squeezed hard so it fits in under 300
lines. Never raises.r   NrÃ   rù   Trú   Ú	motorway_z.logi   i   )ÚwidthÚheightr:  r;  é¼  )r�  rÓ   r:  r;  rá  r�  i0  rÓ   r  iø  i  rã  r#  Úcss)ÚtypeÚqualityÚclipÚscaleÚasciiz	Motorway z	 picture r
  r  r(  zclip z bytes z  lines below are one base64 JPEGr  r*  r+  rF   rÚ  úno picture: )Úbase64r.  r  r/  r0  r1  r   r2  Úviewport_sizern   râ  Úbounding_boxr*  r2   r9  Ú
screenshotÚ	b64encoder+  r3  r4  r5  r   r<  r=  rm   )r™  Útagrb  rD  r?  rù   r  Úvwr@  Úbtnr¡  r�  rÓ   Úrawr   r4  r”   rB   rA  s                      r   Ú_export_picturerM  ¶  s‘  € ó Ü�7‰7�?‰?œ2Ÿ7™7Ÿ?™?¬2¯7©7¯?©?¼2¿7¹7¿?¹?Ì8Ó;TÓ+UÓVÓW€DÜ�7‰7�<‰<˜˜f fÓ-€DÜ‡K‚K�˜tÒ$Ü�7‰7�<‰<˜ 	¨#¨¨dÐ3Ó4€Dð*Ø×Ñ×A¨T¸SÑ#AˆØ˜Q¨¨G©ÄÀBÀxÁLÐRUÓ@VÑWˆð	Ø×%Ñ%Ð&CÓDˆCÞ(+�#×"Ñ"Ô$°ˆCÞÜ˜œ3˜s 3™x›=¨3Ñ.Ó/�Ü˜œ3˜s 3™x›=¨2Ñ-Ó.�Ø´°S¸"¸W¹+È¹/Ó1JÔVYÐZ]Ð_aÐbjÑ_kÐnoÑ_oÓVpÑq�ð �o‰o 6°2¸DÈˆoÐNˆØ×Ñ Ó$×+Ñ+¨GÓ4ˆØ˜C˜5 	¬(×*;Ñ*;×*?Ñ*?Ó*A×*KÑ*KÐU^Ð*KÐ*_Ð)`Ð`bÐcfÐbgÐhØ˜�v˜W¤S¨£X JÐ.NÐOðQˆä�$˜ gÒ.°!Ø�G‰G�D—I‘I˜d“O dÑ*Ô+Ü˜1œc $›i¨Ö-�Ø—‘˜˜Q˜q 3™w˜¨$Ñ.Ö/ñ .÷ /Ð.øô ó 	Ùð	ú÷ /Õ.ûô ó *Ü�$˜ gÒ.°!Ø�G‰G�l 1 # RÐ(Ô)÷ /×.×.Ö.þð*ús{   Ã/J Ä
BI> ÆBJ ÈAJÉ5J É>
JÊJ Ê
JÊJ Ê
JÊJ ÊJ Ê
K1Ê)K,Ê4KË
K,Ë
K)	Ë!K,Ë)K,Ë,K1c           	      óP  • [         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  S[        R                  R                  5       R                  SS9 SU 3/nU H9  n[        S	[        U5      S
5       H  nUR                  SXgUS
-    -   5        M     M;      UR                  SU R                   35        UR                  SSR                  S U R                  R                   5       5      -   5        U R!                  S5      nUR#                  S USS  5       5         U R%                  [         R                  R                  US5      SS9  UR                  S5        [)        [         R                  R                  US5      SSS9 n
U
R+                  SR                  U5      S-   5        SSS5        g! [&         a  n	UR                  SU	 35         Sn	A	NySn	A	ff = f! [&         a  n	UR                  SU	 35         Sn	A	N¤Sn	A	ff = f! , (       d  f       g= f)zöWhat Motorway's export screen showed when the CSV did not come:
the address, every open tab, every visible button, menu item and
dialog line, written to data/logs/motorway_export.log (which Dealer
OS's diagnostics carry) with a picture beside it.rÃ   rù   Trú   zMotorway export r
  r  r(  r   i|  z
  answer: zpage: ztabs: z | c              3   ó8   #   • U  H  oR                   v •  M     g 7fr	   )rÒ  )r   r  s     r   r   Ú_export_eyes.<locals>.<genexpr>ì  s   é € Ð*MÒ:L°Q¯5®5Ò:Lùs   ‚aÞ  () => {
          const vis = e => { const r = e.getBoundingClientRect(); const s = getComputedStyle(e); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; };
          const pick = (sel, kind, n) => [...document.querySelectorAll(sel)].filter(vis).map(e => kind + ': ' + (e.innerText || e.textContent || '').trim().replace(/\s+/g, ' ').slice(0, n || 80) + (e.disabled ? ' [disabled]' : '') + (e.getAttribute('href') ? ' -> ' + e.getAttribute('href').slice(0, 80) : ''));
          const named = pick('button', 'button').filter(s => s !== 'button: ');
          const blank = pick('button', 'button').length - named.length;
          const dialogs = [...document.querySelectorAll('[role=dialog], dialog')].filter(vis).map(d => 'dialog: ' + (d.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 500) + ' || its buttons: ' + [...d.querySelectorAll('button')].map(b => (b.innerText || b.getAttribute('aria-label') || '').trim()).join(' | '));
          return [...named, 'unlabelled buttons: ' + blank, ...pick('[role=menuitem], [role=option], [role=menuitemradio], [role=menuitemcheckbox]', 'menu item'),
                  ...dialogs, ...pick('a[download], a[href*=csv], a[href*=export]', 'link')];
        }c              3   ó,   #   • U  H
  nS U 3v •  M     g7f)r  Nrˆ  )r   r3   s     r   r   rP  ö  s   é € Ð2¢z !�r˜!˜•X¢zùs   ‚Nr!  zmotorway_export.pngF)r  Ú	full_pagez&picture: data/logs/motorway_export.pngrC  zcould not read the screen: zmotorway_export.logr  r*  r+  rF   )r.  r  r/  r0  r1  r   r2  r3  r4  r5  rm   r   r=   rÒ  ÚcontextrŽ  rÙ  r7  rG  r9  r<  r=  )r™  rb  r^  r?  rù   r}   r�  rB   r~  rA  r”   s              r   Ú_export_eyesrT  Û  s  € ô
 �7‰7�?‰?œ2Ÿ7™7Ÿ?™?¬2¯7©7¯?©?¼2¿7¹7¿?¹?Ì8Ó;TÓ+UÓVÓW€DÜ�7‰7�<‰<˜˜f fÓ-€DÜ‡K‚K�˜tÒ$Ø¤× 1Ñ 1× 5Ñ 5Ó 7× AÑ AÈ9Ð AÐ UÐVÐVXÐY\ÐX]Ð^Ð_€Eó ˆÜ�qœ#˜a›& #Ö&ˆAØ�L‰L˜¨¨A°©G¨Ñ4Ö5ó 'ñ ð8Ø�‰�v˜dŸh™h˜ZÐ(Ô)Ø�‰�X §
¡
Ñ*M¸$¿,¹,×:LÒ:LÓ*MÓ MÑMÔNØ�}‰}ð ó ˆð 	�‰Ñ2 t¨D¨S¡zÓ2Ô2ð	-Ø�O‰O¤§¡§¡¨dÐ4IÓ!JÐV[ˆOÑ\Ø�L‰LÐAÔBô
 
Œb�g‰g�l‰l˜4Ð!6Ó7¸ÀwÒ	OÐSTØ	�‰�—	‘	˜%Ó  4Ñ'Ô(÷ 
PÐ	Oøô	 ó 	-Ø�L‰L˜<¨ sÐ+×,Ñ,ûð	-ûäó 8Ø�‰Ð2°1°#Ð6×7Ñ7ûð8úç	OÕ	OúsI   Ä#BI, Æ,?I È$JÉ
I)ÉI$ÉI, É$I)É)I, É,
JÉ6JÊJÊ
J%c           
      ó‚  ^^• SSK nSSKn[        R                  R	                  UR
                  " 5       S5      n[        R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       nUR                  [        R                  S   S   SSS	9  UR                  S
SS9  U(       aW  UR                  S5        UR                  [        R                  S   S   SSS	9  UR                  S
SS9  UR                  S5        [        U5      nUR!                  SSS9  UR                  S5        [#        USS5        [%        U5      (       dX  [        U5        UR&                  R)                  S5        UR                  S5        UR!                  SSS9  UR                  S5        [%        U5      (       d'  [+        USU(       a  SU S3OS-   5        [-        S5      e[/        U5      n	[#        USSU	 S35        UR1                  S5      mT V
s/ s H)  oªR3                  5       =(       d    SR5                  5       PM+     nn
[7        U5      mTc  [+        US5        [-        S5      e/ n/ n[9        XgUU4S  jS!XÍS"9n[#        US#S$UT   < S%[;        U5       35        Ub  UR=                  U5        O~/ nU(       a  [?        XlUS&S'US(9OSnURA                  U5        U(       d@  [+        US)UT   < S*3S+R	                  S, U 5       5      =(       d    S--   US.9  [-        S/5      e[C        S0U 35        URE                  5         [G        US1S29 n[I        URK                  U5      5      nSSS5        W(       d  [-        S35      e[M        U5      $ s  sn
f ! URE                  5         f = f! , (       d  f       NE= f)4a+  Download the filtered stock as a CSV (the report Motorway provides) and
turn every row into a Car. This reads the whole brief matching list in one
go and carries owners, engine size, CAP, service history and the link, so no
per car detail page is needed. Fails loudly if the download or parse fails.r   Nzbidbrain_mw_export.csvrT   r�  Ú	stock_urlrƒ  r„  r…  úa[href^="/vehicles/"]r‰  rŠ  zL() => { try { localStorage.clear(); sessionStorage.clear(); } catch (e) {} }r$  rá  é@  r<  Úmenuzafter the first Download pressr¹  i,  i„  z0pressed Download twice and its menu never openedz (closed first: r  r   zbMotorway's Download menu did not open. What the screen showed is in data/logs/motorway_export.log.Úchosenz!after ticking Filtered vehicles (z)no Download button after opening the menuzzMotorway download button not found. The page may have changed. What the screen showed is in data/logs/motorway_export.log.c                  ó*   >• T T   R                  5       $ r	   r×  )ÚbuttonsrÁ  s   €€r   rÒ  Úread_export.<locals>.<lambda>K  s   ø€ ¸À¹×9MÑ9MÔ9Or   i ¿ )r~  r  Úpressedza moment after pressing z; csv looking answers seen: é–   rS   )r.  r/  r0  rÛ  z*, no file came; csv looking answers seen: rº  c              3   óB   #   • U  H  u  pp4US S  SU SU 3v •  M     g 7f)Nr¨  rQ   rˆ  )r   rì  r3   ró  rÔ  s        r   r   Úread_export.<locals>.<genexpr>W  s.   é € Ð/^ÒY]É+È!ÐPR°1°T°c°7°)¸1¸Q¸C¸qÀÀÕ0EÒY]ùs   ‚rÖ   )r^  zƒMotorway did not produce the CSV within 3 minutes of pressing Download. What the screen showed is in data/logs/motorway_export.log.zMotorway export: r  r+  z9Motorway CSV downloaded but had no rows. Stopping loudly.)'rî  rï  r.  r  r   Ú
gettempdirr!   r�  rŽ  r�  r�  rs   r’  rÙ  r   rÂ  rž  rM  rã  r¼  r½  rT  r“  rß  r•  rw  r   r¥  r  r   Úsave_asr7  r7  Úprintr–  r<  r7   Ú
DictReaderÚparse_export_rows)r—  r‚  Úclear_storagerî  rï  rA   r˜  r™  Úclosedr`  rÀ  r¤  r~  r  r£  ÚpolledÚnoter”   r¢  r\  rÁ  s                      @@r   Úread_exportrk    s�  ù€ ó
 ÛÜ
�'‰'�,‰,�x×*Ò*Ó,Ð.FÓ
G€CÜ
×
%Ò
% j°*ÀxÑ
P€CðQØ"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆØ�	‰	”'—-‘- 
Ñ+¨KÑ8ÐEWÐafˆ	Ñgð 	×ÑÐ6ÀÐÑFÞð �M‰MÐhÔiØ�I‰I”g—m‘m JÑ/°Ñ<ÐI[ÐejˆIÑkØ×"Ñ"Ð#:ÀEÐ"ÑJØ×!Ñ! $Ô'ô ! Ó&ˆØ�
‰
Ð0¸$ˆ
Ñ?Ø×Ñ˜cÔ"ô 	˜˜fÐ&FÔGÜ ×%Ñ%Ü˜DÔ!Ø�M‰M×Ñ Ô)Ø×!Ñ! #Ô&Ø�J‰JÐ4¸dˆJÑCØ×!Ñ! #Ô&Ü ×%Ñ%Ü˜ÐQÖu{ÐWgÐhnÐgoÐopÑUqð  BDñ   Eô  FÜð   Dó  Eð  Eô (¨Ó-ˆÜ˜˜hÐ*KÈFÈ8ÐSTÐ(UÔVØ×)Ñ)Ð*GÓHˆÙ:AÓBº'°Q—<‘<“>×' R×.Ñ.Ö0¹'ˆÐBÜ$ VÓ,ˆØ‰=Ü˜ÐJÔKÜð  ]ó ^ð ^ð
 ˆØˆÜ% cÕ1OÐQWÐ^bÑrˆÜ˜˜iÐ+CÀFÈ5ÁMÑCTÐTpÔqtÐuyÓqzÐp{Ð)|Ô}ØÑØ×Ñ˜SÕ!ð ˆFÞ\`Ô% c°¸SÈ!ÐQWÒXÐfjˆDØ�M‰M˜&Ô!ÞÜ˜T X¨f°U©mÑ->Ð>hÐ#iØ&*§i¡iÑ/^ÑY]Ó/^Ó&^×&hÐbhñ$jØqwòyä"ð $aó bð bäÐ% d VÐ,Ô-à�	‰	Œä	ˆc˜KÒ	(¨AÜ�C—N‘N 1Ó%Ó&ˆ÷ 
)æÜÐVÓWÐWÜ˜TÓ"Ð"ùòE Cøð8 	�	‰	�úç	(Õ	(ús,   ÁGN È0NÉC/N ÍN0ÎN ÎN-Î0
N>c                 ó@  • / nU  GH‚  nS[        UR                  SS5      5      R                  5       ;  a  M3  [        S/0 SUR                  S5      =(       d    SR	                  5       _SUR                  S5      =(       d    SR	                  5       _SUR                  S	5      =(       d    SR	                  5       _S
[        UR                  S5      5      _S[        UR                  S5      5      _S[        UR                  S5      5      _S[        UR                  S5      5      _S[        UR                  S5      5      _S[        UR                  S5      5      _SUR                  S5      =(       d    SR	                  5       _S[        UR                  S	5      UR                  S5      UR                  S5      5      _SUR                  S5      =(       d    SR	                  5       _SUR                  S5      =(       d    SR	                  5       _SUR                  S5      =(       d    SR	                  5       _S UR                  S!5      =(       d    SS"-   UR                  S#5      =(       d    S-   R	                  5       _S$UR                  S%5      =(       d    SR	                  5       _S&UR                  S'5      =(       d    SR	                  5       _S(S)_6nUR                  S5      =(       d    SR	                  5       Ul        UR                  S*5      =(       d    SR	                  5       R                  5       S+:X  a6  UR                  S,5      =(       d    SR	                  5       =(       d    S-Ul	        UR                  U5        GM…     U(       d  [        S.5      eU$ )0aO  Turn already parsed CSV rows (a list of dicts, one per csv.DictReader
row) into Cars. Pulled out of read_export as a pure function so the
column mapping can be tested against synthetic rows, no browser or real
download needed, the same separation every other reader in this project
already keeps between the live fetch and the parsing.z	live salezBuying typer   rW   ÚVRMrX   ÚMakerY   ÚModelr[   ÚYearr\   ÚMileager”  zNumber of ownersrR   zExterior grader]   zReserve pricer•  zCAP clean valuer6  zService historyrd   zEngine sizeÚFuelr`   ÚTransmissionr_   rª   z	Body typeÚ	equipmentÚ	EquipmentrQ   zAdditional specificationsr²   ÚLocationrb   zMotorway vehicle linkrc   rV   z%Mechanical issues disclosed by sellerrÏ   z!Additional mechanical informationri  z;Motorway CSV had rows but no auction cars. Stopping loudly.rˆ  )r0   rŽ   rk   r    r   r4   rœ  r¡  ru   rq  r=   r“  )r¢  rw   râ   rŠ   s       r   rf  rf  e  sü  € ð €DÜˆàœc !§%¡%¨°rÓ":Ó;×AÑAÓCÓCÙÜò 
Ø—‘�u“×# ×*Ñ*Ô,ð
à—%‘%˜“-×% 2×,Ñ,Ô.ð
ð —5‘5˜“>×' R×.Ñ.Ô0ð
ô �a—e‘e˜F“mÔ$ð	
ô
 ˜Ÿ™˜yÓ)Ô*ð
ô ˜Ÿ™Ð0Ó1Ô2ð
ô �q—u‘uÐ-Ó.Ô/ð
ô ˜1Ÿ5™5 Ó1Ô2ð
ô ˜QŸU™UÐ#4Ó5Ô6ð
ð ŸU™UÐ#4Ó5×;¸×BÑBÔDð
ô $ A§E¡E¨'£N°A·E±E¸-Ó4HÈ!Ï%É%ÐPVË-ÔXð
ð Ÿ%™% Ó/×5°2×<Ñ<Ô>ð
ð —%‘%˜“-×% 2×,Ñ,Ô.ð
ð —u‘u˜[Ó)×/¨R×6Ñ6Ô8ð
ð Ÿ™˜kÓ*×0¨b°CÑ7¸1¿5¹5ÐA\Ó;]×;cÐacÑd×kÑkÔmð
ð  —e‘e˜JÓ'×-¨2×4Ñ4Ô6ð!
ð" Ÿ™Ð6Ó7×=¸2×DÑDÔFð#
ñ$ ð%
ˆð( —U‘U˜6“]×( b×/Ñ/Ó1ˆŒ	ð �E‰EÐ9Ó:×@¸b×GÑGÓI×OÑOÓQÐUZÓZØŸe™eÐ$GÓH×NÈB×UÑUÓW×oÐ[oˆCŒNØ�‰�C×ñQ öR ÜÐXÓYÐYØ€Kr   c                 óî  • [         R                  " U SSS9n UR                  (       a  UR                  S   OUR                  5       nUR	                  [         R
                  S   S   SSS9   UR                  S	S
S9  UR                  5       nUR                  5         [        U5      nU(       d  [        S5      eU(       a  USU $ U$ ! [         a    [        S5      ef = f! UR                  5         f = f)z¨Open the logged in stock list and read the cards. Fails loudly if the
page does not look like the stock list, so a changed page never passes
quietly wrong data through.rT   Tr�  r   rV  rƒ  r„  r…  rW  r‰  rŠ  z³Motorway stock list did not show any vehicle cards. The page may have changed, or the login may have expired, or it is before the 4:30pm stock time. Stopping rather than guessing.z:Read the Motorway page but found no cars. Stopping loudly.N)r!   r�  rŽ  r�  r�  rs   r’  r9  r“  rÆ  r–  r‹   )r—  Úlimitr˜  r™  r?   rw   s         r   Ú	read_livery  š  så   € ô ×
%Ò
% j°*ÀtÑ
L€CðØ"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆØ�	‰	”'—-‘- 
Ñ+¨KÑ8ÐEWÐafˆ	Ñgð	Ø×"Ñ"Ð#:ÀEÐ"ÑJð —L‘L“Nˆ	à�	‰	Œä˜Ó#€DÞÜÐWÓXÐXÞ ˆ4��ˆ<Ð* dÐ*øô ó 	ÜðDóð ð	ûð 	�	‰	�ús$   ˜AC" Á.C	 Á>C" Ã	CÃC" Ã"C4z+label[class*="ShortlistStar_shortlistStar"]c           	      ó  • U R                  SU 3SSS9   U R                  [        SS9  U R                  [        5      R                  nUR                  S	5      S
:H  nXB:w  aj  UR                  5         [        S5       HK  nU R                  S5        U R                  [        5      R                  R                  S	5      S
:H  U:X  d  MK    O   Un[        S5       Ht  nU R                  SU 3SSS9  U R                  [        SS9  U R                  S5        U R                  [        5      R                  R                  S	5      S
:H  nXb:X  d  Mt    O   Xb:w  a  [	        SU SU SU S35      eg! [         a    [	        SU S35      ef = f)až  Add or remove one car from Motorway's own shortlist, on an already
open page (the same continuous session shape every other Motorway
write or detail read in this project uses, never a fresh context per
car). Navigates to the car's own detail page, reads the real current
state, clicks only when it differs from the desired one, then verifies
with a fresh page reload (never trusts the click's own local DOM
change alone, a lesson from Carwow's own equivalent, see its own
docstring). Returns True once genuinely confirmed, raises loudly on
anything else (the star not found at all, or the state still not
matching after a real reload) rather than silently claim success.z$https://pro.motorway.co.uk/vehicles/rƒ  r„  r…  rµ  rŠ  zMotorway vehicle z€ did not show its own shortlist star. The page may have changed, the car may no longer be listed, or the login may have expired.rÉ  rÐ   r…  r¶  é   r$  z still shows shortlisted=z after trying to set it to r  T)r�  r’  Ú_SHORTLIST_STAR_SELr9  r“  rÌ  r“   rx  rž  rm   r   )r™  rt  ÚwatchedÚstarÚcurrentrÔ  Ú	confirmeds          r   Úset_watchlistr�  Ä  s·  € ð 	‡I�IÐ4°[°MÐBØ+°Uð ñ <ð'Ø×ÑÔ2¸EÐÑBð �<‰<Ô+Ó,×2Ñ2€DØ× Ñ  Ó0°FÑ:€GØÓØ�
‰
Œô �r–ˆAØ×!Ñ! #Ô&Ø—‘Ô0Ó1×7Ñ7ß"‘] >Ó2°fñ=ØAHõIáñ	 ð €IÜ�1ŽXˆØ�	‰	Ð8¸¸ÐFØ/¸ð 	ñ 	@à×ÑÔ2¸EÐÑBØ×Ñ˜dÔ#Ø—L‘LÔ!4Ó5×;Ñ;×IÑIÈ.ÓYÐ]cÑcˆ	ØÕÙñ ð ÓÜØ ˜}Ð,EÀiÀ[ð Q)Ø)0¨	°ð4ó5ð 	5ð øôI ó 'ÜØ ˜}ð -&ð &ó'ð 	'ð'ús   –E, Å,Fc                 ó  • SSK nSSKJn  U R                  USSS9  U R                  =(       d    SR                  5       nSU;   d  S	U;   a  UR                  S
U5      $  U R                  SSSS9  U R                  SS5      nUR                  U5      n U R                  SSS9   U R                  S5      n	UR                  XvU	5      n
UR!                  5       U
S'   X*S'   UR#                  U5      nUR%                  SX5        USS  H  n['        SUSS  35        M     U
$ ! [         a3  nUR                  S[        U5      R                   S3U5      s SnA$ SnAff = f! [         a     NÉf = f! [         a    Sn	 NÉf = f)a  The shortlist deep read of one Motorway car page (2026-09-17): its
embedded data and its words, handed to bidbrain.deep_read.from_motorway
for the push's deep block. A dead login or a page without its data
is an answer with an error, never blank. Reads only, never acts.r   N)Ú	deep_readrƒ  r„  r…  r   z/signinz/loginz<Motorway login needed. Run: python3 login.py motorway manualr  r  rŒ  r  r  z,Motorway's car page did not carry its data (z2); the page may have changed or the login expired.r  rX  rŠ  r  Úread_atÚasked_atrT   r)  z    éŒ   )r:  Úbidbrainrƒ  r�  rÒ  rk   Úfailedr’  r  ro  r9  r>  Ú__name__Úwait_for_load_staterw  Úfrom_motorwayÚ	now_stampÚmotorway_key_notesÚ	log_notesrd  )r™  rb   r…  rB  Ú_deeprÒ  r  rÃ   rA  r  r  r\  rØ   s                r   rƒ  rƒ  ú  s¨  € ó
 Ý+Ø‡I�IˆkÐ&8À%€IÑHØ�8‰8�>�r×
 Ñ
 Ó
"€CØ�CÓ˜8 s›?Ø�|‰|ÐZÐ\dÓeÐeð[Ø×ÑÐ/°zÈ5ÐÑQØ×$Ñ$Ð%5Ð7MÓNˆØ�{‰{˜4Ó ˆðØ× Ñ  ¸Ð Ñ=ðØ—‘ Ó'ˆð 	×Ñ˜D¨Ó.€AØ—?‘?Ó$€A€i�LØ€j�MØ×$Ñ$ TÓ*€EØ	‡O�O�J Ô3ð �3�B‹ZˆÜ��Q�t˜�W�IÐÖñ à€Høô' ó [Ø�|‰|ÐJÌ4ÐPQË7×K[ÑK[ÐJ\ð  ]Oð  Pð  RZó  [õ  	[ûð[ûô ó Ùðûô ó ØŠðúsB   Á4D  ÂE  Â$E0 Ä 
EÄ*(EÅEÅEÅ 
E-Å,E-Å0E?Å>E?)rˆ  Nr	   )T)r6   T)r6   TNN)NN)éð   r  N)rˆ  r  )�Ú__doc__r.  rG   Úbidbrain.readersr   Úcompiler   r   r3  rÈ  rI   rï  rñ  Úpricingr    r   r!   Úplatesr"   rg   rp   rh   rl   rj   ro   rq   rr   r4   r8   rD   rM   r‹   r�   rv   rö   rë   r»   rÅ   rä   rà   rß   rá   ræ   rï   rô   r÷   r-  rC  rÁ   r‘  r'  r{  ry  ra  rc  r:   rk   rd  ri  r  rš  r¤  r­  r°  r²  r³  rÃ  rÇ  rÉ  Ú
IGNORECASErÎ  r×  rÚ  rÛ  r  rÿ  r
  r  r  r  r  r  r"  r*  r6  r0  r3  r<  rB  r@  rC  rH  rL  rF  rI  rN  rP  rY  rc  rz  r|  r  r7  rŠ  r8  rš  rœ  r¡  r¥  r´  rµ  rÂ  rß  rã  rô  r  r  r  r  r  r7  rM  rT  rk  rf  ry  r|  r�  rƒ  )rB   rC   s   00r   Ú<module>r—     s>  ðñó  
Û 	å *ð �JŠJÐ7Ó8€	òeó Û Û Û Ý Ý Ý à
�
Š
Ð2Ó3€Ø	‡z‚zÐ-Ó.€Ø
�*Š*Ð'Ó
(€Ø�:Š:Ð*¨B¯D©DÓ1€Ø
�
Š
Ð#Ó$€ÚU€ÚN€	ò+ð �ZŠZÐBÀBÇDÁDÓI€
ò
ò>ò{#ð@Q€ò%ð6 mØ9ØPØ8ØIñ€ð .ØCØ7Øfñ	€òòðNd€à6€ØL€òò0òò6ò"ð$ Ð ô9òxð" H€ð
F€ð �JŠJÐ2Ó3€	ðIÐ ð �jŠjÐNÐPR×PTÑPTÓU€Ø�jŠjÐAÓB€Ù$-ÚXÐZ[ô%]ô ^ò %]™D˜Aˆ1�7‰7‹9�aŠ<ñ %]ò ^€ôò>9ôx/ôd òFò*:ð. �jŠjÐ.Ó/€òò=ðJ —Z’ZØVóXÐ òEð@ Ÿ*š*ØvØ‡M�MóÐ ò
)ð\ �ZŠZØlØ‡M�Mó€
ò
)ð& XÐ ò.òb6ð: LÐ ð —
’
ÐCÓD€Ø—j’jÐ!OÓPÐ ò òF+ò27ô.)ðf J€ò(òVô (ðV z€ò"ôJ%ðP �jŠjÐ,¨b¯d©dÓ3€Ø—:’:Ð6Ó7Ð Ø—
’
Ð8¸"¿$¹$Ó?€ò;ò-ò
ò.ð2 �jŠjÐ>ÀÇÁÓE€ò òFFðR —J’JØSóUÐ òò''ðT —:’:Ð0°"·$±$Ó7Ð òòHòV+òYò&ðP€òòò4&1òR
òô"@YðF €ò.ò"
òô*òZ"*ôJ$)ôN`#òF2ôj+ðN DÐ ò3õl"ùóY7^s   Å,M*