Add Stadium Progress system and themed loading spinners

Stadium Progress & Achievements:
- Add StadiumVisit and Achievement SwiftData models
- Create Progress tab with interactive map view
- Implement photo-based visit import with GPS/date matching
- Add achievement badges (count-based, regional, journey)
- Create shareable progress cards for social media
- Add canonical data infrastructure (stadium identities, team aliases)
- Implement score resolution from free APIs (MLB, NBA, NHL stats)

UI Improvements:
- Add ThemedSpinner and ThemedSpinnerCompact components
- Replace all ProgressView() with themed spinners throughout app
- Fix sport selection state not persisting when navigating away

Bug Fixes:
- Fix Coast to Coast trips showing only 1 city (validation issue)
- Fix stadium progress showing 0/0 (filtering issue)
- Remove "Stadium Quest" title from progress view

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-08 20:20:03 -06:00
parent 2281440bf8
commit 92d808caf5
55 changed files with 14348 additions and 61 deletions

View File

@@ -34,6 +34,40 @@ CONTAINER = "iCloud.com.sportstime.app"
HOST = "https://api.apple-cloudkit.com"
BATCH_SIZE = 200
# Hardcoded credentials
DEFAULT_KEY_ID = "152be0715e0276e31aaea5cbfe79dc872f298861a55c70fae14e5fe3e026cff9"
DEFAULT_KEY_FILE = "eckey.pem"
def show_menu():
"""Show interactive menu and return selected action."""
print("\n" + "="*50)
print("CloudKit Import - Select Action")
print("="*50)
print("\n 1. Import all (stadiums, teams, games, league structure, team aliases)")
print(" 2. Stadiums only")
print(" 3. Games only")
print(" 4. League structure only")
print(" 5. Team aliases only")
print(" 6. Canonical only (league structure + team aliases)")
print(" 7. Delete all then import")
print(" 8. Delete only (no import)")
print(" 9. Dry run (preview only)")
print(" 0. Exit")
print()
while True:
try:
choice = input("Enter choice [1-9, 0 to exit]: ").strip()
if choice == '0':
return None
if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9']:
return int(choice)
print("Invalid choice. Please enter 1-9 or 0.")
except (EOFError, KeyboardInterrupt):
print("\nExiting.")
return None
def deterministic_uuid(string: str) -> str:
"""
@@ -214,19 +248,55 @@ def import_data(ck, records, name, dry_run, verbose):
def main():
p = argparse.ArgumentParser(description='Import JSON to CloudKit')
p.add_argument('--key-id', default=os.environ.get('CLOUDKIT_KEY_ID'))
p.add_argument('--key-file', default=os.environ.get('CLOUDKIT_KEY_FILE'))
p.add_argument('--key-id', default=DEFAULT_KEY_ID)
p.add_argument('--key-file', default=DEFAULT_KEY_FILE)
p.add_argument('--container', default=CONTAINER)
p.add_argument('--env', choices=['development', 'production'], default='development')
p.add_argument('--data-dir', default='./data')
p.add_argument('--stadiums-only', action='store_true')
p.add_argument('--games-only', action='store_true')
p.add_argument('--league-structure-only', action='store_true', help='Import only league structure')
p.add_argument('--team-aliases-only', action='store_true', help='Import only team aliases')
p.add_argument('--canonical-only', action='store_true', help='Import only canonical data (league structure + team aliases)')
p.add_argument('--delete-all', action='store_true', help='Delete all records before importing')
p.add_argument('--delete-only', action='store_true', help='Only delete records, do not import')
p.add_argument('--dry-run', action='store_true')
p.add_argument('--verbose', '-v', action='store_true')
p.add_argument('--interactive', '-i', action='store_true', help='Show interactive menu')
args = p.parse_args()
# Show interactive menu if no action flags provided or --interactive
has_action_flag = any([
args.stadiums_only, args.games_only, args.league_structure_only,
args.team_aliases_only, args.canonical_only, args.delete_all,
args.delete_only, args.dry_run
])
if args.interactive or not has_action_flag:
choice = show_menu()
if choice is None:
return
# Map menu choice to flags
if choice == 1: # Import all
pass # Default behavior
elif choice == 2: # Stadiums only
args.stadiums_only = True
elif choice == 3: # Games only
args.games_only = True
elif choice == 4: # League structure only
args.league_structure_only = True
elif choice == 5: # Team aliases only
args.team_aliases_only = True
elif choice == 6: # Canonical only
args.canonical_only = True
elif choice == 7: # Delete all then import
args.delete_all = True
elif choice == 8: # Delete only
args.delete_only = True
elif choice == 9: # Dry run
args.dry_run = True
print(f"\n{'='*50}")
print(f"CloudKit Import {'(DRY RUN)' if args.dry_run else ''}")
print(f"{'='*50}")
@@ -236,14 +306,16 @@ def main():
data_dir = Path(args.data_dir)
stadiums = json.load(open(data_dir / 'stadiums.json'))
games = json.load(open(data_dir / 'games.json')) if (data_dir / 'games.json').exists() else []
print(f"Loaded {len(stadiums)} stadiums, {len(games)} games\n")
league_structure = json.load(open(data_dir / 'league_structure.json')) if (data_dir / 'league_structure.json').exists() else []
team_aliases = json.load(open(data_dir / 'team_aliases.json')) if (data_dir / 'team_aliases.json').exists() else []
print(f"Loaded {len(stadiums)} stadiums, {len(games)} games, {len(league_structure)} league structures, {len(team_aliases)} team aliases\n")
ck = None
if not args.dry_run:
if not HAS_CRYPTO:
sys.exit("Error: pip install cryptography")
if not args.key_id or not args.key_file:
sys.exit("Error: --key-id and --key-file required (or use --dry-run)")
if not os.path.exists(args.key_file):
sys.exit(f"Error: Key file not found: {args.key_file}")
ck = CloudKit(args.key_id, open(args.key_file, 'rb').read(), args.container, args.env)
# Handle deletion
@@ -252,8 +324,8 @@ def main():
sys.exit("Error: --key-id and --key-file required for deletion")
print("--- Deleting Existing Records ---")
# Delete in order: Games first (has references), then Teams, then Stadiums
for record_type in ['Game', 'Team', 'Stadium']:
# Delete in order: dependent records first, then base records
for record_type in ['Game', 'TeamAlias', 'Team', 'LeagueStructure', 'Stadium']:
print(f" Deleting {record_type} records...")
deleted = ck.delete_all(record_type, verbose=args.verbose)
print(f" Deleted {deleted} {record_type} records")
@@ -264,14 +336,21 @@ def main():
print()
return
stats = {'stadiums': 0, 'teams': 0, 'games': 0}
stats = {'stadiums': 0, 'teams': 0, 'games': 0, 'league_structures': 0, 'team_aliases': 0}
team_map = {}
# Determine what to import based on flags
import_stadiums = not args.games_only and not args.league_structure_only and not args.team_aliases_only and not args.canonical_only
import_teams = not args.games_only and not args.league_structure_only and not args.team_aliases_only and not args.canonical_only
import_games = not args.stadiums_only and not args.league_structure_only and not args.team_aliases_only and not args.canonical_only
import_league_structure = args.league_structure_only or args.canonical_only or (not args.stadiums_only and not args.games_only and not args.team_aliases_only)
import_team_aliases = args.team_aliases_only or args.canonical_only or (not args.stadiums_only and not args.games_only and not args.league_structure_only)
# Build stadium UUID lookup (stadium string ID -> UUID)
stadium_uuid_map = {s['id']: deterministic_uuid(s['id']) for s in stadiums}
# Import stadiums & teams
if not args.games_only:
if import_stadiums:
print("--- Stadiums ---")
recs = [{
'recordType': 'Stadium', 'recordName': stadium_uuid_map[s['id']],
@@ -310,7 +389,7 @@ def main():
stats['teams'] = import_data(ck, recs, 'teams', args.dry_run, args.verbose)
# Import games
if not args.stadiums_only and games:
if import_games and games:
# Rebuild team_map if only importing games (--games-only flag)
if not team_map:
for s in stadiums:
@@ -388,8 +467,63 @@ def main():
stats['games'] = import_data(ck, recs, 'games', args.dry_run, args.verbose)
# Import league structure
if import_league_structure and league_structure:
print("--- League Structure ---")
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
recs = [{
'recordType': 'LeagueStructure',
'recordName': ls['id'], # Use the id as recordName
'fields': {
'structureId': {'value': ls['id']},
'sport': {'value': ls['sport']},
'type': {'value': ls['type']},
'name': {'value': ls['name']},
'displayOrder': {'value': ls['display_order']},
'schemaVersion': {'value': 1},
'lastModified': {'value': now_ms, 'type': 'TIMESTAMP'},
**({'abbreviation': {'value': ls['abbreviation']}} if ls.get('abbreviation') else {}),
**({'parentId': {'value': ls['parent_id']}} if ls.get('parent_id') else {}),
}
} for ls in league_structure]
stats['league_structures'] = import_data(ck, recs, 'league structures', args.dry_run, args.verbose)
# Import team aliases
if import_team_aliases and team_aliases:
print("--- Team Aliases ---")
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
recs = []
for ta in team_aliases:
fields = {
'aliasId': {'value': ta['id']},
'teamCanonicalId': {'value': ta['team_canonical_id']},
'aliasType': {'value': ta['alias_type']},
'aliasValue': {'value': ta['alias_value']},
'schemaVersion': {'value': 1},
'lastModified': {'value': now_ms, 'type': 'TIMESTAMP'},
}
# Add optional date fields
if ta.get('valid_from'):
try:
dt = datetime.strptime(ta['valid_from'], '%Y-%m-%d')
fields['validFrom'] = {'value': int(dt.timestamp() * 1000), 'type': 'TIMESTAMP'}
except:
pass
if ta.get('valid_until'):
try:
dt = datetime.strptime(ta['valid_until'], '%Y-%m-%d')
fields['validUntil'] = {'value': int(dt.timestamp() * 1000), 'type': 'TIMESTAMP'}
except:
pass
recs.append({
'recordType': 'TeamAlias',
'recordName': ta['id'], # Use the id as recordName
'fields': fields
})
stats['team_aliases'] = import_data(ck, recs, 'team aliases', args.dry_run, args.verbose)
print(f"\n{'='*50}")
print(f"COMPLETE: {stats['stadiums']} stadiums, {stats['teams']} teams, {stats['games']} games")
print(f"COMPLETE: {stats['stadiums']} stadiums, {stats['teams']} teams, {stats['games']} games, {stats['league_structures']} league structures, {stats['team_aliases']} team aliases")
if args.dry_run:
print("[DRY RUN - nothing imported]")
print()

View File

@@ -0,0 +1,227 @@
[
{
"id": "mlb_league",
"sport": "MLB",
"type": "league",
"name": "Major League Baseball",
"abbreviation": "MLB",
"parent_id": null,
"display_order": 0
},
{
"id": "mlb_al",
"sport": "MLB",
"type": "conference",
"name": "American League",
"abbreviation": "AL",
"parent_id": "mlb_league",
"display_order": 1
},
{
"id": "mlb_nl",
"sport": "MLB",
"type": "conference",
"name": "National League",
"abbreviation": "NL",
"parent_id": "mlb_league",
"display_order": 2
},
{
"id": "mlb_al_east",
"sport": "MLB",
"type": "division",
"name": "AL East",
"abbreviation": null,
"parent_id": "mlb_al",
"display_order": 3
},
{
"id": "mlb_al_central",
"sport": "MLB",
"type": "division",
"name": "AL Central",
"abbreviation": null,
"parent_id": "mlb_al",
"display_order": 4
},
{
"id": "mlb_al_west",
"sport": "MLB",
"type": "division",
"name": "AL West",
"abbreviation": null,
"parent_id": "mlb_al",
"display_order": 5
},
{
"id": "mlb_nl_east",
"sport": "MLB",
"type": "division",
"name": "NL East",
"abbreviation": null,
"parent_id": "mlb_nl",
"display_order": 6
},
{
"id": "mlb_nl_central",
"sport": "MLB",
"type": "division",
"name": "NL Central",
"abbreviation": null,
"parent_id": "mlb_nl",
"display_order": 7
},
{
"id": "mlb_nl_west",
"sport": "MLB",
"type": "division",
"name": "NL West",
"abbreviation": null,
"parent_id": "mlb_nl",
"display_order": 8
},
{
"id": "nba_league",
"sport": "NBA",
"type": "league",
"name": "National Basketball Association",
"abbreviation": "NBA",
"parent_id": null,
"display_order": 9
},
{
"id": "nba_eastern",
"sport": "NBA",
"type": "conference",
"name": "Eastern Conference",
"abbreviation": "East",
"parent_id": "nba_league",
"display_order": 10
},
{
"id": "nba_western",
"sport": "NBA",
"type": "conference",
"name": "Western Conference",
"abbreviation": "West",
"parent_id": "nba_league",
"display_order": 11
},
{
"id": "nba_atlantic",
"sport": "NBA",
"type": "division",
"name": "Atlantic",
"abbreviation": null,
"parent_id": "nba_eastern",
"display_order": 12
},
{
"id": "nba_central",
"sport": "NBA",
"type": "division",
"name": "Central",
"abbreviation": null,
"parent_id": "nba_eastern",
"display_order": 13
},
{
"id": "nba_southeast",
"sport": "NBA",
"type": "division",
"name": "Southeast",
"abbreviation": null,
"parent_id": "nba_eastern",
"display_order": 14
},
{
"id": "nba_northwest",
"sport": "NBA",
"type": "division",
"name": "Northwest",
"abbreviation": null,
"parent_id": "nba_western",
"display_order": 15
},
{
"id": "nba_pacific",
"sport": "NBA",
"type": "division",
"name": "Pacific",
"abbreviation": null,
"parent_id": "nba_western",
"display_order": 16
},
{
"id": "nba_southwest",
"sport": "NBA",
"type": "division",
"name": "Southwest",
"abbreviation": null,
"parent_id": "nba_western",
"display_order": 17
},
{
"id": "nhl_league",
"sport": "NHL",
"type": "league",
"name": "National Hockey League",
"abbreviation": "NHL",
"parent_id": null,
"display_order": 18
},
{
"id": "nhl_eastern",
"sport": "NHL",
"type": "conference",
"name": "Eastern Conference",
"abbreviation": "East",
"parent_id": "nhl_league",
"display_order": 19
},
{
"id": "nhl_western",
"sport": "NHL",
"type": "conference",
"name": "Western Conference",
"abbreviation": "West",
"parent_id": "nhl_league",
"display_order": 20
},
{
"id": "nhl_atlantic",
"sport": "NHL",
"type": "division",
"name": "Atlantic",
"abbreviation": null,
"parent_id": "nhl_eastern",
"display_order": 21
},
{
"id": "nhl_metropolitan",
"sport": "NHL",
"type": "division",
"name": "Metropolitan",
"abbreviation": null,
"parent_id": "nhl_eastern",
"display_order": 22
},
{
"id": "nhl_central",
"sport": "NHL",
"type": "division",
"name": "Central",
"abbreviation": null,
"parent_id": "nhl_western",
"display_order": 23
},
{
"id": "nhl_pacific",
"sport": "NHL",
"type": "division",
"name": "Pacific",
"abbreviation": null,
"parent_id": "nhl_western",
"display_order": 24
}
]

View File

@@ -0,0 +1,610 @@
[
{
"id": "alias_mlb_1",
"team_canonical_id": "team_mlb_wsn",
"alias_type": "name",
"alias_value": "Montreal Expos",
"valid_from": "1969-01-01",
"valid_until": "2004-12-31"
},
{
"id": "alias_mlb_2",
"team_canonical_id": "team_mlb_wsn",
"alias_type": "abbreviation",
"alias_value": "MON",
"valid_from": "1969-01-01",
"valid_until": "2004-12-31"
},
{
"id": "alias_mlb_3",
"team_canonical_id": "team_mlb_wsn",
"alias_type": "city",
"alias_value": "Montreal",
"valid_from": "1969-01-01",
"valid_until": "2004-12-31"
},
{
"id": "alias_mlb_4",
"team_canonical_id": "team_mlb_oak",
"alias_type": "name",
"alias_value": "Kansas City Athletics",
"valid_from": "1955-01-01",
"valid_until": "1967-12-31"
},
{
"id": "alias_mlb_5",
"team_canonical_id": "team_mlb_oak",
"alias_type": "abbreviation",
"alias_value": "KCA",
"valid_from": "1955-01-01",
"valid_until": "1967-12-31"
},
{
"id": "alias_mlb_6",
"team_canonical_id": "team_mlb_oak",
"alias_type": "city",
"alias_value": "Kansas City",
"valid_from": "1955-01-01",
"valid_until": "1967-12-31"
},
{
"id": "alias_mlb_7",
"team_canonical_id": "team_mlb_oak",
"alias_type": "name",
"alias_value": "Philadelphia Athletics",
"valid_from": "1901-01-01",
"valid_until": "1954-12-31"
},
{
"id": "alias_mlb_8",
"team_canonical_id": "team_mlb_oak",
"alias_type": "abbreviation",
"alias_value": "PHA",
"valid_from": "1901-01-01",
"valid_until": "1954-12-31"
},
{
"id": "alias_mlb_9",
"team_canonical_id": "team_mlb_oak",
"alias_type": "city",
"alias_value": "Philadelphia",
"valid_from": "1901-01-01",
"valid_until": "1954-12-31"
},
{
"id": "alias_mlb_10",
"team_canonical_id": "team_mlb_cle",
"alias_type": "name",
"alias_value": "Cleveland Indians",
"valid_from": "1915-01-01",
"valid_until": "2021-12-31"
},
{
"id": "alias_mlb_11",
"team_canonical_id": "team_mlb_tbr",
"alias_type": "name",
"alias_value": "Tampa Bay Devil Rays",
"valid_from": "1998-01-01",
"valid_until": "2007-12-31"
},
{
"id": "alias_mlb_12",
"team_canonical_id": "team_mlb_mia",
"alias_type": "name",
"alias_value": "Florida Marlins",
"valid_from": "1993-01-01",
"valid_until": "2011-12-31"
},
{
"id": "alias_mlb_13",
"team_canonical_id": "team_mlb_mia",
"alias_type": "city",
"alias_value": "Florida",
"valid_from": "1993-01-01",
"valid_until": "2011-12-31"
},
{
"id": "alias_mlb_14",
"team_canonical_id": "team_mlb_laa",
"alias_type": "name",
"alias_value": "Anaheim Angels",
"valid_from": "1997-01-01",
"valid_until": "2004-12-31"
},
{
"id": "alias_mlb_15",
"team_canonical_id": "team_mlb_laa",
"alias_type": "name",
"alias_value": "Los Angeles Angels of Anaheim",
"valid_from": "2005-01-01",
"valid_until": "2015-12-31"
},
{
"id": "alias_mlb_16",
"team_canonical_id": "team_mlb_laa",
"alias_type": "name",
"alias_value": "California Angels",
"valid_from": "1965-01-01",
"valid_until": "1996-12-31"
},
{
"id": "alias_mlb_17",
"team_canonical_id": "team_mlb_tex",
"alias_type": "name",
"alias_value": "Washington Senators",
"valid_from": "1961-01-01",
"valid_until": "1971-12-31"
},
{
"id": "alias_mlb_18",
"team_canonical_id": "team_mlb_tex",
"alias_type": "abbreviation",
"alias_value": "WS2",
"valid_from": "1961-01-01",
"valid_until": "1971-12-31"
},
{
"id": "alias_mlb_19",
"team_canonical_id": "team_mlb_tex",
"alias_type": "city",
"alias_value": "Washington",
"valid_from": "1961-01-01",
"valid_until": "1971-12-31"
},
{
"id": "alias_mlb_20",
"team_canonical_id": "team_mlb_mil",
"alias_type": "name",
"alias_value": "Seattle Pilots",
"valid_from": "1969-01-01",
"valid_until": "1969-12-31"
},
{
"id": "alias_mlb_21",
"team_canonical_id": "team_mlb_mil",
"alias_type": "abbreviation",
"alias_value": "SEP",
"valid_from": "1969-01-01",
"valid_until": "1969-12-31"
},
{
"id": "alias_mlb_22",
"team_canonical_id": "team_mlb_mil",
"alias_type": "city",
"alias_value": "Seattle",
"valid_from": "1969-01-01",
"valid_until": "1969-12-31"
},
{
"id": "alias_mlb_23",
"team_canonical_id": "team_mlb_hou",
"alias_type": "name",
"alias_value": "Houston Colt .45s",
"valid_from": "1962-01-01",
"valid_until": "1964-12-31"
},
{
"id": "alias_nba_24",
"team_canonical_id": "team_nba_brk",
"alias_type": "name",
"alias_value": "New Jersey Nets",
"valid_from": "1977-01-01",
"valid_until": "2012-04-30"
},
{
"id": "alias_nba_25",
"team_canonical_id": "team_nba_brk",
"alias_type": "abbreviation",
"alias_value": "NJN",
"valid_from": "1977-01-01",
"valid_until": "2012-04-30"
},
{
"id": "alias_nba_26",
"team_canonical_id": "team_nba_brk",
"alias_type": "city",
"alias_value": "New Jersey",
"valid_from": "1977-01-01",
"valid_until": "2012-04-30"
},
{
"id": "alias_nba_27",
"team_canonical_id": "team_nba_brk",
"alias_type": "name",
"alias_value": "New York Nets",
"valid_from": "1968-01-01",
"valid_until": "1977-12-31"
},
{
"id": "alias_nba_28",
"team_canonical_id": "team_nba_okc",
"alias_type": "name",
"alias_value": "Seattle SuperSonics",
"valid_from": "1967-01-01",
"valid_until": "2008-07-01"
},
{
"id": "alias_nba_29",
"team_canonical_id": "team_nba_okc",
"alias_type": "abbreviation",
"alias_value": "SEA",
"valid_from": "1967-01-01",
"valid_until": "2008-07-01"
},
{
"id": "alias_nba_30",
"team_canonical_id": "team_nba_okc",
"alias_type": "city",
"alias_value": "Seattle",
"valid_from": "1967-01-01",
"valid_until": "2008-07-01"
},
{
"id": "alias_nba_31",
"team_canonical_id": "team_nba_mem",
"alias_type": "name",
"alias_value": "Vancouver Grizzlies",
"valid_from": "1995-01-01",
"valid_until": "2001-05-31"
},
{
"id": "alias_nba_32",
"team_canonical_id": "team_nba_mem",
"alias_type": "abbreviation",
"alias_value": "VAN",
"valid_from": "1995-01-01",
"valid_until": "2001-05-31"
},
{
"id": "alias_nba_33",
"team_canonical_id": "team_nba_mem",
"alias_type": "city",
"alias_value": "Vancouver",
"valid_from": "1995-01-01",
"valid_until": "2001-05-31"
},
{
"id": "alias_nba_34",
"team_canonical_id": "team_nba_nop",
"alias_type": "name",
"alias_value": "New Orleans Hornets",
"valid_from": "2002-01-01",
"valid_until": "2013-04-30"
},
{
"id": "alias_nba_35",
"team_canonical_id": "team_nba_nop",
"alias_type": "abbreviation",
"alias_value": "NOH",
"valid_from": "2002-01-01",
"valid_until": "2013-04-30"
},
{
"id": "alias_nba_36",
"team_canonical_id": "team_nba_nop",
"alias_type": "name",
"alias_value": "New Orleans/Oklahoma City Hornets",
"valid_from": "2005-01-01",
"valid_until": "2007-12-31"
},
{
"id": "alias_nba_37",
"team_canonical_id": "team_nba_cho",
"alias_type": "name",
"alias_value": "Charlotte Bobcats",
"valid_from": "2004-01-01",
"valid_until": "2014-04-30"
},
{
"id": "alias_nba_38",
"team_canonical_id": "team_nba_cho",
"alias_type": "abbreviation",
"alias_value": "CHA",
"valid_from": "2004-01-01",
"valid_until": "2014-04-30"
},
{
"id": "alias_nba_39",
"team_canonical_id": "team_nba_was",
"alias_type": "name",
"alias_value": "Washington Bullets",
"valid_from": "1974-01-01",
"valid_until": "1997-05-31"
},
{
"id": "alias_nba_40",
"team_canonical_id": "team_nba_was",
"alias_type": "name",
"alias_value": "Capital Bullets",
"valid_from": "1973-01-01",
"valid_until": "1973-12-31"
},
{
"id": "alias_nba_41",
"team_canonical_id": "team_nba_was",
"alias_type": "name",
"alias_value": "Baltimore Bullets",
"valid_from": "1963-01-01",
"valid_until": "1972-12-31"
},
{
"id": "alias_nba_42",
"team_canonical_id": "team_nba_lac",
"alias_type": "name",
"alias_value": "San Diego Clippers",
"valid_from": "1978-01-01",
"valid_until": "1984-05-31"
},
{
"id": "alias_nba_43",
"team_canonical_id": "team_nba_lac",
"alias_type": "abbreviation",
"alias_value": "SDC",
"valid_from": "1978-01-01",
"valid_until": "1984-05-31"
},
{
"id": "alias_nba_44",
"team_canonical_id": "team_nba_lac",
"alias_type": "city",
"alias_value": "San Diego",
"valid_from": "1978-01-01",
"valid_until": "1984-05-31"
},
{
"id": "alias_nba_45",
"team_canonical_id": "team_nba_lac",
"alias_type": "name",
"alias_value": "Buffalo Braves",
"valid_from": "1970-01-01",
"valid_until": "1978-05-31"
},
{
"id": "alias_nba_46",
"team_canonical_id": "team_nba_lac",
"alias_type": "abbreviation",
"alias_value": "BUF",
"valid_from": "1970-01-01",
"valid_until": "1978-05-31"
},
{
"id": "alias_nba_47",
"team_canonical_id": "team_nba_lac",
"alias_type": "city",
"alias_value": "Buffalo",
"valid_from": "1970-01-01",
"valid_until": "1978-05-31"
},
{
"id": "alias_nba_48",
"team_canonical_id": "team_nba_sac",
"alias_type": "name",
"alias_value": "Kansas City Kings",
"valid_from": "1975-01-01",
"valid_until": "1985-05-31"
},
{
"id": "alias_nba_49",
"team_canonical_id": "team_nba_sac",
"alias_type": "abbreviation",
"alias_value": "KCK",
"valid_from": "1975-01-01",
"valid_until": "1985-05-31"
},
{
"id": "alias_nba_50",
"team_canonical_id": "team_nba_sac",
"alias_type": "city",
"alias_value": "Kansas City",
"valid_from": "1975-01-01",
"valid_until": "1985-05-31"
},
{
"id": "alias_nba_51",
"team_canonical_id": "team_nba_uta",
"alias_type": "name",
"alias_value": "New Orleans Jazz",
"valid_from": "1974-01-01",
"valid_until": "1979-05-31"
},
{
"id": "alias_nba_52",
"team_canonical_id": "team_nba_uta",
"alias_type": "city",
"alias_value": "New Orleans",
"valid_from": "1974-01-01",
"valid_until": "1979-05-31"
},
{
"id": "alias_nhl_53",
"team_canonical_id": "team_nhl_ari",
"alias_type": "name",
"alias_value": "Arizona Coyotes",
"valid_from": "2014-01-01",
"valid_until": "2024-04-30"
},
{
"id": "alias_nhl_54",
"team_canonical_id": "team_nhl_ari",
"alias_type": "name",
"alias_value": "Phoenix Coyotes",
"valid_from": "1996-01-01",
"valid_until": "2013-12-31"
},
{
"id": "alias_nhl_55",
"team_canonical_id": "team_nhl_ari",
"alias_type": "abbreviation",
"alias_value": "PHX",
"valid_from": "1996-01-01",
"valid_until": "2013-12-31"
},
{
"id": "alias_nhl_56",
"team_canonical_id": "team_nhl_ari",
"alias_type": "city",
"alias_value": "Phoenix",
"valid_from": "1996-01-01",
"valid_until": "2013-12-31"
},
{
"id": "alias_nhl_57",
"team_canonical_id": "team_nhl_ari",
"alias_type": "name",
"alias_value": "Winnipeg Jets",
"valid_from": "1979-01-01",
"valid_until": "1996-05-31"
},
{
"id": "alias_nhl_58",
"team_canonical_id": "team_nhl_car",
"alias_type": "name",
"alias_value": "Hartford Whalers",
"valid_from": "1979-01-01",
"valid_until": "1997-05-31"
},
{
"id": "alias_nhl_59",
"team_canonical_id": "team_nhl_car",
"alias_type": "abbreviation",
"alias_value": "HFD",
"valid_from": "1979-01-01",
"valid_until": "1997-05-31"
},
{
"id": "alias_nhl_60",
"team_canonical_id": "team_nhl_car",
"alias_type": "city",
"alias_value": "Hartford",
"valid_from": "1979-01-01",
"valid_until": "1997-05-31"
},
{
"id": "alias_nhl_61",
"team_canonical_id": "team_nhl_col",
"alias_type": "name",
"alias_value": "Quebec Nordiques",
"valid_from": "1979-01-01",
"valid_until": "1995-05-31"
},
{
"id": "alias_nhl_62",
"team_canonical_id": "team_nhl_col",
"alias_type": "abbreviation",
"alias_value": "QUE",
"valid_from": "1979-01-01",
"valid_until": "1995-05-31"
},
{
"id": "alias_nhl_63",
"team_canonical_id": "team_nhl_col",
"alias_type": "city",
"alias_value": "Quebec",
"valid_from": "1979-01-01",
"valid_until": "1995-05-31"
},
{
"id": "alias_nhl_64",
"team_canonical_id": "team_nhl_dal",
"alias_type": "name",
"alias_value": "Minnesota North Stars",
"valid_from": "1967-01-01",
"valid_until": "1993-05-31"
},
{
"id": "alias_nhl_65",
"team_canonical_id": "team_nhl_dal",
"alias_type": "abbreviation",
"alias_value": "MNS",
"valid_from": "1967-01-01",
"valid_until": "1993-05-31"
},
{
"id": "alias_nhl_66",
"team_canonical_id": "team_nhl_dal",
"alias_type": "city",
"alias_value": "Minnesota",
"valid_from": "1967-01-01",
"valid_until": "1993-05-31"
},
{
"id": "alias_nhl_67",
"team_canonical_id": "team_nhl_njd",
"alias_type": "name",
"alias_value": "Colorado Rockies",
"valid_from": "1976-01-01",
"valid_until": "1982-05-31"
},
{
"id": "alias_nhl_68",
"team_canonical_id": "team_nhl_njd",
"alias_type": "abbreviation",
"alias_value": "CLR",
"valid_from": "1976-01-01",
"valid_until": "1982-05-31"
},
{
"id": "alias_nhl_69",
"team_canonical_id": "team_nhl_njd",
"alias_type": "city",
"alias_value": "Colorado",
"valid_from": "1976-01-01",
"valid_until": "1982-05-31"
},
{
"id": "alias_nhl_70",
"team_canonical_id": "team_nhl_njd",
"alias_type": "name",
"alias_value": "Kansas City Scouts",
"valid_from": "1974-01-01",
"valid_until": "1976-05-31"
},
{
"id": "alias_nhl_71",
"team_canonical_id": "team_nhl_njd",
"alias_type": "abbreviation",
"alias_value": "KCS",
"valid_from": "1974-01-01",
"valid_until": "1976-05-31"
},
{
"id": "alias_nhl_72",
"team_canonical_id": "team_nhl_njd",
"alias_type": "city",
"alias_value": "Kansas City",
"valid_from": "1974-01-01",
"valid_until": "1976-05-31"
},
{
"id": "alias_nhl_73",
"team_canonical_id": "team_nhl_wpg",
"alias_type": "name",
"alias_value": "Atlanta Thrashers",
"valid_from": "1999-01-01",
"valid_until": "2011-05-31"
},
{
"id": "alias_nhl_74",
"team_canonical_id": "team_nhl_wpg",
"alias_type": "abbreviation",
"alias_value": "ATL",
"valid_from": "1999-01-01",
"valid_until": "2011-05-31"
},
{
"id": "alias_nhl_75",
"team_canonical_id": "team_nhl_wpg",
"alias_type": "city",
"alias_value": "Atlanta",
"valid_from": "1999-01-01",
"valid_until": "2011-05-31"
},
{
"id": "alias_nhl_76",
"team_canonical_id": "team_nhl_fla",
"alias_type": "city",
"alias_value": "Miami",
"valid_from": "1993-01-01",
"valid_until": "1998-12-31"
}
]

View File

@@ -0,0 +1,405 @@
#!/usr/bin/env python3
"""
Generate Canonical Data for SportsTime App
==========================================
Generates team_aliases.json and league_structure.json from team mappings.
Usage:
python generate_canonical_data.py
python generate_canonical_data.py --output ./data
"""
import argparse
import json
from datetime import datetime
from pathlib import Path
# =============================================================================
# LEAGUE STRUCTURE
# =============================================================================
MLB_STRUCTURE = {
"leagues": [
{"id": "mlb_al", "name": "American League", "abbreviation": "AL"},
{"id": "mlb_nl", "name": "National League", "abbreviation": "NL"},
],
"divisions": [
# American League
{"id": "mlb_al_east", "name": "AL East", "parent_id": "mlb_al", "teams": ["NYY", "BOS", "TOR", "BAL", "TBR"]},
{"id": "mlb_al_central", "name": "AL Central", "parent_id": "mlb_al", "teams": ["CLE", "DET", "MIN", "CHW", "KCR"]},
{"id": "mlb_al_west", "name": "AL West", "parent_id": "mlb_al", "teams": ["HOU", "SEA", "TEX", "LAA", "OAK"]},
# National League
{"id": "mlb_nl_east", "name": "NL East", "parent_id": "mlb_nl", "teams": ["ATL", "PHI", "NYM", "MIA", "WSN"]},
{"id": "mlb_nl_central", "name": "NL Central", "parent_id": "mlb_nl", "teams": ["MIL", "CHC", "STL", "PIT", "CIN"]},
{"id": "mlb_nl_west", "name": "NL West", "parent_id": "mlb_nl", "teams": ["LAD", "ARI", "SDP", "SFG", "COL"]},
]
}
NBA_STRUCTURE = {
"conferences": [
{"id": "nba_eastern", "name": "Eastern Conference", "abbreviation": "East"},
{"id": "nba_western", "name": "Western Conference", "abbreviation": "West"},
],
"divisions": [
# Eastern Conference
{"id": "nba_atlantic", "name": "Atlantic", "parent_id": "nba_eastern", "teams": ["BOS", "BRK", "NYK", "PHI", "TOR"]},
{"id": "nba_central", "name": "Central", "parent_id": "nba_eastern", "teams": ["CHI", "CLE", "DET", "IND", "MIL"]},
{"id": "nba_southeast", "name": "Southeast", "parent_id": "nba_eastern", "teams": ["ATL", "CHO", "MIA", "ORL", "WAS"]},
# Western Conference
{"id": "nba_northwest", "name": "Northwest", "parent_id": "nba_western", "teams": ["DEN", "MIN", "OKC", "POR", "UTA"]},
{"id": "nba_pacific", "name": "Pacific", "parent_id": "nba_western", "teams": ["GSW", "LAC", "LAL", "PHO", "SAC"]},
{"id": "nba_southwest", "name": "Southwest", "parent_id": "nba_western", "teams": ["DAL", "HOU", "MEM", "NOP", "SAS"]},
]
}
NHL_STRUCTURE = {
"conferences": [
{"id": "nhl_eastern", "name": "Eastern Conference", "abbreviation": "East"},
{"id": "nhl_western", "name": "Western Conference", "abbreviation": "West"},
],
"divisions": [
# Eastern Conference
{"id": "nhl_atlantic", "name": "Atlantic", "parent_id": "nhl_eastern", "teams": ["BOS", "BUF", "DET", "FLA", "MTL", "OTT", "TBL", "TOR"]},
{"id": "nhl_metropolitan", "name": "Metropolitan", "parent_id": "nhl_eastern", "teams": ["CAR", "CBJ", "NJD", "NYI", "NYR", "PHI", "PIT", "WSH"]},
# Western Conference
{"id": "nhl_central", "name": "Central", "parent_id": "nhl_western", "teams": ["ARI", "CHI", "COL", "DAL", "MIN", "NSH", "STL", "WPG"]},
{"id": "nhl_pacific", "name": "Pacific", "parent_id": "nhl_western", "teams": ["ANA", "CGY", "EDM", "LAK", "SEA", "SJS", "VAN", "VGK"]},
]
}
# =============================================================================
# TEAM ALIASES (Historical name changes, relocations, abbreviation changes)
# =============================================================================
# Format: {current_abbrev: [(alias_type, alias_value, valid_from, valid_until), ...]}
MLB_ALIASES = {
# Washington Nationals (formerly Montreal Expos)
"WSN": [
("name", "Montreal Expos", "1969-01-01", "2004-12-31"),
("abbreviation", "MON", "1969-01-01", "2004-12-31"),
("city", "Montreal", "1969-01-01", "2004-12-31"),
],
# Oakland Athletics (moving to Sacramento, formerly in Kansas City and Philadelphia)
"OAK": [
("name", "Kansas City Athletics", "1955-01-01", "1967-12-31"),
("abbreviation", "KCA", "1955-01-01", "1967-12-31"),
("city", "Kansas City", "1955-01-01", "1967-12-31"),
("name", "Philadelphia Athletics", "1901-01-01", "1954-12-31"),
("abbreviation", "PHA", "1901-01-01", "1954-12-31"),
("city", "Philadelphia", "1901-01-01", "1954-12-31"),
],
# Cleveland Guardians (formerly Indians)
"CLE": [
("name", "Cleveland Indians", "1915-01-01", "2021-12-31"),
],
# Tampa Bay Rays (formerly Devil Rays)
"TBR": [
("name", "Tampa Bay Devil Rays", "1998-01-01", "2007-12-31"),
],
# Miami Marlins (formerly Florida Marlins)
"MIA": [
("name", "Florida Marlins", "1993-01-01", "2011-12-31"),
("city", "Florida", "1993-01-01", "2011-12-31"),
],
# Los Angeles Angels (various names)
"LAA": [
("name", "Anaheim Angels", "1997-01-01", "2004-12-31"),
("name", "Los Angeles Angels of Anaheim", "2005-01-01", "2015-12-31"),
("name", "California Angels", "1965-01-01", "1996-12-31"),
],
# Texas Rangers (formerly Washington Senators II)
"TEX": [
("name", "Washington Senators", "1961-01-01", "1971-12-31"),
("abbreviation", "WS2", "1961-01-01", "1971-12-31"),
("city", "Washington", "1961-01-01", "1971-12-31"),
],
# Milwaukee Brewers (briefly Seattle Pilots)
"MIL": [
("name", "Seattle Pilots", "1969-01-01", "1969-12-31"),
("abbreviation", "SEP", "1969-01-01", "1969-12-31"),
("city", "Seattle", "1969-01-01", "1969-12-31"),
],
# Houston Astros (formerly Colt .45s)
"HOU": [
("name", "Houston Colt .45s", "1962-01-01", "1964-12-31"),
],
}
NBA_ALIASES = {
# Brooklyn Nets (formerly New Jersey Nets, New York Nets)
"BRK": [
("name", "New Jersey Nets", "1977-01-01", "2012-04-30"),
("abbreviation", "NJN", "1977-01-01", "2012-04-30"),
("city", "New Jersey", "1977-01-01", "2012-04-30"),
("name", "New York Nets", "1968-01-01", "1977-12-31"),
],
# Oklahoma City Thunder (formerly Seattle SuperSonics)
"OKC": [
("name", "Seattle SuperSonics", "1967-01-01", "2008-07-01"),
("abbreviation", "SEA", "1967-01-01", "2008-07-01"),
("city", "Seattle", "1967-01-01", "2008-07-01"),
],
# Memphis Grizzlies (formerly Vancouver Grizzlies)
"MEM": [
("name", "Vancouver Grizzlies", "1995-01-01", "2001-05-31"),
("abbreviation", "VAN", "1995-01-01", "2001-05-31"),
("city", "Vancouver", "1995-01-01", "2001-05-31"),
],
# New Orleans Pelicans (formerly Hornets, formerly Charlotte Hornets original)
"NOP": [
("name", "New Orleans Hornets", "2002-01-01", "2013-04-30"),
("abbreviation", "NOH", "2002-01-01", "2013-04-30"),
("name", "New Orleans/Oklahoma City Hornets", "2005-01-01", "2007-12-31"),
],
# Charlotte Hornets (current, formerly Bobcats)
"CHO": [
("name", "Charlotte Bobcats", "2004-01-01", "2014-04-30"),
("abbreviation", "CHA", "2004-01-01", "2014-04-30"),
],
# Washington Wizards (formerly Bullets)
"WAS": [
("name", "Washington Bullets", "1974-01-01", "1997-05-31"),
("name", "Capital Bullets", "1973-01-01", "1973-12-31"),
("name", "Baltimore Bullets", "1963-01-01", "1972-12-31"),
],
# Los Angeles Clippers (formerly San Diego, Buffalo)
"LAC": [
("name", "San Diego Clippers", "1978-01-01", "1984-05-31"),
("abbreviation", "SDC", "1978-01-01", "1984-05-31"),
("city", "San Diego", "1978-01-01", "1984-05-31"),
("name", "Buffalo Braves", "1970-01-01", "1978-05-31"),
("abbreviation", "BUF", "1970-01-01", "1978-05-31"),
("city", "Buffalo", "1970-01-01", "1978-05-31"),
],
# Sacramento Kings (formerly Kansas City Kings, etc.)
"SAC": [
("name", "Kansas City Kings", "1975-01-01", "1985-05-31"),
("abbreviation", "KCK", "1975-01-01", "1985-05-31"),
("city", "Kansas City", "1975-01-01", "1985-05-31"),
],
# Utah Jazz (formerly New Orleans Jazz)
"UTA": [
("name", "New Orleans Jazz", "1974-01-01", "1979-05-31"),
("city", "New Orleans", "1974-01-01", "1979-05-31"),
],
}
NHL_ALIASES = {
# Arizona/Utah Hockey Club (formerly Phoenix Coyotes, originally Winnipeg Jets)
"ARI": [
("name", "Arizona Coyotes", "2014-01-01", "2024-04-30"),
("name", "Phoenix Coyotes", "1996-01-01", "2013-12-31"),
("abbreviation", "PHX", "1996-01-01", "2013-12-31"),
("city", "Phoenix", "1996-01-01", "2013-12-31"),
("name", "Winnipeg Jets", "1979-01-01", "1996-05-31"), # Original Jets
],
# Carolina Hurricanes (formerly Hartford Whalers)
"CAR": [
("name", "Hartford Whalers", "1979-01-01", "1997-05-31"),
("abbreviation", "HFD", "1979-01-01", "1997-05-31"),
("city", "Hartford", "1979-01-01", "1997-05-31"),
],
# Colorado Avalanche (formerly Quebec Nordiques)
"COL": [
("name", "Quebec Nordiques", "1979-01-01", "1995-05-31"),
("abbreviation", "QUE", "1979-01-01", "1995-05-31"),
("city", "Quebec", "1979-01-01", "1995-05-31"),
],
# Dallas Stars (formerly Minnesota North Stars)
"DAL": [
("name", "Minnesota North Stars", "1967-01-01", "1993-05-31"),
("abbreviation", "MNS", "1967-01-01", "1993-05-31"),
("city", "Minnesota", "1967-01-01", "1993-05-31"),
],
# New Jersey Devils (formerly Kansas City Scouts, Colorado Rockies)
"NJD": [
("name", "Colorado Rockies", "1976-01-01", "1982-05-31"),
("abbreviation", "CLR", "1976-01-01", "1982-05-31"),
("city", "Colorado", "1976-01-01", "1982-05-31"),
("name", "Kansas City Scouts", "1974-01-01", "1976-05-31"),
("abbreviation", "KCS", "1974-01-01", "1976-05-31"),
("city", "Kansas City", "1974-01-01", "1976-05-31"),
],
# Winnipeg Jets (current, formerly Atlanta Thrashers)
"WPG": [
("name", "Atlanta Thrashers", "1999-01-01", "2011-05-31"),
("abbreviation", "ATL", "1999-01-01", "2011-05-31"),
("city", "Atlanta", "1999-01-01", "2011-05-31"),
],
# Florida Panthers (originally in Miami)
"FLA": [
("city", "Miami", "1993-01-01", "1998-12-31"),
],
# Vegas Golden Knights (no aliases, expansion team)
# Seattle Kraken (no aliases, expansion team)
}
def generate_league_structure() -> list[dict]:
"""Generate league_structure.json data."""
structures = []
order = 0
# MLB
structures.append({
"id": "mlb_league",
"sport": "MLB",
"type": "league",
"name": "Major League Baseball",
"abbreviation": "MLB",
"parent_id": None,
"display_order": order,
})
order += 1
for league in MLB_STRUCTURE["leagues"]:
structures.append({
"id": league["id"],
"sport": "MLB",
"type": "conference", # AL/NL are like conferences
"name": league["name"],
"abbreviation": league["abbreviation"],
"parent_id": "mlb_league",
"display_order": order,
})
order += 1
for div in MLB_STRUCTURE["divisions"]:
structures.append({
"id": div["id"],
"sport": "MLB",
"type": "division",
"name": div["name"],
"abbreviation": None,
"parent_id": div["parent_id"],
"display_order": order,
})
order += 1
# NBA
structures.append({
"id": "nba_league",
"sport": "NBA",
"type": "league",
"name": "National Basketball Association",
"abbreviation": "NBA",
"parent_id": None,
"display_order": order,
})
order += 1
for conf in NBA_STRUCTURE["conferences"]:
structures.append({
"id": conf["id"],
"sport": "NBA",
"type": "conference",
"name": conf["name"],
"abbreviation": conf["abbreviation"],
"parent_id": "nba_league",
"display_order": order,
})
order += 1
for div in NBA_STRUCTURE["divisions"]:
structures.append({
"id": div["id"],
"sport": "NBA",
"type": "division",
"name": div["name"],
"abbreviation": None,
"parent_id": div["parent_id"],
"display_order": order,
})
order += 1
# NHL
structures.append({
"id": "nhl_league",
"sport": "NHL",
"type": "league",
"name": "National Hockey League",
"abbreviation": "NHL",
"parent_id": None,
"display_order": order,
})
order += 1
for conf in NHL_STRUCTURE["conferences"]:
structures.append({
"id": conf["id"],
"sport": "NHL",
"type": "conference",
"name": conf["name"],
"abbreviation": conf["abbreviation"],
"parent_id": "nhl_league",
"display_order": order,
})
order += 1
for div in NHL_STRUCTURE["divisions"]:
structures.append({
"id": div["id"],
"sport": "NHL",
"type": "division",
"name": div["name"],
"abbreviation": None,
"parent_id": div["parent_id"],
"display_order": order,
})
order += 1
return structures
def generate_team_aliases() -> list[dict]:
"""Generate team_aliases.json data."""
aliases = []
alias_id = 1
for sport, sport_aliases in [("MLB", MLB_ALIASES), ("NBA", NBA_ALIASES), ("NHL", NHL_ALIASES)]:
for current_abbrev, alias_list in sport_aliases.items():
team_canonical_id = f"team_{sport.lower()}_{current_abbrev.lower()}"
for alias_type, alias_value, valid_from, valid_until in alias_list:
aliases.append({
"id": f"alias_{sport.lower()}_{alias_id}",
"team_canonical_id": team_canonical_id,
"alias_type": alias_type,
"alias_value": alias_value,
"valid_from": valid_from,
"valid_until": valid_until,
})
alias_id += 1
return aliases
def main():
parser = argparse.ArgumentParser(description='Generate canonical data JSON files')
parser.add_argument('--output', type=str, default='./data', help='Output directory')
args = parser.parse_args()
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
# Generate league structure
print("Generating league_structure.json...")
league_structure = generate_league_structure()
with open(output_dir / 'league_structure.json', 'w') as f:
json.dump(league_structure, f, indent=2)
print(f" Created {len(league_structure)} structure entries")
# Generate team aliases
print("Generating team_aliases.json...")
team_aliases = generate_team_aliases()
with open(output_dir / 'team_aliases.json', 'w') as f:
json.dump(team_aliases, f, indent=2)
print(f" Created {len(team_aliases)} alias entries")
print(f"\nFiles written to {output_dir}")
if __name__ == '__main__':
main()