Skip to content

Commit

Permalink
update for aiorequestful v0.4.0 changes
Browse files Browse the repository at this point in the history
  • Loading branch information
geo-martino committed Jul 10, 2024
1 parent 307f6e5 commit f5d3a4f
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 50 deletions.
3 changes: 2 additions & 1 deletion musify/libraries/remote/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from collections.abc import Mapping, MutableMapping
from typing import Any

from aiorequestful.types import URLInput
from yarl import URL

from musify.libraries.remote.core import RemoteResponse
from musify.types import UnitMutableSequence, UnitSequence, MusifyEnum

type APIInputValueSingle[T: RemoteResponse] = str | URL | Mapping[str, Any] | T
type APIInputValueSingle[T: RemoteResponse] = URLInput | Mapping[str, Any] | T
type APIInputValueMulti[T: RemoteResponse] = (
UnitSequence[str] |
UnitMutableSequence[URL] |
Expand Down
3 changes: 2 additions & 1 deletion musify/libraries/remote/core/wrangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from abc import ABCMeta, abstractmethod
from collections.abc import Mapping

from aiorequestful.types import URLInput
from yarl import URL

from musify.libraries.remote.core import RemoteResponse
Expand Down Expand Up @@ -87,7 +88,7 @@ def get_item_type(
of the input ``values``.
Or when the list contains strings representing many differing remote object types or only IDs.
"""
if isinstance(values, str | URL | Mapping | RemoteResponse):
if isinstance(values, URLInput | Mapping | RemoteResponse):
return cls._get_item_type(value=values, kind=kind)

if len(values) == 0:
Expand Down
33 changes: 17 additions & 16 deletions musify/libraries/remote/spotify/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
from pathlib import Path

from aiohttp import ClientResponse
from aiorequestful.auth import AuthRequest
from aiorequestful.auth.oauth2 import AuthorisationCodeFlow
from aiorequestful.auth.utils import AuthRequest
from aiorequestful.cache.backend.base import ResponseCache, ResponseRepository
from aiorequestful.cache.session import CachedSession
from aiorequestful.types import URLInput
from yarl import URL

from musify.libraries.remote.core.exception import APIError
from musify.libraries.remote.spotify.api.cache import SpotifyRequestSettings, SpotifyPaginatedRequestSettings
from musify.libraries.remote.spotify.api.cache import SpotifyRepositorySettings, SpotifyPaginatedRepositorySettings
from musify.libraries.remote.spotify.api.item import SpotifyAPIItems
from musify.libraries.remote.spotify.api.misc import SpotifyAPIMisc
from musify.libraries.remote.spotify.api.playlist import SpotifyAPIPlaylists
Expand Down Expand Up @@ -110,28 +111,28 @@ async def _setup_cache(self) -> None:
cache = self.handler.session.cache
cache.repository_getter = self._get_cache_repository

cache.create_repository(SpotifyRequestSettings(name="tracks"))
cache.create_repository(SpotifyRequestSettings(name="audio_features"))
cache.create_repository(SpotifyRequestSettings(name="audio_analysis"))
cache.create_repository(SpotifyRepositorySettings(name="tracks"))
cache.create_repository(SpotifyRepositorySettings(name="audio_features"))
cache.create_repository(SpotifyRepositorySettings(name="audio_analysis"))

cache.create_repository(SpotifyRequestSettings(name="albums"))
cache.create_repository(SpotifyPaginatedRequestSettings(name="album_tracks"))
cache.create_repository(SpotifyRepositorySettings(name="albums"))
cache.create_repository(SpotifyPaginatedRepositorySettings(name="album_tracks"))

cache.create_repository(SpotifyRequestSettings(name="artists"))
cache.create_repository(SpotifyPaginatedRequestSettings(name="artist_albums"))
cache.create_repository(SpotifyRepositorySettings(name="artists"))
cache.create_repository(SpotifyPaginatedRepositorySettings(name="artist_albums"))

cache.create_repository(SpotifyRequestSettings(name="shows"))
cache.create_repository(SpotifyRequestSettings(name="episodes"))
cache.create_repository(SpotifyPaginatedRequestSettings(name="show_episodes"))
cache.create_repository(SpotifyRepositorySettings(name="shows"))
cache.create_repository(SpotifyRepositorySettings(name="episodes"))
cache.create_repository(SpotifyPaginatedRepositorySettings(name="show_episodes"))

cache.create_repository(SpotifyRequestSettings(name="audiobooks"))
cache.create_repository(SpotifyRequestSettings(name="chapters"))
cache.create_repository(SpotifyPaginatedRequestSettings(name="audiobook_chapters"))
cache.create_repository(SpotifyRepositorySettings(name="audiobooks"))
cache.create_repository(SpotifyRepositorySettings(name="chapters"))
cache.create_repository(SpotifyPaginatedRepositorySettings(name="audiobook_chapters"))

await cache

@staticmethod
def _get_cache_repository(cache: ResponseCache, url: str | URL) -> ResponseRepository | None:
def _get_cache_repository(cache: ResponseCache, url: URLInput) -> ResponseRepository | None:
path = URL(url).path
path_split = [part.replace("-", "_") for part in path.split("/")[2:]]

Expand Down
5 changes: 3 additions & 2 deletions musify/libraries/remote/spotify/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from aiorequestful.cache.backend.base import ResponseRepository
from aiorequestful.cache.exception import CacheError
from aiorequestful.cache.session import CachedSession
from aiorequestful.types import URLInput
from yarl import URL

from musify.libraries.remote.core.api import RemoteAPI
Expand Down Expand Up @@ -36,7 +37,7 @@ def _format_key(key: str | RemoteObjectType | None) -> str | None:
return key.lower().rstrip("s") + "s"

@staticmethod
def format_next_url(url: str | URL, offset: int = 0, limit: int = 20) -> str:
def format_next_url(url: URLInput, offset: int = 0, limit: int = 20) -> str:
"""Format a `next` style URL for looping through API pages"""
url = URL(url)

Expand Down Expand Up @@ -91,7 +92,7 @@ def _enrich_with_parent_response(
## Cache utilities
###########################################################################
async def _get_responses_from_cache(
self, method: str, url: str | URL, id_list: Collection[str]
self, method: str, url: URLInput, id_list: Collection[str]
) -> tuple[list[dict[str, Any]], Collection[str], Collection[str]]:
"""
Attempt to find the given ``id_list`` in the cache of the request handler and return results.
Expand Down
21 changes: 13 additions & 8 deletions musify/libraries/remote/spotify/api/cache.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
from http import HTTPMethod
from typing import Any

from aiorequestful.cache.backend.base import RequestSettings
from aiorequestful.cache.backend.base import ResponseRepositorySettings
from aiorequestful.types import MethodInput, URLInput
from yarl import URL

from musify.libraries.remote.core.types import RemoteIDType
from musify.libraries.remote.core.exception import RemoteObjectTypeError
from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler


class SpotifyRequestSettings(RequestSettings):
class SpotifyRepositorySettings(ResponseRepositorySettings):

@property
def fields(self) -> tuple[str, ...]:
return "id",

def get_key(self, url: str | URL, *_, **__) -> tuple[str | None, ...]:
def get_key(self, method: MethodInput, url: URLInput, **__) -> tuple[str | None, ...]:
if HTTPMethod(method) != HTTPMethod.GET:
return (None,)

try:
return SpotifyDataWrangler.convert(str(url), type_in=RemoteIDType.URL, type_out=RemoteIDType.ID),
except RemoteObjectTypeError:
Expand All @@ -28,24 +33,24 @@ def get_name(response: dict[str, Any]) -> str | None:
return response.get("name")


class SpotifyPaginatedRequestSettings(SpotifyRequestSettings):
class SpotifyPaginatedRepositorySettings(SpotifyRepositorySettings):

@property
def fields(self) -> tuple[str, ...]:
return *super().fields, "offset", "size"

def get_key(self, url: str | URL, *_, **__) -> tuple[str | int | None, ...]:
base = super().get_key(url=url)
def get_key(self, method: MethodInput, url: URLInput, **__) -> tuple[str | int | None, ...]:
base = super().get_key(method=method, url=url)
return *base, self.get_offset(url), self.get_limit(url)

@staticmethod
def get_offset(url: str | URL) -> int:
def get_offset(url: URLInput) -> int:
"""Extracts the offset for a paginated request from the given ``url``."""
params = URL(url).query
return int(params.get("offset", 0))

@staticmethod
def get_limit(url: str | URL) -> int:
def get_limit(url: URLInput) -> int:
"""Extracts the limit for a paginated request from the given ``url``."""
params = URL(url).query
return int(params.get("limit", 50))
3 changes: 2 additions & 1 deletion musify/libraries/remote/spotify/api/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from itertools import batched
from typing import Any

from aiorequestful.types import URLInput
from yarl import URL

from musify.libraries.remote.core import RemoteResponse
Expand Down Expand Up @@ -40,7 +41,7 @@ def _get_unit(self, key: str | None = None, kind: str | None = None) -> str:
###########################################################################
async def _get_items(
self,
url: str | URL,
url: URLInput,
id_list: Collection[str],
params: MutableMapping[str, Any] | None = None,
key: str | None = None,
Expand Down
13 changes: 7 additions & 6 deletions musify/libraries/remote/spotify/wrangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from collections.abc import Mapping
from typing import Any

from aiorequestful.types import URLInput
from yarl import URL

from musify.exception import MusifyEnumError
Expand All @@ -26,7 +27,7 @@ class SpotifyDataWrangler(RemoteDataWrangler):
url_ext = URL("https://open.spotify.com")

@classmethod
def get_id_type(cls, value: str | URL, kind: RemoteObjectType | None = None) -> RemoteIDType:
def get_id_type(cls, value: URLInput, kind: RemoteObjectType | None = None) -> RemoteIDType:
value = str(value).strip().casefold()
uri_split = value.split(':')

Expand All @@ -44,7 +45,7 @@ def get_id_type(cls, value: str | URL, kind: RemoteObjectType | None = None) ->
raise RemoteIDTypeError(f"Could not determine ID type of given value: {value}")

@classmethod
def validate_id_type(cls, value: str | URL, kind: RemoteIDType = RemoteIDType.ALL) -> bool:
def validate_id_type(cls, value: URLInput, kind: RemoteIDType = RemoteIDType.ALL) -> bool:
value = str(value).strip().casefold()

if kind == RemoteIDType.URL:
Expand Down Expand Up @@ -113,7 +114,7 @@ def _get_item_type_from_mapping(cls, value: Mapping[str, Any]) -> RemoteObjectTy
@classmethod
def convert(
cls,
value: str | URL,
value: URLInput,
kind: RemoteObjectType | None = None,
type_in: RemoteIDType = RemoteIDType.ALL,
type_out: RemoteIDType = RemoteIDType.ID
Expand Down Expand Up @@ -141,7 +142,7 @@ def convert(

@classmethod
def _get_id(
cls, value: str | URL, kind: RemoteObjectType | None = None, type_in: RemoteIDType = RemoteIDType.ALL
cls, value: URLInput, kind: RemoteObjectType | None = None, type_in: RemoteIDType = RemoteIDType.ALL
) -> tuple[RemoteObjectType, str]:
if isinstance(value, URL) or type_in == RemoteIDType.URL_EXT or type_in == RemoteIDType.URL:
try:
Expand All @@ -163,7 +164,7 @@ def _get_id(
return kind, id_

@classmethod
def _get_id_from_url(cls, value: str | URL, kind: RemoteObjectType | None = None) -> tuple[RemoteObjectType, str]:
def _get_id_from_url(cls, value: URLInput, kind: RemoteObjectType | None = None) -> tuple[RemoteObjectType, str]:
url_path = URL(value).path.split("/")
for chunk in url_path:
try:
Expand Down Expand Up @@ -194,7 +195,7 @@ def _get_id_from_uri(cls, value: str) -> tuple[RemoteObjectType, str]:
def extract_ids(cls, values: APIInputValueMulti[RemoteResponse], kind: RemoteObjectType | None = None) -> list[str]:
def extract_id(value: APIInputValueSingle[RemoteResponse]) -> str:
"""Extract an ID from a given ``value``"""
if isinstance(value, str | URL):
if isinstance(value, URLInput):
return cls.convert(value, kind=kind, type_out=RemoteIDType.ID)
elif isinstance(value, Mapping) and "id" in value:
return value["id"]
Expand Down
11 changes: 6 additions & 5 deletions tests/libraries/remote/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from aiohttp import ClientResponse
from aioresponses import aioresponses
from aioresponses.core import RequestCall
from aiorequestful.types import URLInput
from yarl import URL

from musify.libraries.remote.core.types import RemoteIDType, RemoteObjectType
Expand Down Expand Up @@ -84,7 +85,7 @@ def calculate_pages_from_response(self, response: Mapping[str, Any]) -> int:
async def get_requests(
self,
method: str | None = None,
url: str | URL | re.Pattern[str] | None = None, # matches given after params have been stripped
url: URLInput | re.Pattern[str] | None = None, # matches given after params have been stripped
params: dict[str, Any] | None = None,
response: dict[str, Any] | None = None
) -> list[tuple[URL, RequestCall, ClientResponse | None]]:
Expand Down Expand Up @@ -112,11 +113,11 @@ def _get_match_from_method(actual: str, expected: str | None = None) -> bool:
return match

@staticmethod
def _get_match_from_url(actual: str | URL, expected: str | URL | re.Pattern[str] | None = None) -> bool:
def _get_match_from_url(actual: URLInput, expected: URLInput | re.Pattern[str] | None = None) -> bool:
match = expected is None
if not match:
actual = str(URL(actual).with_query(None))
if isinstance(expected, str | URL):
if isinstance(expected, URLInput):
match = actual == str(URL(expected).with_query(None))
elif isinstance(expected, re.Pattern):
match = bool(expected.search(actual))
Expand All @@ -135,7 +136,7 @@ def _get_match_from_params(actual: RequestCall, expected: dict[str, Any] | None
return match

async def _get_match_from_expected_response(
self, actual: str | URL, expected: dict[str, Any] | None = None
self, actual: URLInput, expected: dict[str, Any] | None = None
) -> bool:
match = expected is None
if not match:
Expand All @@ -151,7 +152,7 @@ async def _get_match_from_expected_response(

return match

def _get_response_from_url(self, url: str | URL) -> ClientResponse | None:
def _get_response_from_url(self, url: URLInput) -> ClientResponse | None:
response = None
for response in self._responses:
if str(response.url) == url:
Expand Down
23 changes: 14 additions & 9 deletions tests/libraries/remote/spotify/api/test_cache_settings.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
from copy import deepcopy
from http import HTTPMethod
from random import choice

import pytest

from musify.libraries.remote.spotify.api import SpotifyAPI
from musify.libraries.remote.spotify.api.cache import SpotifyRequestSettings, SpotifyPaginatedRequestSettings
from musify.libraries.remote.spotify.api.cache import SpotifyRepositorySettings, SpotifyPaginatedRepositorySettings
from tests.libraries.remote.spotify.api.mock import SpotifyMock


class TestSpotifyRequestSettings:

@pytest.fixture(scope="class")
def settings(self) -> SpotifyRequestSettings:
def settings(self) -> SpotifyRepositorySettings:
"""Yields a :py:class:`SpotifyRequestSettings` object as a pytest.fixture."""
return SpotifyRequestSettings(name="test")
return SpotifyRepositorySettings(name="test")

def test_core_getters(self, settings: SpotifyRequestSettings, api_mock: SpotifyMock):
def test_core_getters(self, settings: SpotifyRepositorySettings, api_mock: SpotifyMock):
for responses in api_mock.item_type_map.values():
response = deepcopy(choice(responses))
name = response["display_name"] if response["type"] == "user" else response["name"]
assert settings.get_name(response) == name
assert settings.get_key(response["href"]) == (response["id"],)
assert settings.get_key(method=HTTPMethod.GET, url=response["href"]) == (response["id"],)
assert settings.get_key(method="GET", url=response["href"]) == (response["id"],)

# does not store post requests
assert settings.get_key(method=HTTPMethod.POST, url=response["href"]) == (None,)

response = deepcopy(choice(api_mock.user_tracks))
assert settings.get_name(response) is None

url = f"{api_mock.url_api}/me/tracks"
assert settings.get_key(url) == (None,)
assert settings.get_key(method=HTTPMethod.GET, url=url) == (None,)

@pytest.fixture(scope="class")
def settings_paginated(self) -> SpotifyPaginatedRequestSettings:
def settings_paginated(self) -> SpotifyPaginatedRepositorySettings:
"""Yields a :py:class:`SpotifyPaginatedRequestSettings` object as a pytest.fixture."""
return SpotifyPaginatedRequestSettings(name="test")
return SpotifyPaginatedRepositorySettings(name="test")

def test_paginated_getters(
self,
settings_paginated: SpotifyPaginatedRequestSettings,
settings_paginated: SpotifyPaginatedRepositorySettings,
api: SpotifyAPI,
api_mock: SpotifyMock
):
Expand Down
3 changes: 2 additions & 1 deletion tests/libraries/remote/spotify/api/test_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest
from aioresponses.core import RequestCall
from aiorequestful.types import URLInput
from yarl import URL

from musify import PROGRAM_NAME
Expand Down Expand Up @@ -42,7 +43,7 @@ def _get_payload_from_request(request: RequestCall) -> dict[str, Any] | None:
return request.kwargs.get("body", request.kwargs.get("json"))

@classmethod
async def _get_payloads_from_url_base(cls, url: str | URL, api_mock: SpotifyMock) -> list[dict[str, Any]]:
async def _get_payloads_from_url_base(cls, url: URLInput, api_mock: SpotifyMock) -> list[dict[str, Any]]:
return [
cls._get_payload_from_request(req) for _, req, _ in await api_mock.get_requests(url=url)
if cls._get_payload_from_request(req)
Expand Down

0 comments on commit f5d3a4f

Please sign in to comment.