(3.0.1) mypy compliance
This commit is contained in:
parent
18d2c504c7
commit
a7113babb7
7 changed files with 81 additions and 41 deletions
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
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.
|
"""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,20 +85,27 @@ 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:
|
||||||
yield Voice(
|
if isinstance(request.data, dict):
|
||||||
id=voice["id"],
|
for voice in request.data.get("voices", []):
|
||||||
name=voice["name"],
|
yield Voice(
|
||||||
gender=voice["gender"],
|
id=voice["id"],
|
||||||
source=voice["source"],
|
name=voice["name"],
|
||||||
language=Language(**voice["language"]),
|
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(
|
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)
|
||||||
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!}")
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
VERSION = "3.0.0"
|
VERSION = "3.0.1"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue