Skip to content

Commit

Permalink
feat: make number of retries configurable (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ravencentric authored Mar 16, 2024
1 parent 7949d79 commit 66d32d9
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 70 deletions.
23 changes: 18 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
name: Tests

on: [push, pull_request, workflow_dispatch]
on:
push:
branches:
- main
paths:
- 'src/**'
pull_request:
branches:
- main
paths:
- 'src/**'
workflow_dispatch:

defaults:
run:
Expand All @@ -10,7 +21,6 @@ jobs:
test:
name: Tests
strategy:
max-parallel: 1
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
Expand Down Expand Up @@ -46,13 +56,16 @@ jobs:
run: poetry install

- name: Run tests
run: |
poetry run pytest
run: poetry run pytest

- name: Build
run: poetry build

- name: Get version
id: version
run: echo "version=$(poetry run python -c 'import pyanilist; print(pyanilist.__version__)')" >> $GITHUB_OUTPUT

- uses: actions/upload-artifact@v4
with:
name: pyanilist-${{ matrix.python-version }}-${{ matrix.os }}
name: pyanilist-${{ steps.version.outputs.version }}-${{ matrix.python-version }}-${{ matrix.os }}
path: "dist/*"
26 changes: 16 additions & 10 deletions src/pyanilist/_clients/_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import httpx
from pydantic import validate_call
from tenacity import retry, stop_after_attempt, wait_incrementing
from tenacity import AsyncRetrying, stop_after_attempt, wait_incrementing

from .._enums import MediaFormat, MediaSeason, MediaStatus, MediaType
from .._models import Media
Expand All @@ -14,26 +14,26 @@


class AsyncAnilist:
def __init__(self, api_url: str = "https://graphql.anilist.co", **httpx_async_client_kwargs: Any) -> None:
def __init__(
self, api_url: str = "https://graphql.anilist.co", retries: int = 5, **httpx_async_client_kwargs: Any
) -> None:
"""
Async Anilist API client.
Parameters
----------
api_url : str, optional
The URL of the Anilist API. Default is "https://graphql.anilist.co".
retries : int, optional
Number of times to retry a failed request before raising an error. Default is 5.
httpx_async_client_kwargs : Any, optional
Keyword arguments to pass to the internal [httpx.AsyncClient()](https://www.python-httpx.org/api/#asyncclient)
used to make the POST request.
"""
self.api_url = api_url
self.retries = retries
self.httpx_async_client_kwargs = httpx_async_client_kwargs

@retry(
stop=stop_after_attempt(5),
wait=wait_incrementing(start=0, increment=1),
reraise=True,
)
async def _post_request(
self,
id: AnilistID | None = None,
Expand Down Expand Up @@ -92,9 +92,15 @@ async def _post_request(
"variables": {key: value for key, value in query_variables.items() if value is not None},
}

async with httpx.AsyncClient(**self.httpx_async_client_kwargs) as client:
response = await client.post(self.api_url, json=payload)
response.raise_for_status()
async for attempt in AsyncRetrying(
stop=stop_after_attempt(self.retries),
wait=wait_incrementing(start=0, increment=1),
reraise=True,
):
with attempt:
async with httpx.AsyncClient(**self.httpx_async_client_kwargs) as client:
response = await client.post(self.api_url, json=payload)
response.raise_for_status()

return response

Expand Down
24 changes: 15 additions & 9 deletions src/pyanilist/_clients/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import httpx
from pydantic import validate_call
from tenacity import retry, stop_after_attempt, wait_incrementing
from tenacity import Retrying, stop_after_attempt, wait_incrementing

from .._enums import MediaFormat, MediaSeason, MediaStatus, MediaType
from .._models import Media
Expand All @@ -14,26 +14,26 @@


class Anilist:
def __init__(self, api_url: str = "https://graphql.anilist.co", **httpx_client_kwargs: Any) -> None:
def __init__(
self, api_url: str = "https://graphql.anilist.co", retries: int = 5, **httpx_client_kwargs: Any
) -> None:
"""
Anilist API client.
Parameters
----------
api_url : str, optional
The URL of the Anilist API. Default is "https://graphql.anilist.co".
retries : int, optional
Number of times to retry a failed request before raising an error. Default is 5.
httpx_client_kwargs : Any, optional
Keyword arguments to pass to the internal [httpx.Client()](https://www.python-httpx.org/api/#client)
used to make the POST request.
"""
self.api_url = api_url
self.retries = retries
self.httpx_client_kwargs = httpx_client_kwargs

@retry(
stop=stop_after_attempt(5),
wait=wait_incrementing(start=0, increment=1),
reraise=True,
)
def _post_request(
self,
id: AnilistID | None = None,
Expand Down Expand Up @@ -92,8 +92,14 @@ def _post_request(
"variables": {key: value for key, value in query_variables.items() if value is not None},
}

with httpx.Client(**self.httpx_client_kwargs) as client:
response = client.post(self.api_url, json=payload).raise_for_status()
for attempt in Retrying(
stop=stop_after_attempt(self.retries),
wait=wait_incrementing(start=0, increment=1),
reraise=True,
):
with attempt:
with httpx.Client(**self.httpx_client_kwargs) as client:
response = client.post(self.api_url, json=payload).raise_for_status()

return response

Expand Down
23 changes: 0 additions & 23 deletions tests/test_anilist.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import pytest
from pyanilist import (
Anilist,
HTTPStatusError,
HttpUrl,
MediaFormat,
MediaSeason,
MediaSource,
MediaStatus,
MediaType,
ValidationError,
)


Expand Down Expand Up @@ -57,30 +54,10 @@ def test_anilist_with_all_constraints() -> None:
assert media.site_url == HttpUrl("https://anilist.co/anime/21459")


def test_anilist_title_doesnt_exist() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
Anilist().search("Title does not exist", type=MediaType.MANGA)


def test_anilist_bad_search_combo() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
Anilist().search("Attack on titan", season_year=1999)


def test_anilist_wrong_input_types() -> None:
with pytest.raises(ValidationError):
Anilist().search(123456789, season_year="hello", type=True) # type: ignore


def test_anilist_id() -> None:
media = Anilist().get(16498)
assert media.title.romaji == "Shingeki no Kyojin"
assert media.start_date.year == 2013
assert media.source == MediaSource.MANGA
assert media.type == MediaType.ANIME
assert media.site_url == HttpUrl("https://anilist.co/anime/16498")


def test_anilist_bad_id() -> None:
with pytest.raises(HTTPStatusError, match="400 Bad Request"):
Anilist().get(9999999999)
23 changes: 0 additions & 23 deletions tests/test_async_anilist.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import pytest
from pyanilist import (
AsyncAnilist,
HTTPStatusError,
HttpUrl,
MediaFormat,
MediaSeason,
MediaSource,
MediaStatus,
MediaType,
ValidationError,
)


Expand Down Expand Up @@ -57,30 +54,10 @@ async def test_anilist_with_all_constraints() -> None:
assert media.site_url == HttpUrl("https://anilist.co/anime/21459")


async def test_anilist_title_doesnt_exist() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
await AsyncAnilist().search("Title does not exist", type=MediaType.MANGA)


async def test_anilist_bad_search_combo() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
await AsyncAnilist().search("Attack on titan", season_year=1999)


async def test_anilist_wrong_input_types() -> None:
with pytest.raises(ValidationError):
await AsyncAnilist().search(123456789, season_year="hello", type=True) # type: ignore


async def test_anilist_id() -> None:
media = await AsyncAnilist().get(16498)
assert media.title.romaji == "Shingeki no Kyojin"
assert media.start_date.year == 2013
assert media.source == MediaSource.MANGA
assert media.type == MediaType.ANIME
assert media.site_url == HttpUrl("https://anilist.co/anime/16498")


async def test_anilist_bad_id() -> None:
with pytest.raises(HTTPStatusError, match="400 Bad Request"):
await AsyncAnilist().get(9999999999)
29 changes: 29 additions & 0 deletions tests/test_async_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest
from pyanilist import (
AsyncAnilist,
HTTPStatusError,
MediaType,
ValidationError,
)

anilist = AsyncAnilist(retries=1)


async def test_anilist_title_doesnt_exist() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
await anilist.search("Title does not exist", type=MediaType.MANGA)


async def test_anilist_bad_search_combo() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
await anilist.search("Attack on titan", season_year=1999)


async def test_anilist_wrong_input_types() -> None:
with pytest.raises(ValidationError):
await anilist.search(123456789, season_year="hello", type=True) # type: ignore


async def test_anilist_bad_id() -> None:
with pytest.raises(HTTPStatusError, match="400 Bad Request"):
await anilist.get(9999999999)
29 changes: 29 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest
from pyanilist import (
Anilist,
HTTPStatusError,
MediaType,
ValidationError,
)

anilist = Anilist(retries=1)


def test_anilist_title_doesnt_exist() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
anilist.search("Title does not exist", type=MediaType.MANGA)


def test_anilist_bad_search_combo() -> None:
with pytest.raises(HTTPStatusError, match="Not Found."):
anilist.search("Attack on titan", season_year=1999)


def test_anilist_wrong_input_types() -> None:
with pytest.raises(ValidationError):
anilist.search(123456789, season_year="hello", type=True) # type: ignore


def test_anilist_bad_id() -> None:
with pytest.raises(HTTPStatusError, match="400 Bad Request"):
anilist.get(9999999999)

0 comments on commit 66d32d9

Please sign in to comment.