Pulls data from the Sportradar Soccer API to identify own goals across one or more configured soccer competitions/seasons and generates a self-contained HTML report.
├── config.py # API key, competition/season list, file paths
├── step1_fetch_all_games.py # Step 1: fetch all games via season summaries
├── step2_sync_recorded_flag.py # Step 2: sync games.recorded from recordings JSON (Supabase)
├── step3_fetch_regular_timelines.py # Step 3: fetch regular timelines for completed matches
├── step4_fetch_extended_timelines.py # Step 4: fetch extended timelines for completed matches
├── step5_extract_own_goals.py # Step 5: extract own goals from regular timelines
├── step6_extract_var_and_shootouts.py # Step 6: build VAR + shootout derived tables
├── step7_generate_reports.py # Step 7: generate all report HTML files
├── gen_report_recordings_library_html.py # recordings-only report regeneration from export JSON
├── report_navigation.py # Shared nav bar for all report pages
├── run_all.py # Orchestrate Steps 1–7
├── run_extended_timeline_pipeline.py # Extended-only runner (full-backfill/daily)
├── data/
│ ├── schedule.csv # Matches across configured competitions/seasons
│ ├── own_goals.csv # Extracted own goal records
│ └── timelines/ # Cached raw JSON from Sportradar (one file per match)
├── report_own_goals.html # Own goals (primary; nav links to the three below)
├── report_penalty_shootouts.html
├── report_var_events.html
└── report_var_unpaired.html
config.py → COMPETITIONS listcompetition_id, competition_name, season_id, season_namepublic."Competitions", public."Seasons (current sr:season:ID)", public."All Games (sr:sport_events)", public."Completed Matches - full sport_event_timelines", public."Completed Matches - Extended Timeline", public.own_goals — see supabase/migrations/timeline.type == "score_change" && timeline.method == "own_goal"$env:PYTHONUTF8="1"
python run_all.py
run_all.py step orderstep1_fetch_all_games.py)recorded flag in All Games (Supabase only; step2_sync_recorded_flag.py)step3_fetch_regular_timelines.py)step4_fetch_extended_timelines.py)step5_extract_own_goals.py)step6_extract_var_and_shootouts.py)step7_generate_reports.py)# Step 1 - Refresh all games only
python step1_fetch_all_games.py
# Step 3 - Fetch any new/missing regular timelines
python step3_fetch_regular_timelines.py
# Step 4 - Extended timelines only (separate runner)
# one-time full backfill for completed games missing extended timeline rows
python run_extended_timeline_pipeline.py --full-backfill
# daily kickoff-window probe for newly completed games
python run_extended_timeline_pipeline.py --daily
# Step 5 - Re-extract own goals from regular timelines
python step5_extract_own_goals.py
# Step 7 - Rebuild report HTML files
python step7_generate_reports.py
# Recordings page only (after updating recordings export JSON)
python gen_report_recordings_library_html.py
The trial API key allows 1 request/second. step3_fetch_regular_timelines.py enforces a 1.1 second delay between requests. With 261 completed matches, the first run takes ~5 minutes. Subsequent runs only fetch new matches.
Reports on GitHub Pages (same folder on main):
The weekly/daily workflows commit these HTML files next to report_own_goals.html so Pages serves them automatically.
main / root (/)After the weekly workflow runs at 1:07 AM UTC on Wednesdays, the email arrives Tuesday evening in San Francisco and is sent via Brevo.
Setup:
BREVO_API_KEY, Value: your Brevo API keysportsdata@keepstagecoachfree.com is verified in your Brevo accountdata/schedule.csv: Includes all 393 matches (261 completed, 132 upcoming as of project start). Statuses: closed/ended = completed.data/timelines/: Raw JSON cached per match. Filenames are the sport event ID with colons replaced by underscores.data/own_goals.csv: One row per own goal with: player name, team, minute, benefiting team, score at time of OG, final score, commentary text.og_player_team field is the team the scorer plays for (the unfortunate one); benefiting_team is who it counts as a goal for.