From 76e28614ae4629ebdaeaaed7dfe8e52a8bc5f2cb Mon Sep 17 00:00:00 2001 From: Shawn Date: Sat, 23 May 2026 19:50:16 -0400 Subject: [PATCH] feat: tag Disney assets with park/resort codes and display names - Script: scripts/tag_disney_properties.py (supports dry-run + --apply) - Maps 112 Disney location IDs to park/resort/office/other codes - Updated 994 assets: disney_park column + appended display name with emoji - Added disney_park column to locations table, populated 112 rows - Covers all 364 locations via case-insensitive substring matching --- scripts/tag_disney_properties.py | 336 +++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 scripts/tag_disney_properties.py diff --git a/scripts/tag_disney_properties.py b/scripts/tag_disney_properties.py new file mode 100644 index 0000000..663d379 --- /dev/null +++ b/scripts/tag_disney_properties.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +"""Tag Disney assets with their park/resort code and update display names. + +Step 1: Map each location name → disney_park code + display name +Step 2: Update assets (disney_park + append display name) +Step 3: Update locations (add disney_park column if missing, set values) + +Usage: + python scripts/tag_disney_properties.py # dry-run + python scripts/tag_disney_properties.py --apply # actually update the DB +""" + +import argparse +import sqlite3 +import sys +import os + +# ── Disney property mapping ────────────────────────────────────────────────── +# Each location name pattern is mapped to (disney_park_code, display_name, emoji) +# Patterns are checked as case-insensitive substring matches, ordered by priority. + +PARK_MAPPING = [ + # --- Resorts (specific before generic! order matters) --- + ("D-All Star Movie", "resort", "🏨 All Star Movies"), + ("D-All Star Music", "resort", "🏨 All Star Music"), + ("D-All Star Sports", "resort", "🏨 All Star Sports"), + ("D-ALL STAR", "resort", "🏨 All Star"), + ("ART OF ANIMATION", "resort", "🏨 Art of Animation"), + ("D-ART OF ANIMATION", "resort", "🏨 Art of Animation"), + ("Animal Kngdm LODGE", "resort", "🏨 Animal Kingdom Lodge"), + ("Animal Kngdm Lodge", "resort", "🏨 Animal Kingdom Lodge"), + ("BAY LAKE TOWER", "resort", "🏨 Bay Lake Tower"), + ("D-BAY LAKE TOWER", "resort", "🏨 Bay Lake Tower"), + ("BLIZZARD BEACH", "resort", "🏨 Blizzard Beach"), + ("D-BLIZZARD BEACH", "resort", "🏨 Blizzard Beach"), + ("Boardwalk CAST", "resort", "🏨 Boardwalk"), + ("Boardwalk GUEST", "resort", "🏨 Boardwalk"), + ("D-Boardwalk", "resort", "🏨 Boardwalk"), + ("CARIBBEAN BEACH", "resort", "🏨 Caribbean Beach"), + ("D-CARIBBEAN BEACH", "resort", "🏨 Caribbean Beach"), + ("D-Caribbean Beach", "resort", "🏨 Caribbean Beach"), + ("CORONADO SPRINGS", "resort", "🏨 Coronado Springs"), + ("D-CORONADO SPRINGS", "resort", "🏨 Coronado Springs"), + ("ContemporaryHotel", "resort", "🏨 Contemporary"), + ("D-ContemporaryHotel", "resort", "🏨 Contemporary"), + ("FANTASIA GOLF", "resort", "🏨 Fantasia Golf"), + ("D-FANTASIA GOLF", "resort", "🏨 Fantasia Golf"), + ("FORT WILDERNESS", "resort", "🏨 Fort Wilderness"), + ("D-FORT WILDERNESS", "resort", "🏨 Fort Wilderness"), + ("GRAN DESTINO", "resort", "🏨 Gran Destino"), + ("D-GRAN DESTINO", "resort", "🏨 Gran Destino"), + ("D-Gran Destino", "resort", "🏨 Gran Destino"), + ("GRAND FLORIDIAN", "resort", "🏨 Grand Floridian"), + ("D-GRAND FLORIDIAN", "resort", "🏨 Grand Floridian"), + ("D-GRAND FLORIDIAN", "resort", "🏨 Grand Floridian"), + ("Island Tower Polynesian", "resort", "🏨 Island Tower Polynesian"), + ("D-Island Tower", "resort", "🏨 Island Tower"), + ("Kidani Village", "resort", "🏨 Kidani Village"), + ("D-Kidani Village", "resort", "🏨 Kidani Village"), + ("OLD KEY WEST", "resort", "🏨 Old Key West"), + ("D-OLD KEY WEST", "resort", "🏨 Old Key West"), + ("POLYNESIAN RESORT", "resort", "🏨 Polynesian Resort"), + ("D-POLYNESIAN RESORT", "resort", "🏨 Polynesian Resort"), + ("POP CENTURY", "resort", "🏨 Pop Century"), + ("D-POP CENTURY", "resort", "🏨 Pop Century"), + ("PORT ORLEANS FrQt", "resort", "🏨 Port Orleans French Quarter"), + ("D-Port Orleans FrQt", "resort", "🏨 Port Orleans French Quarter"), + ("Port Orleans Rvsd", "resort", "🏨 Port Orleans Riverside"), + ("D-Port Orleans Rvsd", "resort", "🏨 Port Orleans Riverside"), + ("RIVIERA RESORT", "resort", "🏨 Riviera Resort"), + ("D-RIVIERA RESORT", "resort", "🏨 Riviera Resort"), + ("Saratoga Springs", "resort", "🏨 Saratoga Springs"), + ("D-Saratoga Springs", "resort", "🏨 Saratoga Springs"), + ("Treehouse Villas", "resort", "🏨 Treehouse Villas"), + ("D-Treehouse Villas", "resort", "🏨 Treehouse Villas"), + ("TYPHOON LAGOON CAST", "resort", "🏨 Typhoon Lagoon"), + ("D-TYPHOON LAGOON", "resort", "🏨 Typhoon Lagoon"), + ("WILDERNESS LODGE", "resort", "🏨 Wilderness Lodge"), + ("D-WILDERNESS LODGE", "resort", "🏨 Wilderness Lodge"), + ("Winter Summerland", "resort", "🏨 Winter Summerland"), + ("D-Winter Summerland", "resort", "🏨 Winter Summerland"), + ("YACHT AND BEACH", "resort", "🏨 Yacht and Beach"), + ("D-YACHT AND BEACH", "resort", "🏨 Yacht and Beach"), +] + +# ── Office mapping ──────────────────────────────────────────────────────────── +OFFICE_MAPPING = [ + ("DRC Cast", "office", "🏢 DRC"), +] + +# ── Other mapping ───────────────────────────────────────────────────────────── +OTHER_MAPPING = [ + ("DISNEY VENDING", "other", "📍 Disney Vending"), + ("DISNEY WORLD SS", "other", "📍 Support Services"), + ("D-ESPN", "other", "📍 ESPN"), + ("ESPN", "other", "📍 ESPN"), + ("WIDE WORLD SPORTS", "other", "📍 Wide World of Sports"), + ("D-WIDE WORLD SPORTS", "other", "📍 Wide World of Sports"), + ("Celebration", "other", "📍 Celebration"), + ("D-Celebration", "other", "📍 Celebration"), +] + +# ── Build the full mapping ──────────────────────────────────────────────────── +# Preprocess: lowercase all patterns for case-insensitive matching +# Order matters: more specific patterns come first + +def _normalise(name): + """Strip D- prefix checks and normalise whitespace.""" + return ' '.join(name.split()).upper() + +def _build_location_mapping(): + """Build dict: location_name_upper -> (disney_park_code, display_name).""" + mapping = {} + + # Park patterns - we'll handle these specially since we're doing substring matching + park_patterns = [ + # (substring_check_fn, code, display_name) + ] + + # Resort patterns + resort_patterns = [] + for substr, code, display in PARK_MAPPING: + if substr is None: + continue + resort_patterns.append((substr.upper(), code, display)) + + office_patterns = [] + for substr, code, display in OFFICE_MAPPING: + office_patterns.append((substr.upper(), code, display)) + + other_patterns = [] + for substr, code, display in OTHER_MAPPING: + other_patterns.append((substr.upper(), code, display)) + + # Park detection patterns (ordered by specificity) + # These need to be checked BEFORE the generic patterns + park_checks = [ + # Check for specific parks first + (lambda n: "MAGIC KINGDOM" in n, "magic-kingdom", "🏰 Magic Kingdom"), + (lambda n: n.startswith("D-EPCOT"), "epcot", "🌍 Epcot"), + (lambda n: "EPCOT CAST" in n, "epcot", "🌍 Epcot"), + (lambda n: "EPCOT GUEST" in n, "epcot", "🌍 Epcot"), + (lambda n: "EPCOT" in n and n.startswith("D-"), "epcot", "🌍 Epcot"), + (lambda n: "HOLLYWOOD STUDIOS" in n, "hollywood-studios", "🎬 Hollywood Studios"), + (lambda n: "ANIMAL KINGDOM" in n and "LODGE" not in n and "CAST" in n, "animal-kingdom", "🌿 Animal Kingdom"), + (lambda n: "ANIMAL KNGDM" in n and "LODGE" not in n and "LODG" not in n, "animal-kingdom", "🌿 Animal Kingdom"), + (lambda n: "ANIMAL KINGDOM GUEST" in n, "animal-kingdom", "🌿 Animal Kingdom"), + (lambda n: "DISNEY SPRINGS" in n, "disney-springs", "🛍️ Disney Springs"), + ] + + def classify(name): + """Return (code, display) for a Disney location name, or None.""" + n = _normalise(name) + + # 1) Check parks first (most specific) + for check_fn, code, display in park_checks: + if check_fn(n): + return code, display + + # 2) Check office + for substr, code, display in office_patterns: + if substr in n: + return code, display + + # 3) Check resorts + for substr, code, display in resort_patterns: + if substr in n: + return code, display + + # 4) Check other + for substr, code, display in other_patterns: + if substr in n: + return code, display + + return None + + return classify + + +def get_db_path(): + """Find the assets.db path.""" + # Try common locations + candidates = [ + os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets.db"), + "/home/oplabs/projects/canteen-asset-tracker/assets.db", + ] + for p in candidates: + if os.path.exists(p): + return p + raise FileNotFoundError("Could not find assets.db") + + +def main(): + parser = argparse.ArgumentParser(description="Tag Disney assets with park/resort codes") + parser.add_argument("--apply", action="store_true", help="Actually update the database") + args = parser.parse_args() + + classify = _build_location_mapping() + + db_path = get_db_path() + print(f"Database: {db_path}") + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cur = conn.cursor() + + # ── Step 1: Load all Disney locations ──────────────────────────────────── + cur.execute("SELECT id, name FROM locations ORDER BY name") + all_locations = [dict(r) for r in cur.fetchall()] + + # Build: location_id -> (disney_park_code, display_name) + loc_park_map = {} # location_id -> (code, display_name) + loc_display_map = {} # location_id -> display_name (for appending) + unmapped_locations = [] + + for loc in all_locations: + name = loc["name"] + result = classify(name) + if result: + loc_park_map[loc["id"]] = result + else: + # Check if this is a Disney location (starts with D-, DISNEY, DRC, Disney Springs) + n_upper = _normalise(name) + if any(name.startswith(p) for p in ["D-", "DISNEY", "DRC", "Disney Springs"]): + n = _normalise(name) + if n.startswith("D-ANIMAL KINGDOM") and "LODGE" not in n: + loc_park_map[loc["id"]] = ("animal-kingdom", "🌿 Animal Kingdom") + elif n.startswith("D-ANIMAL KNGDM") and "LODGE" not in n: + loc_park_map[loc["id"]] = ("animal-kingdom", "🌿 Animal Kingdom") + else: + unmapped_locations.append(loc) + # Non-Disney locations are ignored + + # ── Step 1b: Print unmapped Disney locations ───────────────────────────── + if unmapped_locations: + print(f"\n⚠️ {len(unmapped_locations)} Disney locations could not be mapped:") + for loc in unmapped_locations: + print(f" ID {loc['id']}: {loc['name']}") + + # ── Step 2: Update assets ──────────────────────────────────────────────── + cur.execute("SELECT id, name, location_id FROM assets WHERE is_disney=1") + disney_assets = [dict(r) for r in cur.fetchall()] + + park_counts = {} + name_updates = [] + name_skipped = [] + + for asset in disney_assets: + loc_id = asset["location_id"] + if loc_id not in loc_park_map: + continue + code, display = loc_park_map[loc_id] + park_counts[display] = park_counts.get(display, 0) + 1 + + # Check if the display name is already appended + current_name = asset["name"] + suffix = f" {display}" + if suffix not in current_name: + new_name = current_name + suffix + name_updates.append((code, new_name, asset["id"])) + else: + name_skipped.append((asset["id"], current_name)) + # Still update the disney_park code even if name already has suffix + name_updates.append((code, current_name, asset["id"])) + + # ── Step 3: Identify locations that need updating ──────────────────────── + # Check if locations has the column + cur.execute("PRAGMA table_info(locations)") + loc_cols = [r["name"] for r in cur.fetchall()] + needs_add_column = "disney_park" not in loc_cols + + # Build set of location IDs that are Disney + disney_loc_ids = set(loc_park_map.keys()) + + print(f"\n{'='*60}") + print(f"MODE: {'APPLY' if args.apply else 'DRY-RUN'} (pass --apply to execute)") + print(f"{'='*60}") + + print(f"\n📊 Summary: {len(disney_assets)} Disney assets, {len(loc_park_map)} mapped locations") + + print(f"\n📋 Assets per park/resort:") + for display, count in sorted(park_counts.items(), key=lambda x: -x[1]): + print(f" {display}: {count} assets") + + print(f"\n📝 Name updates: {len(name_updates)}") + print(f"⏭️ Already tagged (skipped): {len(name_skipped)}") + + # Show a sample + print(f"\n🔍 Sample updates (first 5):") + for i, (code, new_name, aid) in enumerate(name_updates[:5]): + old = next((a["name"] for a in disney_assets if a["id"] == aid), "") + print(f" Asset {aid}:") + print(f" Before: {old}") + print(f" After: {new_name}") + print(f" Park: {code}") + + # ── Execute updates ────────────────────────────────────────────────────── + if args.apply: + print(f"\n{'='*60}") + print("APPLYING CHANGES...") + print(f"{'='*60}") + + # Update assets + updated_assets = 0 + for code, new_name, aid in name_updates: + cur.execute( + "UPDATE assets SET disney_park = ?, name = ? WHERE id = ?", + (code, new_name, aid), + ) + updated_assets += 1 + + # Add column to locations if needed + if needs_add_column: + print(f"\n Adding disney_park column to locations table...") + cur.execute("ALTER TABLE locations ADD COLUMN disney_park TEXT DEFAULT NULL") + + # Update locations + updated_locations = 0 + for loc_id, (code, display) in loc_park_map.items(): + cur.execute( + "UPDATE locations SET disney_park = ? WHERE id = ?", + (code, loc_id), + ) + updated_locations += 1 + + conn.commit() + print(f"\n✅ Done! Updated {updated_assets} assets and {updated_locations} locations.") + else: + print(f"\n💡 Run with --apply to execute these changes.") + if needs_add_column: + print(" (Note: disney_park column will be added to locations table)") + + conn.close() + + +if __name__ == "__main__": + main()