ó
    
5¯j»\  ã                   óÜ  • S r SSKrSSKrSSKJrJr  \R                  " S5      r\R                  " S\R                  5      r	\R                  " S\R                  5      r
\R                  " S5      rS	 rS
 rS rS rS rS rS rS rS rSrS rS rS rS rS rS/S jrS0S jr\R                  " S5      r\R                  " S\R<                  5      r\R                  " S\R                  \R<                  -  5      r \R                  " S\R                  \R<                  -  5      r!\R                  " S\R                  \R<                  -  5      r"\R                  " S\R<                  5      r#\R                  " S \R<                  5      r$\R                  " S!\R<                  5      r%S"r&S# r'S$ r(S% r)S1S& jr*S'S(.S) jr+S* r,S+ r-S2S, jr.\R                  " S-\R<                  5      r/S. r0g)3a  
Clickdealer in stock reader.

Reads Steven's own in stock list from myclickdealer.co.uk (stock_list.php,
status ins) and counts vehicles per make and model, so the cockpit can flag an
auction car as filling a gap or as a model he already holds plenty of.

The list is a classic server rendered table. Each data row carries the VRM and
a combined "MAKE MODEL" cell (for example VAUXHALL CORSA, BMW 1 SERIES,
MERCEDES-BENZ A-CLASS). Recon captured the real layout 2026-06-10, see
data/inspect/clickdealer_tab0.html.

Model matching: Clickdealer, Motorway and Carwow name the same car differently
(MERCEDES-BENZ A-CLASS versus Mercedes A 180). model_key() normalises both
sides through CompMatch's own canonical grouping (bid_suggest.model_key), so
MERCEDES-BENZ A-CLASS and Mercedes A Class both key to "mercedes a", and a BMW
118D keys to "bmw 1" alongside a BMW 1 Series. This is an annotation, never a
gate, so an occasional mismatch only mislabels a badge.

This reads a report screen only. It never changes anything on Clickdealer.
é    Né   )Úbid_suggestÚbrowserz0[A-Z]{2}\d{2}\s?[A-Z]{3}|[A-Z]\d{1,3}\s?[A-Z]{3}z<tr[^>]*>(.*?)</tr>z<td[^>]*>(.*?)</td>z<[^>]+>c                 ó    • SR                  [        U =(       d    S5      R                  5       R                  SS5      R	                  5       5      $ )z4Lower case, hyphens to spaces, collapsed whitespace.Ú Ú Ú-)ÚjoinÚstrÚlowerÚreplaceÚsplit©Útexts    Ú=/Users/stevendouglas/BidBrain/bidbrain/readers/clickdealer.pyÚ_normr   %   s8   € à�8‰8”C˜Ÿ
 “O×)Ñ)Ó+×3Ñ3°C¸Ó=×CÑCÓEÓFÐFó    c                 ó¨   • [        U 5      (       a  [        U5      (       d  g[        R                  " U  S[        U5       35      R                  5       $ )aK  The shared make+model key used on BOTH sides of the stock match, and
by the sold history note and the top picks. CompMatch's own canonical
grouping (bid_suggest.model_key) lower cased, reused rather than copied
so all three land on exactly the same group.

Examples: ("MERCEDES-BENZ", "A-CLASS") -> "mercedes a",
("Vauxhall", "Corsa Energy AC") -> "vauxhall corsa",
("BMW", "1 Series Sport") and ("BMW", "118D M Sport") -> "bmw 1".

Before 2026-09-20 this took the first token of the make plus the first
token of the model on its own, which split one real model in two
wherever a platform prints the engine badge as the model word: a BMW
118D keyed to "bmw 118d" and never met the "bmw 1" stock count, a
Mercedes A 180 never met an A-CLASS, an "MG Motor UK ZS" keyed to "mg
motor", and Motorway's truncated "Insig" never met an Insignia. So a
model that was actually sitting in stock read as a gap, and the top
picks, which score partly on that gap, inherited it. CompMatch had
already solved the same problem for the suggested bid.

An empty make or an empty model still keys to "" (never a guess).r   r   )r   r   Ú	model_keyr   )ÚmakeÚmodels     r   r   r   *   sC   € ô* ��;‰;œe EŸl™lØô × Ò  D 6¨¬5°«<¨.Ð!9Ó:×@Ñ@ÓBÐBr   c           	      óÖ   • [         R                  U 5       Vs/ s HD  nSR                  [        R	                  SU5      R                  5       5      R                  5       PMF     sn$ s  snf )Nr   )Ú_CELL_REÚfindallr
   Ú_TAG_REÚsubr   Ústrip)Úrow_htmlÚts     r   Ú_cellsr    G   sN   € ÜCK×CSÑCSÐT\ÔC]Ó^ÒC]¸aˆC�H‰H”W—[‘[  aÓ(×.Ñ.Ó0Ó1×7Ñ7Ö9ÑC]Ñ^Ð^ùÒ^s   ˜AA&c                 ó,  • 0 n/ n[         R                  U 5       Hõ  n[        U5      n[        U5      S:  a  M  Sn[	        USS 5       H,  u  pgU(       d  M  [
        R                  U5      (       d  M*  Un  O   Uc  Md  US-   [        U5      :  a  XES-      OSnUR                  5       n	[        U	5      S:  a  M   U	S   SR                  U	SS 5      pº[        X«5      nU(       d  MÌ  UR                  US5      S-   X'   UR                  X«45        M÷     X4$ )	a&  Parse the stock_list.php report into {model_key: count} plus the raw
(make, model) pairs for the snapshot. Returns (counts, pairs). A vehicle row
is one with a VRM in its early cells and the combined make/model cell two
cells after the VRM (Keytag, ?, Stock#, VRM, Registered Date, Make/Model).é   Né   r   r   r   r   é   )Ú_ROW_REr   r    ÚlenÚ	enumerateÚ_VRM_REÚ	fullmatchr   r
   r   ÚgetÚappend)ÚhtmlÚcountsÚpairsÚrowÚcÚvrm_idxÚiÚcellÚmmÚtoksr   r   Úkeys                r   Úparse_stockr7   K   s	  € ð
 €FØ€EÜ�‰˜tÖ$ˆÜ�3‹KˆÜˆq‹6�A‹:ÙØˆÜ   2 A Ö'‰GˆAßˆtœ×)Ñ)¨$×/Ó/Ø�Ùñ (ð ‰?ÙØ&¨™{¬S°«VÓ3ˆQ˜‰{Š^¸ˆØ�x‰x‹zˆÜˆt‹9�q‹=ÙØ˜1‘g˜sŸx™x¨¨Q¨R¨Ó1ˆeÜ˜Ó$ˆÞÙØ—j‘j  aÓ(¨1Ñ,ˆ‰Ø�‰�d�]Ö#ñ) %ð* ˆ=Ðr   c                 ó  • [        U =(       d    S5      R                  SS5      R                  SS5      R                  SS5      R                  5       n [        [	        [        U5      5      5      $ ! [         a     gf = f)z<'12,399.00' -> 12399 (whole pounds). None when not a number.r   Ú,z&pound;ô   Â£N)r   r   r   ÚintÚroundÚfloatÚ
ValueError)r   r   s     r   Ú_moneyr?   j   si   € äˆD�J�B‹×Ñ  RÓ(×0Ñ0°¸BÓ?×GÑGÈÈbÓQ×WÑWÓY€AðÜ”5œ˜q›“?Ó#Ð#øÜó Ùðús   ÁA1 Á1
A>Á=A>c                 óð   • [         R                  " S[        U =(       d    S5      R                  5       5      nU(       a7  UR	                  S5       SUR	                  S5       SUR	                  S5       3$ S$ )z3'05/05/2026' -> '2026-05-05'. None when not a date.z(\d{2})/(\d{2})/(\d{4})r   é   r	   r   r$   N)Úrer)   r   r   Úgroup©r   Úms     r   Ú	_date_isorF   s   sZ   € ä
�ŠÐ/´°T·Z¸R³×1FÑ1FÓ1HÓI€AÞ89ˆa�g‰g�a‹jˆ\˜˜1Ÿ7™7 1›:˜, a¨¯©°«
 |Ð4ÐC¸tÐCr   c                 óš   •  [        [        U =(       d    S5      R                  SS5      R                  5       5      $ ! [         a     g f = f)Nr   r9   )r;   r   r   r   r>   r   s    r   Ú_intrH   y   sB   € ðÜ”3�t—z˜r“?×*Ñ*¨3°Ó3×9Ñ9Ó;Ó<Ð<øÜó Ùðús   ‚:= ½
A
Á	A
c                 óê  • / n[         R                  U 5       GH#  n[        U5      n[        U5      S:  a  M   Sn[	        USS 5       H,  u  pVU(       d  M  [
        R                  U5      (       d  M*  Un  O   Ub  US:  a  Mk  [        R                  " SU5      n UR                  X4S-
     X4   X4S-      X4S-      X4S-      [        X4S-      5      [        X4S-      5      [        X4S	-      5      [        X4S
-      5      [        X4S-      5      [        X4S-      5      U(       a  UR                  S5      OSS.5        GM&     U Vs/ s H  oˆS   (       d  M  UPM     sn$ ! [         a     GMT  f = fs  snf )a$  Parse the sold vehicles report. Each vehicle row is anchored on its VRM
cell; the columns sit at fixed offsets from it (verified against the live
May 2026 report). Returns a list of dicts, one per sold car, including the
vehicle_details id so the supplier can be read from the car's own page.é   Né   rA   z/vehicle_details\.php\?id=(\d+)é   é	   é
   é   é   é   é   r$   )Ústock_numberÚregr   r   ÚfuelÚmileageÚ
sale_priceÚpurchase_priceÚtotal_marginÚ	sold_dateÚdays_to_sellÚ
vehicle_idrZ   )r%   r   r    r&   r'   r(   r)   rB   Úsearchr+   rH   r?   rF   rC   Ú
IndexError)	r,   Úoutr/   r0   r1   r2   r3   ÚvidÚrs	            r   Ú
parse_soldrb   €   sc  € ð
 €CÜ�‰˜t×$ˆÜ�3‹KˆÜˆq‹6�B‹;ÙØˆÜ   2 A Ö'‰GˆAßˆtœ×)Ñ)¨$×/Ó/Ø�Ùñ (ð ‰?˜g¨›kÙÜ�iŠiÐ:¸CÓ@ˆð	Ø�J‰JØ !¨A¡+¡Ø‘zØ A™+™Ø Q™;™Ø A™+™Ü ¨A¡+¡Ó/Ü$ Q°¡|¡_Ó5Ü"(¨°R©<©Ó"9ô !' q°2©¡Ó 7Ü& q°2©¡Ó7Ü $ Q°¡|¡_Ó 5Þ.1˜cŸi™i¨œl°tñ÷ ñ %ñ@ Ó-’s�! �n�A‘sÑ-Ð-øô ó 	Ûð	üâ-s   ÂB EÅE0ÅE0Å
E-Å,E-c                 ó–  • 0 n[         R                  U 5       Hœ  n[        U5      n[        U5      S:  a  M  Sn[	        USS 5       H,  u  pVU(       d  M  [
        R                  U5      (       d  M*  Un  O   Ub  US:  a  Mj  US   nU(       d  Mx   [        X4S-      5      [        X4S-      5      S.X'   Mž     U$ ! [         a     M¯  f = f)zíParse the sales margins report into {stock_number: {prep_total, margin}}.
Anchored on the VRM cell: SIV (prep total) is six cells after it, Margin
twelve after (verified live: net sales minus purchase minus SIV equals the
Margin column).é   Né   r$   r   r#   )Ú
prep_totalÚmargin)	r%   r   r    r&   r'   r(   r)   r?   r^   )r,   r_   r/   r0   r1   r2   r3   Ústock_nos           r   Úparse_marginsri   ©   sÐ   € ð
 €CÜ�‰˜tÖ$ˆÜ�3‹KˆÜˆq‹6�B‹;ÙØˆÜ   3 B Ö(‰GˆAßˆtœ×)Ñ)¨$×/Ó/Ø�Ùñ )ð ‰?˜g¨›kÙØ�Q‘4ˆÞÙð	ä$ Q°¡{¡^Ó4Ü  ¨R¡<¡Ó1ñˆC‹Mñ %ð* €Jøô ó 	Úð	ús   Â#B:Â:
CÃC)z
buyers feeÚshippingÚdeliveryÚ
collectionzvat c                 ó²   ^• U =(       d    SR                  5       R                  5       m[        T5      =(       a    [        U4S j[         5       5      (       + $ )Nr   c              3   óF   >#   • U  H  nTR                  U5      v •  M     g 7f©N)Ú
startswith)Ú.0Úxr   s     €r   Ú	<genexpr>Úis_prep_type.<locals>.<genexpr>Ð   s   øé € ÐGº°1˜qŸ|™|¨AŸ˜ºùs   ƒ!)r   r   ÚboolÚanyÚNON_PREP_TYPES)Úexpense_typer   s    @r   Úis_prep_typery   Î   s<   ø€ Ø	×	˜×"Ñ"Ó$×*Ñ*Ó,€AÜ�‹7×Gœ3ÔG½ÓGÓGÔGÐGr   c                 óR   • U (       d  g[        S U R                  5        5       5      $ )zgThe actual prep spend from a per car expense split, fees excluded.
None when there is no split to read.Nc              3   óN   #   • U  H  u  p[        U5      (       d  M  Uv •  M     g 7fro   )ry   )rq   r   Úvs      r   rs   Úactual_prep.<locals>.<genexpr>Ø   s   é € Ð>š]‘T�Q¬l¸1¯o�q‰qš]ùs   ‚%œ	%)ÚsumÚitems)r   s    r   Úactual_prepr€   Ó   s!   € ö ØÜÑ>˜UŸ[™[œ]Ó>Ó>Ð>r   c                 óÂ  • 0 nSn[         R                  U 5       HÀ  n[        U5      n[        U5      S:¼  a  US   S:X  a  [	        US   5      nM6  [        U5      S:¼  d  MG  [
        R                  " SUS   =(       d    S5      (       d  Mq  US   =(       d    SR                  5       n[	        US   5      nU(       d  M¤  Uc  M©  UR                  US	5      U-   X'   MÂ     X4$ )
a  Parse a car's own SIV page (siv.php?id=N, the screen behind the SIV
button on the vehicle page) into a prep split by expense type plus the
total. Expense rows read: date, supplier, type, description, amount, View.
Returns (split_dict, total) where split is {type: pounds}.NrL   r   z	Total SIVrA   é   z	\d+\.\d\dr   r   )	r%   r   r    r&   r?   rB   r]   r   r*   )r,   r   Útotalr/   r0   ÚtypÚamounts          r   Ú	parse_sivr†   Û   sÁ   € ð
 €EØ€EÜ�‰˜tÖ$ˆÜ�3‹KˆÜˆq‹6�Q‹;˜1˜Q™4 ;Ó.Ü˜1˜Q™4“LˆEÙÜˆq‹6�Q�;œ2Ÿ9š9 \°1°Q±4·:¸2×>Ó>Ø�Q‘4—:˜2×$Ñ$Ó&ˆCÜ˜A˜a™D“\ˆFßˆs�vÓ)Ø"ŸY™Y s¨AÓ.°Ñ7�“
ñ %ð ˆ<Ðr   c                 ó  • [         R                  " SU [         R                  5      nU(       d  g[         R                  " SUR                  S5      5      nU(       a(  UR                  S5      R	                  5       =(       d    S$ S$ )z‚The supplier a car was bought from, read from the selected option of the
purchase form's supplier dropdown on vehicle_details.php.z4<select[^>]*name="v\[supplier\]"[^>]*>(.*?)</select>Nz*<option[^>]*selected[^>]*>([^<]*)</option>r$   )rB   r]   ÚSrC   r   )r,   rE   Úsels      r   Úparse_supplierrŠ   ï   s`   € ô 	�	Š	ÐIÈ4ÔQS×QUÑQUÓV€AÞØÜ
�)Š)ÐAÀ1Ç7Á7È1Ã:Ó
N€CÞ+.ˆ3�9‰9�Q‹<×ÑÓ×' 4Ð8°DÐ8r   c                 ó6   • SS K nUR                  X5      S   nU$ )Nr   r$   )ÚcalendarÚ
monthrange)ÚyearÚmonthrŒ   Úlasts       r   Ú_month_ranger‘   ù   s   € ÛØ×Ñ˜tÓ+¨AÑ.€DØ€Kr   c                 ó  • [         R                  S   S   n[        X5      n[         R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       nU SU SU SU SU S	U S
3n	UR                  U	SSS9  UR                  5       n
SU
;  a  SU
;  a  [        SU SUS S35      e[        U
5      nU SUS SU SU SUS SU S3nUR                  USSS9  [        UR                  5       5      n[        US5       GH&  u  pïUR                  US   0 5      nUR                  S5      US'   SUS'   SUS'   UR                  S5      (       aƒ   UR                  U SUS    3SS S9  [        UR                  5       5      US'    UR                  U S!US    3SS S9  [        UR                  5       5      u  nnU(       a  UUS'   US   c  UUS'   UR                  S"5      b!  UR                  S5      b  US"   US   -
  US#'   OSUS#'   U(       d  GM  U" U[!        U5      5        GM)     UUR#                  5         $ ! [         a     NÄf = f! [         a     N‡f = f! UR#                  5         f = f)$zûRead one month of sold cars: the sold vehicles report joined to the
sales margins report by stock number, plus each car's supplier from its own
page. Returns a list of dicts ready for db.record_sales_history. Fails
loudly if the reports do not render.ÚclickdealerÚbase_url©Úheadlessr   z+/sold_vehicles.php?start_day=1&start_month=z&start_year=z	&end_day=z&end_month=z
&end_year=z:&user_id=&sale_type=&location_id=&view=&action=View+ReportÚdomcontentloadedé`ê  ©Ú
wait_untilÚtimeoutÚSoldDateÚDaysz(Sold vehicles report did not render for r	   Ú02dÚ.z!/sales_margins.php?start_date=01/Ú/z
&end_date=zp&complete_month=&user_id=&sale_type=&view=&location_id=&supplier=&warranty_type_id=&buyer_id=&action=View+Reportr$   rS   rf   NÚsourceÚ
prep_splitr\   z/vehicle_details.php?id=i@œ  z/siv.php?id=rY   rg   )r   ÚSITESr‘   Úopen_reader_contextÚpagesÚnew_pageÚgotoÚcontentÚRuntimeErrorrb   ri   r'   r*   rŠ   Ú	Exceptionr†   r&   Úclose)Ú
playwrightrŽ   r�   r–   ÚprogressÚbaser�   ÚctxÚpageÚsold_urlÚ	sold_htmlÚsoldÚmargins_urlÚmarginsr2   ra   rE   r   Ú	siv_totals                      r   Úread_sales_monthr·   ÿ   sÐ  € ô
 �=‰=˜Ñ'¨
Ñ3€DÜ˜Ó$€DÜ
×
%Ò
% j°-È(Ñ
S€Cð7Ø"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆà�fÐGÈÀwØ" 4 &¨	°$°°{À5À'ÈÐTXÐSYØPðRˆð 	�	‰	�(Ð'9À5ˆ	ÑIØ—L‘L“Nˆ	Ø˜YÓ&¨6¸Ó+BÜÐ!IÈ$ÈÈqÐQVÐWZÐP[Ð[\Ð]Ó^Ð^Ü˜)Ó$ˆà˜Ð@ÀÀsÀÈ1ÈTÈFØ# D 6¨¨5°¨+°Q°t°fð =7ð8ˆð 	�	‰	�+Ð*<Àeˆ	ÑLÜ §¡£Ó/ˆä˜d A×&‰DˆAØ—‘˜A˜nÑ-¨rÓ2ˆAØŸe™e LÓ1ˆAˆl‰OØˆAˆh‰KØ"ˆAˆl‰OØ�u‰u�\×"Ñ"ðØ—I‘I  Ð&>¸qÀ¹Ð>OÐPØ);ÀUð ñ Lä"0°·±³Ó"@�A�h‘Kð
Ø—I‘I   l°1°\±?Ð2CÐDØ);ÀUð ñ Lä'0°·±³Ó'@Ñ$�E˜9ÞØ*/˜˜,™à˜‘Ñ.Ø*3˜˜,™ð
 �u‰u�^Ó$Ñ0°Q·U±U¸<Ó5HÑ5TØ Ñ/°!°L±/ÑA��(’à"��(‘ß‰xÙ˜œC ›I×&ñC 'ðD à�	‰	�øô/ !ó Ùðûô !ó Ùðûð 	�	‰	�úsU   ¹D*I1 Å$5IÆAI!Ç&=I1 È(I1 É
IÉI1 ÉIÉI1 É!
I.É+I1 É-I.É.I1 É1Jc                 óX  • [         R                  R                  5       n[        R                  S   S    SUR
                   SUR                   SUR                   3n[        R                  " U SUS9n UR                  (       a  UR                  S   OUR                  5       nUR                  USS	S
9  UR                  5       nSU;  a  [        SUR                   S35      e[        U5      u  pxU(       d  [        S5      eXx4UR!                  5         $ ! UR!                  5         f = f)z�Open Steven's Clickdealer in stock list and return {model_key: count}.
Fails loudly if the report does not render, never guesses.r“   Ú	stock_urlz
&date_day=z&date_month=z&date_year=r•   r   r—   r˜   r™   zMake / ModelzUClickdealer stock list did not render the expected report (no Make / Model header at zd). The page may have changed or the login may have expired. Run: python3 login.py clickdealer manualziClickdealer stock list rendered but no vehicle rows were read. The layout may have changed, not guessing.)ÚdatetimeÚdateÚtodayr   r£   Údayr�   rŽ   r¤   r¥   r¦   r§   r¨   r©   Úurlr7   r«   )	r¬   r–   r¼   r¾   r¯   r°   r,   r-   r.   s	            r   Úread_in_stockr¿   A  s  € ô �M‰M×ÑÓ!€EÜ�m‰m˜MÑ*¨;Ñ7Ð8Ø˜Ÿ	™	�{ ,¨u¯{©{¨m¸;ÀuÇzÁzÀlðT€Cä
×
%Ò
% j°-È(Ñ
S€CðØ"ŸyŸyˆs�y‰y˜Š|¨c¯l©l«nˆØ�	‰	�#Ð"4¸eˆ	ÑDØ�|‰|‹~ˆØ Ó%Üð.Ø.2¯h©h¨Zð 8bðbóð ô
 $ DÓ)‰ˆÞÜð=óð ð ˆ}à�	‰	�øˆ�	‰	�ús   Á5BD ÄD)zC([A-Za-z_:\-\[\]\.]+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s"\'>]+))z<input\b[^>]*>z<select\b([^>]*)>(.*?)</select>z#<textarea\b([^>]*)>(.*?)</textarea>z<option\b([^>]*)>(.*?)</option>z<img\b[^>]*>zC<a\b[^>]*href=[\"']([^\"']+\.(?:jpe?g|png|webp)(?:\?[^\"']*)?)[\"']z\.(jpe?g|png|webp)(\?|$))ÚlogoÚiconÚbuttonÚbtnÚspinnerÚloadingÚblankÚpixelÚspacerÚflagÚarrowÚavatarÚbadgeÚcaptchaÚbannerÚplaceholderÚno_imageÚnoimagezno-imagec                 ó.  • 0 n[         R                  U 5       Hy  nUR                  S5      b  UR                  S5      O3UR                  S5      b  UR                  S5      OUR                  S5      XR                  S5      R                  5       '   M{     U$ )Nr   rA   rL   r$   )Ú_TAG_ATTR_REÚfinditerrC   r   )Útagr_   rE   s      r   Ú
_tag_attrsrÖ   x  sƒ   € Ø
€CÜ×"Ñ" 3Ö'ˆØ01·±¸³
Ñ0F !§'¡'¨!¤*Ð[\×[bÑ[bÐcdÓ[eÑ[qÈQÏWÉWÐUVÌZÐwx×w~Ñw~ð  @Aó  xBˆ�G‰G�A‹J×ÑÓÓñ (à€Jr   c                 ó‚   • SS K nUR                  [        R                  SU =(       d    S5      5      R	                  5       $ )Nr   r   )r,   Úunescaper   r   r   )r   Ú_htmls     r   Ú	_unescaperÚ     s,   € ÛØ�>‰>œ'Ÿ+™+ b¨$¯*°"Ó5Ó6×<Ñ<Ó>Ð>r   c           
      ó   ^• 0 mU4S jn[         R                  U 5       H‹  n[        U5      nUR                  S5      =(       d    SR	                  5       nUS;   a  M>  US;   a  SUR	                  5       ;  a  MZ  U" UR                  S5      [        UR                  SS	5      5      5        M�     [        R                  U 5       H  u  pV[        U5      R                  S5      nS	n[        R                  U5       H=  u  pš[        R                  " S
U	[        R                  5      (       d  M2  [        U
5      n  O   U" Xx5        M�     [        R                  U 5       H/  u  pVU" [        U5      R                  S5      [        U5      5        M1     T$ )aC  Every named form field on a car's page and its value, {name: value}:
an input's value (a checkbox or radio only when checked, never a button),
a select's chosen option text, a textarea's text. Names are as the page
has them (v[make], v[mileage] ...). Later fields with the same name win
only when the earlier one was empty.c                 ó„   >• U (       d  g U=(       d    SR                  5       nU T;  d  TU    (       d  U(       a  UTU '   g g g ©Nr   )r   )ÚnameÚvaluer_   s     €r   ÚputÚ!parse_vehicle_fields.<locals>.putŒ  s:   ø€ ÞØØ—˜"×#Ñ#Ó%ˆØ�s‹? 3 t§9¶ØˆC�ŠIð 27 9r   Útyper   )ÚsubmitrÂ   ÚimageÚresetÚfileÚpassword)ÚcheckboxÚradioÚcheckedrÞ   rß   r   z\bselected\b)Ú	_INPUT_REr   rÖ   r*   r   rÚ   Ú
_SELECT_REÚ
_OPTION_RErB   r]   ÚIÚ_TEXTAREA_RE)r,   rà   rÕ   ÚaÚkindÚattrsÚbodyrÞ   ÚchosenÚoattrsÚotextr_   s              @r   Úparse_vehicle_fieldsr÷   „  sE  ø€ ð €Cõô × Ñ  Ö&ˆÜ�s‹OˆØ—‘�f“×' ×.Ñ.Ó0ˆØÐMÓMÙØÐ(Ó(¨Y¸c¿i¹i»kÓ-IÙÙˆA�E‰E�&‹Mœ9 Q§U¡U¨7°BÓ%7Ó8Ö9ñ 'ô "×)Ñ)¨$Ö/‰ˆÜ˜%Ó ×$Ñ$ VÓ,ˆØˆÜ'×/Ñ/°Ö5‰MˆFÜ�yŠy˜¨&´"·$±$×7Ó7Ü" 5Ó)�Ùñ 6ñ 	ˆDÖñ 0ô $×+Ñ+¨DÖ1‰ˆÙŒJ�uÓ×!Ñ! &Ó)¬9°T«?Ö;ñ 2à€Jr   c                 ó€  ^^^^	• SSK Jm	  [        5       / smmUUUU	4S jn[        R	                  U 5       HU  n[        U5      nU" UR                  S5      =(       d)    UR                  S5      =(       d    UR                  S5      5        MW     [        R	                  U 5       H  nU" U5        M     S n[        TUS9$ )	zâThe car's pictures in page order as absolute urls: every img (src,
data-src, data-original) and every link straight to an image file,
less the site's own furniture (logos, icons, buttons). The first is
the one Dealer OS shows.r   )Úurljoinc                 óÆ  >^• U (       a  U R                  S5      (       a  g T" TU R                  5       5      nUR                  5       m[        R                  " ST5      (       a  g [
        R	                  T5      (       d  [        U4S jS 5       5      (       d  g [        U4S j[         5       5      (       a  g UT;  a#  TR                  U5        TR                  U5        g g )Nzdata:z\.gif(\?|$)c              3   ó,   >#   • U  H	  oT;   v •  M     g 7fro   © ©rq   ÚwÚlows     €r   rs   Ú4parse_vehicle_photos.<locals>.add.<locals>.<genexpr>·  s   øé € Ð4eÒGdÀ!¸#¶XÒGdùó   ƒ)rä   ÚphotoÚpicturec              3   ó,   >#   • U  H	  oT;   v •  M     g 7fro   rü   rý   s     €r   rs   r   ¹  s   øé € Ð,¢˜A�CŽx¢ùr  )
rp   r   r   rB   r]   Ú_IMAGE_EXT_RErv   Ú
_NOT_A_CARÚaddr+   )Úsrcr¾   rÿ   r_   Úpage_urlÚseenrù   s     @€€€€r   r  Ú!parse_vehicle_photos.<locals>.add°  s£   ù€ Þ�c—n‘n W×-Ñ-ØÙ�h §	¡	£Ó,ˆØ�i‰i‹kˆÜ�9Š9�^ S×)Ñ)ØÜ×#Ñ# C×(Ñ(´Ô4eÑGdÓ4e×1eÑ1eØÜÔ,¥Ó,×,Ñ,ØØ�d‹?Ø�H‰H�SŒMØ�J‰J�s�Oð r   zdata-srczdata-originalr  c                 óB   • U R                  5       nSU;   d  SU;   a  S$ S$ )Nz
/vehicles/z	//images.r   r$   )r   )r¾   rÿ   s     r   Ú	car_firstÚ'parse_vehicle_photos.<locals>.car_firstÇ  s&   € Ø�i‰i‹kˆØ! SÓ(¨K¸3Ó,>ˆqÐFÀQÐFr   ©r6   )	Úurllib.parserù   ÚsetÚ_IMG_REr   rÖ   r*   Ú	_A_IMG_REÚsorted)
r,   r	  r  rÕ   rð   Úhrefr  r_   r
  rù   s
    `     @@@r   Úparse_vehicle_photosr  ¨  sœ   û€ õ
 %Ü“�r€I€Dˆ#÷ð ô �‰˜tÖ$ˆÜ�s‹OˆÙˆA�E‰E�*Ó×G §¡ Ó!7×G¸1¿5¹5À»<ÖHñ %ô ×!Ñ! $Ö'ˆÙˆDŽ	ñ (ò
Gô �#˜9Ñ%Ð%r   Fr   c                ó*  • U  Vs0 s H  o3UR                  5       _M     nnU Hh  nUR                  5        HQ  u  pgX   nXW;   d  M  U(       d  M  U(       a  [        R                  " SU5      (       a  M>  XhR	                  5       4s  s  $    Mj     gs  snf )a  The first field whose name holds one of the words (in the order the
words are given) and has a value, as (name, value); (None, None) if none.
text: a value that is only a number is skipped (v[variant_id]=54668 is
an id, not the derivative; the probe of 2026-09-14 took it).z	[\d.,\s]+)NN)r   r   rB   r)   r   )	Úfieldsr   ÚwordsÚkrÿ   rþ   rÞ   Úname_lowÚvals	            r   Ú_fieldr  Í  sx   € ñ
 "(Ó
(¢˜Aˆa�g‰g‹iŠ<¡€CÐ
(ÛˆØ!Ÿi™ižk‰NˆDØ‘,ˆCØ�}§ ®d´r·|²|ÀLÐRU×7VÓ7VØŸY™Y›[Ð(Ô(ó *ñ ð
 ùò )s   …Bc                 óÈ   • [         R                  " SU =(       d    S5      nU(       d  g  [        UR                  S5      R	                  SS5      5      $ ! [
         a     g f = f)Nz\d[\d,]*(?:\.\d+)?r   r   r9   )rB   r]   r=   rC   r   r>   rD   s     r   Ú_numberr  Û  sT   € Ü
�	Š	Ð'¨¯°Ó4€AÞØðÜ�Q—W‘W˜Q“Z×'Ñ'¨¨RÓ0Ó1Ð1øÜó Ùðús   ª)A Á
A!Á A!c                 ó   ^ ^^• 0 m0 mSU UU4S jjnS nS nS nU" SSU5        TS   c
  U" SSU5        U" S	S
SS9  U" SSU5        S nU" SSUSS9  U" SSUSS9  U" SSU5        U" SSS SS9  U" SSSS9  U(       a  US   OSTS'   TTS'   T$ )zÙWhat the Sold tab shows, read off a car's page fields: {year,
derivative, litres, fuel, gearbox, mileage, colour, body, photo, keys}.
keys names the field each fact came from, for checking. Anything not
found is None.Nc                 ó†   >• [        T/UQ7SU06u  pEUc  S T	U '   g U(       a  U" U5      OUnUT	U '   Ub  US:w  a  UTU '   g g g )Nr   r   )r  )
Úfactr  Úconvr   rÞ   r  r|   r  Úkeysr_   s
          €€€r   ÚtakeÚvehicle_details.<locals>.takeí  s[   ø€ Ü˜6Ð5 EÒ5°Ñ5‰	ˆØ‰<ØˆC�‰IØÞ‰D�ŒI 3ˆØˆˆD‰	Ø‰=˜Q "›WØˆD�ŠJð %ˆ=r   c                 óˆ   • [         R                  " SU =(       d    S5      nU(       a  [        UR                  S5      5      $ S $ )Nz\b(19[89]\d|20[0-4]\d)\br   r$   )rB   r]   r;   rC   )r  rE   s     r   rŽ   Úvehicle_details.<locals>.year÷  s0   € Ü�IŠIÐ1°3·9¸"Ó=ˆÞ"#Œs�1—7‘7˜1“:‹Ð-¨Ð-r   c                 óZ   • [        U 5      nUc  g US:”  a  [        US-  5      S-  S $ US $ )Né2   éd   rN   z.1f)r  r<   ©r  Úns     r   ÚlitresÚvehicle_details.<locals>.litresû  s<   € Ü�C‹LˆØ‰9ØØˆr‹6Ü˜A ™G“n rÑ)¨#Ð.Ð/Ø�C�Ðr   c                 óD   • [        U 5      nUb  US:¼  a  [        U5      $ S $ )Nr   )r  r;   r,  s     r   ÚmilesÚvehicle_details.<locals>.miles  s$   € Ü�C‹LˆØ™¨1°«6Œs�1‹vÐ;°tÐ;r   rŽ   )Úyear_of_manufactureÚmanufacture_yearÚ
model_yearÚreg_yearrŽ   )Údate_of_registrationÚregistration_dateÚreg_dateÚfirst_registeredÚ
registeredÚ
derivative)Úeditionr<  ÚvariantÚtrimÚdescriptionÚtitleTr   r.  )Úengine_sizeÚ	engine_ccÚengineÚcapacityÚccc                 óP   • U R                  5       (       a  U R                  5       $ U $ ro   ©ÚisupperrA  ©r|   s    r   Ú<lambda>Ú!vehicle_details.<locals>.<lambda>  s   €  1§9¡9§;¡;�a—g‘g“iÐ5°AÐ5r   rU   )rU   Úgearbox)ÚtransmissionrM  rV   )rV   Úodometerr1  Úcolour)rP  Úcolorc                 óP   • U R                  5       (       a  U R                  5       $ U $ ro   rH  rJ  s    r   rK  rL    s   € ¸q¿y¹y¿{¹{°!·'±'³)Ð2QÐPQÐ2Qr   ró   )Ú	body_typeÚbodytypeÚ
body_styler   r  r$  )NFrü   )	r  Úphotosr%  rŽ   r.  r1  Úplainr$  r_   s	   `      @@r   Úvehicle_detailsrX  å  s×   ú€ ð
 €DØ
€C÷ñ ò.òò<ñ 	ˆÐ^Ð`dÔeØ
ˆ6�{ÑÙˆVÐpÐrvÔwÙˆÐ[ÐbfÒgÙˆÐKÈVÔTÙ5€EÙˆ�˜E¨Ò-ÙˆÐ/°¸TÒBÙˆÐ4°eÔ<ÙˆÐ&Ñ(QÐX\Ò]ÙˆÐ8¸tÒDÞ &�6˜!’9¨D€Cˆ�LØ€Cˆ�KØ€Jr   c                 óÎ  • [        U 5      nUnUR                  S5      (       a  [        US   5      OSUR                  S5      UR                  S5      UR                  S5      /nSR                  S U 5       5      =(       d    UR                  S5      US'   S	R                  S
 UR                  S5      UR                  S5      UR                  S5      4 5       5      nU=(       d    SUS'   UR                  S5      US'   UR                  S5      =(       d    SUS'   UR                  S5      US'   UR                  S5      =(       d    0 R	                  5        VVs/ s H  u  pxU SU 3PM     n	nnUR                  S5      (       a  U	R                  SUS   SS  35        X’=(       d    /  V
s/ s H  n
[        U
5      (       a  M  U
PM     sn
-   SS US'   U$ s  snnf s  sn
f )u‰  base (clickdealer_sale_report's plain row) filled in from the car's
page: the name becomes "2018 Ford Kuga 1.5 TDCi Titanium 5dr", the spec
"1.5 Â· Diesel Â· Manual", plus mileage, colour and the first photo (a
url, the caller swaps it for Dealer OS's lasting link). record_keys
names where each fact came from; on a probe it carries every field and
picture found so the reader can be checked.rŽ   Nr   r   r<  r   c              3   ó6   #   • U  H  o(       d  M  Uv •  M     g 7fro   rü   ©rq   rr   s     r   rs   Ú"sold_car_report.<locals>.<genexpr>!  s   é € Ð0¢d «aŸ1™1¢dùó   ‚
�	rÞ   u    Â· c              3   ó6   #   • U  H  o(       d  M  Uv •  M     g 7fro   rü   r[  s     r   rs   r\  "  s   é € ÐZÒ"T˜QÓXY—q‘qÒ"Tùr]  r.  rU   rM  ÚspecrV   rP  r  r$  Ú=zphoto=iºÿÿÿé<   Úrecord_keys)Údictr*   r   r
   r   r+   Úis_private_field)r®   ÚdetailsÚ
probe_keysÚrepÚdÚbitsr_  r"  rÞ   Úusedr  s              r   Úsold_car_reportrk    s¢  € ô ˆt‹*€CØ€AØŸe™e FŸm™mŒC��&‘	ŒN°°s·w±w¸v³ÈÏÉÐPWÓHXÐZ[×Z_ÑZ_Ð`lÓZmÐn€DØ—(‘(Ñ0¡dÓ0Ó0×C°C·G±G¸F³O€Cˆ�KØ�;‰;ÑZ 1§5¡5¨£?°A·E±E¸&³MÀ1Ç5Á5ÈÓCSÑ"TÓZÓZ€DØ—,˜$€Cˆ�KØ—U‘U˜9Ó%€Cˆ	�NØ—E‘E˜(“O×+ t€Cˆ�MØ—5‘5˜“>€Cˆ�LØ01·±°f³×0CÀ×/JÑ/JÔ/LÔMÒ/L¡ ˆtˆf�A�d�VÓÑ/L€DÑMØ‡u�uˆW‡~�~Ø�‰�f˜Q˜w™Z¨¨Ð-Ð.Ð/Ô0Ø×.>¸BÐ.>Ó![Ò.>¨ÔGWÐXY×GZ§!Ñ.>Ñ![Ñ[Ð]`Ð^`Ða€CˆÑØ€Jùó	 Nùò "\s   ÅGÆ1G"Ç	G"zŽchassis|vin\b|engine_number|customer|enquiry|mot_number|payment|finance|purchase|cost|price|vat|dealer_id|seller|supplier|stock_plan|referencec                 óP   • [        [        R                  U =(       d    S5      5      $ rÝ   )ru   Ú_PRIVATE_FIELD_REr]   r  s    r   rd  rd  4  s   € ÜÔ!×(Ñ(¨¯°Ó3Ó4Ð4r   )TN)T)r   ro   )1Ú__doc__rº   rB   r   r   r   Úcompiler(   rˆ   r%   r   r   r   r   r    r7   r?   rF   rH   rb   ri   rw   ry   r€   r†   rŠ   r‘   r·   r¿   rÓ   rî   rë   rì   rï   rí   r  r  r  r  rÖ   rÚ   r÷   r  r  r  rX  rk  rm  rd  rü   r   r   Ú<module>rp     sé  ðñó, Û 	ç #ð �*Š*ÐHÓ
I€à
�*Š*Ð+¨R¯T©TÓ
2€Ø�:Š:Ð,¨b¯d©dÓ3€Ø
�*Š*�ZÓ
 €òGò
Cò:_òò>òDòò&.òRðD N€òHò
?òò(9òô?ôDðP �zŠzÐ`Óa€Ø�JŠJÐ(¨"¯$©$Ó/€	Ø�ZŠZÐ:¸B¿D¹DÀ2Ç4Á4¹KÓH€
Ø�zŠzÐ@À"Ç$Á$ÈÏÉÁ+ÓN€Ø�ZŠZÐ:¸B¿D¹DÀ2Ç4Á4¹KÓH€
Ø
�*Š*�_ b§d¡dÓ
+€Ø�JŠJÐ]Ð_a×_cÑ_cÓd€	ð —
’
Ð6¸¿¹Ó=€ðC€
òò?ò
!ôH"&ðJ !&õ òò/ôdð4 —J’Jð   qð  su÷  swñ  swó  xÐ ó5r   