From 33b63fecc4bea73c5bd9403b0d1723fc55826d21 Mon Sep 17 00:00:00 2001 From: cswimr Date: Tue, 17 Sep 2024 23:24:14 -0400 Subject: [PATCH] (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])'` --- pyflowery/__init__.py | 12 +++++++- pyflowery/exceptions.py | 19 +++++++++++++ pyflowery/models.py | 16 +++++++++-- pyflowery/pyflowery.py | 2 -- pyflowery/rest_adapter.py | 59 ++++++++++++++++++++++++++++----------- pyflowery/version.py | 2 +- pyproject.toml | 2 +- tests/tests.py | 2 +- 8 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 pyflowery/exceptions.py diff --git a/pyflowery/__init__.py b/pyflowery/__init__.py index 7d8a8e6..969f590 100644 --- a/pyflowery/__init__.py +++ b/pyflowery/__init__.py @@ -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', ] diff --git a/pyflowery/exceptions.py b/pyflowery/exceptions.py new file mode 100644 index 0000000..b1c2ed0 --- /dev/null +++ b/pyflowery/exceptions.py @@ -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) diff --git a/pyflowery/models.py b/pyflowery/models.py index b57cc8d..8203e7a 100644 --- a/pyflowery/models.py +++ b/pyflowery/models.py @@ -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})" diff --git a/pyflowery/pyflowery.py b/pyflowery/pyflowery.py index fb3ecfd..f868dc2 100644 --- a/pyflowery/pyflowery.py +++ b/pyflowery/pyflowery.py @@ -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 diff --git a/pyflowery/rest_adapter.py b/pyflowery/rest_adapter.py index 9b1d490..ab9fe71 100644 --- a/pyflowery/rest_adapter.py +++ b/pyflowery/rest_adapter.py @@ -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. diff --git a/pyflowery/version.py b/pyflowery/version.py index 2be0457..2f2e521 100644 --- a/pyflowery/version.py +++ b/pyflowery/version.py @@ -1 +1 @@ -VERSION = "1.0.2" +VERSION = "1.0.3" diff --git a/pyproject.toml b/pyproject.toml index f4ad24d..730225a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] license = "GPL 3.0-only" diff --git a/tests/tests.py b/tests/tests.py index 0c7027c..b4b7cb8 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -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