feat: add Django web app, CloudKit sync, dashboard, and game_datetime_utc export

Adds the full Django application layer on top of sportstime_parser:
- core: Sport, Team, Stadium, Game models with aliases and league structure
- scraper: orchestration engine, adapter, job management, Celery tasks
- cloudkit: CloudKit sync client, sync state tracking, sync jobs
- dashboard: staff dashboard for monitoring scrapers, sync, review queue
- notifications: email reports for scrape/sync results
- Docker setup for deployment (Dockerfile, docker-compose, entrypoint)

Game exports now use game_datetime_utc (ISO 8601 UTC) instead of
venue-local date+time strings, matching the canonical format used
by the iOS app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-19 14:04:27 -06:00
parent 4353d5943c
commit 63acf7accb
114 changed files with 13070 additions and 887 deletions

View File

@@ -2,6 +2,7 @@
from datetime import datetime, date, timezone
from typing import Optional
from zoneinfo import ZoneInfo
from bs4 import BeautifulSoup
import re
@@ -45,7 +46,12 @@ class NBAScraper(BaseScraper):
Sources (in priority order):
1. Basketball-Reference - Most reliable, complete historical data
2. ESPN API - Good for current/future seasons
3. CBS Sports - Backup option
3. CBS Sports - Backup option (not implemented)
Source Timezones:
- basketball_reference: Eastern Time (ET) - times displayed as "7:30p"
- espn: UTC - ISO 8601 format with "Z" suffix
- cbs: Not implemented
"""
def __init__(self, season: int, **kwargs):
@@ -196,6 +202,25 @@ class NBAScraper(BaseScraper):
self._logger.debug(f"Could not parse date: {date_text}")
return None
# Get game start time (format: "7:30p" or "10:00p") - times are in ET
time_cell = row.find("td", {"data-stat": "game_start_time"})
if time_cell:
time_text = time_cell.get_text(strip=True)
if time_text:
try:
# Parse time like "7:30p" or "10:00p"
# Normalize: "7:30p" -> "7:30 PM", "10:00p" -> "10:00 PM"
time_normalized = time_text.replace("p", " PM").replace("a", " AM")
game_time = datetime.strptime(time_normalized, "%I:%M %p")
# Combine date and time with ET timezone (Basketball-Reference uses ET)
game_date = game_date.replace(
hour=game_time.hour,
minute=game_time.minute,
tzinfo=ZoneInfo("America/New_York"),
)
except ValueError:
self._logger.debug(f"Could not parse time: {time_text}, using midnight")
# Get teams
away_cell = row.find("td", {"data-stat": "visitor_team_name"})
home_cell = row.find("td", {"data-stat": "home_team_name"})
@@ -648,6 +673,7 @@ class NBAScraper(BaseScraper):
country=info.country,
latitude=info.latitude,
longitude=info.longitude,
timezone=info.timezone,
surface="hardwood",
roof_type="dome",
)