(1.0.3) retry handlers and more
added automatic retry handlers for 429 and 5xx error codes, as well as custom exceptions for ratelimiting, client errors, and server errors also added a more advanced user agent string constructor. previously, setting FloweryAPIConfig.user_agent would override the default user_agent. so, if you set user_agent to `foobar`, the user agent string would be `foobar` in requests. now, the user agent string sent by requests would be the following on my development machine: `'User-Agent': 'PyFlowery/1.0.3 PyFloweryTests (Python 3.12.6 (main, Sep 8 2024, 13:18:56) [GCC 14.2.1 20240805])'`
This commit is contained in:
parent
d585072d3e
commit
33b63fecc4
8 changed files with 89 additions and 25 deletions
|
@ -1,3 +1,9 @@
|
||||||
|
from pyflowery.exceptions import (
|
||||||
|
ClientError,
|
||||||
|
InternalServerError,
|
||||||
|
RetryLimitExceeded,
|
||||||
|
TooManyRequests,
|
||||||
|
)
|
||||||
from pyflowery.models import FloweryAPIConfig, Language, Result, Voice
|
from pyflowery.models import FloweryAPIConfig, Language, Result, Voice
|
||||||
from pyflowery.pyflowery import FloweryAPI
|
from pyflowery.pyflowery import FloweryAPI
|
||||||
from pyflowery.version import VERSION
|
from pyflowery.version import VERSION
|
||||||
|
@ -5,8 +11,12 @@ from pyflowery.version import VERSION
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'FloweryAPI',
|
'FloweryAPI',
|
||||||
'FloweryAPIConfig',
|
'FloweryAPIConfig',
|
||||||
|
'Language',
|
||||||
'Result',
|
'Result',
|
||||||
'Voice',
|
'Voice',
|
||||||
'Language',
|
|
||||||
'VERSION',
|
'VERSION',
|
||||||
|
'ClientError',
|
||||||
|
'InternalServerError',
|
||||||
|
'RetryLimitExceeded',
|
||||||
|
'TooManyRequests',
|
||||||
]
|
]
|
||||||
|
|
19
pyflowery/exceptions.py
Normal file
19
pyflowery/exceptions.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
class InternalServerError(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
super(InternalServerError, self).__init__(message)
|
||||||
|
|
||||||
|
class ClientError(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
super(ClientError, self).__init__(message)
|
||||||
|
|
||||||
|
class TooManyRequests(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
super(TooManyRequests, self).__init__(message)
|
||||||
|
|
||||||
|
class RetryLimitExceeded(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
super(RetryLimitExceeded, self).__init__(message)
|
|
@ -1,5 +1,6 @@
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from logging import Logger, getLogger
|
from logging import Logger, getLogger
|
||||||
|
from sys import version as pyversion
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
from pyflowery.version import VERSION
|
from pyflowery.version import VERSION
|
||||||
|
@ -54,9 +55,20 @@ class FloweryAPIConfig:
|
||||||
"""Configuration for the Flowery API
|
"""Configuration for the Flowery API
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
user_agent (str): User-Agent string to use for the HTTP requests
|
user_agent (str | None): User-Agent string to use for the HTTP requests
|
||||||
logger (Logger): Logger to use for logging messages
|
logger (Logger): Logger to use for logging messages
|
||||||
|
allow_truncation (bool): Whether to allow truncation of text that is too long
|
||||||
|
retry_limit (int): Number of times to retry a request before giving up
|
||||||
|
interval (int): Seconds to wait between each retried request, multiplied by how many attempted requests have been made
|
||||||
"""
|
"""
|
||||||
user_agent: str = f"PyFlowery/{VERSION}"
|
user_agent: str | None = None
|
||||||
logger: Logger = getLogger('pyflowery')
|
logger: Logger = getLogger('pyflowery')
|
||||||
allow_truncation: bool = False
|
allow_truncation: bool = False
|
||||||
|
retry_limit: int = 3
|
||||||
|
interval: int = 5
|
||||||
|
|
||||||
|
def prepended_user_agent(self) -> str:
|
||||||
|
"""Return the user_agent with the PyFlowery module version prepended"""
|
||||||
|
if not self.user_agent:
|
||||||
|
return f"PyFlowery/{VERSION} (Python {pyversion})"
|
||||||
|
return f"PyFlowery/{VERSION} {self.user_agent} (Python {pyversion})"
|
||||||
|
|
|
@ -72,6 +72,4 @@ 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('/tts', params, timeout=180)
|
request = await self.adapter.get('/tts', params, timeout=180)
|
||||||
if request.status_code in range(400, 600):
|
|
||||||
raise ValueError(request.data['message'])
|
|
||||||
return request.data
|
return request.data
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
"""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 json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
from pyflowery.exceptions import (
|
||||||
|
ClientError,
|
||||||
|
InternalServerError,
|
||||||
|
RetryLimitExceeded,
|
||||||
|
TooManyRequests,
|
||||||
|
)
|
||||||
from pyflowery.models import FloweryAPIConfig, Result
|
from pyflowery.models import FloweryAPIConfig, Result
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,8 +24,7 @@ class RestAdapter:
|
||||||
"""
|
"""
|
||||||
def __init__(self, config = FloweryAPIConfig):
|
def __init__(self, config = FloweryAPIConfig):
|
||||||
self._url = "https://api.flowery.pw/v1"
|
self._url = "https://api.flowery.pw/v1"
|
||||||
self._user_agent = config.user_agent
|
self.config = config
|
||||||
self._logger = config.logger
|
|
||||||
|
|
||||||
async def _do(self, http_method: str, endpoint: str, params: dict = None, timeout: float = 60):
|
async def _do(self, http_method: str, endpoint: str, params: dict = None, timeout: float = 60):
|
||||||
"""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.
|
||||||
|
@ -34,26 +40,45 @@ class RestAdapter:
|
||||||
"""
|
"""
|
||||||
full_url = self._url + endpoint
|
full_url = self._url + endpoint
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': self._user_agent,
|
'User-Agent': self.config.prepended_user_agent(),
|
||||||
}
|
}
|
||||||
sanitized_params = {k: str(v) if isinstance(v, bool) else v for k, v in params.items()} if params else None
|
sanitized_params = {k: str(v) if isinstance(v, bool) else v for k, v in params.items()} if params else None
|
||||||
self._logger.debug("Making %s request to %s with params %s", http_method, full_url, sanitized_params)
|
retry_counter = 0
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.request(method=http_method, url=full_url, params=sanitized_params, headers=headers, timeout=timeout) as response:
|
while retry_counter < self.config.retry_limit:
|
||||||
try:
|
self.config.logger.debug("Making %s request to %s with headers %s and params %s", http_method, full_url, headers, sanitized_params)
|
||||||
data = await response.json()
|
async with session.request(method=http_method, url=full_url, params=sanitized_params, headers=headers, timeout=timeout) as response:
|
||||||
except (JSONDecodeError, aiohttp.ContentTypeError):
|
try:
|
||||||
data = await response.read()
|
data = await response.json()
|
||||||
|
except (JSONDecodeError, aiohttp.ContentTypeError):
|
||||||
|
data = await response.read()
|
||||||
|
|
||||||
result = Result(
|
result = Result(
|
||||||
success=response.status in range(200, 299),
|
success=response.status,
|
||||||
status_code=response.status,
|
status_code=response.status,
|
||||||
message=response.reason,
|
message=response.reason,
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
self._logger.debug("Received response: %s %s", response.status, response.reason)
|
self.config.logger.debug("Received response: %s %s", response.status, response.reason)
|
||||||
return result
|
try:
|
||||||
|
if result.status_code == 429:
|
||||||
|
raise TooManyRequests(f"{result.message} - {result.data}")
|
||||||
|
elif 400 <= result.status_code < 500:
|
||||||
|
raise ClientError(f"{result.status_code} - {result.message} - {result.data}")
|
||||||
|
elif 500 <= result.status_code < 600:
|
||||||
|
raise InternalServerError(f"{result.status_code} - {result.message} - {result.data}")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
except (TooManyRequests, InternalServerError) as e:
|
||||||
|
if retry_counter < self.config.retry_limit:
|
||||||
|
interval = self.config.interval * retry_counter
|
||||||
|
self.config.logger.error("%s - retrying in %s seconds", e, interval, exc_info=True)
|
||||||
|
retry_counter += 1
|
||||||
|
await asleep(interval)
|
||||||
|
continue
|
||||||
|
raise RetryLimitExceeded(message=f"Request failed more than {self.config.retry_limit} times, not retrying") from e
|
||||||
|
return result
|
||||||
|
|
||||||
async def get(self, endpoint: str, params: dict = None, timeout: float = 60) -> Result:
|
async def get(self, endpoint: str, params: dict = None, timeout: float = 60) -> Result:
|
||||||
"""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.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
VERSION = "1.0.2"
|
VERSION = "1.0.3"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pyflowery"
|
name = "pyflowery"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
description = "A Python API wrapper for the Flowery API"
|
description = "A Python API wrapper for the Flowery API"
|
||||||
authors = ["cswimr <seaswimmerthefsh@gmail.com>"]
|
authors = ["cswimr <seaswimmerthefsh@gmail.com>"]
|
||||||
license = "GPL 3.0-only"
|
license = "GPL 3.0-only"
|
||||||
|
|
|
@ -14,7 +14,7 @@ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(messag
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
root.addHandler(handler)
|
root.addHandler(handler)
|
||||||
|
|
||||||
api = FloweryAPI(FloweryAPIConfig())
|
api = FloweryAPI(FloweryAPIConfig(user_agent="PyFloweryTests"))
|
||||||
|
|
||||||
ALEXANDER = "fa3ea565-121f-5efd-b4e9-59895c77df23" # TikTok
|
ALEXANDER = "fa3ea565-121f-5efd-b4e9-59895c77df23" # TikTok
|
||||||
JACOB = "38f45366-68e8-5d39-b1ef-3fd4eeb61cdb" # Microsoft Azure
|
JACOB = "38f45366-68e8-5d39-b1ef-3fd4eeb61cdb" # Microsoft Azure
|
||||||
|
|
Loading…
Reference in a new issue