(1.0.3) retry handlers and more
Some checks failed
Actions / lint (push) Failing after 16s
Actions / build (push) Successful in 19s

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:
Seaswimmer 2024-09-17 23:24:14 -04:00
parent d585072d3e
commit 33b63fecc4
Signed by: cswimr
GPG key ID: 3813315477F26F82
8 changed files with 89 additions and 25 deletions

View file

@ -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
View 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)

View file

@ -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})"

View file

@ -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

View file

@ -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.

View file

@ -1 +1 @@
VERSION = "1.0.2" VERSION = "1.0.3"

View file

@ -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"

View file

@ -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