(3.0.1) mypy compliance
Some checks failed
Actions / Build (push) Successful in 18s
Actions / Lint with Ruff & Pylint (push) Failing after 15s
Actions / Build Documentation (push) Successful in 24s

This commit is contained in:
Seaswimmer 2024-11-15 09:51:11 -05:00
parent 18d2c504c7
commit a7113babb7
Signed by: cswimr
GPG key ID: A9C162E867C851FA
7 changed files with 81 additions and 41 deletions

View file

@ -1,6 +1,7 @@
from pyflowery.exceptions import ( from pyflowery.exceptions import (
ClientError, ClientError,
InternalServerError, InternalServerError,
ResponseError,
RetryLimitExceeded, RetryLimitExceeded,
TooManyRequests, TooManyRequests,
) )
@ -9,14 +10,15 @@ from pyflowery.pyflowery import FloweryAPI
from pyflowery.version import VERSION from pyflowery.version import VERSION
__all__ = [ __all__ = [
'FloweryAPI', "FloweryAPI",
'FloweryAPIConfig', "FloweryAPIConfig",
'Language', "Language",
'Result', "Result",
'Voice', "Voice",
'VERSION', "VERSION",
'ClientError', "ResponseError",
'InternalServerError', "ClientError",
'RetryLimitExceeded', "InternalServerError",
'TooManyRequests', "RetryLimitExceeded",
"TooManyRequests",
] ]

View file

@ -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): class InternalServerError(Exception):
"""Raised when the API returns a 5xx status code""" """Raised when the API returns a 5xx status code"""
def __init__(self, message):
def __init__(self, message) -> None:
self.message = message self.message = message
class ClientError(Exception): class ClientError(Exception):
"""Raised when the API returns a 4xx status code""" """Raised when the API returns a 4xx status code"""
def __init__(self, message):
def __init__(self, message) -> None:
self.message = message self.message = message
class TooManyRequests(Exception): class TooManyRequests(Exception):
"""Raised when the API returns a 429 status code""" """Raised when the API returns a 429 status code"""
def __init__(self, message):
def __init__(self, message) -> None:
self.message = message self.message = message
class RetryLimitExceeded(Exception): class RetryLimitExceeded(Exception):
"""Raised when the retry limit is exceeded""" """Raised when the retry limit is exceeded"""
def __init__(self, message):
def __init__(self, message) -> None:
self.message = message self.message = message

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
from typing import AsyncGenerator, Tuple from typing import AsyncGenerator, Tuple
from pyflowery.exceptions import ResponseError
from pyflowery.models import FloweryAPIConfig, Language, Voice from pyflowery.models import FloweryAPIConfig, Language, Voice
from pyflowery.rest_adapter import RestAdapter from pyflowery.rest_adapter import RestAdapter
@ -15,8 +16,8 @@ class FloweryAPI:
def __init__(self, config: FloweryAPIConfig) -> None: def __init__(self, config: FloweryAPIConfig) -> None:
self.config = config self.config = config
self.adapter = RestAdapter(config) self.adapter = RestAdapter(config=config)
self._voices_cache: Tuple[Voice] = () self._voices_cache: Tuple[Voice, ...] = ()
try: try:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
except RuntimeError: except RuntimeError:
@ -30,9 +31,12 @@ class FloweryAPI:
async def _populate_voices_cache(self) -> None: 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.""" """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._voices_cache = tuple([voice async for voice in self.fetch_voices()]) # pylint: disable=consider-using-generator
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!") 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. """Get a set of voices from the cache.
Args: Args:
@ -43,8 +47,8 @@ class FloweryAPI:
Tuple[Voice] | None: A tuple of Voice objects if found, otherwise None Tuple[Voice] | None: A tuple of Voice objects if found, otherwise None
""" """
if voice_id: if voice_id:
voice = next((voice for voice in self._voices_cache if voice.id == voice_id)) voice = next((voice for voice in self._voices_cache if voice.id == voice_id), None)
return (voice,) or None return (voice,) if voice else None
if name: if name:
voices = [] voices = []
for voice in self._voices_cache: for voice in self._voices_cache:
@ -81,13 +85,16 @@ class FloweryAPI:
TooManyRequests: Raised when the Flowery API returns a 429 status code TooManyRequests: Raised when the Flowery API returns a 429 status code
ClientError: Raised when the Flowery API returns a 4xx status code ClientError: Raised when the Flowery API returns a 4xx status code
InternalServerError: Raised when the Flowery API returns a 5xx 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 RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded
Returns: Returns:
AsyncGenerator[Voice, None]: A generator of Voices AsyncGenerator[Voice, None]: A generator of Voices
""" """
request = await self.adapter.get(endpoint="/tts/voices") request = await self.adapter.get(endpoint="/tts/voices")
for voice in request.data["voices"]: if request is not None:
if isinstance(request.data, dict):
for voice in request.data.get("voices", []):
yield Voice( yield Voice(
id=voice["id"], id=voice["id"],
name=voice["name"], name=voice["name"],
@ -95,6 +102,10 @@ class FloweryAPI:
source=voice["source"], source=voice["source"],
language=Language(**voice["language"]), 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( 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 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 TooManyRequests: Raised when the Flowery API returns a 429 status code
ClientError: Raised when the Flowery API returns a 4xx status code ClientError: Raised when the Flowery API returns a 4xx status code
InternalServerError: Raised when the Flowery API returns a 5xx 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 RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded
Returns: Returns:
@ -132,5 +144,13 @@ class FloweryAPI:
} }
if voice: if voice:
params["voice"] = voice.id if isinstance(voice, Voice) else voice params["voice"] = voice.id if isinstance(voice, Voice) else voice
request = await self.adapter.get(endpoint="/tts", params=params, timeout=180) request = await self.adapter.get(endpoint="/tts", params=params, timeout=180)
if request is not None:
if isinstance(request.data, bytes):
return request.data 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!}")

View file

@ -1,4 +1,5 @@
"""This module contains the RestAdapter class, which is used to make requests to the Flowery API.""" "" """This module contains the RestAdapter class, which is used to make requests to the Flowery API.""" ""
from asyncio import sleep as asleep from asyncio import sleep as asleep
from json import JSONDecodeError from json import JSONDecodeError
@ -27,7 +28,7 @@ class RestAdapter:
self._url = "https://api.flowery.pw/v1" self._url = "https://api.flowery.pw/v1"
self.config = config 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. """Internal method to make a request to the Flowery API. You shouldn't use this directly.
Args: Args:
@ -55,26 +56,26 @@ class RestAdapter:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
while retry_counter < self.config.retry_limit: 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) 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: try:
data = await response.json() data = await response.json()
except (JSONDecodeError, aiohttp.ContentTypeError): except (JSONDecodeError, aiohttp.ContentTypeError):
data = await response.read() data = await response.read()
result = Result( result = Result(
success=response.status, success=response.status < 300,
status_code=response.status, status_code=response.status,
message=response.reason, message=response.reason or "Unknown error",
data=data, data=data,
) )
self.config.logger.debug("Received response: %s %s", response.status, response.reason) self.config.logger.debug("Received response: %s %s", response.status, response.reason)
try: try:
if result.status_code == 429: 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: 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: 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: except (TooManyRequests, InternalServerError) as e:
if retry_counter < self.config.retry_limit: if retry_counter < self.config.retry_limit:
interval = self.config.interval * retry_counter interval = self.config.interval * retry_counter
@ -84,8 +85,9 @@ class RestAdapter:
continue continue
raise RetryLimitExceeded(message=f"Request failed more than {self.config.retry_limit} times, not retrying") from e raise RetryLimitExceeded(message=f"Request failed more than {self.config.retry_limit} times, not retrying") from e
return result 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. """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). 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).

View file

@ -1 +1 @@
VERSION = "3.0.0" VERSION = "3.0.1"

View file

@ -1,6 +1,6 @@
[project] [project]
name = "pyflowery" name = "pyflowery"
version = "3.0.0" version = "3.0.1"
description = "A Python API wrapper for the Flowery API" description = "A Python API wrapper for the Flowery API"
readme = "README.md" readme = "README.md"
requires-python = "<4.0,>=3.11" requires-python = "<4.0,>=3.11"

View file

@ -22,8 +22,10 @@ STORMTROOPER = "191c5adc-a092-5eea-b4ff-ce01f66153ae" # TikTok
async def test_fetch_tts() -> None: async def test_fetch_tts() -> None:
"""Test the fetch_tts method""" """Test the fetch_tts method"""
voice = api.get_voices(voice_id=ALEXANDER)[0] voice = api.get_voices(voice_id=ALEXANDER)
tts = await api.fetch_tts(text="Sphinx of black quartz, judge my vow. The quick brown fox jumps over a lazy dog.", voice=voice) 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: try:
with open(file="test.mp3", mode="wb") as f: with open(file="test.mp3", mode="wb") as f:
f.write(tts) f.write(tts)