#!/usr/bin/env python3 """ Fetch all SRD 5.2 data from Open5e API and cache as flat JSON files. """ import json import os import sys import time import urllib.request import urllib.error DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data") SRD_FILTER = "document__key__in=srd-2024" SOURCES = { "spells": f"https://api.open5e.com/v2/spells/?limit=100&{SRD_FILTER}", "creatures": f"https://api.open5e.com/v2/creatures/?limit=100&{SRD_FILTER}", "classes": f"https://api.open5e.com/v2/classes/?limit=100&{SRD_FILTER}", "magic-items": "https://api.open5e.com/v2/magicitems/?limit=100", "equipment": f"https://api.open5e.com/v2/items/?limit=100&{SRD_FILTER}", } os.makedirs(DATA_DIR, exist_ok=True) def fetch_page(url: str, retries: int = 3) -> dict: """Fetch a single page from the API with retries.""" for attempt in range(retries): try: req = urllib.request.Request(url, headers={"User-Agent": "Cupcake-DnD-API/1.0"}) with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode()) return data except Exception as e: if attempt < retries - 1: wait = 2 ** attempt print(f" ⚠ Retry {attempt + 1}/{retries} after {wait}s: {e}") time.sleep(wait) else: raise def fetch_all(name: str, first_url: str) -> list: """Fetch all pages of a resource, returning combined results.""" print(f"\nšŸ“¦ Fetching {name}...") all_results = [] url = first_url page = 0 while url: page += 1 print(f" šŸ“„ Page {page}: {url[:80]}...") data = fetch_page(url) results = data.get("results", []) all_results.extend(results) print(f" Got {len(results)} items (total: {len(all_results)})") url = data.get("next") print(f"āœ… {name}: {len(all_results)} total items") return all_results def simplify_spell(s: dict) -> dict: """Extract key fields from a spell.""" return { "key": s.get("key", "").replace("srd-2024_", ""), "name": s.get("name", ""), "level": s.get("level", 0), "school": s.get("school", {}).get("name") if isinstance(s.get("school"), dict) else s.get("school"), "casting_time": s.get("casting_time", ""), "range": s.get("range", ""), "components": {"material": s.get("material"), "verbal": s.get("verbal"), "somatic": s.get("somatic")}, "duration": s.get("duration", ""), "concentration": s.get("concentration", False), "ritual": s.get("ritual", False), "description": s.get("desc", ""), "higher_level": s.get("higher_level", ""), "classes": [c.get("name") for c in (s.get("classes") or [])] if isinstance(s.get("classes"), list) else [], "subclasses": [c.get("name") for c in (s.get("subclasses") or [])] if isinstance(s.get("subclasses"), list) else [], } def simplify_creature(c: dict) -> dict: """Extract key fields from a creature.""" return { "key": c.get("key", ""), "name": c.get("name", ""), "size": c.get("size", {}).get("name") if isinstance(c.get("size"), dict) else c.get("size"), "type": c.get("type", {}).get("name") if isinstance(c.get("type"), dict) else c.get("type"), "alignment": c.get("alignment", ""), "armor_class": c.get("armor_class", 0), "hit_points": c.get("hit_points", 0), "hit_dice": c.get("hit_dice", ""), "speed": c.get("speed", {}), "strength": c.get("strength", 10), "dexterity": c.get("dexterity", 10), "constitution": c.get("constitution", 10), "intelligence": c.get("intelligence", 10), "wisdom": c.get("wisdom", 10), "charisma": c.get("charisma", 10), "saving_throws": c.get("saving_throws", {}), "skills": c.get("skills", {}), "damage_vulnerabilities": c.get("damage_vulnerabilities", []), "damage_resistances": c.get("damage_resistances", []), "damage_immunities": c.get("damage_immunities", []), "condition_immunities": c.get("condition_immunities", []), "senses": c.get("senses", ""), "languages": c.get("languages", ""), "challenge_rating": c.get("challenge_rating", "0"), "xp": c.get("xp", 0), "traits": [{"name": t.get("name"), "desc": t.get("desc")} for t in (c.get("traits") or [])], "actions": [{"name": a.get("name"), "desc": a.get("desc")} for a in (c.get("actions") or [])], "legendary_actions": [{"name": a.get("name"), "desc": a.get("desc")} for a in (c.get("legendary_actions") or [])], "description": c.get("desc", ""), } def simplify_class(c: dict) -> dict: """Extract key fields from a class.""" return { "key": c.get("key", ""), "name": c.get("name", ""), "hit_dice": c.get("hit_dice", ""), "description": c.get("desc", ""), "spellcasting_ability": c.get("spellcasting_ability", {}).get("key") if isinstance(c.get("spellcasting_ability"), dict) else c.get("spellcasting_ability"), "subclasses": [s.get("name") for s in (c.get("subclasses") or [])] if isinstance(c.get("subclasses"), list) else [], } def simplify_magic_item(m: dict) -> dict: """Extract key fields from a magic item.""" return { "key": m.get("key", ""), "name": m.get("name", ""), "type": m.get("type", {}).get("name") if isinstance(m.get("type"), dict) else m.get("type"), "rarity": m.get("rarity", {}).get("name") if isinstance(m.get("rarity"), dict) else m.get("rarity"), "requires_attunement": m.get("requires_attunement", ""), "description": m.get("desc", ""), } def simplify_item(i: dict) -> dict: """Extract key fields from equipment/item.""" cat = i.get("category") or {} return { "key": i.get("key", ""), "name": i.get("name", ""), "category": cat.get("name") if isinstance(cat, dict) else cat, "cost": i.get("cost", ""), "weight": i.get("weight", ""), "description": i.get("desc", ""), "armor_class": i.get("armor_class"), "stealth_disadvantage": i.get("stealth_disadvantage", False), "strength_requirement": i.get("strength_requirement"), "damage": i.get("damage", {}).get("damage_dice") if isinstance(i.get("damage"), dict) else None, "properties": [p.get("name") for p in (i.get("properties") or [])] if isinstance(i.get("properties"), list) else [], } SIMPLIFIERS = { "spells": simplify_spell, "creatures": simplify_creature, "classes": simplify_class, "magic-items": simplify_magic_item, "equipment": simplify_item, } def main(): for name, first_url in SOURCES.items(): try: raw_data = fetch_all(name, first_url) simplifier = SIMPLIFIERS.get(name) if simplifier: simplified = [simplifier(item) for item in raw_data] else: simplified = raw_data # Build index: name -> slug and key -> item indexed = { item.get("key"): item for item in simplified if item.get("key") } # Also build a name index name_index = {} for item in simplified: name_lower = item.get("name", "").lower() slug = item.get("key", "") if name_lower: name_index[name_lower] = slug output = { "count": len(simplified), "results": simplified, "_index": { "by_key": {k: True for k in indexed.keys()}, "by_name": name_index, } } # Remove _index for display but keep for reference filepath = os.path.join(DATA_DIR, f"{name}.json") with open(filepath, "w") as f: json.dump(output, f, indent=2) print(f"šŸ’¾ Saved {len(simplified)} items to {filepath}") # Also save raw data for debugging raw_filepath = os.path.join(DATA_DIR, f"{name}_raw.json") with open(raw_filepath, "w") as f: json.dump({"count": len(raw_data), "results": raw_data}, f, indent=2) print(f"šŸ’¾ Raw backup saved to {raw_filepath}") except Exception as e: print(f"āŒ Failed to fetch {name}: {e}") sys.exit(1) print("\n✨ All data fetched successfully!") print(json.dumps({"status": "ok", "dir": DATA_DIR}, indent=2)) if __name__ == "__main__": main()