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 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 12:39:00 +01:00
parent 053dece33e
commit 0a2d42a6d0
3 changed files with 235 additions and 54 deletions

View File

@@ -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

View File

@@ -1,24 +1,24 @@
"""Entry point for running seeds. """Entry point for running seeds.
Usage: Usage:
python -m app.seeds # Run seed python -m app.seeds # Run seed
python -m app.seeds --verify # Run seed + verification 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 --export # Export all seed data from DB to JSON files
""" """
import asyncio import asyncio
import sys import sys
from app.core.database import engine 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(): async def main():
verbose = "--verbose" in sys.argv or "-v" in sys.argv verbose = "--verbose" in sys.argv or "-v" in sys.argv
engine.echo = verbose engine.echo = verbose
if "--export-bosses" in sys.argv: if "--export" in sys.argv:
await export_bosses() await export_all()
return return
await seed() await seed()

View File

@@ -10,6 +10,7 @@ from sqlalchemy.orm import selectinload
from app.core.database import async_session from app.core.database import async_session
from app.models.boss_battle import BossBattle from app.models.boss_battle import BossBattle
from app.models.boss_pokemon import BossPokemon from app.models.boss_pokemon import BossPokemon
from app.models.evolution import Evolution
from app.models.game import Game from app.models.game import Game
from app.models.pokemon import Pokemon from app.models.pokemon import Pokemon
from app.models.route import Route from app.models.route import Route
@@ -220,73 +221,234 @@ async def verify():
print("\nVerification complete!") print("\nVerification complete!")
async def export_bosses(): def _write_json(filename: str, data) -> Path:
"""Export boss battles from the database to seed JSON files.""" """Write data as JSON to DATA_DIR, return the path."""
print("Exporting boss battles...") 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: async with async_session() as session:
# Load version group data to get the first game slug for filenames
with open(VG_JSON) as f: with open(VG_JSON) as f:
vg_data = json.load(f) vg_data = json.load(f)
# Query all version groups with their games await _export_games(session)
vg_result = await session.execute( await _export_pokemon(session)
select(VersionGroup).options(selectinload(VersionGroup.games)) 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 _write_json("evolutions.json", data)
for vg_slug, vg_info in vg_data.items(): print(f"Evolutions: {len(data)} exported")
vg = slug_to_vg.get(vg_slug)
if vg is None:
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 continue
# Query boss battles for this version group # Load routes for this version group with encounters + pokemon
result = await session.execute( result = await session.execute(
select(BossBattle) select(Route)
.where(BossBattle.version_group_id == vg.id) .where(Route.version_group_id == game.version_group_id)
.options( .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 continue
first_game_slug = list(vg_info["games"].keys())[0] parent_routes = [r for r in routes if r.parent_route_id is None]
data = [ children_by_parent: dict[int, list[Route]] = {}
{ for r in routes:
"name": b.name, if r.parent_route_id is not None:
"boss_type": b.boss_type, children_by_parent.setdefault(r.parent_route_id, []).append(r)
"badge_name": b.badge_name,
"badge_image_url": b.badge_image_url, def format_encounters(route: Route) -> list[dict]:
"level_cap": b.level_cap, game_encounters = [
"order": b.order, enc
"location": b.location, for enc in route.route_encounters
"sprite_url": b.sprite_url, if enc.game_id == game.id
"pokemon": [ ]
{ return [
"pokeapi_id": bp.pokemon.pokeapi_id, {
"pokemon_name": bp.pokemon.name, "pokeapi_id": enc.pokemon.pokeapi_id,
"level": bp.level, "pokemon_name": enc.pokemon.name,
"order": bp.order, "method": enc.encounter_method,
} "encounter_rate": enc.encounter_rate,
for bp in sorted(b.pokemon, key=lambda p: p.order) "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" def format_route(route: Route) -> dict:
with open(out_path, "w") as f: data: dict = {
json.dump(data, f, indent=2) "name": route.name,
f.write("\n") "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 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")