Ë
    Š‰Œjh*  ã            	      óX  — d Z ddlmZ ddlZddlZddlmZ ddlmZ  ee	«      j                  «       j                  Zej                  j                  dz  Zej                  j                  d ee«      «       ej                  j                  d ee«      «       ddlmZ ddlmZ dd	lmZmZ dd
lmZmZmZ ddlmZ deedœ	 	 	 	 	 	 	 	 	 	 	 dd„Zddddœ	 	 	 	 	 	 	 	 	 	 	 dd„Zddddœ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dd„Z dddddeeddœ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dd„Z!y)uM  
V3 wiring for `LiveReadThroughAdapter` â€” the Â§6 seam #1 (data â†’ engine) reading **V3 natively**.

`LiveReadThroughAdapter` was designed DI-friendly from day one: it takes `fetch_positions` (live
snapshot) + `fetch_static` (meta / course / teams) as injectable callables, with V2 HTTP as the
default. This module is the V3 production wiring â€” both callables back into the V3 `Store`
(`reads.boat_position_at` for live fixes, `broadcast_feed.build_feed` for static parts) so the
broadcast pixel + interactive paths run end-to-end off V3 with **no V2 dependency**. The
StreamingScorer, LiveFeedServer, and renderer downstream are unchanged.

The boat-ref join key (`{userId}:boat:<b>`) flows through as the engine's `deviceId` â€” exactly as
`broadcast_feed.resolve_teams` already does â€” so the engine joins frames â†” teams identically.
é    )ÚannotationsN)ÚCallable)ÚPathÚingest)Úresolve_static)ÚLiveReadThroughAdapter)ÚPRODUCER_READ_ENVÚPRODUCER_READ_HEADER)ÚDEFAULT_MAX_AGE_MSÚDEFAULT_PRIMARY_MOUNTÚboat_position_at)ÚStore©Únow_msÚprimary_mountÚ
max_age_msc               ó2   ‡ ‡‡‡‡— |xs d„ Šdˆˆˆˆˆ fd„}|S )uZ  A `fetch_positions` that snapshots the V3 store at call time â€” the V3 analogue of the V2
    `/api/viz/{i}/positions` polling. For each boat, returns its **canonical** position (primary mount
    + failover, Â§13.1) at `now_ms()` if it has a fresh fix within `max_age_ms`, else omits it. Output
    shape matches what `positions_to_frames` expects: `{boatRef: {lat, lon, speed, course, heel,
    timestampUtcMs}}`. `heel` is already derived into the boat frame by ingest, so renderers never
    infer heel from an uncalibrated device roll. `now_ms` is injectable for tests; default is wall-clock UTC.c                 óB   — t        t        j                  «       dz  «      S )Niè  )ÚintÚtime© ó    ú=/srv/vr/releases/25f455cc4e75/v3/broadcast/live/v3_adapter.pyú<lambda>z$v3_fetch_positions.<locals>.<lambda>/   s   € œc¤$§)¡)£+°Ñ"4Ó5€ r   c                 óî   •—  ‰«       } i }‰D ]e  }t        ‰|| ‰‰¬«      }|€Œ|j                  |j                  |j                  xs d|j                  xs d|j
                  |j                  dœ||<   Œg |S )N)r   r   ç        ©ÚlatÚlonÚspeedÚcourseÚheelÚtimestampUtcMs)r   ÚlatitudeÚ	longituder    r!   Úheel_degÚt_ms)	ÚtÚoutÚboatÚfixÚ_nowÚboatsr   r   Ústores	       €€€€€r   Úfetchz!v3_fetch_positions.<locals>.fetch1   s}   ø€ Ù‹FˆØ!ˆØò 		ˆDÜ" 5¨$°ÀÐ[eÔfˆCØˆ{Øà—|‘|¨C¯M©MØŸ™Ò) c°S·Z±ZÒ5FÀ3ØŸ™Ø"%§(¡(ñ	ˆC�ŠIð			ð ˆ
r   ©ÚreturnÚdictr   )r.   r-   r   r   r   r/   r,   s   `` `` @r   Úv3_fetch_positionsr3   !   s!   ü€ ð Ò6Ñ5€D÷ñ ð €Lr   )ÚtokenÚproducer_tokenr   c               ó  ‡‡‡‡‡	‡
— ddl Š	ddl}ddlŠ
| j                  d«      Š|}‰xs |j                  j                  d«      Š‰xs# |j                  j                  t        «      xs dj                  «       xs dŠdˆˆˆ	ˆˆˆ
fd„}|S )uˆ  A `fetch_positions` that polls the **gcloud read API** `GET /v3/view/{instance}/positions`
    instead of querying the store â€” Phase 0 of `docs/plans/live-broadcast-feed-in-gcloud.md`. That
    endpoint runs co-located with the DB + a warm `LiveStateCache`, so ONE cached HTTP call returns all
    boats fast, replacing the ~N cross-boundary SQL round-trips per tick that `v3_fetch_positions` does
    from the render PC. Output shape matches `positions_to_frames` (`{boatRef: {lat, lon, speed, course,
    timestampUtcMs}}`). A transient read miss yields `{}` (boats hold last position), same effect as a
    stale-fix omit.

    `token` reaches a non-public instance; a public one needs none. IT IS A PARAMETER, and
    `$VR_READ_TOKEN` is only the fallback, because the producer is given its credential on the
    COMMAND LINE (`--read-token`) and every other read on the plane â€” roster, course, wind, clips â€”
    takes it from there through `RestBackend`. Reading the environment only meant this one fetch ran
    anonymous while the rest authenticated, and its 401 is swallowed by the `except` below into the
    same empty dict a transient miss returns. Measured 2026-08-09: the producer took a race ON AIR
    (roster, course and wind all read fine) and then published nothing for the whole race, reporting
    "no positions from the read plane" â€” a credential fault wearing a network fault's clothes.r   Nú/ÚVR_READ_TOKENÚ c                 óü  •— ‰› d‰› d�} ‰j                   j                  | «      }‰
r|j                  dd‰
› �«       ‰	r|j                  t        ‰	«       	 ‰j                   j	                  |d¬«      5 }‰j                  |j                  «       j                  d«      «      }d d d «       xs i j                  «       D ��ci c]’  \  }}t        |t        «      r}|j                  d«      �l||j                  d«      |j                  d	«      |j                  d
«      xs d|j                  d«      xs d|j                  d«      |j                  d«      dœ“Œ” c}}S # 1 sw Y   Œ»xY w# t        $ r i cY S w xY wc c}}w )Nz	/v3/view/z
/positionsÚAuthorizationzBearer é   )Útimeoutzutf-8r   r   r    r   r!   r"   r#   r   )ÚrequestÚRequestÚ
add_headerr
   ÚurlopenÚloadsÚreadÚdecodeÚ	ExceptionÚitemsÚ
isinstancer2   Úget)ÚurlÚreqÚrÚdataÚrefÚpÚbaseÚinstance_idÚjsonr5   r4   Úurllibs         €€€€€€r   r/   z#http_fetch_positions.<locals>.fetchb   s_  ø€ ð ��i ˜}¨JÐ7ˆØ�n‰n×$Ñ$ SÓ)ˆÙØ�N‰N˜?¨g°e°WÐ,=Ô>ÙØ�N‰NÔ/°Ô@ð	Ø—‘×'Ñ'¨°QÐ'Ó7ð <¸1Ø—z‘z !§&¡&£(§/¡/°'Ó":Ó;�÷<ð  $šz r×0Ñ0Ó2÷	Eñ �C˜Ü˜a¤Ô&¨1¯5©5°«<Ð+Cð ˜QŸU™U 5›\°!·%±%¸³,Ø Ÿu™u W›~Ò4°ÀÇÁÀhÃÒ@VÐSVØŸe™e F›mØ()¯©Ð.>Ó(?ñAñ Aó Eð 	E÷	<ð <ûäò 	ØŠIð	üóEs1   ÁE' Á2/EÂ!E' Ã BE8ÅE$Å E' Å'E5Å4E5r0   )rQ   ÚosÚurllib.requestÚrstripÚenvironrH   r	   Ústrip)Ú	read_baserP   r4   r5   r   rS   Ú_r/   rO   rQ   rR   s    ```    @@@r   Úhttp_fetch_positionsrZ   B   sz   ý€ ó0 ÛÛØ×Ñ˜CÓ €DØ€AØÒ4�R—Z‘Z—^‘^ OÓ4€EØ$ÒO¨¯
©
¯©Ô7HÓ(IÒOÈR×VÑVÓXÒ`Ð\`€N÷Eò EðB €Lr   )Úteam_configÚ	course_idÚwind_serialsc               ó,   ‡ ‡‡‡‡‡‡— dˆˆˆˆˆˆ ˆfd„}|S )ux  A `fetch_static` that returns `(meta, course, teams)` from V3 reads via `broadcast_feed.resolve_static`
    â€” the V3 replacement for the V2 Firestore resolution. The injected `meta` and `course` come from the
    management plane (Â§15.1); `teams` is derived from `entry_occupancy` at the gun. `course_id` enables
    wind merge into `meta`. Uses `resolve_static` (NOT `build_feed`) so the live path never materialises
    frames across the open-ended live window â€” that walks every second of a 24 h span and hangs against a
    real DB; live positions come from the separate `fetch_positions` poll, so frames here are unneeded.c            
     ój   •— d } ‰�ddl m}  |‰«      } t        ‰‰‰‰‰‰	‰| ¬«      }|d   |d   |d   fS )Nr   )Ú_course_wind_from_deg)Úmetar!   r[   r\   Úwind_direction_fallback_degra   r!   Úteams)Úbroadcast_feedr`   r   )
Úfallbackr`   Úfeedr-   r!   r\   rP   ra   r.   r[   s
      €€€€€€€r   r/   zv3_fetch_static.<locals>.fetch—   sS   ø€ ØˆØÐ Ý<Ù,¨VÓ4ˆHÜØ�; Ø˜f°+ÈØ(0ô
ˆð
 �F‰|˜T (™^¨T°'©]Ð:Ð:r   )r1   ztuple[dict, dict, list]r   )	r.   rP   r-   ra   r!   r[   r\   r]   r/   s	   ```````  r   Úv3_fetch_staticrg   †   s   þ€ ÷"
;ó 
;ð €Lr   )r[   r\   r]   Úwindr   r   r   rX   c       
        ó„   — |rt        |||
¬«      nt        | ||
||¬«      }t        |||	|t        | |||||||¬«      ¬«      S )uŽ  Construct a `LiveReadThroughAdapter` wired to V3 reads â€” the production path replacing V2
    polling. The downstream contract (StreamingScorer â†� frames; LiveFeedServer â†� deltas) is unchanged.
    Usage:

        adapter = build_v3_live_adapter(store, "flight-1", "g-day1", boats,
                                        meta=meta, course=course, team_config=cfg)
        meta_s, course_s, teams = adapter.resolve_meta_course_teams()
        scorer = StreamingScorer(meta_s, course_s, teams)
        for batch in adapter.poll_frames(interval_s=1.0):
            for sec, devs in batch.items():
                scorer.push_frame(int(sec), devs)
    )r   r   )ra   r!   r[   r\   r]   )rh   Úfetch_positionsÚfetch_static)rZ   r3   r   rg   )r.   rP   Úgroup_idr-   ra   r!   r[   r\   r]   rh   r   r   r   rX   rj   s                  r   Úbuild_v3_live_adapterrm   ¥   sb   € ñB HQÔ˜Y¨¸FÕCÜØ�5 °}ÐQ[ô
ð ô "Ø�X DØ'Ü$Ø�; ¨D¸Ø#¨yÀ|ô
ôð r   )r.   r   r-   ú	list[str]r   úCallable[[], int] | Noner   Ústrr   r   r1   úCallable[[], dict])rX   rp   rP   rp   r4   ú
str | Noner5   rr   r   ro   r1   rq   )r.   r   rP   rp   r-   rn   ra   r2   r!   r2   r[   údict | Noner\   rr   r]   úlist[str] | Noner1   z%Callable[[], tuple[dict, dict, list]])r.   r   rP   rp   rl   rp   r-   rn   ra   r2   r!   r2   r[   rs   r\   rr   r]   rt   rh   rs   r   ro   r   rp   r   r   rX   rr   r1   r   )"Ú__doc__Ú
__future__r   Úsysr   Úcollections.abcr   Úpathlibr   Ú__file__ÚresolveÚparentÚ_HEREÚ_INGESTÚpathÚinsertrp   rd   r   Úlive_adapterr   Úproducer_read_authr	   r
   Úreadsr   r   r   r.   r   r3   rZ   rg   rm   r   r   r   ú<module>r„      s6  ðñõ #ã 
Û Ý $Ý áˆX‹×ÑÓ ×'Ñ'€Ø
�,‰,×
Ñ
 Ñ
(€Ø ‡�‡��‘3�u“:Ô Ø ‡�‡��‘3�w“<Ô  å )Ý /ß Fß MÑ MÝ ð (,Ø.Ø(ñØðàðð %ð	ð
 ðð ðð óðJ Ø!%Ø'+ñAØðAàðAð ð	Að
 ðAð %ðAð óAðV  $Ø Ø%)ñØðàðð ðð
 ðð ðð ðð ðð #ðð +óðN  $Ø Ø%)ØØ'+Ø.Ø(Ø ñ-Øð-àð-ð ð-ð ð	-ð ð-ð ð-ð ð-ð ð-ð #ð-ð ð-ð %ð-ð ð-ð ð-ð ð-ð  ô!-r   