
    +jNr                       d Z ddlmZ ddlZddlmZmZ ddlmZ ddl	Z
ddlZg dZi ddd	dd
ddddddddddddddddddddddddddddddddddZd dZd!dZd"dZe G d d             Zd#dZy)$a  ReferenceData: one interface, two sources.

fixtures mode : reads parquet files from a directory (sandbox / testing / CI)
mysql mode    : reads the live tvviewers tables on the server (same frames)

The engine only ever sees the frames, so swapping source is a config change.
    )annotationsN)	dataclassfield)Path)
tvuniverse
gl_ratingsglobal_sportstelecast_multipliersweekday_weightshour_weightsteam_weightshost_factor UNKNOWNNONEz-NONE-NANNOT_SUPPLIEDARCHIVEDARCHIVERECORDED_ARCHIVEARCHIVE_RECORDEDzMAGAZINE/STUDIOSTUDIONON_LIVERECORDEDDELAYEDREPEATRERUN	LIVE_LINK)z	LIVE-LINKr   OTHERSOTHERc                    t        |       j                         j                         j                  dd      }|dv ryt        j                  ||      S )z7Fold a raw telecast_type into the canonical vocabulary. _)r   r   NATr   r   )strstripupperreplace_TELECAST_CANONget)tss     2/var/www/html/crystal/crystal2/engine/reference.pycanonical_telecastr.   ,   sF    A&&sC0A&&q!$$    c                   | | d   dkD     j                         }|d   j                  t              |d<   g d}|j                  |dgz         d   j	                         j                         }||d   dk(     |dgz      j                  ddi      }|j                  ||d	
      }||d   dkD     }|j                  rt        j                  ddg      S |d   |d   z  |d<   |j                  d      d   j                  ddg      }||d   dk\     }t        j                  |j                  |d   j                  t              j                  d      S )a  Measure telecast-type multipliers from the evidence pool itself.

    Within each (country, channel, event_name) group containing LIVE rows, take
    the median ama_000 per type and express it as a ratio to that group's LIVE
    median. Comparing only inside a group controls for channel size and event
    pull, so what survives is the telecast-type effect. The median across groups
    is the multiplier.

    This replaces the hardcoded constants in world.py, which were invented for
    the synthetic world and understated every non-live category (ARCHIVE was
    0.04 against a measured 0.36; NON_EVENT/STUDIO/UNKNOWN were absent entirely
    and fell through to a 0.15 catch-all).
    ama_000r   telecast_typett)countrychannel
event_nameLIVElive_amacolumnsinner)onhow
multiplierratiomediansize   r2   r>   )copymapr.   groupbyr@   reset_indexrenamemergeemptypd	DataFrameaggindexastypefloatvalues)gsdkeysmedlivejrM   s          r-   derive_telecast_multipliersrX   4   sa    	2i=1""$A $$%78AdG/D
))DD6M
"9
-
4
4
6
B
B
DCD	V#$TYK%78VY
3V4 			$4W	-A	!J-!
Aww||_l$CDD9*-AgJ
))D/'
"
&
&&'9
:C
c&kQ
C<<#))'*8}';';E'B'I'IK L Lr/   c                p   ddl m} ddlm}  |t	        | j                  d      xs | d               } |t	        | j                  d      xs | d               }| j                  d      xs | d	   }t        | j                  d
      xs | j                  dd            } |d| d| d| d| d	d      S )a  SQLAlchemy engine for the read-only tvviewers reference database.

    Credentials are URL-encoded: a password containing '@', '/', '#' or ':'
    otherwise corrupts the DSN (an '@' makes SQLAlchemy read the tail of the
    password as the hostname).

    The read-only tvviewers reference DB and the read-write crystal
    application schema are usually different servers, so explicit ref_db_*
    keys win; db_* is the fallback for single-server setups.
    r   )
quote_plus)create_engineref_db_userdb_userref_db_passdb_passref_db_hostdb_hostref_db_portdb_porti  zmysql+pymysql://:@z
/tvviewersT)pool_pre_ping)urllib.parserZ   
sqlalchemyr[   r%   r*   int)cfgrZ   r[   userpwdhostports          r-   reference_enginero   T   s     ((c#''-0BC	NCDD
S/A3y>B
CC77=!3S^Dsww}%AD)ABD
4&#avQtfJ? r/   c                  `   e Zd ZU  ee      Zded<    ee      Zded<   dZded<   dZ	d	ed
<   e
d%d       Ze
d&d       Zd'dZd(dZd)dZd)dZe
d*d       Zd+dZd,dZed        Zd-dZd.dZd/dZd0dZd1dZd2dZd3dZd4dZd5dZ	 	 	 	 d6dZd7dZ d-d Z!d8d!Z"d9d"Z#d:d#Z$d-d$Z%y);ReferenceData)default_factorydictframescalibrationassumed constants (world.py)r%   calibration_sourceN
str | None
source_dirc           	          | |j                         D ci c]  \  }}||j                          c}}      S c c}}w )Nrt   )itemsrD   )clsrt   kvs       r-   from_frameszReferenceData.from_framest   s/    6<<>B41a1affh;BCCBs   :
c                j   t        |      }t        |j                  d            D ci c]"  }|j                  t	        j
                  |      $ }}t        D cg c]	  }||vs| }}|rt        d| d|        | |      }t        |      |_	        |j                  |dz         |S c c}w c c}w )u  Read every parquet in the directory, not just the core eight.

        FRAMES lists what the estimator cannot run without. The snapshot also
        carries sport_interest, event_category, event_country_interest_factor
        and the rest; iterating FRAMES alone silently dropped them, which
        disabled affinity — so every cross-market projection came back
        NO_AFFINITY_DATA in fixtures mode while working in mysql mode.
        z	*.parquetzFixtures missing: z in r{   zcalibration.json)r   sortedglobstemrK   read_parquetFRAMESFileNotFoundErrorr%   ry   load_calibration)r}   fixtures_dirpfrt   nmissingobjs           r-   from_fixtureszReferenceData.from_fixturesx   s     6<QVVK=P6QR!&&"//!,,RR$8188#&8	aS$IJJ QQ!334
 S8s   'B+	B0#B0c                $   t        |      }|j                         si | _        d| _        yt	        j
                  |j                               }|| _        d|j                  dd      dd}|j                  d      r|d	z  }|| _        |j                  d
      r%t        j                  |d
         | j                  d
<   |j                  d      r%t        j                  |d         | j                  d<   | j                  j                  dd       y)a  Replace assumed constants with quantities measured from evidence.

        worker/fit_model.py estimates the hour and weekday curves, the three
        exponents and the per-tier residual sigma from global_sports itself.
        Where that file exists its values win; where it does not, the engine
        falls back to the constants in world.py and says so through
        `calibration_source`.
        rv   Fzmeasured from generated_from_rowsr   ,z rowsstaleu8    (STALE — snapshot rebuilt since; re-run fit_model.py)r   r   _lookupsNT)r   existsru   rw   jsonloads	read_textr*   rK   rL   rt   __dict__pop)selfpathr   calsrcs        r-   r   zReferenceData.load_calibration   s     Jxxz!D&DD#jj'sww'<a@C5I777MMC"%77>"*,,,s>7J*KDKK'77$%-/\\#>O:P-QDKK)**d+r/   c                    t        | di       j                  d      xs i }|j                  d      sdddddS t        |d         t        |d         t        |d	         d
dS )a  Fitted exponents for the cross-market ratios.

        Defaults to 1.0 each -- the original assumption that audience scales
        proportionally with market size, channel share and appetite -- so the
        engine behaves exactly as before when no calibration is present.
        ru   	exponentsok      ?F)tvushareaffinityfittedr   r   r   T)getattrr*   rP   )r   es     r-   r   zReferenceData.exponents   sl     D-,00=CuuT{#OOQuXqz1B!!J-0DB 	Br/   c                    t        | di       j                  d      }|r6|j                         D ci c]  \  }}t        |      t	        |       c}}S d S c c}}w )Nru   sigma_by_tierr   r*   r|   ri   rP   )r   r,   r~   r   s       r-   r   zReferenceData.sigma_by_tier   sM    D-,00A;<QWWY7TQAa 7F$F7   Ac                    t        | di       j                  d      }|r6|j                         D ci c]  \  }}t        |      t	        |       c}}S d S c c}}w )Nru   correction_by_tierr   )r   cr~   r   s       r-   r   z ReferenceData.correction_by_tier   sN    D-,001EF;<QWWY7TQAa 7F$F7r   c                   t        |      fdfd} d      j                  ddi       d       d       d	       d
       d       d       d       d       d       |d       |d       |d       d      d}|d   }t        j                  |d   d      }|j                  j                         dz  dz  j                  d      j                  t              |d<   t        j                  |d   d      j                  j                  j                  d      j                  t              |d<   |d   j                  d       |d!<   |d"   j                  t              |d"<   d#d$lm}m} t!        |      }t#        |      d%k\  r||d&<   nBd#d'lm}	 t        j&                  |	j)                         D 
cg c]
  \  }
}|
|d( c}}
      |d&<   t        j&                  |j)                         D 
cg c]
  \  }
}|
|d) c}}
      |d*<   t        j&                  t+        |      D cg c]
  \  }}||d+ c}}      |d,<    | |-      S c c}}
w c c}}
w c c}}w ).u  Server mode. Column names below are NOT guesses — each is taken
        verbatim from the original Crystal source (estimateuploadfile),
        cited per table. This still needs Claude Code's Task 1 (SHOW CREATE
        TABLE) to confirm data types / nullability / indexes before trusting
        it at scale, but the column names themselves should already be
        correct, not placeholders.c                0    t        j                  |       S N)rK   read_sql)sqlengs    r-   <lambda>z*ReferenceData.from_mysql.<locals>.<lambda>   s    C- r/   c                    	  |       S # t         $ rG}t        dt        |      j                   d| dd d       t	        j
                         cY d}~S d}~ww xY w)a  For frames the ESTIMATOR does not need.

            ott_multipliers and the pan tables feed the viewership report only.
            If one of them is missing or renamed the report should lose a sheet,
            not the whole run lose its reference data.
            z([reference] optional frame unavailable: z: N   T)flush)	Exceptionprinttype__name__rK   rL   )r   excqs     r-   q_optz'ReferenceData.from_mysql.<locals>.q_opt   sa    &v &@c++,Bse559c;BFH||~%&s    	A<AAAa6  
                SELECT global_id, country, channel, sub_genre,
                       event_name, sports_teams, match_level, prog_date,
                       telecast_type, ama_000, start_time, end_time
                FROM global_sports
                WHERE ama_000 IS NOT NULL AND ama_000 > 0
            	prog_datedater9   aX  
                SELECT gl_id, gsiq_country AS country, gsiq_channel AS channel,
                       real_country, real_channel, ti_total_day_share,
                       prefer_gl_ratings_ama, mean_ama_000, max_ama_000
                FROM gl_ratings
                WHERE gsiq_channel IS NOT NULL AND TRIM(gsiq_channel) <> ''
            z
                SELECT territory AS country, total_individuals_2025 AS tvuniverse_000,
                       missing_channel_weight, world_sub_region, gdp_billion_usd
                FROM tvuniverse
            z
                SELECT team_name AS team, event_name, event_season,
                       ama_team_weight AS weight
                FROM sports_team_weight
                WHERE ama_team_weight IS NOT NULL
            z
                SELECT event_name, host_country_name AS host_country,
                       host_country_factor AS factor
                FROM event_host_country_factor
                WHERE active_flag = 1
            a  
                SELECT id, event_name, event_season, country, event_ama_000,
                       event_country_tvuniverse_percentage, global_event_ama_000,
                       country_perc_of_global_event_ama
                FROM event_country_tvuniverse_factor
                WHERE event_country_tvuniverse_percentage IS NOT NULL
                  AND event_country_tvuniverse_percentage > 0
            a  
                SELECT event_name, country, event_country_interest_factor AS factor
                FROM event_country_interest_factor
                WHERE event_country_interest_factor IS NOT NULL
                  AND event_country_interest_factor > 0
            z
                SELECT event_name, season_name, sport, match_level,
                       match_level_weight AS factor
                FROM match_level_weight_by_event
            z
                SELECT country, channel, cap_ama_000
                FROM global_sports_ama_caps
                WHERE cap_ama_000 IS NOT NULL AND cap_ama_000 > 0
            a  
                SELECT country, sports_category,
                       follow_percentage,
                       interest_lineartv_avg_percentage
                FROM national_sport_interest
                WHERE follow_percentage IS NOT NULL AND follow_percentage > 0
            z
                SELECT country, tv_to_ott_multiplier, ooh_multiplier,
                       household_min_size, household_max_size
                FROM ott_multipliers
                WHERE tv_to_ott_multiplier IS NOT NULL
            zn
                SELECT channel_name, pan_region, pan_countries
                FROM pan_channels
            z
                SELECT country_name, channel_name, pan_channel_name, pan_region
                FROM pan_mapping
                WHERE pan_channel_name IS NOT NULL
            z
                SELECT event_name, sub_genre AS sports_category
                FROM event_category_map
                WHERE sub_genre IS NOT NULL AND TRIM(sub_genre) <> ''
            )r	   r   r   r   r   event_country_tvuniverse_factorevent_country_interest_factormatch_level_weightsglobal_sports_ama_capssport_interestott_multiplierspan_channelspan_mappingevent_categoryr	   
start_timecoerce)errorsi        hourrB   weekdaysports_teamsr   teamsr2      )	WEEKDAY_WHOUR_W   r
   )TELECAST_MULTrC   )r   weightr   )r   r   r   r{   )ro   rH   rK   to_timedeltadttotal_secondsfillnarO   ri   to_datetimer   rE   r.   worldr   r   rX   lenr   rL   r|   	enumerate)r}   rj   r   rt   rR   str   r   derivedr   r~   r   hwr   r   s                 @@r-   
from_mysqlzReferenceData.from_mysql   s    s#-	&,     f 56             01 2 0 ./ 0 . $% & $ '( ) '   !   % &   " #  ! "    ! YQ
n O$ __R-h?ee))+t3b8@@DKKCP6
6
8D"WWVVAYvvc{ 	9
 (//37 !1556HI? 	-
 .b1w<1-4F)*,-/\\*0024Q $%A6 4.5F)* %'LL5>__5FGTQa(G%I !!#2;F2CD$!Qa1%D"F~ &!!4 HDs   I
I#
:I)
c                    t        |      }|j                  dd       | j                  j                         D ]  \  }}|j	                  || dz  d         y )NT)parentsexist_okz.parquetFrN   )r   mkdirrt   r|   
to_parquet)r   r   r   namedfs        r-   save_fixtureszReferenceData.save_fixtures  sW    t D))+ 	>HD"MM!h//uM=	>r/   c                `    | j                   j                  di       }||vr
 |       ||<   ||   S )Nr   )r   
setdefault)r   r   buildr   s       r-   _cachezReferenceData._cache  s3    MM$$Z4q=gAdGwr/   c                   | j                  |g      j                  |d      t        |      dk(  r|d      nt        t	        fd|D               }t        t	        ||   j                  t                          S )Nsubsetfirstr   keepr   r   c              3  (   K   | ]	  }|     y wr    ).0r~   rS   s     r-   	<genexpr>z,ReferenceData._first_wins.<locals>.<genexpr>  s     :NA1Q4:Ns   )dropnadrop_duplicatesr   listziprs   rO   rP   )r   rT   valueidxrS   s       @r-   _first_winszReferenceData._first_wins  sr    IIeWI%T8INaQjS:N:N5O0PCQuX__U3455r/   c                L      j                  d fd      j                  |      S )Nr   c                 F     j                   j                  d   dgd      S )Nr   r4   tvuniverse_000r  rt   r   s   r-   r   z#ReferenceData.tvu.<locals>.<lambda>  s&    $*:*:KK%	{4D+F r/   r   r*   )r   r4   s   ` r-   r   zReferenceData.tvu  s'    {{5 #F GGJs7|	Tr/   c                P      j                  d fd      j                  ||f      S )Nr   c                 H     j                   j                  d   ddgd      S )Nr   r4   r5   ti_total_day_sharer	  r
  s   r-   r   z%ReferenceData.share.<locals>.<lambda>  s(    D,<,<KK%	9'= -" r/   r  )r   r4   r5   s   `  r-   r   zReferenceData.share  s*    {{7 %" ##&3'9#:	;r/   c                z      j                  d fd      }t        |      }||v r||   S |j                  dd      S )Ntelecastc                 F     j                   j                  d   dgd      S )Nr
   r2   r>   r	  r
  s   r-   r   z-ReferenceData.telecast_mult.<locals>.<lambda>  s'    D,<,<KK.//1BL-R r/   r   g333333?)r   r.   r*   )r   r+   rS   keys   `   r-   telecast_multzReferenceData.telecast_mult  sI     KK
 %R S #!8S6M uuY%%r/   c                `      j                  d fd      j                  t        |      d      S )Nr   c                 F     j                   j                  d   dgd      S )Nr   r   r   r	  r
  s   r-   r   z)ReferenceData.weekday_w.<locals>.<lambda>  s&    d.>.>KK)*YK/C r/   r   r   r*   ri   )r   wds   ` r-   	weekday_wzReferenceData.weekday_w  s/    {{9 'C DDGCBQTDU	Vr/   c                `      j                  d fd      j                  t        |      d      S )Nr   c                 F     j                   j                  d   dgd      S )Nr   r   r   r	  r
  s   r-   r   z&ReferenceData.hour_w.<locals>.<lambda>  $    4+;+;KK'&8,= r/         ?r  )r   r   s   ` r-   hour_wzReferenceData.hour_w  s,    {{6 $= >>Ac#a&#>N	Or/   c                N      j                  d fd      j                  |d      S )Nteamc                 F     j                   j                  d   dgd      S )Nr   r  r   r	  r
  s   r-   r   z&ReferenceData.team_w.<locals>.<lambda>  r  r/   r   r  )r   r  s   ` r-   team_wzReferenceData.team_w  s)    
 {{6 $= >>Ac$n	Mr/   c                R      j                  d fd      j                  ||fd      S )Nrm   c                 H     j                   j                  d   ddgd      S )Nr   r6   host_countryfactorr	  r
  s   r-   r   z&ReferenceData.host_f.<locals>.<lambda>  s(    4+;+;KK&~(F, r/   r   r  )r   eventr4   s   `  r-   host_fzReferenceData.host_f  s,    {{6 $ sE7+S1	2r/   c                n     d j                   vry j                  d fd      }|j                  |      S )z)Which sport category an event belongs to.r   Nevcatc                     t         j                  d   j                  dgd      ddg   j                  dd             S )Nr   r6   r   r   sports_categoryF)rN   r   )rs   rt   r   
itertuplesr
  s   r-   r   z(ReferenceData.sport_of.<locals>.<lambda>  sF    KK()_\N_A-.00:
T
0R*T r/   rt   r   r*   )r   r&  rS   s   `  r-   sport_ofzReferenceData.sport_of  s9    4;;.KK "T U uuU|r/   c                r     d j                   vry fd} j                  d|      j                  ||f      S )u  Share of a country's population that follows this sport (percent).

        Prefers the linear-TV figure where present — it measures watching on
        TV rather than merely following the sport — and falls back to overall
        follow_percentage, which is populated far more widely.

        Duplicate (country, sport) rows exist (e.g. New Zealand lists Rugby at
        both 18% and 8.2%), so take the MAX rather than an arbitrary first
        match: the higher figure is the broader definition of following.
        r   Nc                 F   j                   d   j                         } | d   j                  | d         j                  t              | d<   | j                  ddg      d   j                         }|j                         D ci c]  \  }}|t	        |       c}}S c c}}w )Nr    interest_lineartv_avg_percentagefollow_percentager   r4   r+  )rt   rD   r   rO   rP   rF   maxr|   )r   gr~   r   r   s       r-   r   z%ReferenceData.affinity.<locals>.build  s    -.335B<=r"567u sG

I'89:3?CCEA,-GGI6DAqAuQxK666s   Br   r-  )r   r4   sportr   s   `   r-   r   zReferenceData.affinity  s<     4;;.	7 {{:u-117E2BCCr/   c                    | j                  |      }|sy| j                  ||      }| j                  ||      }|r|sy||z  S )zTarget-vs-source appetite for this event's sport.

        Returns None when either side is unknown, so the caller can decline to
        project rather than silently assuming parity.
        N)r.  r   )r   r&  target_countrysource_countryr5  a_ta_ss          r-   affinity_ratiozReferenceData.affinity_ratio   sJ     e$mmNE2mmNE2#Syr/   c                P      fd} j                  d|      j                  |      S )Nc                     j                   d   } d| j                  vri S | j                  dg      j                  d      }t	        t        |d   |d               S )Nr   world_sub_regionr   r4   )rt   r:   r   r   rs   r  )r   rS   r   s     r-   r   z'ReferenceData.sub_region.<locals>.build  s]    \*B!3			"4!5	6FFyQAAiL!,>*?@AAr/   	subregionr  r   r4   r   s   `  r-   
sub_regionzReferenceData.sub_region  s'    	B {{;.227;;r/   c                P      fd} j                  d|      j                  |      S )Nc                 j    j                   d   } d| j                  vri S j                  | dgd      S )Nr   gdp_billion_usdr4   )rt   r:   r  r   r   s    r-   r   z ReferenceData.gdp.<locals>.build  s:    \*B 

2	##B5FGGr/   gdpr  r@  s   `  r-   rF  zReferenceData.gdp  s'    	H
 {{5%(,,W55r/   c                   | j                  |      }|r| j                  ||      nd}|r| j                  ||      nd}|r|syt        t        j                  ||z              }| j                  |      | j                  |      }	}|r)|	r'|dt        t        j                  ||	z              z  z  }| j                  |      | j                  |      }}
|
r|r
|
|k7  r|dz  }t        |      S )u?  Distance between two markets for projecting THIS event. Lower is closer.

        Three terms, all in log space so ratios either way cost the same:
          appetite  — how differently the two markets follow the sport
          scale     — how differently sized their economies are, which stands in
                      for TV spend, channel count and production values
          geography — a flat penalty for leaving the sub-region

        Returns None when appetite cannot be measured, so the caller can fall
        back rather than rank on scale alone.
        Nr  g      ?)r.  r   absnplogrF  rA  rP   )r   targetsourcer&  r5  r9  r:  rS   g_tg_sr_tr_ss               r-   
similarityzReferenceData.similarity"  s     e$.3dmmFE*.3dmmFE*#sSy!"88F#TXXf%5S3s266#),---A??6*DOOF,CS33#:IAQxr/   c                P      fd} j                  d|      j                  |      S )z;(tv_to_ott, out_of_home) multipliers for a market, or None.c                    j                   j                  d      } | | j                  ri S | j                  dg      j	                  d      }|j                  d      D ci c]X  }|j                  t        |j                        t        j                  |j                        rt        |j                        ndfZ c}S c c}w )Nr   tv_to_ott_multiplierr   r4   Fr   g        )rt   r*   rJ   r   r   r,  r4   rP   rT  rK   notnaooh_multiplier)r   rS   rr   s      r-   r   z ReferenceData.ott.<locals>.build>  s    !23BzRXX			"8!9	:JJ9UA \\\68 IIa&<&< =;=88ADTDT;Ua&6&6 7[^ ` ` 8 8 8s   #ACottr  r@  s   `  r-   rX  zReferenceData.ott<  s&    	8 {{5%(,,W55r/   c                     | j                   d   S )Nr	   r{   r
  s    r-   evidencezReferenceData.evidenceH  s    {{?++r/   c                P      fd} j                  d|      j                  |      S )Nc                     j                   d   j                  dg      } | j                  d      d   j                         j	                  t
              j                         S )Nr   r  r   r4   )rt   r   rF   r3  rO   rP   to_dictrE  s    r-   r   z+ReferenceData.flagship_share.<locals>.buildL  sU    \*11:N9O1PB::i()=>BBDKKERZZ\\r/   flagshipr  r@  s   `  r-   flagship_sharezReferenceData.flagship_shareK  s'    	] {{:u-11'::r/   )rt   rs   return'ReferenceData')r   
str | Pathr`  ra  )r   rb  r`  bool)r`  rs   )r`  zdict | None)rj   rs   r`  ra  )r   rb  r`  None)r   r%   )r4   r%   r`  float | None)r4   r%   r5   r%   r`  re  )r+   r%   r`  rP   )r  ri   r`  rP   )r   ri   r`  rP   )r  r%   r`  rP   )r&  r%   r4   r%   r`  rP   )r&  r%   r`  rx   )r4   r%   r5  r%   r`  re  )r&  r%   r7  r%   r8  r%   r`  re  )r4   r%   r`  rx   )rK  r%   rL  r%   r&  r%   r`  re  )r4   r%   r`  ztuple[float, float] | None)r`  pd.DataFrame)&r   
__module____qualname__r   rs   rt   __annotations__ru   rw   ry   classmethodr   r   r   r   r   r   r   r   r   staticmethodr  r   r   r  r  r  r!  r'  r.  r   r;  rA  rF  rQ  rX  rZ  r_  r   r/   r-   rq   rq   l   s   .FD.d3K3<<!J
! D D  (6BGG Y" Y"v> 6 6T;
&VOM2D.'*/;"<64
6,;r/   rq   c                    g }g d}|D ]&  }|| j                   vs|j                  d| d       ( d| j                   vrd| j                   vr|j                  d       t        |       dk(  r|j                  d       |S )	zBPlain-language problems the dashboard can show. Empty list = fine.)r6   country_namechannel_namer2   zThe schedule is missing a 'z	' column.r   broadcast_timez0Add an 'hour' (0-23) or 'broadcast_time' column.r   zThe schedule has no rows.)r:   appendr   )r   problemsrequiredr   s       r-   validate_schedulers  R  s    HNH HBJJOO9!IFGH RZZ$4BJJ$FJK
2w!|34Or/   )r`  r%   )rR   rf  r`  rf  )rj   rs   )r   rf  r`  z	list[str])__doc__
__future__r   r   dataclassesr   r   pathlibr   numpyrI  pandasrK   r   r)   r.   rX   ro   rq   rs  r   r/   r-   <module>rz     s1   #  (   
L	9&.	;@)I() 	 %i 	 $6y x "*8 
 '
 5>z j ": ;)%L@0 b; b; b;Jr/   