(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.pyflowery import FloweryAPI
from pyflowery.version import VERSION
@ -5,8 +11,12 @@ from pyflowery.version import VERSION
__all__ = [
'FloweryAPI',
'FloweryAPIConfig',
'Language',
'Result',
'Voice',
'Language',
'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 logging import Logger, getLogger
from sys import version as pyversion
from typing import Dict, List, Union
from pyflowery.version import VERSION
@ -54,9 +55,20 @@ class FloweryAPIConfig:
"""Configuration for the Flowery API
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
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')
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:
params['voice'] = voice.id if isinstance(voice, Voice) else voice
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

View file

@ -1,8 +1,15 @@
"""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
import aiohttp
from pyflowery.exceptions import (
ClientError,
InternalServerError,
RetryLimitExceeded,
TooManyRequests,
)
from pyflowery.models import FloweryAPIConfig, Result
@ -17,8 +24,7 @@ class RestAdapter:
"""
def __init__(self, config = FloweryAPIConfig):
self._url = "https://api.flowery.pw/v1"
self._user_agent = config.user_agent
self._logger = config.logger
self.config = config
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.
@ -34,26 +40,45 @@ class RestAdapter:
"""
full_url = self._url + endpoint
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
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 session.request(method=http_method, url=full_url, params=sanitized_params, headers=headers, timeout=timeout) as response:
try:
data = await response.json()
except (JSONDecodeError, aiohttp.ContentTypeError):
data = await response.read()
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:
try:
data = await response.json()
except (JSONDecodeError, aiohttp.ContentTypeError):
data = await response.read()
result = Result(
success=response.status in range(200, 299),
status_code=response.status,
message=response.reason,
data=data,
)
self._logger.debug("Received response: %s %s", response.status, response.reason)
return result
result = Result(
success=response.status,
status_code=response.status,
message=response.reason,
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}")
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:
"""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]
name = "pyflowery"
version = "1.0.2"
version = "1.0.3"
description = "A Python API wrapper for the Flowery API"
authors = ["cswimr <seaswimmerthefsh@gmail.com>"]
license = "GPL 3.0-only"

View file

@ -14,7 +14,7 @@ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(messag
handler.setFormatter(formatter)
root.addHandler(handler)
api = FloweryAPI(FloweryAPIConfig())
api = FloweryAPI(FloweryAPIConfig(user_agent="PyFloweryTests"))
ALEXANDER = "fa3ea565-121f-5efd-b4e9-59895c77df23" # TikTok
JACOB = "38f45366-68e8-5d39-b1ef-3fd4eeb61cdb" # Microsoft Azure