Skip to content

Commit

Permalink
Merge pull request #6 from #1-introduces-pokemon-fetch-by-type
Browse files Browse the repository at this point in the history
pokedex#1: "introduces pokemon fetch by type"
  • Loading branch information
withtwoemms authored May 27, 2023
2 parents 1902c4d + f99f18b commit 02ea46e
Show file tree
Hide file tree
Showing 18 changed files with 16,381 additions and 12 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# pokedex
a tool for sourcing info about different Pokémon
a tool for sourcing info about different Pokémon from the [PokéAPI](https://pokeapi.co/)


### Setup
Expand All @@ -16,3 +16,13 @@ Once installed, scripts can executed using the `run` subcommand:
```
poetry run <script>
```


### Usage

There exists a `poetry` buildscript called `get-pokemon`.
It can be invoked like so:

```
poetry run get-pokemon
```
83 changes: 73 additions & 10 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added pokedex/api/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions pokedex/api/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import json
import sys
from argparse import ArgumentParser

from pokedex.api.client import get_pokemon_by_type


def go(args=sys.argv):
parser = ArgumentParser(prog="get-pokemon", description="Catch 'em all")
parser.add_argument("--by", choices=["type"], help="get pokemon by type")

any_args_given = len(sys.argv) > 1

if not any_args_given:
parser.print_help(sys.stderr)
sys.exit(1)

args = parser.parse_args()
if args.by:
result = list(get_pokemon_by_type("fairy"))
output = json.dumps(result, indent=4)
print(output)
61 changes: 61 additions & 0 deletions pokedex/api/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Any, Dict, Generator, Iterable, List, Optional, Union

import requests

from pokedex.api.constants import BASE_URL
from pokedex.api.models import PokeApiRequest, PokeApiResource, PokeApiResourceRef, PokemonRef

PokeApiEndpoints = Dict[str, str]
PokemonTypeRefs = List[PokeApiResourceRef]
PokemonTypes = Dict[str, Union[Optional[str], List[Dict[str, str]]]]
Pokemon = Dict[str, Any]


def get_endpoints(endpoints_request: PokeApiRequest) -> PokeApiEndpoints:
response: requests.Response = endpoints_request()
response.raise_for_status() # TODO: handle error states
endpoints: PokeApiEndpoints = response.json()
return endpoints


def select_endpoint(name: str, endpoints: PokeApiEndpoints) -> PokeApiRequest:
return PokeApiRequest(endpoints[name])


def get_pokemon_type_refs(request: PokeApiRequest) -> PokemonTypeRefs:
response: requests.Response = request()
response.raise_for_status() # TODO: handle error states
pokemon_type_resource = PokeApiResource(**response.json())
pokemon_type_refs: List[PokeApiResourceRef] = pokemon_type_resource.results
return pokemon_type_refs


def select_pokemon_type(pokemon_type: str, pokemon_type_refs: PokemonTypeRefs) -> Optional[PokeApiRequest]:
for pokemon_type_ref in pokemon_type_refs:
if pokemon_type_ref.name == pokemon_type:
return pokemon_type_ref.as_request()


def get_pokemon_refs(pokemon_type_request: PokeApiRequest) -> Generator[PokemonRef, None, None]:
response: requests.Response = pokemon_type_request()
response.raise_for_status() # TODO: handle error states
pokemon_refs = response.json()["pokemon"]
for pokemon_ref in pokemon_refs:
yield PokemonRef(**pokemon_ref).as_request()


# TODO: make concurrent
def get_pokemon(pokemon_requests: Iterable[PokeApiRequest]) -> Generator[Pokemon, None, None]:
for pokemon_request in pokemon_requests:
response: requests.Response = pokemon_request()
response.raise_for_status() # TODO: handle error states
yield response.json()


def get_pokemon_by_type(pokemon_type: str):
endpoints = get_endpoints(PokeApiRequest(BASE_URL))
endpoint = select_endpoint("type", endpoints)
pokemon_type_refs = get_pokemon_type_refs(endpoint)
pokemon_refs_request = select_pokemon_type(pokemon_type, pokemon_type_refs)
pokemon_refs = get_pokemon_refs(pokemon_refs_request)
yield from get_pokemon(pokemon_refs)
1 change: 1 addition & 0 deletions pokedex/api/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BASE_URL = "https://pokeapi.co/api/v2/"
38 changes: 38 additions & 0 deletions pokedex/api/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

import requests
from pydantic import BaseModel

ApiResponseType = Dict[str, Any]


@dataclass(frozen=True)
class PokeApiRequest:
url: str

def __call__(self) -> ApiResponseType:
return requests.get(self.url)


class PokeApiResourceRef(BaseModel):
name: str
url: str

def as_request(self) -> PokeApiRequest:
return PokeApiRequest(self.url)


class PokemonRef(BaseModel):
pokemon: PokeApiResourceRef
slot: int

def as_request(self) -> PokeApiRequest:
return self.pokemon.as_request()


class PokeApiResource(BaseModel):
count: int
next: Optional[str]
previous: Optional[str]
results: List[PokeApiResourceRef]
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ authors = ["withtwoemms <withthoemms@gmail.com>"]
[tool.poetry.dependencies]
python = "^3.8.1"
requests = "2.31.0"
urllib3 = "<=1.26.16"
pydantic = "^1.10.8"

[tool.poetry.dev-dependencies]
black = "23.3.0"
Expand All @@ -18,6 +20,7 @@ pre-commit = "3.3.2"
tests = "scripts:tests"
lint = "scripts:lint"
check = "scripts:check"
get-pokemon = "pokedex.api.__main__:go"

[build-system]
requires = ["poetry-core==1.5.0"]
Expand Down
2 changes: 1 addition & 1 deletion scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


def tests():
subprocess.run(["python", "-u", "-m", "unittest", "discover", "tests/"])
subprocess.run(["python", "-u", "-m", "unittest", "discover", "-t", "."])


def lint():
Expand Down
Empty file added tests/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pathlib import Path
from typing import Dict

from requests.models import Response

parentdir = Path(__file__).parent

resource_paths: Dict[str, Path] = {
"endpoints.response": parentdir / "endpoints.response.json",
"pokemon.types.response": parentdir / "pokemon.types.response.json",
"pokemon.refs.fairy.response": parentdir / "pokemon.refs.fairy.response.json",
"jigglypuff.response": parentdir / "jigglypuff.response.json",
}


def resource(name: str):
return resource_paths[name].open("rb").read()


def craft_response(contents: str, status_code: int):
response = Response()
response._content = contents
response.status_code = status_code
return response
50 changes: 50 additions & 0 deletions tests/fixtures/endpoints.response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"ability": "https://pokeapi.co/api/v2/ability/",
"berry": "https://pokeapi.co/api/v2/berry/",
"berry-firmness": "https://pokeapi.co/api/v2/berry-firmness/",
"berry-flavor": "https://pokeapi.co/api/v2/berry-flavor/",
"characteristic": "https://pokeapi.co/api/v2/characteristic/",
"contest-effect": "https://pokeapi.co/api/v2/contest-effect/",
"contest-type": "https://pokeapi.co/api/v2/contest-type/",
"egg-group": "https://pokeapi.co/api/v2/egg-group/",
"encounter-condition": "https://pokeapi.co/api/v2/encounter-condition/",
"encounter-condition-value": "https://pokeapi.co/api/v2/encounter-condition-value/",
"encounter-method": "https://pokeapi.co/api/v2/encounter-method/",
"evolution-chain": "https://pokeapi.co/api/v2/evolution-chain/",
"evolution-trigger": "https://pokeapi.co/api/v2/evolution-trigger/",
"gender": "https://pokeapi.co/api/v2/gender/",
"generation": "https://pokeapi.co/api/v2/generation/",
"growth-rate": "https://pokeapi.co/api/v2/growth-rate/",
"item": "https://pokeapi.co/api/v2/item/",
"item-attribute": "https://pokeapi.co/api/v2/item-attribute/",
"item-category": "https://pokeapi.co/api/v2/item-category/",
"item-fling-effect": "https://pokeapi.co/api/v2/item-fling-effect/",
"item-pocket": "https://pokeapi.co/api/v2/item-pocket/",
"language": "https://pokeapi.co/api/v2/language/",
"location": "https://pokeapi.co/api/v2/location/",
"location-area": "https://pokeapi.co/api/v2/location-area/",
"machine": "https://pokeapi.co/api/v2/machine/",
"move": "https://pokeapi.co/api/v2/move/",
"move-ailment": "https://pokeapi.co/api/v2/move-ailment/",
"move-battle-style": "https://pokeapi.co/api/v2/move-battle-style/",
"move-category": "https://pokeapi.co/api/v2/move-category/",
"move-damage-class": "https://pokeapi.co/api/v2/move-damage-class/",
"move-learn-method": "https://pokeapi.co/api/v2/move-learn-method/",
"move-target": "https://pokeapi.co/api/v2/move-target/",
"nature": "https://pokeapi.co/api/v2/nature/",
"pal-park-area": "https://pokeapi.co/api/v2/pal-park-area/",
"pokeathlon-stat": "https://pokeapi.co/api/v2/pokeathlon-stat/",
"pokedex": "https://pokeapi.co/api/v2/pokedex/",
"pokemon": "https://pokeapi.co/api/v2/pokemon/",
"pokemon-color": "https://pokeapi.co/api/v2/pokemon-color/",
"pokemon-form": "https://pokeapi.co/api/v2/pokemon-form/",
"pokemon-habitat": "https://pokeapi.co/api/v2/pokemon-habitat/",
"pokemon-shape": "https://pokeapi.co/api/v2/pokemon-shape/",
"pokemon-species": "https://pokeapi.co/api/v2/pokemon-species/",
"region": "https://pokeapi.co/api/v2/region/",
"stat": "https://pokeapi.co/api/v2/stat/",
"super-contest-effect": "https://pokeapi.co/api/v2/super-contest-effect/",
"type": "https://pokeapi.co/api/v2/type/",
"version": "https://pokeapi.co/api/v2/version/",
"version-group": "https://pokeapi.co/api/v2/version-group/"
}
Loading

0 comments on commit 02ea46e

Please sign in to comment.