"""Build the species-level catalog from a pinned PokéAPI CSV snapshot. Run with --source-dir to reuse downloaded CSVs. Default downloads into a temporary folder. Encounter absence is not evidence of trade/transfer/event exclusivity. """ import argparse import concurrent.futures import csv import json from pathlib import Path import tempfile import urllib.request REVISION = '575291cdb197a7e3a320297be276c9de4ef8401a' TABLES = ['pokemon', 'pokemon_species', 'pokemon_species_names', 'pokemon_types', 'pokemon_stats', 'type_names', 'versions', 'version_names', 'version_groups', 'pokemon_dex_numbers', 'pokedex_version_groups', 'pokemon_game_indices', 'encounters'] def build(directory): def rows(name): with (directory / f'{name}.csv').open() as f: return list(csv.DictReader(f)) species = rows('pokemon_species') pokemon = rows('pokemon') species_for = {int(p['id']): int(p['species_id']) for p in pokemon} defaults = {int(p['species_id']): int(p['id']) for p in pokemon if p['is_default'] == '1'} names = {int(p['pokemon_species_id']): p['name'] for p in rows('pokemon_species_names') if p['local_language_id'] == '9'} type_names = {int(t['type_id']): t['name'] for t in rows('type_names') if t['local_language_id'] == '9'} types = {} for p in rows('pokemon_types'): types.setdefault(int(p['pokemon_id']), {})[int(p['slot'])] = type_names[int(p['type_id'])] stats = {} for p in rows('pokemon_stats'): stats.setdefault(int(p['pokemon_id']), {})[int(p['stat_id'])] = int(p['base_stat']) stat_fields = ['hp', 'attack', 'defense', 'specialAttack', 'specialDefense', 'speed'] mons = [] for s in species: number = int(s['id']) default = defaults[number] mons.append(dict(number=number, name=names[number], generation=int(s['generation_id']), type1=types[default][1], type2=types[default].get(2), **{name: stats[default][i + 1] for i, name in enumerate(stat_fields)})) groups = {int(g['id']): g for g in rows('version_groups')} versions = rows('versions') game_names = {int(v['version_id']): v['name'] for v in rows('version_names') if v['local_language_id'] == '9'} games = [dict(sourceId=int(v['id']), name=game_names.get(int(v['id']), v['identifier'].replace('-', ' ').title()), generation=int(groups[int(v['version_group_id'])]['generation_id'])) for v in versions] for game, version in zip(games, versions): if version['identifier'].endswith('-japan'): game['name'] += ' (Japan)' versions_for_group = {} for v in versions: versions_for_group.setdefault(int(v['version_group_id']), []).append(int(v['id'])) groups_for_dex = {} for d in rows('pokedex_version_groups'): groups_for_dex.setdefault(int(d['pokedex_id']), []).append(int(d['version_group_id'])) entries = {} # Membership and internal game indices establish presence, not how it is obtained. for p in rows('pokemon_dex_numbers'): for group in groups_for_dex.get(int(p['pokedex_id']), []): for version in versions_for_group.get(group, []): entries[(int(p['species_id']), version)] = 'unknown' for p in rows('pokemon_game_indices'): if int(p['game_index']) > 0: entries[(species_for[int(p['pokemon_id'])], int(p['version_id']))] = 'unknown' for p in rows('encounters'): entries[(species_for[int(p['pokemon_id'])], int(p['version_id']))] = 'catchable' return dict(source={'repository': 'https://github.com/PokeAPI/pokeapi', 'revision': REVISION, 'scope': 'All species and game versions in this snapshot; default-form types/stats. Unknown means presence without confirmed encounter data. Missing pairs have no source record, not proven absence.'}, pokemon=mons, games=games, entries=[[p, g, availability] for (p, g), availability in sorted(entries.items())]) def main(): parser = argparse.ArgumentParser() parser.add_argument('--source-dir', type=Path) args = parser.parse_args() with tempfile.TemporaryDirectory() as temporary: directory = args.source_dir or Path(temporary) if not args.source_dir: def download(table): url = f'https://raw.githubusercontent.com/PokeAPI/pokeapi/{REVISION}/data/v2/csv/{table}.csv' with urllib.request.urlopen(url, timeout=60) as response: (directory / f'{table}.csv').write_bytes(response.read()) with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: list(executor.map(download, TABLES)) catalog = build(directory) target = Path(__file__).resolve().parent.parent / 'data/pokemon-catalog.json' target.write_text(json.dumps(catalog, ensure_ascii=False, separators=(',', ':')) + '\n') print(f"Built {len(catalog['pokemon'])} species, {len(catalog['games'])} versions, {len(catalog['entries'])} entries") if __name__ == '__main__': main()