From fd31a3162bb865c65d834b426354f8b70d9afb84 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Tue, 10 Jun 2025 18:24:00 -0600 Subject: [PATCH] Replace debug prints with logging (#38) --- disagreement/__init__.py | 10 +- disagreement/ext/app_commands/handler.py | 37 +++-- disagreement/ext/commands/cog.py | 21 ++- disagreement/ext/commands/core.py | 30 +++-- disagreement/gateway.py | 165 ++++++++++++++--------- disagreement/http.py | 23 +++- 6 files changed, 188 insertions(+), 98 deletions(-) diff --git a/disagreement/__init__.py b/disagreement/__init__.py index 1e8527c..2e59fcc 100644 --- a/disagreement/__init__.py +++ b/disagreement/__init__.py @@ -35,7 +35,11 @@ from .enums import GatewayIntent, GatewayOpcode # Export enums from .error_handler import setup_global_error_handler from .hybrid_context import HybridContext from .ext import tasks +from .logging_config import setup_logging -# Set up logging if desired -# import logging -# logging.getLogger(__name__).addHandler(logging.NullHandler()) +import logging + + +# Configure a default logger if none has been configured yet +if not logging.getLogger().hasHandlers(): + setup_logging(logging.INFO) diff --git a/disagreement/ext/app_commands/handler.py b/disagreement/ext/app_commands/handler.py index 0ac29e2..b9ca1e6 100644 --- a/disagreement/ext/app_commands/handler.py +++ b/disagreement/ext/app_commands/handler.py @@ -1,6 +1,7 @@ # disagreement/ext/app_commands/handler.py import inspect +import logging from typing import ( TYPE_CHECKING, Dict, @@ -64,6 +65,9 @@ if not TYPE_CHECKING: Message = Any +logger = logging.getLogger(__name__) + + class AppCommandHandler: """ Manages application command registration, parsing, and dispatching. @@ -544,7 +548,7 @@ class AppCommandHandler: await command.invoke(ctx, *parsed_args, **parsed_kwargs) except Exception as e: - print(f"Error invoking app command '{command.name}': {e}") + logger.error("Error invoking app command '%s': %s", command.name, e) await self.dispatch_app_command_error(ctx, e) # else: # # Default error reply if no handler on client @@ -594,34 +598,43 @@ class AppCommandHandler: payload = cmd_or_group.to_dict() commands_to_sync.append(payload) except AttributeError: - print( - f"Warning: Command or group '{cmd_or_group.name}' does not have a to_dict() method. Skipping." + logger.warning( + "Command or group '%s' does not have a to_dict() method. Skipping.", + cmd_or_group.name, ) except Exception as e: - print( - f"Error converting command/group '{cmd_or_group.name}' to dict: {e}. Skipping." + logger.error( + "Error converting command/group '%s' to dict: %s. Skipping.", + cmd_or_group.name, + e, ) if not commands_to_sync: - print( - f"No commands to sync for {'guild ' + str(guild_id) if guild_id else 'global'} scope." + logger.info( + "No commands to sync for %s scope.", + f"guild {guild_id}" if guild_id else "global", ) return try: if guild_id: - print( - f"Syncing {len(commands_to_sync)} commands for guild {guild_id}..." + logger.info( + "Syncing %s commands for guild %s...", + len(commands_to_sync), + guild_id, ) await self.client._http.bulk_overwrite_guild_application_commands( application_id, guild_id, commands_to_sync ) else: - print(f"Syncing {len(commands_to_sync)} global commands...") + logger.info( + "Syncing %s global commands...", + len(commands_to_sync), + ) await self.client._http.bulk_overwrite_global_application_commands( application_id, commands_to_sync ) - print("Command sync successful.") + logger.info("Command sync successful.") except Exception as e: - print(f"Error syncing application commands: {e}") + logger.error("Error syncing application commands: %s", e) # Consider re-raising or specific error handling diff --git a/disagreement/ext/commands/cog.py b/disagreement/ext/commands/cog.py index b7c9302..f6f4680 100644 --- a/disagreement/ext/commands/cog.py +++ b/disagreement/ext/commands/cog.py @@ -1,6 +1,7 @@ # disagreement/ext/commands/cog.py import inspect +import logging from typing import TYPE_CHECKING, List, Tuple, Callable, Awaitable, Any, Dict, Union if TYPE_CHECKING: @@ -16,6 +17,8 @@ else: # pragma: no cover - runtime imports for isinstance checks # EventDispatcher might be needed if cogs register listeners directly # from disagreement.event_dispatcher import EventDispatcher +logger = logging.getLogger(__name__) + class Cog: """ @@ -59,8 +62,10 @@ class Cog: cmd.cog = self # Assign the cog instance to the command if cmd.name in self._commands: # This should ideally be caught earlier or handled by CommandHandler - print( - f"Warning: Duplicate command name '{cmd.name}' in cog '{self.cog_name}'. Overwriting." + logger.warning( + "Duplicate command name '%s' in cog '%s'. Overwriting.", + cmd.name, + self.cog_name, ) self._commands[cmd.name.lower()] = cmd # Also register aliases @@ -79,8 +84,10 @@ class Cog: # For AppCommandGroup, its commands will have cog set individually if they are AppCommands self._app_commands_and_groups.append(app_cmd_obj) else: - print( - f"Warning: Member '{member_name}' in cog '{self.cog_name}' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup." + logger.warning( + "Member '%s' in cog '%s' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup.", + member_name, + self.cog_name, ) elif isinstance(member, (AppCommand, AppCommandGroup)): @@ -92,8 +99,10 @@ class Cog: # This is a method decorated with @commands.Cog.listener or @commands.listener if not inspect.iscoroutinefunction(member): # Decorator should have caught this, but double check - print( - f"Warning: Listener '{member_name}' in cog '{self.cog_name}' is not a coroutine. Skipping." + logger.warning( + "Listener '%s' in cog '%s' is not a coroutine. Skipping.", + member_name, + self.cog_name, ) continue diff --git a/disagreement/ext/commands/core.py b/disagreement/ext/commands/core.py index 21dfa7b..adb816a 100644 --- a/disagreement/ext/commands/core.py +++ b/disagreement/ext/commands/core.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import logging import inspect from typing import ( TYPE_CHECKING, @@ -31,6 +32,8 @@ from .errors import ( from .converters import run_converters, DEFAULT_CONVERTERS, Converter from disagreement.typing import Typing +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from .cog import Cog from disagreement.client import Client @@ -224,8 +227,10 @@ class CommandHandler: self.commands[command.name.lower()] = command for alias in command.aliases: if alias in self.commands: - print( - f"Warning: Alias '{alias}' for command '{command.name}' conflicts with an existing command or alias." + logger.warning( + "Alias '%s' for command '%s' conflicts with an existing command or alias.", + alias, + command.name, ) self.commands[alias.lower()] = command @@ -241,6 +246,7 @@ class CommandHandler: def add_cog(self, cog_to_add: "Cog") -> None: from .cog import Cog + if not isinstance(cog_to_add, Cog): raise TypeError("Argument must be a subclass of Cog.") @@ -258,8 +264,9 @@ class CommandHandler: for event_name, callback in cog_to_add.get_listeners(): self.client._event_dispatcher.register(event_name.upper(), callback) else: - print( - f"Warning: Client does not have '_event_dispatcher'. Listeners for cog '{cog_to_add.cog_name}' not registered." + logger.warning( + "Client does not have '_event_dispatcher'. Listeners for cog '%s' not registered.", + cog_to_add.cog_name, ) if hasattr(cog_to_add, "cog_load") and inspect.iscoroutinefunction( @@ -267,7 +274,7 @@ class CommandHandler: ): asyncio.create_task(cog_to_add.cog_load()) - print(f"Cog '{cog_to_add.cog_name}' added.") + logger.info("Cog '%s' added.", cog_to_add.cog_name) def remove_cog(self, cog_name: str) -> Optional["Cog"]: cog_to_remove = self.cogs.pop(cog_name, None) @@ -277,8 +284,11 @@ class CommandHandler: if hasattr(self.client, "_event_dispatcher"): for event_name, callback in cog_to_remove.get_listeners(): - print( - f"Note: Listener '{callback.__name__}' for event '{event_name}' from cog '{cog_name}' needs manual unregistration logic in EventDispatcher." + logger.debug( + "Listener '%s' for event '%s' from cog '%s' needs manual unregistration logic in EventDispatcher.", + callback.__name__, + event_name, + cog_name, ) if hasattr(cog_to_remove, "cog_unload") and inspect.iscoroutinefunction( @@ -287,7 +297,7 @@ class CommandHandler: asyncio.create_task(cog_to_remove.cog_unload()) cog_to_remove._eject() - print(f"Cog '{cog_name}' removed.") + logger.info("Cog '%s' removed.", cog_name) return cog_to_remove async def get_prefix(self, message: "Message") -> Union[str, List[str], None]: @@ -493,11 +503,11 @@ class CommandHandler: ctx.kwargs = parsed_kwargs await command.invoke(ctx, *parsed_args, **parsed_kwargs) except CommandError as e: - print(f"Command error for '{command.name}': {e}") + logger.error("Command error for '%s': %s", command.name, e) if hasattr(self.client, "on_command_error"): await self.client.on_command_error(ctx, e) except Exception as e: - print(f"Unexpected error invoking command '{command.name}': {e}") + logger.error("Unexpected error invoking command '%s': %s", command.name, e) exc = CommandInvokeError(e) if hasattr(self.client, "on_command_error"): await self.client.on_command_error(ctx, exc) diff --git a/disagreement/gateway.py b/disagreement/gateway.py index 0bab06c..ddcea94 100644 --- a/disagreement/gateway.py +++ b/disagreement/gateway.py @@ -5,6 +5,7 @@ Manages the WebSocket connection to the Discord Gateway. """ import asyncio +import logging import traceback import aiohttp import json @@ -28,6 +29,9 @@ ZLIB_SUFFIX = b"\x00\x00\xff\xff" MAX_DECOMPRESSION_SIZE = 10 * 1024 * 1024 # 10 MiB, adjust as needed +logger = logging.getLogger(__name__) + + class GatewayClient: """ Handles the Discord Gateway WebSocket connection, heartbeating, and event dispatching. @@ -84,13 +88,17 @@ class GatewayClient: return except Exception as e: # noqa: BLE001 if attempt >= self._max_retries - 1: - print(f"Reconnect failed after {attempt + 1} attempts: {e}") + logger.error( + "Reconnect failed after %s attempts: %s", attempt + 1, e + ) raise jitter = random.uniform(0, delay) wait_time = min(delay + jitter, self._max_backoff) - print( - f"Reconnect attempt {attempt + 1} failed: {e}. " - f"Retrying in {wait_time:.2f} seconds..." + logger.warning( + "Reconnect attempt %s failed: %s. Retrying in %.2f seconds...", + attempt + 1, + e, + wait_time, ) await asyncio.sleep(wait_time) delay = min(delay * 2, self._max_backoff) @@ -112,21 +120,23 @@ class GatewayClient: self._buffer.clear() # Reset buffer after successful decompression return json.loads(decompressed.decode("utf-8")) except zlib.error as e: - print(f"Zlib decompression error: {e}") + logger.error("Zlib decompression error: %s", e) self._buffer.clear() # Clear buffer on error self._inflator = zlib.decompressobj() # Reset inflator return None except json.JSONDecodeError as e: - print(f"JSON decode error after decompression: {e}") + logger.error("JSON decode error after decompression: %s", e) return None async def _send_json(self, payload: Dict[str, Any]): if self._ws and not self._ws.closed: if self.verbose: - print(f"GATEWAY SEND: {payload}") + logger.debug("GATEWAY SEND: %s", payload) await self._ws.send_json(payload) else: - print("Gateway send attempted but WebSocket is closed or not available.") + logger.warning( + "Gateway send attempted but WebSocket is closed or not available." + ) # raise GatewayException("WebSocket is not connected.") async def _heartbeat(self): @@ -140,7 +150,7 @@ class GatewayClient: """Manages the heartbeating loop.""" if self._heartbeat_interval is None: # This should not happen if HELLO was processed correctly - print("Error: Heartbeat interval not set. Cannot start keep_alive.") + logger.error("Heartbeat interval not set. Cannot start keep_alive.") return try: @@ -150,9 +160,9 @@ class GatewayClient: self._heartbeat_interval / 1000 ) # Interval is in ms except asyncio.CancelledError: - print("Keep_alive task cancelled.") + logger.debug("Keep_alive task cancelled.") except Exception as e: - print(f"Error in keep_alive loop: {e}") + logger.error("Error in keep_alive loop: %s", e) # Potentially trigger a reconnect here or notify client await self._client_instance.close_gateway(code=1000) # Generic close @@ -174,12 +184,12 @@ class GatewayClient: if self._shard_id is not None and self._shard_count is not None: payload["d"]["shard"] = [self._shard_id, self._shard_count] await self._send_json(payload) - print("Sent IDENTIFY.") + logger.info("Sent IDENTIFY.") async def _resume(self): """Sends the RESUME payload to the Gateway.""" if not self._session_id or self._last_sequence is None: - print("Cannot RESUME: session_id or last_sequence is missing.") + logger.warning("Cannot RESUME: session_id or last_sequence is missing.") await self._identify() # Fallback to identify return @@ -192,8 +202,10 @@ class GatewayClient: }, } await self._send_json(payload) - print( - f"Sent RESUME for session {self._session_id} at sequence {self._last_sequence}." + logger.info( + "Sent RESUME for session %s at sequence %s.", + self._session_id, + self._last_sequence, ) async def update_presence( @@ -238,8 +250,9 @@ class GatewayClient: if event_name == "READY": # Special handling for READY if not isinstance(raw_event_d_payload, dict): - print( - f"Error: READY event 'd' payload is not a dict or is missing: {raw_event_d_payload}" + logger.error( + "READY event 'd' payload is not a dict or is missing: %s", + raw_event_d_payload, ) # Consider raising an error or attempting a reconnect return @@ -259,8 +272,8 @@ class GatewayClient: ) app_id_str = str(app_id_value) else: - print( - f"Warning: Could not find application ID in READY payload. App commands may not work." + logger.warning( + "Could not find application ID in READY payload. App commands may not work." ) # Parse and store the bot's own user object @@ -274,20 +287,29 @@ class GatewayClient: raw_event_d_payload["user"] ) self._client_instance.user = bot_user_obj - print( - f"Gateway READY. Bot User: {bot_user_obj.username}#{bot_user_obj.discriminator}. Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}" + logger.info( + "Gateway READY. Bot User: %s#%s. Session ID: %s. App ID: %s. Resume URL: %s", + bot_user_obj.username, + bot_user_obj.discriminator, + self._session_id, + app_id_str, + self._resume_gateway_url, ) except Exception as e: - print(f"Error parsing bot user from READY payload: {e}") - print( - f"Gateway READY (user parse failed). Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}" + logger.error("Error parsing bot user from READY payload: %s", e) + logger.info( + "Gateway READY (user parse failed). Session ID: %s. App ID: %s. Resume URL: %s", + self._session_id, + app_id_str, + self._resume_gateway_url, ) else: - print( - f"Warning: Bot user object not found or invalid in READY payload." - ) - print( - f"Gateway READY (no user). Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}" + logger.warning("Bot user object not found or invalid in READY payload.") + logger.info( + "Gateway READY (no user). Session ID: %s. App ID: %s. Resume URL: %s", + self._session_id, + app_id_str, + self._resume_gateway_url, ) await self._dispatcher.dispatch(event_name, raw_event_d_payload) @@ -306,15 +328,16 @@ class GatewayClient: self._client_instance.process_interaction(interaction) ) # type: ignore else: - print( - "Warning: Client instance does not have process_interaction method for INTERACTION_CREATE." + logger.warning( + "Client instance does not have process_interaction method for INTERACTION_CREATE." ) else: - print( - f"Error: INTERACTION_CREATE event 'd' payload is not a dict: {raw_event_d_payload}" + logger.error( + "INTERACTION_CREATE event 'd' payload is not a dict: %s", + raw_event_d_payload, ) elif event_name == "RESUMED": - print("Gateway RESUMED successfully.") + logger.info("Gateway RESUMED successfully.") # RESUMED 'd' payload is often an empty object or debug info. # Ensure it's a dict for the dispatcher. event_data_to_dispatch = ( @@ -330,7 +353,7 @@ class GatewayClient: # print(f"GATEWAY RECV EVENT: {event_name} | DATA: {event_data_to_dispatch}") await self._dispatcher.dispatch(event_name, event_data_to_dispatch) else: - print(f"Received dispatch with no event name: {data}") + logger.warning("Received dispatch with no event name: %s", data) async def _process_message(self, msg: aiohttp.WSMessage): """Processes a single message from the WebSocket.""" @@ -338,19 +361,20 @@ class GatewayClient: try: data = json.loads(msg.data) except json.JSONDecodeError: - print( - f"Failed to decode JSON from Gateway: {msg.data[:200]}" - ) # Log snippet + logger.error("Failed to decode JSON from Gateway: %s", msg.data[:200]) return elif msg.type == aiohttp.WSMsgType.BINARY: decompressed_data = await self._decompress_message(msg.data) if decompressed_data is None: - print("Failed to decompress or decode binary message from Gateway.") + logger.error( + "Failed to decompress or decode binary message from Gateway." + ) return data = decompressed_data elif msg.type == aiohttp.WSMsgType.ERROR: - print( - f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}" + logger.error( + "WebSocket error: %s", + self._ws.exception() if self._ws else "Unknown WSError", ) raise GatewayException( f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}" @@ -361,15 +385,17 @@ class GatewayClient: if self._ws and hasattr(self._ws, "close_code") else "N/A" ) - print(f"WebSocket connection closed by server. Code: {close_code}") + logger.warning( + "WebSocket connection closed by server. Code: %s", close_code + ) # Raise an exception to signal the closure to the client's main run loop raise GatewayException(f"WebSocket closed by server. Code: {close_code}") else: - print(f"Received unhandled WebSocket message type: {msg.type}") + logger.warning("Received unhandled WebSocket message type: %s", msg.type) return if self.verbose: - print(f"GATEWAY RECV: {data}") + logger.debug("GATEWAY RECV: %s", data) op = data.get("op") # 'd' payload (event_data) is handled specifically by each opcode handler below @@ -378,12 +404,16 @@ class GatewayClient: elif op == GatewayOpcode.HEARTBEAT: # Server requests a heartbeat await self._heartbeat() elif op == GatewayOpcode.RECONNECT: # Server requests a reconnect - print("Gateway requested RECONNECT. Closing and will attempt to reconnect.") + logger.info( + "Gateway requested RECONNECT. Closing and will attempt to reconnect." + ) await self.close(code=4000, reconnect=True) elif op == GatewayOpcode.INVALID_SESSION: # The 'd' payload for INVALID_SESSION is a boolean indicating resumability can_resume = data.get("d") is True - print(f"Gateway indicated INVALID_SESSION. Resumable: {can_resume}") + logger.warning( + "Gateway indicated INVALID_SESSION. Resumable: %s", can_resume + ) if not can_resume: self._session_id = None # Clear session_id to force re-identify self._last_sequence = None @@ -395,13 +425,16 @@ class GatewayClient: not isinstance(hello_d_payload, dict) or "heartbeat_interval" not in hello_d_payload ): - print( - f"Error: HELLO event 'd' payload is invalid or missing heartbeat_interval: {hello_d_payload}" + logger.error( + "HELLO event 'd' payload is invalid or missing heartbeat_interval: %s", + hello_d_payload, ) await self.close(code=1011) # Internal error, malformed HELLO return self._heartbeat_interval = hello_d_payload["heartbeat_interval"] - print(f"Gateway HELLO. Heartbeat interval: {self._heartbeat_interval}ms.") + logger.info( + "Gateway HELLO. Heartbeat interval: %sms.", self._heartbeat_interval + ) # Start heartbeating if self._keep_alive_task: self._keep_alive_task.cancel() @@ -409,45 +442,51 @@ class GatewayClient: # Identify or Resume if self._session_id and self._resume_gateway_url: # Check if we can resume - print("Attempting to RESUME session.") + logger.info("Attempting to RESUME session.") await self._resume() else: - print("Performing initial IDENTIFY.") + logger.info("Performing initial IDENTIFY.") await self._identify() elif op == GatewayOpcode.HEARTBEAT_ACK: self._last_heartbeat_ack = time.monotonic() # print("Received heartbeat ACK.") pass # Good, connection is alive else: - print(f"Received unhandled Gateway Opcode: {op} with data: {data}") + logger.warning( + "Received unhandled Gateway Opcode: %s with data: %s", op, data + ) async def _receive_loop(self): """Continuously receives and processes messages from the WebSocket.""" if not self._ws or self._ws.closed: - print("Receive loop cannot start: WebSocket is not connected or closed.") + logger.warning( + "Receive loop cannot start: WebSocket is not connected or closed." + ) return try: async for msg in self._ws: await self._process_message(msg) except asyncio.CancelledError: - print("Receive_loop task cancelled.") + logger.debug("Receive_loop task cancelled.") except aiohttp.ClientConnectionError as e: - print(f"ClientConnectionError in receive_loop: {e}. Attempting reconnect.") + logger.warning( + "ClientConnectionError in receive_loop: %s. Attempting reconnect.", e + ) await self.close(code=1006, reconnect=True) # Abnormal closure except Exception as e: - print(f"Unexpected error in receive_loop: {e}") + logger.error("Unexpected error in receive_loop: %s", e) traceback.print_exc() await self.close(code=1011, reconnect=True) finally: - print("Receive_loop ended.") + logger.info("Receive_loop ended.") # If the loop ends unexpectedly (not due to explicit close), # the main client might want to try reconnecting. async def connect(self): """Connects to the Discord Gateway.""" if self._ws and not self._ws.closed: - print("Gateway already connected or connecting.") + logger.warning("Gateway already connected or connecting.") return gateway_url = ( @@ -456,14 +495,14 @@ class GatewayClient: if not gateway_url.endswith("?v=10&encoding=json&compress=zlib-stream"): gateway_url += "?v=10&encoding=json&compress=zlib-stream" - print(f"Connecting to Gateway: {gateway_url}") + logger.info("Connecting to Gateway: %s", gateway_url) try: await self._http._ensure_session() # Ensure the HTTP client's session is active assert ( self._http._session is not None ), "HTTPClient session not initialized after ensure_session" self._ws = await self._http._session.ws_connect(gateway_url, max_msg_size=0) - print("Gateway WebSocket connection established.") + logger.info("Gateway WebSocket connection established.") if self._receive_task: self._receive_task.cancel() @@ -488,7 +527,7 @@ class GatewayClient: async def close(self, code: int = 1000, *, reconnect: bool = False): """Closes the Gateway connection.""" - print(f"Closing Gateway connection with code {code}...") + logger.info("Closing Gateway connection with code %s...", code) if self._keep_alive_task and not self._keep_alive_task.done(): self._keep_alive_task.cancel() try: @@ -507,7 +546,7 @@ class GatewayClient: if self._ws and not self._ws.closed: await self._ws.close(code=code) - print("Gateway WebSocket closed.") + logger.info("Gateway WebSocket closed.") self._ws = None # Do not reset session_id, last_sequence, or resume_gateway_url here @@ -515,7 +554,7 @@ class GatewayClient: # The connect logic will decide whether to resume or re-identify. # However, if it's a non-resumable close (e.g. Invalid Session non-resumable), clear them. if code == 4009: # Invalid session, not resumable - print("Clearing session state due to non-resumable invalid session.") + logger.info("Clearing session state due to non-resumable invalid session.") self._session_id = None self._last_sequence = None self._resume_gateway_url = None # This might be re-fetched anyway diff --git a/disagreement/http.py b/disagreement/http.py index b2855a4..83ea71d 100644 --- a/disagreement/http.py +++ b/disagreement/http.py @@ -5,6 +5,7 @@ HTTP client for interacting with the Discord REST API. """ import asyncio +import logging import aiohttp # pylint: disable=import-error import json from urllib.parse import quote @@ -28,6 +29,8 @@ if TYPE_CHECKING: # Discord API constants API_BASE_URL = "https://discord.com/api/v10" # Using API v10 +logger = logging.getLogger(__name__) + class HTTPClient: """Handles HTTP requests to the Discord API.""" @@ -86,7 +89,13 @@ class HTTPClient: final_headers.update(custom_headers) if self.verbose: - print(f"HTTP REQUEST: {method} {url} | payload={payload} params={params}") + logger.debug( + "HTTP REQUEST: %s %s | payload=%s params=%s", + method, + url, + payload, + params, + ) route = f"{method.upper()}:{endpoint}" @@ -119,7 +128,9 @@ class HTTPClient: ) # Fallback to text if JSON parsing fails if self.verbose: - print(f"HTTP RESPONSE: {response.status} {url} | {data}") + logger.debug( + "HTTP RESPONSE: %s %s | %s", response.status, url, data + ) self._rate_limiter.release(route, response.headers) @@ -150,8 +161,12 @@ class HTTPClient: ) if attempt < 4: # Don't log on the last attempt before raising - print( - f"{error_message} Retrying after {retry_after}s (Attempt {attempt + 1}/5). Global: {is_global}" + logger.warning( + "%s Retrying after %ss (Attempt %s/5). Global: %s", + error_message, + retry_after, + attempt + 1, + is_global, ) continue # Retry the request else: # Last attempt failed