disagreement 0.1.0rc2__py3-none-any.whl → 0.1.0rc3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
disagreement/__init__.py CHANGED
@@ -14,7 +14,7 @@ __title__ = "disagreement"
14
14
  __author__ = "Slipstream"
15
15
  __license__ = "BSD 3-Clause License"
16
16
  __copyright__ = "Copyright 2025 Slipstream"
17
- __version__ = "0.1.0rc2"
17
+ __version__ = "0.1.0rc3"
18
18
 
19
19
  from .client import Client, AutoShardedClient
20
20
  from .models import Message, User, Reaction
@@ -35,7 +35,11 @@ from .enums import GatewayIntent, GatewayOpcode # Export enums
35
35
  from .error_handler import setup_global_error_handler
36
36
  from .hybrid_context import HybridContext
37
37
  from .ext import tasks
38
+ from .logging_config import setup_logging
38
39
 
39
- # Set up logging if desired
40
- # import logging
41
- # logging.getLogger(__name__).addHandler(logging.NullHandler())
40
+ import logging
41
+
42
+
43
+ # Configure a default logger if none has been configured yet
44
+ if not logging.getLogger().hasHandlers():
45
+ setup_logging(logging.INFO)
disagreement/client.py CHANGED
@@ -123,14 +123,10 @@ class Client:
123
123
 
124
124
  self._closed: bool = False
125
125
  self._ready_event: asyncio.Event = asyncio.Event()
126
- self.application_id: Optional[Snowflake] = None # For Application Commands
127
126
  self.user: Optional["User"] = (
128
127
  None # The bot's own user object, populated on READY
129
128
  )
130
129
 
131
- # Initialize AppCommandHandler
132
- self.app_command_handler: AppCommandHandler = AppCommandHandler(client=self)
133
-
134
130
  # Internal Caches
135
131
  self._guilds: Dict[Snowflake, "Guild"] = {}
136
132
  self._channels: Dict[Snowflake, "Channel"] = (
@@ -1,6 +1,7 @@
1
1
  # disagreement/ext/app_commands/handler.py
2
2
 
3
3
  import inspect
4
+ import logging
4
5
  from typing import (
5
6
  TYPE_CHECKING,
6
7
  Dict,
@@ -64,6 +65,9 @@ if not TYPE_CHECKING:
64
65
  Message = Any
65
66
 
66
67
 
68
+ logger = logging.getLogger(__name__)
69
+
70
+
67
71
  class AppCommandHandler:
68
72
  """
69
73
  Manages application command registration, parsing, and dispatching.
@@ -544,7 +548,7 @@ class AppCommandHandler:
544
548
  await command.invoke(ctx, *parsed_args, **parsed_kwargs)
545
549
 
546
550
  except Exception as e:
547
- print(f"Error invoking app command '{command.name}': {e}")
551
+ logger.error("Error invoking app command '%s': %s", command.name, e)
548
552
  await self.dispatch_app_command_error(ctx, e)
549
553
  # else:
550
554
  # # Default error reply if no handler on client
@@ -594,34 +598,43 @@ class AppCommandHandler:
594
598
  payload = cmd_or_group.to_dict()
595
599
  commands_to_sync.append(payload)
596
600
  except AttributeError:
597
- print(
598
- f"Warning: Command or group '{cmd_or_group.name}' does not have a to_dict() method. Skipping."
601
+ logger.warning(
602
+ "Command or group '%s' does not have a to_dict() method. Skipping.",
603
+ cmd_or_group.name,
599
604
  )
600
605
  except Exception as e:
601
- print(
602
- f"Error converting command/group '{cmd_or_group.name}' to dict: {e}. Skipping."
606
+ logger.error(
607
+ "Error converting command/group '%s' to dict: %s. Skipping.",
608
+ cmd_or_group.name,
609
+ e,
603
610
  )
604
611
 
605
612
  if not commands_to_sync:
606
- print(
607
- f"No commands to sync for {'guild ' + str(guild_id) if guild_id else 'global'} scope."
613
+ logger.info(
614
+ "No commands to sync for %s scope.",
615
+ f"guild {guild_id}" if guild_id else "global",
608
616
  )
609
617
  return
610
618
 
611
619
  try:
612
620
  if guild_id:
613
- print(
614
- f"Syncing {len(commands_to_sync)} commands for guild {guild_id}..."
621
+ logger.info(
622
+ "Syncing %s commands for guild %s...",
623
+ len(commands_to_sync),
624
+ guild_id,
615
625
  )
616
626
  await self.client._http.bulk_overwrite_guild_application_commands(
617
627
  application_id, guild_id, commands_to_sync
618
628
  )
619
629
  else:
620
- print(f"Syncing {len(commands_to_sync)} global commands...")
630
+ logger.info(
631
+ "Syncing %s global commands...",
632
+ len(commands_to_sync),
633
+ )
621
634
  await self.client._http.bulk_overwrite_global_application_commands(
622
635
  application_id, commands_to_sync
623
636
  )
624
- print("Command sync successful.")
637
+ logger.info("Command sync successful.")
625
638
  except Exception as e:
626
- print(f"Error syncing application commands: {e}")
639
+ logger.error("Error syncing application commands: %s", e)
627
640
  # Consider re-raising or specific error handling
@@ -58,4 +58,4 @@ class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
58
58
  # The correct one will be called depending on how the command is dispatched.
59
59
  # The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
60
60
  # The prefix CommandHandler will use PrefixCommand.invoke.
61
- # This seems acceptable.
61
+ # This seems acceptable.
@@ -1,6 +1,7 @@
1
1
  # disagreement/ext/commands/cog.py
2
2
 
3
3
  import inspect
4
+ import logging
4
5
  from typing import TYPE_CHECKING, List, Tuple, Callable, Awaitable, Any, Dict, Union
5
6
 
6
7
  if TYPE_CHECKING:
@@ -16,6 +17,8 @@ else: # pragma: no cover - runtime imports for isinstance checks
16
17
  # EventDispatcher might be needed if cogs register listeners directly
17
18
  # from disagreement.event_dispatcher import EventDispatcher
18
19
 
20
+ logger = logging.getLogger(__name__)
21
+
19
22
 
20
23
  class Cog:
21
24
  """
@@ -59,8 +62,10 @@ class Cog:
59
62
  cmd.cog = self # Assign the cog instance to the command
60
63
  if cmd.name in self._commands:
61
64
  # This should ideally be caught earlier or handled by CommandHandler
62
- print(
63
- f"Warning: Duplicate command name '{cmd.name}' in cog '{self.cog_name}'. Overwriting."
65
+ logger.warning(
66
+ "Duplicate command name '%s' in cog '%s'. Overwriting.",
67
+ cmd.name,
68
+ self.cog_name,
64
69
  )
65
70
  self._commands[cmd.name.lower()] = cmd
66
71
  # Also register aliases
@@ -79,8 +84,10 @@ class Cog:
79
84
  # For AppCommandGroup, its commands will have cog set individually if they are AppCommands
80
85
  self._app_commands_and_groups.append(app_cmd_obj)
81
86
  else:
82
- print(
83
- f"Warning: Member '{member_name}' in cog '{self.cog_name}' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup."
87
+ logger.warning(
88
+ "Member '%s' in cog '%s' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup.",
89
+ member_name,
90
+ self.cog_name,
84
91
  )
85
92
 
86
93
  elif isinstance(member, (AppCommand, AppCommandGroup)):
@@ -92,8 +99,10 @@ class Cog:
92
99
  # This is a method decorated with @commands.Cog.listener or @commands.listener
93
100
  if not inspect.iscoroutinefunction(member):
94
101
  # Decorator should have caught this, but double check
95
- print(
96
- f"Warning: Listener '{member_name}' in cog '{self.cog_name}' is not a coroutine. Skipping."
102
+ logger.warning(
103
+ "Listener '%s' in cog '%s' is not a coroutine. Skipping.",
104
+ member_name,
105
+ self.cog_name,
97
106
  )
98
107
  continue
99
108
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import logging
6
7
  import inspect
7
8
  from typing import (
8
9
  TYPE_CHECKING,
@@ -31,6 +32,8 @@ from .errors import (
31
32
  from .converters import run_converters, DEFAULT_CONVERTERS, Converter
32
33
  from disagreement.typing import Typing
33
34
 
35
+ logger = logging.getLogger(__name__)
36
+
34
37
  if TYPE_CHECKING:
35
38
  from .cog import Cog
36
39
  from disagreement.client import Client
@@ -224,8 +227,10 @@ class CommandHandler:
224
227
  self.commands[command.name.lower()] = command
225
228
  for alias in command.aliases:
226
229
  if alias in self.commands:
227
- print(
228
- f"Warning: Alias '{alias}' for command '{command.name}' conflicts with an existing command or alias."
230
+ logger.warning(
231
+ "Alias '%s' for command '%s' conflicts with an existing command or alias.",
232
+ alias,
233
+ command.name,
229
234
  )
230
235
  self.commands[alias.lower()] = command
231
236
 
@@ -241,6 +246,7 @@ class CommandHandler:
241
246
 
242
247
  def add_cog(self, cog_to_add: "Cog") -> None:
243
248
  from .cog import Cog
249
+
244
250
  if not isinstance(cog_to_add, Cog):
245
251
  raise TypeError("Argument must be a subclass of Cog.")
246
252
 
@@ -258,8 +264,9 @@ class CommandHandler:
258
264
  for event_name, callback in cog_to_add.get_listeners():
259
265
  self.client._event_dispatcher.register(event_name.upper(), callback)
260
266
  else:
261
- print(
262
- f"Warning: Client does not have '_event_dispatcher'. Listeners for cog '{cog_to_add.cog_name}' not registered."
267
+ logger.warning(
268
+ "Client does not have '_event_dispatcher'. Listeners for cog '%s' not registered.",
269
+ cog_to_add.cog_name,
263
270
  )
264
271
 
265
272
  if hasattr(cog_to_add, "cog_load") and inspect.iscoroutinefunction(
@@ -267,7 +274,7 @@ class CommandHandler:
267
274
  ):
268
275
  asyncio.create_task(cog_to_add.cog_load())
269
276
 
270
- print(f"Cog '{cog_to_add.cog_name}' added.")
277
+ logger.info("Cog '%s' added.", cog_to_add.cog_name)
271
278
 
272
279
  def remove_cog(self, cog_name: str) -> Optional["Cog"]:
273
280
  cog_to_remove = self.cogs.pop(cog_name, None)
@@ -277,8 +284,11 @@ class CommandHandler:
277
284
 
278
285
  if hasattr(self.client, "_event_dispatcher"):
279
286
  for event_name, callback in cog_to_remove.get_listeners():
280
- print(
281
- f"Note: Listener '{callback.__name__}' for event '{event_name}' from cog '{cog_name}' needs manual unregistration logic in EventDispatcher."
287
+ logger.debug(
288
+ "Listener '%s' for event '%s' from cog '%s' needs manual unregistration logic in EventDispatcher.",
289
+ callback.__name__,
290
+ event_name,
291
+ cog_name,
282
292
  )
283
293
 
284
294
  if hasattr(cog_to_remove, "cog_unload") and inspect.iscoroutinefunction(
@@ -287,7 +297,7 @@ class CommandHandler:
287
297
  asyncio.create_task(cog_to_remove.cog_unload())
288
298
 
289
299
  cog_to_remove._eject()
290
- print(f"Cog '{cog_name}' removed.")
300
+ logger.info("Cog '%s' removed.", cog_name)
291
301
  return cog_to_remove
292
302
 
293
303
  async def get_prefix(self, message: "Message") -> Union[str, List[str], None]:
@@ -493,11 +503,11 @@ class CommandHandler:
493
503
  ctx.kwargs = parsed_kwargs
494
504
  await command.invoke(ctx, *parsed_args, **parsed_kwargs)
495
505
  except CommandError as e:
496
- print(f"Command error for '{command.name}': {e}")
506
+ logger.error("Command error for '%s': %s", command.name, e)
497
507
  if hasattr(self.client, "on_command_error"):
498
508
  await self.client.on_command_error(ctx, e)
499
509
  except Exception as e:
500
- print(f"Unexpected error invoking command '{command.name}': {e}")
510
+ logger.error("Unexpected error invoking command '%s': %s", command.name, e)
501
511
  exc = CommandInvokeError(e)
502
512
  if hasattr(self.client, "on_command_error"):
503
513
  await self.client.on_command_error(ctx, exc)
disagreement/gateway.py CHANGED
@@ -5,6 +5,7 @@ Manages the WebSocket connection to the Discord Gateway.
5
5
  """
6
6
 
7
7
  import asyncio
8
+ import logging
8
9
  import traceback
9
10
  import aiohttp
10
11
  import json
@@ -28,6 +29,9 @@ ZLIB_SUFFIX = b"\x00\x00\xff\xff"
28
29
  MAX_DECOMPRESSION_SIZE = 10 * 1024 * 1024 # 10 MiB, adjust as needed
29
30
 
30
31
 
32
+ logger = logging.getLogger(__name__)
33
+
34
+
31
35
  class GatewayClient:
32
36
  """
33
37
  Handles the Discord Gateway WebSocket connection, heartbeating, and event dispatching.
@@ -84,13 +88,17 @@ class GatewayClient:
84
88
  return
85
89
  except Exception as e: # noqa: BLE001
86
90
  if attempt >= self._max_retries - 1:
87
- print(f"Reconnect failed after {attempt + 1} attempts: {e}")
91
+ logger.error(
92
+ "Reconnect failed after %s attempts: %s", attempt + 1, e
93
+ )
88
94
  raise
89
95
  jitter = random.uniform(0, delay)
90
96
  wait_time = min(delay + jitter, self._max_backoff)
91
- print(
92
- f"Reconnect attempt {attempt + 1} failed: {e}. "
93
- f"Retrying in {wait_time:.2f} seconds..."
97
+ logger.warning(
98
+ "Reconnect attempt %s failed: %s. Retrying in %.2f seconds...",
99
+ attempt + 1,
100
+ e,
101
+ wait_time,
94
102
  )
95
103
  await asyncio.sleep(wait_time)
96
104
  delay = min(delay * 2, self._max_backoff)
@@ -112,21 +120,23 @@ class GatewayClient:
112
120
  self._buffer.clear() # Reset buffer after successful decompression
113
121
  return json.loads(decompressed.decode("utf-8"))
114
122
  except zlib.error as e:
115
- print(f"Zlib decompression error: {e}")
123
+ logger.error("Zlib decompression error: %s", e)
116
124
  self._buffer.clear() # Clear buffer on error
117
125
  self._inflator = zlib.decompressobj() # Reset inflator
118
126
  return None
119
127
  except json.JSONDecodeError as e:
120
- print(f"JSON decode error after decompression: {e}")
128
+ logger.error("JSON decode error after decompression: %s", e)
121
129
  return None
122
130
 
123
131
  async def _send_json(self, payload: Dict[str, Any]):
124
132
  if self._ws and not self._ws.closed:
125
133
  if self.verbose:
126
- print(f"GATEWAY SEND: {payload}")
134
+ logger.debug("GATEWAY SEND: %s", payload)
127
135
  await self._ws.send_json(payload)
128
136
  else:
129
- print("Gateway send attempted but WebSocket is closed or not available.")
137
+ logger.warning(
138
+ "Gateway send attempted but WebSocket is closed or not available."
139
+ )
130
140
  # raise GatewayException("WebSocket is not connected.")
131
141
 
132
142
  async def _heartbeat(self):
@@ -140,7 +150,7 @@ class GatewayClient:
140
150
  """Manages the heartbeating loop."""
141
151
  if self._heartbeat_interval is None:
142
152
  # This should not happen if HELLO was processed correctly
143
- print("Error: Heartbeat interval not set. Cannot start keep_alive.")
153
+ logger.error("Heartbeat interval not set. Cannot start keep_alive.")
144
154
  return
145
155
 
146
156
  try:
@@ -150,9 +160,9 @@ class GatewayClient:
150
160
  self._heartbeat_interval / 1000
151
161
  ) # Interval is in ms
152
162
  except asyncio.CancelledError:
153
- print("Keep_alive task cancelled.")
163
+ logger.debug("Keep_alive task cancelled.")
154
164
  except Exception as e:
155
- print(f"Error in keep_alive loop: {e}")
165
+ logger.error("Error in keep_alive loop: %s", e)
156
166
  # Potentially trigger a reconnect here or notify client
157
167
  await self._client_instance.close_gateway(code=1000) # Generic close
158
168
 
@@ -174,12 +184,12 @@ class GatewayClient:
174
184
  if self._shard_id is not None and self._shard_count is not None:
175
185
  payload["d"]["shard"] = [self._shard_id, self._shard_count]
176
186
  await self._send_json(payload)
177
- print("Sent IDENTIFY.")
187
+ logger.info("Sent IDENTIFY.")
178
188
 
179
189
  async def _resume(self):
180
190
  """Sends the RESUME payload to the Gateway."""
181
191
  if not self._session_id or self._last_sequence is None:
182
- print("Cannot RESUME: session_id or last_sequence is missing.")
192
+ logger.warning("Cannot RESUME: session_id or last_sequence is missing.")
183
193
  await self._identify() # Fallback to identify
184
194
  return
185
195
 
@@ -192,8 +202,10 @@ class GatewayClient:
192
202
  },
193
203
  }
194
204
  await self._send_json(payload)
195
- print(
196
- f"Sent RESUME for session {self._session_id} at sequence {self._last_sequence}."
205
+ logger.info(
206
+ "Sent RESUME for session %s at sequence %s.",
207
+ self._session_id,
208
+ self._last_sequence,
197
209
  )
198
210
 
199
211
  async def update_presence(
@@ -238,8 +250,9 @@ class GatewayClient:
238
250
 
239
251
  if event_name == "READY": # Special handling for READY
240
252
  if not isinstance(raw_event_d_payload, dict):
241
- print(
242
- f"Error: READY event 'd' payload is not a dict or is missing: {raw_event_d_payload}"
253
+ logger.error(
254
+ "READY event 'd' payload is not a dict or is missing: %s",
255
+ raw_event_d_payload,
243
256
  )
244
257
  # Consider raising an error or attempting a reconnect
245
258
  return
@@ -259,8 +272,8 @@ class GatewayClient:
259
272
  )
260
273
  app_id_str = str(app_id_value)
261
274
  else:
262
- print(
263
- f"Warning: Could not find application ID in READY payload. App commands may not work."
275
+ logger.warning(
276
+ "Could not find application ID in READY payload. App commands may not work."
264
277
  )
265
278
 
266
279
  # Parse and store the bot's own user object
@@ -274,20 +287,29 @@ class GatewayClient:
274
287
  raw_event_d_payload["user"]
275
288
  )
276
289
  self._client_instance.user = bot_user_obj
277
- print(
278
- 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}"
290
+ logger.info(
291
+ "Gateway READY. Bot User: %s#%s. Session ID: %s. App ID: %s. Resume URL: %s",
292
+ bot_user_obj.username,
293
+ bot_user_obj.discriminator,
294
+ self._session_id,
295
+ app_id_str,
296
+ self._resume_gateway_url,
279
297
  )
280
298
  except Exception as e:
281
- print(f"Error parsing bot user from READY payload: {e}")
282
- print(
283
- f"Gateway READY (user parse failed). Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}"
299
+ logger.error("Error parsing bot user from READY payload: %s", e)
300
+ logger.info(
301
+ "Gateway READY (user parse failed). Session ID: %s. App ID: %s. Resume URL: %s",
302
+ self._session_id,
303
+ app_id_str,
304
+ self._resume_gateway_url,
284
305
  )
285
306
  else:
286
- print(
287
- f"Warning: Bot user object not found or invalid in READY payload."
288
- )
289
- print(
290
- f"Gateway READY (no user). Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}"
307
+ logger.warning("Bot user object not found or invalid in READY payload.")
308
+ logger.info(
309
+ "Gateway READY (no user). Session ID: %s. App ID: %s. Resume URL: %s",
310
+ self._session_id,
311
+ app_id_str,
312
+ self._resume_gateway_url,
291
313
  )
292
314
 
293
315
  await self._dispatcher.dispatch(event_name, raw_event_d_payload)
@@ -306,15 +328,16 @@ class GatewayClient:
306
328
  self._client_instance.process_interaction(interaction)
307
329
  ) # type: ignore
308
330
  else:
309
- print(
310
- "Warning: Client instance does not have process_interaction method for INTERACTION_CREATE."
331
+ logger.warning(
332
+ "Client instance does not have process_interaction method for INTERACTION_CREATE."
311
333
  )
312
334
  else:
313
- print(
314
- f"Error: INTERACTION_CREATE event 'd' payload is not a dict: {raw_event_d_payload}"
335
+ logger.error(
336
+ "INTERACTION_CREATE event 'd' payload is not a dict: %s",
337
+ raw_event_d_payload,
315
338
  )
316
339
  elif event_name == "RESUMED":
317
- print("Gateway RESUMED successfully.")
340
+ logger.info("Gateway RESUMED successfully.")
318
341
  # RESUMED 'd' payload is often an empty object or debug info.
319
342
  # Ensure it's a dict for the dispatcher.
320
343
  event_data_to_dispatch = (
@@ -330,7 +353,7 @@ class GatewayClient:
330
353
  # print(f"GATEWAY RECV EVENT: {event_name} | DATA: {event_data_to_dispatch}")
331
354
  await self._dispatcher.dispatch(event_name, event_data_to_dispatch)
332
355
  else:
333
- print(f"Received dispatch with no event name: {data}")
356
+ logger.warning("Received dispatch with no event name: %s", data)
334
357
 
335
358
  async def _process_message(self, msg: aiohttp.WSMessage):
336
359
  """Processes a single message from the WebSocket."""
@@ -338,19 +361,20 @@ class GatewayClient:
338
361
  try:
339
362
  data = json.loads(msg.data)
340
363
  except json.JSONDecodeError:
341
- print(
342
- f"Failed to decode JSON from Gateway: {msg.data[:200]}"
343
- ) # Log snippet
364
+ logger.error("Failed to decode JSON from Gateway: %s", msg.data[:200])
344
365
  return
345
366
  elif msg.type == aiohttp.WSMsgType.BINARY:
346
367
  decompressed_data = await self._decompress_message(msg.data)
347
368
  if decompressed_data is None:
348
- print("Failed to decompress or decode binary message from Gateway.")
369
+ logger.error(
370
+ "Failed to decompress or decode binary message from Gateway."
371
+ )
349
372
  return
350
373
  data = decompressed_data
351
374
  elif msg.type == aiohttp.WSMsgType.ERROR:
352
- print(
353
- f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}"
375
+ logger.error(
376
+ "WebSocket error: %s",
377
+ self._ws.exception() if self._ws else "Unknown WSError",
354
378
  )
355
379
  raise GatewayException(
356
380
  f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}"
@@ -361,15 +385,17 @@ class GatewayClient:
361
385
  if self._ws and hasattr(self._ws, "close_code")
362
386
  else "N/A"
363
387
  )
364
- print(f"WebSocket connection closed by server. Code: {close_code}")
388
+ logger.warning(
389
+ "WebSocket connection closed by server. Code: %s", close_code
390
+ )
365
391
  # Raise an exception to signal the closure to the client's main run loop
366
392
  raise GatewayException(f"WebSocket closed by server. Code: {close_code}")
367
393
  else:
368
- print(f"Received unhandled WebSocket message type: {msg.type}")
394
+ logger.warning("Received unhandled WebSocket message type: %s", msg.type)
369
395
  return
370
396
 
371
397
  if self.verbose:
372
- print(f"GATEWAY RECV: {data}")
398
+ logger.debug("GATEWAY RECV: %s", data)
373
399
  op = data.get("op")
374
400
  # 'd' payload (event_data) is handled specifically by each opcode handler below
375
401
 
@@ -378,12 +404,16 @@ class GatewayClient:
378
404
  elif op == GatewayOpcode.HEARTBEAT: # Server requests a heartbeat
379
405
  await self._heartbeat()
380
406
  elif op == GatewayOpcode.RECONNECT: # Server requests a reconnect
381
- print("Gateway requested RECONNECT. Closing and will attempt to reconnect.")
407
+ logger.info(
408
+ "Gateway requested RECONNECT. Closing and will attempt to reconnect."
409
+ )
382
410
  await self.close(code=4000, reconnect=True)
383
411
  elif op == GatewayOpcode.INVALID_SESSION:
384
412
  # The 'd' payload for INVALID_SESSION is a boolean indicating resumability
385
413
  can_resume = data.get("d") is True
386
- print(f"Gateway indicated INVALID_SESSION. Resumable: {can_resume}")
414
+ logger.warning(
415
+ "Gateway indicated INVALID_SESSION. Resumable: %s", can_resume
416
+ )
387
417
  if not can_resume:
388
418
  self._session_id = None # Clear session_id to force re-identify
389
419
  self._last_sequence = None
@@ -395,13 +425,16 @@ class GatewayClient:
395
425
  not isinstance(hello_d_payload, dict)
396
426
  or "heartbeat_interval" not in hello_d_payload
397
427
  ):
398
- print(
399
- f"Error: HELLO event 'd' payload is invalid or missing heartbeat_interval: {hello_d_payload}"
428
+ logger.error(
429
+ "HELLO event 'd' payload is invalid or missing heartbeat_interval: %s",
430
+ hello_d_payload,
400
431
  )
401
432
  await self.close(code=1011) # Internal error, malformed HELLO
402
433
  return
403
434
  self._heartbeat_interval = hello_d_payload["heartbeat_interval"]
404
- print(f"Gateway HELLO. Heartbeat interval: {self._heartbeat_interval}ms.")
435
+ logger.info(
436
+ "Gateway HELLO. Heartbeat interval: %sms.", self._heartbeat_interval
437
+ )
405
438
  # Start heartbeating
406
439
  if self._keep_alive_task:
407
440
  self._keep_alive_task.cancel()
@@ -409,45 +442,51 @@ class GatewayClient:
409
442
 
410
443
  # Identify or Resume
411
444
  if self._session_id and self._resume_gateway_url: # Check if we can resume
412
- print("Attempting to RESUME session.")
445
+ logger.info("Attempting to RESUME session.")
413
446
  await self._resume()
414
447
  else:
415
- print("Performing initial IDENTIFY.")
448
+ logger.info("Performing initial IDENTIFY.")
416
449
  await self._identify()
417
450
  elif op == GatewayOpcode.HEARTBEAT_ACK:
418
451
  self._last_heartbeat_ack = time.monotonic()
419
452
  # print("Received heartbeat ACK.")
420
453
  pass # Good, connection is alive
421
454
  else:
422
- print(f"Received unhandled Gateway Opcode: {op} with data: {data}")
455
+ logger.warning(
456
+ "Received unhandled Gateway Opcode: %s with data: %s", op, data
457
+ )
423
458
 
424
459
  async def _receive_loop(self):
425
460
  """Continuously receives and processes messages from the WebSocket."""
426
461
  if not self._ws or self._ws.closed:
427
- print("Receive loop cannot start: WebSocket is not connected or closed.")
462
+ logger.warning(
463
+ "Receive loop cannot start: WebSocket is not connected or closed."
464
+ )
428
465
  return
429
466
 
430
467
  try:
431
468
  async for msg in self._ws:
432
469
  await self._process_message(msg)
433
470
  except asyncio.CancelledError:
434
- print("Receive_loop task cancelled.")
471
+ logger.debug("Receive_loop task cancelled.")
435
472
  except aiohttp.ClientConnectionError as e:
436
- print(f"ClientConnectionError in receive_loop: {e}. Attempting reconnect.")
473
+ logger.warning(
474
+ "ClientConnectionError in receive_loop: %s. Attempting reconnect.", e
475
+ )
437
476
  await self.close(code=1006, reconnect=True) # Abnormal closure
438
477
  except Exception as e:
439
- print(f"Unexpected error in receive_loop: {e}")
478
+ logger.error("Unexpected error in receive_loop: %s", e)
440
479
  traceback.print_exc()
441
480
  await self.close(code=1011, reconnect=True)
442
481
  finally:
443
- print("Receive_loop ended.")
482
+ logger.info("Receive_loop ended.")
444
483
  # If the loop ends unexpectedly (not due to explicit close),
445
484
  # the main client might want to try reconnecting.
446
485
 
447
486
  async def connect(self):
448
487
  """Connects to the Discord Gateway."""
449
488
  if self._ws and not self._ws.closed:
450
- print("Gateway already connected or connecting.")
489
+ logger.warning("Gateway already connected or connecting.")
451
490
  return
452
491
 
453
492
  gateway_url = (
@@ -456,14 +495,14 @@ class GatewayClient:
456
495
  if not gateway_url.endswith("?v=10&encoding=json&compress=zlib-stream"):
457
496
  gateway_url += "?v=10&encoding=json&compress=zlib-stream"
458
497
 
459
- print(f"Connecting to Gateway: {gateway_url}")
498
+ logger.info("Connecting to Gateway: %s", gateway_url)
460
499
  try:
461
500
  await self._http._ensure_session() # Ensure the HTTP client's session is active
462
501
  assert (
463
502
  self._http._session is not None
464
503
  ), "HTTPClient session not initialized after ensure_session"
465
504
  self._ws = await self._http._session.ws_connect(gateway_url, max_msg_size=0)
466
- print("Gateway WebSocket connection established.")
505
+ logger.info("Gateway WebSocket connection established.")
467
506
 
468
507
  if self._receive_task:
469
508
  self._receive_task.cancel()
@@ -488,7 +527,7 @@ class GatewayClient:
488
527
 
489
528
  async def close(self, code: int = 1000, *, reconnect: bool = False):
490
529
  """Closes the Gateway connection."""
491
- print(f"Closing Gateway connection with code {code}...")
530
+ logger.info("Closing Gateway connection with code %s...", code)
492
531
  if self._keep_alive_task and not self._keep_alive_task.done():
493
532
  self._keep_alive_task.cancel()
494
533
  try:
@@ -507,7 +546,7 @@ class GatewayClient:
507
546
 
508
547
  if self._ws and not self._ws.closed:
509
548
  await self._ws.close(code=code)
510
- print("Gateway WebSocket closed.")
549
+ logger.info("Gateway WebSocket closed.")
511
550
 
512
551
  self._ws = None
513
552
  # Do not reset session_id, last_sequence, or resume_gateway_url here
@@ -515,7 +554,7 @@ class GatewayClient:
515
554
  # The connect logic will decide whether to resume or re-identify.
516
555
  # However, if it's a non-resumable close (e.g. Invalid Session non-resumable), clear them.
517
556
  if code == 4009: # Invalid session, not resumable
518
- print("Clearing session state due to non-resumable invalid session.")
557
+ logger.info("Clearing session state due to non-resumable invalid session.")
519
558
  self._session_id = None
520
559
  self._last_sequence = None
521
560
  self._resume_gateway_url = None # This might be re-fetched anyway
disagreement/http.py CHANGED
@@ -5,6 +5,7 @@ HTTP client for interacting with the Discord REST API.
5
5
  """
6
6
 
7
7
  import asyncio
8
+ import logging
8
9
  import aiohttp # pylint: disable=import-error
9
10
  import json
10
11
  from urllib.parse import quote
@@ -28,6 +29,8 @@ if TYPE_CHECKING:
28
29
  # Discord API constants
29
30
  API_BASE_URL = "https://discord.com/api/v10" # Using API v10
30
31
 
32
+ logger = logging.getLogger(__name__)
33
+
31
34
 
32
35
  class HTTPClient:
33
36
  """Handles HTTP requests to the Discord API."""
@@ -86,7 +89,13 @@ class HTTPClient:
86
89
  final_headers.update(custom_headers)
87
90
 
88
91
  if self.verbose:
89
- print(f"HTTP REQUEST: {method} {url} | payload={payload} params={params}")
92
+ logger.debug(
93
+ "HTTP REQUEST: %s %s | payload=%s params=%s",
94
+ method,
95
+ url,
96
+ payload,
97
+ params,
98
+ )
90
99
 
91
100
  route = f"{method.upper()}:{endpoint}"
92
101
 
@@ -119,7 +128,9 @@ class HTTPClient:
119
128
  ) # Fallback to text if JSON parsing fails
120
129
 
121
130
  if self.verbose:
122
- print(f"HTTP RESPONSE: {response.status} {url} | {data}")
131
+ logger.debug(
132
+ "HTTP RESPONSE: %s %s | %s", response.status, url, data
133
+ )
123
134
 
124
135
  self._rate_limiter.release(route, response.headers)
125
136
 
@@ -150,8 +161,12 @@ class HTTPClient:
150
161
  )
151
162
 
152
163
  if attempt < 4: # Don't log on the last attempt before raising
153
- print(
154
- f"{error_message} Retrying after {retry_after}s (Attempt {attempt + 1}/5). Global: {is_global}"
164
+ logger.warning(
165
+ "%s Retrying after %ss (Attempt %s/5). Global: %s",
166
+ error_message,
167
+ retry_after,
168
+ attempt + 1,
169
+ is_global,
155
170
  )
156
171
  continue # Retry the request
157
172
  else: # Last attempt failed
disagreement/models.py CHANGED
@@ -4,12 +4,12 @@
4
4
  Data models for Discord objects.
5
5
  """
6
6
 
7
- import json
8
- import asyncio
9
- import aiohttp # pylint: disable=import-error
10
7
  import asyncio
8
+ import json
11
9
  from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union
12
10
 
11
+ import aiohttp # pylint: disable=import-error
12
+ from .color import Color
13
13
  from .errors import DisagreementException, HTTPException
14
14
  from .enums import ( # These enums will need to be defined in disagreement/enums.py
15
15
  VerificationLevel,
@@ -25,7 +25,6 @@ from .enums import ( # These enums will need to be defined in disagreement/enum
25
25
  # SelectMenuType will be part of ComponentType or a new enum if needed
26
26
  )
27
27
  from .permissions import Permissions
28
- from .color import Color
29
28
 
30
29
 
31
30
  if TYPE_CHECKING:
@@ -1087,7 +1086,6 @@ class TextChannel(Channel):
1087
1086
  )
1088
1087
  self.last_pin_timestamp: Optional[str] = data.get("last_pin_timestamp")
1089
1088
 
1090
-
1091
1089
  def history(
1092
1090
  self,
1093
1091
  *,
@@ -1143,7 +1141,6 @@ class TextChannel(Channel):
1143
1141
  self._client._messages.pop(mid, None)
1144
1142
  return ids
1145
1143
 
1146
-
1147
1144
  def __repr__(self) -> str:
1148
1145
  return f"<TextChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1149
1146
 
disagreement/py.typed ADDED
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.1.0rc2
3
+ Version: 0.1.0rc3
4
4
  Summary: A Python library for the Discord API.
5
5
  Author-email: Slipstream <me@slipstreamm.dev>
6
6
  License: BSD 3-Clause
@@ -1,22 +1,23 @@
1
- disagreement/__init__.py,sha256=4T6_19N6SjwgFXFONo6GjB3VbgODPZUc5soxJoVS_zY,1084
1
+ disagreement/__init__.py,sha256=FAY6OvLuMyjkrBWEzl3tFemLgyWR-AUL-my3oeN-HB0,1169
2
2
  disagreement/audio.py,sha256=P6inobI8CNhNVkaRKU58RMYtLq1RrSREioF0Mui5VlA,3351
3
3
  disagreement/cache.py,sha256=juabGFl4naQih5OUIVN2aN-vAfw2ZC2cI38s4nGEn8U,1525
4
- disagreement/client.py,sha256=7or564FS7fXALDyRvNrt-H32mtV1VLysuIs3CtQ9L3s,53182
4
+ disagreement/client.py,sha256=ig5ZBCIPnAYxShhQzW9qPSRkauGnNo16DO4RzPCHLo8,52973
5
5
  disagreement/color.py,sha256=g-1ynMGCUbY0f6jJXzMLS1aJFoZg91bdMetFkZgaCC0,2387
6
6
  disagreement/components.py,sha256=tEYJ2RHVpIFtZuPPxZ0v8ssUw_x7ybhYBzHNsRiXXvU,5250
7
7
  disagreement/enums.py,sha256=CP03oF28maaPUVckOfE_tnVIm2ZOc5WL8mWlocmjeOQ,9785
8
8
  disagreement/error_handler.py,sha256=c2lb6aTMnhTtITQuR6axZUtEaasYKUgmdSxAHEkeq50,1028
9
9
  disagreement/errors.py,sha256=XiYVPy8uFUGVi_EIf81yK7QbC7KyN4JHplSJSWw2RRk,3185
10
10
  disagreement/event_dispatcher.py,sha256=mp4LVhIj0SW1P2NruqbYpZoYH33X5rXvkAl3-RK40kE,11460
11
- disagreement/gateway.py,sha256=mhFtBm_YPtbleQJklv3ph3DXE4LZxC1BhtNkd7Y-akQ,23113
12
- disagreement/http.py,sha256=4FnVMIRjrNSpQ9IUEeWQGP6jYAhv3YTTANqFDNv0SdY,31046
11
+ disagreement/gateway.py,sha256=EnRQKdYzeQ08h_Rx2J5vg1p5b47fWetkwIWrhXyUOXE,24132
12
+ disagreement/http.py,sha256=6T1z3O6D0IL3qrUhtY-bwsnMR16a_zCMVMUU98QwKsc,31395
13
13
  disagreement/hybrid_context.py,sha256=VYCmcreTZdPBU9v-Cy48W38vgWO2t8nM2ulC6_z4HjU,1095
14
14
  disagreement/i18n.py,sha256=1L4rcFuKP0XjHk9dVwbNh4BkLk2ZlxxZ_-tecGWa9S0,718
15
15
  disagreement/interactions.py,sha256=aUZwwEOLsEds15i6G-rxmSSDCDmaxz_cfoTYS4tv6Ao,21735
16
16
  disagreement/logging_config.py,sha256=4q6baQPE6X_0lfaBTFMU1uqc03x5SbJqo2hsApdDFac,686
17
- disagreement/models.py,sha256=JeHKJWrc7mN70oFNgd7Iic9_SgHfDlgz7aTRgMA-5PA,75070
17
+ disagreement/models.py,sha256=IkPBT5Ki0IpDr2_NCIhiYSSL4aI28MakJ_PMZ81JcAA,75053
18
18
  disagreement/oauth.py,sha256=TfDdCwg1J7osM9wDi61dtNBA5BrQk5DeQrrHsYycH34,2810
19
19
  disagreement/permissions.py,sha256=7g5cIlg-evHXOL0-pmtT5EwqcB-stXot1HZSLz724sE,3008
20
+ disagreement/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
21
  disagreement/rate_limiter.py,sha256=ubwR_UTPs2MotipBdtqpgwQKx0IHt2I3cdfFcXTFv7g,2521
21
22
  disagreement/shard_manager.py,sha256=e9F8tx_4IEOlTX3-S3t51lfJToc6Ue3RVBzoNAiVKxs,2161
22
23
  disagreement/typing.py,sha256=_1oFWfZ4HyH5Q3bnF7CO24s79z-3_B5Qb69kWiwLhhU,1242
@@ -30,12 +31,12 @@ disagreement/ext/app_commands/commands.py,sha256=0O5fJQg2esTQzx2FyEpM2ZrrLckNmv8
30
31
  disagreement/ext/app_commands/context.py,sha256=Xcm4Ka5K5uTQGviixF5LeCDdOdF9YQS5F7lZi2m--8s,20831
31
32
  disagreement/ext/app_commands/converters.py,sha256=J1VEmo-7H9K7kGPJodu5FX4RmFFI1BuzhlQAEs2MsD4,21036
32
33
  disagreement/ext/app_commands/decorators.py,sha256=dKiD4ZEsafRoPvfgn9zuQ9vvXXo2qYTMquHvyUM1604,23251
33
- disagreement/ext/app_commands/handler.py,sha256=XO9yLgcV7aIxzhTMgFcQ1Tbr4GRZRfDBzkIAkiu6mw8,26045
34
- disagreement/ext/app_commands/hybrid.py,sha256=yRDnlnOqgo79X669WhBHQ6LJCCuFDXKUbmlC_u3aXR0,3329
34
+ disagreement/ext/app_commands/handler.py,sha256=36E58u8S7x-Ciop6kT6g7DdsPS2Wwo7rcvoBd7jmhWQ,26331
35
+ disagreement/ext/app_commands/hybrid.py,sha256=u3kHNbVfY3-liymgzEIkFO5YV3WM_DqwytzdN9EXWMY,3330
35
36
  disagreement/ext/commands/__init__.py,sha256=HxZWVfc4qvP_bCRbKTVZoMqXFq19Gj4mQvRumvQiApQ,1130
36
- disagreement/ext/commands/cog.py,sha256=U57yMrUpqj3_-W1-koyfGgH43MZG_JzJOl46kTur7iA,6636
37
+ disagreement/ext/commands/cog.py,sha256=-F2ZOXXC07r96xlt9NomRgqlIqlcxzBiha2Zhg1DVp4,6845
37
38
  disagreement/ext/commands/converters.py,sha256=mh8xJr1FIiah6bdYy0KsdccfYcPii2Yc_IdhzCTw5uE,5864
38
- disagreement/ext/commands/core.py,sha256=vwsj3GR9wrEy0y-1HJv16Hg9-9xnm61tvT9b14QlYEI,19296
39
+ disagreement/ext/commands/core.py,sha256=xHeqenDqh2xiH7n9UyZl12sI1c0erorLPwi3uusmm1E,19525
39
40
  disagreement/ext/commands/decorators.py,sha256=Ox_D9KCFtMa-RiljFjOcsPb3stmDStRKeLw1DVeOdAw,6608
40
41
  disagreement/ext/commands/errors.py,sha256=cG5sPA-osUq2gts5scrl5yT-BHEYVHLTb4TULjAmbaY,2065
41
42
  disagreement/ext/commands/help.py,sha256=yw0ydupOsPwmnhsIIoxa93xjj9MAcBcGfD8BXa7V8G8,1456
@@ -46,8 +47,8 @@ disagreement/ui/item.py,sha256=bm-EmQEZpe8Kt8JrRw-o0uQdccgjErORcFsBqaXOcV8,1112
46
47
  disagreement/ui/modal.py,sha256=w0ZEVslXzx2-RWUP4jdVB54zDuT81jpueVWZ70byFnI,4137
47
48
  disagreement/ui/select.py,sha256=XjWRlWkA09QZaDDLn-wDDOWIuj0Mb4VCWJEOAaExZXw,3018
48
49
  disagreement/ui/view.py,sha256=QhWoYt39QKXwl1X6Mkm5gNNEqd8bt7O505lSpiG0L04,5567
49
- disagreement-0.1.0rc2.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
50
- disagreement-0.1.0rc2.dist-info/METADATA,sha256=qMql6NO40Fgv8w-yvoFgBq7V8glJbRJvN3-pyeDnQOY,4889
51
- disagreement-0.1.0rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
- disagreement-0.1.0rc2.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
53
- disagreement-0.1.0rc2.dist-info/RECORD,,
50
+ disagreement-0.1.0rc3.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
51
+ disagreement-0.1.0rc3.dist-info/METADATA,sha256=4pe1v3qHLPGJD35qHIHxdI81xIiM0MdVKjS6HGsTYYs,4889
52
+ disagreement-0.1.0rc3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ disagreement-0.1.0rc3.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
54
+ disagreement-0.1.0rc3.dist-info/RECORD,,