Skip to content

Commit

Permalink
makes use of centrally-located DeferredRequest protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
withtwoemms committed Jan 4, 2024
1 parent 71bc806 commit 20af921
Show file tree
Hide file tree
Showing 11 changed files with 56 additions and 67 deletions.
29 changes: 18 additions & 11 deletions pokedex/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@

from pokedex.api import PokeApiEndpoints, Pokemon
from pokedex.api.constants import BASE_URL
from pokedex.api.models import PokeApiRequest, PokeApiResource, PokeApiResourceRef, PokemonRef
from pokedex.api.models import PokeApiResource, PokeApiResourceRef, PokemonRef
from pokedex.api.request import ApiRequest
from pokedex.api.request.protocol import DeferredRequest


def get_endpoints(endpoints_request: PokeApiRequest) -> PokeApiEndpoints:
ApiRequestType = ApiRequest.type()


def get_endpoints(endpoints_request: DeferredRequest) -> PokeApiEndpoints:
response: requests.Response = endpoints_request()
response.raise_for_status()
endpoints: PokeApiEndpoints = response.json()
Expand All @@ -19,17 +24,17 @@ def get_endpoints(endpoints_request: PokeApiRequest) -> PokeApiEndpoints:
return endpoints


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


def get_resource(request: PokeApiRequest) -> PokeApiResource:
def get_resource(request: DeferredRequest) -> PokeApiResource:
response: requests.Response = request()
response.raise_for_status() # TODO: handle error states
return PokeApiResource(**response.json())


def generate_pokemon_requests(api_request: PokeApiRequest, response_key: str) -> Generator[PokeApiRequest, None, None]:
def generate_pokemon_requests(api_request: DeferredRequest, response_key: str) -> Generator[DeferredRequest, None, None]:
response: requests.Response = api_request()
response.raise_for_status() # TODO: handle error states
resource_refs = response.json()[response_key]
Expand All @@ -41,7 +46,7 @@ def generate_pokemon_requests(api_request: PokeApiRequest, response_key: str) ->
yield model.as_request()


def get_pokemon(pokemon_requests: Iterable[PokeApiRequest]) -> Generator[Pokemon, None, None]:
def get_pokemon(pokemon_requests: Iterable[DeferredRequest]) -> Generator[Pokemon, None, None]:
calls = (Call(Closure(pokemon_request)) for pokemon_request in pokemon_requests)
for result in Procedure(calls).execute(synchronously=False, should_raise=True):
# TODO: consider how to proceed if `result.successful => False`
Expand All @@ -52,7 +57,7 @@ def get_pokemon(pokemon_requests: Iterable[PokeApiRequest]) -> Generator[Pokemon

def search_endpoint(
endpoint_name: str, resource_ref_name: str, api_resource: Optional[PokeApiResource] = None
) -> Optional[PokeApiRequest]:
) -> Optional[DeferredRequest]:
if not api_resource:
api_resource = fetch(endpoint_name)

Expand All @@ -68,19 +73,21 @@ def search_endpoint(
return search_endpoint(endpoint_name, resource_ref_name, new_api_resource)


# TODO -- consider passing an `api_request_type: type[DeferredRequest]` param.
# Doing so would give entrypoints explicit control over implementation selection at runtime.
def fetch(endpoint_name: str) -> PokeApiResource:
endpoints = get_endpoints(PokeApiRequest(BASE_URL))
endpoints = get_endpoints(ApiRequestType(BASE_URL))
endpoint = select_endpoint(endpoint_name, endpoints)
return get_resource(endpoint)


def get_pokemon_by_move(pokemon_move: str) -> Generator[PokeApiRequest, None, None]:
def get_pokemon_by_move(pokemon_move: str) -> Generator[DeferredRequest, None, None]:
endpoint_request = search_endpoint("move", pokemon_move)
if endpoint_request:
yield from generate_pokemon_requests(endpoint_request, "learned_by_pokemon")


def get_pokemon_by_type(pokemon_type: str) -> Generator[PokeApiRequest, None, None]:
def get_pokemon_by_type(pokemon_type: str) -> Generator[DeferredRequest, None, None]:
endpoint_request = search_endpoint("type", pokemon_type)
if endpoint_request:
yield from generate_pokemon_requests(endpoint_request, "pokemon")
27 changes: 7 additions & 20 deletions pokedex/api/models.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
from dataclasses import dataclass
from typing import List, Optional

import requests
from pydantic import BaseModel

from pokedex.db.client import cached_get


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

def __call__(self) -> requests.Response:
return cached_get(self.url)

@property
def __name__(self):
return f"{self.__class__.__name__}:{self.url}"
from pokedex.api.request import ApiRequest
from pokedex.api.request.protocol import DeferredRequest


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

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


class PokemonRef(BaseModel):
Expand All @@ -34,7 +21,7 @@ class PokemonRef(BaseModel):
def as_api_resource_ref(self) -> PokeApiResourceRef:
return self.pokemon

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


Expand All @@ -45,6 +32,6 @@ class PokeApiResource(BaseModel):
results: List[PokeApiResourceRef]

@property
def next_request(self) -> Optional[PokeApiRequest]:
def next_request(self) -> Optional[DeferredRequest]:
if self.next:
return PokeApiRequest(self.next)
return ApiRequest.type()(self.next)
15 changes: 15 additions & 0 deletions pokedex/api/request/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from enum import Enum

from pokedex.api.request.implementations.default import PokeApiRequest
from pokedex.api.request.implementations.cached import CachedPokeApiRequest
from pokedex.api.request.protocol import DeferredRequest
from pokedex.constants import API_REQUEST_IMPL


class ApiRequest(Enum):
DEFAULT: DeferredRequest = PokeApiRequest
CACHED: DeferredRequest = CachedPokeApiRequest

@staticmethod
def type() -> type[DeferredRequest]:
return ApiRequest[API_REQUEST_IMPL].value
14 changes: 0 additions & 14 deletions pokedex/api/request/implementations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +0,0 @@
from enum import Enum

from pokedex.api.request.implementations.default import PokeApiRequest
from pokedex.api.request.implementations.cached import CachedPokeApiRequest
from pokedex.api.request.protocol import DeferredRequest


class ApiRequest(Enum):
DEFAULT: DeferredRequest = PokeApiRequest
CACHED: DeferredRequest = CachedPokeApiRequest

@property
def type(self):
return self.value
4 changes: 4 additions & 0 deletions pokedex/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from pathlib import Path
from os import environ


PROJECTROOT = Path(__file__).parent.parent.absolute()
DBROOT = PROJECTROOT / "pokedex" / "db"
CACHEPATH = DBROOT / "cache"

API_REQUEST_IMPL = environ.get("API_REQUEST_IMPL") or "DEFAULT"
2 changes: 1 addition & 1 deletion pokedex/db/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

from actionpack import Action

from pokedex.api.request.protocol import DeferredRequest
from pokedex.constants import CACHEPATH
from pokedex.db.models import DeferredRequest


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion pokedex/db/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import requests
from actionpack import KeyedProcedure

from pokedex.api.request.protocol import DeferredRequest
from pokedex.db.actions import DbInsert, DbInsertRequestResult, DbRead
from pokedex.db.models import DeferredRequest


def persist_requests(requests: Iterable[DeferredRequest]):
Expand Down
10 changes: 0 additions & 10 deletions pokedex/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,6 @@
from dataclasses import asdict, dataclass
from typing import Any, Dict, List, Type, Union

from requests import Response
from typing_extensions import Protocol


class DeferredRequest(Protocol):
url: str

def __call__(self) -> Response:
pass


JSON = Union[Dict[str, Any], List[Any], int, str, float, bool, Type[None]]

Expand Down
2 changes: 1 addition & 1 deletion tests/pokedex/api/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from requests.exceptions import HTTPError

from pokedex.api.client import get_pokemon, get_pokemon_by_move, get_pokemon_by_type
from pokedex.api.models import PokeApiRequest
from pokedex.api.request.implementations.default import PokeApiRequest
from tests.fixtures import craft_response, resource


Expand Down
2 changes: 1 addition & 1 deletion tests/pokedex/db/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class TestActions(TestCase):
@patch("dbm.open")
def test_can_read_cache(self, mock_kv_open):
key = "https://pokeapi.co/api/v2/pokemon/39/"
key = "https://pokeapi.co/api/v2/pokemon/18/"
pokemon_data = resource("jigglypuff.response")
mock_kv_open.return_value.__enter__.return_value.__getitem__.return_value = pokemon_data
mock_kv_open.assert_not_called()
Expand Down
16 changes: 8 additions & 8 deletions tests/pokedex/db/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import requests
from requests.exceptions import HTTPError

from pokedex.api.models import PokeApiRequest
from pokedex.api.request.implementations.cached import CachedPokeApiRequest
from pokedex.db.actions import DbRead
from pokedex.db.client import cached_get, persist_requests
from tests.fixtures import craft_response, craft_result, resource
Expand All @@ -13,14 +13,14 @@
class TestClientCanCache(TestCase):
@patch("pokedex.db.actions.DbInsertRequestResult")
@patch.object(
target=PokeApiRequest,
target=CachedPokeApiRequest,
attribute="__call__",
side_effect=[
craft_response(resource("jigglypuff.response"), status_code=200),
],
)
def test_can_persist_request(self, mock_request, mock_db_insert):
request_url = "https://pokeapi.co/api/v2/pokemon/39/"
request_url = "https://pokeapi.co/api/v2/pokemon/18/"
mock_request.url = request_url
key, response_data = next(persist_requests((mock_request,)))
assert key is request_url
Expand All @@ -36,7 +36,7 @@ class TestClientCanRetrieveFromCache(TestCase):
],
)
def test_cache_hit(self, mock_db_reads):
pokemon_response = cached_get("https://pokeapi.co/api/v2/pokemon/39/")
pokemon_response = cached_get("https://pokeapi.co/api/v2/pokemon/18/")
assert pokemon_response.json()["name"] == "jigglypuff"

@patch.object(
Expand All @@ -53,20 +53,20 @@ def test_cache_hit(self, mock_db_reads):
)
def test_cache_miss_request_fail(self, mock_requests, mock_db_reads):
with self.assertRaises(HTTPError):
cached_get("https://pokeapi.co/api/v2/pokemon/39/")
cached_get("https://pokeapi.co/api/v2/pokemon/18/")

@patch.object(
target=DbRead,
attribute="perform",
side_effect=[craft_result(value=Exception("missing key."), successful=False)],
)
@patch.object(
target=PokeApiRequest,
attribute="__call__",
target=requests,
attribute="get",
side_effect=[
craft_response(resource("jigglypuff.response"), status_code=200),
],
)
def test_cache_miss_request_success(self, mock_requests, mock_db_reads):
pokemon_response = cached_get("https://pokeapi.co/api/v2/pokemon/39/")
pokemon_response = cached_get("https://pokeapi.co/api/v2/pokemon/18/")
assert pokemon_response.json()["name"] == "jigglypuff"

0 comments on commit 20af921

Please sign in to comment.