diff --git a/src/chaturbate_poller/__main__.py b/src/chaturbate_poller/__main__.py index 75806d11..166e9153 100644 --- a/src/chaturbate_poller/__main__.py +++ b/src/chaturbate_poller/__main__.py @@ -59,6 +59,7 @@ async def start_polling( # pylint: disable=too-many-arguments # noqa: PLR0913 """Start polling Chaturbate events.""" if verbose is True: logging.getLogger("chaturbate_poller").setLevel(logging.DEBUG) + if not username or not token: logging.error( "CB_USERNAME and CB_TOKEN must be provided as arguments or environment variables." diff --git a/src/chaturbate_poller/chaturbate_client.py b/src/chaturbate_poller/chaturbate_client.py index 1802f552..991cdeb6 100644 --- a/src/chaturbate_poller/chaturbate_client.py +++ b/src/chaturbate_poller/chaturbate_client.py @@ -1,62 +1,15 @@ """Chaturbate poller module.""" import logging -from logging.config import dictConfig from types import TracebackType import httpx from backoff import constant, expo, on_exception -from backoff._typing import Details from chaturbate_poller.constants import DEFAULT_BASE_URL, TESTBED_BASE_URL, HttpStatusCode from chaturbate_poller.influxdb_client import InfluxDBHandler -from chaturbate_poller.logging_config import LOGGING_CONFIG from chaturbate_poller.models import EventsAPIResponse - -dictConfig(LOGGING_CONFIG) - -logger = logging.getLogger(__name__) -"""Logger for the module.""" - - -def backoff_handler(details: Details) -> None: - """Handle backoff events. - - Args: - details (Details): The backoff details. - """ - wait = details["wait"] - tries = details["tries"] - logger.info("Backing off %s seconds after %s tries", int(wait), int(tries)) - - -def giveup_handler(details: Details) -> None: - """Handle giveup events. - - Args: - details (Details): The giveup details. - """ - tries = details.get("tries", 0) - exception = details.get("exception") - - if exception and hasattr(exception, "response"): - response = exception.response - status_code = response.status_code - try: - response_dict = response.json() - status_text = response_dict.get("status", "Unknown error") - except ValueError: - status_text = "Error parsing response JSON" - else: - status_code = None - status_text = "No response available" - - logger.error( - "Giving up after %s tries due to server error code %s: %s", - int(tries), - status_code, - status_text, - ) +from chaturbate_poller.utils import ChaturbateUtils class ChaturbateClient: @@ -91,6 +44,7 @@ def __init__( self.token = token self._client: httpx.AsyncClient | None = None self.influxdb_handler: InfluxDBHandler = InfluxDBHandler() + self.logger = logging.getLogger(__name__) @property def client(self) -> httpx.AsyncClient: @@ -127,8 +81,8 @@ async def __aexit__( jitter=None, exception=httpx.ReadError, max_tries=10, - on_giveup=giveup_handler, - on_backoff=backoff_handler, + on_giveup=ChaturbateUtils().giveup_handler, + on_backoff=ChaturbateUtils().backoff_handler, logger=None, ) @on_exception( @@ -137,10 +91,10 @@ async def __aexit__( base=1.25, factor=5, exception=httpx.HTTPStatusError, - giveup=lambda retry: not need_retry(retry), - on_giveup=giveup_handler, + giveup=lambda retry: not ChaturbateUtils().need_retry(retry), + on_giveup=ChaturbateUtils().giveup_handler, max_tries=6, - on_backoff=backoff_handler, + on_backoff=ChaturbateUtils().backoff_handler, logger=None, raise_on_giveup=False, ) @@ -158,7 +112,7 @@ async def fetch_events(self, url: str | None = None) -> EventsAPIResponse: httpx.HTTPStatusError: For other HTTP errors. """ url = url or self._construct_url() - logger.debug("%s", url) + self.logger.debug("%s", url) response = await self.client.get(url, timeout=None) try: response.raise_for_status() @@ -178,25 +132,3 @@ def _construct_url(self) -> str: """ timeout_param = f"?timeout={self.timeout}" if self.timeout else "" return f"{self.base_url.format(username=self.username, token=self.token)}{timeout_param}" - - -def need_retry(exception: Exception) -> bool: - """Determine if the request should be retried based on the exception. - - Args: - exception (Exception): The exception raised. - - Returns: - bool: True if the request should be retried, False otherwise. - """ - if isinstance(exception, httpx.HTTPStatusError): - status_code = exception.response.status_code - if status_code in { - HttpStatusCode.INTERNAL_SERVER_ERROR, - HttpStatusCode.BAD_GATEWAY, - HttpStatusCode.SERVICE_UNAVAILABLE, - HttpStatusCode.GATEWAY_TIMEOUT, - HttpStatusCode.WEB_SERVER_IS_DOWN, - }: - return True - return False diff --git a/src/chaturbate_poller/utils.py b/src/chaturbate_poller/utils.py new file mode 100644 index 00000000..88425cb2 --- /dev/null +++ b/src/chaturbate_poller/utils.py @@ -0,0 +1,75 @@ +"""Utility functions for the Chaturbate poller.""" + +import logging + +import httpx +from backoff._typing import Details + +from chaturbate_poller.constants import HttpStatusCode + + +class ChaturbateUtils: + """Utility functions for the Chaturbate poller.""" + + def __init__(self) -> None: + """Initialize the utility class.""" + self.logger = logging.getLogger(__name__) + + def backoff_handler(self, details: Details) -> None: + """Handle backoff events. + + Args: + details (Details): The backoff details. + """ + wait = details["wait"] + tries = details["tries"] + self.logger.info("Backing off %s seconds after %s tries", int(wait), int(tries)) + + def giveup_handler(self, details: Details) -> None: + """Handle giveup events. + + Args: + details (Details): The giveup details. + """ + tries = details.get("tries", 0) + exception = details.get("exception") + + if exception and hasattr(exception, "response"): + response = exception.response + status_code = response.status_code + try: + response_dict = response.json() + status_text = response_dict.get("status", "Unknown error") + except ValueError: + status_text = "Error parsing response JSON" + else: + status_code = None + status_text = "No response available" + + self.logger.error( + "Giving up after %s tries due to server error code %s: %s", + int(tries), + status_code, + status_text, + ) + + def need_retry(self, exception: Exception) -> bool: + """Determine if the request should be retried based on the exception. + + Args: + exception (Exception): The exception raised. + + Returns: + bool: True if the request should be retried, False otherwise. + """ + if isinstance(exception, httpx.HTTPStatusError): + status_code = exception.response.status_code + if status_code in { + HttpStatusCode.INTERNAL_SERVER_ERROR, + HttpStatusCode.BAD_GATEWAY, + HttpStatusCode.SERVICE_UNAVAILABLE, + HttpStatusCode.GATEWAY_TIMEOUT, + HttpStatusCode.WEB_SERVER_IS_DOWN, + }: + return True + return False diff --git a/tests/test_chaturbate_client.py b/tests/test_chaturbate_client.py index a443d646..a4d5b087 100644 --- a/tests/test_chaturbate_client.py +++ b/tests/test_chaturbate_client.py @@ -18,12 +18,7 @@ from pydantic import ValidationError from pytest_mock import MockerFixture -from chaturbate_poller.chaturbate_client import ( - ChaturbateClient, - backoff_handler, - giveup_handler, - need_retry, -) +from chaturbate_poller.chaturbate_client import ChaturbateClient from chaturbate_poller.constants import API_TIMEOUT, TESTBED_BASE_URL, HttpStatusCode from chaturbate_poller.format_messages import format_message, format_user_event from chaturbate_poller.logging_config import LOGGING_CONFIG, CustomFormatter @@ -37,6 +32,7 @@ Tip, User, ) +from chaturbate_poller.utils import ChaturbateUtils USERNAME = "testuser" """str: The Chaturbate username.""" @@ -183,7 +179,7 @@ def test_backoff_handler(self, caplog) -> None: # noqa: ANN001 """Test the backoff handler.""" caplog.set_level(logging.INFO) # Providing required keys "wait" and "tries" in the details dict - backoff_handler( + ChaturbateUtils().backoff_handler( { "wait": 1.0, "tries": 1, @@ -198,7 +194,7 @@ def test_backoff_handler(self, caplog) -> None: # noqa: ANN001 def test_giveup_handler(self, caplog) -> None: # noqa: ANN001 """Test the giveup handler.""" caplog.set_level(logging.ERROR) - giveup_handler( + ChaturbateUtils().giveup_handler( { # type: ignore[typeddict-item] "tries": 6, "exception": HTTPStatusError( @@ -213,7 +209,7 @@ def test_giveup_handler(self, caplog) -> None: # noqa: ANN001 def test_giveup_handler_no_exception(self, caplog) -> None: # noqa: ANN001 """Test the giveup handler with no exception.""" caplog.set_level(logging.ERROR) - giveup_handler( + ChaturbateUtils().giveup_handler( { # type: ignore[typeddict-item] "tries": 6, } @@ -335,7 +331,7 @@ def test_need_retry( expected_retry: bool, # noqa: FBT001 ) -> None: """Test need_retry function.""" - assert need_retry(exception) == expected_retry + assert ChaturbateUtils().need_retry(exception) == expected_retry class TestTipModel: @@ -1032,9 +1028,9 @@ def test_http_status_error(self, status_code: int, *, expected: bool) -> None: request=Request("GET", "https://error.url.com"), response=Response(status_code), ) - assert need_retry(exception) == expected + assert ChaturbateUtils().need_retry(exception) == expected def test_non_http_status_error(self) -> None: """Test need_retry with a non-HTTPStatusError exception.""" exception = TimeoutException("Timeout occurred") - assert not need_retry(exception) + assert not ChaturbateUtils().need_retry(exception)