diff --git a/.circleci/config.yml b/.circleci/config.yml index cc091f507..81b5b69f6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,7 +16,7 @@ jobs: command: make install - run: name: Pre-commit checks - command: SKIP=ruff-format uv run pre-commit run --all-files --show-diff-on-failure + command: SKIP=ruff-format,ruff-check,ty-check,generate-openapi uv run pre-commit run --all-files --show-diff-on-failure - run: name: Format Check command: make format-check diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 000000000..c7c2e78c4 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,79 @@ +name: Code Quality + +on: + pull_request: + paths: + - "**.py" + - "**.toml" + - "Makefile" + - "uv.lock" + - ".github/workflows/code-quality.yml" + push: + branches-ignore: + - master + - staging + paths: + - "**.py" + - "**.toml" + - "Makefile" + - "uv.lock" + - ".github/workflows/code-quality.yml" + +jobs: + ruff: + name: Ruff (lint) + runs-on: ubuntu-latest + # TODO: remove once the codebase is fully lint-clean + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup uv + uses: ./.github/actions/setup-uv + - name: Install dependencies + run: make install + - name: Lint + run: make lint-check || true + - name: Summary + if: always() + run: | + { + echo "## Ruff lint results" + echo + echo "
Expand to see all errors (backlog while the codebase is being linted incrementally)" + echo + echo '```' + uv run ruff check --output-format=concise . || true + echo '```' + echo + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + + ty: + name: ty (type-check) + runs-on: ubuntu-latest + # TODO: remove once the codebase is fully typed + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup uv + uses: ./.github/actions/setup-uv + - name: Install dependencies + run: make install + - name: Type-check + run: make typecheck || true + - name: Summary + if: always() + run: | + { + echo "## ty type-check results" + echo + echo "
Expand to see all errors (backlog while the codebase is being typed incrementally)" + echo + echo '```' + uv run ty check --output-format=concise || true + echo '```' + echo + echo "
" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index c943ef06a..090a341b4 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -2,6 +2,10 @@ name: Database on: pull_request: + push: + branches-ignore: + - master + - staging jobs: csv: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3bb6fcdf..36955dd17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,14 +20,6 @@ repos: args: - --markdown-linebreak-ext=md - # TODO: Enable in the future when adding the ruff linter - # - repo: https://github.com/pre-commit/pygrep-hooks - # rev: v1.9.0 - # hooks: - # - id: python-check-blanket-noqa - # - id: python-check-blanket-type-ignore - # - id: python-use-type-annotations - - repo: local hooks: - id: check-csv @@ -37,6 +29,13 @@ repos: args: [--encoding, utf-8] files: \.csv$ + - id: ruff-check + name: Ruff Check + entry: uv run ruff check . + language: system + types_or: [python, pyi] + pass_filenames: false + - id: ruff-format name: Ruff Format entry: uv run ruff format . @@ -44,6 +43,20 @@ repos: types_or: [python, pyi] pass_filenames: false + - id: ty-check + name: Ty Check + entry: uv run ty check + language: system + types_or: [python, pyi] + pass_filenames: false + + - id: generate-openapi + name: Generate OpenAPI Schema + entry: uv run manage.py spectacular --file openapi.yml --settings=config.docker_compose + language: system + files: ^(pokemon_v2/|config/|openapi\.yml) + pass_filenames: false + - id: build-and-test name: Build and Test entry: >- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a03e87e19..3fa06a642 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,13 +18,17 @@ git checkout -b my_new_branch ``` - Write some code, fix something, and add a test to prove that it works. **No pull request will be accepted without tests passing, or without new tests if new features are added.** -- Make sure your code passes the pre-commit hooks, if you have the hooks installed it should run automatically on commit. You can run them manually with: +- Make sure your code changes passes the pre-commit hooks, linting, formatting, and typechecking. You can run them manually with: ```bash -make pre-commit -# or -uv run pre-commit run --all-files +make pre-commit # or: uv run pre-commit run --all-files +make lint-check # or: uv run ruff check . +make format # or: uv run ruff format . +make typecheck # or: uv run ty check ``` +> [!NOTE] +> As of right now we are not strictly enforcing linting and typechecking, but we will be in the future. Please try to make sure your code passes these checks. + - Commit your code and push it to GitHub - [Open a new pull request](https://help.github.com/articles/creating-a-pull-request/) and describe the changes you have made. diff --git a/Makefile b/Makefile index f4d042445..68778b3f3 100755 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ veekun_pokedex_repository = ../pokedex local_config = --settings=config.local -docker_config = --settings=config.docker-compose +docker_config = --settings=config.docker_compose gql_compose_config_deprecated = -f docker-compose.yml -f docker-compose-dev.yml -f Resources/compose/docker-compose-prod-graphql.yml gql_compose_config = -f docker-compose.yml -f Resources/compose/docker-compose-prod-graphql.yml @@ -101,6 +101,15 @@ format: check-uv # Format the source code format-check: check-uv # Check the source code has been formatted uv run ruff format . --check +lint-check: check-uv # Lint the source code + uv run ruff check . + +lint-fix: check-uv # Lint the source code and fix issues + uv run ruff check . --fix + +typecheck: check-uv # Type-check the source code with ty + uv run ty check + pull: git checkout master git pull diff --git a/README.md b/README.md index 327638441..6ae1d695f 100755 --- a/README.md +++ b/README.md @@ -54,6 +54,17 @@ A RESTful API for Pokémon - [pokeapi.co](https://pokeapi.co) > [!NOTE] > Pre-commit hooks are optional but recommended for maintaining code quality and consistency. If you do not want it to automatically run on every commit, you can run it manually with `make pre-commit` before commiting and pushing your changes. +- Lint, format, and typecheck your code changes: + + ```sh + make lint-check # or: uv run ruff check . + make format # or: uv run ruff format . + make typecheck # or: uv run ty check + ``` + +> [!NOTE] +> As of right now we are not strictly enforcing linting and typechecking, but we will be in the future. Please try to make sure your code passes these checks. + - Set up the local development environment using the following command: ```sh @@ -107,8 +118,8 @@ If you don't have `make` on your machine you can use the following commands ```sh docker compose up -d -docker compose exec -T app python manage.py migrate --settings=config.docker-compose -docker compose exec -T app sh -c 'echo "from data.v2.build import build_all; build_all()" | python manage.py shell --settings=config.docker-compose' +docker compose exec -T app python manage.py migrate --settings=config.docker_compose +docker compose exec -T app sh -c 'echo "from data.v2.build import build_all; build_all()" | python manage.py shell --settings=config.docker_compose' ``` Browse [localhost/api/v2/](http://localhost/api/v2/) or [localhost/api/v2/pokemon/bulbasaur/](http://localhost/api/v2/pokemon/bulbasaur/) on port `80`. @@ -159,8 +170,8 @@ Configure `kubectl` to point to a cluster and then run the following commands to kubectl apply -k Resources/k8s/kustomize/base/ kubectl config set-context --current --namespace pokeapi # (Optional) Set pokeapi ns as the working ns # Wait for the cluster to spin up -kubectl exec --namespace pokeapi deployment/pokeapi -- python manage.py migrate --settings=config.docker-compose # Migrate the DB -kubectl exec --namespace pokeapi deployment/pokeapi -- sh -c 'echo "from data.v2.build import build_all; build_all()" | python manage.py shell --settings=config.docker-compose' # Build the db +kubectl exec --namespace pokeapi deployment/pokeapi -- python manage.py migrate --settings=config.docker_compose # Migrate the DB +kubectl exec --namespace pokeapi deployment/pokeapi -- sh -c 'echo "from data.v2.build import build_all; build_all()" | python manage.py shell --settings=config.docker_compose' # Build the db kubectl wait --namespace pokeapi --timeout=120s --for=condition=complete job/load-graphql # Wait for Graphql configuration job to finish ``` diff --git a/Resources/docker/app/Dockerfile b/Resources/docker/app/Dockerfile index 1405625b4..dec9772b4 100644 --- a/Resources/docker/app/Dockerfile +++ b/Resources/docker/app/Dockerfile @@ -18,7 +18,7 @@ FROM python:3.14-alpine RUN apk add --no-cache git ENV PYTHONUNBUFFERED=1 -ENV DJANGO_SETTINGS_MODULE='config.docker-compose' +ENV DJANGO_SETTINGS_MODULE='config.docker_compose' ENV PATH="/code/.venv/bin:$PATH" WORKDIR /code diff --git a/Resources/docker/app/README.md b/Resources/docker/app/README.md index 28f2f50b5..626339a43 100644 --- a/Resources/docker/app/README.md +++ b/Resources/docker/app/README.md @@ -46,6 +46,6 @@ Pokémon data isn't automatically present in this image. All Pokémon data is pe When the container is up and running, run the following shell commands: ```sh -docker exec pokeapi python manage.py migrate --settings=config.docker-compose -docker exec pokeapi sh -c 'echo "from data.v2.build import build_all; build_all()" | python manage.py shell --settings=config.docker-compose' +docker exec pokeapi python manage.py migrate --settings=config.docker_compose +docker exec pokeapi sh -c 'echo "from data.v2.build import build_all; build_all()" | python manage.py shell --settings=config.docker_compose' ``` diff --git a/config/__init__.py b/config/__init__.py old mode 100755 new mode 100644 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 000000000..28a5fd2db --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for PokeAPI. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_asgi_application() diff --git a/config/docker.py b/config/docker.py old mode 100755 new mode 100644 index e8be3ed0f..7000aa198 --- a/config/docker.py +++ b/config/docker.py @@ -1,7 +1,9 @@ # Docker settings -from .settings import * +# ruff: noqa: F405 +# pyright: reportConstantRedefinition=false +from .settings import * # noqa: F403 -DATABASES = { +DATABASES: dict[str, DatabaseSettings] = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "pokeapi", @@ -13,7 +15,7 @@ } -CACHES = { +CACHES: dict[str, CacheSettings] = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", @@ -24,3 +26,7 @@ } DEBUG = True + +for template in TEMPLATES: + if "OPTIONS" in template and "debug" in template["OPTIONS"]: + template["OPTIONS"]["debug"] = DEBUG diff --git a/config/docker-compose.py b/config/docker_compose.py similarity index 74% rename from config/docker-compose.py rename to config/docker_compose.py index 158a698ed..5a3e119e9 100644 --- a/config/docker-compose.py +++ b/config/docker_compose.py @@ -1,19 +1,22 @@ # Docker settings +# ruff: noqa: F405 +# pyright: reportConstantRedefinition=false import os -from .settings import * -DATABASES = { +from .settings import * # noqa: F403 + +DATABASES: dict[str, DatabaseSettings] = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.environ.get("POSTGRES_DB", "pokeapi"), "USER": os.environ.get("POSTGRES_USER", "ash"), "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "pokemon"), "HOST": os.environ.get("POSTGRES_HOST", "db"), - "PORT": os.environ.get("POSTGRES_PORT", 5432), + "PORT": os.environ.get("POSTGRES_PORT", "5432"), } } -CACHES = { +CACHES: dict[str, CacheSettings] = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": os.environ.get("REDIS_CONNECTION_STRING", "redis://cache:6379/1"), diff --git a/config/local.py b/config/local.py old mode 100755 new mode 100644 index d181da646..b6b81dd63 --- a/config/local.py +++ b/config/local.py @@ -1,16 +1,22 @@ -from .settings import * +# pyright: reportConstantRedefinition=false +# ruff: noqa: F405 +from .settings import * # noqa: F403 -DATABASES = { +DATABASES: dict[str, DatabaseSettings] = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } -CACHES = { +CACHES: dict[str, CacheSettings] = { "default": { "BACKEND": "django.core.cache.backends.dummy.DummyCache", } } DEBUG = True + +for template in TEMPLATES: + if "OPTIONS" in template and "debug" in template["OPTIONS"]: + template["OPTIONS"]["debug"] = DEBUG diff --git a/config/settings.py b/config/settings.py old mode 100755 new mode 100644 index c85964262..3a033f40a --- a/config/settings.py +++ b/config/settings.py @@ -1,14 +1,61 @@ +# ruff: noqa: E501 # Production settings import os +import textwrap from pathlib import Path +from typing import TypedDict + + +class DatabaseSettings(TypedDict, total=False): + ENGINE: str + NAME: str | Path + USER: str + PASSWORD: str + HOST: str + PORT: str | int | None + CONN_MAX_AGE: int | None + + +class CacheSettings(TypedDict, total=False): + BACKEND: str + LOCATION: str + OPTIONS: dict[str, str | int | None] | None + + +class DRFSettings(TypedDict, total=False): + DEFAULT_RENDERER_CLASSES: tuple[str, ...] + DEFAULT_PARSER_CLASSES: tuple[str, ...] + DEFAULT_PAGINATION_CLASS: str | None + PAGE_SIZE: int | None + DEFAULT_SCHEMA_CLASS: str -BASE_DIR = Path(__file__).resolve().parent.parent -DEBUG = False +class TemplateSettings(TypedDict, total=False): + BACKEND: str + DIRS: list[str] + APP_DIRS: bool + OPTIONS: dict[str, list[str] | bool | None] -TEMPLATE_DEBUG = DEBUG -ADMINS = (os.environ.get("ADMINS", "admin,admin@noemail.com").split(","),) +class SpectacularSettings(TypedDict, total=False): + TITLE: str + DESCRIPTION: str + SORT_OPERATIONS: bool + SERVERS: list[dict[str, str]] + EXTERNAL_DOCS: dict[str, str] + VERSION: str + SERVE_INCLUDE_SCHEMA: bool + AUTHENTICATION_WHITELIST: list[str] + OAS_VERSION: str + COMPONENT_SPLIT_REQUEST: bool + TAGS: list[dict[str, str | dict[str, str]]] + + +BASE_DIR = Path(__file__).resolve().parent.parent + +DEBUG: bool = False + +ADMINS = [tuple(admin.split(",")) for admin in os.environ.get("ADMINS", "admin,admin@noemail.com").split(";")] EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" @@ -55,7 +102,7 @@ WSGI_APPLICATION = "config.wsgi.application" -DATABASES = { +DATABASES: dict[str, DatabaseSettings] = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "pokeapi_co_db", @@ -67,7 +114,7 @@ } } -CACHES = { +CACHES: dict[str, CacheSettings] = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", @@ -79,8 +126,6 @@ SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-a(!_5+l3$#l1f4n!x+&ns_+8$4q@df*3rh$n#2h@l$2gti7!7-") -CUSTOM_APPS = ("pokemon_v2",) - INSTALLED_APPS = ( "django.contrib.admin", "django.contrib.auth", @@ -91,25 +136,25 @@ "rest_framework", "cachalot", "drf_spectacular", -) + CUSTOM_APPS + "pokemon_v2", +) -CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_ALL_ORIGINS = True -CORS_ALLOW_METHODS = "GET" +CORS_ALLOW_METHODS = ("GET",) CORS_URLS_REGEX = r"^/api/.*$" -REST_FRAMEWORK = { +REST_FRAMEWORK: DRFSettings = { "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), - "DEFAULT_PARSER_CLASSES": ("rest_framework.renderers.JSONRenderer",), + "DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",), "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 20, - "PAGINATE_BY": 20, "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } -TEMPLATES = [ +TEMPLATES: list[TemplateSettings] = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], @@ -120,30 +165,34 @@ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], + "debug": DEBUG, }, }, ] DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -SPECTACULAR_SETTINGS = { +SPECTACULAR_SETTINGS: SpectacularSettings = { "TITLE": "PokéAPI", - "DESCRIPTION": """All the Pokémon data you'll ever need in one place, easily accessible through a modern free open-source RESTful API. + "DESCRIPTION": textwrap.dedent( + """ + All the Pokémon data you'll ever need in one place, easily accessible through a modern free open-source RESTful API. -## What is this? + ## What is this? -This is a full RESTful API linked to an extensive database detailing everything about the Pokémon main game series. + This is a full RESTful API linked to an extensive database detailing everything about the Pokémon main game series. -We've covered everything from Pokémon to Berry Flavors. + We've covered everything from Pokémon to Berry Flavors. -## Where do I start? + ## Where do I start? -We have awesome [documentation](https://pokeapi.co/docs/v2) on how to use this API. It takes minutes to get started. + We have awesome [documentation](https://pokeapi.co/docs/v2) on how to use this API. It takes minutes to get started. -This API will always be publicly available and will never require any extensive setup process to consume. + This API will always be publicly available and will never require any extensive setup process to consume. -Created by [**Paul Hallett**](https://github.com/phalt) and other [**PokéAPI contributors**](https://github.com/PokeAPI/pokeapi#contributing) around the world. Pokémon and Pokémon character names are trademarks of Nintendo. - """, + Created by [**Paul Hallett**](https://github.com/phalt) and other [**PokéAPI contributors**](https://github.com/PokeAPI/pokeapi#contributing) around the world. Pokémon and Pokémon character names are trademarks of Nintendo. + """ + ).strip(), "SORT_OPERATIONS": False, "SERVERS": [{"url": "https://pokeapi.co"}], "EXTERNAL_DOCS": {"url": "https://pokeapi.co/docs/v2"}, diff --git a/config/urls.py b/config/urls.py old mode 100755 new mode 100644 index 51fc6fc9f..70d969a27 --- a/config/urls.py +++ b/config/urls.py @@ -1,7 +1,6 @@ from django.urls import include, path -from pokemon_v2 import urls as pokemon_v2_urls -# pylint: disable=invalid-name +from pokemon_v2 import urls as pokemon_v2_urls urlpatterns = [ path("", include(pokemon_v2_urls)), diff --git a/config/wsgi.py b/config/wsgi.py old mode 100755 new mode 100644 index 5526cb895..7d219822b --- a/config/wsgi.py +++ b/config/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for mysite project. +WSGI config for PokeAPI. It exposes the WSGI callable as a module-level variable named ``application``. diff --git a/data/v2/build.py b/data/v2/build.py index 79b464eb3..4a75fe6e5 100644 --- a/data/v2/build.py +++ b/data/v2/build.py @@ -15,8 +15,9 @@ import os import os.path import re -import json + from django.db import connection + from pokemon_v2.models import * # why this way? how about use `__file__` @@ -43,16 +44,16 @@ ) IMAGE_DIR = os.getcwd() + "/data/v2/sprites/sprites/" CRIES_DIR = os.getcwd() + "/data/v2/cries/cries/" -RESOURCE_IMAGES = [] -RESOURCE_CRIES = [] +RESOURCE_IMAGES: list[str] = [] +RESOURCE_CRIES: list[str] = [] -for root, dirs, files in os.walk(IMAGE_DIR): +for root, _dirs, files in os.walk(IMAGE_DIR): for file in files: image_path = os.path.join(root.replace(IMAGE_DIR, ""), file) image_path = image_path.replace("\\", "/") # convert Windows-style path to Unix RESOURCE_IMAGES.append(image_path) -for root, dirs, files in os.walk(CRIES_DIR): +for root, _dirs, files in os.walk(CRIES_DIR): for file in files: cry_path = os.path.join(root.replace(CRIES_DIR, ""), file) cry_path = cry_path.replace("\\", "/") # convert Windows-style path to Unix @@ -69,13 +70,12 @@ def with_iter(context, iterable=None): if iterable is None: iterable = context with context: - for value in iterable: - yield value + yield from iterable def load_data(file_name): # with_iter closes the file when it has finished - return csv.reader(with_iter(open(DATA_LOCATION + file_name, "rt", encoding="utf8")), delimiter=",") + return csv.reader(with_iter(open(DATA_LOCATION + file_name, encoding="utf8")), delimiter=",") def clear_table(model): @@ -445,7 +445,7 @@ def csv_record_to_objects(info): elif re.search(r"^hm[0-9]", info[1]): file_name = "hm-normal.png" else: - file_name = "%s.png" % info[1] + file_name = f"{info[1]}.png" item_sprites = "items/{0}" sprites = {"default": file_path_or_none(item_sprites.format(file_name))} @@ -594,8 +594,8 @@ def csv_record_to_objects(info): "generation-ix": ["scarlet-violet"], } sprites = {} - for generation in game_map.keys(): - for game in game_map[generation]: + for generation, games in game_map.items(): + for game in games: if generation not in sprites: sprites[generation] = {} sprites[generation][game] = { @@ -786,12 +786,12 @@ def csv_record_to_objects(info): build_generic((MoveFlavorText,), "move_flavor_text.csv", csv_record_to_objects) + existing_effect_ids = set(MoveEffect.objects.values_list("pk", flat=True)) + def csv_record_to_objects(info): - _move_effect = None - try: - _move_effect = MoveEffect.objects.get(pk=int(info[6])) if info[6] != "" else None - except: - pass + effect_id = int(info[6]) if info[6] != "" else None + if effect_id not in existing_effect_ids: + effect_id = None yield MoveChange( move_id=int(info[0]), @@ -800,7 +800,7 @@ def csv_record_to_objects(info): power=int(info[3]) if info[3] != "" else None, pp=int(info[4]) if info[4] != "" else None, accuracy=int(info[5]) if info[5] != "" else None, - move_effect_id=_move_effect.pk if _move_effect else None, + move_effect_id=effect_id, move_effect_chance=int(info[7]) if info[7] != "" else None, ) @@ -1148,7 +1148,7 @@ def csv_record_to_objects(info): id=int(info[0]), location_id=int(info[1]), game_index=int(info[2]), - name=("{}-{}".format(location.name, info[3]) if info[3] else "{}-{}".format(location.name, "area")), + name=(f"{location.name}-{info[3]}" if info[3] else "{}-{}".format(location.name, "area")), ) build_generic((LocationArea,), "location_areas.csv", csv_record_to_objects) @@ -1283,14 +1283,14 @@ def try_image_names(path, info, extension): identifier = info[1] species_id = info[2] if "-" in identifier: - form_file_name = "%s.%s" % ( + form_file_name = "{}.{}".format( species_id + "-" + identifier.split("-", 1)[1], extension, ) - id_file_name = "%s.%s" % (pokemon_id, extension) + id_file_name = f"{pokemon_id}.{extension}" file_name = id_file_name if file_path_or_none(path + id_file_name) else form_file_name else: - file_name = "%s.%s" % (info[0], extension) + file_name = f"{info[0]}.{extension}" return file_path_or_none(path + file_name) def csv_record_to_objects(info): @@ -1727,7 +1727,7 @@ def csv_record_to_objects(info): build_generic((PokemonSprites,), "pokemon.csv", csv_record_to_objects) def try_cry_names(path, info, extension): - file_name = "%s.%s" % (info[0], extension) + file_name = f"{info[0]}.{extension}" return file_path_or_none(path + file_name, image_file=False) def csv_record_to_objects(info): @@ -1834,42 +1834,42 @@ def csv_record_to_objects(info): build_generic((PokemonForm,), "pokemon_forms.csv", csv_record_to_objects) - def try_image_names(path, info, extension): + def try_form_image_names(path, info, extension): form_identifier = info[2] pokemon_id = info[3] pokemon = Pokemon.objects.get(pk=int(pokemon_id)) - species_id = getattr(pokemon, "pokemon_species_id") + species_id = getattr(pokemon.pokemon_species, "pk", 0) is_default = int(info[5]) if form_identifier: - form_file_name = "%s-%s.%s" % (species_id, form_identifier, extension) - id_file_name = "%s.%s" % (pokemon_id, extension) + form_file_name = f"{species_id}-{form_identifier}.{extension}" + id_file_name = f"{pokemon_id}.{extension}" file_name = id_file_name if file_path_or_none(path + id_file_name) else form_file_name if id_file_name and form_file_name and (not is_default): file_name = form_file_name else: - file_name = "%s.%s" % (species_id, extension) + file_name = f"{species_id}.{extension}" return file_path_or_none(path + file_name) def csv_record_to_objects(info): poke_sprites = "pokemon/" sprites = { - "front_default": try_image_names(poke_sprites, info, "png"), - "front_shiny": try_image_names(poke_sprites + "shiny/", info, "png"), - "back_default": try_image_names(poke_sprites + "back/", info, "png"), - "back_shiny": try_image_names(poke_sprites + "back/shiny/", info, "png"), - "front_female": try_image_names(poke_sprites + "female/", info, "png"), - "front_shiny_female": try_image_names(poke_sprites + "shiny/female/", info, "png"), - "back_female": try_image_names(poke_sprites + "back/female/", info, "png"), - "back_shiny_female": try_image_names(poke_sprites + "back/shiny/female/", info, "png"), + "front_default": try_form_image_names(poke_sprites, info, "png"), + "front_shiny": try_form_image_names(poke_sprites + "shiny/", info, "png"), + "back_default": try_form_image_names(poke_sprites + "back/", info, "png"), + "back_shiny": try_form_image_names(poke_sprites + "back/shiny/", info, "png"), + "front_female": try_form_image_names(poke_sprites + "female/", info, "png"), + "front_shiny_female": try_form_image_names(poke_sprites + "shiny/female/", info, "png"), + "back_female": try_form_image_names(poke_sprites + "back/female/", info, "png"), + "back_shiny_female": try_form_image_names(poke_sprites + "back/shiny/female/", info, "png"), "versions": { "generation-viii": { "brilliant-diamond-shining-pearl": { - "front_default": try_image_names( + "front_default": try_form_image_names( poke_sprites + "versions/generation-viii/brilliant-diamond-shining-pearl/", info, "png", ), - "front_female": try_image_names( + "front_female": try_form_image_names( poke_sprites + "versions/generation-viii/brilliant-diamond-shining-pearl/female/", info, "png", @@ -1878,12 +1878,12 @@ def csv_record_to_objects(info): }, "generation-ix": { "scarlet-violet": { - "front_default": try_image_names( + "front_default": try_form_image_names( poke_sprites + "versions/generation-ix/scarlet-violet/", info, "png", ), - "front_female": try_image_names( + "front_female": try_form_image_names( poke_sprites + "versions/generation-ix/scarlet-violet/female/", info, "png", diff --git a/manage.py b/manage.py index aabb81818..f1aba7954 100755 --- a/manage.py +++ b/manage.py @@ -5,7 +5,7 @@ import sys -def main(): +def main() -> None: """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") try: diff --git a/openapi.yml b/openapi.yml index 396d56759..9241f485b 100644 --- a/openapi.yml +++ b/openapi.yml @@ -2,16 +2,22 @@ openapi: 3.1.0 info: title: PokéAPI version: 2.10.0 - description: "All the Pokémon data you'll ever need in one place, easily accessible - through a modern free open-source RESTful API.\n\n## What is this?\n\nThis is - a full RESTful API linked to an extensive database detailing everything about - the Pokémon main game series.\n\nWe've covered everything from Pokémon to Berry - Flavors.\n\n## Where do I start?\n\nWe have awesome [documentation](https://pokeapi.co/docs/v2) - on how to use this API. It takes minutes to get started.\n\nThis API will always - be publicly available and will never require any extensive setup process to consume.\n\nCreated - by [**Paul Hallett**](https://github.com/phalt) and other [**PokéAPI contributors**](https://github.com/PokeAPI/pokeapi#contributing) - around the world. Pokémon and Pokémon character names are trademarks of Nintendo.\n - \ " + description: |- + All the Pokémon data you'll ever need in one place, easily accessible through a modern free open-source RESTful API. + + ## What is this? + + This is a full RESTful API linked to an extensive database detailing everything about the Pokémon main game series. + + We've covered everything from Pokémon to Berry Flavors. + + ## Where do I start? + + We have awesome [documentation](https://pokeapi.co/docs/v2) on how to use this API. It takes minutes to get started. + + This API will always be publicly available and will never require any extensive setup process to consume. + + Created by [**Paul Hallett**](https://github.com/phalt) and other [**PokéAPI contributors**](https://github.com/PokeAPI/pokeapi#contributing) around the world. Pokémon and Pokémon character names are trademarks of Nintendo. paths: /api/v2/meta/: get: @@ -28,17 +34,7 @@ paths: content: application/json: schema: - type: object - properties: - deploy_date: - type: string - nullable: true - hash: - type: string - nullable: true - tag: - type: string - nullable: true + $ref: '#/components/schemas/PokeapiMetaResponse' description: '' /api/v2/ability/: get: @@ -3038,7 +3034,7 @@ paths: description: '' /api/v2/pokemon/{pokemon_id}/encounters: get: - operationId: pokemon_encounters_retrieve + operationId: pokemon_encounters_list description: Handles Pokemon Encounters as a sub-resource. summary: Get pokemon encounter parameters: @@ -3059,97 +3055,7 @@ paths: schema: type: array items: - type: object - required: - - location_area - - version_details - properties: - location_area: - type: object - required: - - name - - url - properties: - name: - type: string - example: cerulean-city-area - url: - type: string - format: uri - example: https://pokeapi.co/api/v2/location-area/281/ - version_details: - type: array - items: - type: object - required: - - encounter_details - - max_chance - - version - properties: - encounter_details: - type: array - items: - type: object - required: - - chance - - condition_values - - max_level - - method - - min_level - properties: - chance: - type: number - example: 100 - condition_values: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - example: story-progress-beat-red - url: - type: string - format: uri - example: https://pokeapi.co/api/v2/encounter-condition-value/55/ - max_level: - type: number - example: 10 - method: - type: object - required: - - name - - url - properties: - name: - type: string - example: gift - url: - type: string - format: uri - example: https://pokeapi.co/api/v2/encounter-method/18/ - min_level: - type: number - example: 10 - max_chance: - type: number - example: 100 - version: - type: object - required: - - name - - url - properties: - name: - type: string - example: red - url: - type: string - format: uri - example: https://pokeapi.co/api/v2/version/1/ + $ref: '#/components/schemas/PokemonEncounterResponse' description: '' components: schemas: @@ -3213,34 +3119,7 @@ components: pokemon: type: array items: - type: object - required: - - is_hidden - - slot - - pokemon - properties: - is_hidden: - type: boolean - slot: - type: integer - format: int32 - examples: - - 3 - pokemon: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - gloom - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon/44/ + $ref: '#/components/schemas/AbilityPokemonDetail' readOnly: true required: - effect_changes @@ -3290,6 +3169,20 @@ components: required: - language - name + AbilityPokemonDetail: + type: object + properties: + is_hidden: + type: boolean + slot: + type: integer + maximum: 2147483647 + minimum: -2147483648 + pokemon: + $ref: '#/components/schemas/PokemonSummary' + required: + - pokemon + - slot AbilitySummary: type: object properties: @@ -3313,27 +3206,39 @@ components: type: string maxLength: 200 growth_time: - type: integer + type: + - integer + - 'null' maximum: 2147483647 minimum: -2147483648 max_harvest: - type: integer + type: + - integer + - 'null' maximum: 2147483647 minimum: -2147483648 natural_gift_power: - type: integer + type: + - integer + - 'null' maximum: 2147483647 minimum: -2147483648 size: - type: integer + type: + - integer + - 'null' maximum: 2147483647 minimum: -2147483648 smoothness: - type: integer + type: + - integer + - 'null' maximum: 2147483647 minimum: -2147483648 soil_dryness: - type: integer + type: + - integer + - 'null' maximum: 2147483647 minimum: -2147483648 firmness: @@ -3341,32 +3246,7 @@ components: flavors: type: array items: - type: object - required: - - potency - - flavor - properties: - potency: - type: integer - examples: - - 10 - flavor: - type: object - require: - - name - - url - properties: - name: - type: string - description: The name of the flavor - examples: - - spicy - url: - type: string - format: uri - description: The URL to get more information about the flavor - examples: - - https://pokeapi.co/api/v2/berry-flavor/1/ + $ref: '#/components/schemas/BerryFlavorMap' readOnly: true item: $ref: '#/components/schemas/ItemSummary' @@ -3427,6 +3307,18 @@ components: required: - name - url + BerryFlavorBerryMap: + type: object + properties: + potency: + type: integer + maximum: 2147483647 + minimum: -2147483648 + berry: + $ref: '#/components/schemas/BerrySummary' + required: + - berry + - potency BerryFlavorDetail: type: object properties: @@ -3439,32 +3331,7 @@ components: berries: type: array items: - type: object - required: - - potency - - berry - properties: - potency: - type: integer - examples: - - 10 - berry: - type: object - require: - - name - - url - properties: - name: - type: string - description: The name of the berry - examples: - - rowap - url: - type: string - format: uri - description: The URL to get more information about the berry - examples: - - https://pokeapi.co/api/v2/berry/64/ + $ref: '#/components/schemas/BerryFlavorBerryMap' readOnly: true contest_type: $ref: '#/components/schemas/ContestTypeSummary' @@ -3479,6 +3346,18 @@ components: - id - name - names + BerryFlavorMap: + type: object + properties: + potency: + type: integer + maximum: 2147483647 + minimum: -2147483648 + flavor: + $ref: '#/components/schemas/BerryFlavorSummary' + required: + - flavor + - potency BerryFlavorName: type: object properties: @@ -3538,15 +3417,6 @@ components: type: array items: type: integer - format: int32 - examples: - - - 0 - - 5 - - 10 - - 15 - - 20 - - 25 - - 30 readOnly: true highest_stat: $ref: '#/components/schemas/StatSummary' @@ -3741,22 +3611,7 @@ components: pokemon_species: type: array items: - type: object - required: - - potency - - flavor - properties: - name: - type: string - description: Pokemon species name. - examples: - - bulbasaur - url: - type: string - format: uri - description: The URL to get more information about the species - examples: - - https://pokeapi.co/api/v2/pokemon-species/1/ + $ref: '#/components/schemas/PokemonSpeciesSummary' readOnly: true required: - id @@ -3928,6 +3783,21 @@ components: required: - name - url + EncounterPokemonDetail: + type: object + properties: + min_perfect_ivs: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + always_shiny: + type: boolean + never_shiny: + type: boolean + is_alpha: + type: boolean EvolutionChainDetail: type: object properties: @@ -3937,288 +3807,34 @@ components: baby_trigger_item: $ref: '#/components/schemas/ItemSummary' chain: - type: object - required: - - evolution_details - - evolves_to - - is_baby - - species - properties: - evolution_details: - type: array - items: {} - examples: [] - evolves_to: - type: array - items: - type: object - required: - - evolution_details - - evolves_to - - is_baby - - species - properties: - evolution_details: - type: array - items: - type: object - required: - - version_group - - is_default - - gender - - held_item - - item - - known_move - - known_move_type - - location - - min_affection - - min_beauty - - min_damage_taken - - min_happiness - - min_level - - min_move_count - - min_steps - - near_special_rock - - needs_multiplayer - - needs_overworld_rain - - party_species - - party_type - - relative_physical_stats - - time_of_day - - trade_species - - trigger - - turn_upside_down - - used_move - - region - - base_form - - evolved_form - properties: - version_group: - type: object - nullable: false - required: - - name - - url - properties: - name: - type: string - examples: - - 1 - url: - type: string - format: uri - examples: - - 2 - is_default: - type: boolean - gender: - type: object - nullable: true - required: - - name - - url - properties: - name: - type: string - examples: - - 1 - url: - type: string - format: uri - examples: - - 2 - held_item: - type: object - nullable: true - required: - - name - - url - properties: - name: - type: string - examples: - - 1 - url: - type: string - format: uri - examples: - - 2 - item: - type: object - nullable: true - required: - - name - - url - properties: - name: - type: string - examples: - - 1 - url: - type: string - format: uri - examples: - - 2 - known_move: - type: '' - nullable: true - known_move_type: - type: '' - nullable: true - location: - type: object - nullable: true - required: - - name - - url - properties: - name: - type: string - url: - type: string - format: uri - min_affection: - type: integer - format: int32 - nullable: true - min_beauty: - type: integer - format: int32 - nullable: true - min_damage_taken: - type: integer - format: int32 - nullable: true - min_happiness: - type: integer - format: int32 - nullable: true - min_level: - type: integer - format: int32 - nullable: true - min_move_count: - type: integer - format: int32 - nullable: true - min_steps: - type: integer - format: int32 - nullable: true - near_special_rock: - type: boolean - nullable: true - needs_multiplayer: - type: boolean - nullable: true - needs_overworld_rain: - type: boolean - nullable: true - party_species: - type: string - nullable: true - party_type: - type: string - nullable: true - relative_physical_stats: - type: string - nullable: true - time_of_day: - type: string - trade_species: - type: string - nullable: true - trigger: - type: object - required: - - name - - url - properties: - name: - type: string - url: - type: string - format: uri - turn_upside_down: - type: boolean - used_move: - type: '' - nullable: true - region: - type: object - nullable: true - required: - - name - - url - properties: - name: - type: string - url: - type: string - format: uri - base_form: - type: object - nullable: true - required: - - name - - url - properties: - name: - type: string - url: - type: string - format: uri - evolved_form: - type: object - nullable: true - required: - - name - - url - properties: - name: - type: string - url: - type: string - format: uri - is_baby: - type: boolean - species: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - happiny - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-species/440/ - is_baby: - type: boolean - species: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - happiny - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-species/440/ + allOf: + - $ref: '#/components/schemas/EvolutionChainLink' readOnly: true required: - baby_trigger_item - chain - id + EvolutionChainLink: + type: object + properties: + is_baby: + type: boolean + species: + $ref: '#/components/schemas/PokemonSpeciesSummary' + evolution_details: + type: array + items: + $ref: '#/components/schemas/PokemonEvolution' + evolves_to: + type: array + items: + type: object + additionalProperties: {} + required: + - evolution_details + - evolves_to + - is_baby + - species EvolutionChainSummary: type: object properties: @@ -4245,20 +3861,7 @@ components: pokemon_species: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - ivysaur - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-species/2/ + $ref: '#/components/schemas/PokemonSpeciesSummary' readOnly: true required: - id @@ -4315,55 +3918,28 @@ components: pokemon_species_details: type: array items: - type: object - required: - - rate - - pokemon_species - properties: - rate: - type: integer - format: int32 - examples: - - 1 - pokemon_species: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bulbasaur - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-species/1/ + $ref: '#/components/schemas/GenderPokemonSpecies' readOnly: true required_for_evolution: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - wormadam - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-species/413/ + $ref: '#/components/schemas/PokemonSpeciesSummary' readOnly: true required: - id - name - pokemon_species_details - required_for_evolution + GenderPokemonSpecies: + type: object + properties: + rate: + type: integer + pokemon_species: + $ref: '#/components/schemas/PokemonSpeciesSummary' + required: + - pokemon_species + - rate GenderSummary: type: object properties: @@ -4536,20 +4112,7 @@ components: items: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - master-ball - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/item/1/ + $ref: '#/components/schemas/ItemSummary' readOnly: true names: type: array @@ -4657,20 +4220,7 @@ components: attributes: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - countable - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/item-attribute/1/ + $ref: '#/components/schemas/ItemAttributeSummary' readOnly: true category: $ref: '#/components/schemas/ItemCategorySummary' @@ -4702,105 +4252,21 @@ components: held_by_pokemon: type: array items: - type: object - required: - - pokemon - - version-details - properties: - pokemon: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - farfetchd - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon/83/ - version-details: - type: array - items: - type: object - required: - - rarity - - version - properties: - rarity: - type: integer - format: int32 - examples: - - 5 - version: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - ruby - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version/7/ + $ref: '#/components/schemas/PokemonHeldItem' readOnly: true sprites: - type: object - required: - - default - properties: - default: - type: string - format: uri - examples: - - https://pokeapi.co/media/sprites/items/master-ball.png + allOf: + - $ref: '#/components/schemas/ItemSprites' readOnly: true baby_trigger_for: - type: object - required: - - url - properties: - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/evolution-chain/51/ + oneOf: + - $ref: '#/components/schemas/EvolutionChainSummary' + - type: 'null' readOnly: true machines: type: array items: - type: object - required: - - machine - - version_group - properties: - machine: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/machine/1/ - version_group: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - sword-shield - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version-group/20/ + $ref: '#/components/schemas/ItemMachine' readOnly: true required: - attributes @@ -4905,6 +4371,16 @@ components: required: - game_index - generation + ItemMachine: + type: object + properties: + machine: + $ref: '#/components/schemas/MachineSummary' + version_group: + $ref: '#/components/schemas/VersionGroupSummary' + required: + - machine + - version_group ItemName: type: object properties: @@ -4986,6 +4462,15 @@ components: required: - currency - version_group + ItemSprites: + type: object + properties: + default: + type: + - string + - 'null' + required: + - default ItemSummary: type: object properties: @@ -5067,54 +4552,7 @@ components: encounter_method_rates: type: array items: - type: object - required: - - encounter_method - - version_details - properties: - encounter_method: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - old-rod - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/encounter-method/2/ - version_details: - type: array - items: - type: object - required: - - rate - - version - properties: - rate: - type: integer - format: int32 - examples: - - 5 - version: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - platinum - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version/14/ + $ref: '#/components/schemas/LocationAreaEncounterRate' readOnly: true location: $ref: '#/components/schemas/LocationSummary' @@ -5126,109 +4564,7 @@ components: pokemon_encounters: type: array items: - type: object - required: - - pokemon - - version_details - properties: - pokemon: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - tentacool - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon/72/ - version_details: - type: array - items: - type: object - required: - - version - - max_chance - - encounter_details - properties: - version: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - diamond - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version/12/ - max_chance: - type: integer - format: int32 - examples: - - 60 - encounter_details: - type: object - required: - - min_level - - max_level - - condition_value - - chance - - method - properties: - min_level: - type: integer - format: int32 - examples: - - 20 - max_level: - type: integer - format: int32 - examples: - - 30 - condition_values: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - slot2-sapphire - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/encounter-condition-value/10/ - chance: - type: integer - format: int32 - examples: - - 60 - method: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - surf - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/encounter-method/5/ + $ref: '#/components/schemas/LocationAreaPokemonEncounter' readOnly: true required: - encounter_method_rates @@ -5238,6 +4574,55 @@ components: - name - names - pokemon_encounters + LocationAreaEncounterDetail: + type: object + properties: + min_level: + type: integer + max_level: + type: integer + chance: + type: integer + default: 0 + method: + $ref: '#/components/schemas/EncounterMethodSummary' + condition_values: + type: array + items: + $ref: '#/components/schemas/EncounterConditionValueSummary' + readOnly: true + pokemon_details: + oneOf: + - $ref: '#/components/schemas/EncounterPokemonDetail' + - type: 'null' + readOnly: true + required: + - condition_values + - max_level + - min_level + - pokemon_details + LocationAreaEncounterRate: + type: object + properties: + encounter_method: + $ref: '#/components/schemas/EncounterMethodSummary' + version_details: + type: array + items: + $ref: '#/components/schemas/LocationAreaEncounterVersionDetail' + required: + - encounter_method + - version_details + LocationAreaEncounterVersionDetail: + type: object + properties: + rate: + type: integer + version: + $ref: '#/components/schemas/VersionSummary' + required: + - rate + - version LocationAreaName: type: object properties: @@ -5249,6 +4634,33 @@ components: required: - language - name + LocationAreaPokemonEncounter: + type: object + properties: + pokemon: + $ref: '#/components/schemas/PokemonSummary' + version_details: + type: array + items: + $ref: '#/components/schemas/LocationAreaPokemonEncounterVersion' + required: + - pokemon + - version_details + LocationAreaPokemonEncounterVersion: + type: object + properties: + version: + $ref: '#/components/schemas/VersionSummary' + max_chance: + type: integer + encounter_details: + type: array + items: + $ref: '#/components/schemas/LocationAreaEncounterDetail' + required: + - encounter_details + - max_chance + - version LocationAreaSummary: type: object properties: @@ -5425,35 +4837,7 @@ components: effect_entries: type: array items: - type: object - required: - - effect - - short_effect - - language - properties: - effect: - type: string - examples: - - Inflicts regular damage. - short_effect: - type: string - examples: - - Inflicts regular damage with no additional effect. - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ + $ref: '#/components/schemas/MoveEffectEffectText' readOnly: true type: $ref: '#/components/schemas/TypeSummary' @@ -5464,6 +4848,34 @@ components: - effect_entries - type - version_group + MoveComboUsage: + type: object + properties: + use_before: + type: + - array + - 'null' + items: + $ref: '#/components/schemas/MoveSummary' + use_after: + type: + - array + - 'null' + items: + $ref: '#/components/schemas/MoveSummary' + required: + - use_after + - use_before + MoveCombos: + type: object + properties: + normal: + $ref: '#/components/schemas/MoveComboUsage' + super: + $ref: '#/components/schemas/MoveComboUsage' + required: + - normal + - super MoveDamageClassDescription: type: object properties: @@ -5564,95 +4976,9 @@ components: maximum: 2147483647 minimum: -2147483648 contest_combos: - type: object - required: - - normal - - super - properties: - normal: - type: object - required: - - use_before - - use_after - properties: - use_before: - type: array - nullable: true - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - fire-punch - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/7/ - use_after: - type: array - nullable: true - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - ice-punch - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/8/ - super: - type: object - required: - - use_before - - use_after - properties: - use_before: - type: array - nullable: true - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - night-slash - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/400/ - use_after: - type: array - nullable: true - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - focus-energy - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/116/ + oneOf: + - $ref: '#/components/schemas/MoveCombos' + - type: 'null' readOnly: true contest_type: $ref: '#/components/schemas/ContestTypeSummary' @@ -5663,86 +4989,12 @@ components: effect_entries: type: array items: - type: object - required: - - effect - - short_effect - - language - properties: - effect: - type: string - examples: - - Inflicts regular damage. - short_effect: - type: string - examples: - - Inflicts regular damage with no additional effect. - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ + $ref: '#/components/schemas/MoveEffectEffectText' readOnly: true effect_changes: type: array items: - type: object - required: - - effect_entries - - version_group - properties: - effect_entries: - type: array - items: - type: object - required: - - effect - - language - properties: - effect: - type: string - examples: - - Hits Pokémon under the effects of dig and fly. - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ - version_group: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - gold-silver - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version-group/3/ + $ref: '#/components/schemas/MoveEffectChange' readOnly: true generation: $ref: '#/components/schemas/GenerationSummary' @@ -5763,31 +5015,7 @@ components: stat_changes: type: array items: - type: object - required: - - change - - stat - properties: - change: - type: integer - format: int32 - examples: - - 2 - stat: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - attack - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/stat/1/ + $ref: '#/components/schemas/MoveMetaStatChange' readOnly: true super_contest_effect: $ref: '#/components/schemas/SuperContestEffectSummary' @@ -5798,36 +5026,7 @@ components: machines: type: array items: - type: object - required: - - machine - - version_group - properties: - machine: - type: object - required: - - url - properties: - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/machine/1/ - version_group: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - sword-shield - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version-group/1/ + $ref: '#/components/schemas/ItemMachine' readOnly: true flavor_text_entries: type: array @@ -5837,20 +5036,7 @@ components: learned_by_pokemon: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - clefairy - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon/35/ + $ref: '#/components/schemas/PokemonSummary' readOnly: true required: - contest_combos @@ -5873,6 +5059,45 @@ components: - super_contest_effect - target - type + MoveEffectChange: + type: object + properties: + version_group: + $ref: '#/components/schemas/VersionGroupSummary' + effect_entries: + type: array + items: + $ref: '#/components/schemas/MoveEffectChangeEffectText' + readOnly: true + required: + - effect_entries + - version_group + MoveEffectChangeEffectText: + type: object + properties: + effect: + type: string + maxLength: 6000 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - effect + - language + MoveEffectEffectText: + type: object + properties: + effect: + type: string + maxLength: 6000 + short_effect: + type: string + maxLength: 300 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - effect + - language + - short_effect MoveFlavorText: type: object properties: @@ -5918,20 +5143,7 @@ components: version_groups: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - red-blue - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version-group/1/ + $ref: '#/components/schemas/VersionGroupSummary' readOnly: true required: - descriptions @@ -6045,20 +5257,7 @@ components: moves: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - thunder-punch - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/9/ + $ref: '#/components/schemas/MoveSummary' readOnly: true names: type: array @@ -6121,20 +5320,7 @@ components: moves: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - sing - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/47/ + $ref: '#/components/schemas/MoveSummary' readOnly: true required: - descriptions @@ -6154,6 +5340,18 @@ components: required: - name - url + MoveMetaStatChange: + type: object + properties: + change: + type: integer + maximum: 2147483647 + minimum: -2147483648 + stat: + $ref: '#/components/schemas/StatSummary' + required: + - change + - stat MoveName: type: object properties: @@ -6165,6 +5363,18 @@ components: required: - language - name + MoveStatChange: + type: object + properties: + change: + type: integer + maximum: 2147483647 + minimum: -2147483648 + move: + $ref: '#/components/schemas/MoveSummary' + required: + - change + - move MoveSummary: type: object properties: @@ -6284,31 +5494,7 @@ components: pokeathlon_stat_changes: type: array items: - type: object - required: - - max_change - - pokeathlon_stat - properties: - max_change: - type: integer - format: int32 - examples: - - 1 - pokeathlon_stat: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - power - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokeathlon-stat/2/ + $ref: '#/components/schemas/NaturePokeathlonStat' readOnly: true move_battle_style_preferences: type: array @@ -6342,6 +5528,18 @@ components: required: - language - name + NaturePokeathlonStat: + type: object + properties: + max_change: + type: integer + maximum: 2147483647 + minimum: -2147483648 + pokeathlon_stat: + $ref: '#/components/schemas/PokeathlonStatSummary' + required: + - max_change + - pokeathlon_stat NatureSummary: type: object properties: @@ -7499,37 +6697,7 @@ components: pokemon_encounters: type: array items: - type: object - required: - - base_score - - pokemon-species - - rate - properties: - base_score: - type: integer - format: int32 - examples: - - 50 - pokemon-species: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bulbasaur - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-species/1/ - rate: - type: integer - format: int32 - examples: - - 30 + $ref: '#/components/schemas/PalParkEncounter' readOnly: true required: - id @@ -7560,79 +6728,81 @@ components: required: - name - url - PokeathlonStatDetail: + PalParkEncounter: type: object properties: - id: - type: integer - readOnly: true - name: - type: string + base_score: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + rate: + type: integer + maximum: 2147483647 + minimum: -2147483648 + pokemon_species: + $ref: '#/components/schemas/PokemonSpeciesSummary' + required: + - pokemon_species + - rate + PokeapiMetaResponse: + type: object + properties: + deploy_date: + type: + - string + - 'null' + hash: + type: + - string + - 'null' + tag: + type: + - string + - 'null' + required: + - deploy_date + - hash + - tag + PokeathlonStatAffectingNature: + type: object + properties: + max_change: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nature: + $ref: '#/components/schemas/NatureSummary' + required: + - max_change + - nature + PokeathlonStatAffectingNatures: + type: object + properties: + increase: + type: array + items: + $ref: '#/components/schemas/PokeathlonStatAffectingNature' + decrease: + type: array + items: + $ref: '#/components/schemas/PokeathlonStatAffectingNature' + required: + - decrease + - increase + PokeathlonStatDetail: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string maxLength: 200 affecting_natures: - type: object - required: - - decrease - - increase - properties: - decrease: - type: array - items: - type: object - required: - - max_change - - nature - properties: - max_change: - type: integer - format: int32 - maximum: -1 - examples: - - -1 - nature: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - hardy - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/nature/1/ - increase: - type: array - items: - type: object - required: - - max_change - - nature - properties: - max_change: - type: integer - format: int32 - minimum: 1 - examples: - - 2 - nature: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - hardy - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/nature/1/ + allOf: + - $ref: '#/components/schemas/PokeathlonStatAffectingNatures' readOnly: true names: type: array @@ -7702,51 +6872,14 @@ components: pokemon_entries: type: array items: - type: object - required: - - entry_number - - pokemon_species - properties: - entry_number: - type: integer - format: int32 - examples: - - 1 - pokemon_species: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bulbasaur - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-species/1/ + $ref: '#/components/schemas/PokemonDexNumber' readOnly: true region: $ref: '#/components/schemas/RegionSummary' version_groups: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - the-teal-mask - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version-group/26/ + $ref: '#/components/schemas/VersionGroupSummary' readOnly: true required: - descriptions @@ -7780,6 +6913,34 @@ components: required: - name - url + PokemonAbility: + type: object + properties: + is_hidden: + type: boolean + slot: + type: integer + maximum: 2147483647 + minimum: -2147483648 + ability: + $ref: '#/components/schemas/AbilitySummary' + required: + - ability + - slot + PokemonAbilityPast: + type: object + properties: + is_hidden: + type: boolean + slot: + type: integer + maximum: 2147483647 + minimum: -2147483648 + ability: + $ref: '#/components/schemas/AbilitySummary' + required: + - ability + - slot PokemonColorDetail: type: object properties: @@ -7828,6 +6989,20 @@ components: required: - name - url + PokemonCries: + type: object + properties: + latest: + type: + - string + - 'null' + legacy: + type: + - string + - 'null' + required: + - latest + - legacy PokemonDetail: type: object properties: @@ -7866,89 +7041,12 @@ components: abilities: type: array items: - type: object - required: - - ability - - is_hidden - - slot - properties: - ability: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - sand-veil - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/ability/8/ - is_hidden: - type: boolean - slot: - type: integer - format: int32 - examples: - - 1 + $ref: '#/components/schemas/PokemonAbility' readOnly: true past_abilities: type: array items: - type: object - required: - - abilities - - generation - properties: - abilities: - type: array - items: - type: object - required: - - ability - - is_hidden - - slot - properties: - ability: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - levitate - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/ability/26/ - is_hidden: - type: boolean - slot: - type: integer - format: int32 - examples: - - 1 - generation: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - generation-vi - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/generation/6/ + $ref: '#/components/schemas/PokemonPastAbility' readOnly: true forms: type: array @@ -7963,169 +7061,25 @@ components: held_items: type: array items: - type: object - required: - - item - - version_details - properties: - item: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - soft-sand - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/item/214/ - version_details: - type: array - items: - type: object - required: - - rarity - - version - properties: - rarity: - type: integer - format: int32 - examples: - - 5 - version: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - diamond - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version/12/ + $ref: '#/components/schemas/PokemonHeldItem' readOnly: true location_area_encounters: type: string - examples: - - https://pokeapi.co/api/v2/pokemon/1/encounters readOnly: true moves: type: array items: - type: object - required: - - move - - version_group_details - properties: - move: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - scratch - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/10/ - version_group_details: - type: array - items: - type: object - required: - - level_learned_at - - move_learn_method - - version_group - properties: - level_learned_at: - type: integer - format: int32 - examples: - - 1 - move_learn_method: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - level-up - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move-learn-method/1/ - version_group: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - red-blue - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version-group/1/ + $ref: '#/components/schemas/PokemonMove' readOnly: true species: $ref: '#/components/schemas/PokemonSpeciesSummary' sprites: - type: object - properties: - front_default: - type: string - format: uri - exmaple: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/1.png - additionalProperties: - type: string - format: uri - nullable: true - examples: - - https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/1.png - examples: - - back_default: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/1.png - back_female: null - back_shiny: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/1.png - back_shiny_female: null - front_default: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png - front_female: null - front_shiny: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/1.png - front_shiny_female: null + allOf: + - $ref: '#/components/schemas/PokemonSprites' readOnly: true cries: - type: object - required: - - latest - - legacy - properties: - latest: - type: string - format: uri - examples: - - https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/latest/50.ogg - legacy: - type: string - format: uri - examples: - - https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/legacy/50.ogg + allOf: + - $ref: '#/components/schemas/PokemonCries' readOnly: true stats: type: array @@ -8135,141 +7089,17 @@ components: past_stats: type: array items: - type: object - required: - - generation - - stats - properties: - generation: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - generation-vi - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/generation/6/ - stats: - type: array - items: - type: object - required: - - base_stat - - effort - - stat - properties: - base_stat: - type: integer - format: int32 - examples: - - 45 - effort: - type: integer - format: int32 - examples: - - 0 - stat: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - speed - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/stat/6/ + $ref: '#/components/schemas/PokemonPastStat' readOnly: true types: type: array items: - type: object - required: - - slot - - type - properties: - slot: - type: integer - format: int32 - examples: - - 1 - type: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - ghost - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/8/ + $ref: '#/components/schemas/PokemonType' readOnly: true past_types: type: array items: - type: object - required: - - generation - - types - properties: - generation: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - generation-v - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/generation/5/ - types: - type: array - items: - type: object - required: - - slot - - type - properties: - slot: - type: integer - format: int32 - examples: - - 1 - type: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - normal - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/1/ + $ref: '#/components/schemas/PokemonPastType' readOnly: true required: - abilities @@ -8298,6 +7128,175 @@ components: required: - entry_number - pokedex + PokemonDexNumber: + type: object + properties: + entry_number: + type: integer + pokemon_species: + $ref: '#/components/schemas/PokemonSpeciesSummary' + required: + - entry_number + - pokemon_species + PokemonEncounterDetailResponse: + type: object + properties: + chance: + type: integer + condition_values: + type: array + items: + $ref: '#/components/schemas/EncounterConditionValueSummary' + max_level: + type: integer + method: + $ref: '#/components/schemas/EncounterMethodSummary' + min_level: + type: integer + required: + - chance + - condition_values + - max_level + - method + - min_level + PokemonEncounterResponse: + type: object + properties: + location_area: + $ref: '#/components/schemas/LocationAreaSummary' + version_details: + type: array + items: + $ref: '#/components/schemas/PokemonEncounterVersionDetailResponse' + required: + - location_area + - version_details + PokemonEncounterVersionDetailResponse: + type: object + properties: + version: + $ref: '#/components/schemas/VersionSummary' + max_chance: + type: integer + encounter_details: + type: array + items: + $ref: '#/components/schemas/PokemonEncounterDetailResponse' + required: + - encounter_details + - max_chance + - version + PokemonEvolution: + type: object + properties: + version_group: + $ref: '#/components/schemas/VersionGroupSummary' + is_default: + type: boolean + item: + $ref: '#/components/schemas/ItemSummary' + trigger: + $ref: '#/components/schemas/EvolutionTriggerSummary' + gender: + type: + - integer + - 'null' + held_item: + $ref: '#/components/schemas/ItemSummary' + known_move: + $ref: '#/components/schemas/MoveSummary' + known_move_type: + $ref: '#/components/schemas/TypeSummary' + location: + $ref: '#/components/schemas/LocationSummary' + min_level: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + min_happiness: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + min_beauty: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + min_affection: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + near_special_rock: + type: boolean + needs_multiplayer: + type: boolean + needs_overworld_rain: + type: boolean + party_species: + $ref: '#/components/schemas/PokemonSpeciesSummary' + party_type: + $ref: '#/components/schemas/TypeSummary' + relative_physical_stats: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + time_of_day: + type: string + maxLength: 10 + trade_species: + $ref: '#/components/schemas/PokemonSpeciesSummary' + turn_upside_down: + type: boolean + region: + $ref: '#/components/schemas/RegionSummary' + base_form: + $ref: '#/components/schemas/PokemonSummary' + evolved_form: + $ref: '#/components/schemas/PokemonSummary' + used_move: + $ref: '#/components/schemas/MoveSummary' + min_move_count: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + min_steps: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + min_damage_taken: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + required: + - base_form + - evolved_form + - held_item + - item + - known_move + - known_move_type + - location + - party_species + - party_type + - region + - trade_species + - trigger + - used_move + - version_group PokemonFormDetail: type: object properties: @@ -8331,151 +7330,30 @@ components: pokemon: $ref: '#/components/schemas/PokemonSummary' sprites: - type: object - properties: - default: - type: string - format: uri - examples: - - https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/412.png - additionalProperties: - type: string - format: uri - nullable: true - examples: - - https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/412.png - examples: - - back_default: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/412.png - back_female: null - back_shiny: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/412.png - back_shiny_female: null - front_default: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/412.png - front_female: null - front_shiny: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/412.png - front_shiny_female: null + allOf: + - $ref: '#/components/schemas/PokemonFormSprites' readOnly: true version_group: $ref: '#/components/schemas/VersionGroupSummary' form_names: type: array items: - type: object - required: - - language - - name - properties: - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ - name: - type: string - examples: - - Plant Cloak + $ref: '#/components/schemas/PokemonFormName' readOnly: true names: type: array items: - type: object - required: - - language - - name - properties: - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ - name: - type: string - examples: - - Plant Cloak + $ref: '#/components/schemas/PokemonFormName' readOnly: true types: type: array items: - type: object - required: - - slot - - type - properties: - slot: - type: integer - format: int32 - examples: - - 1 - type: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bug - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/7/ + $ref: '#/components/schemas/PokemonFormType' readOnly: true trigger_conditions: type: array items: - type: object - required: - - trigger - - name - - url - properties: - trigger: - type: string - examples: - - held-item - name: - type: string - examples: - - venusaurite - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/item/698/ - base_form: - type: object - nullable: true - properties: - name: - type: string - examples: - - necrozma-dusk - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon-form/10314/ + $ref: '#/components/schemas/PokemonFormTriggerCondition' readOnly: true required: - form_name @@ -8488,6 +7366,23 @@ components: - trigger_conditions - types - version_group + PokemonFormName: + type: object + properties: + name: + type: string + maxLength: 200 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - language + - name + PokemonFormSprites: + type: object + properties: + sprites: {} + required: + - sprites PokemonFormSummary: type: object properties: @@ -8501,6 +7396,28 @@ components: required: - name - url + PokemonFormTriggerCondition: + type: object + properties: + trigger: + type: string + base_form: + $ref: '#/components/schemas/PokemonFormSummary' + required: + - base_form + - trigger + PokemonFormType: + type: object + properties: + slot: + type: integer + maximum: 2147483647 + minimum: -2147483648 + type: + $ref: '#/components/schemas/TypeSummary' + required: + - slot + - type PokemonGameIndex: type: object properties: @@ -8561,6 +7478,102 @@ components: required: - name - url + PokemonHeldItem: + type: object + properties: + pokemon: + $ref: '#/components/schemas/PokemonSummary' + version_details: + type: array + items: + $ref: '#/components/schemas/PokemonHeldItemVersion' + required: + - pokemon + - version_details + PokemonHeldItemVersion: + type: object + properties: + rarity: + type: integer + version: + $ref: '#/components/schemas/VersionSummary' + required: + - rarity + - version + PokemonMove: + type: object + properties: + move: + $ref: '#/components/schemas/MoveSummary' + version_group_details: + type: array + items: + $ref: '#/components/schemas/PokemonMoveVersionGroup' + required: + - move + - version_group_details + PokemonMoveVersionGroup: + type: object + properties: + level_learned_at: + type: integer + move_learn_method: + $ref: '#/components/schemas/MoveLearnMethodSummary' + version_group: + $ref: '#/components/schemas/VersionGroupSummary' + order: + type: integer + required: + - level_learned_at + - move_learn_method + - version_group + PokemonPastAbility: + type: object + properties: + abilities: + type: array + items: + $ref: '#/components/schemas/PokemonAbilityPast' + generation: + $ref: '#/components/schemas/GenerationSummary' + required: + - abilities + - generation + PokemonPastStat: + type: object + properties: + generation: + $ref: '#/components/schemas/GenerationSummary' + stats: + type: array + items: + $ref: '#/components/schemas/PokemonStat' + required: + - generation + - stats + PokemonPastType: + type: object + properties: + generation: + $ref: '#/components/schemas/GenerationSummary' + types: + type: array + items: + $ref: '#/components/schemas/TypePokemon' + required: + - generation + - types + PokemonShapeAwesomeName: + type: object + properties: + awesome_name: + type: string + maxLength: 30 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - awesome_name + - language PokemonShapeDetail: type: object properties: @@ -8573,48 +7586,12 @@ components: awesome_names: type: array items: - type: object - required: - - awesome_name - - language - properties: - awesome_name: - type: string - examples: - - Pomaceous - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ + $ref: '#/components/schemas/PokemonShapeAwesomeName' readOnly: true names: type: array items: - type: object - required: - - url - - name - properties: - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ - name: - type: string - examples: - - Ball + $ref: '#/components/schemas/PokemonShapeName' readOnly: true pokemon_species: type: array @@ -8627,6 +7604,17 @@ components: - name - names - pokemon_species + PokemonShapeName: + type: object + properties: + name: + type: string + maxLength: 200 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - language + - name PokemonShapeSummary: type: object properties: @@ -8709,20 +7697,7 @@ components: egg_groups: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - monster - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/egg-group/1/ + $ref: '#/components/schemas/EggGroupSummary' readOnly: true color: $ref: '#/components/schemas/PokemonColorSummary' @@ -8739,65 +7714,12 @@ components: names: type: array items: - type: object - required: - - language - - name - properties: - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ - name: - type: string - examples: - - bulbasaur + $ref: '#/components/schemas/PokemonSpeciesName' readOnly: true pal_park_encounters: type: array items: - type: object - required: - - area - - base_score - - rate - properties: - area: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - field - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pal-park-area/2/ - base_score: - type: integer - format: int32 - examples: - - 50 - rate: - type: integer - format: int32 - examples: - - 30 + $ref: '#/components/schemas/PokemonSpeciesPalParkEncounter' readOnly: true form_descriptions: type: array @@ -8812,56 +7734,12 @@ components: genera: type: array items: - type: object - required: - - genus - - language - properties: - genus: - type: string - examples: - - Seed Pokémon - language: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - en - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/language/9/ + $ref: '#/components/schemas/PokemonSpeciesGenus' readOnly: true varieties: type: array items: - type: object - required: - - is_default - - pokemon - properties: - is_default: - type: boolean - pokemon: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bulbasaur - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokemon/1/ + $ref: '#/components/schemas/PokemonSpeciesVariety' readOnly: true required: - color @@ -8894,6 +7772,46 @@ components: - flavor_text - language - version + PokemonSpeciesGenus: + type: object + properties: + genus: + type: string + maxLength: 30 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - genus + - language + PokemonSpeciesName: + type: object + properties: + name: + type: string + maxLength: 200 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - language + - name + PokemonSpeciesPalParkEncounter: + type: object + properties: + base_score: + type: + - integer + - 'null' + maximum: 2147483647 + minimum: -2147483648 + rate: + type: integer + maximum: 2147483647 + minimum: -2147483648 + area: + $ref: '#/components/schemas/PalParkAreaSummary' + required: + - area + - rate PokemonSpeciesSummary: type: object properties: @@ -8907,6 +7825,25 @@ components: required: - name - url + PokemonSpeciesVariety: + type: object + properties: + is_default: + type: boolean + pokemon: + $ref: '#/components/schemas/PokemonSummary' + required: + - is_default + - pokemon + PokemonSprites: + type: object + properties: + front_default: + type: + - string + - 'null' + required: + - front_default PokemonStat: type: object properties: @@ -8937,6 +7874,18 @@ components: required: - name - url + PokemonType: + type: object + properties: + slot: + type: integer + maximum: 2147483647 + minimum: -2147483648 + type: + $ref: '#/components/schemas/TypeSummary' + required: + - slot + - type RegionDetail: type: object properties: @@ -8969,20 +7918,7 @@ components: version_groups: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - red-blue - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/version-group/1/ + $ref: '#/components/schemas/VersionGroupSummary' readOnly: true required: - id @@ -9016,6 +7952,34 @@ components: required: - name - url + StatAffectingMoves: + type: object + properties: + increase: + type: array + items: + $ref: '#/components/schemas/MoveStatChange' + decrease: + type: array + items: + $ref: '#/components/schemas/MoveStatChange' + required: + - decrease + - increase + StatAffectingNatures: + type: object + properties: + increase: + type: array + items: + $ref: '#/components/schemas/NatureSummary' + decrease: + type: array + items: + $ref: '#/components/schemas/NatureSummary' + required: + - decrease + - increase StatDetail: type: object properties: @@ -9032,127 +7996,17 @@ components: is_battle_only: type: boolean affecting_moves: - type: object - required: - - decrease - - increase - properties: - increase: - type: array - items: - type: object - required: - - change - - move - properties: - change: - type: integer - format: int32 - examples: - - -1 - move: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - swords-dance - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/14/ - decrease: - type: array - items: - type: object - required: - - change - - move - properties: - change: - type: integer - format: int32 - examples: - - 5 - move: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - growl - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move/45/ + allOf: + - $ref: '#/components/schemas/StatAffectingMoves' readOnly: true affecting_natures: - type: object - required: - - increase - - decrease - properties: - increase: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - lonely - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/nature/6/ - decrease: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bold - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/nature/2/ + allOf: + - $ref: '#/components/schemas/StatAffectingNatures' readOnly: true affecting_items: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - protein - - x-attack - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/item/46/ + $ref: '#/components/schemas/ItemSummary' readOnly: true characteristics: type: array @@ -9256,253 +8110,13 @@ components: type: string maxLength: 200 damage_relations: - type: object - required: - - no_damage_to - - half_damage_to - - double_damage_to - - no_damage_from - - half_damage_from - - double_damage_from - properties: - no_damage_to: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - flying - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/3/ - half_damage_to: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bug - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/7/ - double_damage_to: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - poison - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/4/ - no_damage_from: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - electric - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/13/ - half_damage_from: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - poison - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/4/ - double_damage_from: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - water - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/11/ + allOf: + - $ref: '#/components/schemas/TypeRelationships' readOnly: true past_damage_relations: type: array items: - type: object - required: - - generation - - damage_relations - properties: - generation: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - generation-v - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/generation/5/ - damage_relations: - type: object - required: - - no_damage_to - - half_damage_to - - double_damage_to - - no_damage_from - - half_damage_from - - double_damage_from - properties: - no_damage_to: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - flying - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/3/ - half_damage_to: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - bug - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/7/ - double_damage_to: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - poison - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/4/ - no_damage_from: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - electric - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/13/ - half_damage_from: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - poison - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/4/ - double_damage_from: - type: array - items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - water - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/type/11/ + $ref: '#/components/schemas/TypePastRelationships' readOnly: true game_indices: type: array @@ -9516,37 +8130,12 @@ components: names: type: array items: - $ref: '#/components/schemas/AbilityName' + $ref: '#/components/schemas/TypeName' readOnly: true pokemon: type: array items: - type: object - required: - - potency - - flavor - properties: - slot: - type: integer - examples: - - 1 - pokemon: - type: object - require: - - name - - url - properties: - name: - type: string - description: The name of the pokemon - examples: - - sandshrew - url: - type: string - format: uri - description: The URL to get more information about the pokemon - examples: - - https://pokeapi.co/api/v2/pokemon/27/ + $ref: '#/components/schemas/TypePokemon' readOnly: true moves: type: array @@ -9554,37 +8143,8 @@ components: $ref: '#/components/schemas/MoveSummary' readOnly: true sprites: - type: object - additionalProperties: - type: object - additionalProperties: - type: object - properties: - name-icon: - type: string - format: uri - examples: - - https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/colosseum/1.png - examples: - - colosseum: - name_icon: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/colosseum/1.png - examples: - - generation-ix: - scarlet-violet: - name_icon: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-ix/scarlet-violet/1.png - examples: - - sprites: - generation-iii: - colosseum: - name_icon: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/colosseum/1.png - emerald: - name_icon: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/emerald/1.png - firered-leafgreen: - name_icon: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/firered-leafgreen/1.png - ruby-sapphire: - name_icon: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/ruby-sapphire/1.png - xd: - name_icon: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/xd/1.png + allOf: + - $ref: '#/components/schemas/TypeSprite' readOnly: true required: - damage_relations @@ -9610,6 +8170,79 @@ components: required: - game_index - generation + TypeName: + type: object + properties: + name: + type: string + maxLength: 200 + language: + $ref: '#/components/schemas/LanguageSummary' + required: + - language + - name + TypePastRelationships: + type: object + properties: + generation: + $ref: '#/components/schemas/GenerationSummary' + damage_relations: + $ref: '#/components/schemas/TypeRelationships' + required: + - damage_relations + - generation + TypePokemon: + type: object + properties: + slot: + type: integer + maximum: 2147483647 + minimum: -2147483648 + pokemon: + $ref: '#/components/schemas/PokemonSummary' + required: + - pokemon + - slot + TypeRelationships: + type: object + properties: + no_damage_to: + type: array + items: + $ref: '#/components/schemas/TypeSummary' + half_damage_to: + type: array + items: + $ref: '#/components/schemas/TypeSummary' + double_damage_to: + type: array + items: + $ref: '#/components/schemas/TypeSummary' + no_damage_from: + type: array + items: + $ref: '#/components/schemas/TypeSummary' + half_damage_from: + type: array + items: + $ref: '#/components/schemas/TypeSummary' + double_damage_from: + type: array + items: + $ref: '#/components/schemas/TypeSummary' + required: + - double_damage_from + - double_damage_to + - half_damage_from + - half_damage_to + - no_damage_from + - no_damage_to + TypeSprite: + type: object + properties: + sprites: {} + required: + - sprites TypeSummary: type: object properties: @@ -9668,56 +8301,17 @@ components: move_learn_methods: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - level-up - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/move-learn-method/1/ + $ref: '#/components/schemas/MoveLearnMethodSummary' readOnly: true pokedexes: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - kanto - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/pokedex/2/ + $ref: '#/components/schemas/PokedexSummary' readOnly: true regions: type: array items: - type: object - required: - - name - - url - properties: - name: - type: string - examples: - - kanto - url: - type: string - format: uri - examples: - - https://pokeapi.co/api/v2/region/1/ + $ref: '#/components/schemas/RegionSummary' readOnly: true versions: type: array diff --git a/pokemon_v2/api.py b/pokemon_v2/api.py index e69d6ef8c..75039128c 100644 --- a/pokemon_v2/api.py +++ b/pokemon_v2/api.py @@ -1,71 +1,154 @@ +# ruff: noqa: F405, E501 +from __future__ import annotations + +import itertools import re import subprocess -from rest_framework import viewsets -from rest_framework.response import Response -from rest_framework.views import APIView -from django.shortcuts import get_object_or_404 +from typing import TYPE_CHECKING, Any, cast + +from django.core.exceptions import FieldError +from django.db.models import Q, QuerySet from django.http import Http404 -from django.db.models import Q -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter +from django.shortcuts import get_object_or_404 from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import ( + OpenApiParameter, + extend_schema, # pyright: ignore[reportUnknownVariableType] + extend_schema_view, # pyright: ignore[reportUnknownVariableType] +) +from rest_framework import serializers, viewsets +from rest_framework.response import Response +from rest_framework.views import APIView +from typing_extensions import override + +from .models import * # noqa: F403 +from .serializers import * # noqa: F403 + +if TYPE_CHECKING: + from rest_framework.request import Request + from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList + +__all__: tuple[str, ...] = ( + "AbilityResource", + "BerryFirmnessResource", + "BerryFlavorResource", + "BerryResource", + "CharacteristicResource", + "ContestEffectResource", + "ContestTypeResource", + "CurrencyResource", + "EggGroupResource", + "EncounterConditionResource", + "EncounterConditionValueResource", + "EncounterMethodResource", + "EvolutionChainResource", + "EvolutionTriggerResource", + "GenderResource", + "GenerationResource", + "GrowthRateResource", + "ItemAttributeResource", + "ItemCategoryResource", + "ItemFlingEffectResource", + "ItemPocketResource", + "ItemResource", + "LanguageResource", + "ListOrDetailSerialRelation", + "LocationAreaResource", + "LocationResource", + "MachineResource", + "MoveBattleStyleResource", + "MoveDamageClassResource", + "MoveLearnMethodResource", + "MoveMetaAilmentResource", + "MoveMetaCategoryResource", + "MoveResource", + "MoveTargetResource", + "NameOrIdRetrieval", + "NatureResource", + "PalParkAreaResource", + "PokeapiCommonViewset", + "PokeapiMetaResponseSerializer", + "PokeapiMetaView", + "PokeathlonStatResource", + "PokedexResource", + "PokemonColorResource", + "PokemonEncounterDetailResponseSerializer", + "PokemonEncounterResponseSerializer", + "PokemonEncounterVersionDetailResponseSerializer", + "PokemonEncounterView", + "PokemonFormResource", + "PokemonHabitatResource", + "PokemonResource", + "PokemonShapeResource", + "PokemonSpeciesResource", + "RegionResource", + "StatResource", + "SuperContestEffectResource", + "TypeResource", + "VersionGroupResource", + "VersionResource", +) -from .models import * -from .serializers import * - -# pylint: disable=no-member, attribute-defined-outside-init ########################### # BEHAVIOR ABSTRACTIONS # ########################### -class ListOrDetailSerialRelation: +class ListOrDetailSerialRelation(viewsets.GenericViewSet[Any]): """ - Mixin to allow association with separate serializers - for list or detail view. + Mixin to allow association with separate serializers for list or detail view. """ - list_serializer_class = None + list_serializer_class: type[serializers.BaseSerializer[Any]] | None = None - def get_serializer_class(self): - if self.action == "list" and self.list_serializer_class is not None: - return self.list_serializer_class + @override + def get_serializer_class(self) -> type[serializers.BaseSerializer[Any]]: + if self.action == "list": + if self.list_serializer_class is not None: + return self.list_serializer_class + raise AttributeError("list_serializer_class must be set for list view") + if self.serializer_class is None: + raise AttributeError("serializer_class must be set for detail view") return self.serializer_class -class NameOrIdRetrieval: +class NameOrIdRetrieval(viewsets.GenericViewSet[Any]): """ - Mixin to allow retrieval of resources by - pk (in this case ID) or by name + Mixin to allow retrieval of resources by pk (in this case ID) or by name. """ - idPattern = re.compile(r"^-?[0-9]+$") + ID_PATTERN = re.compile(r"^-?[0-9]+$") # Allow alphanumeric, hyphen, plus, and space (Space added for test cases using name for lookup, ex: 'base pkm') - namePattern = re.compile(r"^[0-9A-Za-z\-\+ ]+$") + NAME_PATTERN = re.compile(r"^[0-9A-Za-z\-\+ ]+$") - def get_queryset(self): + @override + def get_queryset(self) -> QuerySet[Any]: queryset = super().get_queryset() - filter = self.request.GET.get("q", "") + filter_q = self.request.GET.get("q", "") - if filter: - queryset = queryset.filter(Q(name__icontains=filter)) + if filter_q: + queryset = queryset.filter(Q(name__icontains=filter_q)) return queryset - def get_object(self): - queryset = self.get_queryset() - queryset = self.filter_queryset(queryset) + @override + def get_object(self) -> Any: + queryset = self.filter_queryset(self.get_queryset()) lookup = self.kwargs["pk"] - if self.idPattern.match(lookup): + if self.ID_PATTERN.match(lookup): lookup_id = int(lookup) if abs(lookup_id) > 2147483647: raise Http404 - resp = get_object_or_404(queryset, pk=lookup) + resp = get_object_or_404(queryset, pk=lookup_id) - elif self.namePattern.match(lookup): - resp = get_object_or_404(queryset, name__iexact=lookup) + elif self.NAME_PATTERN.match(lookup): + try: + resp = get_object_or_404(queryset, name__iexact=lookup) + except FieldError as err: + raise Http404 from err else: raise Http404 @@ -90,12 +173,11 @@ def get_object(self): @extend_schema_view(list=extend_schema(parameters=[q_query_string_parameter])) -class PokeapiCommonViewset(ListOrDetailSerialRelation, NameOrIdRetrieval, viewsets.ReadOnlyModelViewSet): +class PokeapiCommonViewset(ListOrDetailSerialRelation, NameOrIdRetrieval, viewsets.ReadOnlyModelViewSet[Any]): + @override @extend_schema(parameters=[retrieve_path_parameter]) - def retrieve(self, request, pk=None): - return super().retrieve(request, pk) - - pass + def retrieve(self, request: Request, *args: Any, pk: str | int | None = None, **kwargs: Any) -> Response: + return super().retrieve(request, *args, pk=pk, **kwargs) ########## @@ -114,7 +196,7 @@ def retrieve(self, request, pk=None): ) ) class AbilityResource(PokeapiCommonViewset): - queryset = Ability.objects.all() + queryset = Ability.objects.select_related("generation") serializer_class = AbilityDetailSerializer list_serializer_class = AbilitySummarySerializer @@ -497,7 +579,7 @@ class LocationResource(PokeapiCommonViewset): summary="List location areas", ) ) -class LocationAreaResource(ListOrDetailSerialRelation, viewsets.ReadOnlyModelViewSet): +class LocationAreaResource(ListOrDetailSerialRelation, viewsets.ReadOnlyModelViewSet[LocationArea]): queryset = LocationArea.objects.all() serializer_class = LocationAreaDetailSerializer list_serializer_class = LocationAreaSummarySerializer @@ -770,7 +852,7 @@ class PokemonShapeResource(PokeapiCommonViewset): ), ) class PokemonResource(PokeapiCommonViewset): - queryset = Pokemon.objects.all() + queryset = Pokemon.objects.select_related("pokemon_species").prefetch_related("pokemonsprites", "pokemoncries") serializer_class = PokemonDetailSerializer list_serializer_class = PokemonSummarySerializer @@ -887,204 +969,105 @@ class VersionGroupResource(PokeapiCommonViewset): list_serializer_class = VersionGroupSummarySerializer -@extend_schema( - description="Handles Pokemon Encounters as a sub-resource.", - summary="Get pokemon encounter", - tags=["encounters"], - responses={ - "200": { - "type": "array", - "items": { - "type": "object", - "required": ["location_area", "version_details"], - "properties": { - "location_area": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "example": "cerulean-city-area"}, - "url": { - "type": "string", - "format": "uri", - "example": "https://pokeapi.co/api/v2/location-area/281/", - }, - }, - }, - "version_details": { - "type": "array", - "items": { - "type": "object", - "required": ["encounter_details", "max_chance", "version"], - "properties": { - "encounter_details": { - "type": "array", - "items": { - "type": "object", - "required": [ - "chance", - "condition_values", - "max_level", - "method", - "min_level", - ], - "properties": { - "chance": { - "type": "number", - "example": 100, - }, - "condition_values": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "example": "story-progress-beat-red", - }, - "url": { - "type": "string", - "format": "uri", - "example": "https://pokeapi.co/api/v2/encounter-condition-value/55/", - }, - }, - }, - }, - "max_level": { - "type": "number", - "example": 10, - }, - "method": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "example": "gift", - }, - "url": { - "type": "string", - "format": "uri", - "example": "https://pokeapi.co/api/v2/encounter-method/18/", - }, - }, - }, - "min_level": { - "type": "number", - "example": 10, - }, - }, - }, - }, - "max_chance": {"type": "number", "example": 100}, - "version": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "example": "red"}, - "url": { - "type": "string", - "format": "uri", - "example": "https://pokeapi.co/api/v2/version/1/", - }, - }, - }, - }, - }, - }, - }, - }, - } - }, -) -class PokemonEncounterView(APIView): - def get(self, request, pokemon_id): - self.context = dict(request=request) - - try: - pokemon = Pokemon.objects.get(pk=pokemon_id) - except Pokemon.DoesNotExist: - raise Http404 - - encounter_objects = Encounter.objects.filter(pokemon=pokemon) - - area_ids = encounter_objects.values_list("location_area", flat=True).distinct().order_by("location_area") - - location_area_objects = LocationArea.objects.filter(pk__in=area_ids) - version_objects = Version.objects - - encounters_list = [] - - for area_id in area_ids: - location_area = location_area_objects.get(pk=area_id) +class PokemonEncounterDetailResponseSerializer(serializers.Serializer[dict[str, Any]]): + chance = serializers.IntegerField() + condition_values = EncounterConditionValueSummarySerializer(many=True) + max_level = serializers.IntegerField() + method = EncounterMethodSummarySerializer() + min_level = serializers.IntegerField() - area_encounters = encounter_objects.filter(location_area_id=area_id) - version_ids = area_encounters.values_list("version_id", flat=True).distinct().order_by("version_id") - version_details_list = [] +class PokemonEncounterVersionDetailResponseSerializer(serializers.Serializer[dict[str, Any]]): + version = VersionSummarySerializer() + max_chance = serializers.IntegerField() + encounter_details = PokemonEncounterDetailResponseSerializer(many=True) - for version_id in version_ids: - version = version_objects.get(pk=version_id) - version_encounters = area_encounters.filter(version_id=version_id).order_by("encounter_slot_id") +class PokemonEncounterResponseSerializer(serializers.Serializer[dict[str, Any]]): + location_area = LocationAreaSummarySerializer() + version_details = PokemonEncounterVersionDetailResponseSerializer(many=True) - encounters_data = EncounterDetailSerializer(version_encounters, many=True, context=self.context).data - max_chance = 0 - encounter_details_list = [] +class PokemonLocationAreaEncounterSerializer(serializers.Serializer[Any]): + location_area = LocationAreaSummarySerializer() + version_details = LocationAreaPokemonEncounterVersionSerializer(many=True) - for encounter in encounters_data: - slot = EncounterSlot.objects.get(pk=encounter["encounter_slot"]) - slot_data = EncounterSlotSerializer(slot, context=self.context).data - del encounter["pokemon"] - del encounter["encounter_slot"] - del encounter["location_area"] - del encounter["version"] - encounter["chance"] = slot_data["chance"] - max_chance += slot_data["chance"] - encounter["method"] = slot_data["encounter_method"] +@extend_schema( + description="Handles Pokemon Encounters as a sub-resource.", + summary="Get pokemon encounter", + tags=["encounters"], + responses={"200": PokemonEncounterResponseSerializer(many=True)}, +) +class PokemonEncounterView(APIView): + def get(self, request: Request, pokemon_id: int) -> Response: + self.context = {"request": request} - encounter_details_list.append(encounter) + try: + pokemon = Pokemon.objects.get(pk=pokemon_id) + except Pokemon.DoesNotExist as e: + raise Http404 from e + + encounters = ( + Encounter.objects.filter(pokemon=pokemon) + .select_related( + "location_area", + "version", + "encounter_slot", + "encounter_slot__encounter_method", + ) + .prefetch_related( + "encounterconditionvaluemap_set", + "encounterconditionvaluemap_set__encounter_condition_value", + "encounterpokemondetail_set", + ) + .order_by("location_area_id", "version_id", "encounter_slot_id") + ) - version_details_list.append( + grouped_data: list[dict[str, Any]] = [] + for location_area, area_group in itertools.groupby(encounters, key=lambda e: e.location_area): + version_details: list[dict[str, Any]] = [] + for version, ver_group in itertools.groupby(area_group, key=lambda e: e.version): + encounter_list = list(ver_group) + max_chance = sum(e.encounter_slot.rarity for e in encounter_list if e.encounter_slot) + version_details.append( { - "version": VersionSummarySerializer(version, context=self.context).data, + "version": version, "max_chance": max_chance, - "encounter_details": encounter_details_list, + "encounter_details": encounter_list, } ) - - encounters_list.append( + grouped_data.append( { - "location_area": LocationAreaSummarySerializer(location_area, context=self.context).data, - "version_details": version_details_list, + "location_area": location_area, + "version_details": version_details, } ) - return Response(encounters_list) + data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonLocationAreaEncounterSerializer(grouped_data, many=True, context=self.context).data, # pyright: ignore[reportUnknownMemberType] + ) + return Response(data) + + +class PokeapiMetaResponseSerializer(serializers.Serializer[dict[str, Any]]): + deploy_date = serializers.CharField(allow_null=True) + hash = serializers.CharField(allow_null=True) + tag = serializers.CharField(allow_null=True) @extend_schema( description="Returns metadata about the current deployed version of the API, including the git commit hash, deploy date, and tag (if any).", summary="Get API metadata", tags=["utility"], - responses={ - 200: { - "type": "object", - "properties": { - "deploy_date": {"type": "string", "nullable": True}, - "hash": {"type": "string", "nullable": True}, - "tag": {"type": "string", "nullable": True}, - }, - } - }, + responses={"200": PokeapiMetaResponseSerializer}, ) class PokeapiMetaView(APIView): - def get(self, request): + def get(self, _request: Request) -> Response: try: git_hash = subprocess.check_output(["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL).decode().strip() - except Exception: + except (subprocess.CalledProcessError, OSError, ValueError): git_hash = None try: @@ -1093,7 +1076,7 @@ def get(self, request): .decode() .strip() ) - except Exception: + except (subprocess.CalledProcessError, OSError, ValueError): deploy_date = None try: @@ -1102,8 +1085,8 @@ def get(self, request): .decode() .strip() ) - tag = tag_output if tag_output else None - except Exception: + tag = tag_output or None + except (subprocess.CalledProcessError, OSError, ValueError): tag = None return Response( diff --git a/pokemon_v2/migrations/0001_squashed_0002_auto_20160301_1408.py b/pokemon_v2/migrations/0001_squashed_0002_auto_20160301_1408.py index e0f591830..e037a683f 100644 --- a/pokemon_v2/migrations/0001_squashed_0002_auto_20160301_1408.py +++ b/pokemon_v2/migrations/0001_squashed_0002_auto_20160301_1408.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0003_auto_20160530_1132.py b/pokemon_v2/migrations/0003_auto_20160530_1132.py index a5f27cf9d..b70b66aa8 100644 --- a/pokemon_v2/migrations/0003_auto_20160530_1132.py +++ b/pokemon_v2/migrations/0003_auto_20160530_1132.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0004_iso639length_20191217.py b/pokemon_v2/migrations/0004_iso639length_20191217.py index bff709f2c..ff9cdaf78 100644 --- a/pokemon_v2/migrations/0004_iso639length_20191217.py +++ b/pokemon_v2/migrations/0004_iso639length_20191217.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0009_pokemontypepast.py b/pokemon_v2/migrations/0009_pokemontypepast.py index a9351a893..0a8a22ca7 100644 --- a/pokemon_v2/migrations/0009_pokemontypepast.py +++ b/pokemon_v2/migrations/0009_pokemontypepast.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.11 on 2021-02-06 22:03 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0010_pokemonformtype.py b/pokemon_v2/migrations/0010_pokemonformtype.py index 91046acbf..3b577d58c 100644 --- a/pokemon_v2/migrations/0010_pokemonformtype.py +++ b/pokemon_v2/migrations/0010_pokemonformtype.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.11 on 2021-02-18 20:45 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0011_typeefficacypast.py b/pokemon_v2/migrations/0011_typeefficacypast.py index 7f96f04e3..3ea6362f1 100644 --- a/pokemon_v2/migrations/0011_typeefficacypast.py +++ b/pokemon_v2/migrations/0011_typeefficacypast.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.11 on 2021-02-24 13:42 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0013_pokemonabilitypast.py b/pokemon_v2/migrations/0013_pokemonabilitypast.py index 421b1f97c..eb74b2e2a 100644 --- a/pokemon_v2/migrations/0013_pokemonabilitypast.py +++ b/pokemon_v2/migrations/0013_pokemonabilitypast.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.15 on 2023-02-27 15:33 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0015_pokemoncries.py b/pokemon_v2/migrations/0015_pokemoncries.py index 05099ce1c..edae3ce23 100644 --- a/pokemon_v2/migrations/0015_pokemoncries.py +++ b/pokemon_v2/migrations/0015_pokemoncries.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.23 on 2024-02-02 18:02 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0016_typesprites.py b/pokemon_v2/migrations/0016_typesprites.py index a9e752d63..195e52139 100644 --- a/pokemon_v2/migrations/0016_typesprites.py +++ b/pokemon_v2/migrations/0016_typesprites.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.23 on 2024-07-29 02:09 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0020_add_regional_evolution_fields.py b/pokemon_v2/migrations/0020_add_regional_evolution_fields.py index 3834f8865..8f4f94737 100644 --- a/pokemon_v2/migrations/0020_add_regional_evolution_fields.py +++ b/pokemon_v2/migrations/0020_add_regional_evolution_fields.py @@ -1,7 +1,7 @@ # Generated migration for regional evolution metadata -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0021_add_evolution_methods_and_fields.py b/pokemon_v2/migrations/0021_add_evolution_methods_and_fields.py index 65f59d430..bc19607be 100644 --- a/pokemon_v2/migrations/0021_add_evolution_methods_and_fields.py +++ b/pokemon_v2/migrations/0021_add_evolution_methods_and_fields.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.25 on 2025-12-31 17:47 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/pokemon_v2/migrations/0032_pokemonformcondition_base_form.py b/pokemon_v2/migrations/0032_evolution_condition_and_form_updates.py similarity index 75% rename from pokemon_v2/migrations/0032_pokemonformcondition_base_form.py rename to pokemon_v2/migrations/0032_evolution_condition_and_form_updates.py index c5b5db217..3ba5ce360 100644 --- a/pokemon_v2/migrations/0032_pokemonformcondition_base_form.py +++ b/pokemon_v2/migrations/0032_evolution_condition_and_form_updates.py @@ -8,6 +8,11 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AlterField( + model_name="pokemonevolution", + name="time_of_day", + field=models.CharField(blank=True, default="", max_length=10), + ), migrations.AddField( model_name="pokemonformcondition", name="base_form", diff --git a/pokemon_v2/migrations/0033_itemprice.py b/pokemon_v2/migrations/0033_item_prices_and_currencies.py similarity index 95% rename from pokemon_v2/migrations/0033_itemprice.py rename to pokemon_v2/migrations/0033_item_prices_and_currencies.py index daf8a6942..a07256c1c 100644 --- a/pokemon_v2/migrations/0033_itemprice.py +++ b/pokemon_v2/migrations/0033_item_prices_and_currencies.py @@ -1,12 +1,10 @@ -# Generated by Django 5.2.10 on 2026-03-07 00:00 - import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("pokemon_v2", "0032_pokemonformcondition_base_form"), + ("pokemon_v2", "0032_evolution_condition_and_form_updates"), ] operations = [ @@ -115,4 +113,8 @@ class Migration(migrations.Migration): "abstract": False, }, ), + migrations.RemoveField( + model_name="item", + name="cost", + ), ] diff --git a/pokemon_v2/models.py b/pokemon_v2/models.py index d77bd5138..7998e1a7c 100644 --- a/pokemon_v2/models.py +++ b/pokemon_v2/models.py @@ -1,11 +1,250 @@ +# pyright: reportIncompatibleVariableOverride=false +from __future__ import annotations + +from typing import Any + from django.db import models +from typing_extensions import override + +__all__: tuple[str, ...] = ( + "Ability", + "AbilityChange", + "AbilityChangeEffectText", + "AbilityEffectText", + "AbilityFlavorText", + "AbilityName", + "Berry", + "BerryFirmness", + "BerryFirmnessName", + "BerryFlavor", + "BerryFlavorMap", + "BerryFlavorName", + "Characteristic", + "CharacteristicDescription", + "ContestCombo", + "ContestEffect", + "ContestEffectEffectText", + "ContestEffectFlavorText", + "ContestType", + "ContestTypeName", + "Currency", + "CurrencyName", + "EggGroup", + "EggGroupName", + "Encounter", + "EncounterCondition", + "EncounterConditionName", + "EncounterConditionValue", + "EncounterConditionValueMap", + "EncounterConditionValueName", + "EncounterMethod", + "EncounterMethodName", + "EncounterPokemonDetail", + "EncounterSlot", + "EvolutionChain", + "EvolutionTrigger", + "EvolutionTriggerName", + "Experience", + "Gender", + "Generation", + "GenerationName", + "GrowthRate", + "GrowthRateDescription", + "HasAbility", + "HasCharacteristic", + "HasContestEffect", + "HasContestType", + "HasDescription", + "HasEffect", + "HasEggGroup", + "HasEncounterCondition", + "HasEncounterMethod", + "HasEvolutionTrigger", + "HasFlavorText", + "HasFlingEffect", + "HasGameIndex", + "HasGender", + "HasGeneration", + "HasGrowthRate", + "HasItem", + "HasItemAttribute", + "HasItemCategory", + "HasItemPocket", + "HasLanguage", + "HasLocation", + "HasLocationArea", + "HasMetaAilment", + "HasMetaCategory", + "HasMove", + "HasMoveAttribute", + "HasMoveDamageClass", + "HasMoveEffect", + "HasMoveLearnMethod", + "HasMoveTarget", + "HasName", + "HasNature", + "HasOrder", + "HasPokeathlonStat", + "HasPokedex", + "HasPokemon", + "HasPokemonColor", + "HasPokemonForm", + "HasPokemonHabitat", + "HasPokemonShape", + "HasPokemonSpecies", + "HasRegion", + "HasShortEffect", + "HasStat", + "HasSuperContestEffect", + "HasType", + "HasTypeEfficacy", + "HasVersion", + "HasVersionGroup", + "IsDescription", + "IsFlavorText", + "IsName", + "Item", + "ItemAttribute", + "ItemAttributeDescription", + "ItemAttributeMap", + "ItemAttributeName", + "ItemCategory", + "ItemCategoryName", + "ItemEffectText", + "ItemFlavorText", + "ItemFlingEffect", + "ItemFlingEffectEffectText", + "ItemGameIndex", + "ItemName", + "ItemPocket", + "ItemPocketName", + "ItemPrice", + "ItemSprites", + "Language", + "LanguageName", + "Location", + "LocationArea", + "LocationAreaEncounterRate", + "LocationAreaName", + "LocationGameIndex", + "LocationName", + "Machine", + "Move", + "MoveAttribute", + "MoveAttributeDescription", + "MoveAttributeMap", + "MoveAttributeName", + "MoveBattleStyle", + "MoveBattleStyleName", + "MoveChange", + "MoveDamageClass", + "MoveDamageClassDescription", + "MoveDamageClassName", + "MoveEffect", + "MoveEffectChange", + "MoveEffectChangeEffectText", + "MoveEffectEffectText", + "MoveFlavorText", + "MoveLearnMethod", + "MoveLearnMethodDescription", + "MoveLearnMethodName", + "MoveMeta", + "MoveMetaAilment", + "MoveMetaAilmentName", + "MoveMetaCategory", + "MoveMetaCategoryDescription", + "MoveMetaStatChange", + "MoveName", + "MoveTarget", + "MoveTargetDescription", + "MoveTargetName", + "Nature", + "NatureBattleStylePreference", + "NatureName", + "NaturePokeathlonStat", + "PalPark", + "PalParkArea", + "PalParkAreaName", + "PokeApiModel", + "PokeathlonStat", + "PokeathlonStatName", + "Pokedex", + "PokedexDescription", + "PokedexName", + "PokedexVersionGroup", + "Pokemon", + "PokemonAbility", + "PokemonAbilityPast", + "PokemonColor", + "PokemonColorName", + "PokemonCries", + "PokemonDexNumber", + "PokemonEggGroup", + "PokemonEvolution", + "PokemonForm", + "PokemonFormCondition", + "PokemonFormGeneration", + "PokemonFormName", + "PokemonFormSprites", + "PokemonFormTrigger", + "PokemonFormType", + "PokemonGameIndex", + "PokemonHabitat", + "PokemonHabitatName", + "PokemonItem", + "PokemonMove", + "PokemonShape", + "PokemonShapeName", + "PokemonSpecies", + "PokemonSpeciesDescription", + "PokemonSpeciesFlavorText", + "PokemonSpeciesName", + "PokemonSprites", + "PokemonStat", + "PokemonStatPast", + "PokemonType", + "PokemonTypePast", + "Region", + "RegionName", + "Stat", + "StatName", + "SuperContestCombo", + "SuperContestEffect", + "SuperContestEffectFlavorText", + "Type", + "TypeEfficacy", + "TypeEfficacyPast", + "TypeGameIndex", + "TypeName", + "TypeSprites", + "Version", + "VersionGroup", + "VersionGroupMoveLearnMethod", + "VersionGroupRegion", + "VersionName", +) + + +############################ +# BASE MODEL FOR POKEAPI # +############################ + + +class PokeApiModel(models.Model): + class Meta: + abstract = True + + @override + def __str__(self) -> str: + return f"{self.__class__.__name__}({self.pk})" + ##################### # ABSTRACT MODELS # ##################### -class HasAbility(models.Model): +class HasAbility(PokeApiModel): ability = models.ForeignKey( "Ability", blank=True, @@ -18,7 +257,7 @@ class Meta: abstract = True -class HasCharacteristic(models.Model): +class HasCharacteristic(PokeApiModel): characteristic = models.ForeignKey( "Characteristic", blank=True, @@ -31,7 +270,7 @@ class Meta: abstract = True -class HasContestType(models.Model): +class HasContestType(PokeApiModel): contest_type = models.ForeignKey( "ContestType", blank=True, @@ -44,7 +283,7 @@ class Meta: abstract = True -class HasContestEffect(models.Model): +class HasContestEffect(PokeApiModel): contest_effect = models.ForeignKey( "ContestEffect", blank=True, @@ -57,7 +296,7 @@ class Meta: abstract = True -class HasCurrency(models.Model): +class HasCurrency(PokeApiModel): currency = models.ForeignKey( "Currency", blank=True, @@ -70,7 +309,7 @@ class Meta: abstract = True -class HasSuperContestEffect(models.Model): +class HasSuperContestEffect(PokeApiModel): super_contest_effect = models.ForeignKey( "SuperContestEffect", blank=True, @@ -83,14 +322,14 @@ class Meta: abstract = True -class HasDescription(models.Model): +class HasDescription(PokeApiModel): description = models.CharField(max_length=2000, default="") class Meta: abstract = True -class HasGender(models.Model): +class HasGender(PokeApiModel): gender = models.ForeignKey( "Gender", blank=True, @@ -103,14 +342,14 @@ class Meta: abstract = True -class HasEffect(models.Model): +class HasEffect(PokeApiModel): effect = models.CharField(max_length=6000) class Meta: abstract = True -class HasEggGroup(models.Model): +class HasEggGroup(PokeApiModel): egg_group = models.ForeignKey( "EggGroup", blank=True, @@ -123,7 +362,7 @@ class Meta: abstract = True -class HasEncounterMethod(models.Model): +class HasEncounterMethod(PokeApiModel): encounter_method = models.ForeignKey( "EncounterMethod", blank=True, @@ -136,7 +375,7 @@ class Meta: abstract = True -class HasEncounterCondition(models.Model): +class HasEncounterCondition(PokeApiModel): encounter_condition = models.ForeignKey( "EncounterCondition", blank=True, @@ -149,7 +388,7 @@ class Meta: abstract = True -class HasEvolutionTrigger(models.Model): +class HasEvolutionTrigger(PokeApiModel): evolution_trigger = models.ForeignKey( "EvolutionTrigger", blank=True, @@ -162,14 +401,14 @@ class Meta: abstract = True -class HasFlavorText(models.Model): +class HasFlavorText(PokeApiModel): flavor_text = models.CharField(max_length=500) class Meta: abstract = True -class HasFlingEffect(models.Model): +class HasFlingEffect(PokeApiModel): item_fling_effect = models.ForeignKey( "ItemFlingEffect", blank=True, @@ -182,14 +421,14 @@ class Meta: abstract = True -class HasGameIndex(models.Model): +class HasGameIndex(PokeApiModel): game_index = models.IntegerField() class Meta: abstract = True -class HasGeneration(models.Model): +class HasGeneration(PokeApiModel): generation = models.ForeignKey( "Generation", blank=True, @@ -202,7 +441,7 @@ class Meta: abstract = True -class HasGrowthRate(models.Model): +class HasGrowthRate(PokeApiModel): growth_rate = models.ForeignKey( "GrowthRate", blank=True, @@ -215,7 +454,7 @@ class Meta: abstract = True -class HasItem(models.Model): +class HasItem(PokeApiModel): item = models.ForeignKey( "Item", blank=True, @@ -228,7 +467,7 @@ class Meta: abstract = True -class HasItemAttribute(models.Model): +class HasItemAttribute(PokeApiModel): item_attribute = models.ForeignKey( "ItemAttribute", blank=True, @@ -241,7 +480,7 @@ class Meta: abstract = True -class HasItemCategory(models.Model): +class HasItemCategory(PokeApiModel): item_category = models.ForeignKey( "ItemCategory", blank=True, @@ -254,7 +493,7 @@ class Meta: abstract = True -class HasItemPocket(models.Model): +class HasItemPocket(PokeApiModel): item_pocket = models.ForeignKey( "ItemPocket", blank=True, @@ -267,7 +506,7 @@ class Meta: abstract = True -class HasLanguage(models.Model): +class HasLanguage(PokeApiModel): language = models.ForeignKey( "Language", blank=True, @@ -280,7 +519,7 @@ class Meta: abstract = True -class HasLocation(models.Model): +class HasLocation(PokeApiModel): location = models.ForeignKey( "Location", blank=True, @@ -293,7 +532,7 @@ class Meta: abstract = True -class HasLocationArea(models.Model): +class HasLocationArea(PokeApiModel): location_area = models.ForeignKey( "LocationArea", blank=True, @@ -306,7 +545,7 @@ class Meta: abstract = True -class HasMetaAilment(models.Model): +class HasMetaAilment(PokeApiModel): move_meta_ailment = models.ForeignKey( "MoveMetaAilment", blank=True, @@ -319,7 +558,7 @@ class Meta: abstract = True -class HasMetaCategory(models.Model): +class HasMetaCategory(PokeApiModel): move_meta_category = models.ForeignKey( "MoveMetaCategory", blank=True, @@ -332,7 +571,7 @@ class Meta: abstract = True -class HasMove(models.Model): +class HasMove(PokeApiModel): move = models.ForeignKey( "Move", blank=True, @@ -345,7 +584,7 @@ class Meta: abstract = True -class HasMoveDamageClass(models.Model): +class HasMoveDamageClass(PokeApiModel): move_damage_class = models.ForeignKey( "MoveDamageClass", blank=True, @@ -358,21 +597,21 @@ class Meta: abstract = True -class HasMoveEffect(models.Model): +class HasMoveEffect(PokeApiModel): move_effect = models.ForeignKey("MoveEffect", blank=True, null=True, on_delete=models.CASCADE) class Meta: abstract = True -class HasMoveAttribute(models.Model): +class HasMoveAttribute(PokeApiModel): move_attribute = models.ForeignKey("MoveAttribute", blank=True, null=True, on_delete=models.CASCADE) class Meta: abstract = True -class HasMoveTarget(models.Model): +class HasMoveTarget(PokeApiModel): move_target = models.ForeignKey( "MoveTarget", blank=True, @@ -385,14 +624,14 @@ class Meta: abstract = True -class HasName(models.Model): +class HasName(PokeApiModel): name = models.CharField(max_length=200, db_index=True) class Meta: abstract = True -class HasNature(models.Model): +class HasNature(PokeApiModel): nature = models.ForeignKey( "Nature", blank=True, @@ -405,14 +644,14 @@ class Meta: abstract = True -class HasOrder(models.Model): +class HasOrder(PokeApiModel): order = models.IntegerField(blank=True, null=True) class Meta: abstract = True -class HasPokeathlonStat(models.Model): +class HasPokeathlonStat(PokeApiModel): pokeathlon_stat = models.ForeignKey( "PokeathlonStat", blank=True, @@ -425,7 +664,7 @@ class Meta: abstract = True -class HasPokedex(models.Model): +class HasPokedex(PokeApiModel): pokedex = models.ForeignKey( "Pokedex", blank=True, @@ -438,7 +677,7 @@ class Meta: abstract = True -class HasPokemon(models.Model): +class HasPokemon(PokeApiModel): pokemon = models.ForeignKey( "Pokemon", blank=True, @@ -451,7 +690,7 @@ class Meta: abstract = True -class HasPokemonColor(models.Model): +class HasPokemonColor(PokeApiModel): pokemon_color = models.ForeignKey( "PokemonColor", blank=True, @@ -464,7 +703,7 @@ class Meta: abstract = True -class HasPokemonForm(models.Model): +class HasPokemonForm(PokeApiModel): pokemon_form = models.ForeignKey( "PokemonForm", blank=True, @@ -477,7 +716,7 @@ class Meta: abstract = True -class HasPokemonHabitat(models.Model): +class HasPokemonHabitat(PokeApiModel): pokemon_habitat = models.ForeignKey( "PokemonHabitat", blank=True, @@ -491,7 +730,7 @@ class Meta: # HasPokemonMoveMethod -class HasMoveLearnMethod(models.Model): +class HasMoveLearnMethod(PokeApiModel): move_learn_method = models.ForeignKey( "MoveLearnMethod", blank=True, @@ -504,7 +743,7 @@ class Meta: abstract = True -class HasPokemonShape(models.Model): +class HasPokemonShape(PokeApiModel): pokemon_shape = models.ForeignKey( "PokemonShape", blank=True, @@ -517,7 +756,7 @@ class Meta: abstract = True -class HasPokemonSpecies(models.Model): +class HasPokemonSpecies(PokeApiModel): pokemon_species = models.ForeignKey( "PokemonSpecies", blank=True, @@ -530,7 +769,7 @@ class Meta: abstract = True -class HasRegion(models.Model): +class HasRegion(PokeApiModel): region = models.ForeignKey( "Region", blank=True, @@ -543,14 +782,14 @@ class Meta: abstract = True -class HasShortEffect(models.Model): +class HasShortEffect(PokeApiModel): short_effect = models.CharField(max_length=300) class Meta: abstract = True -class HasStat(models.Model): +class HasStat(PokeApiModel): stat = models.ForeignKey( "Stat", blank=True, @@ -563,7 +802,7 @@ class Meta: abstract = True -class HasType(models.Model): +class HasType(PokeApiModel): type = models.ForeignKey( "Type", blank=True, @@ -576,7 +815,7 @@ class Meta: abstract = True -class HasTypeEfficacy(models.Model): +class HasTypeEfficacy(PokeApiModel): damage_type = models.ForeignKey( "Type", blank=True, @@ -599,7 +838,7 @@ class Meta: abstract = True -class HasVersion(models.Model): +class HasVersion(PokeApiModel): version = models.ForeignKey( "Version", blank=True, @@ -612,7 +851,7 @@ class Meta: abstract = True -class HasVersionGroup(models.Model): +class HasVersionGroup(PokeApiModel): version_group = models.ForeignKey( "VersionGroup", blank=True, @@ -783,7 +1022,7 @@ class TypeEfficacyPast(HasTypeEfficacy, HasGeneration): class TypeSprites(HasType): - sprites = models.JSONField() + sprites: models.JSONField[Any] = models.JSONField() ################# @@ -906,7 +1145,7 @@ class ItemPrice(HasItem, HasVersionGroup, HasCurrency): class ItemSprites(HasItem): - sprites = models.JSONField() + sprites: models.JSONField[Any] = models.JSONField() #################### @@ -924,7 +1163,7 @@ class ContestTypeName(HasContestType, IsName): color = models.CharField(max_length=10) -class ContestEffect(models.Model): +class ContestEffect(PokeApiModel): appeal = models.IntegerField() jam = models.IntegerField() @@ -938,7 +1177,7 @@ class ContestEffectFlavorText(HasLanguage, HasFlavorText, HasContestEffect): pass -class ContestCombo(models.Model): +class ContestCombo(PokeApiModel): first_move = models.ForeignKey( "Move", blank=True, @@ -1028,7 +1267,7 @@ class BerryFlavorName(IsName): ) -class BerryFlavorMap(models.Model): +class BerryFlavorMap(PokeApiModel): berry = models.ForeignKey(Berry, blank=True, null=True, related_name="%(class)s", on_delete=models.CASCADE) berry_flavor = models.ForeignKey( @@ -1184,7 +1423,7 @@ class EncounterConditionValueName(IsName): ) -class EncounterConditionValueMap(models.Model): +class EncounterConditionValueMap(PokeApiModel): encounter = models.ForeignKey(Encounter, blank=True, null=True, on_delete=models.CASCADE) encounter_condition_value = models.ForeignKey( @@ -1192,7 +1431,7 @@ class EncounterConditionValueMap(models.Model): ) -class EncounterPokemonDetail(models.Model): +class EncounterPokemonDetail(PokeApiModel): encounter = models.ForeignKey(Encounter, blank=True, null=True, on_delete=models.CASCADE) min_perfect_ivs = models.IntegerField(blank=True, null=True) @@ -1290,7 +1529,7 @@ class MoveBattleStyleName(IsName): ######################## -class MoveEffect(models.Model): +class MoveEffect(PokeApiModel): pass @@ -1489,7 +1728,7 @@ class PalPark(HasPokemonSpecies): ########################## -class SuperContestEffect(models.Model): +class SuperContestEffect(PokeApiModel): appeal = models.IntegerField() @@ -1497,7 +1736,7 @@ class SuperContestEffectFlavorText(IsFlavorText, HasSuperContestEffect): pass -class SuperContestCombo(models.Model): +class SuperContestCombo(PokeApiModel): first_move = models.ForeignKey(Move, blank=True, null=True, related_name="first", on_delete=models.CASCADE) second_move = models.ForeignKey(Move, blank=True, null=True, related_name="second", on_delete=models.CASCADE) @@ -1508,7 +1747,7 @@ class SuperContestCombo(models.Model): ###################### -class EvolutionChain(models.Model): +class EvolutionChain(PokeApiModel): baby_trigger_item = models.ForeignKey(Item, blank=True, null=True, on_delete=models.CASCADE) @@ -1668,7 +1907,7 @@ class PokemonEvolution(HasEvolutionTrigger, HasGender): held_item = models.ForeignKey(Item, blank=True, null=True, related_name="held_item", on_delete=models.CASCADE) - time_of_day = models.CharField(max_length=10, blank=True, null=True) + time_of_day = models.CharField(max_length=10, blank=True, default="") known_move = models.ForeignKey(Move, blank=True, null=True, on_delete=models.CASCADE) @@ -1769,7 +2008,7 @@ class PokemonFormName(HasPokemonForm, IsName): class PokemonFormSprites(HasPokemonForm): - sprites = models.JSONField() + sprites: models.JSONField[Any] = models.JSONField() class PokemonFormTrigger(HasName): @@ -1873,8 +2112,8 @@ class PokemonTypePast(HasPokemon, HasType, HasGeneration): class PokemonSprites(HasPokemon): - sprites = models.JSONField() + sprites: models.JSONField[Any] = models.JSONField() class PokemonCries(HasPokemon): - cries = models.JSONField() + cries: models.JSONField[Any] = models.JSONField() diff --git a/pokemon_v2/serializers.py b/pokemon_v2/serializers.py index ac841e9ff..430c1c385 100644 --- a/pokemon_v2/serializers.py +++ b/pokemon_v2/serializers.py @@ -1,14 +1,262 @@ -from collections import OrderedDict -import json -from django.urls import reverse -from rest_framework import serializers -from drf_spectacular.utils import extend_schema_field +# ruff: noqa: F405 +# pyright: reportIncompatibleVariableOverride=false, reportUnknownMemberType=false +from __future__ import annotations -# pylint: disable=redefined-builtin +import itertools +from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast -# PokeAPI v2 serializers in order of dependency +from django.db.models import Q +from drf_spectacular.utils import extend_schema_field # pyright: ignore[reportUnknownVariableType] +from rest_framework import serializers +from rest_framework.reverse import reverse + +from .models import * # noqa: F403 + +if TYPE_CHECKING: + from collections.abc import Sequence + + from django.db import models + from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList + + class EncounterWithRelations(Protocol): + encounterconditionvaluemap_set: models.Manager[EncounterConditionValueMap] + encounterpokemondetail_set: models.Manager[EncounterPokemonDetail] + + class PokemonWithRelations(Protocol): + pokemonsprites: models.Manager[PokemonSprites] + pokemoncries: models.Manager[PokemonCries] + + +__all__: tuple[str, ...] = ( + "AbilityChangeEffectTextSerializer", + "AbilityChangeSerializer", + "AbilityDetailSerializer", + "AbilityEffectTextSerializer", + "AbilityFlavorTextSerializer", + "AbilityNameSerializer", + "AbilityPokemonDetailSerializer", + "AbilitySummarySerializer", + "BerryDetailSerializer", + "BerryFirmnessDetailSerializer", + "BerryFirmnessNameSerializer", + "BerryFirmnessSummarySerializer", + "BerryFlavorBerryMapSerializer", + "BerryFlavorDetailSerializer", + "BerryFlavorMapSerializer", + "BerryFlavorNameSerializer", + "BerryFlavorSummarySerializer", + "BerrySummarySerializer", + "CharacteristicDescriptionSerializer", + "CharacteristicDetailSerializer", + "CharacteristicSummarySerializer", + "ContestEffectDetailSerializer", + "ContestEffectEffectTextSerializer", + "ContestEffectFlavorTextSerializer", + "ContestEffectSummarySerializer", + "ContestTypeDetailSerializer", + "ContestTypeNameSerializer", + "ContestTypeSummarySerializer", + "CurrencyDetailSerializer", + "CurrencyNameSerializer", + "CurrencySummarySerializer", + "EggGroupDetailSerializer", + "EggGroupNameSerializer", + "EggGroupSummarySerializer", + "EncounterConditionDetailSerializer", + "EncounterConditionNameSerializer", + "EncounterConditionSummarySerializer", + "EncounterConditionValueDetailSerializer", + "EncounterConditionValueMapSerializer", + "EncounterConditionValueNameSerializer", + "EncounterConditionValueSummarySerializer", + "EncounterDetailSerializer", + "EncounterMethodDetailSerializer", + "EncounterMethodNameSerializer", + "EncounterMethodSummarySerializer", + "EncounterPokemonDetailSerializer", + "EncounterSlotSerializer", + "EvolutionChainDetailSerializer", + "EvolutionChainLinkSerializer", + "EvolutionChainSummarySerializer", + "EvolutionTriggerDetailSerializer", + "EvolutionTriggerNameSerializer", + "EvolutionTriggerSummarySerializer", + "ExperienceSerializer", + "GenderDetailSerializer", + "GenderPokemonSpeciesSerializer", + "GenderSummarySerializer", + "GenerationDetailSerializer", + "GenerationNameSerializer", + "GenerationSummarySerializer", + "GrowthRateDescriptionSerializer", + "GrowthRateDetailSerializer", + "GrowthRateSummarySerializer", + "ItemAttributeDescriptionSerializer", + "ItemAttributeDetailSerializer", + "ItemAttributeNameSerializer", + "ItemAttributeSummarySerializer", + "ItemCategoryDetailSerializer", + "ItemCategoryNameSerializer", + "ItemCategorySummarySerializer", + "ItemDetailSerializer", + "ItemEffectTextSerializer", + "ItemFlavorTextSerializer", + "ItemFlingEffectDetailSerializer", + "ItemFlingEffectEffectTextSerializer", + "ItemFlingEffectSummarySerializer", + "ItemGameIndexSerializer", + "ItemMachineSerializer", + "ItemNameSerializer", + "ItemPocketDetailSerializer", + "ItemPocketNameSerializer", + "ItemPocketSummarySerializer", + "ItemPriceSerializer", + "ItemSpritesSerializer", + "ItemSummarySerializer", + "LanguageDetailSerializer", + "LanguageNameSerializer", + "LanguageSummarySerializer", + "LocationAreaDetailSerializer", + "LocationAreaEncounterDetailSerializer", + "LocationAreaEncounterRateSerializer", + "LocationAreaEncounterVersionDetailSerializer", + "LocationAreaNameSerializer", + "LocationAreaPokemonEncounterSerializer", + "LocationAreaPokemonEncounterVersionSerializer", + "LocationAreaSummarySerializer", + "LocationDetailSerializer", + "LocationGameIndexSerializer", + "LocationNameSerializer", + "LocationSummarySerializer", + "MachineDetailSerializer", + "MachineSummarySerializer", + "MoveBattleStyleDetailSerializer", + "MoveBattleStyleNameSerializer", + "MoveBattleStyleSummarySerializer", + "MoveChangeSerializer", + "MoveComboUsageSerializer", + "MoveCombosSerializer", + "MoveDamageClassDescriptionSerializer", + "MoveDamageClassDetailSerializer", + "MoveDamageClassNameSerializer", + "MoveDamageClassSummarySerializer", + "MoveDetailSerializer", + "MoveEffectChangeEffectTextSerializer", + "MoveEffectChangeSerializer", + "MoveEffectEffectTextSerializer", + "MoveFlavorTextSerializer", + "MoveLearnMethodDescriptionSerializer", + "MoveLearnMethodDetailSerializer", + "MoveLearnMethodNameSerializer", + "MoveLearnMethodSummarySerializer", + "MoveMetaAilmentDetailSerializer", + "MoveMetaAilmentNameSerializer", + "MoveMetaAilmentSummarySerializer", + "MoveMetaCategoryDescriptionSerializer", + "MoveMetaCategoryDetailSerializer", + "MoveMetaCategorySummarySerializer", + "MoveMetaSerializer", + "MoveMetaStatChangeSerializer", + "MoveNameSerializer", + "MoveStatChangeSerializer", + "MoveSummarySerializer", + "MoveTargetDescriptionSerializer", + "MoveTargetDetailSerializer", + "MoveTargetNameSerializer", + "MoveTargetSummarySerializer", + "NatureBattleStylePreferenceSerializer", + "NatureDetailSerializer", + "NatureNameSerializer", + "NaturePokeathlonStatSerializer", + "NatureSummarySerializer", + "PalParkAreaDetailSerializer", + "PalParkAreaNameSerializer", + "PalParkAreaSummarySerializer", + "PalParkEncounterSerializer", + "PokeathlonStatAffectingNatureSerializer", + "PokeathlonStatAffectingNaturesSerializer", + "PokeathlonStatDetailSerializer", + "PokeathlonStatNameSerializer", + "PokeathlonStatSummarySerializer", + "PokedexDescriptionSerializer", + "PokedexDetailSerializer", + "PokedexNameSerializer", + "PokedexSummarySerializer", + "PokemonAbilityPastSerializer", + "PokemonAbilitySerializer", + "PokemonColorDetailSerializer", + "PokemonColorNameSerializer", + "PokemonColorSummarySerializer", + "PokemonCriesSerializer", + "PokemonDetailSerializer", + "PokemonDexEntrySerializer", + "PokemonDexNumberSerializer", + "PokemonEvolutionSerializer", + "PokemonFormConditionSerializer", + "PokemonFormDetailSerializer", + "PokemonFormNameSerializer", + "PokemonFormSpritesSerializer", + "PokemonFormSummarySerializer", + "PokemonFormTriggerConditionSerializer", + "PokemonFormTypeSerializer", + "PokemonGameIndexSerializer", + "PokemonHabitatDetailSerializer", + "PokemonHabitatNameSerializer", + "PokemonHabitatSummarySerializer", + "PokemonHeldItemSerializer", + "PokemonHeldItemVersionSerializer", + "PokemonMoveSerializer", + "PokemonMoveVersionGroupSerializer", + "PokemonPastAbilitySerializer", + "PokemonPastStatSerializer", + "PokemonPastTypeSerializer", + "PokemonShapeAwesomeNameSerializer", + "PokemonShapeDetailSerializer", + "PokemonShapeNameSerializer", + "PokemonShapeSummarySerializer", + "PokemonSpeciesDescriptionSerializer", + "PokemonSpeciesDetailSerializer", + "PokemonSpeciesEvolutionSerializer", + "PokemonSpeciesFlavorTextSerializer", + "PokemonSpeciesGenusSerializer", + "PokemonSpeciesNameSerializer", + "PokemonSpeciesPalParkEncounterSerializer", + "PokemonSpeciesSummarySerializer", + "PokemonSpeciesVarietySerializer", + "PokemonSpritesSerializer", + "PokemonStatPastSerializer", + "PokemonStatSerializer", + "PokemonSummarySerializer", + "PokemonTypePastSerializer", + "PokemonTypeSerializer", + "RegionDetailSerializer", + "RegionNameSerializer", + "RegionSummarySerializer", + "StatAffectingMovesSerializer", + "StatAffectingNaturesSerializer", + "StatDetailSerializer", + "StatNameSerializer", + "StatSummarySerializer", + "SuperContestEffectDetailSerializer", + "SuperContestEffectFlavorTextSerializer", + "SuperContestEffectSummarySerializer", + "TypeDetailSerializer", + "TypeEfficacyPastSerializer", + "TypeGameIndexSerializer", + "TypeNameSerializer", + "TypePastRelationshipsSerializer", + "TypePokemonSerializer", + "TypeRelationshipsSerializer", + "TypeSpriteSerializer", + "TypeSummarySerializer", + "VersionDetailSerializer", + "VersionGroupDetailSerializer", + "VersionGroupSummarySerializer", + "VersionNameSerializer", + "VersionSummarySerializer", +) -from .models import * +# PokeAPI v2 serializers in order of dependency ######################### # SUMMARY SERIALIZERS # @@ -20,295 +268,295 @@ # with reference accross models due to script running order -class AbilitySummarySerializer(serializers.HyperlinkedModelSerializer): +class AbilitySummarySerializer(serializers.HyperlinkedModelSerializer[Ability]): class Meta: model = Ability fields = ("name", "url") -class BerryFirmnessSummarySerializer(serializers.HyperlinkedModelSerializer): +class BerryFirmnessSummarySerializer(serializers.HyperlinkedModelSerializer[BerryFirmness]): class Meta: model = BerryFirmness fields = ("name", "url") -class BerryFlavorSummarySerializer(serializers.HyperlinkedModelSerializer): +class BerryFlavorSummarySerializer(serializers.HyperlinkedModelSerializer[BerryFlavor]): class Meta: model = BerryFlavor fields = ("name", "url") -class BerrySummarySerializer(serializers.HyperlinkedModelSerializer): +class BerrySummarySerializer(serializers.HyperlinkedModelSerializer[Berry]): class Meta: model = Berry fields = ("name", "url") -class CharacteristicSummarySerializer(serializers.HyperlinkedModelSerializer): +class CharacteristicSummarySerializer(serializers.HyperlinkedModelSerializer[Characteristic]): class Meta: model = Characteristic fields = ("url",) -class ContestEffectSummarySerializer(serializers.HyperlinkedModelSerializer): +class ContestEffectSummarySerializer(serializers.HyperlinkedModelSerializer[ContestEffect]): class Meta: model = ContestEffect fields = ("url",) -class ContestTypeSummarySerializer(serializers.HyperlinkedModelSerializer): +class ContestTypeSummarySerializer(serializers.HyperlinkedModelSerializer[ContestType]): class Meta: model = ContestType fields = ("name", "url") -class EggGroupSummarySerializer(serializers.HyperlinkedModelSerializer): +class EggGroupSummarySerializer(serializers.HyperlinkedModelSerializer[EggGroup]): class Meta: model = EggGroup fields = ("name", "url") -class EncounterConditionSummarySerializer(serializers.HyperlinkedModelSerializer): +class EncounterConditionSummarySerializer(serializers.HyperlinkedModelSerializer[EncounterCondition]): class Meta: model = EncounterCondition fields = ("name", "url") -class EncounterConditionValueSummarySerializer(serializers.HyperlinkedModelSerializer): +class EncounterConditionValueSummarySerializer(serializers.HyperlinkedModelSerializer[EncounterConditionValue]): class Meta: model = EncounterConditionValue fields = ("name", "url") -class EncounterMethodSummarySerializer(serializers.HyperlinkedModelSerializer): +class EncounterMethodSummarySerializer(serializers.HyperlinkedModelSerializer[EncounterMethod]): class Meta: model = EncounterMethod fields = ("name", "url") -class EvolutionTriggerSummarySerializer(serializers.HyperlinkedModelSerializer): +class EvolutionTriggerSummarySerializer(serializers.HyperlinkedModelSerializer[EvolutionTrigger]): class Meta: model = EvolutionTrigger fields = ("name", "url") -class EvolutionChainSummarySerializer(serializers.HyperlinkedModelSerializer): +class EvolutionChainSummarySerializer(serializers.HyperlinkedModelSerializer[EvolutionChain]): class Meta: model = EvolutionChain fields = ("url",) -class GenerationSummarySerializer(serializers.HyperlinkedModelSerializer): +class GenerationSummarySerializer(serializers.HyperlinkedModelSerializer[Generation]): class Meta: model = Generation fields = ("name", "url") -class GenderSummarySerializer(serializers.HyperlinkedModelSerializer): +class GenderSummarySerializer(serializers.HyperlinkedModelSerializer[Gender]): class Meta: model = Gender fields = ("name", "url") -class GrowthRateSummarySerializer(serializers.HyperlinkedModelSerializer): +class GrowthRateSummarySerializer(serializers.HyperlinkedModelSerializer[GrowthRate]): class Meta: model = GrowthRate fields = ("name", "url") -class CurrencySummarySerializer(serializers.HyperlinkedModelSerializer): +class CurrencySummarySerializer(serializers.HyperlinkedModelSerializer[Currency]): class Meta: model = Currency fields = ("name", "url") -class ItemPocketSummarySerializer(serializers.HyperlinkedModelSerializer): +class ItemPocketSummarySerializer(serializers.HyperlinkedModelSerializer[ItemPocket]): class Meta: model = ItemPocket fields = ("name", "url") -class ItemCategorySummarySerializer(serializers.HyperlinkedModelSerializer): +class ItemCategorySummarySerializer(serializers.HyperlinkedModelSerializer[ItemCategory]): class Meta: model = ItemCategory fields = ("name", "url") -class ItemAttributeSummarySerializer(serializers.HyperlinkedModelSerializer): +class ItemAttributeSummarySerializer(serializers.HyperlinkedModelSerializer[ItemAttribute]): class Meta: model = ItemAttribute fields = ("name", "url") -class ItemFlingEffectSummarySerializer(serializers.HyperlinkedModelSerializer): +class ItemFlingEffectSummarySerializer(serializers.HyperlinkedModelSerializer[ItemFlingEffect]): class Meta: model = ItemFlingEffect fields = ("name", "url") -class ItemSummarySerializer(serializers.HyperlinkedModelSerializer): +class ItemSummarySerializer(serializers.HyperlinkedModelSerializer[Item]): class Meta: model = Item fields = ("name", "url") -class LanguageSummarySerializer(serializers.HyperlinkedModelSerializer): +class LanguageSummarySerializer(serializers.HyperlinkedModelSerializer[Language]): class Meta: model = Language fields = ("name", "url") -class LocationSummarySerializer(serializers.HyperlinkedModelSerializer): +class LocationSummarySerializer(serializers.HyperlinkedModelSerializer[Location]): class Meta: model = Location fields = ("name", "url") -class LocationAreaSummarySerializer(serializers.HyperlinkedModelSerializer): +class LocationAreaSummarySerializer(serializers.HyperlinkedModelSerializer[LocationArea]): class Meta: model = LocationArea fields = ("name", "url") -class MachineSummarySerializer(serializers.HyperlinkedModelSerializer): +class MachineSummarySerializer(serializers.HyperlinkedModelSerializer[Machine]): class Meta: model = Machine fields = ("url",) -class MoveBattleStyleSummarySerializer(serializers.HyperlinkedModelSerializer): +class MoveBattleStyleSummarySerializer(serializers.HyperlinkedModelSerializer[MoveBattleStyle]): class Meta: model = MoveBattleStyle fields = ("name", "url") -class MoveDamageClassSummarySerializer(serializers.HyperlinkedModelSerializer): +class MoveDamageClassSummarySerializer(serializers.HyperlinkedModelSerializer[MoveDamageClass]): class Meta: model = MoveDamageClass fields = ("name", "url") -class MoveMetaAilmentSummarySerializer(serializers.HyperlinkedModelSerializer): +class MoveMetaAilmentSummarySerializer(serializers.HyperlinkedModelSerializer[MoveMetaAilment]): class Meta: model = MoveMetaAilment fields = ("name", "url") -class MoveMetaCategorySummarySerializer(serializers.HyperlinkedModelSerializer): +class MoveMetaCategorySummarySerializer(serializers.HyperlinkedModelSerializer[MoveMetaCategory]): class Meta: model = MoveMetaCategory fields = ("name", "url") -class MoveTargetSummarySerializer(serializers.HyperlinkedModelSerializer): +class MoveTargetSummarySerializer(serializers.HyperlinkedModelSerializer[MoveTarget]): class Meta: model = MoveTarget fields = ("name", "url") -class MoveSummarySerializer(serializers.HyperlinkedModelSerializer): +class MoveSummarySerializer(serializers.HyperlinkedModelSerializer[Move]): class Meta: model = Move fields = ("name", "url") -class MoveLearnMethodSummarySerializer(serializers.HyperlinkedModelSerializer): +class MoveLearnMethodSummarySerializer(serializers.HyperlinkedModelSerializer[MoveLearnMethod]): class Meta: model = MoveLearnMethod fields = ("name", "url") -class NatureSummarySerializer(serializers.HyperlinkedModelSerializer): +class NatureSummarySerializer(serializers.HyperlinkedModelSerializer[Nature]): class Meta: model = Nature fields = ("name", "url") -class PalParkAreaSummarySerializer(serializers.HyperlinkedModelSerializer): +class PalParkAreaSummarySerializer(serializers.HyperlinkedModelSerializer[PalParkArea]): class Meta: model = PalParkArea fields = ("name", "url") -class PokeathlonStatSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokeathlonStatSummarySerializer(serializers.HyperlinkedModelSerializer[PokeathlonStat]): class Meta: model = PokeathlonStat fields = ("name", "url") -class PokedexSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokedexSummarySerializer(serializers.HyperlinkedModelSerializer[Pokedex]): class Meta: model = Pokedex fields = ("name", "url") -class PokemonColorSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokemonColorSummarySerializer(serializers.HyperlinkedModelSerializer[PokemonColor]): class Meta: model = PokemonColor fields = ("name", "url") -class PokemonHabitatSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokemonHabitatSummarySerializer(serializers.HyperlinkedModelSerializer[PokemonHabitat]): class Meta: model = PokemonHabitat fields = ("name", "url") -class PokemonShapeSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokemonShapeSummarySerializer(serializers.HyperlinkedModelSerializer[PokemonShape]): class Meta: model = PokemonShape fields = ("name", "url") -class PokemonSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokemonSummarySerializer(serializers.HyperlinkedModelSerializer[Pokemon]): class Meta: model = Pokemon fields = ("name", "url") -class PokemonSpeciesSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokemonSpeciesSummarySerializer(serializers.HyperlinkedModelSerializer[PokemonSpecies]): class Meta: model = PokemonSpecies fields = ("name", "url") -class PokemonFormSummarySerializer(serializers.HyperlinkedModelSerializer): +class PokemonFormSummarySerializer(serializers.HyperlinkedModelSerializer[PokemonForm]): class Meta: model = PokemonForm fields = ("name", "url") -class RegionSummarySerializer(serializers.HyperlinkedModelSerializer): +class RegionSummarySerializer(serializers.HyperlinkedModelSerializer[Region]): class Meta: model = Region fields = ("name", "url") -class StatSummarySerializer(serializers.HyperlinkedModelSerializer): +class StatSummarySerializer(serializers.HyperlinkedModelSerializer[Stat]): class Meta: model = Stat fields = ("name", "url") -class SuperContestEffectSummarySerializer(serializers.HyperlinkedModelSerializer): +class SuperContestEffectSummarySerializer(serializers.HyperlinkedModelSerializer[SuperContestEffect]): class Meta: model = SuperContestEffect fields = ("url",) -class TypeSummarySerializer(serializers.HyperlinkedModelSerializer): +class TypeSummarySerializer(serializers.HyperlinkedModelSerializer[Type]): class Meta: model = Type fields = ("name", "url") -class VersionSummarySerializer(serializers.HyperlinkedModelSerializer): +class VersionSummarySerializer(serializers.HyperlinkedModelSerializer[Version]): class Meta: model = Version fields = ("name", "url") -class VersionGroupSummarySerializer(serializers.HyperlinkedModelSerializer): +class VersionGroupSummarySerializer(serializers.HyperlinkedModelSerializer[VersionGroup]): class Meta: model = VersionGroup fields = ("name", "url") @@ -319,64 +567,7 @@ class Meta: ##################### -class BerryFlavorMapSerializer(serializers.ModelSerializer): - berry = BerrySummarySerializer() - flavor = BerryFlavorSummarySerializer(source="berry_flavor") - - class Meta: - model = BerryFlavorMap - fields = ("potency", "berry", "flavor") - - -class ItemAttributeMapSerializer(serializers.ModelSerializer): - item = ItemSummarySerializer() - attribute = ItemAttributeSummarySerializer(source="item_attribute") - - class Meta: - model = ItemAttributeMap - fields = ( - "item", - "attribute", - ) - - -class MoveMetaStatChangeSerializer(serializers.ModelSerializer): - stat = StatSummarySerializer() - move = MoveSummarySerializer() - - class Meta: - model = MoveMetaStatChange - fields = ("change", "move", "stat") - - -class NaturePokeathlonStatSerializer(serializers.ModelSerializer): - pokeathlon_stat = PokeathlonStatSummarySerializer() - nature = NatureSummarySerializer() - - class Meta: - model = NaturePokeathlonStat - fields = ("max_change", "nature", "pokeathlon_stat") - - -class PokemonAbilitySerializer(serializers.ModelSerializer): - pokemon = PokemonSummarySerializer() - ability = AbilitySummarySerializer() - - class Meta: - model = PokemonAbility - fields = ("is_hidden", "slot", "ability", "pokemon") - - -class PokemonAbilityPastSerializer(serializers.ModelSerializer): - generation = GenerationSummarySerializer() - ability = AbilitySummarySerializer() - - class Meta: - model = PokemonAbilityPast - fields = ("is_hidden", "pokemon", "generation", "slot", "ability") - - -class PokemonDexEntrySerializer(serializers.ModelSerializer): +class PokemonDexEntrySerializer(serializers.ModelSerializer[PokemonDexNumber]): entry_number = serializers.IntegerField(source="pokedex_number") pokedex = PokedexSummarySerializer() @@ -385,64 +576,7 @@ class Meta: fields = ("entry_number", "pokedex") -class PokemonTypeSerializer(serializers.ModelSerializer): - pokemon = PokemonSummarySerializer() - type = TypeSummarySerializer() - - class Meta: - model = PokemonType - fields = ("slot", "pokemon", "type") - - -class PokemonFormTypeSerializer(serializers.ModelSerializer): - pokemon_form = PokemonFormSummarySerializer() - type = TypeSummarySerializer() - - class Meta: - model = PokemonFormType - fields = ("slot", "pokemon_form", "type") - - -class PokemonTypePastSerializer(serializers.ModelSerializer): - generation = GenerationSummarySerializer() - type = TypeSummarySerializer() - - class Meta: - model = PokemonTypePast - fields = ("pokemon", "generation", "slot", "type") - - -class PokedexVersionGroupSerializer(serializers.ModelSerializer): - pokedex = PokedexSummarySerializer() - version_group = VersionGroupSummarySerializer() - - class Meta: - model = PokedexVersionGroup - fields = ("pokedex", "version_group") - - -class VersionGroupMoveLearnMethodSerializer(serializers.ModelSerializer): - version_group = VersionGroupSummarySerializer() - move_learn_method = MoveLearnMethodSummarySerializer() - - class Meta: - model = ItemAttributeMap - fields = ("version_group", "move_learn_method") - - -class VersionGroupRegionSerializer(serializers.ModelSerializer): - version_group = VersionGroupSummarySerializer() - region = RegionSummarySerializer() - - class Meta: - model = ItemAttributeMap - fields = ( - "version_group", - "region", - ) - - -class EncounterConditionValueMapSerializer(serializers.ModelSerializer): +class EncounterConditionValueMapSerializer(serializers.ModelSerializer[EncounterConditionValueMap]): condition_value = EncounterConditionValueSummarySerializer(source="encounter_condition_value") class Meta: @@ -455,7 +589,7 @@ class Meta: ################################ -class CharacteristicDescriptionSerializer(serializers.ModelSerializer): +class CharacteristicDescriptionSerializer(serializers.ModelSerializer[CharacteristicDescription]): language = LanguageSummarySerializer() class Meta: @@ -463,7 +597,7 @@ class Meta: fields = ("description", "language") -class CharacteristicDetailSerializer(serializers.ModelSerializer): +class CharacteristicDetailSerializer(serializers.ModelSerializer[Characteristic]): descriptions = CharacteristicDescriptionSerializer(many=True, read_only=True, source="characteristicdescription") highest_stat = StatSummarySerializer(source="stat") gene_modulo = serializers.IntegerField(source="gene_mod_5") @@ -479,24 +613,9 @@ class Meta: "descriptions", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "integer", - "format": "int32", - }, - "examples": [[0, 5, 10, 15, 20, 25, 30]], - } - ) - def get_values(self, obj): - mod = obj.gene_mod_5 - values = [] - while mod <= 31: - values.append(mod) - mod += 5 - - return values + @extend_schema_field(serializers.ListField(child=serializers.IntegerField())) + def get_values(self, obj: Characteristic) -> list[int]: + return list(range(obj.gene_mod_5, 32, 5)) ######################### @@ -504,7 +623,7 @@ def get_values(self, obj): ######################### -class SuperContestEffectFlavorTextSerializer(serializers.ModelSerializer): +class SuperContestEffectFlavorTextSerializer(serializers.ModelSerializer[SuperContestEffectFlavorText]): language = LanguageSummarySerializer() class Meta: @@ -512,7 +631,7 @@ class Meta: fields = ("flavor_text", "language") -class SuperContestEffectDetailSerializer(serializers.ModelSerializer): +class SuperContestEffectDetailSerializer(serializers.ModelSerializer[SuperContestEffect]): flavor_text_entries = SuperContestEffectFlavorTextSerializer( many=True, read_only=True, source="supercontesteffectflavortext" ) @@ -523,7 +642,7 @@ class Meta: fields = ("id", "appeal", "flavor_text_entries", "moves") -class ContestEffectEffectTextSerializer(serializers.ModelSerializer): +class ContestEffectEffectTextSerializer(serializers.ModelSerializer[ContestEffectEffectText]): language = LanguageSummarySerializer() class Meta: @@ -531,7 +650,7 @@ class Meta: fields = ("effect", "language") -class ContestEffectFlavorTextSerializer(serializers.ModelSerializer): +class ContestEffectFlavorTextSerializer(serializers.ModelSerializer[ContestEffectFlavorText]): language = LanguageSummarySerializer() class Meta: @@ -539,7 +658,7 @@ class Meta: fields = ("flavor_text", "language") -class ContestEffectDetailSerializer(serializers.ModelSerializer): +class ContestEffectDetailSerializer(serializers.ModelSerializer[ContestEffect]): effect_entries = ContestEffectEffectTextSerializer(many=True, read_only=True, source="contesteffecteffecttext") flavor_text_entries = ContestEffectFlavorTextSerializer(many=True, read_only=True, source="contesteffectflavortext") @@ -548,7 +667,7 @@ class Meta: fields = ("id", "appeal", "jam", "effect_entries", "flavor_text_entries") -class ContestTypeNameSerializer(serializers.ModelSerializer): +class ContestTypeNameSerializer(serializers.ModelSerializer[ContestTypeName]): language = LanguageSummarySerializer() class Meta: @@ -556,7 +675,7 @@ class Meta: fields = ("name", "color", "language") -class ContestTypeDetailSerializer(serializers.ModelSerializer): +class ContestTypeDetailSerializer(serializers.ModelSerializer[ContestType]): names = ContestTypeNameSerializer(many=True, read_only=True, source="contesttypename") berry_flavor = BerryFlavorSummarySerializer(read_only=True, source="berryflavor") @@ -565,30 +684,12 @@ class Meta: fields = ("id", "name", "berry_flavor", "names") -class SuperContestComboSerializer(serializers.ModelSerializer): - first_move = MoveSummarySerializer() - second_move = MoveSummarySerializer() - - class Meta: - model = SuperContestCombo - fields = ("first_move", "second_move") - - -class ContestComboSerializer(serializers.ModelSerializer): - first_move = MoveSummarySerializer() - second_move = MoveSummarySerializer() - - class Meta: - model = ContestCombo - fields = ("first_move", "second_move") - - ######################## # REGION SERIALIZERS # ######################## -class RegionNameSerializer(serializers.ModelSerializer): +class RegionNameSerializer(serializers.ModelSerializer[RegionName]): language = LanguageSummarySerializer() class Meta: @@ -596,7 +697,7 @@ class Meta: fields = ("name", "language") -class RegionDetailSerializer(serializers.ModelSerializer): +class RegionDetailSerializer(serializers.ModelSerializer[Region]): names = RegionNameSerializer(many=True, read_only=True, source="regionname") locations = LocationSummarySerializer(many=True, read_only=True, source="location") version_groups = serializers.SerializerMethodField("get_region_version_groups") @@ -615,32 +716,13 @@ class Meta: "version_groups", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["red-blue"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version-group/1/"], - }, - }, - }, - } - ) - def get_region_version_groups(self, obj): - vg_regions = VersionGroupRegion.objects.filter(region=obj) - data = VersionGroupRegionSerializer(vg_regions, many=True, context=self.context).data - groups = [] - - for group in data: - groups.append(group["version_group"]) - - return groups + @extend_schema_field(VersionGroupSummarySerializer(many=True)) + def get_region_version_groups(self, obj: Region) -> ReturnList[ReturnDict[str, Any]]: + version_groups = VersionGroup.objects.filter(versiongroupregion__region=obj).distinct() + return cast( + "ReturnList[ReturnDict[str, Any]]", + VersionGroupSummarySerializer(version_groups, many=True, context=self.context).data, + ) ############################ @@ -648,7 +730,7 @@ def get_region_version_groups(self, obj): ############################ -class GenerationNameSerializer(serializers.ModelSerializer): +class GenerationNameSerializer(serializers.ModelSerializer[GenerationName]): language = LanguageSummarySerializer() class Meta: @@ -656,7 +738,7 @@ class Meta: fields = ("name", "language") -class GenerationDetailSerializer(serializers.ModelSerializer): +class GenerationDetailSerializer(serializers.ModelSerializer[Generation]): main_region = RegionSummarySerializer(source="region") names = GenerationNameSerializer(many=True, read_only=True, source="generationname") abilities = AbilitySummarySerializer(many=True, read_only=True, source="ability") @@ -685,7 +767,16 @@ class Meta: ######################## -class GenderDetailSerializer(serializers.ModelSerializer): +class GenderPokemonSpeciesSerializer(serializers.ModelSerializer[PokemonSpecies]): + rate = serializers.IntegerField(source="gender_rate") + pokemon_species = PokemonSpeciesSummarySerializer(source="*") + + class Meta: + model = PokemonSpecies + fields = ("rate", "pokemon_species") + + +class GenderDetailSerializer(serializers.ModelSerializer[Gender]): pokemon_species_details = serializers.SerializerMethodField("get_species") required_for_evolution = serializers.SerializerMethodField("get_required") @@ -693,76 +784,26 @@ class Meta: model = Gender fields = ("id", "name", "pokemon_species_details", "required_for_evolution") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["rate", "pokemon_species"], - "properties": { - "rate": {"type": "integer", "format": "int32", "examples": [1]}, - "pokemon_species": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bulbasaur"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/1/"], - }, - }, - }, - }, - }, - } - ) - def get_species(self, obj): - species_objects = [] - - if obj.name == "female": - species_objects = PokemonSpecies.objects.filter(gender_rate__gt=0) - elif obj.name == "male": - species_objects = PokemonSpecies.objects.filter(gender_rate__range=[0, 7]) - elif obj.name == "genderless": - species_objects = PokemonSpecies.objects.filter(gender_rate=-1) - - details = [] - - for species in species_objects: - detail = OrderedDict() - detail["rate"] = species.gender_rate - detail["pokemon_species"] = PokemonSpeciesSummarySerializer(species, context=self.context).data - details.append(detail) - - return details - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["wormadam"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/413/"], - }, - }, - }, - } - ) - def get_required(self, obj): - evo_objects = PokemonEvolution.objects.filter(gender=obj) - species_list = [] - - for evo in evo_objects: - species = PokemonSpeciesSummarySerializer(evo.evolved_species, context=self.context).data - species_list.append(species) + @extend_schema_field(GenderPokemonSpeciesSerializer(many=True)) + def get_species(self, obj: Gender) -> ReturnList[ReturnDict[str, Any]]: + gender_filters = { + "female": Q(gender_rate__gt=0), + "male": Q(gender_rate__range=[0, 7]), + "genderless": Q(gender_rate=-1), + } + species_objects = PokemonSpecies.objects.filter(gender_filters.get(obj.name, Q(pk__in=[]))) + return cast( + "ReturnList[ReturnDict[str, Any]]", + GenderPokemonSpeciesSerializer(species_objects, many=True, context=self.context).data, + ) - return species_list + @extend_schema_field(PokemonSpeciesSummarySerializer(many=True)) + def get_required(self, obj: Gender) -> ReturnList[ReturnDict[str, Any]]: + species = PokemonSpecies.objects.filter(evolved_species__gender=obj).distinct().order_by("id") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesSummarySerializer(species, many=True, context=self.context).data, + ) ############################# @@ -770,13 +811,13 @@ def get_required(self, obj): ############################# -class ExperienceSerializer(serializers.ModelSerializer): +class ExperienceSerializer(serializers.ModelSerializer[Experience]): class Meta: model = Experience fields = ("level", "experience") -class GrowthRateDescriptionSerializer(serializers.ModelSerializer): +class GrowthRateDescriptionSerializer(serializers.ModelSerializer[GrowthRateDescription]): language = LanguageSummarySerializer() class Meta: @@ -784,7 +825,7 @@ class Meta: fields = ("description", "language") -class GrowthRateDetailSerializer(serializers.ModelSerializer): +class GrowthRateDetailSerializer(serializers.ModelSerializer[GrowthRate]): descriptions = GrowthRateDescriptionSerializer(many=True, read_only=True, source="growthratedescription") levels = ExperienceSerializer(many=True, read_only=True, source="experience") pokemon_species = PokemonSpeciesSummarySerializer(many=True, read_only=True, source="pokemonspecies") @@ -799,7 +840,7 @@ class Meta: ########################## -class LanguageNameSerializer(serializers.ModelSerializer): +class LanguageNameSerializer(serializers.ModelSerializer[LanguageName]): language = LanguageSummarySerializer(source="local_language") class Meta: @@ -807,7 +848,7 @@ class Meta: fields = ("name", "language") -class LanguageDetailSerializer(serializers.ModelSerializer): +class LanguageDetailSerializer(serializers.ModelSerializer[Language]): names = LanguageNameSerializer(many=True, read_only=True, source="languagename_language") class Meta: @@ -820,7 +861,7 @@ class Meta: ######################################## -class EncounterConditionNameSerializer(serializers.ModelSerializer): +class EncounterConditionNameSerializer(serializers.ModelSerializer[EncounterConditionName]): language = LanguageSummarySerializer() class Meta: @@ -828,7 +869,7 @@ class Meta: fields = ("name", "language") -class EncounterConditionDetailSerializer(serializers.ModelSerializer): +class EncounterConditionDetailSerializer(serializers.ModelSerializer[EncounterCondition]): names = EncounterConditionNameSerializer(many=True, read_only=True, source="encounterconditionname") values = EncounterConditionValueSummarySerializer(many=True, read_only=True, source="encounterconditionvalue") @@ -837,7 +878,7 @@ class Meta: fields = ("id", "name", "values", "names") -class EncounterConditionValueNameSerializer(serializers.ModelSerializer): +class EncounterConditionValueNameSerializer(serializers.ModelSerializer[EncounterConditionValueName]): language = LanguageSummarySerializer() class Meta: @@ -845,7 +886,7 @@ class Meta: fields = ("name", "language") -class EncounterConditionValueDetailSerializer(serializers.ModelSerializer): +class EncounterConditionValueDetailSerializer(serializers.ModelSerializer[EncounterConditionValue]): condition = EncounterConditionSummarySerializer(source="encounter_condition") names = EncounterConditionValueNameSerializer(many=True, read_only=True, source="encounterconditionvaluename") @@ -854,7 +895,7 @@ class Meta: fields = ("id", "name", "condition", "names") -class EncounterMethodNameSerializer(serializers.ModelSerializer): +class EncounterMethodNameSerializer(serializers.ModelSerializer[EncounterMethodName]): language = LanguageSummarySerializer() class Meta: @@ -862,7 +903,7 @@ class Meta: fields = ("name", "language") -class EncounterMethodDetailSerializer(serializers.ModelSerializer): +class EncounterMethodDetailSerializer(serializers.ModelSerializer[EncounterMethod]): names = EncounterMethodNameSerializer(many=True, read_only=True, source="encountermethodname") class Meta: @@ -870,7 +911,7 @@ class Meta: fields = ("id", "name", "order", "names") -class EncounterSlotSerializer(serializers.ModelSerializer): +class EncounterSlotSerializer(serializers.ModelSerializer[EncounterSlot]): encounter_method = EncounterMethodSummarySerializer() chance = serializers.IntegerField(source="rarity") @@ -879,7 +920,7 @@ class Meta: fields = ("id", "slot", "chance", "encounter_method", "version_group") -class EncounterPokemonDetailSerializer(serializers.ModelSerializer): +class EncounterPokemonDetailSerializer(serializers.ModelSerializer[EncounterPokemonDetail]): class Meta: model = EncounterPokemonDetail fields = ( @@ -890,7 +931,7 @@ class Meta: ) -class EncounterDetailSerializer(serializers.ModelSerializer): +class EncounterDetailSerializer(serializers.ModelSerializer[Encounter]): version = VersionSummarySerializer() location_area = LocationAreaSummarySerializer() pokemon = PokemonSummarySerializer() @@ -910,43 +951,85 @@ class Meta: "pokemon_details", ) - def get_encounter_conditions(self, obj): + @extend_schema_field(EncounterConditionValueSummarySerializer(many=True)) + def get_encounter_conditions(self, obj: Encounter) -> list[dict[str, Any]]: condition_values = EncounterConditionValueMap.objects.filter(encounter=obj) - data = EncounterConditionValueMapSerializer(condition_values, many=True, context=self.context).data - values = [] + data = cast( + "ReturnList[ReturnDict[str, Any]]", + EncounterConditionValueMapSerializer(condition_values, many=True, context=self.context).data, + ) + return [item["condition_value"] for item in data] + + @extend_schema_field(EncounterPokemonDetailSerializer(allow_null=True)) + def get_encounter_pokemon_details(self, obj: Encounter) -> ReturnDict[str, Any] | None: + encounter_pokemon_details = EncounterPokemonDetail.objects.filter(encounter=obj).first() + return cast( + "ReturnDict[str, Any] | None", + EncounterPokemonDetailSerializer(encounter_pokemon_details, context=self.context).data + if encounter_pokemon_details + else None, + ) - for map in data: - values.append(map["condition_value"]) - return values +class LocationAreaNameSerializer(serializers.ModelSerializer[LocationAreaName]): + language = LanguageSummarySerializer() - def get_encounter_pokemon_details(self, obj): - encounter_pokemon_details = EncounterPokemonDetail.objects.filter(encounter=obj) - data = EncounterPokemonDetailSerializer(encounter_pokemon_details, many=True, context=self.context).data + class Meta: + model = LocationAreaName + fields = ("name", "language") - pokemon_details = data[0] if len(data) else None - return pokemon_details +class LocationAreaEncounterVersionDetailSerializer(serializers.Serializer[Any]): + rate = serializers.IntegerField() + version = VersionSummarySerializer() -class LocationAreaEncounterRateSerializer(serializers.ModelSerializer): +class LocationAreaEncounterRateSerializer(serializers.Serializer[Any]): encounter_method = EncounterMethodSummarySerializer() - version = VersionSummarySerializer() + version_details = LocationAreaEncounterVersionDetailSerializer(many=True) - class Meta: - model = LocationAreaEncounterRate - fields = ("rate", "encounter_method", "version") +class LocationAreaEncounterDetailSerializer(serializers.Serializer[Any]): + min_level = serializers.IntegerField() + max_level = serializers.IntegerField() + chance = serializers.IntegerField(source="encounter_slot.rarity", default=0) + method = EncounterMethodSummarySerializer(source="encounter_slot.encounter_method", default=None) + condition_values = serializers.SerializerMethodField("get_encounter_conditions") + pokemon_details = serializers.SerializerMethodField("get_encounter_pokemon_details") -class LocationAreaNameSerializer(serializers.ModelSerializer): - language = LanguageSummarySerializer() + @extend_schema_field(EncounterConditionValueSummarySerializer(many=True)) + def get_encounter_conditions(self, obj: Encounter) -> list[ReturnDict[str, Any]]: + condition_maps = cast("EncounterWithRelations", obj).encounterconditionvaluemap_set.all() + return [ + cast( + "ReturnDict[str, Any]", + EncounterConditionValueSummarySerializer(cv.encounter_condition_value, context=self.context).data, + ) + for cv in condition_maps + ] + + @extend_schema_field(EncounterPokemonDetailSerializer(allow_null=True)) + def get_encounter_pokemon_details(self, obj: Encounter) -> ReturnDict[str, Any] | None: + details_list = list(cast("EncounterWithRelations", obj).encounterpokemondetail_set.all()) + details = details_list[0] if details_list else None + return cast( + "ReturnDict[str, Any] | None", + EncounterPokemonDetailSerializer(details, context=self.context).data if details else None, + ) - class Meta: - model = LocationAreaName - fields = ("name", "language") + +class LocationAreaPokemonEncounterVersionSerializer(serializers.Serializer[Any]): + version = VersionSummarySerializer() + max_chance = serializers.IntegerField() + encounter_details = LocationAreaEncounterDetailSerializer(many=True) -class LocationAreaDetailSerializer(serializers.ModelSerializer): +class LocationAreaPokemonEncounterSerializer(serializers.Serializer[Any]): + pokemon = PokemonSummarySerializer() + version_details = LocationAreaPokemonEncounterVersionSerializer(many=True) + + +class LocationAreaDetailSerializer(serializers.ModelSerializer[LocationArea]): location = LocationSummarySerializer() encounter_method_rates = serializers.SerializerMethodField("get_method_rates") pokemon_encounters = serializers.SerializerMethodField("get_encounters") @@ -964,260 +1047,72 @@ class Meta: "pokemon_encounters", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["encounter_method", "version_details"], - "properties": { - "encounter_method": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["old-rod"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/encounter-method/2/"], - }, - }, - }, - "version_details": { - "type": "array", - "items": { - "type": "object", - "required": ["rate", "version"], - "properties": { - "rate": { - "type": "integer", - "format": "int32", - "examples": [5], - }, - "version": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["platinum"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version/14/"], - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_method_rates(self, obj): + @extend_schema_field(LocationAreaEncounterRateSerializer(many=True)) + def get_method_rates(self, obj: LocationAreaEncounterRate) -> ReturnList[ReturnDict[str, Any]]: # Get encounters related to this area and pull out unique encounter methods - encounter_rates = LocationAreaEncounterRate.objects.filter(location_area=obj).order_by("encounter_method_id") - method_ids = encounter_rates.values("encounter_method_id").distinct() - encounter_rate_list = [] - - for id in method_ids: - encounter_rate_details = OrderedDict() - - # Get each Unique Item by ID - encounter_method_object = EncounterMethod.objects.get(pk=id["encounter_method_id"]) - encounter_method_data = EncounterMethodSummarySerializer(encounter_method_object, context=self.context).data - encounter_rate_details["encounter_method"] = encounter_method_data - - # Get Versions associated with each unique item - area_encounter_objects = encounter_rates.filter(encounter_method_id=id["encounter_method_id"]) - serializer = LocationAreaEncounterRateSerializer(area_encounter_objects, many=True, context=self.context) - encounter_rate_details["version_details"] = [] - - for area_encounter in serializer.data: - version_detail = OrderedDict() - - version_detail["rate"] = area_encounter["rate"] - version_detail["version"] = area_encounter["version"] - - encounter_rate_details["version_details"].append(version_detail) - - encounter_rate_list.append(encounter_rate_details) - - return encounter_rate_list - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["pokemon", "version_details"], - "properties": { - "pokemon": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["tentacool"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon/72/"], - }, - }, - }, - "version_details": { - "type": "array", - "items": { - "type": "object", - "required": ["version", "max_chance", "encounter_details"], - "properties": { - "version": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["diamond"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version/12/"], - }, - }, - }, - "max_chance": { - "type": "integer", - "format": "int32", - "examples": [60], - }, - "encounter_details": { - "type": "object", - "required": [ - "min_level", - "max_level", - "condition_value", - "chance", - "method", - ], - "properties": { - "min_level": { - "type": "integer", - "format": "int32", - "examples": [20], - }, - "max_level": { - "type": "integer", - "format": "int32", - "examples": [30], - }, - "condition_values": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["slot2-sapphire"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": [ - "https://pokeapi.co/api/v2/encounter-condition-value/10/" - ], - }, - }, - }, - "chance": { - "type": "integer", - "format": "int32", - "examples": [60], - }, - "method": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["surf"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/encounter-method/5/"], - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_encounters(self, obj): - # get versions for later use - version_objects = Version.objects.all().order_by("id") - version_data = { - version_object.id: data - for version_object, data in zip( - version_objects, - VersionSummarySerializer(version_objects, many=True, context=self.context).data, - ) - } - - # all encounters associated with location area - all_encounters = Encounter.objects.filter(location_area=obj).order_by("pokemon") - encounters_list = [] - - # break encounters into pokemon groupings - for poke in all_encounters.values("pokemon").distinct(): - pokemon_object = Pokemon.objects.get(pk=poke["pokemon"]) - - pokemon_detail = OrderedDict() - pokemon_detail["pokemon"] = PokemonSummarySerializer(pokemon_object, context=self.context).data - pokemon_detail["version_details"] = [] - - poke_encounters = all_encounters.filter(pokemon=poke["pokemon"]).order_by("version") - - # each pokemon has multiple versions it could be encountered in - for ver in poke_encounters.values("version").distinct(): - version_detail = OrderedDict() - version_detail["version"] = version_data[ver["version"]] - version_detail["max_chance"] = 0 - version_detail["encounter_details"] = [] - - poke_data = EncounterDetailSerializer( - poke_encounters.filter(version=ver["version"]), - many=True, - context=self.context, - ).data + rates = ( + LocationAreaEncounterRate.objects.filter(location_area=obj, encounter_method__isnull=False) + .select_related("encounter_method", "version") + .order_by("encounter_method_id") + ) + grouped_rates: list[dict[str, Any]] = [ + { + "encounter_method": method, + "version_details": list(group_rates), + } + for method, group_rates in itertools.groupby(rates, key=lambda r: r.encounter_method) + ] + return cast( + "ReturnList[ReturnDict[str, Any]]", + LocationAreaEncounterRateSerializer(grouped_rates, many=True, context=self.context).data, + ) - # each version has multiple ways a pokemon can be encountered - for encounter in poke_data: - slot = EncounterSlot.objects.get(pk=encounter["encounter_slot"]) - slot_data = EncounterSlotSerializer(slot, context=self.context).data - del encounter["pokemon"] - del encounter["encounter_slot"] - del encounter["location_area"] - del encounter["version"] - encounter["chance"] = slot_data["chance"] - version_detail["max_chance"] += slot_data["chance"] - encounter["method"] = slot_data["encounter_method"] + @extend_schema_field(LocationAreaPokemonEncounterSerializer(many=True)) + def get_encounters(self, obj: LocationArea) -> ReturnList[ReturnDict[str, Any]]: + encounters = ( + Encounter.objects.filter(location_area=obj) + .select_related( + "pokemon", + "version", + "encounter_slot", + "encounter_slot__encounter_method", + ) + .prefetch_related( + "encounterconditionvaluemap_set", + "encounterpokemondetail_set", + ) + .order_by("pokemon_id", "version_id") + ) - version_detail["encounter_details"].append(encounter) + grouped_data: list[dict[str, Any]] = [] + for pokemon, poke_group in itertools.groupby(encounters, key=lambda e: e.pokemon): + version_details = [] - pokemon_detail["version_details"].append(version_detail) + for version, ver_group in itertools.groupby(poke_group, key=lambda e: e.version): + encounter_list = list(ver_group) + max_chance = sum(e.encounter_slot.rarity for e in encounter_list if e.encounter_slot) + version_details.append( + { + "version": version, + "max_chance": max_chance, + "encounter_details": encounter_list, + } + ) - encounters_list.append(pokemon_detail) + grouped_data.append( + { + "pokemon": pokemon, + "version_details": version_details, + } + ) - return encounters_list + return cast( + "ReturnList[ReturnDict[str, Any]]", + LocationAreaPokemonEncounterSerializer(grouped_data, many=True, context=self.context).data, + ) -class LocationGameIndexSerializer(serializers.ModelSerializer): +class LocationGameIndexSerializer(serializers.ModelSerializer[LocationGameIndex]): generation = GenerationSummarySerializer() class Meta: @@ -1225,7 +1120,7 @@ class Meta: fields = ("game_index", "generation") -class LocationNameSerializer(serializers.ModelSerializer): +class LocationNameSerializer(serializers.ModelSerializer[LocationName]): language = LanguageSummarySerializer() class Meta: @@ -1233,7 +1128,7 @@ class Meta: fields = ("name", "language") -class LocationDetailSerializer(serializers.ModelSerializer): +class LocationDetailSerializer(serializers.ModelSerializer[Location]): region = RegionSummarySerializer() names = LocationNameSerializer(many=True, read_only=True, source="locationname") game_indices = LocationGameIndexSerializer(many=True, read_only=True, source="locationgameindex") @@ -1249,7 +1144,7 @@ class Meta: ######################### -class AbilityEffectTextSerializer(serializers.ModelSerializer): +class AbilityEffectTextSerializer(serializers.ModelSerializer[AbilityEffectText]): language = LanguageSummarySerializer() class Meta: @@ -1257,7 +1152,7 @@ class Meta: fields = ("effect", "short_effect", "language") -class AbilityFlavorTextSerializer(serializers.ModelSerializer): +class AbilityFlavorTextSerializer(serializers.ModelSerializer[AbilityFlavorText]): flavor_text = serializers.CharField() language = LanguageSummarySerializer() version_group = VersionGroupSummarySerializer() @@ -1267,7 +1162,7 @@ class Meta: fields = ("flavor_text", "language", "version_group") -class AbilityChangeEffectTextSerializer(serializers.ModelSerializer): +class AbilityChangeEffectTextSerializer(serializers.ModelSerializer[AbilityChangeEffectText]): language = LanguageSummarySerializer() class Meta: @@ -1278,7 +1173,7 @@ class Meta: ) -class AbilityChangeSerializer(serializers.ModelSerializer): +class AbilityChangeSerializer(serializers.ModelSerializer[AbilityChange]): version_group = VersionGroupSummarySerializer() effect_entries = AbilityChangeEffectTextSerializer(many=True, read_only=True, source="abilitychangeeffecttext") @@ -1287,7 +1182,7 @@ class Meta: fields = ("version_group", "effect_entries") -class AbilityNameSerializer(serializers.ModelSerializer): +class AbilityNameSerializer(serializers.ModelSerializer[AbilityName]): language = LanguageSummarySerializer() class Meta: @@ -1295,7 +1190,15 @@ class Meta: fields = ("name", "language") -class AbilityDetailSerializer(serializers.ModelSerializer): +class AbilityPokemonDetailSerializer(serializers.ModelSerializer[PokemonAbility]): + pokemon = PokemonSummarySerializer() + + class Meta: + model = PokemonAbility + fields = ("is_hidden", "slot", "pokemon") + + +class AbilityDetailSerializer(serializers.ModelSerializer[Ability]): effect_entries = AbilityEffectTextSerializer(many=True, read_only=True, source="abilityeffecttext") flavor_text_entries = AbilityFlavorTextSerializer(many=True, read_only=True, source="abilityflavortext") names = AbilityNameSerializer(many=True, read_only=True, source="abilityname") @@ -1317,41 +1220,13 @@ class Meta: "pokemon", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["is_hidden", "slot", "pokemon"], - "properties": { - "is_hidden": {"type": "boolean"}, - "slot": {"type": "integer", "format": "int32", "examples": [3]}, - "pokemon": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["gloom"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon/44/"], - }, - }, - }, - }, - }, - } - ) - def get_ability_pokemon(self, obj): - pokemon_ability_objects = PokemonAbility.objects.filter(ability=obj) - data = PokemonAbilitySerializer(pokemon_ability_objects, many=True, context=self.context).data - pokemon = [] - - for poke in data: - del poke["ability"] - pokemon.append(poke) - - return pokemon + @extend_schema_field(AbilityPokemonDetailSerializer(many=True)) + def get_ability_pokemon(self, obj: Ability) -> ReturnList[ReturnDict[str, Any]]: + pokemon_ability_objects = PokemonAbility.objects.filter(ability=obj).select_related("pokemon") + return cast( + "ReturnList[ReturnDict[str, Any]]", + AbilityPokemonDetailSerializer(pokemon_ability_objects, many=True, context=self.context).data, + ) ###################### @@ -1359,7 +1234,7 @@ def get_ability_pokemon(self, obj): ###################### -class StatNameSerializer(serializers.ModelSerializer): +class StatNameSerializer(serializers.ModelSerializer[StatName]): language = LanguageSummarySerializer() class Meta: @@ -1367,7 +1242,25 @@ class Meta: fields = ("name", "language") -class StatDetailSerializer(serializers.ModelSerializer): +class MoveStatChangeSerializer(serializers.ModelSerializer[MoveMetaStatChange]): + move = MoveSummarySerializer() + + class Meta: + model = MoveMetaStatChange + fields = ("change", "move") + + +class StatAffectingMovesSerializer(serializers.Serializer[Any]): + increase = MoveStatChangeSerializer(many=True) + decrease = MoveStatChangeSerializer(many=True) + + +class StatAffectingNaturesSerializer(serializers.Serializer[Any]): + increase = NatureSummarySerializer(many=True) + decrease = NatureSummarySerializer(many=True) + + +class StatDetailSerializer(serializers.ModelSerializer[Stat]): names = StatNameSerializer(many=True, read_only=True, source="statname") move_damage_class = MoveDamageClassSummarySerializer() characteristics = CharacteristicSummarySerializer(many=True, read_only=True, source="characteristic") @@ -1390,198 +1283,56 @@ class Meta: "names", ) - @extend_schema_field( - field={ - "type": "object", - "required": ["decrease", "increase"], - "properties": { - "increase": { - "type": "array", - "items": { - "type": "object", - "required": ["change", "move"], - "properties": { - "change": { - "type": "integer", - "format": "int32", - "examples": [-1], - }, - "move": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["swords-dance"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/14/"], - }, - }, - }, - }, - }, - }, - "decrease": { - "type": "array", - "items": { - "type": "object", - "required": ["change", "move"], - "properties": { - "change": { - "type": "integer", - "format": "int32", - "examples": [5], - }, - "move": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["growl"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/45/"], - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_moves_that_affect(self, obj): - stat_change_objects = MoveMetaStatChange.objects.filter(stat=obj) - stat_changes = MoveMetaStatChangeSerializer(stat_change_objects, many=True, context=self.context).data - changes = OrderedDict([("increase", []), ("decrease", [])]) - - for change in stat_changes: - del change["stat"] - if change["change"] > 0: - changes["increase"].append(change) - else: - changes["decrease"].append(change) - - return changes - - @extend_schema_field( - field={ - "type": "object", - "required": ["increase", "decrease"], - "properties": { - "increase": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["lonely"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/nature/6/"], - }, - }, - }, - }, - "decrease": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bold"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/nature/2/"], - }, - }, - }, - }, - }, - } - ) - def get_natures_that_affect(self, obj): - increase_objects = Nature.objects.filter(increased_stat=obj) - increases = NatureSummarySerializer(increase_objects, many=True, context=self.context).data - decrease_objects = Nature.objects.filter(decreased_stat=obj) - decreases = NatureSummarySerializer(decrease_objects, many=True, context=self.context).data - - return OrderedDict([("increase", increases), ("decrease", decreases)]) - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["protein", "x-attack"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/item/46/"], - }, - }, - }, - } - ) - def get_items_that_affect(self, obj): + @extend_schema_field(StatAffectingMovesSerializer) + def get_moves_that_affect(self, obj: Stat) -> ReturnDict[str, Any]: + stat_change_objects = MoveMetaStatChange.objects.filter(stat=obj).select_related("move") + increases = stat_change_objects.filter(change__gt=0) + decreases = stat_change_objects.filter(change__lte=0) + + return cast( + "ReturnDict[str, Any]", + StatAffectingMovesSerializer({"increase": increases, "decrease": decreases}, context=self.context).data, + ) + + @extend_schema_field(StatAffectingNaturesSerializer) + def get_natures_that_affect(self, obj: Stat) -> ReturnDict[str, Any]: + increases = Nature.objects.filter(increased_stat=obj) + decreases = Nature.objects.filter(decreased_stat=obj) + + return cast( + "ReturnDict[str, Any]", + StatAffectingNaturesSerializer({"increase": increases, "decrease": decreases}, context=self.context).data, + ) + + @extend_schema_field(ItemSummarySerializer(many=True)) + def get_items_that_affect(self, obj: Stat) -> ReturnList[ReturnDict[str, Any]]: """ Get items that affect this stat (like vitamins, X-items, etc.) """ - # Map stat names to their corresponding vitamin items + # Map stat names to their corresponding vitamin and X-item names stat_item_mapping = { "hp": ["hp-up"], - "attack": ["protein"], - "defense": ["iron"], - "special-attack": ["calcium"], - "special-defense": ["zinc"], - "speed": ["carbos"], + "attack": ["protein", "x-attack"], + "defense": ["iron", "x-defense"], + "special-attack": ["calcium", "x-sp-atk"], + "special-defense": ["zinc", "x-sp-def"], + "speed": ["carbos", "x-speed"], + "accuracy": ["x-accuracy"], + "evasion": ["x-evasion"], } # Get the stat name (lowercase) stat_name = obj.name.lower() + # Get the corresponding item names for this stat + item_names = stat_item_mapping.get(stat_name, []) - # Find items that affect this stat - affecting_items = [] - - # Check for vitamin items - if stat_name in stat_item_mapping: - for item_identifier in stat_item_mapping[stat_name]: - try: - item = Item.objects.get(name=item_identifier) - affecting_items.append(ItemSummarySerializer(item, context=self.context).data) - except Item.DoesNotExist: - pass - - # Check for X-items (like X Attack, X Defense, etc.) - x_item_mapping = { - "attack": ["x-attack"], - "defense": ["x-defense"], - "special-attack": ["x-sp-atk"], - "special-defense": ["x-sp-def"], - "speed": ["x-speed"], - "accuracy": ["x-accuracy"], - "evasion": ["x-evasion"], - } - - if stat_name in x_item_mapping: - for item_identifier in x_item_mapping[stat_name]: - try: - item = Item.objects.get(name=item_identifier) - affecting_items.append(ItemSummarySerializer(item, context=self.context).data) - except Item.DoesNotExist: - pass + if not item_names: + return cast("ReturnList[ReturnDict[str, Any]]", []) - return affecting_items + items = Item.objects.filter(name__in=item_names) + return cast( + "ReturnList[ReturnDict[str, Any]]", ItemSummarySerializer(items, many=True, context=self.context).data + ) ############################# @@ -1589,15 +1340,15 @@ def get_items_that_affect(self, obj): ############################# -class ItemPocketNameSerializer(serializers.ModelSerializer): +class ItemPocketNameSerializer(serializers.ModelSerializer[ItemPocketName]): language = LanguageSummarySerializer() class Meta: - model = ItemName + model = ItemPocketName fields = ("name", "language") -class ItemPocketDetailSerializer(serializers.ModelSerializer): +class ItemPocketDetailSerializer(serializers.ModelSerializer[ItemPocket]): names = ItemPocketNameSerializer(many=True, read_only=True, source="itempocketname") categories = ItemCategorySummarySerializer(many=True, read_only=True, source="itemcategory") @@ -1609,15 +1360,17 @@ class Meta: ############################### # ITEM CATEGORY SERIALIZERS # ############################### -class ItemCategoryNameSerializer(serializers.ModelSerializer): + + +class ItemCategoryNameSerializer(serializers.ModelSerializer[ItemCategoryName]): language = LanguageSummarySerializer() class Meta: - model = ItemName + model = ItemCategoryName fields = ("name", "language") -class ItemCategoryDetailSerializer(serializers.ModelSerializer): +class ItemCategoryDetailSerializer(serializers.ModelSerializer[ItemCategory]): names = ItemCategoryNameSerializer(many=True, read_only=True, source="itemcategoryname") pocket = ItemPocketSummarySerializer(source="item_pocket") items = ItemSummarySerializer(many=True, read_only=True, source="item") @@ -1632,7 +1385,7 @@ class Meta: ################################ -class ItemAttributeNameSerializer(serializers.ModelSerializer): +class ItemAttributeNameSerializer(serializers.ModelSerializer[ItemAttributeName]): language = LanguageSummarySerializer() class Meta: @@ -1640,7 +1393,7 @@ class Meta: fields = ("name", "language") -class ItemAttributeDescriptionSerializer(serializers.ModelSerializer): +class ItemAttributeDescriptionSerializer(serializers.ModelSerializer[ItemAttributeDescription]): language = LanguageSummarySerializer() class Meta: @@ -1648,7 +1401,7 @@ class Meta: fields = ("description", "language") -class ItemAttributeDetailSerializer(serializers.ModelSerializer): +class ItemAttributeDetailSerializer(serializers.ModelSerializer[ItemAttribute]): names = ItemAttributeNameSerializer(many=True, read_only=True, source="itemattributename") descriptions = ItemAttributeDescriptionSerializer(many=True, read_only=True, source="itemattributedescription") items = serializers.SerializerMethodField("get_attribute_items") @@ -1657,33 +1410,12 @@ class Meta: model = ItemAttribute fields = ("id", "name", "descriptions", "items", "names") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["master-ball"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/item/1/"], - }, - }, - }, - } - ) - def get_attribute_items(self, obj): - item_map_objects = ItemAttributeMap.objects.filter(item_attribute=obj) - items = [] - - for map in item_map_objects: - item_obj = Item.objects.get(pk=map.item.id) - item = ItemSummarySerializer(item_obj, context=self.context).data - items.append(item) - - return items + @extend_schema_field(ItemSummarySerializer(many=True)) + def get_attribute_items(self, obj: ItemAttribute) -> ReturnList[ReturnDict[str, Any]]: + items = Item.objects.filter(itemattributemap__item_attribute=obj, itemattributemap__item__isnull=False) + return cast( + "ReturnList[ReturnDict[str, Any]]", ItemSummarySerializer(items, many=True, context=self.context).data + ) ########################### @@ -1691,7 +1423,7 @@ def get_attribute_items(self, obj): ########################### -class CurrencyNameSerializer(serializers.ModelSerializer): +class CurrencyNameSerializer(serializers.ModelSerializer[CurrencyName]): language = LanguageSummarySerializer() class Meta: @@ -1699,7 +1431,7 @@ class Meta: fields = ("name", "language") -class CurrencyDetailSerializer(serializers.ModelSerializer): +class CurrencyDetailSerializer(serializers.ModelSerializer[Currency]): names = CurrencyNameSerializer(many=True, read_only=True, source="currencyname") class Meta: @@ -1710,7 +1442,9 @@ class Meta: ################################### # ITEM FLING EFFECT SERIALIZERS # ################################### -class ItemFlingEffectEffectTextSerializer(serializers.ModelSerializer): + + +class ItemFlingEffectEffectTextSerializer(serializers.ModelSerializer[ItemFlingEffectEffectText]): language = LanguageSummarySerializer() class Meta: @@ -1718,7 +1452,7 @@ class Meta: fields = ("effect", "language") -class ItemFlingEffectDetailSerializer(serializers.ModelSerializer): +class ItemFlingEffectDetailSerializer(serializers.ModelSerializer[ItemFlingEffect]): effect_entries = ItemFlingEffectEffectTextSerializer(many=True, read_only=True, source="itemflingeffecteffecttext") items = ItemSummarySerializer(many=True, read_only=True, source="item") @@ -1730,7 +1464,9 @@ class Meta: ####################### # ITEM SERIALIZERS # ####################### -class ItemFlavorTextSerializer(serializers.ModelSerializer): + + +class ItemFlavorTextSerializer(serializers.ModelSerializer[ItemFlavorText]): text = serializers.CharField(source="flavor_text") language = LanguageSummarySerializer() version_group = VersionGroupSummarySerializer() @@ -1740,7 +1476,7 @@ class Meta: fields = ("text", "version_group", "language") -class ItemEffectTextSerializer(serializers.ModelSerializer): +class ItemEffectTextSerializer(serializers.ModelSerializer[ItemEffectText]): language = LanguageSummarySerializer() class Meta: @@ -1748,7 +1484,7 @@ class Meta: fields = ("effect", "short_effect", "language") -class ItemGameIndexSerializer(serializers.ModelSerializer): +class ItemGameIndexSerializer(serializers.ModelSerializer[ItemGameIndex]): generation = GenerationSummarySerializer() class Meta: @@ -1756,7 +1492,7 @@ class Meta: fields = ("game_index", "generation") -class ItemPriceSerializer(serializers.ModelSerializer): +class ItemPriceSerializer(serializers.ModelSerializer[ItemPrice]): currency = CurrencySummarySerializer() version_group = VersionGroupSummarySerializer() @@ -1770,7 +1506,7 @@ class Meta: ) -class ItemNameSerializer(serializers.ModelSerializer): +class ItemNameSerializer(serializers.ModelSerializer[ItemName]): language = LanguageSummarySerializer() class Meta: @@ -1778,13 +1514,30 @@ class Meta: fields = ("name", "language") -class ItemSpritesSerializer(serializers.ModelSerializer): +class ItemSpritesSerializer(serializers.Serializer[Any]): + default = serializers.CharField(allow_null=True) + + +class PokemonHeldItemVersionSerializer(serializers.Serializer[Any]): + rarity = serializers.IntegerField() + version = VersionSummarySerializer() + + +class PokemonHeldItemSerializer(serializers.Serializer[Any]): + pokemon = PokemonSummarySerializer() + version_details = PokemonHeldItemVersionSerializer(many=True) + + +class ItemMachineSerializer(serializers.ModelSerializer[Machine]): + machine = MachineSummarySerializer(source="*") + version_group = VersionGroupSummarySerializer() + class Meta: - model = ItemSprites - fields = ("sprites",) + model = Machine + fields = ("machine", "version_group") -class ItemDetailSerializer(serializers.ModelSerializer): +class ItemDetailSerializer(serializers.ModelSerializer[Item]): names = ItemNameSerializer(many=True, read_only=True, source="itemname") game_indices = ItemGameIndexSerializer(many=True, read_only=True, source="itemgameindex") prices = ItemPriceSerializer(many=True, read_only=True, source="itemprice") @@ -1793,8 +1546,8 @@ class ItemDetailSerializer(serializers.ModelSerializer): category = ItemCategorySummarySerializer(source="item_category") attributes = serializers.SerializerMethodField("get_item_attributes") fling_effect = ItemFlingEffectSummarySerializer(source="item_fling_effect") - held_by_pokemon = serializers.SerializerMethodField(source="get_held_by_pokemon") - baby_trigger_for = serializers.SerializerMethodField(source="get_baby_trigger_for") + held_by_pokemon = serializers.SerializerMethodField("get_held_by_pokemon") + baby_trigger_for = serializers.SerializerMethodField("get_baby_trigger_for") sprites = serializers.SerializerMethodField("get_item_sprites") machines = serializers.SerializerMethodField("get_item_machines") @@ -1818,206 +1571,61 @@ class Meta: "machines", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["machine", "version_group"], - "properties": { - "machine": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/machine/1/"], - }, - "version_group": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["sword-shield"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version-group/20/"], - }, - }, - }, - }, - }, - } - ) - def get_item_machines(self, obj): - machine_objects = Machine.objects.filter(item=obj) + @extend_schema_field(ItemMachineSerializer(many=True)) + def get_item_machines(self, obj: Item) -> list[ReturnDict[str, Any]]: + machine_objects = Machine.objects.filter(item=obj).select_related("version_group") + return cast( + "list[ReturnDict[str, Any]]", + ItemMachineSerializer(machine_objects, many=True, context=self.context).data, + ) - machines = [] + @extend_schema_field(ItemSpritesSerializer) + def get_item_sprites(self, obj: Item) -> dict[str, str | None]: + sprites_object = ItemSprites.objects.filter(item=obj).first() + return sprites_object.sprites if sprites_object else {} + + @extend_schema_field(ItemAttributeSummarySerializer(many=True)) + def get_item_attributes(self, obj: Item) -> ReturnList[ReturnDict[str, Any]]: + attributes = ItemAttribute.objects.filter(itemattributemap__item=obj) + return cast( + "ReturnList[ReturnDict[str, Any]]", + ItemAttributeSummarySerializer(attributes, many=True, context=self.context).data, + ) - for machine_object in machine_objects: - machine_data = MachineSummarySerializer(machine_object, context=self.context).data + @extend_schema_field(PokemonHeldItemSerializer(many=True)) + def get_held_by_pokemon(self, obj: Item) -> ReturnList[ReturnDict[str, Any]]: + pokemon_items = ( + PokemonItem.objects.filter(item=obj) + .select_related("pokemon", "version") + .order_by("pokemon_id", "version_id") + ) + grouped_data: list[dict[str, Any]] = [ + { + "pokemon": pokemon, + "version_details": list(items), + } + for pokemon, items in itertools.groupby(pokemon_items, key=lambda pi: pi.pokemon) + ] + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonHeldItemSerializer(grouped_data, many=True, context=self.context).data, + ) - version_group_data = VersionGroupSummarySerializer(machine_object.version_group, context=self.context).data + @extend_schema_field(EvolutionChainSummarySerializer(allow_null=True)) + def get_baby_trigger_for(self, obj: Item) -> ReturnDict[str, Any] | None: + chain_object = EvolutionChain.objects.filter(baby_trigger_item=obj).first() + return cast( + "ReturnDict[str, Any] | None", + EvolutionChainSummarySerializer(chain_object, context=self.context).data if chain_object else None, + ) - machines.append({"machine": machine_data, "version_group": version_group_data}) - return machines +######################## +# NATURE SERIALIZERS # +######################## - @extend_schema_field( - field={ - "type": "object", - "required": ["default"], - "properties": { - "default": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/media/sprites/items/master-ball.png"], - } - }, - } - ) - def get_item_sprites(self, obj): - sprites_object = ItemSprites.objects.get(item_id=obj) - return sprites_object.sprites - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["countable"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/item-attribute/1/"], - }, - }, - }, - } - ) - def get_item_attributes(self, obj): - item_attribute_maps = ItemAttributeMap.objects.filter(item=obj) - serializer = ItemAttributeMapSerializer(item_attribute_maps, many=True, context=self.context) - data = serializer.data - - attributes = [] - - for map in data: - attribute = OrderedDict() - attribute["name"] = map["attribute"]["name"] - attribute["url"] = map["attribute"]["url"] - attributes.append(attribute) - - return attributes - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["pokemon", "version-details"], - "properties": { - "pokemon": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["farfetchd"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon/83/"], - }, - }, - }, - "version-details": { - "type": "array", - "items": { - "type": "object", - "required": ["rarity", "version"], - "properties": { - "rarity": { - "type": "integer", - "format": "int32", - "examples": [5], - }, - "version": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["ruby"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version/7/"], - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_held_by_pokemon(self, obj): - pokemon_items = PokemonItem.objects.filter(item=obj).order_by("pokemon_id") - pokemon_ids = pokemon_items.values("pokemon_id").distinct() - pokemon_list = [] - - for id in pokemon_ids: - item_pokemon_details = OrderedDict() - - # Get each Unique Item by ID - pokemon_object = Pokemon.objects.get(pk=id["pokemon_id"]) - pokemon_data = PokemonSummarySerializer(pokemon_object, context=self.context).data - item_pokemon_details["pokemon"] = pokemon_data - - # Get Versions associated with each unique item - pokemon_item_objects = pokemon_items.filter(pokemon_id=id["pokemon_id"]) - serializer = PokemonItemSerializer(pokemon_item_objects, many=True, context=self.context) - item_pokemon_details["version_details"] = [] - - for pokemon in serializer.data: - version_detail = OrderedDict() - version_detail["rarity"] = pokemon["rarity"] - version_detail["version"] = pokemon["version"] - item_pokemon_details["version_details"].append(version_detail) - - pokemon_list.append(item_pokemon_details) - - return pokemon_list - - @extend_schema_field( - field={ - "type": "object", - "required": ["url"], - "properties": { - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/evolution-chain/51/"], - } - }, - } - ) - def get_baby_trigger_for(self, obj): - try: - chain_object = EvolutionChain.objects.get(baby_trigger_item=obj) - data = EvolutionChainSummarySerializer(chain_object, context=self.context).data - except EvolutionChain.DoesNotExist: - data = None - - return data - - -######################## -# NATURE SERIALIZERS # -######################## - -class NatureBattleStylePreferenceSerializer(serializers.ModelSerializer): +class NatureBattleStylePreferenceSerializer(serializers.ModelSerializer[NatureBattleStylePreference]): move_battle_style = MoveBattleStyleSummarySerializer() class Meta: @@ -2029,7 +1637,7 @@ class Meta: ) -class NatureNameSerializer(serializers.ModelSerializer): +class NatureNameSerializer(serializers.ModelSerializer[NatureName]): language = LanguageSummarySerializer() class Meta: @@ -2037,7 +1645,15 @@ class Meta: fields = ("name", "language") -class NatureDetailSerializer(serializers.ModelSerializer): +class NaturePokeathlonStatSerializer(serializers.ModelSerializer[NaturePokeathlonStat]): + pokeathlon_stat = PokeathlonStatSummarySerializer() + + class Meta: + model = NaturePokeathlonStat + fields = ("max_change", "pokeathlon_stat") + + +class NatureDetailSerializer(serializers.ModelSerializer[Nature]): names = NatureNameSerializer(many=True, read_only=True, source="naturename") decreased_stat = StatSummarySerializer() increased_stat = StatSummarySerializer() @@ -2064,42 +1680,13 @@ class Meta: "names", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["max_change", "pokeathlon_stat"], - "properties": { - "max_change": { - "type": "integer", - "format": "int32", - "examples": [1], - }, - "pokeathlon_stat": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["power"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokeathlon-stat/2/"], - }, - }, - }, - }, - }, - } - ) - def get_pokeathlon_stats(self, obj): - pokeathlon_stat_objects = NaturePokeathlonStat.objects.filter(nature=obj) - pokeathlon_stats = NaturePokeathlonStatSerializer(pokeathlon_stat_objects, many=True, context=self.context).data - - for stat in pokeathlon_stats: - del stat["nature"] - - return pokeathlon_stats + @extend_schema_field(NaturePokeathlonStatSerializer(many=True)) + def get_pokeathlon_stats(self, obj: Nature) -> ReturnList[ReturnDict[str, Any]]: + pokeathlon_stat_objects = NaturePokeathlonStat.objects.filter(nature=obj).select_related("pokeathlon_stat") + return cast( + "ReturnList[ReturnDict[str, Any]]", + NaturePokeathlonStatSerializer(pokeathlon_stat_objects, many=True, context=self.context).data, + ) ####################### @@ -2107,7 +1694,7 @@ def get_pokeathlon_stats(self, obj): ####################### -class BerryFirmnessNameSerializer(serializers.ModelSerializer): +class BerryFirmnessNameSerializer(serializers.ModelSerializer[BerryFirmnessName]): language = LanguageSummarySerializer() class Meta: @@ -2115,7 +1702,7 @@ class Meta: fields = ("name", "language") -class BerryFirmnessDetailSerializer(serializers.ModelSerializer): +class BerryFirmnessDetailSerializer(serializers.ModelSerializer[BerryFirmness]): names = BerryFirmnessNameSerializer(many=True, read_only=True, source="berryfirmnessname") berries = BerrySummarySerializer(many=True, read_only=True, source="berry") @@ -2124,7 +1711,7 @@ class Meta: fields = ("id", "name", "berries", "names") -class BerryFlavorNameSerializer(serializers.ModelSerializer): +class BerryFlavorNameSerializer(serializers.ModelSerializer[BerryFlavorName]): language = LanguageSummarySerializer() class Meta: @@ -2132,7 +1719,15 @@ class Meta: fields = ("name", "language") -class BerryFlavorDetailSerializer(serializers.ModelSerializer): +class BerryFlavorBerryMapSerializer(serializers.ModelSerializer[BerryFlavorMap]): + berry = BerrySummarySerializer() + + class Meta: + model = BerryFlavorMap + fields = ("potency", "berry") + + +class BerryFlavorDetailSerializer(serializers.ModelSerializer[BerryFlavor]): names = BerryFlavorNameSerializer(many=True, read_only=True, source="berryflavorname") contest_type = ContestTypeSummarySerializer() berries = serializers.SerializerMethodField("get_berries_with_flavor") @@ -2141,46 +1736,26 @@ class Meta: model = BerryFlavor fields = ("id", "name", "berries", "contest_type", "names") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["potency", "berry"], - "properties": { - "potency": {"type": "integer", "examples": [10]}, - "berry": { - "type": "object", - "require": ["name", "url"], - "properties": { - "name": { - "type": "string", - "description": "The name of the berry", - "examples": ["rowap"], - }, - "url": { - "type": "string", - "format": "uri", - "description": "The URL to get more information about the berry", - "examples": ["https://pokeapi.co/api/v2/berry/64/"], - }, - }, - }, - }, - }, - } - ) - def get_berries_with_flavor(self, obj): - flavor_map_objects = BerryFlavorMap.objects.filter(berry_flavor=obj, potency__gt=0).order_by("potency") - flavor_maps = BerryFlavorMapSerializer(flavor_map_objects, many=True, context=self.context).data + @extend_schema_field(BerryFlavorBerryMapSerializer(many=True)) + def get_berries_with_flavor(self, obj: BerryFlavor) -> ReturnList[ReturnDict[str, Any]]: + flavor_map_objects = ( + BerryFlavorMap.objects.filter(berry_flavor=obj, potency__gt=0).select_related("berry").order_by("potency") + ) + return cast( + "ReturnList[ReturnDict[str, Any]]", + BerryFlavorBerryMapSerializer(flavor_map_objects, many=True, context=self.context).data, + ) - for map in flavor_maps: - del map["flavor"] - return flavor_maps +class BerryFlavorMapSerializer(serializers.ModelSerializer[BerryFlavorMap]): + flavor = BerryFlavorSummarySerializer(source="berry_flavor") + + class Meta: + model = BerryFlavorMap + fields = ("potency", "flavor") -class BerryDetailSerializer(serializers.ModelSerializer): +class BerryDetailSerializer(serializers.ModelSerializer[Berry]): item = ItemSummarySerializer() natural_gift_type = TypeSummarySerializer() firmness = BerryFirmnessSummarySerializer(source="berry_firmness") @@ -2203,60 +1778,21 @@ class Meta: "natural_gift_type", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["potency", "flavor"], - "properties": { - "potency": {"type": "integer", "examples": [10]}, - "flavor": { - "type": "object", - "require": ["name", "url"], - "properties": { - "name": { - "type": "string", - "description": "The name of the flavor", - "examples": ["spicy"], - }, - "url": { - "type": "string", - "format": "uri", - "description": "The URL to get more information about the flavor", - "examples": ["https://pokeapi.co/api/v2/berry-flavor/1/"], - }, - }, - }, - }, - }, - } - ) - def get_berry_flavors(self, obj): - flavor_map_objects = BerryFlavorMap.objects.filter(berry=obj) - flavor_maps = BerryFlavorMapSerializer(flavor_map_objects, many=True, context=self.context).data - flavors = [] - - for map in flavor_maps: - del map["berry"] - flavors.append(map) - - return flavors + @extend_schema_field(BerryFlavorMapSerializer(many=True)) + def get_berry_flavors(self, obj: Berry) -> ReturnList[ReturnDict[str, Any]]: + flavor_map_objects = BerryFlavorMap.objects.filter(berry=obj).select_related("berry_flavor") + return cast( + "ReturnList[ReturnDict[str, Any]]", + BerryFlavorMapSerializer(flavor_map_objects, many=True, context=self.context).data, + ) ########################### # EGG GROUP SERIALIZERS # ########################### -class PokemonEggGroupSerializer(serializers.ModelSerializer): - species = PokemonSpeciesSummarySerializer(source="pokemon_species") - egg_group = EggGroupSummarySerializer() - - class Meta: - model = PokemonEggGroup - fields = ("species", "egg_group") -class EggGroupNameSerializer(serializers.ModelSerializer): +class EggGroupNameSerializer(serializers.ModelSerializer[EggGroupName]): language = LanguageSummarySerializer() class Meta: @@ -2264,7 +1800,7 @@ class Meta: fields = ("name", "language") -class EggGroupDetailSerializer(serializers.ModelSerializer): +class EggGroupDetailSerializer(serializers.ModelSerializer[EggGroup]): names = EggGroupNameSerializer(many=True, read_only=True, source="egggroupname") pokemon_species = serializers.SerializerMethodField("get_species") @@ -2272,49 +1808,21 @@ class Meta: model = EggGroup fields = ("id", "name", "names", "pokemon_species") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["potency", "flavor"], - "properties": { - "name": { - "type": "string", - "description": "Pokemon species name.", - "examples": ["bulbasaur"], - }, - "url": { - "type": "string", - "format": "uri", - "description": "The URL to get more information about the species", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/1/"], - }, - }, - }, - } - ) - def get_species(self, obj): - results = PokemonEggGroup.objects.filter(egg_group=obj) - data = PokemonEggGroupSerializer(results, many=True, context=self.context).data - associated_species = [] - for species in data: - associated_species.append(species["species"]) - - return associated_species + @extend_schema_field(PokemonSpeciesSummarySerializer(many=True)) + def get_species(self, obj: EggGroup) -> ReturnList[ReturnDict[str, Any]]: + species = PokemonSpecies.objects.filter(pokemonegggroup__egg_group=obj) + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesSummarySerializer(species, many=True, context=self.context).data, + ) ###################### # TYPE SERIALIZERS # ###################### -# https://stackoverflow.com/a/45987450/3482533 -class TypeEfficacySerializer(serializers.ModelSerializer): - class Meta: - model = TypeEfficacy - fields = "__all__" -class TypeEfficacyPastSerializer(serializers.ModelSerializer): +class TypeEfficacyPastSerializer(serializers.ModelSerializer[TypeEfficacyPast]): generation = GenerationSummarySerializer() class Meta: @@ -2322,7 +1830,7 @@ class Meta: fields = ("target_type", "damage_type", "damage_factor", "generation") -class TypeGameIndexSerializer(serializers.ModelSerializer): +class TypeGameIndexSerializer(serializers.ModelSerializer[TypeGameIndex]): generation = GenerationSummarySerializer() class Meta: @@ -2330,7 +1838,7 @@ class Meta: fields = ("game_index", "generation") -class TypeNameSerializer(serializers.ModelSerializer): +class TypeNameSerializer(serializers.ModelSerializer[TypeName]): language = LanguageSummarySerializer() class Meta: @@ -2338,19 +1846,41 @@ class Meta: fields = ("name", "language") -class TypeSpriteSerializer(serializers.ModelSerializer): +class TypeSpriteSerializer(serializers.ModelSerializer[TypeSprites]): class Meta: model = TypeSprites fields = ("sprites",) -class TypeDetailSerializer(serializers.ModelSerializer): +class TypeRelationshipsSerializer(serializers.Serializer[Any]): + no_damage_to = TypeSummarySerializer(many=True) + half_damage_to = TypeSummarySerializer(many=True) + double_damage_to = TypeSummarySerializer(many=True) + no_damage_from = TypeSummarySerializer(many=True) + half_damage_from = TypeSummarySerializer(many=True) + double_damage_from = TypeSummarySerializer(many=True) + + +class TypePastRelationshipsSerializer(serializers.Serializer[Any]): + generation = GenerationSummarySerializer() + damage_relations = TypeRelationshipsSerializer() + + +class TypePokemonSerializer(serializers.ModelSerializer[PokemonType]): + pokemon = PokemonSummarySerializer() + + class Meta: + model = PokemonType + fields = ("slot", "pokemon") + + +class TypeDetailSerializer(serializers.ModelSerializer[Type]): """ Serializer for the Type resource """ generation = GenerationSummarySerializer() - names = AbilityNameSerializer(many=True, read_only=True, source="typename") + names = TypeNameSerializer(many=True, read_only=True, source="typename") game_indices = TypeGameIndexSerializer(many=True, read_only=True, source="typegameindex") move_damage_class = MoveDamageClassSummarySerializer() damage_relations = serializers.SerializerMethodField("get_type_relationships") @@ -2359,6 +1889,16 @@ class TypeDetailSerializer(serializers.ModelSerializer): moves = MoveSummarySerializer(many=True, read_only=True, source="move") sprites = serializers.SerializerMethodField("get_type_sprites") + FACTOR_PREFIX_MAP: ClassVar[dict[int, str]] = {200: "double", 50: "half", 0: "no"} + RELATION_KEYS = ( + "no_damage_to", + "half_damage_to", + "double_damage_to", + "no_damage_from", + "half_damage_from", + "double_damage_from", + ) + class Meta: model = Type fields = ( @@ -2375,509 +1915,126 @@ class Meta: "sprites", ) - @extend_schema_field( - field={ - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "name-icon": { - "type": "string", - "format": "uri", - "examples": [ - "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/colosseum/1.png" - ], - } - }, - "examples": [ - { - "colosseum": { - "name_icon": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/colosseum/1.png" - } - } - ], - }, - "examples": [ - { - "generation-ix": { - "scarlet-violet": { - "name_icon": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-ix/scarlet-violet/1.png" - } - } - } - ], - }, - "examples": [ - { - "sprites": { - "generation-iii": { - "colosseum": { - "name_icon": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/colosseum/1.png" - }, - "emerald": { - "name_icon": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/emerald/1.png" - }, - "firered-leafgreen": { - "name_icon": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/firered-leafgreen/1.png" - }, - "ruby-sapphire": { - "name_icon": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/ruby-sapphire/1.png" - }, - "xd": { - "name_icon": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/types/generation-iii/xd/1.png" - }, - } - } - } - ], - } - ) - def get_type_sprites(self, obj): - sprites_object = TypeSprites.objects.get(type_id=obj) - return sprites_object.sprites - - # adds an entry for the given type with the given damage - # factor in the given direction to the set of relations - - def add_type_entry(self, relations, type, damage_factor, direction="_damage_to"): - if damage_factor == 200: - relations["double" + direction].append(TypeSummarySerializer(type, context=self.context).data) - elif damage_factor == 50: - relations["half" + direction].append(TypeSummarySerializer(type, context=self.context).data) - elif damage_factor == 0: - relations["no" + direction].append(TypeSummarySerializer(type, context=self.context).data) - - @extend_schema_field( - field={ - "type": "object", - "required": [ - "no_damage_to", - "half_damage_to", - "double_damage_to", - "no_damage_from", - "half_damage_from", - "double_damage_from", - ], - "properties": { - "no_damage_to": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["flying"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/3/"], - }, - }, - }, - }, - "half_damage_to": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bug"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/7/"], - }, - }, - }, - }, - "double_damage_to": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["poison"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/4/"], - }, - }, - }, - }, - "no_damage_from": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["electric"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/13/"], - }, - }, - }, - }, - "half_damage_from": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["poison"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/4/"], - }, - }, - }, - }, - "double_damage_from": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["water"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/11/"], - }, - }, - }, - }, - }, - } - ) - def get_type_relationships(self, obj): - relations = OrderedDict() - relations["no_damage_to"] = [] - relations["half_damage_to"] = [] - relations["double_damage_to"] = [] + @extend_schema_field(TypeSpriteSerializer) + def get_type_sprites(self, obj: Type) -> dict[str, str | None]: + sprites_object = TypeSprites.objects.filter(type=obj).first() + return sprites_object.sprites if sprites_object else {} - relations["no_damage_from"] = [] - relations["half_damage_from"] = [] - relations["double_damage_from"] = [] + def add_type_entry( + self, relations: dict[str, list[Any]], type_obj: Type, damage_factor: int, direction: str = "_damage_to" + ) -> None: + """ + Add an entry for the given type with the given damage factor in the given direction to the set of relations. + """ + if prefix := self.FACTOR_PREFIX_MAP.get(damage_factor): + type_data = cast("ReturnDict[str, Any]", TypeSummarySerializer(type_obj, context=self.context).data) + relations[f"{prefix}{direction}"].append(type_data) - # Damage To - results = TypeEfficacy.objects.filter(damage_type=obj) - serializer = TypeEfficacySerializer(results, many=True, context=self.context) + @extend_schema_field(TypeRelationshipsSerializer) + def get_type_relationships(self, obj: Type) -> dict[str, list[dict[str, Any]]]: + relations: dict[str, list[dict[str, Any]]] = {key: [] for key in self.RELATION_KEYS} - for relation in serializer.data: - type = Type.objects.get(pk=relation["target_type"]) - damage_factor = relation["damage_factor"] - self.add_type_entry(relations, type, damage_factor, direction="_damage_to") + # Damage To + damage_to_efficacy = TypeEfficacy.objects.filter(damage_type=obj).select_related("target_type") + for efficacy in damage_to_efficacy: + if efficacy.target_type: + self.add_type_entry(relations, efficacy.target_type, efficacy.damage_factor, direction="_damage_to") # Damage From - results = TypeEfficacy.objects.filter(target_type=obj) - serializer = TypeEfficacySerializer(results, many=True, context=self.context) - - for relation in serializer.data: - type = Type.objects.get(pk=relation["damage_type"]) - damage_factor = relation["damage_factor"] - self.add_type_entry(relations, type, damage_factor, direction="_damage_from") + damage_from_efficacy = TypeEfficacy.objects.filter(target_type=obj).select_related("damage_type") + for efficacy in damage_from_efficacy: + if efficacy.damage_type: + self.add_type_entry(relations, efficacy.damage_type, efficacy.damage_factor, direction="_damage_from") return relations - # takes a list of past type relations by generation and - # returns a list of lists where each list has the entries - # for a single generation - def group_relations_by_generation(self, serializer_data): - data_by_gen = [] - - current_generation = "" - generation_data = [] - for relation in serializer_data: - gen_name = relation["generation"]["name"] - if gen_name != current_generation: - # first item for this generation so create its list - current_generation = gen_name - generation_data = [relation] - data_by_gen.append(generation_data) - else: - # add to this generation's list - generation_data.append(relation) - - return data_by_gen - - # removes the entry for the given type in - # the given direction from the set of relations - def remove_type_entry(self, relations, type, direction="_damage_to"): - for k in ["double", "half", "no"]: - rel_list = relations[k + direction] - for i, o in enumerate(rel_list): - if o["name"] == type.name: + def remove_type_entry(self, relations: dict[str, list[Any]], type_obj: Type, direction: str = "_damage_to") -> None: + """ + Remove the entry for the given type in the given direction from the set of relations. + """ + for prefix in ("double", "half", "no"): + rel_list = relations[f"{prefix}{direction}"] + for i, item in enumerate(rel_list): + if item["name"] == type_obj.name: del rel_list[i] return - # returns past type relationships for the given type object - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["generation", "damage_relations"], - "properties": { - "generation": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["generation-v"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/generation/5/"], - }, - }, - }, - "damage_relations": { - "type": "object", - "required": [ - "no_damage_to", - "half_damage_to", - "double_damage_to", - "no_damage_from", - "half_damage_from", - "double_damage_from", - ], - "properties": { - "no_damage_to": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["flying"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/3/"], - }, - }, - }, - }, - "half_damage_to": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bug"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/7/"], - }, - }, - }, - }, - "double_damage_to": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["poison"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/4/"], - }, - }, - }, - }, - "no_damage_from": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["electric"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/13/"], - }, - }, - }, - }, - "half_damage_from": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["poison"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/4/"], - }, - }, - }, - }, - "double_damage_from": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["water"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/11/"], - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_type_past_relationships(self, obj): + @extend_schema_field(TypePastRelationshipsSerializer(many=True)) + def get_type_past_relationships(self, obj: Type) -> list[dict[str, Any]]: + """Returns a list of past type relationships for the given type object, grouped by generation.""" # collect data from DB - damage_type_results = list(TypeEfficacyPast.objects.filter(damage_type=obj)) - target_type_results = list(TypeEfficacyPast.objects.filter(target_type=obj)) - serializer = TypeEfficacyPastSerializer( - damage_type_results + target_type_results, many=True, context=self.context + all_past_efficacy = list( + TypeEfficacyPast.objects.filter(Q(damage_type=obj) | Q(target_type=obj)).select_related( + "generation", "target_type", "damage_type" + ) + ) + if not all_past_efficacy: + return [] + + serializer_data = cast( + "ReturnList[ReturnDict[str, Any]]", + TypeEfficacyPastSerializer(all_past_efficacy, many=True, context=self.context).data, ) # group data by generation - data_by_gen = self.group_relations_by_generation(serializer.data) + data_by_gen = [ + list(group) for _, group in itertools.groupby(serializer_data, key=lambda r: r["generation"]["name"]) + ] + all_types = Type.objects.select_related("generation").all() + type_cache = {t.pk: t for t in all_types} + name_to_gen_pk = {t.name: t.generation.pk if t.generation else 0 for t in all_types} # process each generation's data in turn - final_data = [] - past_relations = {} + final_data: list[dict[str, Any]] = [] for gen_data in data_by_gen: + current_gen_name = gen_data[0]["generation"]["name"] + current_gen = Generation.objects.filter(name=current_gen_name).first() # create past relations object for this generation - past_relations = OrderedDict() - - # set generation - past_relations["generation"] = gen_data[0]["generation"] - - # use current damage relations object - past_relations["damage_relations"] = self.get_type_relationships(obj) + past_relations: dict[str, Any] = { + "generation": gen_data[0]["generation"], + "damage_relations": self.get_type_relationships(obj), + } relations = past_relations["damage_relations"] - current_gen = Generation.objects.get(name=gen_data[0]["generation"]["name"]) - # remove types not yet introduced # e.g. Poison has no effect on Steel, but Steel was not present in generation I # so it should be absent from the list - relations["no_damage_to"] = self.remove_newer_types(relations["no_damage_to"], current_gen) - relations["half_damage_to"] = self.remove_newer_types(relations["half_damage_to"], current_gen) - relations["double_damage_to"] = self.remove_newer_types(relations["double_damage_to"], current_gen) - relations["no_damage_from"] = self.remove_newer_types(relations["no_damage_from"], current_gen) - relations["half_damage_from"] = self.remove_newer_types(relations["half_damage_from"], current_gen) - relations["double_damage_from"] = self.remove_newer_types(relations["double_damage_from"], current_gen) + if current_gen: + for key in self.RELATION_KEYS: + relations[key] = [ + item for item in relations[key] if name_to_gen_pk.get(item["name"], 0) <= current_gen.pk + ] # populate offensive relations - results = list(filter(lambda x: x["damage_type"] == obj.id, gen_data)) - for relation in results: - type = Type.objects.get(pk=relation["target_type"]) - - # remove conflicting entry if it exists - self.remove_type_entry(relations, type, direction="_damage_to") - - # add entry - damage_factor = relation["damage_factor"] - self.add_type_entry(relations, type, damage_factor, direction="_damage_to") - - del relation["generation"] - + for relation in (r for r in gen_data if r["damage_type"] == obj.pk): + if target_type_obj := type_cache.get(relation["target_type"]): + self.remove_type_entry(relations, target_type_obj, direction="_damage_to") + self.add_type_entry(relations, target_type_obj, relation["damage_factor"], direction="_damage_to") # populate defensive relations - results = list(filter(lambda x: x["target_type"] == obj.id, gen_data)) - for relation in results: - type = Type.objects.get(pk=relation["damage_type"]) - - # remove conflicting entry if it exists - self.remove_type_entry(relations, type, direction="_damage_from") - - # add entry - damage_factor = relation["damage_factor"] - self.add_type_entry(relations, type, damage_factor, direction="_damage_from") - - del relation["generation"] + for relation in (r for r in gen_data if r["target_type"] == obj.pk): + if damage_type_obj := type_cache.get(relation["damage_type"]): + self.remove_type_entry(relations, damage_type_obj, direction="_damage_from") + self.add_type_entry(relations, damage_type_obj, relation["damage_factor"], direction="_damage_from") - # add to final list final_data.append(past_relations) return final_data - def remove_newer_types(self, relations, current_gen): - return list(filter(lambda x: self.type_is_present(x, current_gen), relations)) - - def type_is_present(self, type, current_gen): - type_obj = Type.objects.get(name=type["name"]) - gen_introduced = Generation.objects.get(pk=type_obj.generation.id) - return gen_introduced.id <= current_gen.id - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["potency", "flavor"], - "properties": { - "slot": {"type": "integer", "examples": [1]}, - "pokemon": { - "type": "object", - "require": ["name", "url"], - "properties": { - "name": { - "type": "string", - "description": "The name of the pokemon", - "examples": ["sandshrew"], - }, - "url": { - "type": "string", - "format": "uri", - "description": "The URL to get more information about the pokemon", - "examples": ["https://pokeapi.co/api/v2/pokemon/27/"], - }, - }, - }, - }, - }, - } - ) - def get_type_pokemon(self, obj): - poke_type_objects = PokemonType.objects.filter(type=obj) - poke_types = PokemonTypeSerializer(poke_type_objects, many=True, context=self.context).data - - for poke_type in poke_types: - del poke_type["type"] - - return poke_types + @extend_schema_field(TypePokemonSerializer(many=True)) + def get_type_pokemon(self, obj: Type) -> ReturnList[ReturnDict[str, Any]]: + poke_type_objects = PokemonType.objects.filter(type=obj).select_related("pokemon") + return cast( + "ReturnList[ReturnDict[str, Any]]", + TypePokemonSerializer(poke_type_objects, many=True, context=self.context).data, + ) ######################### # MACHINE SERIALIZERS # ######################### -class MachineDetailSerializer(serializers.ModelSerializer): + + +class MachineDetailSerializer(serializers.ModelSerializer[Machine]): item = ItemSummarySerializer() version_group = VersionGroupSummarySerializer() move = MoveSummarySerializer() @@ -2890,7 +2047,9 @@ class Meta: ################################### # MOVE BATTLE STYLE SERIALIZERS # ################################### -class MoveBattleStyleNameSerializer(serializers.ModelSerializer): + + +class MoveBattleStyleNameSerializer(serializers.ModelSerializer[MoveBattleStyleName]): language = LanguageSummarySerializer() class Meta: @@ -2898,7 +2057,7 @@ class Meta: fields = ("name", "language") -class MoveBattleStyleDetailSerializer(serializers.ModelSerializer): +class MoveBattleStyleDetailSerializer(serializers.ModelSerializer[MoveBattleStyle]): names = MoveBattleStyleNameSerializer(many=True, read_only=True, source="movebattlestylename") class Meta: @@ -2909,7 +2068,9 @@ class Meta: ################################### # MOVE DAMAGE CLASS SERIALIZERS # ################################### -class MoveDamageClassNameSerializer(serializers.ModelSerializer): + + +class MoveDamageClassNameSerializer(serializers.ModelSerializer[MoveDamageClassName]): language = LanguageSummarySerializer() class Meta: @@ -2917,7 +2078,7 @@ class Meta: fields = ("name", "language") -class MoveDamageClassDescriptionSerializer(serializers.ModelSerializer): +class MoveDamageClassDescriptionSerializer(serializers.ModelSerializer[MoveDamageClassDescription]): language = LanguageSummarySerializer() class Meta: @@ -2925,7 +2086,7 @@ class Meta: fields = ("description", "language") -class MoveDamageClassDetailSerializer(serializers.ModelSerializer): +class MoveDamageClassDetailSerializer(serializers.ModelSerializer[MoveDamageClass]): names = MoveDamageClassNameSerializer(many=True, read_only=True, source="movedamageclassname") descriptions = MoveDamageClassDescriptionSerializer(many=True, read_only=True, source="movedamageclassdescription") moves = MoveSummarySerializer(many=True, read_only=True, source="move") @@ -2944,7 +2105,9 @@ class Meta: ########################### # MOVE META SERIALIZERS # ########################### -class MoveMetaAilmentNameSerializer(serializers.ModelSerializer): + + +class MoveMetaAilmentNameSerializer(serializers.ModelSerializer[MoveMetaAilmentName]): language = LanguageSummarySerializer() class Meta: @@ -2952,7 +2115,7 @@ class Meta: fields = ("name", "language") -class MoveMetaAilmentDetailSerializer(serializers.ModelSerializer): +class MoveMetaAilmentDetailSerializer(serializers.ModelSerializer[MoveMetaAilment]): names = MoveMetaAilmentNameSerializer(many=True, read_only=True, source="movemetaailmentname") moves = serializers.SerializerMethodField("get_ailment_moves") @@ -2960,36 +2123,16 @@ class Meta: model = MoveMetaAilment fields = ("id", "name", "moves", "names") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["thunder-punch"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/9/"], - }, - }, - }, - } - ) - def get_ailment_moves(self, obj): - move_meta_objects = MoveMeta.objects.filter(move_meta_ailment=obj) - moves = [] - - for meta in move_meta_objects: - move_obj = Move.objects.get(pk=meta.move.id) - data = MoveSummarySerializer(move_obj, context=self.context).data - moves.append(data) - - return moves + @extend_schema_field(MoveSummarySerializer(many=True)) + def get_ailment_moves(self, obj: MoveMetaAilment) -> ReturnList[ReturnDict[str, Any]]: + moves = Move.objects.filter(movemeta__move_meta_ailment=obj) + return cast( + "ReturnList[ReturnDict[str, Any]]", + MoveSummarySerializer(moves, many=True, context=self.context).data, + ) -class MoveMetaCategoryDescriptionSerializer(serializers.ModelSerializer): +class MoveMetaCategoryDescriptionSerializer(serializers.ModelSerializer[MoveMetaCategoryDescription]): language = LanguageSummarySerializer() class Meta: @@ -2997,7 +2140,7 @@ class Meta: fields = ("description", "language") -class MoveMetaCategoryDetailSerializer(serializers.ModelSerializer): +class MoveMetaCategoryDetailSerializer(serializers.ModelSerializer[MoveMetaCategory]): descriptions = MoveMetaCategoryDescriptionSerializer( many=True, read_only=True, source="movemetacategorydescription" ) @@ -3007,36 +2150,16 @@ class Meta: model = MoveMetaCategory fields = ("id", "name", "descriptions", "moves") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["sing"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/47/"], - }, - }, - }, - } - ) - def get_category_moves(self, obj): - move_meta_objects = MoveMeta.objects.filter(move_meta_category=obj) - moves = [] - - for meta in move_meta_objects: - move_obj = Move.objects.get(pk=meta.move.id) - data = MoveSummarySerializer(move_obj, context=self.context).data - moves.append(data) - - return moves + @extend_schema_field(MoveSummarySerializer(many=True)) + def get_category_moves(self, obj: MoveMetaCategory) -> ReturnList[ReturnDict[str, Any]]: + moves = Move.objects.filter(movemeta__move_meta_category=obj) + return cast( + "ReturnList[ReturnDict[str, Any]]", + MoveSummarySerializer(moves, many=True, context=self.context).data, + ) -class MoveMetaSerializer(serializers.ModelSerializer): +class MoveMetaSerializer(serializers.ModelSerializer[MoveMeta]): ailment = MoveMetaAilmentSummarySerializer(source="move_meta_ailment") category = MoveMetaCategorySummarySerializer(source="move_meta_category") @@ -3061,7 +2184,9 @@ class Meta: ############################# # MOVE TARGET SERIALIZERS # ############################# -class MoveTargetNameSerializer(serializers.ModelSerializer): + + +class MoveTargetNameSerializer(serializers.ModelSerializer[MoveTargetName]): language = LanguageSummarySerializer() class Meta: @@ -3069,7 +2194,7 @@ class Meta: fields = ("name", "language") -class MoveTargetDescriptionSerializer(serializers.ModelSerializer): +class MoveTargetDescriptionSerializer(serializers.ModelSerializer[MoveTargetDescription]): language = LanguageSummarySerializer() class Meta: @@ -3077,7 +2202,7 @@ class Meta: fields = ("description", "language") -class MoveTargetDetailSerializer(serializers.ModelSerializer): +class MoveTargetDetailSerializer(serializers.ModelSerializer[MoveTarget]): names = MoveTargetNameSerializer(many=True, read_only=True, source="movetargetname") descriptions = MoveTargetDescriptionSerializer(many=True, read_only=True, source="movetargetdescription") moves = MoveSummarySerializer(many=True, read_only=True, source="move") @@ -3090,15 +2215,25 @@ class Meta: ###################### # MOVE SERIALIZERS # ###################### -class MoveNameSerializer(serializers.ModelSerializer): + + +class MoveNameSerializer(serializers.ModelSerializer[MoveName]): language = LanguageSummarySerializer() class Meta: - model = AbilityName + model = MoveName fields = ("name", "language") -class MoveChangeSerializer(serializers.ModelSerializer): +class MoveEffectEffectTextSerializer(serializers.ModelSerializer[MoveEffectEffectText]): + language = LanguageSummarySerializer() + + class Meta: + model = MoveEffectEffectText + fields = ("effect", "short_effect", "language") + + +class MoveChangeSerializer(serializers.ModelSerializer[MoveChange]): version_group = VersionGroupSummarySerializer() type = TypeSummarySerializer() effect_entries = serializers.SerializerMethodField("get_effects") @@ -3116,53 +2251,16 @@ class Meta: "version_group", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["effect", "short_effect", "language"], - "properties": { - "effect": { - "type": "string", - "examples": ["Inflicts regular damage."], - }, - "short_effect": { - "type": "string", - "examples": ["Inflicts regular damage with no additional effect."], - }, - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - }, - }, - } - ) - def get_effects(self, obj): - effect_texts = MoveEffectEffectText.objects.filter(move_effect=obj.move_effect) - data = MoveEffectEffectTextSerializer(effect_texts, many=True, context=self.context).data - - return data - - -class MoveEffectEffectTextSerializer(serializers.ModelSerializer): - language = LanguageSummarySerializer() - - class Meta: - model = MoveEffectEffectText - fields = ("effect", "short_effect", "language") + @extend_schema_field(MoveEffectEffectTextSerializer(many=True)) + def get_effects(self, obj: MoveChange) -> ReturnList[ReturnDict[str, Any]]: + effect_texts = MoveEffectEffectText.objects.filter(move_effect=obj.move_effect).select_related("language") + return cast( + "ReturnList[ReturnDict[str, Any]]", + MoveEffectEffectTextSerializer(effect_texts, many=True, context=self.context).data, + ) -class MoveEffectChangeEffectTextSerializer(serializers.ModelSerializer): +class MoveEffectChangeEffectTextSerializer(serializers.ModelSerializer[MoveEffectChangeEffectText]): language = LanguageSummarySerializer() class Meta: @@ -3170,7 +2268,7 @@ class Meta: fields = ("effect", "language") -class MoveEffectChangeSerializer(serializers.ModelSerializer): +class MoveEffectChangeSerializer(serializers.ModelSerializer[MoveEffectChange]): version_group = VersionGroupSummarySerializer() effect_entries = MoveEffectChangeEffectTextSerializer( many=True, read_only=True, source="moveeffectchangeeffecttext" @@ -3181,7 +2279,7 @@ class Meta: fields = ("version_group", "effect_entries") -class MoveFlavorTextSerializer(serializers.ModelSerializer): +class MoveFlavorTextSerializer(serializers.ModelSerializer[MoveFlavorText]): flavor_text = serializers.CharField() language = LanguageSummarySerializer() version_group = VersionGroupSummarySerializer() @@ -3191,7 +2289,25 @@ class Meta: fields = ("flavor_text", "language", "version_group") -class MoveDetailSerializer(serializers.ModelSerializer): +class MoveComboUsageSerializer(serializers.Serializer[Any]): + use_before = MoveSummarySerializer(many=True, allow_null=True) + use_after = MoveSummarySerializer(many=True, allow_null=True) + + +class MoveCombosSerializer(serializers.Serializer[Any]): + normal = MoveComboUsageSerializer() + super = MoveComboUsageSerializer() + + +class MoveMetaStatChangeSerializer(serializers.ModelSerializer[MoveMetaStatChange]): + stat = StatSummarySerializer() + + class Meta: + model = MoveMetaStatChange + fields = ("change", "stat") + + +class MoveDetailSerializer(serializers.ModelSerializer[Move]): generation = GenerationSummarySerializer() type = TypeSummarySerializer() target = MoveTargetSummarySerializer(source="move_target") @@ -3240,353 +2356,90 @@ class Meta: "learned_by_pokemon", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["clefairy"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon/35/"], - }, - }, - }, - } - ) - def get_learned_by_pokemon(self, obj): - pokemon_moves = PokemonMove.objects.filter(move_id=obj).order_by("pokemon_id") - - pokemon_list = [] - - pokemon_ids = pokemon_moves.values("pokemon_id").distinct() - - for id in pokemon_ids: - pokemon_object = Pokemon.objects.get(pk=id["pokemon_id"]) - pokemon_data = PokemonSummarySerializer(pokemon_object, context=self.context).data - - pokemon_list.append(pokemon_data) - - return pokemon_list - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["machine", "version_group"], - "properties": { - "machine": { - "type": "object", - "required": ["url"], - "properties": { - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/machine/1/"], - } - }, - }, - "version_group": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["sword-shield"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version-group/1/"], - }, - }, - }, - }, - }, - } - ) - def get_move_machines(self, obj): - machine_objects = Machine.objects.filter(move=obj) - - machines = [] - - for machine_object in machine_objects: - machine_data = MachineSummarySerializer(machine_object, context=self.context).data - - version_group_data = VersionGroupSummarySerializer(machine_object.version_group, context=self.context).data - - machines.append({"machine": machine_data, "version_group": version_group_data}) - - return machines - - @extend_schema_field( - field={ - "type": "object", - "required": ["normal", "super"], - "properties": { - "normal": { - "type": "object", - "required": ["use_before", "use_after"], - "properties": { - "use_before": { - "type": "array", - "nullable": True, - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["fire-punch"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/7/"], - }, - }, - }, - }, - "use_after": { - "type": "array", - "nullable": True, - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["ice-punch"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/8/"], - }, - }, - }, - }, - }, - }, - "super": { - "type": "object", - "required": ["use_before", "use_after"], - "properties": { - "use_before": { - "type": "array", - "nullable": True, - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["night-slash"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/400/"], - }, - }, - }, - }, - "use_after": { - "type": "array", - "nullable": True, - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["focus-energy"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/116/"], - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_combos(self, obj): - normal_before_objects = ContestCombo.objects.filter(first_move=obj) - normal_before_data = ContestComboSerializer(normal_before_objects, many=True, context=self.context).data - normal_after_objects = ContestCombo.objects.filter(second_move=obj) - normal_after_data = ContestComboSerializer(normal_after_objects, many=True, context=self.context).data - - super_before_objects = SuperContestCombo.objects.filter(first_move=obj) - super_before_data = SuperContestComboSerializer(super_before_objects, many=True, context=self.context).data - super_after_objects = SuperContestCombo.objects.filter(second_move=obj) - super_after_data = SuperContestComboSerializer(super_after_objects, many=True, context=self.context).data - - details = None - - if normal_before_data or normal_after_data or super_before_data or super_after_data: - details = OrderedDict() - details["normal"] = OrderedDict() - details["normal"]["use_before"] = None - details["normal"]["use_after"] = None - details["super"] = OrderedDict() - details["super"]["use_before"] = None - details["super"]["use_after"] = None - - for combo in normal_before_data: - if details["normal"]["use_before"] is None: - details["normal"]["use_before"] = [] - details["normal"]["use_before"].append(combo["second_move"]) - - for combo in normal_after_data: - if details["normal"]["use_after"] is None: - details["normal"]["use_after"] = [] - details["normal"]["use_after"].append(combo["first_move"]) - - for combo in super_before_data: - if details["super"]["use_before"] is None: - details["super"]["use_before"] = [] - details["super"]["use_before"].append(combo["second_move"]) - - for combo in super_after_data: - if details["super"]["use_after"] is None: - details["super"]["use_after"] = [] - details["super"]["use_after"].append(combo["first_move"]) - - return details - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["effect", "short_effect", "language"], - "properties": { - "effect": { - "type": "string", - "examples": ["Inflicts regular damage."], - }, - "short_effect": { - "type": "string", - "examples": ["Inflicts regular damage with no additional effect."], - }, - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - }, - }, - } - ) - def get_effect_text(self, obj): - effect_texts = MoveEffectEffectText.objects.filter(move_effect=obj.move_effect) - data = MoveEffectEffectTextSerializer(effect_texts, many=True, context=self.context).data - if len(data) > 0: - for key, value in data[0].items(): - if "$effect_chance%" in value: - data[0][key] = value.replace("$effect_chance", f"{obj.move_effect_chance}") - - return data + @extend_schema_field(PokemonSummarySerializer(many=True)) + def get_learned_by_pokemon(self, obj: Move) -> ReturnList[ReturnDict[str, Any]]: + pokemon = Pokemon.objects.filter(pokemonmove__move=obj).distinct().order_by("id") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSummarySerializer(pokemon, many=True, context=self.context).data, + ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["effect_entries", "version_group"], - "properties": { - "effect_entries": { - "type": "array", - "items": { - "type": "object", - "required": ["effect", "language"], - "properties": { - "effect": { - "type": "string", - "examples": ["Hits Pokémon under the effects of dig and fly."], - }, - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - }, - }, - }, - "version_group": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["gold-silver"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version-group/3/"], - }, - }, - }, - }, - }, - } - ) - def get_effect_change_text(self, obj): - effect_changes = MoveEffectChange.objects.filter(move_effect=obj.move_effect) - data = MoveEffectChangeSerializer(effect_changes, many=True, context=self.context).data + @extend_schema_field(ItemMachineSerializer(many=True)) + def get_move_machines(self, obj: Move) -> ReturnList[ReturnDict[str, Any]]: + machine_objects = Machine.objects.filter(move=obj).select_related("version_group") + return cast( + "ReturnList[ReturnDict[str, Any]]", + ItemMachineSerializer(machine_objects, many=True, context=self.context).data, + ) - return data + @extend_schema_field(MoveCombosSerializer(allow_null=True)) + def get_combos(self, obj: Move) -> dict[str, Any] | None: + normal_before = [ + c.second_move + for c in ContestCombo.objects.filter(first_move=obj).select_related("second_move") + if c.second_move + ] + normal_after = [ + c.first_move + for c in ContestCombo.objects.filter(second_move=obj).select_related("first_move") + if c.first_move + ] + super_before = [ + c.second_move + for c in SuperContestCombo.objects.filter(first_move=obj).select_related("second_move") + if c.second_move + ] + super_after = [ + c.first_move + for c in SuperContestCombo.objects.filter(second_move=obj).select_related("first_move") + if c.first_move + ] + + if not (normal_before or normal_after or super_before or super_after): + return None + + def serialize_list(moves: list[Any]) -> list[dict[str, Any]] | None: + if not moves: + return None + return cast( + "list[dict[str, Any]]", + MoveSummarySerializer(moves, many=True, context=self.context).data, + ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["change", "stat"], - "properties": { - "change": {"type": "integer", "format": "int32", "examples": [2]}, - "stat": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["attack"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/stat/1/"], - }, - }, - }, - }, + return { + "normal": { + "use_before": serialize_list(normal_before), + "use_after": serialize_list(normal_after), + }, + "super": { + "use_before": serialize_list(super_before), + "use_after": serialize_list(super_after), }, } - ) - def get_move_stat_change(self, obj): - stat_change_objects = MoveMetaStatChange.objects.filter(move=obj) - stat_changes = MoveMetaStatChangeSerializer(stat_change_objects, many=True, context=self.context).data - for change in stat_changes: - del change["move"] + @extend_schema_field(MoveEffectEffectTextSerializer(many=True)) + def get_effect_text(self, obj: Move) -> ReturnList[ReturnDict[str, Any]]: + effect_texts = MoveEffectEffectText.objects.filter(move_effect=obj.move_effect).select_related("language") + return cast( + "ReturnList[ReturnDict[str, Any]]", + MoveEffectEffectTextSerializer(effect_texts, many=True, context=self.context).data, + ) + + @extend_schema_field(MoveEffectChangeSerializer(many=True)) + def get_effect_change_text(self, obj: Move) -> ReturnList[ReturnDict[str, Any]]: + effect_changes = MoveEffectChange.objects.filter(move_effect=obj.move_effect).select_related("version_group") + return cast( + "ReturnList[ReturnDict[str, Any]]", + MoveEffectChangeSerializer(effect_changes, many=True, context=self.context).data, + ) - return stat_changes + @extend_schema_field(MoveMetaStatChangeSerializer(many=True)) + def get_move_stat_change(self, obj: Move) -> ReturnList[ReturnDict[str, Any]]: + stat_changes = MoveMetaStatChange.objects.filter(move=obj).select_related("stat") + return cast( + "ReturnList[ReturnDict[str, Any]]", + MoveMetaStatChangeSerializer(stat_changes, many=True, context=self.context).data, + ) ########################## @@ -3594,24 +2447,23 @@ def get_move_stat_change(self, obj): ########################## -class PalParkSerializer(serializers.ModelSerializer): - area = PalParkAreaSummarySerializer(read_only=True, source="pal_park_area") - pokemon_species = PokemonSpeciesSummarySerializer() +class PalParkAreaNameSerializer(serializers.HyperlinkedModelSerializer[PalParkAreaName]): + language = LanguageSummarySerializer() class Meta: - model = PalPark - fields = ("base_score", "rate", "area", "pokemon_species") + model = PalParkAreaName + fields = ("name", "language") -class PalParkAreaNameSerializer(serializers.HyperlinkedModelSerializer): - language = LanguageSummarySerializer() +class PalParkEncounterSerializer(serializers.ModelSerializer[PalPark]): + pokemon_species = PokemonSpeciesSummarySerializer() class Meta: - model = PalParkAreaName - fields = ("name", "language") + model = PalPark + fields = ("base_score", "rate", "pokemon_species") -class PalParkAreaDetailSerializer(serializers.ModelSerializer): +class PalParkAreaDetailSerializer(serializers.ModelSerializer[PalParkArea]): names = PalParkAreaNameSerializer(many=True, read_only=True, source="palparkareaname") pokemon_encounters = serializers.SerializerMethodField("get_encounters") @@ -3619,45 +2471,14 @@ class Meta: model = PalParkArea fields = ("id", "name", "names", "pokemon_encounters") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["base_score", "pokemon-species", "rate"], - "properties": { - "base_score": { - "type": "integer", - "format": "int32", - "examples": [50], - }, - "pokemon-species": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bulbasaur"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/1/"], - }, - }, - }, - "rate": {"type": "integer", "format": "int32", "examples": [30]}, - }, - }, - } - ) - def get_encounters(self, obj): - pal_park_objects = PalPark.objects.filter(pal_park_area=obj) - parks = PalParkSerializer(pal_park_objects, many=True, context=self.context).data - encounters = [] - - for encounter in parks: - del encounter["area"] - encounters.append(encounter) + @extend_schema_field(PalParkEncounterSerializer(many=True)) + def get_encounters(self, obj: PalParkArea) -> ReturnList[ReturnDict[str, Any]]: + pal_park_objects = PalPark.objects.filter(pal_park_area=obj).select_related("pokemon_species") - return encounters + return cast( + "ReturnList[ReturnDict[str, Any]]", + PalParkEncounterSerializer(pal_park_objects, many=True, context=self.context).data, + ) ############################### @@ -3665,7 +2486,7 @@ def get_encounters(self, obj): ############################### -class PokemonColorNameSerializer(serializers.HyperlinkedModelSerializer): +class PokemonColorNameSerializer(serializers.HyperlinkedModelSerializer[PokemonColorName]): language = LanguageSummarySerializer() class Meta: @@ -3673,7 +2494,7 @@ class Meta: fields = ("name", "language") -class PokemonColorDetailSerializer(serializers.ModelSerializer): +class PokemonColorDetailSerializer(serializers.ModelSerializer[PokemonColor]): names = PokemonColorNameSerializer(many=True, read_only=True, source="pokemoncolorname") pokemon_species = PokemonSpeciesSummarySerializer(many=True, read_only=True, source="pokemonspecies") @@ -3685,21 +2506,15 @@ class Meta: ############################## # POKEMON FORM SERIALIZERS # ############################## -class PokemonFormSpritesSerializer(serializers.ModelSerializer): - class Meta: - model = PokemonFormSprites - fields = ("sprites",) -class PokemonFormNameSerializer(serializers.ModelSerializer): - language = LanguageSummarySerializer() - +class PokemonFormSpritesSerializer(serializers.ModelSerializer[PokemonFormSprites]): class Meta: - model = PokemonFormName - fields = ("name", "pokemon_name", "language") + model = PokemonFormSprites + fields = ("sprites",) -class PokemonFormConditionSerializer(serializers.ModelSerializer): +class PokemonFormConditionSerializer(serializers.ModelSerializer[PokemonFormCondition]): trigger = serializers.CharField(source="form_trigger.name", read_only=True) item = ItemSummarySerializer() ability = AbilitySummarySerializer() @@ -3711,7 +2526,52 @@ class Meta: fields = ("trigger", "item", "ability", "move", "base_form") -class PokemonFormDetailSerializer(serializers.ModelSerializer): +class PokemonFormNameSerializer(serializers.ModelSerializer[PokemonFormName]): + language = LanguageSummarySerializer() + + class Meta: + model = PokemonFormName + fields = ("name", "language") + + +class PokemonFormTypeSerializer(serializers.ModelSerializer[PokemonFormType]): + type = TypeSummarySerializer() + + class Meta: + model = PokemonFormType + fields = ("slot", "type") + + +class PokemonTypeSerializer(serializers.ModelSerializer[PokemonType]): + type = TypeSummarySerializer() + + class Meta: + model = PokemonType + fields = ("slot", "type") + + +class PokemonFormTriggerConditionSerializer(serializers.Serializer[Any]): + trigger = serializers.CharField() + base_form = PokemonFormSummarySerializer() + + +class PokemonShapeNameSerializer(serializers.ModelSerializer[PokemonShapeName]): + language = LanguageSummarySerializer() + + class Meta: + model = PokemonShapeName + fields = ("name", "language") + + +class PokemonShapeAwesomeNameSerializer(serializers.ModelSerializer[PokemonShapeName]): + language = LanguageSummarySerializer() + + class Meta: + model = PokemonShapeName + fields = ("awesome_name", "language") + + +class PokemonFormDetailSerializer(serializers.ModelSerializer[PokemonForm]): pokemon = PokemonSummarySerializer() version_group = VersionGroupSummarySerializer() sprites = serializers.SerializerMethodField("get_pokemon_form_sprites") @@ -3740,214 +2600,72 @@ class Meta: "trigger_conditions", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["language", "name"], - "properties": { - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - "name": {"type": "string", "examples": ["Plant Cloak"]}, - }, - }, - } - ) - def get_pokemon_form_names(self, obj): - form_results = PokemonFormName.objects.filter(pokemon_form=obj, name__regex=".+") - form_serializer = PokemonFormNameSerializer(form_results, many=True, context=self.context) + @extend_schema_field(PokemonFormNameSerializer(many=True)) + def get_pokemon_form_names(self, obj: PokemonForm) -> ReturnList[ReturnDict[str, Any]]: + form_results = PokemonFormName.objects.filter(pokemon_form=obj, name__regex=".+").select_related("language") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonFormNameSerializer(form_results, many=True, context=self.context).data, + ) - data = form_serializer.data - - for name in data: - del name["pokemon_name"] + @extend_schema_field(PokemonFormNameSerializer(many=True)) + def get_pokemon_form_pokemon_names(self, obj: PokemonForm) -> ReturnList[ReturnDict[str, Any]]: + form_results = PokemonFormName.objects.filter(pokemon_form=obj, pokemon_name__regex=".+").select_related( + "language" + ) + data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonFormNameSerializer(form_results, many=True, context=self.context).data, + ) + for item, fn in zip(data, form_results, strict=True): + item["name"] = fn.pokemon_name return data - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["language", "name"], - "properties": { - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - "name": {"type": "string", "examples": ["Plant Cloak"]}, - }, - }, - } - ) - def get_pokemon_form_pokemon_names(self, obj): - form_results = PokemonFormName.objects.filter(pokemon_form=obj, pokemon_name__regex=".+") - form_serializer = PokemonFormNameSerializer(form_results, many=True, context=self.context) + @extend_schema_field(PokemonFormSpritesSerializer) + def get_pokemon_form_sprites(self, obj: PokemonForm) -> dict[str, Any]: + sprites_object = PokemonFormSprites.objects.filter(pokemon_form=obj).first() + return sprites_object.sprites if sprites_object else {} - data = form_serializer.data + @extend_schema_field(PokemonFormTypeSerializer(many=True)) + def get_pokemon_form_types(self, obj: PokemonForm) -> ReturnList[ReturnDict[str, Any]]: + form_types = PokemonFormType.objects.filter(pokemon_form=obj).select_related("type").order_by("slot") - for name in data: - name["name"] = name["pokemon_name"] - del name["pokemon_name"] + if form_types: + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonFormTypeSerializer(form_types, many=True, context=self.context).data, + ) - return data + # Fall back to parent Pokemon's types if no form-specific types exist + pokemon_types = PokemonType.objects.filter(pokemon=obj.pokemon).select_related("type").order_by("slot") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonTypeSerializer(pokemon_types, many=True, context=self.context).data, + ) - @extend_schema_field( - field={ - "type": "object", - "properties": { - "default": { - "type": "string", - "format": "uri", - "examples": [ - "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/412.png" - ], - } - }, - "additionalProperties": { # Stoplight Elements doesn't render this well - "type": "string", - "format": "uri", - "nullable": True, - "examples": [ - "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/412.png" - ], - }, - "examples": [ - { - "back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/412.png", - "back_female": None, - "back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/412.png", - "back_shiny_female": None, - "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/412.png", - "front_female": None, - "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/412.png", - "front_shiny_female": None, - } - ], - } - ) - def get_pokemon_form_sprites(self, obj): - sprites_object = PokemonFormSprites.objects.get(pokemon_form_id=obj) - return sprites_object.sprites - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["slot", "type"], - "properties": { - "slot": {"type": "integer", "format": "int32", "examples": [1]}, - "type": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bug"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/7/"], - }, - }, - }, - }, - }, - } - ) - def get_pokemon_form_types(self, obj): - form_type_objects = PokemonFormType.objects.filter(pokemon_form=obj) - form_types = PokemonFormTypeSerializer(form_type_objects, many=True, context=self.context).data - - for form_type in form_types: - del form_type["pokemon_form"] - - # defer to parent Pokemon's types if no form-specific types - if form_types == []: - pokemon_object = Pokemon.objects.get(id=obj.pokemon_id) - pokemon_type_objects = PokemonType.objects.filter(pokemon=pokemon_object) - form_types = PokemonTypeSerializer(pokemon_type_objects, many=True, context=self.context).data - - for form_type in form_types: - del form_type["pokemon"] - - return form_types - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["trigger", "name", "url"], - "properties": { - "trigger": { - "type": "string", - "examples": ["held-item"], - }, - "name": { - "type": "string", - "examples": ["venusaurite"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/item/698/"], - }, - "base_form": { - "type": "object", - "nullable": True, - "properties": { - "name": { - "type": "string", - "examples": ["necrozma-dusk"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-form/10314/"], - }, - }, - }, - }, - }, - } - ) - def get_pokemon_form_triggers_conditions(self, obj): - conditions = PokemonFormCondition.objects.filter(pokemon_form=obj) - conditions_data = PokemonFormConditionSerializer(conditions, many=True, context=self.context).data + @extend_schema_field(PokemonFormTriggerConditionSerializer(many=True)) + def get_pokemon_form_triggers_conditions(self, obj: PokemonForm) -> list[dict[str, Any]]: + conditions = PokemonFormCondition.objects.filter(pokemon_form=obj).select_related( + "form_trigger", "item", "ability", "move", "base_form" + ) + conditions_data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonFormConditionSerializer(conditions, many=True, context=self.context).data, + ) - triggers = [] + triggers: list[dict[str, Any]] = [] for condition in conditions_data: - trigger_value = condition.pop("trigger", None) - if not trigger_value: - continue - base_form = condition.pop("base_form", None) - trigger = {"trigger": trigger_value} - for value in condition.values(): - if value: - trigger.update(value) - break - if base_form: - trigger["base_form"] = base_form - triggers.append(trigger) + if trigger_value := condition.get("trigger"): + trigger = {"trigger": trigger_value} + for key, value in condition.items(): + if key not in ("trigger", "base_form") and value: + trigger.update(value) + break + if base_form := condition.get("base_form"): + trigger["base_form"] = base_form + triggers.append(trigger) + return triggers @@ -3956,7 +2674,7 @@ def get_pokemon_form_triggers_conditions(self, obj): ################################# -class PokemonHabitatNameSerializer(serializers.HyperlinkedModelSerializer): +class PokemonHabitatNameSerializer(serializers.HyperlinkedModelSerializer[PokemonHabitatName]): language = LanguageSummarySerializer() class Meta: @@ -3964,7 +2682,7 @@ class Meta: fields = ("name", "language") -class PokemonHabitatDetailSerializer(serializers.ModelSerializer): +class PokemonHabitatDetailSerializer(serializers.ModelSerializer[PokemonHabitat]): names = PokemonHabitatNameSerializer(many=True, read_only=True, source="pokemonhabitatname") pokemon_species = PokemonSpeciesSummarySerializer(many=True, read_only=True, source="pokemonspecies") @@ -3978,7 +2696,7 @@ class Meta: ############################## -class MoveLearnMethodNameSerializer(serializers.HyperlinkedModelSerializer): +class MoveLearnMethodNameSerializer(serializers.HyperlinkedModelSerializer[MoveLearnMethodName]): language = LanguageSummarySerializer() class Meta: @@ -3986,7 +2704,7 @@ class Meta: fields = ("name", "language") -class MoveLearnMethodDescriptionSerializer(serializers.HyperlinkedModelSerializer): +class MoveLearnMethodDescriptionSerializer(serializers.HyperlinkedModelSerializer[MoveLearnMethodDescription]): language = LanguageSummarySerializer() class Meta: @@ -3994,7 +2712,7 @@ class Meta: fields = ("description", "language") -class MoveLearnMethodDetailSerializer(serializers.ModelSerializer): +class MoveLearnMethodDetailSerializer(serializers.ModelSerializer[MoveLearnMethod]): names = MoveLearnMethodNameSerializer(many=True, read_only=True, source="movelearnmethodname") descriptions = MoveLearnMethodDescriptionSerializer(many=True, read_only=True, source="movelearnmethoddescription") version_groups = serializers.SerializerMethodField("get_method_version_groups") @@ -4003,61 +2721,21 @@ class Meta: model = MoveLearnMethod fields = ("id", "name", "names", "descriptions", "version_groups") - # "version_groups": [ - # { - # "name": "red-blue", - # "url": "https://pokeapi.co/api/v2/version-group/1/" - # }, - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["red-blue"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version-group/1/"], - }, - }, - }, - } - ) - def get_method_version_groups(self, obj): - version_group_objects = VersionGroupMoveLearnMethod.objects.filter(move_learn_method=obj) - version_group_data = VersionGroupMoveLearnMethodSerializer( - version_group_objects, many=True, context=self.context - ).data - groups = [] - - for vg in version_group_data: - groups.append(vg["version_group"]) - - return groups - - -# https://stackoverflow.com/a/45987450/3482533 -class PokemonMoveSerializer(serializers.ModelSerializer): - class Meta: - model = PokemonMove - fields = "__all__" + @extend_schema_field(VersionGroupSummarySerializer(many=True)) + def get_method_version_groups(self, obj: MoveLearnMethod) -> ReturnList[ReturnDict[str, Any]]: + version_groups = VersionGroup.objects.filter(versiongroupmovelearnmethod__move_learn_method=obj).distinct() + return cast( + "ReturnList[ReturnDict[str, Any]]", + VersionGroupSummarySerializer(version_groups, many=True, context=self.context).data, + ) ############################### # POKEMON SHAPE SERIALIZERS # ############################### -class PokemonShapeNameSerializer(serializers.HyperlinkedModelSerializer): - language = LanguageSummarySerializer() - - class Meta: - model = PokemonShapeName - fields = ("name", "awesome_name", "language") -class PokemonShapeDetailSerializer(serializers.ModelSerializer): +class PokemonShapeDetailSerializer(serializers.ModelSerializer[PokemonShape]): names = serializers.SerializerMethodField("get_shape_names") awesome_names = serializers.SerializerMethodField("get_shape_awesome_names") pokemon_species = PokemonSpeciesSummarySerializer(many=True, read_only=True, source="pokemonspecies") @@ -4066,100 +2744,89 @@ class Meta: model = PokemonShape fields = ("id", "name", "awesome_names", "names", "pokemon_species") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["url", "name"], - "properties": { - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - "name": {"type": "string", "examples": ["Ball"]}, - }, - }, - } - ) - def get_shape_names(self, obj): - results = PokemonShapeName.objects.filter(pokemon_shape_id=obj) - serializer = PokemonShapeNameSerializer(results, many=True, context=self.context) - data = serializer.data - - for entry in data: - del entry["awesome_name"] - - return data + @extend_schema_field(PokemonShapeNameSerializer(many=True)) + def get_shape_names(self, obj: PokemonShape) -> ReturnList[ReturnDict[str, Any]]: + results = PokemonShapeName.objects.filter(pokemon_shape_id=obj).select_related("language") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonShapeNameSerializer(results, many=True, context=self.context).data, + ) - # "awesome_names": [ - # { - # "awesome_name": "Pomacé", - # "language": { - # "name": "fr", - # "url": "https://pokeapi.co/api/v2/language/5/" - # } - # }, - # { - # "awesome_name": "Pomaceous", - # "language": { - # "name": "en", - # "url": "https://pokeapi.co/api/v2/language/9/" - # } - # } - # ], - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["awesome_name", "language"], - "properties": { - "awesome_name": {"type": "string", "examples": ["Pomaceous"]}, - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - }, - }, - } - ) - def get_shape_awesome_names(self, obj): - results = PokemonShapeName.objects.filter(pokemon_shape_id=obj) - serializer = PokemonShapeNameSerializer(results, many=True, context=self.context) - data = serializer.data + @extend_schema_field(PokemonShapeAwesomeNameSerializer(many=True)) + def get_shape_awesome_names(self, obj: PokemonShape) -> ReturnList[ReturnDict[str, Any]]: + results = PokemonShapeName.objects.filter(pokemon_shape_id=obj).select_related("language") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonShapeAwesomeNameSerializer(results, many=True, context=self.context).data, + ) - for entry in data: - del entry["name"] - return data +############################## +# POKEMON ITEM SERIALIZERS # +############################## ############################## -# POKEMON ITEM SERIALIZERS # +# POKEMON STAT SERIALIZERS # ############################## -class PokemonItemSerializer(serializers.ModelSerializer): + + +######################### +# POKEMON SERIALIZERS # +######################### + + +class PokemonGameIndexSerializer(serializers.ModelSerializer[PokemonGameIndex]): version = VersionSummarySerializer() - item = ItemSummarySerializer() class Meta: - model = PokemonItem - fields = ("rarity", "item", "version") + model = PokemonGameIndex + fields = ("game_index", "version") -############################## -# POKEMON STAT SERIALIZERS # -############################## -class PokemonStatSerializer(serializers.ModelSerializer): +class PokemonAbilitySerializer(serializers.ModelSerializer[PokemonAbility]): + ability = AbilitySummarySerializer() + + class Meta: + model = PokemonAbility + fields = ("is_hidden", "slot", "ability") + + +class PokemonSpritesSerializer(serializers.Serializer[Any]): + front_default = serializers.CharField(allow_null=True) + + +class PokemonCriesSerializer(serializers.Serializer[Any]): + latest = serializers.CharField(allow_null=True) + legacy = serializers.CharField(allow_null=True) + + +class PokemonMoveVersionGroupSerializer(serializers.Serializer[Any]): + level_learned_at = serializers.IntegerField() + move_learn_method = MoveLearnMethodSummarySerializer() + version_group = VersionGroupSummarySerializer() + order = serializers.IntegerField(required=False) + + +class PokemonMoveSerializer(serializers.Serializer[Any]): + move = MoveSummarySerializer() + version_group_details = PokemonMoveVersionGroupSerializer(many=True) + + +class PokemonAbilityPastSerializer(serializers.ModelSerializer[PokemonAbilityPast]): + ability = AbilitySummarySerializer() + + class Meta: + model = PokemonAbilityPast + fields = ("is_hidden", "slot", "ability") + + +class PokemonPastAbilitySerializer(serializers.Serializer[Any]): + abilities = PokemonAbilityPastSerializer(many=True) + generation = GenerationSummarySerializer() + + +class PokemonStatSerializer(serializers.ModelSerializer[PokemonStat]): stat = StatSummarySerializer() class Meta: @@ -4167,29 +2834,62 @@ class Meta: fields = ("base_stat", "effort", "stat") -class PokemonStatPastSerializer(serializers.ModelSerializer): +class PokemonPastStatSerializer(serializers.Serializer[Any]): generation = GenerationSummarySerializer() - stat = StatSummarySerializer() + stats = PokemonStatSerializer(many=True) + + +class PokemonPastTypeSerializer(serializers.Serializer[Any]): + generation = GenerationSummarySerializer() + types = TypePokemonSerializer(many=True) + + +class PokemonSpeciesNameSerializer(serializers.ModelSerializer[PokemonSpeciesName]): + language = LanguageSummarySerializer() class Meta: - model = PokemonStatPast - fields = ("base_stat", "effort", "generation", "stat") + model = PokemonSpeciesName + fields = ("name", "language") -######################### -# POKEMON SERIALIZERS # -######################### +class PokemonSpeciesGenusSerializer(serializers.ModelSerializer[PokemonSpeciesName]): + language = LanguageSummarySerializer() + class Meta: + model = PokemonSpeciesName + fields = ("genus", "language") -class PokemonGameIndexSerializer(serializers.ModelSerializer): - version = VersionSummarySerializer() + +class PokemonSpeciesVarietySerializer(serializers.Serializer[Any]): + is_default = serializers.BooleanField() + pokemon = PokemonSummarySerializer() + + +class PokemonSpeciesPalParkEncounterSerializer(serializers.ModelSerializer[PalPark]): + area = PalParkAreaSummarySerializer(source="pal_park_area") class Meta: - model = PokemonGameIndex - fields = ("game_index", "version") + model = PalPark + fields = ("base_score", "rate", "area") -class PokemonDetailSerializer(serializers.ModelSerializer): +class PokemonStatPastSerializer(serializers.ModelSerializer[PokemonStatPast]): + stat = StatSummarySerializer() + + class Meta: + model = PokemonStatPast + fields = ("base_stat", "effort", "stat") + + +class PokemonTypePastSerializer(serializers.ModelSerializer[PokemonTypePast]): + type = TypeSummarySerializer() + + class Meta: + model = PokemonTypePast + fields = ("slot", "type") + + +class PokemonDetailSerializer(serializers.ModelSerializer[Pokemon]): abilities = serializers.SerializerMethodField("get_pokemon_abilities") past_abilities = serializers.SerializerMethodField("get_past_pokemon_abilities") game_indices = PokemonGameIndexSerializer(many=True, read_only=True, source="pokemongameindex") @@ -4231,944 +2931,205 @@ class Meta: "past_types", ) - @extend_schema_field( - field={ - "type": "object", - "properties": { - "front_default": { - "type": "string", - "format": "uri", - "exmaple": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/1.png", + @extend_schema_field(PokemonSpritesSerializer) + def get_pokemon_sprites(self, obj: Pokemon) -> dict[str, str | None]: + sprites_list = list(cast("PokemonWithRelations", obj).pokemonsprites.all()) + return sprites_list[0].sprites if sprites_list else {} + + @extend_schema_field(PokemonCriesSerializer) + def get_pokemon_cries(self, obj: Pokemon) -> dict[str, str | None]: + cries_list = list(cast("PokemonWithRelations", obj).pokemoncries.all()) + return cries_list[0].cries if cries_list else {} + + @extend_schema_field(PokemonMoveSerializer(many=True)) + def get_pokemon_moves(self, obj: Pokemon) -> list[dict[str, Any]]: + pokemon_moves = ( + PokemonMove.objects.filter(pokemon=obj, move__isnull=False) + .select_related("move", "version_group", "move_learn_method") + .order_by("move__id", "version_group_id") + ) + + vg_cache: dict[int, Any] = {} + mlm_cache: dict[int, Any] = {} + + moves_grouped: dict[int, dict[str, Any]] = {} + for pm in pokemon_moves: + if pm.move is None or pm.version_group is None or pm.move_learn_method is None: + continue + + move_pk = pm.move.pk + if move_pk not in moves_grouped: + moves_grouped[move_pk] = { + "move": MoveSummarySerializer(pm.move, context=self.context).data, + "version_group_details": [], } - }, - "additionalProperties": { # Stoplight Elements doesn't render this well - "type": "string", - "format": "uri", - "nullable": True, - "examples": [ - "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/1.png" - ], - }, - "examples": [ + + vg_pk = pm.version_group.pk + if vg_pk not in vg_cache: + vg_cache[vg_pk] = VersionGroupSummarySerializer(pm.version_group, context=self.context).data + version_group_data = vg_cache[vg_pk] + + mlm_pk = pm.move_learn_method.pk + if mlm_pk not in mlm_cache: + mlm_cache[mlm_pk] = MoveLearnMethodSummarySerializer(pm.move_learn_method, context=self.context).data + move_learn_method_data = mlm_cache[mlm_pk] + + moves_grouped[move_pk]["version_group_details"].append( { - "back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/1.png", - "back_female": None, - "back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/1.png", - "back_shiny_female": None, - "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png", - "front_female": None, - "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/1.png", - "front_shiny_female": None, + "level_learned_at": pm.level, + "version_group": version_group_data, + "move_learn_method": move_learn_method_data, + "order": pm.order, } - ], - } - ) - def get_pokemon_sprites(self, obj): - sprites_object = PokemonSprites.objects.get(pokemon_id=obj) - return sprites_object.sprites - - @extend_schema_field( - field={ - "type": "object", - "required": ["latest", "legacy"], - "properties": { - "latest": { - "type": "string", - "format": "uri", - "examples": ["https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/latest/50.ogg"], - }, - "legacy": { - "type": "string", - "format": "uri", - "examples": ["https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/legacy/50.ogg"], - }, - }, - } - ) - def get_pokemon_cries(self, obj): - cries_object = PokemonCries.objects.get(pokemon_id=obj) - return cries_object.cries - - # { - # "move": { - # "name": "scratch", - # "url": "https://pokeapi.co/api/v2/move/10/" - # }, - # "version_group_details": [ - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "red-blue", - # "url": "https://pokeapi.co/api/v2/version-group/1/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "yellow", - # "url": "https://pokeapi.co/api/v2/version-group/2/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "gold-silver", - # "url": "https://pokeapi.co/api/v2/version-group/3/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "crystal", - # "url": "https://pokeapi.co/api/v2/version-group/4/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "ruby-sapphire", - # "url": "https://pokeapi.co/api/v2/version-group/5/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "emerald", - # "url": "https://pokeapi.co/api/v2/version-group/6/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "firered-leafgreen", - # "url": "https://pokeapi.co/api/v2/version-group/7/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "diamond-pearl", - # "url": "https://pokeapi.co/api/v2/version-group/8/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "platinum", - # "url": "https://pokeapi.co/api/v2/version-group/9/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "heartgold-soulsilver", - # "url": "https://pokeapi.co/api/v2/version-group/10/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "black-white", - # "url": "https://pokeapi.co/api/v2/version-group/11/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "colosseum", - # "url": "https://pokeapi.co/api/v2/version-group/12/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "xd", - # "url": "https://pokeapi.co/api/v2/version-group/13/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "black-2-white-2", - # "url": "https://pokeapi.co/api/v2/version-group/14/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "x-y", - # "url": "https://pokeapi.co/api/v2/version-group/15/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "omega-ruby-alpha-sapphire", - # "url": "https://pokeapi.co/api/v2/version-group/16/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "sun-moon", - # "url": "https://pokeapi.co/api/v2/version-group/17/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "ultra-sun-ultra-moon", - # "url": "https://pokeapi.co/api/v2/version-group/18/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "lets-go-pikachu-lets-go-eevee", - # "url": "https://pokeapi.co/api/v2/version-group/19/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "sword-shield", - # "url": "https://pokeapi.co/api/v2/version-group/20/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "brilliant-diamond-shining-pearl", - # "url": "https://pokeapi.co/api/v2/version-group/23/" - # } - # }, - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "scarlet-violet", - # "url": "https://pokeapi.co/api/v2/version-group/25/" - # } - # } - # ] - # }, - - # "move": { - # "name": "scratch", - # "url": "https://pokeapi.co/api/v2/move/10/" - # }, - # "version_group_details": [ - # { - # "level_learned_at": 1, - # "move_learn_method": { - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - # }, - # "version_group": { - # "name": "red-blue", - # "url": "https://pokeapi.co/api/v2/version-group/1/" - # } - # }, - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["move", "version_group_details"], - "properties": { - "move": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["scratch"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move/10/"], - }, - }, - }, - "version_group_details": { - "type": "array", - "items": { - "type": "object", - "required": [ - "level_learned_at", - "move_learn_method", - "version_group", - ], - "properties": { - "level_learned_at": { - "type": "integer", - "format": "int32", - "examples": [1], - }, - "move_learn_method": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["level-up"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move-learn-method/1/"], - }, - }, - }, - "version_group": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["red-blue"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version-group/1/"], - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_pokemon_moves(self, obj): - version_objects = VersionGroup.objects.all().order_by("id") - version_data = { - version_object.id: data - for version_object, data in zip( - version_objects, - VersionGroupSummarySerializer(version_objects, many=True, context=self.context).data, ) - } - method_objects = MoveLearnMethod.objects.all().order_by("id") - method_data = { - method_object.id: data - for method_object, data in zip( - method_objects, - MoveLearnMethodSummarySerializer(method_objects, many=True, context=self.context).data, + + return list(moves_grouped.values()) + + @extend_schema_field(PokemonHeldItemSerializer(many=True)) + def get_pokemon_held_items(self, obj: Pokemon) -> list[dict[str, Any]]: + pokemon_items = ( + PokemonItem.objects.filter(pokemon=obj, item__isnull=False) + .select_related("item", "version") + .order_by("item__id", "version_id") + ) + + version_cache: dict[int, Any] = {} + + items_grouped: dict[int, dict[str, Any]] = {} + for pi in pokemon_items: + if pi.item is None or pi.version is None: + continue + + item_pk = pi.item.pk + if item_pk not in items_grouped: + items_grouped[item_pk] = { + "item": ItemSummarySerializer(pi.item, context=self.context).data, + "version_details": [], + } + + v_pk = pi.version.pk + if v_pk not in version_cache: + version_cache[v_pk] = VersionSummarySerializer(pi.version, context=self.context).data + + items_grouped[item_pk]["version_details"].append( + { + "rarity": pi.rarity, + "version": version_cache[v_pk], + } ) - } - # Get moves related to this pokemon and pull out unique Move IDs. - # Note that it's important to order by the same column we're using to - # determine if the entries are unique. Otherwise distinct() will - # return apparent duplicates. - - pokemon_moves = PokemonMove.objects.filter(pokemon_id=obj).order_by("move_id") - move_ids = pokemon_moves.values("move_id").distinct() - move_list = [] - - for id in move_ids: - pokemon_move_details = OrderedDict() - - # Get each Unique Move by ID - move_object = Move.objects.get(pk=id["move_id"]) - move_data = MoveSummarySerializer(move_object, context=self.context).data - pokemon_move_details["move"] = move_data - - # Get Versions and Move Methods associated with each unique move - pokemon_move_objects = pokemon_moves.filter(move_id=id["move_id"]) - serializer = PokemonMoveSerializer(pokemon_move_objects, many=True, context=self.context) - pokemon_move_details["version_group_details"] = [] - - for move in serializer.data: - version_detail = OrderedDict() - - version_detail["level_learned_at"] = move["level"] - version_detail["version_group"] = version_data[move["version_group"]] - version_detail["move_learn_method"] = method_data[move["move_learn_method"]] - version_detail["order"] = move["order"] - - pokemon_move_details["version_group_details"].append(version_detail) - - move_list.append(pokemon_move_details) - - return move_list - - # { - # "item": { - # "name": "soft-sand", - # "url": "https://pokeapi.co/api/v2/item/214/" - # }, - # "version_details": [ - # { - # "rarity": 5, - # "version": { - # "name": "diamond", - # "url": "https://pokeapi.co/api/v2/version/12/" - # } - # }, - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["item", "version_details"], - "properties": { - "item": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["soft-sand"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/item/214/"], - }, - }, - }, - "version_details": { - "type": "array", - "items": { - "type": "object", - "required": ["rarity", "version"], - "properties": { - "rarity": { - "type": "integer", - "format": "int32", - "examples": [5], - }, - "version": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["diamond"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version/12/"], - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_pokemon_held_items(self, obj): - # Get items related to this pokemon and pull out unique Item IDs - pokemon_items = PokemonItem.objects.filter(pokemon_id=obj).order_by("item_id") - item_ids = pokemon_items.values("item_id").distinct() - item_list = [] - - for id in item_ids: - pokemon_item_details = OrderedDict() - - # Get each Unique Item by ID - item_object = Item.objects.get(pk=id["item_id"]) - item_data = ItemSummarySerializer(item_object, context=self.context).data - pokemon_item_details["item"] = item_data - - # Get Versions associated with each unique item - pokemon_item_objects = pokemon_items.filter(item_id=id["item_id"]) - serializer = PokemonItemSerializer(pokemon_item_objects, many=True, context=self.context) - pokemon_item_details["version_details"] = [] - - for item in serializer.data: - version_detail = OrderedDict() - - version_detail["rarity"] = item["rarity"] - version_detail["version"] = item["version"] - - pokemon_item_details["version_details"].append(version_detail) - - item_list.append(pokemon_item_details) - - return item_list - - # { - # "ability": { - # "name": "sand-veil", - # "url": "https://pokeapi.co/api/v2/ability/8/" - # }, - # "is_hidden": false, - # "slot": 1 - # }, - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["ability", "is_hidden", "slot"], - "properties": { - "ability": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["sand-veil"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/ability/8/"], - }, - }, - }, - "is_hidden": {"type": "boolean"}, - "slot": {"type": "integer", "format": "int32", "examples": [1]}, - }, - }, - } - ) - def get_pokemon_abilities(self, obj): - pokemon_ability_objects = PokemonAbility.objects.filter(pokemon=obj) - data = PokemonAbilitySerializer(pokemon_ability_objects, many=True, context=self.context).data - abilities = [] - - for ability in data: - del ability["pokemon"] - abilities.append(ability) - - return abilities - - # { - # "abilities": [ - # { - # "ability": { - # "name": "levitate", - # "url": "https://pokeapi.co/api/v2/ability/26/" - # }, - # "is_hidden": false, - # "slot": 1 - # } - # ], - # "generation": { - # "name": "generation-vi", - # "url": "https://pokeapi.co/api/v2/generation/6/" - # } - # } - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["abilities", "generation"], - "properties": { - "abilities": { - "type": "array", - "items": { - "type": "object", - "required": ["ability", "is_hidden", "slot"], - "properties": { - "ability": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["levitate"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/ability/26/"], - }, - }, - }, - "is_hidden": {"type": "boolean"}, - "slot": { - "type": "integer", - "format": "int32", - "examples": [1], - }, - }, - }, - }, - "generation": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["generation-vi"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/generation/6/"], - }, - }, - }, - }, - }, - } - ) - def get_past_pokemon_abilities(self, obj): - pokemon_past_ability_objects = PokemonAbilityPast.objects.filter(pokemon=obj) - pokemon_past_abilities = PokemonAbilityPastSerializer( - pokemon_past_ability_objects, many=True, context=self.context - ).data - - # post-process to the form we want - current_generation = "" - past_obj = {} - final_data = [] - for pokemon_past_ability in pokemon_past_abilities: - del pokemon_past_ability["pokemon"] - - generation = pokemon_past_ability["generation"]["name"] - if generation != current_generation: - current_generation = generation - past_obj = {} - - # create past abilities object for this generation - past_obj["generation"] = pokemon_past_ability["generation"] - del pokemon_past_ability["generation"] - - # create abilities array - past_obj["abilities"] = [pokemon_past_ability] - - # add to past abilities array - final_data.append(past_obj) - - else: - # add to existing array for this generation - del pokemon_past_ability["generation"] - past_obj["abilities"].append(pokemon_past_ability) + return list(items_grouped.values()) - return final_data + @extend_schema_field(PokemonAbilitySerializer(many=True)) + def get_pokemon_abilities(self, obj: Pokemon) -> ReturnList[ReturnDict[str, Any]]: + abilities = PokemonAbility.objects.filter(pokemon=obj).select_related("ability") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonAbilitySerializer(abilities, many=True, context=self.context).data, + ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["generation", "stats"], - "properties": { - "generation": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["generation-vi"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/generation/6/"], - }, - }, - }, - "stats": { - "type": "array", - "items": { - "type": "object", - "required": ["base_stat", "effort", "stat"], - "properties": { - "base_stat": { - "type": "integer", - "format": "int32", - "examples": [45], - }, - "effort": { - "type": "integer", - "format": "int32", - "examples": [0], - }, - "stat": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["speed"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/stat/6/"], - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_past_pokemon_stats(self, obj): - pokemon_past_stat_objects = PokemonStatPast.objects.filter(pokemon=obj) - pokemon_past_stats = PokemonStatPastSerializer(pokemon_past_stat_objects, many=True, context=self.context).data - - # post-process to the form we want - current_generation = "" - past_obj = {} - final_data = [] - for pokemon_past_stat in pokemon_past_stats: - generation = pokemon_past_stat["generation"]["name"] - if generation != current_generation: - current_generation = generation - past_obj = {} - - # create past stats object for this generation - past_obj["generation"] = pokemon_past_stat["generation"] - del pokemon_past_stat["generation"] - - # create stats array - past_obj["stats"] = [pokemon_past_stat] - - # add to past stats array - final_data.append(past_obj) - - else: - # add to existing array for this generation - del pokemon_past_stat["generation"] - past_obj["stats"].append(pokemon_past_stat) + @extend_schema_field(PokemonPastAbilitySerializer(many=True)) + def get_past_pokemon_abilities(self, obj: Pokemon) -> list[dict[str, Any]]: + past_abilities = ( + PokemonAbilityPast.objects.filter(pokemon=obj, generation__isnull=False) + .select_related("generation", "ability") + .order_by("generation_id") + ) - return final_data + final_data: list[dict[str, Any]] = [] + for _, group in itertools.groupby(past_abilities, key=lambda x: cast("Generation", x.generation).name): + group_list: Sequence[PokemonAbilityPast] = list(group) + gen_data = cast( + "ReturnDict[str, Any]", + GenerationSummarySerializer(group_list[0].generation, context=self.context).data, + ) + abilities_data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonAbilityPastSerializer(group_list, many=True, context=self.context).data, + ) + final_data.append( + { + "generation": gen_data, + "abilities": abilities_data, + } + ) - # { - # "slot": 1, - # "type": { - # "name": "ghost", - # "url": "https://pokeapi.co/api/v2/type/8/" - # } - # }, - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["slot", "type"], - "properties": { - "slot": {"type": "integer", "format": "int32", "examples": [1]}, - "type": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["ghost"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/8/"], - }, - }, - }, - }, - }, - } - ) - def get_pokemon_types(self, obj): - poke_type_objects = PokemonType.objects.filter(pokemon=obj) - poke_types = PokemonTypeSerializer(poke_type_objects, many=True, context=self.context).data - - for poke_type in poke_types: - del poke_type["pokemon"] - - return poke_types - - # "past_types": [ - # { - # "generation": { - # "name": "generation-v", - # "url": "https://pokeapi.co/api/v2/generation/5/" - # }, - # "types": [ - # { - # "slot": 1, - # "type": { - # "name": "normal", - # "url": "https://pokeapi.co/api/v2/type/1/" - # } - # } - # ] - # } - # ], - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["generation", "types"], - "properties": { - "generation": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["generation-v"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/generation/5/"], - }, - }, - }, - "types": { - "type": "array", - "items": { - "type": "object", - "required": ["slot", "type"], - "properties": { - "slot": { - "type": "integer", - "format": "int32", - "examples": [1], - }, - "type": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": ["normal"], - }, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/type/1/"], - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - def get_past_pokemon_types(self, obj): - poke_past_type_objects = PokemonTypePast.objects.filter(pokemon=obj) - poke_past_types = PokemonTypePastSerializer(poke_past_type_objects, many=True, context=self.context).data + return final_data - # post-process to the form we want - current_generation = "" - past_obj = {} - final_data = [] - for poke_past_type in poke_past_types: - del poke_past_type["pokemon"] + @extend_schema_field(PokemonPastStatSerializer(many=True)) + def get_past_pokemon_stats(self, obj: Pokemon) -> list[dict[str, Any]]: + past_stats = ( + PokemonStatPast.objects.filter(pokemon=obj, generation__isnull=False) + .select_related("generation", "stat") + .order_by("generation_id") + ) - generation = poke_past_type["generation"]["name"] - if generation != current_generation: - current_generation = generation - past_obj = {} + final_data: list[dict[str, Any]] = [] + for _, group in itertools.groupby(past_stats, key=lambda x: cast("Generation", x.generation).name): + group_list: Sequence[PokemonStatPast] = list(group) + gen_data = cast( + "ReturnDict[str, Any]", + GenerationSummarySerializer(group_list[0].generation, context=self.context).data, + ) + stats_data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonStatPastSerializer(group_list, many=True, context=self.context).data, + ) + final_data.append( + { + "generation": gen_data, + "stats": stats_data, + } + ) - # create past types object for this generation - past_obj["generation"] = poke_past_type["generation"] - del poke_past_type["generation"] + return final_data - # create types array - past_obj["types"] = [poke_past_type] + @extend_schema_field(PokemonTypeSerializer(many=True)) + def get_pokemon_types(self, obj: Pokemon) -> ReturnList[ReturnDict[str, Any]]: + types = PokemonType.objects.filter(pokemon=obj).select_related("type").order_by("slot") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonTypeSerializer(types, many=True, context=self.context).data, + ) - # add to past types array - final_data.append(past_obj) + @extend_schema_field(PokemonPastTypeSerializer(many=True)) + def get_past_pokemon_types(self, obj: Pokemon) -> list[dict[str, Any]]: + past_types = ( + PokemonTypePast.objects.filter(pokemon=obj, generation__isnull=False) + .select_related("generation", "type") + .order_by("generation_id", "slot") + ) - else: - # add to existing array for this generation - del poke_past_type["generation"] - past_obj["types"].append(poke_past_type) + final_data: list[dict[str, Any]] = [] + for _, group in itertools.groupby(past_types, key=lambda x: cast("Generation", x.generation).name): + group_list: Sequence[PokemonTypePast] = list(group) + gen_data = cast( + "ReturnDict[str, Any]", GenerationSummarySerializer(group_list[0].generation, context=self.context).data + ) + types_data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonTypePastSerializer(group_list, many=True, context=self.context).data, + ) + final_data.append( + { + "generation": gen_data, + "types": types_data, + } + ) return final_data - @extend_schema_field( - field={ - "type": "string", - "examples": ["https://pokeapi.co/api/v2/pokemon/1/encounters"], - } - ) - def get_encounters(self, obj): - return reverse("pokemon_encounters", kwargs={"pokemon_id": obj.pk}) + @extend_schema_field(serializers.CharField) + def get_encounters(self, obj: Pokemon) -> str: + return reverse("pokemon_encounters", kwargs={"pokemon_id": obj.pk}, request=self.context.get("request")) ################################# # POKEMON SPECIES SERIALIZERS # ################################# -class EvolutionTriggerNameSerializer(serializers.HyperlinkedModelSerializer): + + +class EvolutionTriggerNameSerializer(serializers.HyperlinkedModelSerializer[EvolutionTriggerName]): language = LanguageSummarySerializer() class Meta: @@ -5176,7 +3137,7 @@ class Meta: fields = ("name", "language") -class EvolutionTriggerDetailSerializer(serializers.HyperlinkedModelSerializer): +class EvolutionTriggerDetailSerializer(serializers.HyperlinkedModelSerializer[EvolutionTrigger]): names = EvolutionTriggerNameSerializer(many=True, read_only=True, source="evolutiontriggername") pokemon_species = serializers.SerializerMethodField("get_species") @@ -5184,38 +3145,16 @@ class Meta: model = EvolutionTrigger fields = ("id", "name", "names", "pokemon_species") - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["ivysaur"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/2/"], - }, - }, - }, - } - ) - def get_species(self, obj): - evo_objects = PokemonEvolution.objects.filter(evolution_trigger=obj) - species_list = [] - species_names = set() - - for evo in evo_objects: - species = PokemonSpeciesSummarySerializer(evo.evolved_species, context=self.context).data - if species["name"] not in species_names: - species_list.append(species) - species_names.add(species["name"]) - - return species_list + @extend_schema_field(PokemonSpeciesSummarySerializer(many=True)) + def get_species(self, obj: EvolutionTrigger) -> ReturnList[ReturnDict[str, Any]]: + species = PokemonSpecies.objects.filter(evolved_species__evolution_trigger=obj).distinct().order_by("id") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesSummarySerializer(species, many=True, context=self.context).data, + ) -class PokemonSpeciesDescriptionSerializer(serializers.ModelSerializer): +class PokemonSpeciesDescriptionSerializer(serializers.ModelSerializer[PokemonSpeciesDescription]): language = LanguageSummarySerializer() class Meta: @@ -5223,7 +3162,7 @@ class Meta: fields = ("description", "language") -class PokemonSpeciesFlavorTextSerializer(serializers.ModelSerializer): +class PokemonSpeciesFlavorTextSerializer(serializers.ModelSerializer[PokemonSpeciesFlavorText]): flavor_text = serializers.CharField() language = LanguageSummarySerializer() version = VersionSummarySerializer() @@ -5233,15 +3172,7 @@ class Meta: fields = ("flavor_text", "language", "version") -class PokemonSpeciesNameSerializer(serializers.ModelSerializer): - language = LanguageSummarySerializer() - - class Meta: - model = PokemonSpeciesName - fields = ("name", "genus", "language") - - -class PokemonSpeciesEvolutionSerializer(serializers.ModelSerializer): +class PokemonSpeciesEvolutionSerializer(serializers.ModelSerializer[PokemonSpecies]): """ This is here purely to help build pokemon evolution chains """ @@ -5251,7 +3182,7 @@ class Meta: fields = ("name", "id", "evolves_from_species", "is_baby") -class PokemonSpeciesDetailSerializer(serializers.ModelSerializer): +class PokemonSpeciesDetailSerializer(serializers.ModelSerializer[PokemonSpecies]): names = serializers.SerializerMethodField("get_pokemon_names") form_descriptions = PokemonSpeciesDescriptionSerializer( many=True, read_only=True, source="pokemonspeciesdescription" @@ -5304,192 +3235,59 @@ class Meta: "varieties", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["language", "name"], - "properties": { - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - "name": {"type": "string", "examples": ["bulbasaur"]}, - }, - }, - } - ) - def get_pokemon_names(self, obj): - species_results = PokemonSpeciesName.objects.filter(pokemon_species=obj) - species_serializer = PokemonSpeciesNameSerializer(species_results, many=True, context=self.context) - - data = species_serializer.data - - for name in data: - del name["genus"] - - return data + @extend_schema_field(PokemonSpeciesNameSerializer(many=True)) + def get_pokemon_names(self, obj: PokemonSpecies) -> ReturnList[ReturnDict[str, Any]]: + species_results = PokemonSpeciesName.objects.filter(pokemon_species=obj).select_related("language") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesNameSerializer(species_results, many=True, context=self.context).data, + ) - # { - # "genus": "Seed Pokémon", - # "language": { - # "name": "en", - # "url": "https://pokeapi.co/api/v2/language/9/" - # } - # }, - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["genus", "language"], - "properties": { - "genus": {"type": "string", "examples": ["Seed Pokémon"]}, - "language": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["en"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/language/9/"], - }, - }, - }, - }, - }, - } - ) - def get_pokemon_genera(self, obj): - results = PokemonSpeciesName.objects.filter(pokemon_species=obj) - serializer = PokemonSpeciesNameSerializer(results, many=True, context=self.context) - data = serializer.data - genera = [] - - for entry in data: - if entry["genus"]: - del entry["name"] - genera.append(entry) - - return genera - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["monster"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/egg-group/1/"], - }, - }, - }, - } - ) - def get_pokemon_egg_groups(self, obj): - results = PokemonEggGroup.objects.filter(pokemon_species=obj) - data = PokemonEggGroupSerializer(results, many=True, context=self.context).data - groups = [] - for group in data: - groups.append(group["egg_group"]) - - return groups - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["is_default", "pokemon"], - "properties": { - "is_default": {"type": "boolean"}, - "pokemon": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bulbasaur"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon/1/"], - }, - }, - }, - }, - }, - } - ) - def get_pokemon_varieties(self, obj): - results = Pokemon.objects.filter(pokemon_species=obj) - summary_data = PokemonSummarySerializer(results, many=True, context=self.context).data - detail_data = PokemonDetailSerializer(results, many=True, context=self.context).data - - varieties = [] - - for index, pokemon in enumerate(detail_data): - entry = OrderedDict() - entry["is_default"] = pokemon["is_default"] - entry["pokemon"] = summary_data[index] - varieties.append(entry) - - return varieties - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["area", "base_score", "rate"], - "properties": { - "area": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["field"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pal-park-area/2/"], - }, - }, - }, - "base_score": { - "type": "integer", - "format": "int32", - "examples": [50], - }, - "rate": {"type": "integer", "format": "int32", "examples": [30]}, - }, - }, - } - ) - def get_encounters(self, obj): - pal_park_objects = PalPark.objects.filter(pokemon_species=obj) - parks = PalParkSerializer(pal_park_objects, many=True, context=self.context).data - encounters = [] + @extend_schema_field(PokemonSpeciesGenusSerializer(many=True)) + def get_pokemon_genera(self, obj: PokemonSpecies) -> ReturnList[ReturnDict[str, Any]]: + results = ( + PokemonSpeciesName.objects.filter(pokemon_species=obj, genus__isnull=False) + .exclude(genus="") + .select_related("language") + ) + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesGenusSerializer(results, many=True, context=self.context).data, + ) - for encounter in parks: - del encounter["pokemon_species"] - encounters.append(encounter) + @extend_schema_field(EggGroupSummarySerializer(many=True)) + def get_pokemon_egg_groups(self, obj: PokemonSpecies) -> ReturnList[ReturnDict[str, Any]]: + egg_groups = EggGroup.objects.filter(pokemonegggroup__pokemon_species=obj).distinct() + return cast( + "ReturnList[ReturnDict[str, Any]]", + EggGroupSummarySerializer(egg_groups, many=True, context=self.context).data, + ) - return encounters + @extend_schema_field(PokemonSpeciesVarietySerializer(many=True)) + def get_pokemon_varieties(self, obj: PokemonSpecies) -> list[dict[str, Any]]: + pokemon_list = Pokemon.objects.filter(pokemon_species=obj) + summaries = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSummarySerializer(pokemon_list, many=True, context=self.context).data, + ) + return [ + { + "is_default": pk.is_default, + "pokemon": summary, + } + for pk, summary in zip(pokemon_list, summaries, strict=True) + ] + + @extend_schema_field(PokemonSpeciesPalParkEncounterSerializer(many=True)) + def get_encounters(self, obj: PokemonSpecies) -> ReturnList[ReturnDict[str, Any]]: + pal_park_objects = PalPark.objects.filter(pokemon_species=obj).select_related("pal_park_area") + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesPalParkEncounterSerializer(pal_park_objects, many=True, context=self.context).data, + ) -class PokemonEvolutionSerializer(serializers.ModelSerializer): +class PokemonEvolutionSerializer(serializers.ModelSerializer[PokemonEvolution]): version_group = VersionGroupSummarySerializer() item = ItemSummarySerializer(source="evolution_item") held_item = ItemSummarySerializer() @@ -5540,349 +3338,69 @@ class Meta: ) -class EvolutionChainDetailSerializer(serializers.ModelSerializer): +class EvolutionChainLinkSerializer(serializers.Serializer[Any]): + is_baby = serializers.BooleanField() + species = PokemonSpeciesSummarySerializer() + evolution_details = PokemonEvolutionSerializer(many=True) + evolves_to = serializers.ListField(child=serializers.DictField()) + + +class EvolutionChainDetailSerializer(serializers.ModelSerializer[EvolutionChain]): baby_trigger_item = ItemSummarySerializer() chain = serializers.SerializerMethodField("build_chain") + POKEMON_EVOLUTION_FK_FIELDS: ClassVar[list[str]] = [ + field.name + for field in PokemonEvolution._meta.get_fields() + if field.is_relation and (field.many_to_one or field.one_to_one) + ] + class Meta: model = EvolutionChain fields = ("id", "baby_trigger_item", "chain") - # TODO: Revisit Schema - @extend_schema_field( - field={ - "type": "object", - "required": ["evolution_details", "evolves_to", "is_baby", "species"], - "properties": { - "evolution_details": {"type": "array", "items": {}, "examples": []}, - "evolves_to": { - "type": "array", - "items": { - "type": "object", - "required": [ - "evolution_details", - "evolves_to", - "is_baby", - "species", - ], - "properties": { - "evolution_details": { - "type": "array", - "items": { - "type": "object", - "required": [ - "version_group", - "is_default", - "gender", - "held_item", - "item", - "known_move", - "known_move_type", - "location", - "min_affection", - "min_beauty", - "min_damage_taken", - "min_happiness", - "min_level", - "min_move_count", - "min_steps", - "near_special_rock", - "needs_multiplayer", - "needs_overworld_rain", - "party_species", - "party_type", - "relative_physical_stats", - "time_of_day", - "trade_species", - "trigger", - "turn_upside_down", - "used_move", - "region", - "base_form", - "evolved_form", - ], - "properties": { - "version_group": { - "type": "object", - "nullable": False, - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": [1], - }, - "url": { - "type": "string", - "format": "uri", - "examples": [2], - }, - }, - }, - "is_default": {"type": "boolean"}, - "gender": { - "type": "", - "nullable": True, - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": [1], - }, - "url": { - "type": "string", - "format": "uri", - "examples": [2], - }, - }, - }, - "held_item": { - "type": "object", - "nullable": True, - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": [1], - }, - "url": { - "type": "string", - "format": "uri", - "examples": [2], - }, - }, - }, - "item": { - "type": "object", - "nullable": True, - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - "examples": [1], - }, - "url": { - "type": "string", - "format": "uri", - "examples": [2], - }, - }, - }, - "known_move": { - "type": "", - "nullable": True, - }, - "known_move_type": { - "type": "", - "nullable": True, - }, - "location": { - "type": "object", - "nullable": True, - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - }, - "url": { - "type": "string", - "format": "uri", - }, - }, - }, - "min_affection": { - "type": "integer", - "format": "int32", - "nullable": True, - }, - "min_beauty": { - "type": "integer", - "format": "int32", - "nullable": True, - }, - "min_damage_taken": { - "type": "integer", - "format": "int32", - "nullable": True, - }, - "min_happiness": { - "type": "integer", - "format": "int32", - "nullable": True, - }, - "min_level": { - "type": "integer", - "format": "int32", - "nullable": True, - }, - "min_move_count": { - "type": "integer", - "format": "int32", - "nullable": True, - }, - "min_steps": { - "type": "integer", - "format": "int32", - "nullable": True, - }, - "near_special_rock": { - "type": "boolean", - "nullable": True, - }, - "needs_multiplayer": { - "type": "boolean", - "nullable": True, - }, - "needs_overworld_rain": { - "type": "boolean", - "nullable": True, - }, - "party_species": { - "type": "string", - "nullable": True, - }, - "party_type": { - "type": "string", - "nullable": True, - }, - "relative_physical_stats": { - "type": "string", - "nullable": True, - }, - "time_of_day": {"type": "string"}, - "trade_species": { - "type": "string", - "nullable": True, - }, - "trigger": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": { - "type": "string", - }, - "url": { - "type": "string", - "format": "uri", - }, - }, - }, - "turn_upside_down": {"type": "boolean"}, - "used_move": { - "type": "", - "nullable": True, - }, - "region": { - "type": "object", - "nullable": True, - "required": ["name", "url"], - "properties": { - "name": {"type": "string"}, - "url": { - "type": "string", - "format": "uri", - }, - }, - }, - "base_form": { - "type": "object", - "nullable": True, - "required": ["name", "url"], - "properties": { - "name": {"type": "string"}, - "url": { - "type": "string", - "format": "uri", - }, - }, - }, - "evolved_form": { - "type": "object", - "nullable": True, - "required": ["name", "url"], - "properties": { - "name": {"type": "string"}, - "url": { - "type": "string", - "format": "uri", - }, - }, - }, - }, - }, - }, - "is_baby": {"type": "boolean"}, - "species": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["happiny"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/440/"], - }, - }, - }, - }, - }, - }, - "is_baby": {"type": "boolean"}, - "species": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["happiny"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/440/"], - }, - }, - }, - }, - } - ) - def build_chain(self, obj): - chain_id = obj.id - - pokemon_objects = PokemonSpecies.objects.filter(evolution_chain_id=chain_id).order_by("order") - summary_data = PokemonSpeciesSummarySerializer(pokemon_objects, many=True, context=self.context).data - ref_data = PokemonSpeciesEvolutionSerializer(pokemon_objects, many=True, context=self.context).data + @extend_schema_field(EvolutionChainLinkSerializer) + def build_chain(self, obj: EvolutionChain) -> dict[str, Any]: + pokemon_objects = PokemonSpecies.objects.filter(evolution_chain=obj).order_by("order") + summary_data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesSummarySerializer(pokemon_objects, many=True, context=self.context).data, + ) + ref_data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonSpeciesEvolutionSerializer(pokemon_objects, many=True, context=self.context).data, + ) - # convert evolution data list to tree evolution_tree = self.build_evolution_tree(ref_data) - - # serialize chain recursively from tree - chain = self.build_chain_link_entry(evolution_tree, summary_data) - - return chain + return self.build_chain_link_entry(evolution_tree, summary_data) # converts a list of Pokemon species evolution data into a tree representing the evolution chain - def build_evolution_tree(self, species_evolution_data): - evolution_tree = OrderedDict() - evolution_tree["species"] = species_evolution_data[0] - evolution_tree["children"] = [] + def build_evolution_tree(self, species_evolution_data: ReturnList[ReturnDict[str, Any]]) -> dict[str, Any]: + if not species_evolution_data: + return {} + + first_species: dict[str, Any] = species_evolution_data[0] + evolution_tree: dict[str, Any] = {"species": first_species, "children": []} for species in species_evolution_data[1:]: - chain_link = OrderedDict() - chain_link["species"] = species - chain_link["children"] = [] + species_item: dict[str, Any] = species + chain_link: dict[str, Any] = { + "species": species_item, + "children": [], + } - evolves_from_species_id = chain_link["species"]["evolves_from_species"] + species_dict: dict[str, Any] = chain_link["species"] + evolves_from_species_id = species_dict["evolves_from_species"] - # find parent link by DFS parent_link = evolution_tree search_stack = [parent_link] - while len(search_stack) > 0: - l = search_stack.pop() - if l["species"]["id"] == evolves_from_species_id: - parent_link = l + while search_stack: + link = search_stack.pop() + if link["species"]["id"] == evolves_from_species_id: + parent_link = link break - - # "left" to "right" requires reversing the list of children - search_stack += reversed(l["children"]) + search_stack.extend(reversed(link["children"])) parent_link["children"].append(chain_link) @@ -5890,50 +3408,56 @@ def build_evolution_tree(self, species_evolution_data): # serializes an evolution chain link recursively # chain_link is a tree representing an evolution chain - def build_chain_link_entry(self, chain_link, summary_data): - entry = OrderedDict() + def build_chain_link_entry( + self, chain_link: dict[str, Any], summary_data: ReturnList[ReturnDict[str, Any]] + ) -> dict[str, Any]: + species = chain_link["species"] evolution_data = None - species = chain_link["species"] if species["evolves_from_species"]: - evolution_object = PokemonEvolution.objects.filter(evolved_species=species["id"]) + evolution_objects = PokemonEvolution.objects.filter(evolved_species=species["id"]).select_related( + *self.POKEMON_EVOLUTION_FK_FIELDS + ) + evolution_data = cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonEvolutionSerializer(evolution_objects, many=True, context=self.context).data, + ) - evolution_data = PokemonEvolutionSerializer(evolution_object, many=True, context=self.context).data + return { + "is_baby": species["is_baby"], + "species": next(x for x in summary_data if x["name"] == species["name"]), + "evolution_details": evolution_data or [], + "evolves_to": [self.build_chain_link_entry(c, summary_data) for c in chain_link["children"]], + } - entry["is_baby"] = species["is_baby"] - species_summary = next(x for x in summary_data if x["name"] == species["name"]) - entry["species"] = species_summary +############################ +# POKEATHLON SERIALIZERS # +############################ - entry["evolution_details"] = evolution_data or [] - evolves_to = [self.build_chain_link_entry(c, summary_data) for c in chain_link["children"]] - entry["evolves_to"] = evolves_to +class PokeathlonStatNameSerializer(serializers.HyperlinkedModelSerializer[PokeathlonStatName]): + language = LanguageSummarySerializer() - return entry + class Meta: + model = PokeathlonStatName + fields = ("name", "language") -class PokemonDexNumberSerializer(serializers.ModelSerializer): - entry_number = serializers.IntegerField(source="pokedex_number") - pokemon_species = PokemonSpeciesSummarySerializer() +class PokeathlonStatAffectingNatureSerializer(serializers.ModelSerializer[NaturePokeathlonStat]): + nature = NatureSummarySerializer() class Meta: - model = PokemonDexNumber - fields = ("pokedex", "entry_number", "pokemon_species") - + model = NaturePokeathlonStat + fields = ("max_change", "nature") -############################ -# POKEATHLON SERIALIZERS # -############################ -class PokeathlonStatNameSerializer(serializers.HyperlinkedModelSerializer): - language = LanguageSummarySerializer() - class Meta: - model = PokeathlonStatName - fields = ("name", "language") +class PokeathlonStatAffectingNaturesSerializer(serializers.Serializer[Any]): + increase = PokeathlonStatAffectingNatureSerializer(many=True) + decrease = PokeathlonStatAffectingNatureSerializer(many=True) -class PokeathlonStatDetailSerializer(serializers.HyperlinkedModelSerializer): +class PokeathlonStatDetailSerializer(serializers.HyperlinkedModelSerializer[PokeathlonStat]): names = PokeathlonStatNameSerializer(many=True, read_only=True, source="pokeathlonstatname") affecting_natures = serializers.SerializerMethodField("get_natures_that_affect") @@ -5941,87 +3465,29 @@ class Meta: model = PokeathlonStat fields = ("id", "name", "affecting_natures", "names") - @extend_schema_field( - field={ - "type": "object", - "required": ["decrease", "increase"], - "properties": { - "decrease": { - "type": "array", - "items": { - "type": "object", - "required": ["max_change", "nature"], - "properties": { - "max_change": { - "type": "integer", - "format": "int32", - "maximum": -1, - "examples": [-1], - }, - "nature": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["hardy"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/nature/1/"], - }, - }, - }, - }, - }, - }, - "increase": { - "type": "array", - "items": { - "type": "object", - "required": ["max_change", "nature"], - "properties": { - "max_change": { - "type": "integer", - "format": "int32", - "minimum": 1, - "examples": [2], - }, - "nature": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["hardy"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/nature/1/"], - }, - }, - }, - }, - }, - }, - }, + @extend_schema_field(PokeathlonStatAffectingNaturesSerializer) + def get_natures_that_affect(self, obj: PokeathlonStat) -> dict[str, ReturnList[ReturnDict[str, Any]]]: + base_qs = NaturePokeathlonStat.objects.filter(pokeathlon_stat=obj).select_related("nature") + increases = base_qs.filter(max_change__gt=0) + decreases = base_qs.filter(max_change__lte=0) + return { + "increase": cast( + "ReturnList[ReturnDict[str, Any]]", + PokeathlonStatAffectingNatureSerializer(increases, many=True, context=self.context).data, + ), + "decrease": cast( + "ReturnList[ReturnDict[str, Any]]", + PokeathlonStatAffectingNatureSerializer(decreases, many=True, context=self.context).data, + ), } - ) - def get_natures_that_affect(self, obj): - stat_change_objects = NaturePokeathlonStat.objects.filter(pokeathlon_stat=obj) - stat_changes = NaturePokeathlonStatSerializer(stat_change_objects, many=True, context=self.context).data - changes = OrderedDict([("increase", []), ("decrease", [])]) - - for change in stat_changes: - del change["pokeathlon_stat"] - if change["max_change"] > 0: - changes["increase"].append(change) - else: - changes["decrease"].append(change) - - return changes ######################### # POKEDEX SERIALIZERS # ######################### -class PokedexNameSerializer(serializers.HyperlinkedModelSerializer): + + +class PokedexNameSerializer(serializers.HyperlinkedModelSerializer[PokedexName]): language = LanguageSummarySerializer() class Meta: @@ -6029,7 +3495,7 @@ class Meta: fields = ("name", "language") -class PokedexDescriptionSerializer(serializers.HyperlinkedModelSerializer): +class PokedexDescriptionSerializer(serializers.HyperlinkedModelSerializer[PokedexDescription]): language = LanguageSummarySerializer() class Meta: @@ -6037,7 +3503,16 @@ class Meta: fields = ("description", "language") -class PokedexDetailSerializer(serializers.ModelSerializer): +class PokemonDexNumberSerializer(serializers.ModelSerializer[PokemonDexNumber]): + entry_number = serializers.IntegerField(source="pokedex_number") + pokemon_species = PokemonSpeciesSummarySerializer() + + class Meta: + model = PokemonDexNumber + fields = ("entry_number", "pokemon_species") + + +class PokedexDetailSerializer(serializers.ModelSerializer[Pokedex]): region = RegionSummarySerializer() names = PokedexNameSerializer(many=True, read_only=True, source="pokedexname") descriptions = PokedexDescriptionSerializer(many=True, read_only=True, source="pokedexdescription") @@ -6057,76 +3532,31 @@ class Meta: "version_groups", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["entry_number", "pokemon_species"], - "properties": { - "entry_number": { - "type": "integer", - "format": "int32", - "examples": [1], - }, - "pokemon_species": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["bulbasaur"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokemon-species/1/"], - }, - }, - }, - }, - }, - } - ) - def get_pokedex_entries(self, obj): - results = PokemonDexNumber.objects.filter(pokedex=obj).order_by("pokedex_number") - serializer = PokemonDexNumberSerializer(results, many=True, context=self.context) - data = serializer.data - - for entry in data: - del entry["pokedex"] - - return data - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["the-teal-mask"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/version-group/26/"], - }, - }, - }, - } - ) - def get_pokedex_version_groups(self, obj): - dex_group_objects = PokedexVersionGroup.objects.filter(pokedex=obj) - dex_groups = PokedexVersionGroupSerializer(dex_group_objects, many=True, context=self.context).data - results = [] - - for dex_group in dex_groups: - results.append(dex_group["version_group"]) + @extend_schema_field(PokemonDexNumberSerializer(many=True)) + def get_pokedex_entries(self, obj: Pokedex) -> ReturnList[ReturnDict[str, Any]]: + entries = ( + PokemonDexNumber.objects.filter(pokedex=obj).select_related("pokemon_species").order_by("pokedex_number") + ) + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokemonDexNumberSerializer(entries, many=True, context=self.context).data, + ) - return results + @extend_schema_field(VersionGroupSummarySerializer(many=True)) + def get_pokedex_version_groups(self, obj: Pokedex) -> ReturnList[ReturnDict[str, Any]]: + version_groups = VersionGroup.objects.filter(pokedexversiongroup__pokedex=obj).distinct() + return cast( + "ReturnList[ReturnDict[str, Any]]", + VersionGroupSummarySerializer(version_groups, many=True, context=self.context).data, + ) ######################### # VERSION SERIALIZERS # ######################### -class VersionNameSerializer(serializers.ModelSerializer): + + +class VersionNameSerializer(serializers.ModelSerializer[VersionName]): language = LanguageSummarySerializer() class Meta: @@ -6134,7 +3564,7 @@ class Meta: fields = ("name", "language") -class VersionDetailSerializer(serializers.ModelSerializer): +class VersionDetailSerializer(serializers.ModelSerializer[Version]): """ Should have a link to Version Group info but the Circular dependency and compilation order fight eachother and I'm @@ -6149,7 +3579,7 @@ class Meta: fields = ("id", "name", "names", "version_group") -class VersionGroupDetailSerializer(serializers.ModelSerializer): +class VersionGroupDetailSerializer(serializers.ModelSerializer[VersionGroup]): generation = GenerationSummarySerializer() versions = VersionSummarySerializer(many=True, read_only=True, source="version") regions = serializers.SerializerMethodField("get_version_group_regions") @@ -6169,87 +3599,26 @@ class Meta: "versions", ) - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["kanto"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/region/1/"], - }, - }, - }, - } - ) - def get_version_group_regions(self, obj): - vg_regions = VersionGroupRegion.objects.filter(version_group=obj) - data = VersionGroupRegionSerializer(vg_regions, many=True, context=self.context).data - regions = [] - - for region in data: - regions.append(region["region"]) - - return regions - - # "name": "level-up", - # "url": "https://pokeapi.co/api/v2/move-learn-method/1/" - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["level-up"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/move-learn-method/1/"], - }, - }, - }, - } - ) - def get_learn_methods(self, obj): - learn_method_objects = VersionGroupMoveLearnMethod.objects.filter(version_group=obj) - learn_method_data = VersionGroupMoveLearnMethodSerializer( - learn_method_objects, many=True, context=self.context - ).data - methods = [] - - for method in learn_method_data: - methods.append(method["move_learn_method"]) - - return methods - - @extend_schema_field( - field={ - "type": "array", - "items": { - "type": "object", - "required": ["name", "url"], - "properties": { - "name": {"type": "string", "examples": ["kanto"]}, - "url": { - "type": "string", - "format": "uri", - "examples": ["https://pokeapi.co/api/v2/pokedex/2/"], - }, - }, - }, - } - ) - def get_version_groups_pokedexes(self, obj): - dex_group_objects = PokedexVersionGroup.objects.filter(version_group=obj) - dex_groups = PokedexVersionGroupSerializer(dex_group_objects, many=True, context=self.context).data - results = [] + @extend_schema_field(RegionSummarySerializer(many=True)) + def get_version_group_regions(self, obj: VersionGroup) -> ReturnList[ReturnDict[str, Any]]: + regions = Region.objects.filter(versiongroupregion__version_group=obj).distinct() + return cast( + "ReturnList[ReturnDict[str, Any]]", + RegionSummarySerializer(regions, many=True, context=self.context).data, + ) - for dex_group in dex_groups: - results.append(dex_group["pokedex"]) + @extend_schema_field(MoveLearnMethodSummarySerializer(many=True)) + def get_learn_methods(self, obj: VersionGroup) -> ReturnList[ReturnDict[str, Any]]: + methods = MoveLearnMethod.objects.filter(versiongroupmovelearnmethod__version_group=obj).distinct() + return cast( + "ReturnList[ReturnDict[str, Any]]", + MoveLearnMethodSummarySerializer(methods, many=True, context=self.context).data, + ) - return results + @extend_schema_field(PokedexSummarySerializer(many=True)) + def get_version_groups_pokedexes(self, obj: VersionGroup) -> ReturnList[ReturnDict[str, Any]]: + pokedexes = Pokedex.objects.filter(pokedexversiongroup__version_group=obj).distinct() + return cast( + "ReturnList[ReturnDict[str, Any]]", + PokedexSummarySerializer(pokedexes, many=True, context=self.context).data, + ) diff --git a/pokemon_v2/test_models.py b/pokemon_v2/test_models.py index 9c78683b6..a6532ee1f 100644 --- a/pokemon_v2/test_models.py +++ b/pokemon_v2/test_models.py @@ -1,18 +1,23 @@ import csv import os import re + from django.conf import settings from django.test import TestCase +from typing_extensions import override + from pokemon_v2.models import * class AbilityTestCase(TestCase): + @override def setUp(self): Ability.objects.create(name="Smell", generation_id=3, is_main_series=True) def fields_are_valid(self): smell = Ability.objects.get(name="Smell") - self.assertEqual(smell.generation_id, 3) + assert smell.generation is not None + self.assertEqual(smell.generation.pk, 3) class EncounterPokemonDetailTestCase(TestCase): @@ -70,10 +75,10 @@ def test_all_csv_identifiers_are_ascii_slugs(self): csv_path = os.path.join(csv_dir, filename) try: - with open(csv_path, "r", encoding="utf-8") as csvfile: + with open(csv_path, encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) - if "identifier" not in reader.fieldnames: + if "identifier" not in (reader.fieldnames or []): continue for row_num, row in enumerate(reader, start=2): @@ -94,13 +99,13 @@ def test_all_csv_identifiers_are_ascii_slugs(self): } ) - except Exception as e: + except Exception as e: # noqa: BLE001 violations.append( { "file": filename, "row": "N/A", "id": "N/A", - "identifier": f"Error reading file: {str(e)}", + "identifier": f"Error reading file: {e!s}", } ) @@ -108,22 +113,27 @@ def test_all_csv_identifiers_are_ascii_slugs(self): # Report violations if violations: - error_lines.append( - "\n\nFound {} resource(s) with invalid identifiers (not ASCII slugs):".format(len(violations)) + error_lines.extend( + ( + f"\n\nFound {len(violations)} resource(s) with invalid identifiers (not ASCII slugs):", + "\nIdentifiers must match pattern: ^[a-z0-9-]+$", + "\nInvalid identifiers found in CSV files:", + ) + ) + + error_lines.extend(" - {file} (row {row}, id={id}): {identifier}".format(**v) for v in violations) + + error_lines.extend( + ( + "\nThese identifiers contain invalid characters and must be normalized.", + "Update the CSV files in data/v2/csv/ to fix these identifiers.", + "\nSuggested fixes:", + " - Remove Unicode apostrophes (') and replace with regular hyphens or remove", + " - Remove Unicode letters (ñ → n)", + " - Remove parentheses and other special characters", + " - Convert to lowercase", + ) ) - error_lines.append("\nIdentifiers must match pattern: ^[a-z0-9-]+$") - error_lines.append("\nInvalid identifiers found in CSV files:") - - for v in violations: - error_lines.append(" - {file} (row {row}, id={id}): {identifier}".format(**v)) - - error_lines.append("\nThese identifiers contain invalid characters and must be normalized.") - error_lines.append("Update the CSV files in data/v2/csv/ to fix these identifiers.") - error_lines.append("\nSuggested fixes:") - error_lines.append(" - Remove Unicode apostrophes (') and replace with regular hyphens or remove") - error_lines.append(" - Remove Unicode letters (ñ → n)") - error_lines.append(" - Remove parentheses and other special characters") - error_lines.append(" - Convert to lowercase") self.fail("\n".join(error_lines)) diff --git a/pokemon_v2/tests.py b/pokemon_v2/tests.py index 753403417..9b525a341 100644 --- a/pokemon_v2/tests.py +++ b/pokemon_v2/tests.py @@ -1,10 +1,10 @@ import json -from datetime import datetime +from datetime import datetime, timezone + from rest_framework import status from rest_framework.test import APITestCase -from pokemon_v2.models import * -# pylint: disable=redefined-builtin +from pokemon_v2.models import * TEST_HOST = "http://testserver" API_V2 = "/api/v2" @@ -598,13 +598,13 @@ def setup_type_data(cls, name="tp", move_damage_class=None, generation=None): generation = generation or cls.setup_generation_data(name="rgn for " + name) - type = Type(name=name, generation=generation, move_damage_class=move_damage_class) - type.save() + type_obj = Type(name=name, generation=generation, move_damage_class=move_damage_class) + type_obj.save() - return type + return type_obj @classmethod - def setup_type_name_data(cls, type, name="tp nm"): + def setup_type_name_data(cls, type, name="tp nm"): # noqa: A002 language = cls.setup_language_data(name="lang for " + name) type_name = TypeName.objects.create(language=language, name=name, type=type) @@ -613,7 +613,7 @@ def setup_type_name_data(cls, type, name="tp nm"): return type_name @classmethod - def setup_type_game_index_data(cls, type, game_index=0): + def setup_type_game_index_data(cls, type, game_index=0): # noqa: A002 generation = cls.setup_generation_data(name="gen for tp gm indx") type_game_index = TypeGameIndex.objects.create(type=type, game_index=game_index, generation=generation) @@ -621,7 +621,7 @@ def setup_type_game_index_data(cls, type, game_index=0): return type_game_index - def setup_type_sprites_data(cls, type): + def setup_type_sprites_data(self, type): # noqa: A002 game_map = { "generation-iii": [ "colosseum", @@ -646,14 +646,14 @@ def setup_type_sprites_data(cls, type): "generation-ix": ["scarlet-violet"], } sprites = {} - for generation in game_map.keys(): - for game in game_map[generation]: + for generation, games in game_map.items(): + for game in games: if generation not in sprites: sprites[generation] = {} - if type.id == 18 and generation.endswith(("-iii", "-iv", "-v")): - sprites[generation][game] = None - elif type.id == 19 and generation.endswith(("-iii", "-iv", "-v", "-vi", "-vii", "-viii")): + if (type.id == 18 and generation.endswith(("-iii", "-iv", "-v"))) or ( + type.id == 19 and generation.endswith(("-iii", "-iv", "-v", "-vi", "-vii", "-viii")) + ): sprites[generation][game] = None else: sprites[generation][game] = { @@ -885,7 +885,7 @@ def setup_move_data( move_damage_class=None, move_effect=None, move_target=None, - type=None, + type=None, # noqa: A002 name="mv", power=20, pp=20, @@ -901,7 +901,7 @@ def setup_move_data( generation = generation or cls.setup_generation_data(name="gen for " + name) - type = type or cls.setup_type_data(name="tp for " + name) + type_obj = type or cls.setup_type_data(name="tp for " + name) move_target = move_target or cls.setup_move_target_data(name="mv trgt for " + name) @@ -910,7 +910,7 @@ def setup_move_data( move = Move.objects.create( name=name, generation=generation, - type=type, + type=type_obj, power=power, pp=pp, accuracy=accuracy, @@ -980,7 +980,7 @@ def setup_move_meta_data( def setup_move_change_data( cls, move=None, - type=None, + type=None, # noqa: A002 move_effect=None, version_group=None, power=20, @@ -1432,10 +1432,10 @@ def setup_pokemon_form_sprites_data( return pokemon_form_sprites @classmethod - def setup_pokemon_form_type_data(cls, pokemon_form, type=None, slot=1): - type = type or cls.setup_type_data(name="tp for pkmn frm") + def setup_pokemon_form_type_data(cls, pokemon_form, type=None, slot=1): # noqa: A002 + type_obj = type or cls.setup_type_data(name="tp for pkmn frm") - form_type = PokemonFormType(pokemon_form=pokemon_form, type=type, slot=slot) + form_type = PokemonFormType(pokemon_form=pokemon_form, type=type_obj, slot=slot) form_type.save() return form_type @@ -1542,19 +1542,19 @@ def setup_pokemon_past_stat_data(cls, pokemon, generation, base_stat=10, effort= return pokemon_stat_past @classmethod - def setup_pokemon_type_data(cls, pokemon, type=None, slot=1): - type = type or cls.setup_type_data(name="tp for pkmn") + def setup_pokemon_type_data(cls, pokemon, type=None, slot=1): # noqa: A002 + type_obj = type or cls.setup_type_data(name="tp for pkmn") - pokemon_type = PokemonType(pokemon=pokemon, type=type, slot=slot) + pokemon_type = PokemonType(pokemon=pokemon, type=type_obj, slot=slot) pokemon_type.save() return pokemon_type @classmethod - def setup_pokemon_past_type_data(cls, pokemon, generation, type=None, slot=1): - type = type or cls.setup_type_data(name="tp for pkmn") + def setup_pokemon_past_type_data(cls, pokemon, generation, type=None, slot=1): # noqa: A002 + type_obj = type or cls.setup_type_data(name="tp for pkmn") - pokemon_type_past = PokemonTypePast(pokemon=pokemon, generation=generation, type=type, slot=slot) + pokemon_type_past = PokemonTypePast(pokemon=pokemon, generation=generation, type=type_obj, slot=slot) pokemon_type_past.save() return pokemon_type_past @@ -2026,7 +2026,7 @@ def test_generation_api(self): ability = self.setup_ability_data(name="ablty for base gen", generation=generation) move = self.setup_move_data(name="mv for base gen", generation=generation) pokemon_species = self.setup_pokemon_species_data(name="pkmn spcs for base gen", generation=generation) - type = self.setup_type_data(name="tp for base gen", generation=generation) + type_obj = self.setup_type_data(name="tp for base gen", generation=generation) version_group = self.setup_version_group_data(name="ver grp for base gen", generation=generation) response = self.client.get("{}/generation/{}/".format(API_V2, generation.pk)) @@ -2060,10 +2060,10 @@ def test_generation_api(self): "{}{}/move/{}/".format(TEST_HOST, API_V2, move.pk), ) # type params - self.assertEqual(response.data["types"][0]["name"], type.name) + self.assertEqual(response.data["types"][0]["name"], type_obj.name) self.assertEqual( response.data["types"][0]["url"], - "{}{}/type/{}/".format(TEST_HOST, API_V2, type.pk), + "{}{}/type/{}/".format(TEST_HOST, API_V2, type_obj.pk), ) # species params self.assertEqual(response.data["pokemon_species"][0]["name"], pokemon_species.name) @@ -2604,8 +2604,8 @@ def test_berry_flavor_api(self): ) def test_berry_api(self): - type = self.setup_type_data(name="tp fr base bry") - berry = self.setup_berry_data(name="base bry", natural_gift_type=type) + type_obj = self.setup_type_data(name="tp fr base bry") + berry = self.setup_berry_data(name="base bry", natural_gift_type=type_obj) berry_flavor = self.setup_berry_flavor_data(name="bry flvr for base bry") berry_flavor_map = self.setup_berry_flavor_map_data(berry=berry, berry_flavor=berry_flavor) @@ -2640,10 +2640,10 @@ def test_berry_api(self): "{}{}/berry-flavor/{}/".format(TEST_HOST, API_V2, berry_flavor.pk), ) # natural gift type - self.assertEqual(response.data["natural_gift_type"]["name"], type.name) + self.assertEqual(response.data["natural_gift_type"]["name"], type_obj.name) self.assertEqual( response.data["natural_gift_type"]["url"], - "{}{}/type/{}/".format(TEST_HOST, API_V2, type.pk), + "{}{}/type/{}/".format(TEST_HOST, API_V2, type_obj.pk), ) # Growth Rate Tests @@ -3166,6 +3166,7 @@ def test_type_api(self): past_damage_relations = response.data["past_damage_relations"] gen_data = past_damage_relations[0]["generation"] self.assertEqual(gen_data["name"], generation.name) + assert past_no_damage_to_relation.generation is not None self.assertEqual( gen_data["url"], "{}{}/generation/{}/".format(TEST_HOST, API_V2, past_no_damage_to_relation.generation.pk), @@ -3229,7 +3230,7 @@ def test_type_api(self): sprites_data = json.loads(type_sprites.sprites) - for generation in game_map.keys(): + for generation in game_map: for game in game_map[generation]: self.assertEqual( json.loads(response.data["sprites"])[generation][game]["name_icon"], @@ -4282,7 +4283,7 @@ def test_pokemon_api(self): # assert that we only got one move record back. pokemon_move = self.setup_move_data(name="mv for pkmn") pokemon_moves = [] - for move in range(0, 4): + for move in range(4): version_group = self.setup_version_group_data(name="ver grp " + str(move) + " for pkmn") new_move = self.setup_pokemon_move_data( pokemon=pokemon, @@ -4323,6 +4324,10 @@ def test_pokemon_api(self): self.assertEqual(response.data["height"], pokemon.height) self.assertEqual(response.data["weight"], pokemon.weight) self.assertEqual(response.data["base_experience"], pokemon.base_experience) + self.assertEqual( + response.data["location_area_encounters"], + "{}{}/pokemon/{}/encounters".format(TEST_HOST, API_V2, pokemon.pk), + ) # species params self.assertEqual(response.data["species"]["name"], pokemon_species.name) self.assertEqual( @@ -4453,23 +4458,23 @@ def test_pokemon_api(self): for i, val in enumerate(pokemon_moves): # pylint: disable=unused-variable version = response.data["moves"][0]["version_group_details"][i] # Learn Level - expected = pokemon_moves[i].level + expected = val.level actual = version["level_learned_at"] self.assertEqual(expected, actual) # Version Group Name - expected = pokemon_moves[i].version_group.name + expected = val.version_group.name actual = version["version_group"]["name"] self.assertEqual(expected, actual) # Version Group URL - expected = "{}{}/version-group/{}/".format(TEST_HOST, API_V2, pokemon_moves[i].version_group.pk) + expected = "{}{}/version-group/{}/".format(TEST_HOST, API_V2, val.version_group.pk) actual = version["version_group"]["url"] self.assertEqual(expected, actual) # Learn Method Name - expected = pokemon_moves[i].move_learn_method.name + expected = val.move_learn_method.name actual = version["move_learn_method"]["name"] self.assertEqual(expected, actual) # Learn Method URL - expected = "{}{}/move-learn-method/{}/".format(TEST_HOST, API_V2, pokemon_moves[i].move_learn_method.pk) + expected = "{}{}/move-learn-method/{}/".format(TEST_HOST, API_V2, val.move_learn_method.pk) actual = version["move_learn_method"]["url"] self.assertEqual(expected, actual) # game indices params @@ -4495,7 +4500,7 @@ def test_pokemon_api(self): sprites_data = json.loads(pokemon_sprites.sprites) cries_data = json.loads(pokemon_cries.cries) response_sprites_data = json.loads(response.data["sprites"]) - response_cries_data = json.loads(response.data["cries"]) + json.loads(response.data["cries"]) # sprite params self.assertEqual( @@ -4853,28 +4858,28 @@ def test_evolution_chain_api_wurmple_bugfix(self): evolves_from_species=basic, evolution_chain=evolution_chain, ) - stage_one_first_evolution = self.setup_pokemon_evolution_data(evolved_species=stage_one_first, min_level=7) + self.setup_pokemon_evolution_data(evolved_species=stage_one_first, min_level=7) stage_two_first = self.setup_pokemon_species_data( name="beautifly", evolves_from_species=stage_one_first, evolution_chain=evolution_chain, ) - stage_two_first_evolution = self.setup_pokemon_evolution_data(evolved_species=stage_two_first, min_level=10) + self.setup_pokemon_evolution_data(evolved_species=stage_two_first, min_level=10) stage_one_second = self.setup_pokemon_species_data( name="cascoon", evolves_from_species=basic, evolution_chain=evolution_chain, ) - stage_one_second_evolution = self.setup_pokemon_evolution_data(evolved_species=stage_one_second, min_level=7) + self.setup_pokemon_evolution_data(evolved_species=stage_one_second, min_level=7) stage_two_second = self.setup_pokemon_species_data( name="dustox", evolves_from_species=stage_one_second, evolution_chain=evolution_chain, ) - stage_two_second_evolution = self.setup_pokemon_evolution_data(evolved_species=stage_two_second, min_level=10) + self.setup_pokemon_evolution_data(evolved_species=stage_two_second, min_level=10) response = self.client.get("{}/evolution-chain/{}/".format(API_V2, evolution_chain.pk)) @@ -5037,24 +5042,24 @@ def test_case_insensitive_api(self): # Set up pokemon data pokemon_species = self.setup_pokemon_species_data(name="pkmn spcs for base pkmn") pokemon = self.setup_pokemon_data(pokemon_species=pokemon_species, name="base pkm") - pokemon_form = self.setup_pokemon_form_data(pokemon=pokemon, name="pkm form for base pkmn") + self.setup_pokemon_form_data(pokemon=pokemon, name="pkm form for base pkmn") generation = self.setup_generation_data(name="base gen") - pokemon_ability = self.setup_pokemon_ability_data(pokemon=pokemon) - pokemon_past_ability = self.setup_pokemon_past_ability_data(pokemon=pokemon, generation=generation) - pokemon_stat = self.setup_pokemon_stat_data(pokemon=pokemon) - pokemon_past_stat = self.setup_pokemon_past_stat_data(pokemon=pokemon, generation=generation) - pokemon_type = self.setup_pokemon_type_data(pokemon=pokemon) - pokemon_past_type = self.setup_pokemon_past_type_data(pokemon=pokemon, generation=generation) - pokemon_item = self.setup_pokemon_item_data(pokemon=pokemon) - pokemon_sprites = self.setup_pokemon_sprites_data(pokemon=pokemon) - pokemon_cries = self.setup_pokemon_cries_data(pokemon, latest=True, legacy=True) - pokemon_game_index = self.setup_pokemon_game_index_data(pokemon=pokemon, game_index=10) + self.setup_pokemon_ability_data(pokemon=pokemon) + self.setup_pokemon_past_ability_data(pokemon=pokemon, generation=generation) + self.setup_pokemon_stat_data(pokemon=pokemon) + self.setup_pokemon_past_stat_data(pokemon=pokemon, generation=generation) + self.setup_pokemon_type_data(pokemon=pokemon) + self.setup_pokemon_past_type_data(pokemon=pokemon, generation=generation) + self.setup_pokemon_item_data(pokemon=pokemon) + self.setup_pokemon_sprites_data(pokemon=pokemon) + self.setup_pokemon_cries_data(pokemon, latest=True, legacy=True) + self.setup_pokemon_game_index_data(pokemon=pokemon, game_index=10) # To test issue #85, we will create one move that has multiple # learn levels in different version groups. Later, we'll # assert that we only got one move record back. pokemon_move = self.setup_move_data(name="mv for pkmn") pokemon_moves = [] - for move in range(0, 4): + for move in range(4): version_group = self.setup_version_group_data(name="ver grp " + str(move) + " for pkmn") new_move = self.setup_pokemon_move_data( pokemon=pokemon, @@ -5105,7 +5110,7 @@ def test_case_insensitive_api(self): # Same test with /language endpoint language = self.setup_language_data(name="base-lang") - language_name = self.setup_language_name_data(language, name="base-lang-name") + self.setup_language_name_data(language, name="base-lang-name") lowercase_name = language.name.lower() uppercase_name = language.name.upper() @@ -5128,7 +5133,7 @@ def test_case_insensitive_api(self): def test_meta_api(self): response = self.client.get("{}/meta/".format(API_V2)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(datetime.fromtimestamp(int(response.data["deploy_date"]))) + self.assertTrue(datetime.fromtimestamp(int(response.data["deploy_date"]), tz=timezone.utc)) self.assertEqual(10, len(response.data["deploy_date"])) self.assertEqual(40, len(response.data["hash"])) self.assertIn("tag", response.data) diff --git a/pokemon_v2/urls.py b/pokemon_v2/urls.py index 564dab604..05034625b 100644 --- a/pokemon_v2/urls.py +++ b/pokemon_v2/urls.py @@ -1,3 +1,4 @@ +# ruff: noqa: F405 ##################################### # # V2 API setup using Django Rest @@ -8,11 +9,12 @@ from typing import TYPE_CHECKING, Any -from django.urls import include, path, re_path +from django.urls import URLPattern, URLResolver, include, path, re_path from rest_framework import routers from rest_framework.reverse import reverse as drf_reverse +from typing_extensions import override -from pokemon_v2.api import * +from pokemon_v2.api import * # noqa: F403 if TYPE_CHECKING: from rest_framework.request import Request @@ -20,6 +22,7 @@ class PokeAPIRootView(routers.APIRootView): + @override def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: response = super().get(request, *args, **kwargs) response.data["meta"] = drf_reverse("meta", request=request) @@ -91,7 +94,7 @@ class PokeAPIRouter(routers.DefaultRouter): # ########################### -urlpatterns = [ +urlpatterns: list[URLPattern | URLResolver] = [ path("api/v2/meta/", PokeapiMetaView.as_view(), name="meta"), path("api/v2/", include(router.urls)), re_path( diff --git a/pyproject.toml b/pyproject.toml index 117445cf9..3918e6630 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,15 @@ dependencies = [ "gunicorn==23.0.0", "legacy-cgi>=2.6.4 ; python_full_version >= '3.13'", "psycopg[binary]==3.3.2", + "typing-extensions>=4.15.0", ] [dependency-groups] dev = [ "coverage==7.13.1", + "django-types>=0.24.0", + "djangorestframework-stubs>=3.17.1", "pre-commit>=4.6.0", "ruff>=0.15.21", + "ty>=0.0.65", ] diff --git a/ruff.toml b/ruff.toml index 4e6fb552a..0f30d9c3c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -4,3 +4,38 @@ exclude = [ "data/v2/cries", "data/v2/sprites", ] + +[lint] +extend-select = [ + "A", # flake8-builtins: shadowing builtins + "ANN", # flake8-annotations: missing type annotations + "B", # flake8-bugbear: likely bugs + "C4", # flake8-comprehensions: simplify comprehensions + "DJ", # flake8-django: django best practices + "E", # pycodestyle: PEP 8 errors + "ERA", # eradicate: commented-out code + "FURB", # refurb: modernize and simplify + "I", # isort: import sorting + "N", # pep8-naming: naming conventions + "PERF", # Perflint: performance anti-patterns + "PGH003", # pygrep-hooks: blanket type: ignore + "PYI", # flake8-pyi: stub file best practices + "RUF", # Ruff-specific: assorted rules + "SIM", # flake8-simplify: simplify expressions + "TC", # flake8-type-checking: TYPE_CHECKING imports + "TID", # flake8-tidy-imports: tidy imports + "F", # Pyflakes: core Python error detection + "UP", # pyupgrade: modern Python syntax + "W", # pycodestyle: PEP 8 warnings +] +preview = true + +[lint.per-file-ignores] +# TODO: Test files +"pokemon_v2/tests.py" = ["ANN", "UP032", "F403", "F405"] +"pokemon_v2/test_models.py" = ["ANN", "F403", "F405"] +# TODO: Data loading scripts +"data/v2/build.py" = ["ANN", "ERA001", "F403", "F405"] +"data/v2/__init__.py" = ["F403", "F405"] +# Django migration generated code +"pokemon_v2/migrations/*.py" = ["RUF012", "ERA001"] diff --git a/ty.toml b/ty.toml new file mode 100644 index 000000000..860da59c7 --- /dev/null +++ b/ty.toml @@ -0,0 +1,16 @@ +[src] +include = [ + "config", + "data/v2/*.py", + "pokemon_v2", +] + +[rules] +blanket-ignore-comment = "error" +missing-override-decorator = "error" +missing-type-argument = "error" + +division-by-zero = "warn" +possibly-missing-attribute = "warn" +possibly-missing-import = "warn" +possibly-unresolved-reference = "warn" diff --git a/uv.lock b/uv.lock index bb5135060..14efaac51 100644 --- a/uv.lock +++ b/uv.lock @@ -206,6 +206,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/79/055dfcc508cfe9f439d9f453741188d633efa9eab90fc78a67b0ab50b137/django_redis-6.0.0-py3-none-any.whl", hash = "sha256:20bf0063a8abee567eb5f77f375143c32810c8700c0674ced34737f8de4e36c0", size = 33687, upload-time = "2025-06-17T18:15:34.165Z" }, ] +[[package]] +name = "django-stubs" +version = "6.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-stubs-ext" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/90/087c6e424e705e05182e543ef6b366a59eb5c92ab008b0dbbba55f357a40/django_stubs-6.0.7.tar.gz", hash = "sha256:bc55431c0af745a64e39cf33a8d36c87dccbedeae2fe26fab47dd355270e8538", size = 282293, upload-time = "2026-07-14T10:08:27.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/86/230ae6056221b543d63f7710d73967fe1c6840693540e99ea3c54f45859c/django_stubs-6.0.7-py3-none-any.whl", hash = "sha256:7ed9a14c438e589272ca04e966dee82a4d1ff7ca5c2171bc986c50a0d03ec35b", size = 547460, upload-time = "2026-07-14T10:08:25.626Z" }, +] + +[[package]] +name = "django-stubs-ext" +version = "6.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/50/917f7224ea470e89cdcdc93d3dfe75b8391adf976cf12f2ecdb5f5d122be/django_stubs_ext-6.0.7.tar.gz", hash = "sha256:c3172c5126614fd2a44d0196b313b44c21f717cb09477ba52b447d41f4ce613e", size = 6665, upload-time = "2026-07-14T10:07:56.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/65/4d73fce956b5ebf26449259664360e539fbe95f0e86be396c28b636ba72a/django_stubs_ext-6.0.7-py3-none-any.whl", hash = "sha256:53a9c7c5a7c7e718cc6308cfce1e7470f2cac0b9d38dbcd60fbfa82704f1d592", size = 10362, upload-time = "2026-07-14T10:07:55.653Z" }, +] + +[[package]] +name = "django-types" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-psycopg2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/7b/8e05b8631fa7de84038d4f24fa57e983107aff889a6d88a9c40a21e15d1c/django_types-0.24.0.tar.gz", hash = "sha256:af903de8b9ee963b7594459a7a20cb8eaaab176ae2b3244ecaa089e0c570b0d1", size = 208426, upload-time = "2026-04-22T22:19:01.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/ab/f5c37ecc08c396df62579246597796697b58253c8b30206870e8d0f644d0/django_types-0.24.0-py3-none-any.whl", hash = "sha256:ddb478ca733e0dde5475118dd59ab340156980f9659fd92de2083326ae96100a", size = 379436, upload-time = "2026-04-22T22:19:03.368Z" }, +] + [[package]] name = "djangorestframework" version = "3.16.1" @@ -218,6 +259,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" }, ] +[[package]] +name = "djangorestframework-stubs" +version = "3.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django-stubs" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/4b/1bee065c6c87ee04b905732deb53acdfc43d0e5673f43a5f75ec4deb7f6f/djangorestframework_stubs-3.17.1.tar.gz", hash = "sha256:5b67655090e2976b778bad572840b732a87ab2911402530d0dca2fbf72f891c5", size = 33470, upload-time = "2026-07-28T21:27:52.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/38/6a04ffca56e24220bf37494fe9f75ce9abe8115776a90c924732bb37823a/djangorestframework_stubs-3.17.1-py3-none-any.whl", hash = "sha256:40f1f22ec1965d1caff17c66794b87799eb8f42e0628ea46b501dcd565d8a803", size = 57562, upload-time = "2026-07-28T21:27:50.58Z" }, +] + [[package]] name = "drf-spectacular" version = "0.29.0" @@ -352,13 +407,17 @@ dependencies = [ { name = "gunicorn" }, { name = "legacy-cgi", marker = "python_full_version >= '3.13'" }, { name = "psycopg", extra = ["binary"] }, + { name = "typing-extensions" }, ] [package.dev-dependencies] dev = [ { name = "coverage" }, + { name = "django-types" }, + { name = "djangorestframework-stubs" }, { name = "pre-commit" }, { name = "ruff" }, + { name = "ty" }, ] [package.metadata] @@ -372,13 +431,17 @@ requires-dist = [ { name = "gunicorn", specifier = "==23.0.0" }, { name = "legacy-cgi", marker = "python_full_version >= '3.13'", specifier = ">=2.6.4" }, { name = "psycopg", extras = ["binary"], specifier = "==3.3.2" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, ] [package.metadata.requires-dev] dev = [ { name = "coverage", specifier = "==7.13.1" }, + { name = "django-types", specifier = ">=0.24.0" }, + { name = "djangorestframework-stubs", specifier = ">=3.17.1" }, { name = "pre-commit", specifier = ">=4.6.0" }, { name = "ruff", specifier = ">=0.15.21" }, + { name = "ty", specifier = ">=0.0.65" }, ] [[package]] @@ -852,6 +915,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "ty" +version = "0.0.65" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cf561927e8e9ab5c1892a833b664aa9cd6f051a75f6280c66d8047246bda/ty-0.0.65.tar.gz", hash = "sha256:b7134bffcc00b715fa8291e84d845782ced810a998dc1f7f11d71c85c4046325", size = 6460098, upload-time = "2026-07-29T18:31:03.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/4e/71e2d325d2b53a1afad81624ad076b2ede413213fc4a18cb05b78c568571/ty-0.0.65-py3-none-linux_armv6l.whl", hash = "sha256:dc556c9f05408bef4c4ef02b2cc382e4e5f797b4b20d64410289848f0d76705f", size = 12298466, upload-time = "2026-07-29T18:30:12.744Z" }, + { url = "https://files.pythonhosted.org/packages/57/77/fec8f29647c55794efa430a7f365e44f5ce7ffb6459d9445a87fac569bec/ty-0.0.65-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d2e0d34cc0a28a17ef0cf81135c5ebabc3562131f9079138ba5e7bae0f56bd", size = 11942421, upload-time = "2026-07-29T18:30:16.076Z" }, + { url = "https://files.pythonhosted.org/packages/13/09/7f3766aef9dc627e2698cf4e3e59cf53389dcae3812040d33c1aa931230f/ty-0.0.65-py3-none-macosx_11_0_arm64.whl", hash = "sha256:685f49a9312bbf69d5b65bbb66384fed1f927403ea030c217b9289092d7e46c4", size = 11451922, upload-time = "2026-07-29T18:30:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/1a77cd50e0befb50f55b8bf9bd3ed3eddf184bf28c61b56727039e0774fc/ty-0.0.65-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f564b5ebe78e2f3a8e7b8eacb1292eb88b7c0f3c8630671cfca31abc0709cd9", size = 11994999, upload-time = "2026-07-29T18:30:22.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/7b/feda16f3a4a0a99be27431e0c9598eeeec0db1eb2fec9a15976698209418/ty-0.0.65-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c983e156fe9e113fb56389e13d327b6b8549fe866de9b269684723a88e9b732d", size = 12090662, upload-time = "2026-07-29T18:30:24.93Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3e/3f69bf9c9307dbdc0771719f65ce5b556e7bdeeaccbdd599d4f57866d801/ty-0.0.65-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3663b7396e8b1a9954e20e732de7ccb0192bf4118473069b4945920d6923921", size = 12822094, upload-time = "2026-07-29T18:30:28.012Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/8fa791b3bb503ee2b46ad81690cd1bdd54519582df6d805cee57fe143e85/ty-0.0.65-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306ed01f29d6e108e98feb233dbbf5878a027603b71bd3743b343977933a9f16", size = 13357833, upload-time = "2026-07-29T18:30:31.122Z" }, + { url = "https://files.pythonhosted.org/packages/c1/73/4dda396a201e1dd0ed3594a9b48e559cb41c4bc048c6cd4c4d1b39eb4313/ty-0.0.65-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28bcfc8898c94f079a9100e684bcf312b6a64ad3a7d4ebb35a4591546030a2cd", size = 12977303, upload-time = "2026-07-29T18:30:33.944Z" }, + { url = "https://files.pythonhosted.org/packages/a5/26/c250c2c569adc53a8591716641388397bcb2a442e4a30b952ae81b50c0e0/ty-0.0.65-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a75bd0c245c38802a8f488378e74f92feb7dd33db7d63fbdd6fdf82791ba730", size = 12579338, upload-time = "2026-07-29T18:30:37.199Z" }, + { url = "https://files.pythonhosted.org/packages/d3/94/4a5647d44753ca218fc930d7e4d9bf468d0ed4a0ad4b3d57588bc1bbacf7/ty-0.0.65-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9e5e1bdea9662d2b5312b4e99f319f4e6e2ea427511b5fbc546141b79ec53f76", size = 12957731, upload-time = "2026-07-29T18:30:39.937Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/1e22fa11a1e0dfb20b1c7f3cbfd8170273aada2a82f9ecd3055275370c44/ty-0.0.65-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:03a88493d4842889f65280ae241e06b399d57eb3c63571054cad21a4c33b3b69", size = 11938625, upload-time = "2026-07-29T18:30:42.603Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0a/fe5f22ef62b193201bc5566762e22049762cd485bfafb5095a7050760054/ty-0.0.65-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:600b8bf6f4940cf7ffb2f43d3716faaf38dcb97cd8617c55771451bc0276408f", size = 12105592, upload-time = "2026-07-29T18:30:45.419Z" }, + { url = "https://files.pythonhosted.org/packages/76/fd/922b3a6e9d697452cdbb4b7e3f636868add5ec652154518a736e4364f3b7/ty-0.0.65-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c28007bc79d648c1ddaf1e65885d07baec48eb87240da442f608e4107c1b7d8", size = 12387335, upload-time = "2026-07-29T18:30:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/77/22/a1a08ebc84c083db2fb55e3b5cd186db0c067692f4921146f601360231e2/ty-0.0.65-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c852da96091ad22361e6586b7c7ba98e1334dcd4d8ffb67e47f4fb673de33f77", size = 12682710, upload-time = "2026-07-29T18:30:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/eaaa410a25bbdea19722109b5422380a0e211b3afcf3071d15953ddbd5db/ty-0.0.65-py3-none-win32.whl", hash = "sha256:cf529d538f1403b14b0511e6ec3cdb95d3d974adabf24cc76cedc533368c3edc", size = 11692341, upload-time = "2026-07-29T18:30:54.35Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0f/6d48f206dce9d7e53fe3b5ea0f0ab5800dd9d2365b2b48f736783436c43f/ty-0.0.65-py3-none-win_amd64.whl", hash = "sha256:234a321e33c7cbbfbd67bfa0b01b685dd9c21f1841781a21e5ca1fa0b25f1d5d", size = 12729355, upload-time = "2026-07-29T18:30:57.275Z" }, + { url = "https://files.pythonhosted.org/packages/96/aa/7446f7725e303cf78e058c893af1f0552b9451895454908706f4c6c3494b/ty-0.0.65-py3-none-win_arm64.whl", hash = "sha256:b9424be1ec56d93ff18609fb1c0a0a2283fe1282cd6d1c7604f97d73b94d61f2", size = 12051375, upload-time = "2026-07-29T18:31:00.579Z" }, +] + +[[package]] +name = "types-psycopg2" +version = "2.9.21.20260724" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/29/09ca8f0ad16105193deabcaf8256059e361e9e37ff24551efd9c739e240b/types_psycopg2-2.9.21.20260724.tar.gz", hash = "sha256:db31031c37de823a2b21c787cb84832174fdda2a99a8b1af648206f19cc32f8c", size = 27644, upload-time = "2026-07-24T04:57:36.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/90/1e77eb76030bb5feb249ecb41cd7647c6ae831ec7bd2b61a61beecac4029/types_psycopg2-2.9.21.20260724-py3-none-any.whl", hash = "sha256:4f87890dd06cea99e3bb0536330ffc2a7320e69059dc1ee0e2c884ef2718f2b4", size = 24964, upload-time = "2026-07-24T04:57:35.703Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260724" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/6f/a28f44bcd56bebed42b028a2894c79853e2f5e6b5279e633cb3f287a05e7/types_pyyaml-6.0.12.20260724.tar.gz", hash = "sha256:3c1ce1bb73cd5ec02e90390c2b1f00e810d241d8825fd73ff359696839271b6b", size = 17893, upload-time = "2026-07-24T04:58:43.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/42/0337fefc615e20ee55d1c8f71b774a9b2b734a04669139c20753b27a2a3a/types_pyyaml-6.0.12.20260724-py3-none-any.whl", hash = "sha256:d57db930a4b2efbc57cf430ec8882765d246929432fa253092f383902329a453", size = 20312, upload-time = "2026-07-24T04:58:42.486Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"