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

@@ -0,0 +1,144 @@
"""Database-aware alias loaders for team and stadium resolution.
These loaders check the Django TeamAlias and StadiumAlias models
in addition to the hardcoded mappings, allowing aliases to be
managed via the admin interface.
"""
from datetime import date
from typing import Optional
class DatabaseTeamAliasLoader:
"""Load team aliases from the Django database.
Checks the core.TeamAlias model for alias mappings,
supporting date-aware lookups for historical names.
"""
def resolve(
self,
value: str,
sport_code: str,
check_date: Optional[date] = None,
) -> Optional[str]:
"""Resolve an alias value to a canonical team ID.
Args:
value: Alias value to look up (case-insensitive)
sport_code: Sport code to filter by
check_date: Date to check validity (None = current date)
Returns:
Canonical team ID if found, None otherwise
"""
from core.models import TeamAlias
from django.db.models import Q
if check_date is None:
check_date = date.today()
value_lower = value.lower().strip()
# Query aliases matching the value and sport
aliases = TeamAlias.objects.filter(
alias__iexact=value_lower,
team__sport__code=sport_code,
).select_related('team')
for alias in aliases:
if alias.is_valid_for_date(check_date):
return alias.team.id
return None
def get_aliases_for_team(
self,
team_id: str,
check_date: Optional[date] = None,
) -> list:
"""Get all aliases for a team.
Args:
team_id: Team ID
check_date: Date to filter by (None = all aliases)
Returns:
List of TeamAlias objects
"""
from core.models import TeamAlias
aliases = TeamAlias.objects.filter(team_id=team_id)
if check_date:
result = []
for alias in aliases:
if alias.is_valid_for_date(check_date):
result.append(alias)
return result
return list(aliases)
class DatabaseStadiumAliasLoader:
"""Load stadium aliases from the Django database.
Checks the core.StadiumAlias model for alias mappings,
supporting date-aware lookups for naming rights changes.
"""
def resolve(
self,
name: str,
sport_code: str,
check_date: Optional[date] = None,
) -> Optional[str]:
"""Resolve a stadium name to a canonical stadium ID.
Args:
name: Stadium name to look up (case-insensitive)
sport_code: Sport code to filter by
check_date: Date to check validity (None = current date)
Returns:
Canonical stadium ID if found, None otherwise
"""
from core.models import StadiumAlias
if check_date is None:
check_date = date.today()
name_lower = name.lower().strip()
# Query aliases matching the name and sport
aliases = StadiumAlias.objects.filter(
alias__iexact=name_lower,
stadium__sport__code=sport_code,
).select_related('stadium')
for alias in aliases:
if alias.is_valid_for_date(check_date):
return alias.stadium.id
return None
# Global instances
_db_team_loader: Optional[DatabaseTeamAliasLoader] = None
_db_stadium_loader: Optional[DatabaseStadiumAliasLoader] = None
def get_db_team_alias_loader() -> DatabaseTeamAliasLoader:
"""Get the database team alias loader."""
global _db_team_loader
if _db_team_loader is None:
_db_team_loader = DatabaseTeamAliasLoader()
return _db_team_loader
def get_db_stadium_alias_loader() -> DatabaseStadiumAliasLoader:
"""Get the database stadium alias loader."""
global _db_stadium_loader
if _db_stadium_loader is None:
_db_stadium_loader = DatabaseStadiumAliasLoader()
return _db_stadium_loader