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:
144
scraper/engine/db_alias_loader.py
Normal file
144
scraper/engine/db_alias_loader.py
Normal 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
|
||||
Reference in New Issue
Block a user