Ë
    Š‰ŒjN*  ã                  óè   — d Z ddlmZ ddlZddlZddlmZmZ ddlm	Z	 ej                  j                  d e e	e«      j                  «       j                  d   dz  «      «       dd„Z G d	„ d
«      Zy)u’	  
Live read-through adapter  â€”  Â§6 seam #1: data â†’ engine.

Production replacement for the demo's batch exporter (demo/export/export_team_race.py). Instead of
pulling a fixed window of tracks from Firestore and scoring in one shot, this reads V2's live position
feed for an in-progress start group and produces the SAME feed dict the engine consumes â€”
`{meta, course, teams, frames}` â€” incrementally as positions arrive, so `core.score_race.enrich`
(via StreamingScorer) is reused verbatim. The batch exporter stays for replays / off-season.

Input (V2): the running backend exposes the live cache over HTTP â€”
    GET /api/viz/{instanceId}/positions  ->  {deviceId: {lat, lon, speed, course, timestampUtcMs, ...}}
(see _legacy/app_viewregatta_com/routers/visualization.py + services/live_state.py). This adapter polls that
endpoint and maps each device's latest fix into the engine's frame contract.

Output contract (identical to the demo exporter, so the engine is unchanged):
    meta   : {title, user, instanceId, groupId, openEnded, raceType,
              trackStartUtcMs, gunUtcMs, trackEndUtcMs, frameStepMs, windFromDeg, windKts}
    course : {name, center, startLine{port,starboard}, finishLine{port,starboard}, waypoints[]}
    teams  : [{teamId, name, boats:[{deviceId, sailId, visualId, color, sternM}]}]
    frames : {"<sec>": {deviceId: {lat, lon, speed, course}}}   sec = (timestampUtcMs - trackStartUtcMs)//1000

What is implemented vs. wired:
  * `positions_to_frames` â€” the pure GPSâ†’frame transform (mirrors the exporter's `pull_frames` record
    shape: lat/lon 7 dp, speed 2 dp, course 1 dp). Fully unit-tested.
  * `poll_frames` â€” incremental streaming with de-dup, over an **injectable** position source (default:
    HTTP GET to the V2 endpoint via `requests`/`urllib`). Testable with a fake source.
  * `resolve_meta_course_teams` â€” needs V2 Firestore (raceInstances / startGroups / courses /
    deviceMounts, resolving the **time-scoped deviceâ†’boat** assignment in effect at the gun). That
    requires V2 credentials, so it runs through an **injectable** `fetch_static` callable; the default
    raises with a pointer to the exporter logic to port (device_for_boat + course doc in
    demo/export/export_team_race.py). Wire this when running against a live V2 instance.

Wind is manual / weather-station only (never motion-derived) â€” passed in via `wind`.
Sub-phase: B-A (live scoring / live input).
é    )ÚannotationsN)ÚCallableÚIterator)ÚPathé   Úscoringc           
     óÐ  — i }| xs i j                  «       D ]Ì  \  }}|j                  d«      }|�"|j                  d«      �|j                  d«      €Œ<t        ||z
  dz  «      }|dk  rŒSt        |d   d«      t        |d   d«      t        |j                  dd«      xs dd«      t        |j                  d	d«      xs dd
«      |dœ|j	                  t        |«      i «      |<   ŒÎ |S )zÆMap a V2 live-positions snapshot to engine frames `{ "<sec>": {dev: {lat,lon,speed,course}} }`.
    Pure: skips devices with no fix or a pre-start (negative-sec) timestamp; rounds like the exporter.ÚtimestampUtcMsÚlatÚlonéè  r   é   Úspeedr   Úcourseé   )r   r   r   r   r
   )ÚitemsÚgetÚintÚroundÚ
setdefaultÚstr)Ú	positionsÚtrack_start_msÚoutÚdevÚpÚtsÚsecs          ú?/srv/vr/releases/25f455cc4e75/v3/broadcast/live/live_adapter.pyÚpositions_to_framesr    /   sð   € ð €CØ’? ×)Ñ)Ó+ò 
‰ˆˆQØ�U‰UÐ#Ó$ˆØˆ:˜Ÿ™˜u›Ð-°·±°u³Ð1EØÜ�2˜Ñ&¨4Ñ/Ó0ˆØ�Š7Øä˜˜5™ 1Ó%¬e°A°e±H¸aÓ.@Ü˜1Ÿ5™5 ¨!Ó,Ò1°°1Ó5ÄÀqÇuÁuÈXÐWXÓGYÒG^Ð]^Ð`aÓAbð !ñ-
ˆ�‰”s˜3“x Ó$ SÒ)ð
ð €Jó    c                  óx   — e Zd ZdZddddddœ	 	 	 	 	 	 	 dd„Zdd„Zdd„Zddej                  dœ	 	 	 dd	„Z	dd
„Z
y)ÚLiveReadThroughAdapterzTStreams the broadcast feed for one live start group out of V2's live position cache.N)Úbase_urlr   Úfetch_positionsÚfetch_staticc               óÌ   — || _         || _        |xs i | _        |r|j                  d«      nd | _        || _        |xs | j                  | _        || _        i | _	        i | _
        y )Nú/)Úinstance_idÚgroup_idÚwindÚrstripr$   r   Ú_http_fetch_positionsÚ_fetch_positionsÚ_fetch_staticÚ_latest_positionsÚ_latest_source_ms)Úselfr)   r*   r+   r$   r   r%   r&   s           r   Ú__init__zLiveReadThroughAdapter.__init__H   si   € ð 'ˆÔØ ˆŒØ’J˜BˆŒ	Ù08˜Ÿ™¨Ô,¸dˆŒØ,ˆÔØ /Ò M°4×3MÑ3MˆÔØ)ˆÔð 35ˆÔð
 :<ˆÕr!   c                ó˜   — | j                   €t        d«      ‚| j                  «       \  }}}|j                  d«      �
|d   | _        |||fS )u@  Build the static feed parts at gun time. Delegates to the injected `fetch_static` (production
        wires this to the V2 Firestore reads â€” raceInstances/startGroups/courses/deviceMounts with the
        time-scoped deviceâ†’boat assignment, as in the batch exporter). Caches trackStartUtcMs for
        frame timing.u²   Provide fetch_static (meta, course, teams) â€” port device_for_boat + course doc + the time-scoped deviceMounts from demo/export/export_team_race.py against the live V2 instance.ÚtrackStartUtcMs)r/   ÚNotImplementedErrorr   r   )r2   Úmetar   Úteamss       r   Úresolve_meta_course_teamsz0LiveReadThroughAdapter.resolve_meta_course_teams_   se   € ð
 ×ÑÐ%Ü%ðnóð ð #×0Ñ0Ó2Ñˆˆf�eØ�8‰8Ð%Ó&Ð2Ø"&Ð'8Ñ"9ˆDÔØ�V˜UÐ"Ð"r!   c                óü  ‡ — ‰ j                   €t        d«      ‚t        ‰ j                  «       ‰ j                   «      }g }|j	                  «       D ]Ü  \  }}|j	                  «       D ]Ä  \  }}|j                  d«      }‰ j                  j                  |«      }	t        |t        t        f«      rt        |	t        t        f«      r||	k  rŒd|‰ j                  |<   t        |t        t        f«      r|‰ j                  |<   |j                  ||f«       |j                  t        |«      «       ŒÆ ŒÞ |si S t        ˆ fd„‰ j                  j                  «       D «       «      }
t        |
«      t!        ‰ j                  «      iS )uX  ONE poll: the frames that are new since `seen` (which this call updates). Split out of
        `poll_frames` so a producer that owns its own loop â€” one that can hand over to the NEXT race
        between ticks without restarting (`serve_v3_live.broadcast_following`) â€” shares this exact
        de-duplication rather than reimplementing it.uJ   track_start_ms unknown â€” call resolve_meta_course_teams() or pass it in.r
   c              3  ó¤   •K  — | ]G  }t        |j                  d «      t        t        f«      r t        |d    ‰j                  z
  dz  «      –— ŒI y­w)r
   r   N)Ú
isinstancer   r   Úfloatr   )Ú.0r   r2   s     €r   ú	<genexpr>z3LiveReadThroughAdapter.poll_once.<locals>.<genexpr>�   sN   øè ø€ ò MØÜ$ Q§U¡UÐ+;Ó%<¼sÄE¸lÔKô ˜AÐ.Ñ/°$×2EÑ2EÑEÈ$ÑN×Oñ Mùs   ƒAA)r   ÚRuntimeErrorr    r.   r   r   r1   r<   r   r=   r0   ÚaddÚappendÚmaxÚvaluesr   Údict)r2   ÚseenÚbatchÚnew_secsr   Údevsr   ÚrecÚ	source_msÚprevious_msÚfrontiers   `          r   Ú	poll_oncez LiveReadThroughAdapter.poll_oncen   s^  ø€ ð
 ×ÑÐ&ÜÐkÓlÐlÜ# D×$9Ñ$9Ó$;¸T×=PÑ=PÓQˆØ ˆØŸ™›ò 	*‰IˆC�Ø ŸJ™J›Lò *‘��SØŸG™GÐ$4Ó5�	Ø"×4Ñ4×8Ñ8¸Ó=�Ü˜y¬3´¨,Ô7Ü& {´S¼%°LÔAØ%¨Ò4ð Ø.1�×&Ñ& sÑ+Ü˜i¬#¬u¨Ô6Ø2;�D×*Ñ*¨3Ñ/ð —‘˜#˜s˜Ô$Ø—‘¤ C£Õ)ñ#*ð	*ñ& ØˆIô
 ó MØ $× 6Ñ 6× =Ñ =Ó ?ôMó Mˆô �H“œt D×$:Ñ$:Ó;Ð<Ð<r!   g      ð?)Ú
interval_sÚ	max_pollsÚsleepc             #  óœ   K  — t        «       }d}|�||k  r5|dz  }| j                  |«      }|r|–— |�||k  r ||«       |€Œ.||k  rŒ4yy­w)a#  Yield incremental frame batches `{ "<sec>": {dev: {lat,lon,speed,course}} }` as new fixes land,
        de-duped by (device, sec) so each second is emitted once. `max_polls`/`sleep` are injectable for
        tests; in production leave them default and stop by breaking out of the generator.r   Nr   )ÚsetrN   )r2   rO   rP   rQ   rF   ÚpollsÚnews          r   Úpoll_framesz"LiveReadThroughAdapter.poll_frames•   sd   è ø€ ô
 &)£UˆØˆØÐ 5¨9Ò#4Ø�Q‰JˆEØ—.‘. Ó&ˆCÙØ’	ØÐ  E¨IÒ$5Ù�jÔ!ð Ñ 5¨9Õ#4ùs   ‚AAÁAÁ
Ac                ó¦  — | j                   st        d«      ‚| j                   › d| j                  › d�}	 dd l}|j	                  |d¬«      j                  «       S # t        $ rm dd l}dd l}|j                  j                  |d¬«      5 }|j                  |j                  «       j                  d«      «      cd d d «       cY S # 1 sw Y   Y y xY ww xY w)NuO   No base_url set and no fetch_positions injected â€” cannot reach V2 live_state.z	/api/viz/z
/positionsr   é   )Útimeoutzutf-8)r$   r@   r)   Úrequestsr   ÚjsonÚImportErrorÚurllib.requestÚrequestÚurlopenÚloadsÚreadÚdecode)r2   ÚurlrZ   r[   ÚurllibÚrs         r   r-   z,LiveReadThroughAdapter._http_fetch_positions¥   s´   € Ø�}Š}ÜÐpÓqÐqØ—‘�˜y¨×)9Ñ)9Ð(:¸*ÐEˆð	<ÛØ—<‘< ¨Q�<Ó/×4Ñ4Ó6Ð6øÜò 	<ÛÛ!Ø—‘×'Ñ'¨°QÐ'Ó7ð <¸1Ø—z‘z !§&¡&£(§/¡/°'Ó":Ó;÷<÷ <õ <úð	<ús)   µ$A Á.CÂ.CÂ6
CÃC	ÃCÃC)N)r)   r   r*   r   r+   zdict | Noner$   z
str | Noner   ú
int | Noner%   zCallable[[], dict] | Noner&   z,Callable[[], tuple[dict, dict, list]] | None)Úreturnztuple[dict, dict, list])rF   zset[tuple[str, str]]rg   rE   )rO   r=   rP   rf   rQ   zCallable[[float], None]rg   zIterator[dict])rg   rE   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r3   r9   rN   ÚtimerQ   rV   r-   © r!   r   r#   r#   E   sp   „ Ù^ð<Ø(,È4Ø>BØNRñ<Ø%ð<Ø>Hð<à";ð<ð  Ló<ó.#ó%=ðN 25ÈdØ59·Z±Zñ"Ø2ð"ØDRó"ô <r!   r#   )r   rE   r   r   rg   rE   )rk   Ú
__future__r   Úsysrl   Úcollections.abcr   r   Úpathlibr   ÚpathÚinsertr   Ú__file__ÚresolveÚparentsr    r#   rm   r!   r   ú<module>rw      sc   ðñ"õF #ã 
Û ß .Ý ð ‡�‡��‘3‘t˜H“~×-Ñ-Ó/×7Ñ7¸Ñ:¸YÑFÓGÔ Hó÷,k<ò k<r!   