From a7113babb7c4ee931173442f7bb0ed58dcb3e92d Mon Sep 17 00:00:00 2001 From: cswimr Date: Fri, 15 Nov 2024 09:51:11 -0500 Subject: [PATCH] (3.0.1) mypy compliance --- pyflowery/__init__.py | 22 +++++++++-------- pyflowery/exceptions.py | 22 +++++++++++++---- pyflowery/pyflowery.py | 50 +++++++++++++++++++++++++++------------ pyflowery/rest_adapter.py | 18 +++++++------- pyflowery/version.py | 2 +- pyproject.toml | 2 +- tests/tests.py | 6 +++-- 7 files changed, 81 insertions(+), 41 deletions(-) diff --git a/pyflowery/__init__.py b/pyflowery/__init__.py index 969f590..1b6cb35 100644 --- a/pyflowery/__init__.py +++ b/pyflowery/__init__.py @@ -1,6 +1,7 @@ from pyflowery.exceptions import ( ClientError, InternalServerError, + ResponseError, RetryLimitExceeded, TooManyRequests, ) @@ -9,14 +10,15 @@ from pyflowery.pyflowery import FloweryAPI from pyflowery.version import VERSION __all__ = [ - 'FloweryAPI', - 'FloweryAPIConfig', - 'Language', - 'Result', - 'Voice', - 'VERSION', - 'ClientError', - 'InternalServerError', - 'RetryLimitExceeded', - 'TooManyRequests', + "FloweryAPI", + "FloweryAPIConfig", + "Language", + "Result", + "Voice", + "VERSION", + "ResponseError", + "ClientError", + "InternalServerError", + "RetryLimitExceeded", + "TooManyRequests", ] diff --git a/pyflowery/exceptions.py b/pyflowery/exceptions.py index 20d9f96..901485e 100644 --- a/pyflowery/exceptions.py +++ b/pyflowery/exceptions.py @@ -1,19 +1,33 @@ +class ResponseError(Exception): + """Raised when an API response is empty or has an unexpected format""" + + def __init__(self, message) -> None: + self.message = "Invalid response from Flowery API: " + message + + class InternalServerError(Exception): """Raised when the API returns a 5xx status code""" - def __init__(self, message): + + def __init__(self, message) -> None: self.message = message + class ClientError(Exception): """Raised when the API returns a 4xx status code""" - def __init__(self, message): + + def __init__(self, message) -> None: self.message = message + class TooManyRequests(Exception): """Raised when the API returns a 429 status code""" - def __init__(self, message): + + def __init__(self, message) -> None: self.message = message + class RetryLimitExceeded(Exception): """Raised when the retry limit is exceeded""" - def __init__(self, message): + + def __init__(self, message) -> None: self.message = message diff --git a/pyflowery/pyflowery.py b/pyflowery/pyflowery.py index 0d5c897..74576b3 100644 --- a/pyflowery/pyflowery.py +++ b/pyflowery/pyflowery.py @@ -1,6 +1,7 @@ import asyncio from typing import AsyncGenerator, Tuple +from pyflowery.exceptions import ResponseError from pyflowery.models import FloweryAPIConfig, Language, Voice from pyflowery.rest_adapter import RestAdapter @@ -15,8 +16,8 @@ class FloweryAPI: def __init__(self, config: FloweryAPIConfig) -> None: self.config = config - self.adapter = RestAdapter(config) - self._voices_cache: Tuple[Voice] = () + self.adapter = RestAdapter(config=config) + self._voices_cache: Tuple[Voice, ...] = () try: loop = asyncio.get_running_loop() except RuntimeError: @@ -30,9 +31,12 @@ class FloweryAPI: async def _populate_voices_cache(self) -> None: """Populate the voices cache. This method is called automatically when the FloweryAPI object is created, and should not be called directly.""" self._voices_cache = tuple([voice async for voice in self.fetch_voices()]) # pylint: disable=consider-using-generator - self.config.logger.info("Voices cache populated!") + if self._voices_cache == (): + raise ValueError("Failed to populate voices cache! Please report this issue at https://www.coastalcommits.com/cswimr/PyFlowery/issues.") + else: + self.config.logger.info("Voices cache populated!") - def get_voices(self, voice_id: str | None = None, name: str | None = None) -> Tuple[Voice] | None: + def get_voices(self, voice_id: str | None = None, name: str | None = None) -> Tuple[Voice, ...] | None: """Get a set of voices from the cache. Args: @@ -43,8 +47,8 @@ class FloweryAPI: Tuple[Voice] | None: A tuple of Voice objects if found, otherwise None """ if voice_id: - voice = next((voice for voice in self._voices_cache if voice.id == voice_id)) - return (voice,) or None + voice = next((voice for voice in self._voices_cache if voice.id == voice_id), None) + return (voice,) if voice else None if name: voices = [] for voice in self._voices_cache: @@ -81,20 +85,27 @@ class FloweryAPI: TooManyRequests: Raised when the Flowery API returns a 429 status code ClientError: Raised when the Flowery API returns a 4xx status code InternalServerError: Raised when the Flowery API returns a 5xx status code + ResponseError: Raised when the Flowery API returns an empty response or a response with an unexpected format RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded Returns: AsyncGenerator[Voice, None]: A generator of Voices """ request = await self.adapter.get(endpoint="/tts/voices") - for voice in request.data["voices"]: - yield Voice( - id=voice["id"], - name=voice["name"], - gender=voice["gender"], - source=voice["source"], - language=Language(**voice["language"]), - ) + if request is not None: + if isinstance(request.data, dict): + for voice in request.data.get("voices", []): + yield Voice( + id=voice["id"], + name=voice["name"], + gender=voice["gender"], + source=voice["source"], + language=Language(**voice["language"]), + ) + else: + raise ResponseError(f"Invalid response from Flowery API: {request.data!r}") + else: + raise ResponseError("Invalid response from Flowery API: Empty Response!}") async def fetch_tts( self, text: str, voice: Voice | str | None = None, translate: bool = False, silence: int = 0, audio_format: str = "mp3", speed: float = 1.0 @@ -114,6 +125,7 @@ class FloweryAPI: TooManyRequests: Raised when the Flowery API returns a 429 status code ClientError: Raised when the Flowery API returns a 4xx status code InternalServerError: Raised when the Flowery API returns a 5xx status code + ResponseError: Raised when the Flowery API returns an empty response or a response with an unexpected format RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded Returns: @@ -132,5 +144,13 @@ class FloweryAPI: } if voice: params["voice"] = voice.id if isinstance(voice, Voice) else voice + request = await self.adapter.get(endpoint="/tts", params=params, timeout=180) - return request.data + + if request is not None: + if isinstance(request.data, bytes): + return request.data + else: + raise ResponseError(f"Invalid response from Flowery API: {request.data!r}") + else: + raise ResponseError("Invalid response from Flowery API: Empty Response!}") diff --git a/pyflowery/rest_adapter.py b/pyflowery/rest_adapter.py index 0a047e7..6df65a5 100644 --- a/pyflowery/rest_adapter.py +++ b/pyflowery/rest_adapter.py @@ -1,4 +1,5 @@ """This module contains the RestAdapter class, which is used to make requests to the Flowery API.""" "" + from asyncio import sleep as asleep from json import JSONDecodeError @@ -27,7 +28,7 @@ class RestAdapter: self._url = "https://api.flowery.pw/v1" self.config = config - async def _do(self, http_method: str, endpoint: str, params: dict = None, timeout: float = 60) -> Result | None: + async def _do(self, http_method: str, endpoint: str, params: dict | None = None, timeout: float = 60) -> Result | None: """Internal method to make a request to the Flowery API. You shouldn't use this directly. Args: @@ -55,26 +56,26 @@ class RestAdapter: async with aiohttp.ClientSession() as session: while retry_counter < self.config.retry_limit: self.config.logger.debug("Making %s request to %s with headers %s and params %s", http_method, full_url, headers, sanitized_params) - async with session.request(method=http_method, url=full_url, params=sanitized_params, headers=headers, timeout=timeout) as response: + async with session.request(method=http_method, url=full_url, params=sanitized_params, headers=headers, timeout=timeout) as response: # type: ignore try: data = await response.json() except (JSONDecodeError, aiohttp.ContentTypeError): data = await response.read() result = Result( - success=response.status, + success=response.status < 300, status_code=response.status, - message=response.reason, + message=response.reason or "Unknown error", data=data, ) self.config.logger.debug("Received response: %s %s", response.status, response.reason) try: if result.status_code == 429: - raise TooManyRequests(f"{result.message} - {result.data}") + raise TooManyRequests(f"{result.message} - {result.data!r}") if 400 <= result.status_code < 500: - raise ClientError(f"{result.status_code} - {result.message} - {result.data}") + raise ClientError(f"{result.status_code} - {result.message} - {result.data!r}") if 500 <= result.status_code < 600: - raise InternalServerError(f"{result.status_code} - {result.message} - {result.data}") + raise InternalServerError(f"{result.status_code} - {result.message} - {result.data!r}") except (TooManyRequests, InternalServerError) as e: if retry_counter < self.config.retry_limit: interval = self.config.interval * retry_counter @@ -84,8 +85,9 @@ class RestAdapter: continue raise RetryLimitExceeded(message=f"Request failed more than {self.config.retry_limit} times, not retrying") from e return result + return None - async def get(self, endpoint: str, params: dict = None, timeout: float = 60) -> Result | None: + async def get(self, endpoint: str, params: dict | None = None, timeout: float = 60) -> Result | None: """Make a GET request to the Flowery API. You should almost never have to use this directly. If you need to use this method because an endpoint is missing, please open an issue on the [CoastalCommits repository](https://www.coastalcommits.com/cswimr/PyFlowery/issues). diff --git a/pyflowery/version.py b/pyflowery/version.py index ea9d694..ce31092 100644 --- a/pyflowery/version.py +++ b/pyflowery/version.py @@ -1 +1 @@ -VERSION = "3.0.0" +VERSION = "3.0.1" diff --git a/pyproject.toml b/pyproject.toml index b4b5cff..33189c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyflowery" -version = "3.0.0" +version = "3.0.1" description = "A Python API wrapper for the Flowery API" readme = "README.md" requires-python = "<4.0,>=3.11" diff --git a/tests/tests.py b/tests/tests.py index 1348f2e..185f18d 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -22,8 +22,10 @@ STORMTROOPER = "191c5adc-a092-5eea-b4ff-ce01f66153ae" # TikTok async def test_fetch_tts() -> None: """Test the fetch_tts method""" - voice = api.get_voices(voice_id=ALEXANDER)[0] - tts = await api.fetch_tts(text="Sphinx of black quartz, judge my vow. The quick brown fox jumps over a lazy dog.", voice=voice) + voice = api.get_voices(voice_id=ALEXANDER) + if voice is None: + raise ValueError("Voice not found") + tts = await api.fetch_tts(text="Sphinx of black quartz, judge my vow. The quick brown fox jumps over a lazy dog.", voice=voice[0]) try: with open(file="test.mp3", mode="wb") as f: f.write(tts)