Replace debug prints with logging (#38)

This commit is contained in:
Slipstream 2025-06-10 18:24:00 -06:00 committed by GitHub
parent 90ee3fcf7f
commit fd31a3162b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 188 additions and 98 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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