
    %i                    h   S r SSKrSSKrSSKrSSKrSSKrSSKJrJrJrJ	r	  SSK
JrJr  SSKJr  SSKJr  SSKJrJrJrJrJrJrJrJrJrJr  SSKJr  SS	KJrJ r J!r!  SS
K"J#r#J$r$  SSK%J&r&  SSK'J(r(J)r)J*r*  SSK+J,r,J-r-  SSK.J/r/  SSK0J1r1  SSK2J3r3J4r4  SSK5J6r6J7r7  SSK8J9r9  SSK:J;r;  SSK<J=r=J>r>  SSK?J@r@  SSKAJBrB  SSKCJDrD  SSKEJFrF  SSKGJHrH  SSKIJJrJJKrKJLrLJMrMJNrNJOrOJPrPJQrQ  SSKRJSrS  \7" 5         \6" \T5      rU\>" 5       rV\;" 5       rW\," \9S9rX\" SSSS 9rY\X\YR                  lX        \YR                  \/\-5        \" S!S"9r\1 S#kr]\VR                  R                  S$:X  a  S$/OB\VR                  R                  R                  S%5       V s/ s H  o R                  5       PM     sn rb\YR                  \\bS&S$/S$/S'9  \YR                  S(5      S)\4S* j5       re\YR                  S(5      S)\4S+ j5       rf\YR                  S(5      S)\4S, j5       rg\YR                  S(5      S)\4S- j5       rh\YR                  S(5      S)\4S. j5       ri\" \j5      R                  R                  S/-  rl\lR                  5       (       aR  \YR                  S0\&" \o" \lS1-  5      S29S1S39  \lS1-  S4-  rp\pR                  S&S&S59  \YR                  S6\&" \o" \p5      S&S79S8S39  \YR                  S95      S: 5       rs\YR                  S;5      S< 5       rt\$" 5       ruS= rv\" \u5      4S>\#4S? jjrw " S@ SA\(5      rx " SB SC\(5      ry " SD SE\(5      rz " SF SG\(5      r{ " SH SI\(5      r| " SJ SK\(5      r} " SL SM\(5      r~ " SN SO\(5      r " SP SQ\(5      r " SR SS\(5      r " ST SU\(5      r " SV SW\(5      r " SX SY\(5      r " SZ S[\(5      r " S\ S]\(5      r " S^ S_\(5      r " S` Sa\(5      r " Sb Sc\(5      r " Sd Se\(5      r " Sf Sg\(5      r " Sh Si\(5      r " Sj Sk\(5      r " Sl Sm\(5      r " Sn So\(5      r " Sp Sq\(5      r " Sr Ss\(5      r " St Su\(5      r " Sv Sw\(5      r " Sx Sy\(5      r " Sz S{\(5      rS|\oS}\o4S~ jrS\S\S\\o   S}\\o   4S jrS\S\oS\S}\S-  4S jrS\S\S\S\oS\S}\4S jr\YGR;                  S\S9S 5       r\YGR;                  S65      \YGR;                  S\S9S 5       5       r\YGR;                  S\S9\YGR;                  S\S9S 5       5       r\YGR;                  S\S9S\4S j5       r\YGR;                  S\S9S\4S j5       r\YGR;                  S\S9S\4S j5       r\YGR;                  S\S9S\4S j5       r\YGR;                  S\S9\YGR;                  S\S9S 5       5       r\YGR;                  S\S9S\4S j5       r\YGR;                  S\S9S 5       r\YGR;                  S\S9S 5       r\YGR;                  S\S9S 5       r\YGR;                  S\S9S 5       r\YGR;                  S\xS9\XGRW                  S5      S)\4S j5       5       r\\GR;                  S\S9\XGRW                  S5      \" SSS9\" SSS9\" SSS9\" SSS9\" SSS9\" \v5      4S)\S\S\S\oS-  S\oS-  S\oS\34S jj5       5       r\\GR;                  S\{S9\XGRW                  S5      \" \v5      4S)\S\S\34S jj5       5       r\\GR;                  S\S9\XGRW                  S5      \" SSS9\" SSS9\" \v5      4S)\S\S\S\34S jj5       5       r\\GR;                  S\S9\XGRW                  S5      \" SSS9\" SSS9\" \v5      4S)\S\S\S\34S jj5       5       r\\GR;                  S\|S9\XGRW                  S5      \" \v5      4S)\S\S\34S jj5       5       r\\GR;                  S\}S9\XGRW                  S5      \" \v5      4S)\S\S\34S jj5       5       r\\GR;                  S5      \XGRW                  S5      \" \v5      4S)\S\S\34S jj5       5       r\\GR;                  S\S9\XGRW                  S5      \" SSS9\" SSS9\" SSS9\" SSS9\" SSS9\" \v5      4S)\S\oS-  S\oS-  S\S\S\oS-  S\34S jj5       5       r\\GRk                  S\S9\" SSS9\" \v5      \" \w5      4S)\S\oS\3S\o4S jj5       r\\GRk                  S\S9\" \v5      \" \w5      4S)\S\3S\o4S jj5       r\\GRk                  S\S9\" \v5      \" \w5      4S)\S\3S\o4S jj5       r\\GR;                  S\S9\XGRW                  S5      \" \w5      4S)\S\o4S jj5       5       r\\GRk                  S\SS9\XGRW                  S5      \" \w5      4S)\S\S\o4S jj5       5       r\\GRw                  S\S9\XGRW                  S5      \" \w5      4S)\S\oS\o4S jj5       5       r\\GRk                  S\SS9\" \v5      \" \w5      4S)\S\3S\o4S jj5       r\\GR;                  S\S9\" SSSS9\" SSSS9\" \v5      \" \w5      4S\S\S\3S\o4S jj5       r\\GR;                  S\S9\" \v5      \" \w5      4S\oS\oS\3S\o4S jj5       r\\GR;                  S\S9\XGRW                  S5      \" \v5      4S)\S\S\34S jj5       5       r\\GR;                  S5      \XGRW                  S5      \" SSS9\" \v5      4S)\S\oS-  S\34S jj5       5       r\\GR;                  S5      \XGRW                  S5      \" \v5      4S)\S\S\34S jj5       5       r\\GR;                  S\S9\XGRW                  S5      \" SSSS9\" \v5      4S)\GS \S\34GS jj5       5       r\\GR;                  GS5      \XGRW                  GS5      \" SGSS9\" GSGSGSS9\" \v5      4S)\GS\oS-  S\S\34GS	 jj5       5       r\\GR;                  GS
5      \XGRW                  GS5      \" SGSS9\" GSGSGSS9\" \v5      4S)\GS\oS-  S\S\34GS jj5       5       r\\GR;                  GS5      \XGRW                  GS5      \" SSS9\" SSS9\" SGSGSS9\" \v5      4S)\S\oS-  S\oS-  S\S\34
GS jj5       5       r\\GR;                  GS5      \XGRW                  GS5      \" SSS9\" SSS9\" SGSGSS9\" \v5      4S)\S\oS-  S\oS-  S\S\34
GS jj5       5       r\\GR;                  GS5      \XGRW                  GS5      \" \v5      4S)\S\S\34GS jj5       5       r\YGR                  GS5      GS\S\4GS j5       r\YGR;                  GS\ S9GS 5       r\YGR                  \\5        \TGS:X  a  SSKr\GR                  " \YGSGSGS9  ggs  sn f (  z.FastAPI application for TipSharks ratings API.    N)UTCdatedatetime	timedelta)BytesIOStringIO)Path)Any)
	APIRouterDependsFastAPIHTTPExceptionQueryRequestResponseSecurity	WebSocketWebSocketDisconnect)CORSMiddleware)HTMLResponsePlainTextResponseRedirectResponse)HTTPAuthorizationCredentials
HTTPBearer)StaticFiles)	BaseModel
ConfigDictField)Limiter_rate_limit_exceeded_handler)RateLimitExceeded)func)Session
joinedload)
get_loggersetup_logging)get_user_rate_limit_key)TipSharksScheduler)HRNZ_ALL_CLUB_CODESget_settings
parse_date)recompute_ratings)AuditLoggerget_session)IngestionService)AuditLogDriver
EntityTypeHorseMeetingRaceRatingSnapshotTrainer)RatingSnapshotRepository)key_funczTipSharks APIzVAdvanced harness racing ratings and predictions powered by multi-runner Elo algorithms0.2.0)titledescriptionversion/v1)prefix>   /admin/races/ratings/webhook
/analytics/predictions*,T)allow_originsallow_credentialsallow_methodsallow_headershttprequestc                 .  #    U R                   R                  n[         H`  nUR                  U5      (       d  M  SU 3nU R                   R                  (       a  U SU R                   R                   3n[        USS9s  $    U" U 5      I Sh  vN $  N7f)zRedirect legacy API paths to /v1/ equivalents.

Handles GET, POST, and other methods transparently via 301 redirect.
Preserves query string parameters.
Keeps /health, /ui/*, /static, /docs/site, and root at their original paths.
r@   ?i-  )urlstatus_codeN)rR   path_LEGACY_API_PREFIXES
startswithqueryr   )rO   	call_nextrT   rA   new_paths        :/root/tipsharks/tipsharks-elo-api/apps/backend/api/main.pylegacy_api_redirectsr[   l   s      ;;D&??6""TF|H{{  &Zq):):(;<#cBB ' 7####s   4BABBBc                 .  #    U" U 5      I Sh  vN nU R                   R                  R                  S5      (       aT  UR                  R	                  SS5        UR                  R	                  SS5        UR                  R	                  SS5        U$  N7f)	z.Avoid stale API responses in browsers/proxies.N)z/v1/ratingsz	/v1/racesrD   rC   Cache-Control.no-store, no-cache, must-revalidate, max-age=0Pragmano-cacheExpires0)rR   rT   rV   headers
setdefault)rO   rX   responses      rZ   add_no_cache_headersrf      s      w''H{{""#UVV##M	
 	##Hj9##Is3O (s   BBBBc                    #    SSK nU R                  R                  S[        UR	                  5       5      5      nX0R
                  l        U" U 5      I Sh  vN nX4R                  S'   U$  N7f)zAdd a unique request ID to every request and response.

Reads ``x-request-id`` from the incoming request headers if present,
otherwise generates a new UUID.  The ID is stored on ``request.state``
and echoed back in the ``x-request-id`` response header.
r   Nzx-request-id)uuidrc   getstruuid4state
request_id)rO   rX   rh   rm   re   s        rZ   add_request_idrn      sZ      $$^S5FGJ)MMw''H'1^$O (s   AA,A*A,c                 (  #    SSK J n  U" 5       nU" U 5      I Sh  vN nU" 5       U-
  n[        R                  SU R                  U R                  R
                  UR                  [        US-  S5      [        U R                  SS5      S.S	9  U$  Nt7f)
zLog basic information about every API request.

Records HTTP method, path, status code, duration, and the request ID
(if one was assigned by the ``add_request_id`` middleware).
r   )timeNzAPI request     rm   )methodrT   rS   duration_msrm   extra)
rp   loggerinfors   rR   rT   rS   roundgetattrrl   )rO   rX   rp   startre   durations         rZ   log_requestsr}      s      FEw''Hv~H
KKnnKK$$#// D!4!'--tD
  	 O (s   BBA5Bc           	      L  #    U R                   R                  nUR                  S5      (       d<  UR                  S5      (       d&  UR                  S5      (       d  U" U 5      I Sh  vN $ Sn [        5        nUR	                  [
        R                  " [        R                  5      5      R                  5       nUb"  UR                  c  UR                  [        S9nUnSSS5        UbN  SU R                  ;   a>   U R                  S   n[        R                   " US5      R                  [        S9nX7::  a   U" U 5      I Sh  vN nSn	 UR&                    Sh  vN n
X-  n	M   N! , (       d  f       N= f! [         a     Nf = f! ["        [$        4 a     Ncf = f N[ NE
 O! [         a    Us $ f = f[(        R*                  " U	5      R-                  5       nS	U S	3nXR                  S
'   Ub  UR/                  S5      UR                  S'   U R                  R1                  S5      nU(       a?  UR3                  S	5      U:X  a*  [5        SUUR                  R1                  SS5      SSSS.S9$ [5        U	UR6                  [9        UR                  5      UR:                  S9$ 7f)aT  Add ETag and Last-Modified headers to rating endpoints for conditional requests.

Computes an ETag from the response body (MD5 hash) and sets Last-Modified
from the most recent rating snapshot timestamp. Handles If-None-Match and
If-Modified-Since request headers to return 304 Not Modified when the
client's cached version is still valid.
z/v1/ratings/horsesz/v1/ratings/driversz/v1/ratings/trainersN)tzinfozif-modified-sincez%a, %d %b %Y %H:%M:%S GMT    "ETagLast-Modifiedzif-none-matchi0   r^   r`   rb   )r   r   r]   r_   ra   )rS   rc   )contentrS   rc   
media_type)rR   rT   rV   r0   rW   r"   maxr8   
created_atscalarr   replacer   	Exceptionrc   r   strptime
ValueError	TypeErrorbody_iteratorhashlibmd5	hexdigeststrftimeri   stripr   rS   dictr   )rO   rX   rT   last_modifiedsession	latest_tsims_strimsre   bodychunketag_hex
etag_valueif_none_matchs                 rZ   rating_etag_middlewarer      s     ;;D 	,--??011??122w''' &*M]gdhh~/H/H&IJQQSI$##+ ) 1 1 1 =I )   %8GOO%K
	oo&9:G##G-HIQQ R C #  w''H D#11 	%MDE (
 ]   I& 		 (
	1 
 {{4 **,HXJaJ  *V  ,9,B,B'-
)
 OO''8M,,S1X="!)!1!1!5!5or!J!Q$	
 		
 ((X%%&&&	 s   A$J$&E&'J$.
E9 8A'E(E9 'J$;=F	 8J$FJ$F% F#F!F#F% &J$(
E62E9 5J$6E9 9
FJ$FJ$	FJ$FJ$!F##F% $J$%F41J$3F44C0J$webz/staticstatic)	directory)namedocs)parentsexist_okz
/docs/site)r   htmlz	docs-sitestartupc                  0  #    [         R                  n U R                  (       d  [        R	                  S5        g[        R                  5         [        R                  5       nU(       a  [        R	                  SSU0S9  g[        R	                  S5        g7f)zStart the background scheduler on application startup.

Reads scheduler configuration from settings and loads default jobs.
Skipped if ``scheduler.enabled`` is False.
z"Scheduler is disabled via settingsNzDefault scheduler jobs loadedjob_idsru   z$No default scheduler jobs configured)settings	schedulerenabledrw   rx   r{   load_default_jobs)sched_settingsr   s     rZ   start_schedulerr   "  si      ''N!!89OO))+G3Iw;OP:;s   BBshutdownc                     #    [         R                  (       a)  [         R                  SS9  [        R	                  S5        gg7f)zEShutdown the background scheduler gracefully on application shutdown.T)waitz#Scheduler shut down on app shutdownN)r   runningr   rw   rx    r   rZ   stop_schedulerr   6  s3      %9: s   A Ac               #   \   #    [        5        n U v   SSS5        g! , (       d  f       g= f7f)z Dependency for database session.Nr/   )r   s    rZ   get_dbr   B  s     	' 
s   ,	,
),credentialsc                 ~    U R                   [        R                  R                  :w  a
  [	        SSS9eU R                   $ )z+Verify admin token for protected endpoints.i  zInvalid authentication tokenrS   detail)r   r   apiadmin_tokenr   )r   s    rZ   verify_admin_tokenr   I  s4    (,,":"::4RSS"""r   c                   F    \ rS rSr% Sr\\S'   \\S'   \" SSSS./0S	9rS
r	g)HealthResponseiQ  zHealth check response.statusr?   exampleshealthyr<   r   r?   json_schema_extrar   N)
__name__
__module____qualname____firstlineno____doc__rj   __annotations__r   model_config__static_attributes__r   r   rZ   r   r   Q  s.     KL$9
Lr   r   c                       \ rS rSr% Sr\\S'   \\S'   Sr\S-  \S'   \	\S'   Sr
\	S-  \S'   Sr\S-  \S	'   \\S
'   \" SSSSSSSSS./0S9rSrg)RatingResponsei_  z!Rating information for an entity.entity_type	entity_idNentity_nameratingrd
race_countas_of_race_idr   horse*   Some Delight     @33333SU@   90  r   r   r   r   r   r   r   r   r   )r   r   r   r   r   rj   r   intr   floatr   r   r   r   r   r   r   rZ   r   r   _  sv    +N"Kt"MB!Jd
!#*!##1$"$%*

Lr   r   c                   V    \ rS rSr% Sr\\S'   \\S'   Sr\S-  \S'   Sr	\
S-  \S'   Srg)	RatingHistoryItemiz  zSingle rating history point.race_idr   Nr   	race_dater   )r   r   r   r   r   r   r   r   r   r   rj   r   r   r   rZ   r   r   z  s+    &LMB IsTz r   r   c                       \ rS rSr% Sr\\S'   \\S'   Sr\	S-  \S'   Sr
\	S-  \S'   \\S'   / r\\   \S	'   \" S
SSSSSSSSSS.SSSSS./S./0S9rSrg)HorseDetailResponsei  z/Detailed horse information with rating history.horse_idr   Ncurrent_rating
current_rdr   rating_historyr   r   r   r   r   r   r   
2026-05-01)r   r   r   r   i40  g     @gfffffV@z
2026-04-28r   r   r   r   r   r   r   r   )r   r   r   r   r   r   r   rj   r   r   r   r   listr   r   r   r   r   r   rZ   r   r     s    9M
I#'NEDL'#J#O.0ND*+0 "*&,"&"$ (-&,"&)5	 (-&,"&)5	'
Lr   r   c                   t    \ rS rSr% Sr\\S'   \\S'   Sr\	S-  \S'   Sr
\	S-  \S'   \\S'   / r\\   \S	'   S
rg)DriverDetailResponsei  z0Detailed driver information with rating history.	driver_idr   Nr   r   r   r   r   r   r   r   r   r   r   r   rj   r   r   r   r   r   r   r   r   r   rZ   r   r     s@    :N
I#'NEDL'#J#O.0ND*+0r   r   c                   t    \ rS rSr% Sr\\S'   \\S'   Sr\	S-  \S'   Sr
\	S-  \S'   \\S'   / r\\   \S	'   S
rg)TrainerDetailResponsei  z1Detailed trainer information with rating history.
trainer_idr   Nr   r   r   r   r   r   r   r   rZ   r   r     s@    ;O
I#'NEDL'#J#O.0ND*+0r   r   c                   `    \ rS rSr% Sr\\S'   \\S'   \\S'   Sr\S-  \S'   Sr	\S-  \S'   S	r
g)
PaginationMetai  z4Pagination metadata with next/prev navigation links.totallimitoffsetNnextprevr   )r   r   r   r   r   r   r   r   rj   r   r   r   r   rZ   r   r     s0    >JJKD#*D#*r   r   c                   j    \ rS rSr% Sr\\   \S'   \\S'   \	" SSSSS	S
SSS./SSSSSS.S./0S9r
Srg)PaginatedRatingResponsei  z-Paginated response for rating list endpoints.datametar   r   r   r   r   r   r   r   r      d   r   z'/v1/ratings/horses?limit=100&offset=100N)r   r   r   r   r   r  r  r   r   )r   r   r   r   r   r   r   r   r   r   r   r   r   r   rZ   r   r     si    7
~

 ,3)++9&,"&*,-2
 "#!$"# I $
Lr   r   c                   4    \ rS rSr% Sr\\   \S'   \\S'   Sr	g)PaginatedRaceResponsei  z+Paginated response for race list endpoints.r  r  r   N)
r   r   r   r   r   r   r   r   r   r   r   r   rZ   r  r    s    5
t*
r   r  c                   J    \ rS rSr% Sr\" SSS9r\\S'   \" SSS9r	\\S'   S	r
g
)IngestionRequesti  zRequest to trigger ingestion..Start date (YYYY-MM-DD)r>   	date_fromEnd date (YYYY-MM-DD)date_tor   N)r   r   r   r   r   r   r  rj   r   r  r   r   r   rZ   r	  r	    s)    '3,EFIsF*ABGSBr   r	  c                   ^    \ rS rSr% Sr\\S'   \\S'   \\S'   \\S'   \" SSS	S
SS./0S9rSr	g)IngestionResponsei  zResponse from ingestion.meetingsracesstarterserrorsr      r      r   r  r  r  r  r   r   N
r   r   r   r   r   r   r   r   r   r   r   r   rZ   r  r    s<    "MJMKJ
Lr   r  c                   z    \ rS rSr% Sr\" SSS9r\\S'   \" SSS9r	\\S'   \" S	S
S9r
\\S'   \" S	SS9r\\S'   Srg)RecomputeRequesti  zRequest to trigger recompute..r
  r  r  r  r  FzClear existing ratings firstdefaultr>   clear_existing"Learn barrier/handicap adjustmentslearn_adjustmentsr   N)r   r   r   r   r   r   r  rj   r   r  r  boolr  r   r   r   rZ   r  r    sZ    '3,EFIsF*ABGSB #AND  $#Gt r   r  c                   :    \ rS rSr% Sr\\S'   \" SSS0/0S9rSr	g)	RecomputeResponsei  zResponse from recompute.snapshots_createdr   i  r   r   Nr  r   r   rZ   r"  r"    s*    "$d+
Lr   r"  c                       \ rS rSr% Sr\" SSS9r\\   S-  \	S'   \" SSS9r
\\   \-  S-  \	S'   \" S	S
S9r\\	S'   \" S	SS9r\\	S'   \" SSS9r\\	S'   \" SSS9r\\	S'   \" SSS9r\\	S'   Srg)ScrapeRequesti"  z&Request to trigger a scrape/ingestion.Nz<Optional HRNZ results URLs or URL paths (e.g., 010741rs.htm)r  urlsz6HRNZ club codes to generate URLs for (two digits each)
club_codes.r
  r  r  r  r  TzRecompute ratings after ingest	recomputeF'Clear existing ratings before recomputer  r  r  r   )r   r   r   r   r   r   r&  r   rj   r   r'  r  r  r(  r   r  r  r   r   r   rZ   r%  r%  "  s    0"RD$s)d
  */L*JS	C$&  3,EFIsF*ABGSBD6VWItW #LND  $#Gt r   r%  c                   t    \ rS rSr% Sr\\S'   \\S'   \\S'   \\S'   \\S'   \\S'   \\S	'   \\S
'   \\S'   Srg)ScrapeResponsei8  z'Response from scrape/ingestion webhook.r  r  r  horsesdriverstrainersr  
recomputedr#  r   N)	r   r   r   r   r   r   r   r   r   r   r   rZ   r+  r+  8  s4    1MJMKLMKr   r+  c                   L    \ rS rSr% Sr\\S'   \\S'   Sr\S-  \S'   \\S'   Srg)	SchedulerJobInfoiI  z"Information about a scheduled job.idr   Nnext_run_timetriggerr   )	r   r   r   r   r   rj   r   r3  r   r   r   rZ   r1  r1  I  s"    ,G
I $M3:$Lr   r1  c                   4    \ rS rSr% Sr\\   \S'   \\S'   Sr	g)SchedulerJobListResponseiR  z%Response with list of scheduled jobs.jobsr   r   N)
r   r   r   r   r   r   r1  r   r   r   r   r   rZ   r6  r6  R  s    /

  Jr   r6  c                   4   \ rS rSr% Sr\" SSS9r\\S'   \" SSS9r	\\S'   \" S	S
S9r
\S	-  \S'   \" SSS9r\\S'   \" SSS9r\\S'   \" SSS9r\\S'   \" S	SS9r\\   S	-  \S'   \" S	SS9r\\   S	-  \S'   \" S	SS9r\S	-  \S'   \" S	SS9r\S	-  \S'   Srg	)AddSchedulerJobRequestiY  zRequest to add a scheduled job..z&Job type: ingest, recompute, or scraper  job_typezCron expression for scheduling	cron_exprNz2Optional custom job ID (auto-generated if omitted)r  job_idHzRacing category (T, H, G)categorytabzData source (tab, ingest)sourceFr)  clearzHRNZ result URLs to scraper&  zHRNZ club codesr'  r
  r  r  r  r   )r   r   r   r   r   r   r:  rj   r   r;  r<  r>  r@  rA  r   r&  r   r'  r  r  r   r   r   rZ   r9  r9  Y  s    )#+STHcT3,LMIsM"VFC$J  #3NOHcO3NOFCO#LE4  #">D$s)d
  $)CT#UJS	D U!$<UVIsTzV:QRGS4ZRr   r9  c                   .    \ rS rSr% Sr\\S'   \\S'   Srg)AddSchedulerJobResponseir  z&Response after adding a scheduled job.r<  messager   N)r   r   r   r   r   rj   r   r   r   r   rZ   rC  rC  r  s    0KLr   rC  c                   8    \ rS rSr% Sr\\S'   \\S'   \\S'   Srg)RemoveSchedulerJobResponseiy  z(Response after removing a scheduled job.r<  removedrD  r   N)	r   r   r   r   r   rj   r   r   r   r   r   rZ   rF  rF  y  s    2KMLr   rF  c                   
   \ rS rSr% Sr\\S'   Sr\S-  \S'   Sr	\S-  \S'   Sr
\S-  \S'   Sr\S-  \S'   Sr\S-  \S	'   Sr\S-  \S
'   \\S'   \\S'   \\S'   \\S'   \\S'   Sr\S-  \S'   Sr\S-  \S'   Sr\S-  \S'   Srg)PredictionResponsei  z Prediction for a single starter.r   N
horse_namer   driver_namer   trainer_namebarriereffective_ratingwin_probabilityplace_probabilityplace_scorepredicted_placingci_lowerci_upperplacingr   )r   r   r   r   r   r   r   rJ  rj   r   rK  r   rL  rM  r   rS  rT  rU  r   r   r   rZ   rI  rI    s    *M!Jd
! IsTz "Kt"!Jd
!#L#*#GS4Z!Hedl!!Hedl!GS4Zr   rI  c                       \ rS rSr% Sr\\S'   Sr\S-  \S'   Sr\	S-  \S'   Sr
\S-  \S'   / r\\   \S'   \" S	S
SSSSSSSSSSSSSSSSSSS./S./0S9rSrg) RacePredictionResponsei  z'Predictions for all starters in a race.r   Nrace_numbervenue
distance_mpredictionsr   r      	Addingtoni  r   r   e   z	Ricky May   zMark Purdonr  r   gffffff?gףp=
?g?r  g @g33333c@r   rJ  r   rK  r   rL  rM  rN  rO  rP  rQ  rR  rS  rT  rU  r   rX  rY  rZ  r[  r   r   )r   r   r   r   r   r   r   rX  rY  rj   rZ  r[  r   rI  r   r   r   r   r   rZ   rW  rW    s    1L"Kt"E3:!Jd
!,.K().$#$("& )+*8),+6*-,9'(06/315+.12(.(.'+$
Lr   rW  c                   8    \ rS rSr% Sr\\S'   \\S'   \\S'   Srg)ConfidenceBucketi  z)Accuracy metrics for a confidence bucket.r  win_accuracy	avg_brierr   N)	r   r   r   r   r   r   r   r   r   r   r   rZ   rc  rc    s    3Jr   rc  c                   8    \ rS rSr% Sr\\S'   \\S'   \\S'   Srg)ConfidenceBucketsi  z'Accuracy metrics grouped by confidence.highmediumlowr   N)r   r   r   r   r   rc  r   r   r   r   rZ   rg  rg    s    1
	r   rg  c                   B    \ rS rSr% Sr\\S'   \\S'   \\S'   \\S'   Sr	g)	DailyTrendItemi  z!Single day in the accuracy trend.r   re  rd  r  r   N)
r   r   r   r   r   rj   r   r   r   r   r   r   rZ   rl  rl    s    +
IJr   rl  c                       \ rS rSr% Sr\\S'   Sr\S-  \S'   Sr\	S-  \S'   Sr
\	S-  \S'   \\S'   \\S	'   \\S
'   \\S'   Srg)RecentRaceAccuracyi  z-Accuracy metrics for a single evaluated race.r   NrX  rY  r   
field_sizewinner_correcttop3_overlapbrier_scorer   )r   r   r   r   r   r   r   rX  rY  rj   r   r   r   r   r   r   rZ   rn  rn    sK    7L"Kt"E3: IsTz Or   rn  c                       \ rS rSr% Sr\\S'   \\   \S'   \	\S'   \\
   \S'   \" SSS	S
SS.SSSSS./SSSS.SSSS.SSSS.S.SSSSSSSS S!./S"./0S#9rS$rg%)&AccuracySummaryi  z'Aggregated prediction accuracy summary.summarydaily_trendconfidence_bucketsrecent_racesr   gQ?gp=
ף?gzG?   overall_win_accuracytop3_accuracyavg_brier_scorenum_races_evaluatedr   gRQ?333333?
   r   re  rd  r     g?gQ?r  rd  re  <   皙?F   gQ?rh  ri  rj  r   r\  r]  Trr   333333?r   rX  rY  r   ro  rp  rq  rr  ru  rv  rw  rx  r   r   N)r   r   r   r   r   r   r   r   rl  rg  rn  r   r   r   r   r   rZ   rt  rt    s    1Mn%%)))** 15)-+//2	  %1)-,0%'	$ +-dQU V%',0)-#
 *,TPTU+ (-+,%0)5*,.2,-+/	%1$&(
*Lr   rt  c                       \ rS rSr% Sr\" SSS9r\\S'   \" SSS9r	\\S'   \" SS	S9r
\\S
'   \" SSS9r\\\4   S-  \S'   \" SSS9r\\\4   S-  \S'   \" SSS9r\S-  \S'   \" SSS9r\S-  \S'   Srg)AuditLogCreateRequesti  z%Request to create an audit log entry..z"Name of the table that was changedr  
table_namez'Primary key value of the changed record	record_idz2Type of change: INSERT, UPDATE, DELETE, or CORRECTactionNz$Snapshot of values before the changer  
old_valuesz#Snapshot of values after the change
new_valueszUser/system identifier
changed_byz$Human-readable reason for the changechange_reasonr   )r   r   r   r   r   r   r  rj   r   r  r  r  r   r
   r  r  r  r   r   r   rZ   r  r    s    /C-QRJR3,UVIsVMFC  )."H)JS#X%  )."G)JS#X%  #4=UVJd
V %"H!M3: r   r  c                       \ rS rSr% Sr\\S'   \\S'   \\S'   \\S'   Sr\	\\
4   S-  \S'   Sr\	\\
4   S-  \S	'   Sr\S-  \S
'   Sr\S-  \S'   Sr\S-  \S'   \" SS9rSrg)AuditLogEntryi/  z(Single audit log entry in API responses.r2  r  r  r  Nr  r  r  r  r   T)from_attributesr   )r   r   r   r   r   r   r   rj   r  r   r
   r  r  r  r   r   r   r   r   r   rZ   r  r  /  s{    2GONK(,JS#X%,(,JS#X%,!Jd
! $M3:$!Jd
!d3Lr   r  c                   4    \ rS rSr% Sr\\   \S'   \\S'   Sr	g)AuditLogListResponsei>  z)Paginated response for audit log listing.r  r   r   N)
r   r   r   r   r   r   r  r   r   r   r   r   rZ   r  r  >  s    3
}
Jr   r  codereturnc                     [        U 5      R                  5       nUR                  5       (       a&  [        U5      nUS:  d  US:  a  [	        S5      eUS $ [	        S5      e)z*Normalize club codes to two-digit strings.r   c   z#Club code must be between 00 and 9902dzClub code must be numeric)rj   r   isdigitr   r   )r  code_strvalues      rZ   _normalize_club_coder  H  sY    4y HH19
BCC
0
11r   
start_dateend_dater'  c                     U Vs/ s H  n[        U5      PM     nn/ nU nXa::  aC  UR                  S5      nU H  nUR                  U U S35        M     U[        SS9-  nXa::  a  MC  U$ s  snf )z:Generate HRNZ result URLs for a date range and club codes.z%m%dzrs.htmr  days)r  r   appendr   )r  r  r'  r  normalized_codesr&  currentdate_prefixs           rZ   _generate_hrnz_urlsr  S  s     @JJzt,T2zJDG

&&v.$DKK;-vV45 %9!$$	 
 K Ks   A*scrapedrR   default_yearc           	      T   U R                  S5      nU(       a  UR                  SS5      R                  5       n[        R                  " SU5      (       d=  S H7  n [
        R                  " X45      nUR                  US9R                  5       s  $    / SQnU H?  n [
        R                  " X45      nSU;  a  UR                  US9nUR                  5       s  $    U R                  S	5      nU(       a   [        R                  " U5      $ [        R                  " S
U5      nU(       a>   [        U[        UR                  S5      5      [        UR                  S5      5      5      $ g! [         a     GM  f = f! [         a     M  f = f! [         a     Nf = f! [         a     gf = f)z.Resolve meeting date from scraped data or URL.date_raw     z\\b\\d{4}\\b)	%A, %d %B%d %B)year)z%A, %d %B %Yr  z%d %B %Yr  z%Yr   z*(?P<mm>\\d{2})(?P<dd>\\d{2})\\d{2}rs\\.htmmmddN)ri   r   r   researchr   r   r   r   fromisoformatr   group)	r  rR   r  r  fmtparsedformatsmeeting_date_strmatchs	            rZ   _resolve_meeting_dater  d  s   {{:&H##FC0668yy(33-%..x=F!>>|><AACC .
 C!**89s?#^^^>F{{}$  {{6*	%%&677 IICSIE	c%++d*;&<c%++dBS>TUU ? "     		  		sH   2E':E94F
 )<F '
E65E69
FF

FF
F'&F'r   r   r   base_urlquery_paramsc                 J   U UUS.nX!-   U :  a<  0 UEXU-   S.EnSR                  S UR                  5        5       5      nU SU 3US'   OSUS'   US:  aH  [        SX!-
  5      n0 UEXS.En	SR                  S	 U	R                  5        5       5      nU SU 3US
'   U$ SUS
'   U$ )a`  Build pagination metadata with next/prev navigation links.

Args:
    total: Total number of results
    limit: Maximum results per page
    offset: Current offset
    base_url: Base URL without query string
    query_params: Current query parameters dict (limit/offset will be overridden)

Returns:
    Dict with total, limit, offset, next, prev keys
)r   r   r   )r   r   &c              3   >   #    U  H  u  pUc  M
  U SU 3v   M     g 7fN=r   .0kvs      rZ   	<genexpr>)_build_pagination_meta.<locals>.<genexpr>  $       
#641!Jqc1#J#6   	rQ   r   Nr   c              3   >   #    U  H  u  pUc  M
  U SU 3v   M     g 7fr  r   r  s      rZ   r  r    r  r  r   )joinitemsr   )
r   r   r   r  r  r  next_paramsquery_stringprev_offsetprev_paramss
             rZ   _build_pagination_metar    s    ( D ~PP%Pxx  
#.#4#4#6 
 
 #1\N3VV z!V^,MMMxx  
#.#4#4#6 
 
 #1\N3V K VKr   /)response_classc                     #    [        SS9$ 7f)zRedirect to web UI home page./ui/rR   r   r   r   rZ   rootr    s      ''   
z/docs/site/c                     #    [        SS9$ 7f)z!Redirect to the static docs site.z/static/docs/r  r  r   r   rZ   serve_docs_siter    s      00r  r  z/ui/index.htmlc                     #    [         S-  S-  n U R                  5       (       a  [        U R                  5       S9$ [	        SSS9e7f)zServe web UI home page.	templatesz
index.htmlr     Web UI not foundr   web_direxistsr   	read_textr   	html_paths    rZ   
serve_homer    sG      +%4II$7$7$9::
C0B
CC   AAz/ui/horse/{horse_id}r   c                    #    [         S-  S-  nUR                  5       (       a  [        UR                  5       S9$ [	        SSS9e7f)zServe horse detail page.r  z
horse.htmlr  r  r  r   r  )r   r  s     rZ   serve_horse_detailr    sG      +%4II$7$7$9::
C0B
CCr  z/ui/driver/{driver_id}r   c                    #    [         S-  S-  nUR                  5       (       a  [        UR                  5       S9$ [	        SSS9e7f)zServe driver detail page.r  zdriver.htmlr  r  r  r   r  )r   r  s     rZ   serve_driver_detailr    G      +%5II$7$7$9::
C0B
CCr  z/ui/trainer/{trainer_id}r   c                    #    [         S-  S-  nUR                  5       (       a  [        UR                  5       S9$ [	        SSS9e7f)zServe trainer detail page.r  ztrainer.htmlr  r  r  r   r  )r   r  s     rZ   serve_trainer_detailr    sG      +%6II$7$7$9::
C0B
CCr  z/ui/race/{race_id}r   c                    #    [         S-  S-  nUR                  5       (       a  [        UR                  5       S9$ [	        SSS9e7f)zServe race detail page.r  z	race.htmlr  r  r  r   r  r   r  s     rZ   serve_race_detailr    sG      +%3II$7$7$9::
C0B
CCr  z/ui/race-cardz/ui/race-card/c                     #    [         S-  S-  n U R                  5       (       a  [        U R                  5       S9$ [	        SSS9e7f)zServe race cards index page.r  race-card.htmlr  r  r  r   r  r  s    rZ   serve_race_cards_indexr    sH      +%(88II$7$7$9::
C0B
CCr  z/ui/race-card/{race_id}c                    #    [         S-  S-  nUR                  5       (       a  [        UR                  5       S9$ [	        SSS9e7f)z)Serve race card view for a specific race.r  r  r  r  r  r   r  r  s     rZ   serve_race_cardr    H      +%(88II$7$7$9::
C0B
CCr  z
/ui/searchc                     #    [         S-  S-  n U R                  5       (       a  [        U R                  5       S9$ [	        SSS9e7f)zServe search page.r  zsearch.htmlr  r  r  r   r  r  s    rZ   serve_searchr     r  r  z/ui/analyticsc                     #    [         S-  S-  n U R                  5       (       a  [        U R                  5       S9$ [	        SSS9e7f)zServe analytics page.r  zanalytics.htmlr  r  r  r   r  r  s    rZ   serve_analyticsr    r  r  z/ui/analytics-dashboardc                     #    [         S-  S-  n U R                  5       (       a  [        U R                  5       S9$ [	        SSS9e7f)zWServe analytics dashboard page with combined accuracy, ratings, and confidence metrics.r  zanalytics-dashboard.htmlr  r  r  r   r  r  s    rZ   serve_analytics_dashboardr  "  sH      +%(BBII$7$7$9::
C0B
CCr  z/ui/data-correctionc                     #    [         S-  S-  n U R                  5       (       a  [        U R                  5       S9$ [	        SSS9e7f)z$Serve data correction workflow page.r  zdata-correction.htmlr  r  r  r   r  r  s    rZ   serve_data_correctionr  +  sH      +%(>>II$7$7$9::
C0B
CCr  z/health)response_modelz
100/minutec                     [        SSS9$ )z?Health check endpoint.

Returns:
    Health status and version
r   r<   r   )r   )rO   s    rZ   health_checkr	  4  s     G<<r   z/ratings/horsesr    )r  le)r  gez
YYYY-MM-DDr  zFilter by venuejsonzResponse format: json or csv
as_of_daterY  formatdbc                    U(       a  [        U5      OSn[        R                  " U[        R                  SUS9n/ n	U GHi  n
UR                  [        5      R                  [        R                  U
R                  :H  5      R                  5       nU(       a  SSKJn  UR                  U5      R                  [        5      R                  [        5      R                  UR                   U
R                  :H  [        R"                  U:H  5      R                  5       nU(       d  M  U	R%                  ['        SU
R                  U(       a  UR(                  OSU
R*                  U
R,                  U
R.                  (       a  U
R.                  R1                  S5      OSU
R2                  S95        GMl     [5        U	5      nXX!-    nUR7                  5       S	:X  a  [9        5       n[:        R<                  " U/ S
QS9nUR?                  5         U HW  nURA                  UR                  URB                  UR*                  UR,                  URD                  UR2                  S
.5        MY     [G        URI                  5       SS9$ [K        U RL                  5      RO                  S5      U RP                  RR                  -   n[U        U RV                  5      n[Y        XUUU5      n[[        U[]        S0 UD6S9$ )aJ  Get top horse ratings.

Args:
    limit: Maximum results to return
    offset: Number of results to skip
    as_of_date: Optional date filter for historical ratings
    venue: Optional venue filter
    format: Response format (json or csv)
    db: Database session

Returns:
    Paginated list of horse ratings in JSON or raw CSV
N順 )r   r  r   Starterr   r   r   csv)r   r   r   r   r   r   
fieldnamestext/csv)r   r   r  r  r   )/r,   r:   get_top_ratingsr4   HORSErW   r5   filterr2  r   firstpackages.core.storage.modelsr  r  r7   r6   r   rY  r  r   r   r   r   r  ri   r   lenlowerr   r  
DictWriterwriteheaderwriterowr   r   r   getvaluerj   r  rstriprR   rT   r   r  r  r   r   )rO   r   r   r  rY  r  r  date_filter	snapshotsresultssnapshotr   r  recent_starterr   pageoutputwriterrr  r  r  s                         rZ   get_horse_ratingsr.  ?  sL   2 -7*Z(DK )88
JGI G&&uxx83E3E'EFLLN  = !dg$$(:(::MMU*   "#",,*/EJJT;;>Fmm8==,,\:QU&44
	
/ H LE FN+D ||~

 	AOO!"MMhh$$"#,,%&__	   1jII 7##$++C07;;3C3CCH,,-L!%,OD">3ID3IJJr   z/ratings/horses/{horse_id}c           	         UR                  [        5      R                  [        R                  U:H  5      R	                  5       nU(       d
  [        SSS9eSSKJn  UR                  U5      R                  [        UR                  [        R                  :H  5      R                  [        [        R                  [        R                  :H  5      R                  UR                  [        R                  :H  UR                   U:H  5      R#                  [        R$                  [        R&                  [        R(                  [        R                  5      R+                  5       n/ nU H;  nUR-                  [/        UR                  UR0                  UR2                  S95        M=     U(       a  US   R0                  OSnU(       a  US   R2                  OSn	[5        UUR6                  UU	[9        U5      US	9$ )
zGet detailed horse information with rating history.

Args:
    horse_id: Horse ID
    db: Database session

Returns:
    Horse details with rating history
r  zHorse not foundr   r   r8   r   r   r   Nr   )rW   r5   r  r2  r  r   r  r8   r  r7   r   r6   
meeting_idr   r4   r  r   order_bymeeting_daterace_datetimerX  allr  r   r   r   r   r   r  )
rO   r   r  r   r8   r&  historyr(  r   r   s
             rZ   get_horse_detailr9    sy   " HHUO""588x#78>>@E4EFF < 	 	dN00DGG;	<	gt'**4	5	&&**:*::$$0

 
  GG	

 
 $ G ..;;	
  .7Yr]))DN%.2!!DJZZ%y> r   z/ratings/driversc                    [         R                  " U[        R                  SS9n/ nU H  nUR	                  [
        5      R                  [
        R                  UR                  :H  5      R                  5       nUR                  [        SUR                  U(       a  UR                  OSUR                  UR                  UR                  (       a  UR                  R!                  S5      OSUR"                  S95        M     [%        U5      nXRX!-    n	['        U R(                  5      R+                  S5      U R,                  R.                  -   n
[1        U R2                  5      n[5        XX*U5      n[7        U	[9        S	0 UD6S9$ )
zGet top driver ratings.

Args:
    limit: Maximum results to return
    offset: Number of results to skip
    db: Database session

Returns:
    Paginated list of driver ratings
r  r   driverNr   r   r  r  r   )r:   r  r4   DRIVERrW   r3   r  r2  r   r  r  r   r   r   r   r  ri   r   r  rj   r  r$  rR   rT   r   r  r  r   r   )rO   r   r   r  r&  r'  r(  r<  r   r*  r  r  r  s                rZ   get_driver_ratingsr>    s4   $ )88
JWI G&!((h6H6H)HIOOQ$",,+1FKKt;;>Fmm8==,,\:QU&44
	
  LEFN+D7##$++C07;;3C3CCH,,-L!%,OD">3ID3IJJr   z/ratings/trainersc                    [         R                  " U[        R                  SS9n/ nU H  nUR	                  [
        5      R                  [
        R                  UR                  :H  5      R                  5       nUR                  [        SUR                  U(       a  UR                  OSUR                  UR                  UR                  (       a  UR                  R!                  S5      OSUR"                  S95        M     [%        U5      nXRX!-    n	['        U R(                  5      R+                  S5      U R,                  R.                  -   n
[1        U R2                  5      n[5        XX*U5      n[7        U	[9        S	0 UD6S9$ )
zGet top trainer ratings.

Args:
    limit: Maximum results to return
    offset: Number of results to skip
    db: Database session

Returns:
    Paginated list of trainer ratings
r  r;  trainerNr   r   r  r  r   )r:   r  r4   TRAINERrW   r9   r  r2  r   r  r  r   r   r   r   r  ri   r   r  rj   r  r$  rR   rT   r   r  r  r   r   )rO   r   r   r  r&  r'  r(  r@  r   r*  r  r  r  s                rZ   get_trainer_ratingsrB  !  s4   $ )88
JgI G((7#**7::9K9K+KLRRT%",,,3GLL;;>Fmm8==,,\:QU&44
	
  LEFN+D7##$++C07;;3C3CCH,,-L!%,OD">3ID3IJJr   z/ratings/drivers/{driver_id}c           	         UR                  [        5      R                  [        R                  U:H  5      R	                  5       nU(       d
  [        SSS9eSSKJn  UR                  U5      R                  [        UR                  [        R                  :H  5      R                  [        [        R                  [        R                  :H  5      R                  UR                  [        R                  :H  UR                   U:H  5      R#                  [        R$                  [        R&                  [        R(                  [        R                  5      R+                  5       n/ nU H;  nUR-                  [/        UR                  UR0                  UR2                  S95        M=     U(       a  US   R0                  OSnU(       a  US   R2                  OSn	[5        UUR6                  UU	[9        U5      US	9$ )
zGet detailed driver information with rating history.

Args:
    driver_id: Driver ID
    db: Database session

Returns:
    Driver details with rating history
r  zDriver not foundr   r   r0  r1  r2  N)r   r   r   r   r   r   )rW   r3   r  r2  r  r   r  r8   r  r7   r   r6   r3  r   r4   r=  r   r4  r5  r6  rX  r7  r  r   r   r   r   r   r  )
rO   r   r  r<  r8   r&  r8  r(  r   r   s
             rZ   get_driver_detailrD  Q  sz   " XXf$$VYY)%;<BBDF4FGG < 	 	dN00DGG;	<	gt'**4	5	&&**;*;;$$	1

 
  GG	

 
 $ G ..;;	
  .7Yr]))DN%.2!!DJ[[%y> r   z/ratings/trainers/{trainer_id}c           	         UR                  [        5      R                  [        R                  U:H  5      R	                  5       nU(       d
  [        SSS9eSSKJn  UR                  U5      R                  [        UR                  [        R                  :H  5      R                  [        [        R                  [        R                  :H  5      R                  UR                  [        R                  :H  UR                   U:H  5      R#                  [        R$                  [        R&                  [        R(                  [        R                  5      R+                  5       n/ nU H;  nUR-                  [/        UR                  UR0                  UR2                  S95        M=     U(       a  US   R0                  OSnU(       a  US   R2                  OSn	[5        UUR6                  UU	[9        U5      US	9$ )
zGet detailed trainer information with rating history.

Args:
    trainer_id: Trainer ID
    db: Database session

Returns:
    Trainer details with rating history
r  zTrainer not foundr   r   r0  r1  r2  N)r   r   r   r   r   r   )rW   r9   r  r2  r  r   r  r8   r  r7   r   r6   r3  r   r4   rA  r   r4  r5  r6  rX  r7  r  r   r   r   r   r   r  )
rO   r   r  r@  r8   r&  r8  r(  r   r   s
             rZ   get_trainer_detailrF    sz   " hhw&&wzzZ'?@FFHG4GHH < 	 	dN00DGG;	<	gt'**4	5	&&**<*<<$$
2

 
  GG	

 
 $ G ..;;	
  .7Yr]))DN%.2!!DJ \\%y> r   z/races/{race_id}c                    SSK Jn  UR                  [        5      R	                  [        [        R                  5      5      R                  [        R                  U:H  5      R                  5       nU(       d
  [        SSS9eUR                  U5      R                  UR                  U:H  5      R                  5       nUR                  UR                  UR                  UR                  UR                  (       a  UR                  R                  OSUR                   UR"                  UR$                  UR&                  (       a  UR&                  R)                  5       OS[+        U5      U Vs/ s HS  nUR,                  UR.                  UR0                  UR2                  UR4                  UR6                  UR8                  S.PMU     snS.$ s  snf )	zpGet race details with starters.

Args:
    race_id: Race ID
    db: Database session

Returns:
    Race details
r   r  r  Race not foundr   N)r   r   r   rM  
handicap_mrU  did_not_finish)r2  r   r3  rX  rY  rZ  
start_typegaitr6  starters_countr  )r  r  rW   r7   optionsr$   meetingr  r2  r  r   r   r7  r3  rX  rY  rZ  rK  rL  r6  	isoformatr  r   r   r   rM  rI  rU  rJ  )rO   r   r  r  racer  ss          rZ   get_racerS    sW     5 		DLL)	*	7"	#		 	 4DEExx ''7(BCGGIH gg77oo'''+||##oooo		;?;M;M++557SWh- 
  JJ[[ll99ll99"#"2"2 
 
s   +AG
rC   z
200/minuterq   r  r  c                    SSK Jn  U(       a	  U" U5      nO[        R                  " 5       [	        SS9-
  nU(       a	  U" U5      n	O[        R                  " 5       n	UR                  [        5      R                  [        R                  5      R                  [        R                  U:  [        R                  U	:*  5      n
U(       a"  U
R                  [        R                  U:H  5      n
U
R                  5       nU
R                  [        [        R                  5      5      R!                  [        R                  [        R"                  [        R$                  [        R&                  5      R)                  U5      R+                  U5      R-                  5       n/ nU H  nUR/                  UR&                  UR0                  UR                  (       a?  UR                  R                  (       a$  UR                  R                  R3                  5       OSUR                  (       a  UR                  R                  OSUR$                  UR4                  UR6                  UR"                  (       a  UR"                  R3                  5       OSS.5        M     [9        U R:                  5      R=                  S5      U R>                  R@                  -   n[C        U RD                  5      n[G        XXOU5      n[I        U[K        S	0 UD6S9$ )
zList races within a date range.r   r+      r  N)r   r3  r5  rY  rX  rZ  rK  r6  r  r  r   )&packages.core.common.utilsr,   r   todayr   rW   r7   r  rO  r  r6   r5  rY  countrN  r$   r4  r6  rX  r2  r   r   r7  r  r3  rP  rZ  rK  rj   r  r$  rR   rT   r   r  r  r  r   )rO   r  r  r   r   rY  r  r,   r  r  rW   r   r  	race_listrQ  r  r  r  s                     rZ   
list_racesrZ    s    6	*
ZZ\I2$66
g&::< 		dll		$$
2G4H4HH4T	U 

 W]]e34KKMEj./	  GG	

 
u		 
 I77"oo ||(A(A LL--779/3||++#//"oo"oo6:6H6HD&&002d	
 * 7##$++C07;;3C3CCH,,-L!%,OD in6Lt6LMMr   z/admin/ingestr?  zJData source: "tab" (TAB API directly) or "ingest" (tab-api-ingest service)r@  tokenc           	        #    [        U R                  5      n[        U R                  5      n[        R	                  SU SU SU S35        [        X!S9nUR                  XE5      I Sh  vN u  pxn	[        UUU	UR                  S   S9$  N 7f)	a  Trigger data ingestion (admin only).

Supports two data sources:
  - "tab" (default):  fetches data directly from the TAB Affiliates API
  - "ingest":         fetches data from the tab-api-ingest TypeScript service

Args:
    request: Ingestion request with date range
    source: Data source selector
    db: Database session
    token: Admin token

Returns:
    Ingestion statistics
zAdmin triggered ingestion:  to z	 (source=))r@  Nr  r  )	r,   r  r  rw   rx   r1   ingest_date_ranger  stats)
rO   r@  r  r[  r  r  servicer  r  r  s
             rZ   trigger_ingestionrb  W  s     2 G--.J'//*H
KK
%j\hZyPQR r1G&-&?&?
&U UHX}}X&	  !Vs   A'B)B
*!Bz/webhook/scrapec                 
  ^ ^"^##    SSK JnJnJnJnJnJn  SSKJn	  SSK	J
n
  [        T R                  5      m#[        T R                  5      m"T R                  nUc  [        R                   R                  n[#        U[$        5      (       a`  UR'                  5       S:X  a  [(        nOEUR+                  S5       Vs/ s H)  oR-                  5       (       d  M  UR-                  5       PM+     nn[#        U[.        5      (       a  [1        S U 5       5      (       a  [(        n/ nU(       a   UR3                  [5        T#T"U5      5        T R:                  (       a  UR3                  T R:                  5        U(       d
  [9        S	SS
9e[/        [<        R?                  U5      5      n[@        RC                  S[%        T#5      [%        T"5      T RD                  [G        U5      S.S9  SSSSSSSS.nU
" 5       nU	" 5        ISh  vN nU GH<  n URI                  U5      I Sh  vN n[K        UUT#RL                  5      nU(       a  URO                  S5      (       d  [@        RC                  SU5        Mj  T#Us=::  a  T"::  d  O  M{  URQ                  5       US'   URS                  U5      nURU                  U5      nURW                  UU5        US==   S-  ss'   US    H9  nURW                  UUS   US   URO                  S5      5        US==   S-  ss'   M;     US    H3  nURW                  UUS   URO                  S5      S9  US==   S-  ss'   M5     US    H3  nURW                  UUS   URO                  S5      S9  US==   S-  ss'   M5     URY                  UUS   5      n0 nU H8  nURW                  UUS   U5      nURZ                  UUS   '   US==   S-  ss'   M:     UR]                  UU5      nU H6  nURW                  UUS   UURO                  S 5      5        US!==   S-  ss'   M8     UR_                  5         GM?     SSS5      ISh  vN   Sn T RD                  (       a&  U"U U#4S& jn![f        Rh                  " U!5      I Sh  vN n [k        US   US   US!   US   US   US   US"   T RD                  U S'9	$ s  snf ! [6         a  n[9        S	[%        U5      S
9UeSnAff = f GN GN! [`         a?  nURc                  5         US"==   S-  ss'   [@        Re                  S#UUS$S%9   SnAGM/  SnAff = f N! , ISh  vN  (       d  f       GN = f N7f)(z:Webhook to trigger HRNZ Playwright scrape with parameters.r   )DriverRepositoryHorseRepositoryMeetingRepositoryRaceRepositoryStarterRepositoryTrainerRepository)HRNZScraper)HRNZDataMapperNr7  rI   c              3   v   #    U  H/  n[        U[        5      =(       a    UR                  5       S :H  v   M1     g7f)r7  N)
isinstancerj   r  )r  r  s     rZ   r  !webhook_scrape.<locals>.<genexpr>  s*     V:4z$$>)>>:s   79  r   zRProvide club_codes (or HRNZ_CLUB_CODES) to generate URLs, or pass urls explicitly.zWebhook triggered scrape)r  r  r(  r&  ru   )r  r  r  r,  r-  r.  r  r  z Skipping non-meeting page for %sr   r  r  r,  r2  r   raw_jsonr-  )r   r.  )r   rO  rX  r   rU  r  r  zFailed to scrape %s: %sTexc_infoc            	         > [        5        n [        U TTTR                  TR                  S9sS S S 5        $ ! , (       d  f       g = f)Nr  r  )r0   r-   r  r  )r   r  rO   r  s    rZ   _run_recompute&webhook_scrape.<locals>._run_recompute  s7    '(#*#9#9&-&?&? s	   !7
A)	r  r  r  r,  r-  r.  r  r/  r#  )6"packages.core.storage.repositoriesrd  re  rf  rg  rh  ri  packages.hrnz_scraperrj  packages.hrnz_scraper.mapperrk  r,   r  r  r'  r   hrnzrm  rj   r  r)   splitr   r   anyextendr  r   r   r&  r   fromkeysrw   rx   r(  r  get_meeting_resultsr  r  ri   rP  map_meetingmap_entitiesupsert	map_racesr2  map_starterscommitr   rollbackerrorasyncio	to_threadr+  )$rO   r  r[  rd  re  rf  rg  rh  ri  rj  rk  r'  r  r&  excr`  mapperscraperrR   r  r5  rO  entitiesr   r<  r@  r  race_id_maprQ  race_objr  starterr#  ru  r  r  s$   `                                 @@rZ   webhook_scraper    s      2;G--.J'//*H##J]]--
*c""&,J *4)9)9#)>)>**,

)>   *d##V:VVV,JD	KKK+J*MN ||GLL!g
 	

 d#$D
KK"j/H **I	
   E F}}C<Q ' ; ;C @@4Wc:??S#7;;w+?+?KK BCH"l>h>"."8"8": ,,W5!..w7!((W5j!Q&!%h/E#**df		*-	 (Oq(O 0 'y1F$++F6Nfjj6F ,  )$)$	 2  (
3G%,,GFOD8I -  *%*%	  4 ((')2DE !D-44R9KTRH7?{{K] 34'Na'N "
 "..wD'G%,,	*I.	 *%*%  ( 		s  }@ 	 #*"3"3N"CCz"Gnz"Xi z"X$$+
 
}  	KCCAsJ	KF  Ap  Qh1$6S4PPQw }}}Z Ds   B3U8R8R8(=U&R= B5U7S#8U;T7S)S&AS)&T7(S)7T79GS):T7?U
T58UU9U=
S SS  U&S))
T233T-&T7-T22T75U7U=U >U
	Uz/admin/recomputec                     [        U R                  5      n[        U R                  5      n[        R	                  SU SU 35        [        UUUU R                  U R                  S9n[        US9$ )zTrigger rating recompute (admin only).

Args:
    request: Recompute request with date range
    db: Database session
    token: Admin token

Returns:
    Recompute statistics
zAdmin triggered recompute: r]  rt  )r#  )	r,   r  r  rw   rx   r-   r  r  r"  )rO   r  r[  r  r  snapshot_counts         rZ   trigger_recomputer  +  sn      G--.J'//*H
KK-j\hZHI&
--!33N ~>>r   z/admin/jobsz	20/minutec           
          [         R                  S5        [        R                  5       n[	        U Vs/ s H  n[        S0 UD6PM     sn[        U5      S9$ s  snf )zList all scheduled background jobs (admin only).

Returns a list of all cron jobs currently registered in the scheduler.

Args:
    token: Admin authentication token

Returns:
    List of scheduled jobs with their triggers and next run times
zAdmin listing scheduled jobs)r7  r   r   )rw   rx   r   	list_jobsr6  r1  r  )rO   r[  r7  jobs       rZ   list_scheduled_jobsr  N  sQ    & KK./ D#156#%%6$i 6s   Ar_  )r  rS   job_requestc           	      v   UR                   =(       d8    UR                   S[        R                  " [        5      R                  S5       3n[        R                  SUR                  UR                  US.S9  UR                  S:X  aK  [        R                  UR                  UR                  UR                  UR                  UR                  US9  OUR                  S:X  a@  [        R                  UR                  UR                  UR                   UR                  US	9  OsUR                  S
:X  aK  [        R#                  UR$                  UR&                  UR                  UR                  UR                  US9  O[)        SSUR                   S3S9e[+        USUR                   SUR                   S3S9$ )a  Add a new scheduled background job (admin only).

Supports three job types:
  - ``ingest``:     Scheduled data ingestion from TAB API
  - ``recompute``:  Scheduled rating recomputation
  - ``scrape``:     Scheduled HRNZ data scraping

Args:
    job_request: Job configuration including cron expression and type-specific params
    token: Admin authentication token

Returns:
    The job ID and confirmation message
_z%Y%m%d_%H%M%SzAdmin adding scheduled job)r:  r;  r<  ru   ingest)r  r  r>  r@  r;  r<  r(  )r  r  rA  r;  r<  scrape)r&  r'  r  r  r;  r<  ro  zUnknown job_type 'z.'. Must be 'ingest', 'recompute', or 'scrape'.r   z
Scheduled z job with cron '')r<  rD  )r<  r:  r   nowr   r   rw   rx   r;  r   add_ingest_jobr  r  r>  r@  add_recompute_jobrA  add_scrape_jobr&  r'  r   rC  )rO   r  r[  r<  s       rZ   add_scheduled_jobr  i  s   4 	 	S""#1X\\#%6%?%?%P$QR 
 KK$#,,$..
   x'  !++'' ))%%!++ 	! 	
 
			,##!++''##!++ 	$ 	
 
			)  !!"--!++''!++ 	! 	
 '(<(<'==kl
 	

 #[1122B;CXCXBYYZ[ r   z/admin/jobs/{job_id}r<  c                     [         R                  SSU0S9  [        R                  U5      nU(       a  [	        USSU S3S9$ [        SSU S	3S
9e)zRemove a scheduled background job (admin only).

Args:
    job_id: The job identifier to remove
    token: Admin authentication token

Returns:
    Confirmation of removal
zAdmin removing scheduled jobr<  ru   TzJob 'z' removed successfully)r<  rG  rD  r  z' not foundr   )rw   rx   r   
remove_jobrF  r   )rO   r<  r[  rG  s       rZ   remove_scheduled_jobr    sk    & KK.x6HKI""6*G)F8#9:
 	
 vhk* r   z/admin/audit-logc                 r   U R                   R                  5       R                  5       nUS;  a  [        SSU R                    S3S9e[        R
                  " UU R                  U R                  UU R                  U R                  U R                  U R                  S9nUc
  [        SS	S9e[        UR                  UR                  UR                  UR                   UR                  UR                  UR                  UR                  UR                  (       a  UR                  R                  5       S
9	$ SS
9	$ )a_  Create an audit log entry (admin only).

Records a data change or correction event in the audit log.
This endpoint allows programmatic creation of audit entries,
e.g. from the data correction UI or automated scripts.

Args:
    request: Audit log entry details
    db: Database session
    token: Admin token

Returns:
    The created audit log entry
)INSERTUPDATEDELETECORRECTro  zInvalid action 'z.'. Must be INSERT, UPDATE, DELETE, or CORRECT.r   )r   r  r  r  r  r  r  r  Nr
  z Failed to create audit log entry	r2  r  r  r  r  r  r  r  r   )r  upperr   r   r.   
log_changer  r  r  r  r  r  r  r2  r   rP  )rO   r  r[  action_upperentrys        rZ   create_audit_logr    s)   ( >>'')//1LDD%gnn%55cd
 	

 ""%%##%%%%%%++	E }4VWW 88##//||######))383C3C5##--/
 
 JN
 
r   zMaximum entries to return)r  r  r>   zNumber of entries to skip)r  r  r>   c                    [         R                  " X U-   S9nXAX-    n[        U5      nX`U-   :  a#  UR                  [        5      R                  5       n[        U Vs/ s H  n[        UR                  UR                  UR                  UR                  UR                  UR                  UR                  UR                  UR                   (       a  UR                   R#                  5       OSS9	PM     snUS9$ s  snf )zList recent audit log entries (admin only).

Args:
    limit: Maximum entries to return
    offset: Number of entries to skip
    db: Database session
    token: Admin token

Returns:
    Paginated list of recent audit log entries
r;  Nr  r  r   )r.   get_recent_changesr  rW   r2   rX  r  r  r2  r  r  r  r  r  r  r  r   rP  )r   r   r  r[  entriesr*  r   es           rZ   list_audit_logsr    s    $ ,,Rv~FGFN+DLEv~"((* 
  44<<++xx<<<<<<oo78||1<<113
 
  
s   BC9z)/admin/audit-log/{table_name}/{record_id}r  r  c                    [         R                  " X U5      n[        U Vs/ s H  n[        UR                  UR
                  UR                  UR                  UR                  UR                  UR                  UR                  UR                  (       a  UR                  R                  5       OSS9	PM     sn[        U5      S9$ s  snf )a  Get all audit log entries for a specific record (admin only).

Args:
    table_name: Name of the table
    record_id: Primary key value of the record
    db: Database session
    token: Admin token

Returns:
    List of audit log entries for the specified record
Nr  r  )r.   get_changes_for_recordr  r  r2  r  r  r  r  r  r  r  r   rP  r  )r  r  r  r[  r  r  s         rZ   get_audit_log_for_recordr  ?  s    ( 00KG 
  44<<++xx<<<<<<oo78||1<<113
 
 'l 
s   BCz/races/{race_id}/predictionsz	50/minutec                 ,   SSK Jn  SSKJn  UR	                  [
        5      R                  [
        R                  U:H  5      R                  5       nU(       d
  [        SSS9eUR	                  U5      R                  UR                  U:H  5      R                  5       nU(       d
  [        SSS9eU Vs0 s H  owR                  U_M     nnUR                  (       dl  SSKJn	  UR	                  [
        5      R                  U	" [
        R                  5      5      R                  [
        R                  U:H  5      R                  5       nU" U5      n
U
R!                  XV5      nUR"                   Vs/ s H  n[%        UR&                  UR(                  UR*                  UR,                  UR.                  UR0                  UR2                  UR4                  UR6                  UR8                  UR:                  UR<                  UR>                  UR@                  URB                  U;   a  XRB                     RD                  OS	S
9PM     nn[G        UURH                  UR                  (       a  UR                  RJ                  OS	URL                  US9$ s  snf s  snf )zGet win probability predictions for a race.

Args:
    race_id: Race ID
    db: Database session

Returns:
    Predictions for all starters
r   PredictionEnginer  r  rH  r   No starters found for race)r$   Nr`  ra  )'!packages.core.ratings.predictionsr  r  r  rW   r7   r  r2  r  r   r   r7  rO  sqlalchemy.ormr$   rN  predict_racer[  rI  r   rJ  r   rK  r   rL  rM  rN  rO  rP  rQ  rR  confidence_interval_lowconfidence_interval_high
starter_idrU  rW  rX  rY  rZ  )rO   r   r  r  r  rQ  r  r  starter_by_idr$   engine
predictionpredr[  s                 rZ   get_race_predictionsr  h  s     C4 88D>  G!34::<D4DEE xx ''7(BCGGIH4PQQ8@AWZZ(MA <<- HHTNWZ-.VDGGw&'UW	 	 b!F$$T4J0 **-, +D+ 	]]nn((**LL!22 00"44(("441122 ??m3 oo.66%	
* +-  2 "$$$(LLdll  d?? S B s   1J-CJz/races/upcomingzYYYY-MM-DD, defaults to todayr   c                    SSK Jn  U(       a  U" U5      O[        R                  " 5       nUR	                  [
        5      R                  [        [
        R                  5      5      R                  [
        R                  5      R                  [        R                  U:H  5      R                  [        R                  [
        R                  5      R!                  5       n/ nU H}  nUR#                  UR$                  UR                  UR                  (       a  UR                  R                  OSUR&                  UR(                  [+        UR,                  5      S.5        M     UR/                  5       [+        U5      US.$ )zGet upcoming races for a specific date.

Args:
    race_date: Date to get races for (defaults to today)
    db: Database session

Returns:
    List of races for the specified date
r   r+   N)r   rX  rY  rZ  rK  starter_count)r   r   r  )rV  r,   r   rW  rW   r7   rN  r$   rO  r  r  r6   r5  r4  rY  rX  r7  r  r2  rZ  rK  r  r  rP  )rO   r   r  r,   target_dater  rY  rQ  s           rZ   get_upcoming_racesr    s   $ 6+4*Y'$**,K 		DLL)	*	dll		$$3	4	'--!1!1	2	 
 I77#///3||++"oo"oo!$T]]!3		
  %%')n r   z/predictions/compare/{race_id}c                 f    SSK Jn  U" U5      nUR                  U5      nU(       d
  [        SSS9eU$ )zCompare prediction to actual result for a completed race.

Args:
    race_id: Race ID
    db: Database session

Returns:
    Prediction accuracy comparison
r   r  r  zRace not found or not completedr   )r  r  compare_prediction_to_actualr   )rO   r   r  r  r  
comparisons         rZ   r  r    s9      Cb!F44W=J4UVVr   z/analytics/accuracyrU  r  im  )r  r  r  r  c                    SSK Jn  SSKJn  [        R
                  " 5       [        US9-
  n[        R
                  " 5       nUR                  [        5      R                  U[        R                  UR                  :H  5      R                  UR                  R                  S5      5      R                  [        5      R                  [        R                   U:  [        R                   U:*  5      R#                  [        R                   R%                  5       [        R&                  R%                  5       R)                  5       5      R+                  5       R-                  S5      R/                  5       nU" U5      nSn	Sn
SnSn0 nSSSS.SSSS.SSSS.S	.n/ nU GH  nUR1                  UR                  5      nU(       d  M(  US
   nUS   nUS   nUR3                  S/ 5      nSnU(       a  [5        US S9nUS   nUS:  a  SnOUS:  a  SnOSnUU   S==   S-  ss'   U(       a  UU   S
==   S-  ss'   UU   S==   U-  ss'   UR3                  S5      nU(       dP  UR6                  (       a?  UR6                  R                   (       a$  UR6                  R                   R9                  5       nU(       d  SnUU;  a	  SSSS.UU'   UU   S==   U-  ss'   U(       a  UU   S
==   S-  ss'   UU   S==   S-  ss'   U	U(       a  SOS-  n	U
U-  n
UU-  nUS-  nUR;                  US   UR3                  S5      UR3                  S5      UR3                  S5      UR3                  SS5      UUUS .5        GM     US:X  a5  [=        SSSSS!./ [?        [A        SSSS"9[A        SSSS"9[A        SSSS"9S	9/ S#9$ / n[C        URE                  5       5       H8  nUU   nUR;                  [G        UUS   US   -  US
   US   -  US   S$95        M:     S%[H        S&[@        4S' jn[=        X-  X-  S(-  X-  US!.U[?        U" US   5      U" US   5      U" US   5      S	9USS)  Vs/ s H  n[K        S*0 UD6PM     snS#9$ s  snf )+aE  Get aggregated prediction accuracy summary.

Computes accuracy metrics for completed races in the specified date range.
Limited to the most recent 200 evaluated races for performance.

Args:
    days: Number of days to look back
    db: Database session

Returns:
    Accuracy summary with daily trend and confidence buckets
r   r  r  r  N           )r  rp  	brier_sumr  rp  rq  rr  r[  c                     U S   $ )NrO  r   )ps    rZ   <lambda>&get_accuracy_summary.<locals>.<lambda>F	  s
    a@Q>Rr   )keyrO  g?rh  r  ri  rj  r  r  r  r   unknown)r  rp  r  r   rX  rY  ro  r  rz  r  r  r  bucketr  c                 j    U S   S:X  a  [        SSSS9$ [        U S   U S   U S   -  U S   U S   -  S9$ )Nr  r   r  r  rp  r  )rc  )r  s    rZ   _bucket_stats+get_accuracy_summary.<locals>._bucket_stats	  sT    '?a#!#MM/ 01F7OC[)F7O;
 	
r   g      @2   r   )&r  r  r  r  r   rW  r   rW   r7   r  r2  r   r  rU  isnotr6   r5  r4  descr6  
nulls_lastdistinctr   r7  r  ri   r   rO  rP  r  rt  rg  rc  sortedkeysrl  r   rn  )rO   r  r  r  r  r  r  r  r  total_winner_correcttotal_top3_overlaptotal_brierr   
daily_databucketsrx  rQ  r  rp  rq  brierr[  predicted_winner_probpredicted_winner
bucket_keyrace_date_strrv  dr  r  r-  s                                  rZ   get_accuracy_summaryr  	  s   & C4	t 44Jzz|H 		gtww'//1	2	%%d+	,	g	$$
2G4H4HH4T	U	'&&++-t/A/A/F/F/H/S/S/U	V		s	 
 b!FKJ"$JqsCEacBG
  "L88A
#$45!.1=) !nn]B7 #";4RS$45F$G! D(J"d*!JJ
G$)$J 01Q61
K(E1("{3$,,2K2K LL55??AM%M
* "#)J}%
 	=!+.%7.}%&671<7=!'*a/*^:l*ua
%i0)~~m<#0'^^K8(nn\1="0 ,$		
e ~ Q(+!$#&'(	 0%AC3O'acSQ$13#N 
 
 	
  KJOO%&!}{+d7m;!"23d7mC7m		
 '
d 
'7 
 $8$E/<sB*7#-	
  ,wv/ !23gen-

 8DCR7HI7H!(-1-7HI  Js   Q5z/export/ratings.csvz	10/minutez<Filter by entity type: horse, driver, trainer (default: all)i  iP  zMaximum rowsr   c                 v   U(       a  [        UR                  5       5      /O.[         R                  [         R                  [         R                  /n[        5       n[        R                  " U/ SQS9nUR                  5         SnU GH  n[        R                  " X8US9n	U	 GH  n
Xr:  a    GOSnU[         R                  :X  ac  UR                  [        5      R                  [        R                  U
R                  :H  5      R!                  5       nU(       a  UR"                  OSnOU[         R                  :X  ac  UR                  [$        5      R                  [$        R                  U
R                  :H  5      R!                  5       nU(       a  UR"                  OSnOvU[         R                  :X  ab  UR                  [&        5      R                  [&        R                  U
R                  :H  5      R!                  5       nU(       a  UR"                  OSnUR)                  UR*                  U
R                  UU
R,                  U
R.                  U
R0                  (       a  U
R0                  R3                  S5      OSU
R4                  S.5        US-  nGM     Xr:  d  GM    O   [7        UR9                  5       SS	S
0S9$ )zExport all ratings as CSV.

Returns a CSV file with all rating snapshots across entity types.
Supports filtering by entity_type and limit.
r   r  r   r;  Nr   r  r  Content-Dispositionz"attachment; filename="ratings.csv"r   r   rc   )r4   r  r  r=  rA  r   r  r   r!  r:   r  rW   r5   r  r2  r   r  r   r3   r9   r"  r  r   r   r  ri   r   r   r#  )rO   r   r   r  entity_typesr+  r,  rows_writtenetr&  r(  r   entitys                rZ   export_ratings_csvr  	  s,   (  {((*+	
 
 1 1:3E3EF  ZF^^
F L,<<R5Q	!H$ KZ%%%%//H<N<N0NOUUW-3fkkz(((HHV$++FII9K9K,KLRRT  .4fkkz)))HHW%,,WZZ8;M;M-MNTTV  .4fkkOO#%88!)!3!3#.&oo"++;C==)),7d%-%;%;
 ALC "F  M P !&(LM r   z/export/ratings.parquetc                 J    SSK n SSKnU(       a  [	        UR                  5       5      /O.[        R                  [        R                  [        R                  /n/ nU GH$  n[        R                  " X8US9n	U	 GH  n
[        U5      U:  a    GOSnU[        R                  :X  ac  UR                  [        5      R                  [        R                  U
R                   :H  5      R#                  5       nU(       a  UR$                  OSnOU[        R                  :X  ac  UR                  [&        5      R                  [&        R                  U
R                   :H  5      R#                  5       nU(       a  UR$                  OSnOvU[        R                  :X  ab  UR                  [(        5      R                  [(        R                  U
R                   :H  5      R#                  5       nU(       a  UR$                  OSnUR+                  UR,                  U
R                   UU
R.                  U
R0                  U
R2                  (       a  U
R2                  R5                  S5      OSU
R6                  S	.5        GM     [        U5      U:  d  GM%    O   U(       d  [9        S
SSS0S9$ UR;                  U5      n[=        5       nUR?                  USS9  URA                  S5        [9        URC                  5       SSS0S9$ ! [         a    [        SSS9Sef = f! [         a    [        SSS9Sef = f)zExport all ratings as Parquet.

Returns a Parquet file with all rating snapshots across entity types.
Requires pyarrow or fastparquet to be installed.
r   N  @Parquet export requires pandas. Install with: pip install pandasr   BParquet export requires pyarrow. Install with: pip install pyarrowr;  r   r   r   application/octet-streamr  z&attachment; filename="ratings.parquet"r  Findex)"pandasImportErrorr   pyarrowr4   r  r  r=  rA  r:   r  r  rW   r5   r  r2  r   r  r   r3   r9   r  r  r   r   r  ri   r   r   	DataFramer   
to_parquetseekr#  )rO   r   r   r  pdr  r  rowsr  r&  r(  r   r  dfbuffers                  rZ   export_ratings_parquetr  	
  s      {((*+	
 
 1 1:3E3EF  D,<<R5Q	!H4yE!KZ%%%%//H<N<N0NOUUW-3fkkz(((HHV$++FII9K9K,KLRRT  .4fkkz)))HHW%,,WZZ8;M;M-MNTTV  .4fkkKK#%88!)!3!3#.&oo"++;C==)),7d%-%;%;
' "B t9I L 1*,TU
 	
 
d	BYFMM&M&
KKN!-&(PQ W  U
 	  W

 	s   K3 L 3L	L"z/export/predictions.csvi'  c                    SSK Jn  SSKJn  U(       a  [	        U5      nO[
        R                  " 5       [        SS9-
  nU(       a  [	        U5      nO[
        R                  " 5       nUR                  [        5      R                  [        R                  5      R                  [        R                  U:  [        R                  U:*  5      R                  [        R                  R!                  5       [        R"                  R!                  5       R%                  5       5      R'                  U5      R)                  5       n	U" U5      n
[+        5       n[,        R.                  " U5      nUR1                  / SQ5        SnU	 GHF  nX:  a    GO?UR                  U5      R                  UR2                  UR4                  :H  5      R)                  5       nU(       d  MZ   U
R7                  X5      nUR:                   GH  nX:  a    M  UR1                  UR4                  UR<                  =(       d    SUR                  (       a  UR                  R>                  OSUR@                  =(       d    SUR                  (       a?  UR                  R                  (       a$  UR                  R                  RC                  5       OSURD                  URF                  URH                  =(       d    SURJ                  =(       d    SURL                  =(       d    SURN                  =(       d    SURP                  =(       d    SURR                  =(       d    SURT                  =(       d    SURV                  S URX                  S	 URZ                  S	 UR\                  S UR^                  UR`                  S URb                  S /5        US
-  nGM     GMI     [e        URg                  5       SSS0S9$ ! [8         a     GMt  f = f)zExport recent predictions as CSV.

Returns a CSV file with predictions for completed races.
Supports filtering by date range and limit.
r   r  r  r\  r  )r   rX  rY  rZ  r   r  r   rJ  r   rK  r   rL  rM  rI  rN  rO  rP  rQ  rR  rS  rT  r   .1fz.4fr  r  r  z&attachment; filename="predictions.csv"r  )4r  r  r  r  r,   r   rW  r   rW   r7   r  rO  r  r6   r5  r4  r  r6  r  r   r7  r   r  r,  r"  r   r2  r  r   r[  rX  rY  rZ  rP  r  r   rJ  r   rK  r   rL  rM  rI  rN  rO  rP  rQ  rR  r  r  r   r#  )rO   r  r  r   r  r  r  r  r  races_with_resultsr  r+  r,  r  rQ  r  r  r  s                     rZ   export_predictions_csvr  m
  s    C4 	*
ZZ\I1$55
g&::< 		dll		  J.  H,

 
'&&++-t/A/A/F/F/H/S/S/U	V	u	  b!FZFZZF
OO	
4 L" 88G$++GOOtww,FGKKM	,,T<J **D$OOGG$$**.,,DLL&&BOO)r  <<DLL,E,E 11;;=OOMMOO)rNN(b$$*OO)r%%+LL&BOO)r,,S1++C0--c2'',**33C844S93: ALC + #` !&(PQ M  		s   O##
O21O2z/export/predictions.parquetc           
      J    SSK n SSKnSSKJn  SSKJn  U(       a  [        U5      n	O[        R                  " 5       [        S	S
9-
  n	U(       a  [        U5      n
O[        R                  " 5       n
UR                  [        5      R                  [        R                  5      R!                  ["        R$                  U	:  ["        R$                  U
:*  5      R'                  ["        R$                  R)                  5       [        R*                  R)                  5       R-                  5       5      R/                  U5      R1                  5       nU" U5      n/ nU GH  n[3        U5      U:  a    GOUR                  U5      R!                  UR4                  UR6                  :H  5      R1                  5       nU(       d  Md   UR9                  X5      nUR<                   GH  n[3        U5      U:  a    M  UR?                  0 SUR6                  _SUR@                  _SUR                  (       a  UR                  RB                  OS_SURD                  _SUR                  (       a?  UR                  R$                  (       a$  UR                  R$                  RG                  5       OS_SURH                  _SURJ                  _SURL                  _SURN                  _SURP                  _SURR                  _SURT                  _SURV                  _SURX                  _SURZ                  _SUR\                  _SUR^                  _UR`                  URb                  URd                  URf                  S.E5        GM     GM      U(       d  [i        SSSS 0S!9$ URk                  U5      n[m        5       nURo                  US"S#9  URq                  S5        [i        URs                  5       SSS 0S!9$ ! [         a    [        SSS9Sef = f! [         a    [        SSS9Sef = f! [:         a     GM  f = f)$zExport recent predictions as Parquet.

Returns a Parquet file with predictions for completed races.
Requires pyarrow or fastparquet to be installed.
r   Nr  r  r   r  r  r  r\  r  r   rX  rY  rZ  r   r  r   rJ  r   rK  r   rL  rM  rI  rN  rO  rP  )rQ  rR  rS  rT  r   r  r  z*attachment; filename="predictions.parquet"r  Fr  ):r  r  r   r  r  r  r  r  r,   r   rW  r   rW   r7   r  rO  r  r6   r5  r4  r  r6  r  r   r7  r  r   r2  r  r   r[  r  rX  rY  rZ  rP  r  r   rJ  r   rK  r   rL  rM  rI  rN  rO  rP  rQ  rR  r  r  r   r  r   r  r  r#  )rO   r  r  r   r  r  r  r  r  r  r  r  r  r  rQ  r  r  r  r	  r
  s                       rZ   export_predictions_parquetr  
  s    C4 	*
ZZ\I1$55
g&::< 		dll		  J.  H,

 
'&&++-t/A/A/F/F/H/S/S/U	V	u	  b!FD"t988G$++GOOtww,FGKKM	,,T<J **D4yE!KKtww!4#3#3 4<<T\\//T !$//	
  <<DLL,E,E 11;;=! !$//  !$//   "4#3#3 !$//  #D$5$5!" t||#$ !$//%& '(=(='( &t';';)* ()?)?+, $(#3#3)-)?)? $ < < $ = =3	 + #^ 1%'S
 	
 
d	BYFMM&M&
KKN!-&(TU W  U
 	  W

 	`  		s(   O! O: 9P!O7:P
P"!P"z/export/race-predictions.pdfc                 *
    SSK Jn  SSKJn  SSKJn  SSKJn  SSKJ	nJ
nJn	Jn
Jn  SSKJn  SSKJn  UR)                  [*        5      R-                  [*        R.                  U:H  5      R1                  5       nU(       d
  [        SSS	9eUR)                  [*        5      R3                  [5        [*        R6                  5      5      R-                  [*        R.                  U:H  5      R1                  5       nUR)                  U5      R-                  UR8                  U:H  5      R;                  5       nU(       d
  [        SSS	9eU" U5      nUR=                  X5      n[?        5       nU" UUS9nU" 5       n/ nUR6                  (       a  UR6                  R@                  OSnUR6                  (       a?  UR6                  RB                  (       a$  UR6                  RB                  RE                  5       OSnSURF                   SU SU SURH                   S[K        URL                  5       3
nURO                  U" SUS   5      5        URO                  U	" SSU-  5      5        URO                  U" UUS   5      5        URO                  U	" SSU-  5      5        / SQ/n[Q        URL                  SS9 H  u  nnURR                  b  URR                  S  S!URT                  S  3OS"nURO                  [W        U5      URX                  =(       d    S#URZ                   3UR\                  =(       d    S"UR^                  S$ UR`                  S% URb                  S% [W        URd                  5      U/5        M     S&U-  S'U-  S(U-  S)U-  S*U-  S*U-  S+U-  S,U-  /nU
" UUSS-9nURg                  U" S.S/S0URi                  S15      4S2S/S0URj                  4S3S4S5S6S7S8S9S/S:S+URl                  4S;S<S:URj                  URi                  S=5      /4S>S?/5      5        URO                  U5        URO                  U	" SSU-  5      5        SS@K7J8nJ7n   URO                  U" SAU Rr                  " U5      RE                  5        SBURt                  Rw                  SCS"5       SDURt                  Rw                  SES"5      S$ 3US   5      5        URy                  U5        UR{                  5       n!SFU SG3n"[}        U!SHSIU" SJ3[W        [K        U!5      5      SK.SL9$ ! [         a    [        SSS	9S
ef = f)May  Export race predictions as a PDF document.

Generates a PDF with a structured table showing each runner's rating,
win probability, place probability, predicted placing, and confidence
interval.  Requires the ``reportlab`` library.

Args:
    race_id: Race ID to export.
    db: Database session.

Returns:
    PDF binary response with appropriate Content-Type and disposition.
r   )colors)letter)getSampleStyleSheet)inch)	ParagraphSimpleDocTemplateSpacerTable
TableStyler  zBPDF export requires reportlab. Install with: pip install reportlabr   Nr  r  r  rH  r  )pagesizeUnknownr   zRace u    — z (z)<br/>Distance: zm  |  Starters: u%   <b>TipSharks — Race Predictions</b>Titler  r  Normalr  )#r5   r3   RatingzWin%zPlace%zPred.zCI Range)r{   z.0fu   –u   —zID r  z.1%r  g?gffffff?g?gffffff?g      ?g333333?)	colWidths
repeatRows
BACKGROUNDr   r   r2  r   z#1a5276	TEXTCOLOR)FONTNAMEr%  r&  zHelvetica-Bold)FONTSIZEr%  r&  	   )r)  r   r  r2  r2     )ALIGNr%  r,  CENTER)r.  )r  r  )rr   r2  LEFT)VALIGNr%  r,  MIDDLEGRIDr,  ROWBACKGROUNDSr+  z#f2f3f4)
TOPPADDINGr%  r,     )BOTTOMPADDINGr%  r,  r6  )r   r   zGenerated: z | Field size: ro  z | Avg rating: 
avg_ratingrace_z_predictions.pdfzapplication/pdfzattachment; filename="r   )r  zContent-Lengthr  )?reportlab.libr  reportlab.lib.pagesizesr  reportlab.lib.stylesr  reportlab.lib.unitsr  reportlab.platypusr  r  r  r  r  r  r   r  r  r  r  rW   r7   r  r2  r  rN  r$   rO  r   r7  r  r   rY  r5  rP  rX  rZ  r  r[  r  	enumerater  r  rj   rJ  r   rK  rN  rO  rP  rR  setStyleHexColorwhitegreyr   r   r  metadatari   buildr#  r   )#rO   r   r  r  r  r  r  r  r  r  r  r  r  r  rQ  r  r  r  r
  docstyleselementsrY  r   
title_text
table_dataidxr  ci
col_widthstabler   r   	pdf_bytesfilenames#                                      rZ   export_race_predictions_pdfrQ  m  s   ((2<,	
 	
 C4 88D>  G!34::<D4DEE 		DLL)	*	7"	#		 	 xx ''7(BCGGIH4PQQb!F$$T4J YF
FV
4C "FH #',,DLLIE <<DLL55 	!!++-    !ugR	{ ;__% &//01	3 
 OOIEvgWXOOF1dTk*+OOIj&*:;<OOF1cDj)* 	RJ z55Q?	T ++7 ++C0D4Q4QRU3VW 	
 	C8S#8  )E((-'',))#.D**+		
 @* 	d
d
d
d
d
d
d
d
	J *
qAE	NNvw	0JKfgv||<?015263<$\\6??9#=>	 46#	
. OOE OOF1cDj)*&OO(,,s+5578 9%..22<GH I%..22<GLN 8		
 IIh!Iwi/0H$%;H:Q#G!#i.1
 C  W
 	s   &S< <Tz/ws/races/{race_id}	websocketc           	        #    SSK JnJn  SSKJn   [        5        nUR                  [        5      R                  [        R                  U:H  5      R                  5       nU(       d"  U R                  SSS9I Sh  vN    SSS5        gUR                  [        5      R                  [        [        R                  5      [        [        R                  5      R                  UR                   5      [        [        R                  5      R                  UR"                  5      [        [        R                  5      R                  UR$                  5      5      R                  [        R                  U:H  5      R                  5       nU(       a  UR                  O/ nU" Xg5      nSSS5        UR-                  X5      I Sh  vN   UR/                  WU 5      I Sh  vN   UR1                  U5      (       d  UR3                  U5          U R5                  5       I Sh  vN n	 [6        R8                  " U	5      n
U
R;                  SS5      nU
R;                  SU5      nUS:X  a]  [=        U[>        5      (       aH  [(        RA                  SSU0S9  UR/                  [6        RB                  " SUS.5      U 5      I Sh  vN   O[(        RE                  SXS.S9   M   GN[! , (       d  f       GN== f! [&         a0    [(        R+                  SS	S
9  U R                  SSS9I Sh  vN     gf = f GNi GNR GN Ny! [6        RF                   a    [(        RI                  SSU0S9   Nf = f! [J         a    [(        RA                  SSU0S9   O&[&         a    [(        R+                  SSU0S	S9   Of = fURM                  X5      I Sh  vN    URO                  U5      S:X  a)  UR1                  U5      (       a  URQ                  U5        ggg! URM                  X5      I Sh  vN    URO                  U5      S:X  a)  UR1                  U5      (       a  URQ                  U5        f f f = f7f)a5  WebSocket endpoint for live race updates.

On connect, validates the race exists in the database and sends
the initial race state (race details + starters). Broadcasts live
updates as they become available via the simulation task.

Args:
    websocket: The WebSocket connection.
    race_id: Race identifier.
r   )_build_initial_statemanagerr  i  rH  )r  reasonNz%Failed to validate race for WebSocketTrq  i  zInternal server errortyper   r   	subscribez!Client subscribed to race updatesru   
subscribed)rW  r   zUnknown WebSocket message typez"Invalid JSON received on WebSocketzWebSocket client disconnectedzWebSocket error)rv   rr  ))apps.backend.api.websocketrT  rU  r  r  r0   rW   r7   r  r2  r  closerN  r$   rO  r  r   r<  r@  r   rw   r  connectsend_personal_messageis_simulation_runningstart_simulationreceive_textr  loadsri   rm  r   rx   dumpsdebugJSONDecodeErrorwarningr   
disconnectget_connection_countstop_simulation)rR  r   rT  rU  r  r   rQ  r  initial_stater  msgmsg_typemsg_race_ids                rZ   websocket_race_endpointrm    s     I4]g==&--dgg.@AGGIDoo48HoIII	 ] d#t||,t}}-88Gt}}-88Ht}}-88I	 7*+  )-t}}"H 1@M+ 8 //)
--- 
'
'y
AAA ((11  )&-"//11Djj&7762.!ggi9{*z+s/K/KKK;(+6   
 "77

L[#QR!  
 LL8'/D ! % G J ],  <tLoo40GoHHH . B 2 '' 8$g.  
  Q3Iw;OP S&y'.BTRS   444''0A5':W:W;
 ;
 ##G,;
5   444''0A5':W:W;
 ;
 ##G,;
5sF  Q 
K* AK:K;K K* Q 	DK"K* *Q >L'?Q L*+Q M" L-M" BL2 6L07L2 ;M" <L2 M" K
K'"K* %Q 'K* *1L$LL$!Q #L$$Q *Q -M" 0L2 2*MM" MM" " N'P  N'$P &N''P *Q >O?AQ QPAQQ z/metricsc                      [        SSS9$ )a  Prometheus metrics endpoint stub.

Returns a placeholder response. To serve real metrics:
1. pip install prometheus-client
2. Create counters/histograms/gauges in a metrics module
3. Call generate_latest(REGISTRY) here

See docs/monitoring.md for the full instrumentation guide.
zN# TipSharks API metrics stub
# Integrate prometheus-client for production use
r  )r   rS   )r   r   r   rZ   metricsro  }  s     b r   __main__z0.0.0.0i@  )hostport)r   r  r  r   r  r  r   r   r   r   ior   r   pathlibr	   typingr
   fastapir   r   r   r   r   r   r   r   r   r   fastapi.middleware.corsr   fastapi.responsesr   r   r   fastapi.securityr   r   fastapi.staticfilesr   pydanticr   r   r   slowapir   r    slowapi.errorsr!   
sqlalchemyr"   r  r#   r$   packages.core.common.loggingr%   r&   packages.core.common.rate_limitr'   packages.core.common.schedulerr(   packages.core.common.settingsr)   r*   rV  r,   packages.core.ratings.recomputer-   packages.core.storage.auditr.   packages.core.storage.databaser0   packages.core.storage.ingestionr1   r  r2   r3   r4   r5   r6   r7   r8   r9   rw  r:   r   rw   r   r   limiterapprl   add_exception_handler
api_routerrU   r   cors_allow_originsr{  r   cors_originsadd_middleware
middlewarer[   rf   rn   r}   r   __file__parentr  r  mountrj   docs_site_dirmkdiron_eventr   r   securityr   r   r   r   r   r   r   r   r   r   r  r	  r  r  r"  r%  r+  r1  r6  r9  rC  rF  rI  rW  rc  rg  rl  rn  rt  r  r  r  r  r   r  r   r   r  r  ri   r  r  r  r  r  r  r  r  r  r   r  r  r  r   r	  r.  r9  r>  rB  rD  rF  rS  rZ  postrb  r  r  r  r  deleter  r  r  r  r  r  r  r  r  r  r  r  rQ  rR  rm  ro  include_routeruvicornrun)origins   0rZ   <module>r     s   4  
   	 3 3       3 O O E + 1 1 9 ,  . B C = K 1 = 3 6 <	 	 	 H 	H	>  	
 2
3
h 		    +-I J e$
  ||&&#- 	E'/||'F'F'L'LS'Q	R'QV,,.'Q	R 
   %%   $ $ $$ 	 	 	 '      0 X' X Xx x.


&
&
.
>>IIis7X3E/FGhIWh&/Mt4IIc-0t<   i< <& j; ; < DLHCU #$@ #Y Y 6!	 !#) #L19 11I 1Y i BI Cy C	  
y 

	 
I ,Y "y y SY S2i   (&Y &Ry 	 Y 
 
1i 1nI (4I 49 2s 2s 2 $26s)	#Y")4 )c ) )PT )X... . 	.
 . 
.d \*( +(
 |41 5 1
 -	,7D 8 .D 	=Ds D >D 	!,?D D @D 	#LAD3 D BD 	l;DS D <D 6	,7D 8 7D 	"<@D3 D AD l3D 4D 6D 7D 	"<@D AD 	|<D =D >2|=' =  3= !2IJ| ss+a("4\Jd8IJ4RS&/jKjKjK jK d
	jK
 :jK jK 	jK  KjKZ ,=PQ| &/>>> 	>  R>B "3JK| ss+a(&/	+K+K+K +K 		+K  L+K\ #4KL| ss+a(&/	+K+K+K +K 		+K  M+K\ .?ST| &/=== 	=  U=@ 0AVW| &/=== 	=  X=@ "#| &/222 	2  $2j )>?| "$LI,Gst,a(d8IJ&/FNFNTzFN 4ZFN 	FN
 FN :FN 	FN  @FNR 1BC ` &/+,''' 	' ' D'T ">B &/+,eee e CeP #4EF &/+,??? ? G?D +   	{ +, 	
, *  
 	{ +,DD'D D DN -   	{ +,  	
B #MsS &/+,2"22 2 T2j "3GHss8STa5PQ&/+,	))) 	) 	) I)X /@T   &/+,	### 	# 	##L .?UV{ &/III 	I  WIX !"| ""A &/..Tz.
 	.  #.b 01{ &/ 	  22 %oF{ bQ3/&/ff
f 	f  GfX %&{ $R t>J&/RRtR R 	R  'Rj )*{ $R t>J&/__t_ _ 	_  +_D )*{ "$LI,Gt>J&/yyTzy 4Zy 	y
 	y  +yx -.{ "$LI,Gt>J&/}}Tz} 4Z} 	}
 	}  /}@ ./{ &/fff 	f  0fX $%[-Y [- [- &[-B $56 7"   : zKK)$/ aa 
Ss   v/