From 0a2d42a6d0eda7b84bb75a65449437bdf09cee95 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sun, 8 Feb 2026 12:39:00 +0100 Subject: [PATCH] Add --export flag to export all seed data from DB to JSON Replaces --export-bosses with a unified --export that dumps games, pokemon, evolutions, routes/encounters, and bosses to seeds/data/. Each export function mirrors the corresponding API export endpoint. Co-Authored-By: Claude Opus 4.6 --- ...-add-cli-export-for-all-seed-data-types.md | 19 ++ backend/src/app/seeds/__main__.py | 12 +- backend/src/app/seeds/run.py | 258 ++++++++++++++---- 3 files changed, 235 insertions(+), 54 deletions(-) create mode 100644 .beans/nuzlocke-tracker-009n--add-cli-export-for-all-seed-data-types.md diff --git a/.beans/nuzlocke-tracker-009n--add-cli-export-for-all-seed-data-types.md b/.beans/nuzlocke-tracker-009n--add-cli-export-for-all-seed-data-types.md new file mode 100644 index 0000000..5b97a54 --- /dev/null +++ b/.beans/nuzlocke-tracker-009n--add-cli-export-for-all-seed-data-types.md @@ -0,0 +1,19 @@ +--- +# nuzlocke-tracker-009n +title: Add CLI export for all seed data types +status: completed +type: feature +priority: normal +created_at: 2026-02-08T11:37:27Z +updated_at: 2026-02-08T11:38:48Z +--- + +Add export functions for games, pokemon, routes/encounters, and evolutions to the seed CLI, matching the existing export API endpoints. Consolidate with the existing --export-bosses into a single --export flag that dumps everything. + +## Checklist +- [x] Add export_games() to run.py — writes games.json +- [x] Add export_pokemon() to run.py — writes pokemon.json +- [x] Add export_routes() to run.py — writes {game_slug}.json per game (routes + encounters) +- [x] Add export_evolutions() to run.py — writes evolutions.json +- [x] Replace --export-bosses with --export flag that exports all data types +- [x] Update __main__.py docstring \ No newline at end of file diff --git a/backend/src/app/seeds/__main__.py b/backend/src/app/seeds/__main__.py index 3cd0658..501de18 100644 --- a/backend/src/app/seeds/__main__.py +++ b/backend/src/app/seeds/__main__.py @@ -1,24 +1,24 @@ """Entry point for running seeds. Usage: - python -m app.seeds # Run seed - python -m app.seeds --verify # Run seed + verification - python -m app.seeds --export-bosses # Export boss data to seed JSON files + python -m app.seeds # Run seed + python -m app.seeds --verify # Run seed + verification + python -m app.seeds --export # Export all seed data from DB to JSON files """ import asyncio import sys from app.core.database import engine -from app.seeds.run import export_bosses, seed, verify +from app.seeds.run import export_all, seed, verify async def main(): verbose = "--verbose" in sys.argv or "-v" in sys.argv engine.echo = verbose - if "--export-bosses" in sys.argv: - await export_bosses() + if "--export" in sys.argv: + await export_all() return await seed() diff --git a/backend/src/app/seeds/run.py b/backend/src/app/seeds/run.py index 8488f16..bc5d202 100644 --- a/backend/src/app/seeds/run.py +++ b/backend/src/app/seeds/run.py @@ -10,6 +10,7 @@ from sqlalchemy.orm import selectinload from app.core.database import async_session from app.models.boss_battle import BossBattle from app.models.boss_pokemon import BossPokemon +from app.models.evolution import Evolution from app.models.game import Game from app.models.pokemon import Pokemon from app.models.route import Route @@ -220,73 +221,234 @@ async def verify(): print("\nVerification complete!") -async def export_bosses(): - """Export boss battles from the database to seed JSON files.""" - print("Exporting boss battles...") +def _write_json(filename: str, data) -> Path: + """Write data as JSON to DATA_DIR, return the path.""" + out_path = DATA_DIR / filename + with open(out_path, "w") as f: + json.dump(data, f, indent=2) + f.write("\n") + return out_path + +async def export_all(): + """Export all seed data from the database to JSON files.""" async with async_session() as session: - # Load version group data to get the first game slug for filenames with open(VG_JSON) as f: vg_data = json.load(f) - # Query all version groups with their games - vg_result = await session.execute( - select(VersionGroup).options(selectinload(VersionGroup.games)) + await _export_games(session) + await _export_pokemon(session) + await _export_evolutions(session) + await _export_routes(session, vg_data) + await _export_bosses(session, vg_data) + + print("Export complete!") + + +async def _export_games(session: AsyncSession): + """Export games to games.json.""" + result = await session.execute(select(Game).order_by(Game.name)) + games = result.scalars().all() + + data = [ + { + "name": g.name, + "slug": g.slug, + "generation": g.generation, + "region": g.region, + "release_year": g.release_year, + "color": g.color, + } + for g in games + ] + + _write_json("games.json", data) + print(f"Games: {len(data)} exported") + + +async def _export_pokemon(session: AsyncSession): + """Export pokemon to pokemon.json.""" + result = await session.execute(select(Pokemon).order_by(Pokemon.pokeapi_id)) + pokemon_list = result.scalars().all() + + data = [ + { + "pokeapi_id": p.pokeapi_id, + "national_dex": p.national_dex, + "name": p.name, + "types": p.types, + "sprite_url": p.sprite_url, + } + for p in pokemon_list + ] + + _write_json("pokemon.json", data) + print(f"Pokemon: {len(data)} exported") + + +async def _export_evolutions(session: AsyncSession): + """Export evolutions to evolutions.json.""" + result = await session.execute( + select(Evolution) + .options( + selectinload(Evolution.from_pokemon), + selectinload(Evolution.to_pokemon), ) - version_groups = vg_result.scalars().all() + .order_by(Evolution.id) + ) + evolutions = result.scalars().all() - slug_to_vg = {vg.slug: vg for vg in version_groups} + data = [ + { + "from_pokeapi_id": e.from_pokemon.pokeapi_id, + "to_pokeapi_id": e.to_pokemon.pokeapi_id, + "trigger": e.trigger, + "min_level": e.min_level, + "item": e.item, + "held_item": e.held_item, + "condition": e.condition, + "region": e.region, + } + for e in evolutions + ] - exported = 0 - for vg_slug, vg_info in vg_data.items(): - vg = slug_to_vg.get(vg_slug) - if vg is None: + _write_json("evolutions.json", data) + print(f"Evolutions: {len(data)} exported") + + +async def _export_routes(session: AsyncSession, vg_data: dict): + """Export routes and encounters per game.""" + # Get all games keyed by slug + game_result = await session.execute(select(Game)) + games_by_slug = {g.slug: g for g in game_result.scalars().all()} + + exported = 0 + for vg_slug, vg_info in vg_data.items(): + for game_slug in vg_info["games"]: + game = games_by_slug.get(game_slug) + if game is None or game.version_group_id is None: continue - # Query boss battles for this version group + # Load routes for this version group with encounters + pokemon result = await session.execute( - select(BossBattle) - .where(BossBattle.version_group_id == vg.id) + select(Route) + .where(Route.version_group_id == game.version_group_id) .options( - selectinload(BossBattle.pokemon).selectinload(BossPokemon.pokemon), + selectinload(Route.route_encounters).selectinload( + RouteEncounter.pokemon + ), ) - .order_by(BossBattle.order) + .order_by(Route.order) ) - bosses = result.scalars().all() + routes = result.scalars().all() - if not bosses: + if not routes: continue - first_game_slug = list(vg_info["games"].keys())[0] - data = [ - { - "name": b.name, - "boss_type": b.boss_type, - "badge_name": b.badge_name, - "badge_image_url": b.badge_image_url, - "level_cap": b.level_cap, - "order": b.order, - "location": b.location, - "sprite_url": b.sprite_url, - "pokemon": [ - { - "pokeapi_id": bp.pokemon.pokeapi_id, - "pokemon_name": bp.pokemon.name, - "level": bp.level, - "order": bp.order, - } - for bp in sorted(b.pokemon, key=lambda p: p.order) - ], + parent_routes = [r for r in routes if r.parent_route_id is None] + children_by_parent: dict[int, list[Route]] = {} + for r in routes: + if r.parent_route_id is not None: + children_by_parent.setdefault(r.parent_route_id, []).append(r) + + def format_encounters(route: Route) -> list[dict]: + game_encounters = [ + enc + for enc in route.route_encounters + if enc.game_id == game.id + ] + return [ + { + "pokeapi_id": enc.pokemon.pokeapi_id, + "pokemon_name": enc.pokemon.name, + "method": enc.encounter_method, + "encounter_rate": enc.encounter_rate, + "min_level": enc.min_level, + "max_level": enc.max_level, + } + for enc in sorted(game_encounters, key=lambda e: -e.encounter_rate) + ] + + def format_child(route: Route) -> dict: + data: dict = { + "name": route.name, + "order": route.order, + "encounters": format_encounters(route), } - for b in bosses - ] + if route.pinwheel_zone is not None: + data["pinwheel_zone"] = route.pinwheel_zone + return data - out_path = DATA_DIR / f"{first_game_slug}-bosses.json" - with open(out_path, "w") as f: - json.dump(data, f, indent=2) - f.write("\n") + def format_route(route: Route) -> dict: + data: dict = { + "name": route.name, + "order": route.order, + "encounters": format_encounters(route), + } + children = children_by_parent.get(route.id, []) + if children: + data["children"] = [ + format_child(c) + for c in sorted(children, key=lambda r: r.order) + ] + return data - print(f" {vg_slug}: {len(bosses)} bosses -> {out_path.name}") + route_data = [format_route(r) for r in parent_routes] + _write_json(f"{game_slug}.json", route_data) exported += 1 - print(f"Exported bosses for {exported} version groups.") + print(f"Routes: {exported} game files exported") + + +async def _export_bosses(session: AsyncSession, vg_data: dict): + """Export boss battles per version group.""" + vg_result = await session.execute(select(VersionGroup)) + slug_to_vg = {vg.slug: vg for vg in vg_result.scalars().all()} + + exported = 0 + for vg_slug, vg_info in vg_data.items(): + vg = slug_to_vg.get(vg_slug) + if vg is None: + continue + + result = await session.execute( + select(BossBattle) + .where(BossBattle.version_group_id == vg.id) + .options( + selectinload(BossBattle.pokemon).selectinload(BossPokemon.pokemon), + ) + .order_by(BossBattle.order) + ) + bosses = result.scalars().all() + + if not bosses: + continue + + first_game_slug = list(vg_info["games"].keys())[0] + data = [ + { + "name": b.name, + "boss_type": b.boss_type, + "badge_name": b.badge_name, + "badge_image_url": b.badge_image_url, + "level_cap": b.level_cap, + "order": b.order, + "location": b.location, + "sprite_url": b.sprite_url, + "pokemon": [ + { + "pokeapi_id": bp.pokemon.pokeapi_id, + "pokemon_name": bp.pokemon.name, + "level": bp.level, + "order": bp.order, + } + for bp in sorted(b.pokemon, key=lambda p: p.order) + ], + } + for b in bosses + ] + + _write_json(f"{first_game_slug}-bosses.json", data) + exported += 1 + + print(f"Bosses: {exported} version group files exported")