Some checks failed
Test and deploy staging / staging (push) Has been cancelled
97 lines
5.0 KiB
Python
97 lines
5.0 KiB
Python
"""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()
|