ó
    vºj]i  ã                   ó  • S r SSKrSSKrSSKrSSKrSSKrSrSrSr	Sr
SrSrS	rS	rSqSq " S
 S\5      rS rSS jrS rS rS r  SS jr   SS jrS r\4S jrS rSrS r  S S jr  S!S jr S r!S r"S"S jr#g)#a  Push BidBrain's finished daily list into Dealer OS.

Dealer OS (Steven's and Mark's own multi tenant dealership platform,
CarDealer-OS/Dealer_OS, live at cardealeros.co.uk) shows BidBrain's live auctions
page. The agreed shape of the integration (Steven 2026-09-01, from the
"BidBrain today, Dealer OS tomorrow" briefing, widened 2026-09-02 once
Dealer OS was chosen as the ONLY screen people use) is the kitchen and the
restaurant: BidBrain stays on the Mac and keeps every scraper and the whole
buying brain, and pushes ONLY finished, decided results across, priced and
gated cars with everything the cockpit itself draws for them, never raw
listings and never the gate or pricing logic itself, so there is exactly
one set of buying rules that can never drift apart.

Standard library only, like the rest of this project: a plain HTTPS POST
of one JSON document, gzipped on the wire, authenticated with a shared
secret in a bearer header. The secret lives in dealer_config.py on this Mac
(DEALER_OS_API_KEY) and, as one entry of BIDBRAIN_API_KEYS, in Dealer OS's
own environment, never in either repo's shared code.

The receiving end (the "letterbox", POST {DEALER_OS_BASE_URL}/api/bidbrain/
cars) is Dealer OS's, built against the contract in DEALER_OS_PUSH.md at
the repo root. A push runs AFTER the run has written its own pages, records
its own separate run_status and site_health, and never raises into run().

The document is a superset of the schema 1 contract: every field added
since (2026-09-02, the cockpit rebuild inside Dealer OS) is additive, so
schema_version stays 1 and an older Dealer OS simply ignores what it does
not know. The one naming trap, kept on purpose so nothing already built
breaks: suggested_bid_comps is the COUNT of comparable sales; the list
itself lives under drawer.compmatch.comps.
é    Nz/api/bidbrain/carsé   é   iÀê!i 	= é   éÈ   c                   ó   • \ rS rSrSrSrg)ÚDealerOsPushErroré]   z>A push that did not land, with a message written for a person.© N)Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__Ú__static_attributes__r
   ó    Ú8/Users/stevendouglas/BidBrain/bidbrain/dealer_os_push.pyr   r   ]   s   † ÜHr   r   c                 ó.   • SSSS.R                  U S5      $ )zÏCarwow's reserve is CAP Clean standing in (2026-08-19), Dealer
Auction's is Retail minus Margin (2026-08-20), DealerWay's is the
current bid (2026-08-24): the cockpit labels each honestly, so does
Dealer OS.zTrade valuezAT Tradez	Bid price)ÚcarwowÚdealerauctionÚ	dealerwayÚReserve)Úget)Úplatform_keys    r   Ú_reserve_labelr   a   s#   € ð
 $°jØ$ñ&ß&)¡c¨,¸	Ó&BðCr   c                 óð  • SSK Jn  U R                  nU R                  nU=(       d    0 nUR	                  UR
                  5      nUR                  U 5      nUb3  UR                  b&  UR                  b  UR                  UR                  -
  OSnSn	Sn
UbZ  UR                  b&  UR                  b  UR                  UR                  -
  OUn	UR                  b  UR                  OUR                  n
[        [        USS5      =(       d    / 5      nUR                  (       a,  UR                  U;  a  UR                  SUR                  5        [        S U R                   =(       d    /  5       5      nSR#                  UR$                  =(       d    SR'                  5       R)                  5       5      =(       d    Sn0 SU_SU=(       d    UR*                  =(       d    S_S	UR,                  =(       d    S_S
UR/                  UR,                  UR,                  5      =(       d    S_SUR0                  =(       d    S_SUR3                  UR,                  UR0                  5      =(       d    S_SUR4                  =(       d    S_SUR7                  U5      =(       d    S_SSR#                  S U=(       d    SUR,                  =(       d    SUR0                  =(       d    SUR4                  =(       d    S4 5       5      R9                  5       _SUR:                  _SUR<                  _SUR>                  _SUR@                  _SURB                  =(       d    S_SURD                  =(       d    S_SURG                  U5      _SURH                  =(       d    S_0 SURK                  U5      _SURM                  U5      =(       d    S_SURN                  =(       d    S_SURP                  =(       d    S_S[S        URT                  5      _SUR                  _S [W        U5      _S!URX                  _S"URZ                  _S#UR\                  =(       d    S_S$UR
                  =(       d    S_S%U_S&UR*                  =(       d    S_S'UR                  =(       d    S_SU_S(UR^                  =(       d    S_S)[        US)S5      =(       d    S_E0 S*[        US*S5      =(       d    S_S+[        US+S5      =(       d    S_S,[S        [        US,S-5      5      _S.U R`                  _S/[        U Rb                  =(       d    / 5      _S0[        U R                   =(       d    / 5      _S1U VVVs/ s H  u  pïnXïUS2.PM     snnn_S3Uc  SOƒURd                  URf                  URh                  UR                  UR                  URj                  UR                  UR                  b&  UR                  b  UR                  UR                  -
  OSS4._S5U_S6U	_S7U
_S8[        US9S5      _S:[        US;S5      _S<[        US=S5      _S>[        US?S5      _S@[S        [        USAS-5      5      _SB[        USCS5      =(       d    S_E[        USDS5      [        USES5      =(       d    S[S        [        USFS-5      5      U[S        [        USGS-5      5      URm                  X5      [o        U=(       d    S[        US*S5      =(       d    S4U5      SSH.E$ s  snnnf )Ia_  One car's decided result as a plain dict. Only fields BidBrain has
actually read or computed: an unread figure is None, never a guess,
the same golden rule the cockpit's own cards follow. Private, never
persisted, card annotations (the _ prefixed attributes daily_run's
_annotate sets on the Car object) are read with getattr so a car that
never went through _annotate still packs, just without them.

Everything the cockpit card's own data attributes and drawer carry is
here too, derived by the SAME render.py helpers the cockpit uses, so
Dealer OS never re-derives a label, a family or a headroom figure.r   ©ÚrenderNÚ
photo_urlsc              3   ór   #   • U  H-  oR                  S 5      =(       d    UR                  S5      v •  M/     g7f)zSeen beforezIn bothN)Ú
startswith)Ú.0Úfs     r   Ú	<genexpr>Úcar_record.<locals>.<genexpr>†   s+   é € ÐfÒWdÐQR—l‘l =Ó1×L°Q·\±\À)Ó5LÔLÒWdùó   ‚57Ú ÚregÚkeyÚmakeÚ
make_canonÚmodelÚmodel_familyÚ
derivativeÚnameÚsearchÚ c              3   ó8   #   • U  H  nU(       d  M  Uv •  M     g 7f©Nr
   ©r!   Úxs     r   r#   r$   ‘   s!   é € ð Cò '= Ü@A÷ ™1ò '=ùs   ‚‘	ÚyearÚmileageÚownersÚgradeÚfuelÚtransmissionÚgearbox_labelÚengineÚengine_labelÚengine_litresÚ	body_typeÚservice_historyÚvat_qualifyingÚreserveÚreserve_labelÚ	cap_cleanÚdistance_milesÚlocationÚplatformr   Úlisting_urlÚ	photo_urlÚauction_ends_atÚkeeper_startÚselling_vrmÚ
cazana_urlÚglass_checkedFÚstatusÚreasonsÚflagsÚinsights)ÚkindÚtopicÚtextÚpricing)Úglass_retailÚcazana_retailÚ
calc_glassÚcalc_cazanaÚgoverning_valueÚgoverning_sourceÚmax_bidÚspreadÚheadroomÚcazana_headroomÚcazana_priceÚsuggested_bidÚ_suggested_bidÚsuggested_bid_compsÚ_suggested_bid_nÚstock_levelÚ_stock_levelÚstock_countÚ_stock_countÚtaste_matchÚ_taste_matchÚtaste_scoreÚ_taste_scoreÚ
_pick_rankÚ_pick_scoreÚ_is_newÚ_starred)Ú	pick_rankÚ
pick_scoreÚis_newÚseen_beforeÚstarredÚdrawerÚ	sightingsÚdeep)8Úbidbrainr   ÚcarrV   Ú_platform_classÚsourceÚ_insight_rowsr]   rB   rZ   r[   ÚlistÚgetattrrI   ÚinsertÚanyrQ   Újoinr'   ÚupperÚsplitrH   r)   r   r+   Ú_model_familyr-   Ú_titleÚlowerr5   r6   r7   r8   r9   r:   Ú_gearbox_labelr<   Ú_engine_labelÚ_engine_litresr?   r@   ÚboolrA   r   rD   rE   rF   rJ   rO   rP   rW   rX   rY   r\   Ú_drawer_dataÚ
_sightings)Úar*   Ú	sale_dater   r{   ÚpÚplatÚrowsr_   r`   ra   Úgalleryru   r'   ÚkÚtr4   s                    r   Ú
car_recordr—   j   s*  € õ  Ø
�%‰%€CØ	�	‰	€AØ×!˜r€JØ×!Ñ! #§*¡*Ó-€DØ×Ñ Ó"€Dà‘M a§i¡iÑ&;ÀÇÁÑ@Wð —‘˜SŸ[™[Ò(Ø^bð à€OØ€LØ�}à Ÿ}™}Ñ8¸S¿[¹[Ñ=Tð ŸM™M¨C¯K©KÒ7Ø[cð 	à()¯©Ñ(A�q—}’}Àq×GXÑGXˆÜ”7˜3 ¨dÓ3×9°rÓ:€GØ
‡}‡}˜Ÿ™¨gÓ5Ø�‰�q˜#Ÿ-™-Ô(ÜÑfÐWX×W^ÑW^×WdÐbdÐWdÓfÓf€KØ
�'‰'�3—7‘7—=˜b×'Ñ'Ó)×/Ñ/Ó1Ó
2×
:°d€CðMØˆsðMàˆs×-�c—o‘o×-¨ðMð 	�—‘× ˜DðMð 	�j—n‘n S§X¡X¨s¯x©xÓ8×@¸Dð	Mð
 	�—‘×"˜dðMð 	˜×,Ñ,¨S¯X©X°s·y±yÓA×IÀTðMð 	�c—n‘n×,¨ðMð 	�—‘˜cÓ"×* dðMð 	�#—(‘(ñ C s§y¨b°#·(±(·.¸bÀ#Ç)Á)Ç/ÈrØ'*§~¡~×';¸ñ'=ó Có CßCHÁ5Ã7ðMð 	�—‘ðMð 	�3—;‘;ðMð 	�#—*‘*ðMð 	�—‘ðMð 	�—‘× ˜DðMð  	˜×(Ñ(×0¨Dð!Mð" 	˜×.Ñ.¨sÓ3ð#Mð$ 	�#—*‘*×$ ñ%Mð& 	˜×,Ñ,¨SÓ1ð'Mð( 	˜×.Ñ.¨sÓ3×;°tð)Mð* 	�S—]‘]×* dð+Mð, 	˜3×.Ñ.×6°$ð-Mð. 	œ$˜s×1Ñ1Ó2ð/Mð0 	�3—;‘;ð1Mð2 	œ¨Ó-ð3Mð4 	�S—]‘]ð5Mð6 	˜#×,Ñ,ð7Mð8 	�C—L‘L×( Dð9Mð: 	�C—J‘J×& $ð;Mð< 	˜ð=Mð> 	�s—‘×.¨$ð?Mð@ 	�S—]‘]×* dðAMðB 	�gðCMðD 	˜3×.Ñ.×6°$ðEMðF 	œ  ^°TÓ:×B¸dòGMðH 	”w˜s M°2Ó6×>¸$ðIMðJ 	”g˜c <°Ó4×<¸ðKMðL 	œœg c¨?¸EÓBÓCðMMðN 	�!—(‘(ðOMðP 	”4˜Ÿ	™	Ÿ RÓ(ðQMðR 	”�a—g‘g—m Ó$ðSMðT 	ÁtÕLÂt¹G¸AÀ!˜a°QÔ7ÁtÓLðUMðV 	˜1™9‘4ØŸN™NØŸ_™_ØŸ,™,ØŸ=™=Ø ×0Ñ0Ø !× 2Ñ 2Ø—y‘yà×+Ñ+Ñ7¸A¿I¹IÑ<Qð ×(Ñ(¨1¯9©9Ò4ØW[ñ
+
ðWMðl 	�HðmMðn 	˜?ðoMðp 	˜ðqMðr 	œ Ð&6¸Ó=ðsMðt 	œw sÐ,>ÀÓEðuMðv 	”w˜s N°DÓ9ðwMðx 	”w˜s N°DÓ9ðyMðz 	”tœG C¨¸Ó?Ó@ð{Mð| 	”w˜s N°AÓ6×;¸!ñ}Mô~ ˜S ,°Ó5Ü˜c =°!Ó4×9¸Ü”w˜s I¨uÓ5Ó6Ø"Üœ  Z°Ó7Ó8Ø×%Ñ% aÓ.ô   §¨¬G°C¸ÈÓ,M×,SÐQSÐ TÐV_Ó`ð òYMð MùôT Ms   Õ[1c                 óÆ   • U (       a/  [        [        U [        5      (       a  U OU 45      (       a  U(       d  / $  SSKJn  UR                  X5      $ ! [         a    / s $ f = f)zireg is the listing plate, or the listing plate and the original
plate underneath, see db.sighting_plates.r   ©Údb)r‚   Ú
isinstanceÚtuplerz   rš   Úsighting_historyÚ	Exception)r'   r�   rš   s      r   rŽ   rŽ   Ø   sU   € ö ”c¤¨C´×!7Ñ!7™#¸c¸V×DÑDÎIØˆ	ðÝØ×"Ñ" 3Ó2Ð2øÜó ØŠ	ðús   ºA ÁA ÁA c                 óh   • U (       d  0 $  SSK Jn  UR                  U 5      $ ! [         a    0 s $ f = f)zZEvery answered deep read for the sale, keyed (platform_key, key),
never fatal to the push.r   r™   )rz   rš   Údeep_reads_forrž   )r�   rš   s     r   Ú_deep_readsr¡   ä   s9   € ö Øˆ	ðÝØ× Ñ  Ó+Ð+øÜó ØŠ	ðús   ‹" ¢1°1c                 óê  • SSK Jn  U R                  nSR                  UR                  =(       d    SR                  5       R                  5       5      =(       d    SnU=(       d    UR                  =(       d    SUUR                  =(       d    SUR                  =(       d    SUR                  U5      =(       d    SUR                  UR                  UR                  [        UR                  UR                   5      5      UR                   =(       d    SUR                  UR                   5      UR                  =(       d    SUR"                  =(       d    S[%        U R&                  =(       d    / 5      S.$ )zÈThe slim record of a car the rules held back (2026-09-10): enough to
recognise it, every reason it was held on, and the link. No pricing,
no drawer, one photo: these are read now and then, not bid on.r   r   r&   N)r(   r'   r)   r+   r.   r5   r6   rB   rC   rG   r   rH   rI   rP   )rz   r   r{   rƒ   r'   r„   r…   rH   r)   r+   r‡   r5   r6   rB   r   r|   r}   rI   r   rP   )r�   r   r{   r'   s       r   Úheld_back_recordr£   ð   sö   € õ  Ø
�%‰%€CØ
�'‰'�3—7‘7—=˜b×'Ñ'Ó)×/Ñ/Ó1Ó
2×
:°d€Cà×-�c—o‘o×-¨ØØ—‘× ˜DØ—‘×"˜dØ—‘˜cÓ"×* dØ—‘Ø—;‘;Ø—;‘;Ü'¨×(>Ñ(>¸s¿z¹zÓ(JÓKØ—J‘J×& $Ø×.Ñ.¨s¯z©zÓ:Ø—‘×.¨$Ø—]‘]×* dÜ˜Ÿ	™	Ÿ RÓ(ñð r   c
           	      ó8  • SSK Jn
  U=(       d9    [        R                  R                  5       R	                  SS9R                  5       n[        U 5      [        U5      -   nU
R                  S U 5       5      nU Vs/ s H  n[        XíU5      PM     nn[        U5      nU(       a=  U H7  nUR                  UR                  S5      UR                  S5      45      US'   M9     [        U=(       d    SUU[        U5      [        U 5      [        U5      US	.nU	b  U	 Vs/ s H  n[        U5      PM     snUS
'   U=(       d    0 R                  5        H  u  nnUUU'   M     Ub  UUS'   U=(       d    0 R                  5        H  u  nnUUU'   M     U$ s  snf s  snf )a¶  The whole document one push sends: the sale date the list is for,
when it was built, which dealership it belongs to, every shortlisted
and held car, and (page) the page level facts the cockpit is drawn
from plus (state) the Mac's own stars, hides and saved views so the
two screens stay in step. Rejected cars were left out until
2026-09-10; now they go as the slim held_back list (held_back_record)
so Steven can check nothing is held back unnecessarily and release one
with a reason. A car with no reg is still sent, Dealer OS keys on
listing_url as a fallback the same way this project's own detail
cache does. partial=True marks a mid run push (one platform finished,
others still to come).r   r   )Úmicrosecondc              3   óL   #   • U  H  oR                   R                  v •  M     g 7fr2   )r{   r)   )r!   r�   s     r   r#   Ú build_payload.<locals>.<genexpr>  s   é € Ð'BºE°q¯©¯
®
ºEùs   ‚"$r   r(   ry   N)Úschema_versionÚdealer_namer�   Úgenerated_atÚpartialÚshortlistedÚheldÚcarsÚ	held_backÚstate)rz   r   ÚdatetimeÚnowÚreplaceÚ	isoformatr   Ú_canon_make_mapr—   r¡   r   ÚSCHEMA_VERSIONrŒ   Úlenr£   Úitems)Ú	shortlistr­   r�   r©   rª   Úpager°   r«   Úrecordsr¯   r   ÚwhenÚeveryr*   r�   r®   ry   ÚcÚdocr•   Úvs                        r   Úbuild_payloadrÁ   	  sz  € õ  Ø×Uœ8×,Ñ,×0Ñ0Ó2×:Ñ:ÀqÐ:ÐI×SÑSÓU€DÜ�‹Oœd 4›jÑ(€EØ×'Ñ'Ñ'B¹EÓ'BÓB€JÙ:?Ó@º%°QŒJ�q iÖ0¹%€DÐ@Ü�yÓ!€DÞÛˆAØŸ™ !§%¡%¨Ó"7¸¿¹¸u»Ð!FÓGˆAˆf‹Iñ ô )Ø"×* dØØÜ˜“=Ü˜9“~Ü�D“	Øñ	€Cð Ñá9BÓCº°AÔ,¨QÖ/¹ÑCˆˆKÑØ—˜×"Ñ"Ö$‰ˆˆ1ØˆˆA‹ñ %àÑØˆˆG‰Ø—˜B×%Ñ%Ö'‰ˆˆ1ØˆˆA‹ñ (à€Jùò1 Aùò" Ds   Á<FÄFc	                 óž  • SSK Jn	  U =(       d    0 n [        U=(       d    / 5      n
U=(       d    0 n0 nU	R                   H5  nU	R	                  U5      [        UR                  US5      5      XÚ;   S.XÍ'   M7     / nU=(       d    /  H   nUR                  U[        U5      S.5        M"     U R                  S5      =(       d    0 nU=(       d    0 UU=(       d    /  Vs/ s H  n[        U5      PM     snU	R                  R                  5        VVs0 s H  u  nnU[        U5      _M     snn[        U	R                  5      UU R                  S5      =(       d    SU R                  S	5      U	R                  U R                  S
5      U R                  S5      U R                  S5      U R                  S5      U R                  S5      S.UR                  5        VVs0 s H  u  nnUS;   d  M  UU_M     snnS.U=(       d    0 U	R                   VVVVVs/ s H  u  nnnnnUUUUUS.PM     snnnnnUUS.$ s  snf s  snnf s  snnf s  snnnnnf )aC  The page level block: the settings that shape the cockpit page, the
health notices, the summary counts, the model families for the Model
filter, which filters apply to which platforms, and the platforms
themselves. settings is the raw db.get_settings() dict merged over the
defaults by the caller (daily_run._get_settings).r   r   T)ÚlabelÚenabledÚhidden)rU   rS   Ú_updated_atÚdetails_stylerw   Údefault_price_ceilingÚtheme_bgÚtheme_priceÚ
theme_pillÚ
theme_iconÚtheme_sidebar)ÚbgÚpriceÚpillÚiconÚsidebar)rÇ   rÈ   )rÇ   rÈ   Ú	top_picksÚthemeÚ
updated_at)r(   rÃ   ÚsiteÚgateÚextra)ÚsummaryÚnoticesÚmodel_familiesÚfilter_scopeÚlenient_filtersÚ	platformsÚsettingsÚintegrationsÚ
run_checksÚrunÚbidbrain_version)rz   r   r   Ú_ALL_PLATFORMSÚ_platform_labelrŒ   r   ÚappendÚ_notice_kindÚ_FILTER_SCOPEr¸   Ú_LENIENT_FILTERSÚTOP_PICKS_LABELÚ_RUN_MODAL_CHECKS)rß   rÚ   rÙ   rÛ   Úplatform_enabledÚhidden_connectionsrà   râ   rã   r   rÅ   rÄ   rÞ   r(   ÚkindsÚnÚupdatedr4   r•   rÀ   ÚlabrÖ   r×   rØ   s                           r   Úpage_recordrò   5  s#  € õ  Ø�~˜2€HÜÐ$×*¨Ó+€FØ×$ "€GØ€IØ×$Ô$ˆØ#)×#9Ñ#9¸#Ó#>Ü%)¨'¯+©+°c¸4Ó*@Ó%AØ$'¡Mñ3ˆ	‹ñ %ð €EØ�m˜ŠmˆØ�‰˜a¬°a«Ñ9Ö:ñ à�l‰l˜=Ó)×/¨R€Gà—=˜bØØ-;×-A¸rÐ-AÓCÒ-A qœ4 ž7Ñ-AÑCØ06×0DÑ0D×0JÑ0JÔ0LÔMÒ0L©¨¨1˜œD ›GšÑ0LÒMÜ × 7Ñ 7Ó8Øà%Ÿ\™\¨/Ó:×F¸hØ%-§\¡\Ð2IÓ%JØ×/Ñ/à—l‘l :Ó.Ø!Ÿ™ mÓ4Ø Ÿ™ \Ó2Ø Ÿ™ \Ó2Ø#Ÿ<™<¨Ó8ñð -4¯M©M¬Oô OªO¡D A qØÐ#MÑMó  ˜1˜aš4©Oò Oñ
ð %×*¨à8>×8PÒ8P÷RÚ8PÑ4˜1˜c 4¨¨uð  !¨3¸ÀdÐUZÔ[Ù8PõRàØ,ñ3ð ùò DùÛMùóOùöRs   ÃH5ÄH:ÇI Ç*I ÈIc                 ó˜   • SSK Jn  U R                  S5      (       a  gUR                  U /5      u  p#pEU(       a  gU(       a  gU(       a  gg)	z¤Same buckets the cockpit's own bell and Run button use
(render._categorize_notices): "ok" for the happy path confirmation,
else glass, dealerkit, platform or other.r   r   zAll ÚokÚglassÚ	dealerkitrG   Úother)rz   r   r    Ú_categorize_notices)rU   r   rõ   rö   rG   Ú_alertss         r   rç   rç   g  sE   € õ  Ø‡��v×ÑØØ*0×*DÑ*DÀdÀVÓ*LÑ'€E�hÞØÞØÞØØr   c                 óÜ  • U R                  S/ 5       H‹  n[        UR                  S5      [        5      (       a  US   S U US'   UR                  S5      n[        U[        5      (       d  MZ  [        UR                  S5      [        5      (       d  M€  US   S U US'   M�     U R                  S/ 5       H4  n[        UR                  S5      [        5      (       d  M)  US   S U US'   M6     U $ )Nr®   r   rw   ÚphotosÚ	purchases)r   r›   r   Údict)Úpayloadrï   r¾   ÚdÚrs        r   Ú_trim_photosr  x  sÊ   € Ø�[‰[˜ Ö$ˆÜ�a—e‘e˜LÓ)¬4×0Ñ0Ø ™o¨b¨qÐ1ˆAˆl‰OØ�E‰E�(‹OˆÜ�aœ×Ó¤:¨a¯e©e°H«o¼t×#DÓ#DØ˜H™+ b q˜/ˆAˆh‹Kñ %ð �[‰[˜ bÖ)ˆÜ�a—e‘e˜LÓ)¬4×0Ó0Ø ™o¨b¨qÐ1ˆAˆl‹Oñ *ð €Nr   c                 óˆ  • 0 nU R                  S5      n[        U[        5      (       a4  [        U5      [        :”  a!  [        U5      [        -
  US'   US[         U S'   U R                  S5      n[        U[        5      (       a4  [        U5      [
        :”  a!  [        U5      [
        -
  US'   US[
         U S'   U(       a  XS'   U $ )aP  The last, most aggressive resort on a genuinely large day (2026-09-
21), after the photo trim above still is not enough: cap held_back and
lost_bids to their own CAP_*_TO length, dropped_counts says exactly how
many of each were left off so nothing reads as the whole list when it
is not. held_back is already sorted cheapest first, so the cap keeps
the ones nearest the buying threshold; lost_bids keeps the most recent
(its own newest first order). purchases, under_offer and every
settings block are never touched here, Dealer OS draws its own
Purchases and settings pages from them whole.r¯   NÚ	lost_bidsÚcapped)r   r›   r   r·   ÚCAP_HELD_BACK_TOÚCAP_LOST_BIDS_TO)rþ   Údroppedr¯   r  s       r   Ú
_cap_listsr  …  sµ   € ð €GØ—‘˜KÓ(€IÜ�)œT×"Ñ"¤s¨9£~Ô8HÓ'HÜ" 9›~Ô0@Ñ@ˆ�ÑØ(Ð):Ô*:Ð;ˆ�ÑØ—‘˜KÓ(€IÜ�)œT×"Ñ"¤s¨9£~Ô8HÓ'HÜ" 9›~Ô0@Ñ@ˆ�ÑØ(Ð):Ô*:Ð;ˆ�ÑÞØ#�ÑØ€Nr   r
   c                 ó\   • [         R                  " [         R                  " U [        S95      $ )zdA plain dict Dealer OS can parse: anything json cannot carry
(a date, a Decimal) becomes its string.)Údefault)ÚjsonÚloadsÚdumpsÚstr)rÿ   s    r   Ú	_jsonabler  ¢  s   € ô �:Š:”d—j’j ¬CÑ0Ó1Ð1r   c                 óê  • U =(       d    0 n S[        U Vs0 s H  owU R                  U5      _M     snU R                  S5      =(       d    0 U R                  S5      =(       d    / U=(       d    0 [        U=(       d    / 5      [        U=(       d    / 5      [        U5      U=(       d    0 R                  5        VVs0 s H  u  pxU[	        U5      1 Sk-  ;   d  M  Xx_M     snnS.5      0$ s  snf s  snnf )a�  Everything the Mac Studio's own Settings page shows, so Dealer OS can
draw a settings page from the push alone (2026-09-03, step three).
buying_rules carries the current value of every rule Dealer OS may send
back; the read only blocks (fee_bands, banned_engine_names,
run_schedule) are shown, never accepted back. updated_at is the Mac's
own stamp per key, the newest wins comparison.Úmac_settingsrà   rí   >   rà   Úpurchase_flowÚpurchase_columnsrí   Úpurchase_dropdowns)Úbuying_rulesrà   rí   Ú	fee_bandsÚbanned_engine_namesÚrun_scheduleÚeditable_sectionsrÕ   )r  r   r   r¸   Úset)	Úmergedr  r  r  rÕ   r  Úbuying_rule_keysr•   rÀ   s	            r   Úmac_settings_blockr  ¨  sê   € ð �\�r€FØœIÙ3CÓDÒ3C¨a˜FŸJ™J q›MÒ)Ñ3CÑDØŸ
™
 >Ó2×8°bØ$Ÿj™jÐ)=Ó>×DÀ"Ø—_ "Ü#Ð$7×$=¸2Ó>Ü˜\×/¨RÓ0Ü!Ð"3Ó4Ø)3×)9°r×(@Ñ(@Ô(Bô ZÒ(B¡ ØœsÐ#3Ó4ò  8Yñ   Yñ  Yó �q’tÑ(Bò Zñ
'ó 
ð 
ð 
ùÚDùóZs   –C*
Â;C/ÃC/c                 óÜ   • S nU" U 5      U" U5      U" U5      [        U=(       d    0 5      U" U5      [        [        U=(       d    / 5      5      [        [        U=(       d    / 5      5      S.$ )aQ  The three record lists the Mac's own Purchases, Under Offer and
Didn't win pages draw (2026-09-02), sent so Dealer OS can draw the same
pages. Each row is exactly what the Mac's own page gets from its API
(serve._purchases_with_bias, db.list_under_offer,
serve._lost_bids_with_bias), minus local file paths. Read only on the
Dealer OS side for now: an edit made there does not come back yet, so
the Mac's own Purchases page stays the place to edit a record.
purchase_settings carries the per dealership dropdown answers, column
layout, journey flags and saved views those pages are built from.c           	      óx  • / nU =(       d    /  GH  n[        U5      R                  5        VVs0 s H  u  p4U[        ;  d  M  X4_M     nnn[        UR                  5       5       H¨  u  p4[	        U[        5      (       a[  [        S U 5       5      (       aD  U Vs/ s H2  n[	        U[        5      (       a  UR                  S5      (       a  M0  UPM4     snXS'   Mu  [	        U[        5      (       d  MŒ  UR                  S5      (       d  M¤  S XS'   Mª     UR                  [        U5      5        GM     U$ s  snnf s  snf )Nc              3   ór   #   • U  H-  n[        U[        5      =(       a    UR                  S 5      v •  M/     g7f)Ú/N)r›   r  r    r3   s     r   r#   Ú/records_block.<locals>.clean.<locals>.<genexpr>Î  s,   é € Ð.cÒabÐ\]¬z¸!¼SÓ/A×/WÀaÇlÁlÐSVÓFWÔ/WÒabùr%   r!  z/Users/)
rý   r¸   ÚLOCAL_PATH_KEYSr   r›   r‚   r  r    ræ   r  )r“   Úoutr   r•   rÀ   rÿ   r4   s          r   ÚcleanÚrecords_block.<locals>.cleanÉ  së   € ØˆØ—˜“ˆAÜ"& q£'§-¡-¤/ÔN¢/™$˜!°Q¼oÑ5M“�’¡/ˆAÑNÜ˜QŸW™W›Yž‘�Ü˜a¤×&Ñ&¬3Ñ.cÑabÓ.c×+cÑ+cÙ'(Ó[¢q !´¸A¼s×1CÑ1CÈÏÉÐUX×HYŸA¡qÑ[�A“DÜ ¤3×'Ó'¨A¯L©L¸×,CÓ,CØ�A“Dñ	 (ð
 �J‰J”y “|×$ñ ð ˆ
ùó Oùò \s   ®D1ÁD1Â/D7ÃD7)rü   Úunder_offerr  Úpurchase_settingsÚpurchase_eventsÚlost_bids_viewsÚdealerkit_stock)r  r   )rü   r'  r  r(  r)  r*  r+  r%  s           r   Úrecords_blockr,  ¾  sg   € ò
ñ ˜9Ó%Ù˜[Ó)Ù˜9Ó%Ü&Ð'8×'>¸BÓ?ñ ! Ó1Ü$¤T¨/×*?¸RÓ%@ÓAä$¤T¨/×*?¸RÓ%@ÓAñð r   c                 ó„   • [         R                  " U 5      R                  S5      nU[        R                  " U[
        SS94$ )zÿThe document as the pair that matters: the JSON bytes Dealer OS
measures once it has decompressed them, and the gzipped bytes that
actually travel. mtime=0 so the same document always compresses to
the same bytes, which keeps a push comparable run to run.úutf-8r   )Úmtime)r  r  ÚencodeÚgzipÚcompressÚCOMPRESS_LEVEL)rþ   Úbodys     r   Ú_encoder5  ã  s5   € ô
 �:Š:�gÓ×%Ñ% gÓ.€DØ”—’˜t¤^¸1Ñ=Ð=Ð=r   c                 óX   • [        U 5      [        :„  =(       d    [        U5      [        :„  $ )zrEither ceiling blown: the uncompressed size Dealer OS enforces, or
the compressed size Vercel will actually carry.)r·   ÚMAX_BODY_BYTESÚMAX_WIRE_BYTES)r4  Úwires     r   Ú_too_bigr:  ì  s!   € ô ˆt‹9”~Ñ%×C¬¨T«´^Ñ)CÐCr   c           
      ól  • U=(       d    SR                  5       R                  S5      nU=(       d    SR                  5       nU(       a  U(       d  [        S5      e[        U 5      u  pV[	        XV5      (       a  [        [        U 5      5      u  pV[	        XV5      (       aY  [        [        [        U 5      5      5      u  pV[	        XV5      (       a*  [        S[        U5      S-   S[        U5      S-   S35      e[        R                  R                  U[        -   USS	S
SU 3SS.S9nU=(       d    [        R                  R                  n U" XsS9 n	U	R                  5       n
SSS5        [        U5      q[        U5      qW
(       d  0 $  [.        R0                  " U
R#                  S5      5      $ ! , (       d  f       NS= f! [        R                  R                   aò  nUR                   S;   a  [        S5      UeUR                   S:X  a  [        S[         S35      UeSn UR                  5       R#                  SS5      SS R                  5       nO! [$         a     Of = fUR                   S;   a(  SU;   d  SU;   a  [        SUR                    SU 35      Ue[        SUR                    S3U(       a  S U 3-   5      UeS-   5      UeSnAf[        R                  R&                   a!  n[        S!U SUR(                   35      UeSnAff = f! [2         a    S"U
R#                  SS5      SS 0s $ f = f)#aJ  POST the document. Returns whatever JSON the server answered with
({} on an empty 2xx). Raises DealerOsPushError, in plain words, on a
missing setting, a refused secret, a missing letterbox, or a network
failure. opener exists only so the tests can hand in a fake; real
callers never pass it. The body is gzipped: Dealer OS's own route
decompresses it and measures the size afterwards. A document over
either ceiling has its photo galleries trimmed and is built once
more; still too big it also has held_back and lost_bids capped
(_cap_lists) and is built a third time before giving up.r&   r!  ztDealer OS is not set up on this Mac: DEALER_OS_BASE_URL and DEALER_OS_API_KEY both need a value in dealer_config.py.zThe list is too big to send (iè  z KB, z• KB compressed, even with the photo galleries trimmed and the held back and lost bids lists capped). Dealer OS accepts about 19 MB once uncompressed.ÚPOSTzapplication/jsonr1  zBearer ÚBidBrain)zContent-TypeúContent-EncodingÚAuthorizationz
User-Agent)ÚdataÚmethodÚheaders)ÚtimeoutN)i‘  i“  z–Dealer OS refused the shared secret. The DEALER_OS_API_KEY in dealer_config.py must be this dealership's own token from Dealer OS's BIDBRAIN_API_KEYS.i”  zDealer OS has no letterbox at zl yet, so there is nowhere to send the list. That route is Dealer OS's side to build (see DEALER_OS_PUSH.md).r.  r³   r   )i�  iŸ  znot valid JSONr>  z–Dealer OS could not read the compressed list, so its side is older than the version that accepts a gzipped push. It needs updating first. It answered z: zDealer OS answered z to the push.r0   zCould not reach Dealer OS at Úraw)ÚstripÚrstripr   r5  r:  r  r  r·   ÚurllibÚrequestÚRequestÚAPI_PATHÚurlopenÚreadÚerrorÚ	HTTPErrorÚcodeÚdecoderž   ÚURLErrorÚreasonÚLAST_BODY_BYTESÚLAST_WIRE_BYTESr  r  Ú
ValueError)rþ   Úbase_urlÚapi_keyrC  Úopenerr4  r9  ÚreqÚdo_openÚresprD  ÚeÚdetails                r   Úpushr^  ò  sF  € ð —˜B×%Ñ%Ó'×.Ñ.¨sÓ3€HØ�}˜"×#Ñ#Ó%€GÞž7ÜðGóHð 	Hô ˜Ó!�J€DÜ�×ÑÜœ\¨'Ó2Ó3‰
ˆÜ�D×ÑÜ ¤¬L¸Ó,AÓ!BÓC‰JˆDÜ˜×#Ñ#Ü'Ø3´C¸³IÀÑ4EÐ3FÀeÜ˜4“y DÑ(Ð)ð *PðPóQð Qô
 �.‰.×
 Ñ
 Ø”8Ñ $¨vØ!3Ø%+Ø$+¨G¨9Ð"5Ø)ñ+ð !ð ,€Cð ×.œŸ™×.Ñ.€GðKÙ�SÒ*¨dØ—)‘)“+ˆC÷ +ô> ˜$“i€OÜ˜$“i€OÞØˆ	ð=Ü�zŠz˜#Ÿ*™* WÓ-Ó.Ð.÷I +Õ*ûä�<‰<×!Ñ!ó cØ�6‰6�ZÓÜ#ð*ó+ð 12ð2ð �6‰6�S‹=Ü#Ø0´°
ð ;1ð 1ó2ð 89ð9ð ˆð	Ø—V‘V“X—_‘_ W¨iÓ8¸¸#Ð>×DÑDÓF‰FøÜó 	Ùð	úà�6‰6�ZÓÐ%5¸Ó%?ÐCUÐY_ÓC_ô $ð/à/0¯v©v¨h°b¸¸ðBóCð IJðJô  Ø! !§&¡& ¨Ð7Î6¸Q¸v¸h¸<ÑZó\Øabð	cØWYÑZó\Øabð	cûä�<‰<× Ñ ó KÜØ+¨H¨:°R¸¿¹°zÐBóDØIJð	KûðKûô ó =Ø�s—z‘z '¨9Ó5°d°sÐ;Ð<Ò<ð=úsz   Å
F? ÅF.Å!F? Æ	$L Æ.
F<Æ8F? Æ<F? Æ?LÇAK
È 1IÉK
É
IÉK
ÉIÉA+K
Ë
!LË+LÌLÌ!L3Ì2L3)NN)r&   NNNFNN)NNNNNNNN)NNNNr
   r
   )NNNNNNN)é   N)$r   r±   r1  r  Úurllib.errorrG  Úurllib.requestrJ  r¶   r3  r7  r8  ÚTRIM_PHOTOS_TOr  r  rS  rT  ÚRuntimeErrorr   r   r—   rŽ   r¡   r£   rÁ   rò   rç   r  r  r#  r  r  r,  r5  r:  r^  r
   r   r   Ú<module>rd     sø   ðñó> Û Û Û Û ð  €ð
 €ð €ð  €Ø€Ø€ð Ð ØÐ ð €Ø€ôI˜ô IòCôkò\	ò	òð2 LPØPTô)ðX FJØMQØ+/ô/òdð" +ô 
òð4 €ò2ð W[ØOQôð, W[ØNRô"òJ>òDõI=r   