disagreement 0.1.0rc1__py3-none-any.whl → 0.1.0rc2__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.0rc1"
17
+ __version__ = "0.1.0rc2"
18
18
 
19
19
  from .client import Client, AutoShardedClient
20
20
  from .models import Message, User, Reaction
@@ -30,7 +30,7 @@ from .errors import (
30
30
  NotFound,
31
31
  )
32
32
  from .color import Color
33
- from .utils import utcnow
33
+ from .utils import utcnow, message_pager
34
34
  from .enums import GatewayIntent, GatewayOpcode # Export enums
35
35
  from .error_handler import setup_global_error_handler
36
36
  from .hybrid_context import HybridContext
disagreement/client.py CHANGED
@@ -16,6 +16,7 @@ from typing import (
16
16
  List,
17
17
  Dict,
18
18
  )
19
+ from types import ModuleType
19
20
 
20
21
  from .http import HTTPClient
21
22
  from .gateway import GatewayClient
@@ -28,6 +29,7 @@ from .ext.commands.core import CommandHandler
28
29
  from .ext.commands.cog import Cog
29
30
  from .ext.app_commands.handler import AppCommandHandler
30
31
  from .ext.app_commands.context import AppCommandContext
32
+ from .ext import loader as ext_loader
31
33
  from .interactions import Interaction, Snowflake
32
34
  from .error_handler import setup_global_error_handler
33
35
  from .voice_client import VoiceClient
@@ -638,6 +640,23 @@ class Client:
638
640
  # import traceback
639
641
  # traceback.print_exception(type(error.original), error.original, error.original.__traceback__)
640
642
 
643
+ # --- Extension Management Methods ---
644
+
645
+ def load_extension(self, name: str) -> ModuleType:
646
+ """Load an extension by name using :mod:`disagreement.ext.loader`."""
647
+
648
+ return ext_loader.load_extension(name)
649
+
650
+ def unload_extension(self, name: str) -> None:
651
+ """Unload a previously loaded extension."""
652
+
653
+ ext_loader.unload_extension(name)
654
+
655
+ def reload_extension(self, name: str) -> ModuleType:
656
+ """Reload an extension by name."""
657
+
658
+ return ext_loader.reload_extension(name)
659
+
641
660
  # --- Model Parsing and Fetching ---
642
661
 
643
662
  def parse_user(self, data: Dict[str, Any]) -> "User":
@@ -920,12 +939,12 @@ class Client:
920
939
  await view._start(self)
921
940
  components_payload = view.to_components_payload()
922
941
  elif components:
923
- from .models import ActionRow as ActionRowModel
942
+ from .models import Component as ComponentModel
924
943
 
925
944
  components_payload = [
926
945
  comp.to_dict()
927
946
  for comp in components
928
- if isinstance(comp, ActionRowModel)
947
+ if isinstance(comp, ComponentModel)
929
948
  ]
930
949
 
931
950
  message_data = await self._http.send_message(
@@ -1016,6 +1035,26 @@ class Client:
1016
1035
  self._voice_clients[guild_id] = voice
1017
1036
  return voice
1018
1037
 
1038
+ async def add_reaction(self, channel_id: str, message_id: str, emoji: str) -> None:
1039
+ """|coro| Add a reaction to a message."""
1040
+
1041
+ await self.create_reaction(channel_id, message_id, emoji)
1042
+
1043
+ async def remove_reaction(
1044
+ self, channel_id: str, message_id: str, emoji: str
1045
+ ) -> None:
1046
+ """|coro| Remove the bot's reaction from a message."""
1047
+
1048
+ await self.delete_reaction(channel_id, message_id, emoji)
1049
+
1050
+ async def clear_reactions(self, channel_id: str, message_id: str) -> None:
1051
+ """|coro| Remove all reactions from a message."""
1052
+
1053
+ if self._closed:
1054
+ raise DisagreementException("Client is closed.")
1055
+
1056
+ await self._http.clear_reactions(channel_id, message_id)
1057
+
1019
1058
  async def create_reaction(
1020
1059
  self, channel_id: str, message_id: str, emoji: str
1021
1060
  ) -> None:
disagreement/color.py CHANGED
@@ -48,3 +48,31 @@ class Color:
48
48
 
49
49
  def to_rgb(self) -> tuple[int, int, int]:
50
50
  return ((self.value >> 16) & 0xFF, (self.value >> 8) & 0xFF, self.value & 0xFF)
51
+
52
+ @classmethod
53
+ def parse(cls, value: "Color | int | str | tuple[int, int, int] | None") -> "Color | None":
54
+ """Convert ``value`` to a :class:`Color` instance.
55
+
56
+ Parameters
57
+ ----------
58
+ value:
59
+ The value to convert. May be ``None``, an existing ``Color``, an
60
+ integer in the ``0xRRGGBB`` format, or a hex string like ``"#RRGGBB"``.
61
+
62
+ Returns
63
+ -------
64
+ Optional[Color]
65
+ A ``Color`` object if ``value`` is not ``None``.
66
+ """
67
+
68
+ if value is None:
69
+ return None
70
+ if isinstance(value, cls):
71
+ return value
72
+ if isinstance(value, int):
73
+ return cls(value)
74
+ if isinstance(value, str):
75
+ return cls.from_hex(value)
76
+ if isinstance(value, tuple) and len(value) == 3:
77
+ return cls.from_rgb(*value)
78
+ raise TypeError("Color value must be Color, int, str, tuple, or None")
disagreement/enums.py CHANGED
@@ -49,6 +49,11 @@ class GatewayIntent(IntEnum):
49
49
  AUTO_MODERATION_CONFIGURATION = 1 << 20
50
50
  AUTO_MODERATION_EXECUTION = 1 << 21
51
51
 
52
+ @classmethod
53
+ def none(cls) -> int:
54
+ """Return a bitmask representing no intents."""
55
+ return 0
56
+
52
57
  @classmethod
53
58
  def default(cls) -> int:
54
59
  """Returns default intents (excluding privileged ones like members, presences, message content)."""
@@ -44,3 +44,5 @@ __all__ = [
44
44
  "OptionMetadata",
45
45
  "AppCommandContext", # To be defined
46
46
  ]
47
+
48
+ from .hybrid import *
@@ -1,58 +1,25 @@
1
1
  # disagreement/ext/app_commands/commands.py
2
2
 
3
3
  import inspect
4
- from typing import Callable, Optional, List, Dict, Any, Union, TYPE_CHECKING
4
+ from typing import Any, Callable, Dict, List, Optional, Union, TYPE_CHECKING
5
5
 
6
+ from disagreement.enums import (
7
+ ApplicationCommandType,
8
+ ApplicationCommandOptionType,
9
+ IntegrationType,
10
+ InteractionContextType,
11
+ )
12
+ from disagreement.interactions import ApplicationCommandOption, Snowflake
6
13
 
7
14
  if TYPE_CHECKING:
8
- from disagreement.ext.commands.core import (
9
- Command as PrefixCommand,
10
- ) # Alias to avoid name clash
11
- from disagreement.interactions import ApplicationCommandOption, Snowflake
12
- from disagreement.enums import (
13
- ApplicationCommandType,
14
- IntegrationType,
15
- InteractionContextType,
16
- ApplicationCommandOptionType, # Added
17
- )
18
- from disagreement.ext.commands.cog import Cog # Corrected import path
19
-
20
- # Placeholder for Cog if not using the existing one or if it needs adaptation
15
+ from disagreement.ext.commands.core import Command as PrefixCommand
16
+ from disagreement.ext.commands.cog import Cog
17
+
21
18
  if not TYPE_CHECKING:
22
- # This dynamic Cog = Any might not be ideal if Cog is used in runtime type checks.
23
- # However, for type hinting purposes when TYPE_CHECKING is false, it avoids import.
24
- # If Cog is needed at runtime by this module (it is, for AppCommand.cog type hint),
25
- # it should be imported directly.
26
- # For now, the TYPE_CHECKING block handles the proper import for static analysis.
27
- # Let's ensure Cog is available at runtime if AppCommand.cog is accessed.
28
- # A simple way is to import it outside TYPE_CHECKING too, if it doesn't cause circularity.
29
- # Given its usage, a forward reference string 'Cog' might be better in AppCommand.cog type hint.
30
- # Let's try importing it directly for runtime, assuming no circularity with this specific module.
31
- try:
32
- from disagreement.ext.commands.cog import Cog
33
- except ImportError:
34
- Cog = Any # Fallback if direct import fails (e.g. during partial builds/tests)
35
- # Import PrefixCommand at runtime for HybridCommand
36
19
  try:
37
20
  from disagreement.ext.commands.core import Command as PrefixCommand
38
- except Exception: # pragma: no cover - safeguard against unusual import issues
39
- PrefixCommand = Any # type: ignore
40
- # Import enums used at runtime
41
- try:
42
- from disagreement.enums import (
43
- ApplicationCommandType,
44
- IntegrationType,
45
- InteractionContextType,
46
- ApplicationCommandOptionType,
47
- )
48
- from disagreement.interactions import ApplicationCommandOption, Snowflake
49
- except Exception: # pragma: no cover
50
- ApplicationCommandType = ApplicationCommandOptionType = IntegrationType = (
51
- InteractionContextType
52
- ) = Any # type: ignore
53
- ApplicationCommandOption = Snowflake = Any # type: ignore
54
- else: # When TYPE_CHECKING is true, Cog and PrefixCommand are already imported above.
55
- pass
21
+ except ImportError:
22
+ PrefixCommand = Any
56
23
 
57
24
 
58
25
  class AppCommand:
@@ -235,59 +202,6 @@ class MessageCommand(AppCommand):
235
202
  super().__init__(callback, type=ApplicationCommandType.MESSAGE, **kwargs)
236
203
 
237
204
 
238
- class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
239
- """
240
- Represents a command that can be invoked as both a slash command
241
- and a traditional prefix-based command.
242
- """
243
-
244
- def __init__(self, callback: Callable[..., Any], **kwargs: Any):
245
- # Initialize SlashCommand part (which calls AppCommand.__init__)
246
- # We need to ensure 'type' is correctly passed for AppCommand
247
- # kwargs for SlashCommand: name, description, guild_ids, default_member_permissions, nsfw, parent, cog, etc.
248
- # kwargs for PrefixCommand: name, aliases, brief, description, cog
249
-
250
- # Pop prefix-specific args before passing to SlashCommand constructor
251
- prefix_aliases = kwargs.pop("aliases", [])
252
- prefix_brief = kwargs.pop("brief", None)
253
- # Description is used by both, AppCommand's constructor will handle it.
254
- # Name is used by both. Cog is used by both.
255
-
256
- # Call SlashCommand's __init__
257
- # This will set up name, description, callback, type=CHAT_INPUT, options, etc.
258
- super().__init__(callback, **kwargs) # This is SlashCommand.__init__
259
-
260
- # Now, explicitly initialize the PrefixCommand parts that SlashCommand didn't cover
261
- # or that need specific values for the prefix version.
262
- # PrefixCommand.__init__(self, callback, name=self.name, aliases=prefix_aliases, brief=prefix_brief, description=self.description, cog=self.cog)
263
- # However, PrefixCommand.__init__ also sets self.params, which AppCommand already did.
264
- # We need to be careful not to re-initialize things unnecessarily or incorrectly.
265
- # Let's manually set the distinct attributes for the PrefixCommand aspect.
266
-
267
- # Attributes from PrefixCommand:
268
- # self.callback is already set by AppCommand
269
- # self.name is already set by AppCommand
270
- self.aliases: List[str] = (
271
- prefix_aliases # This was specific to HybridCommand before, now aligns with PrefixCommand
272
- )
273
- self.brief: Optional[str] = prefix_brief
274
- # self.description is already set by AppCommand (SlashCommand ensures it exists)
275
- # self.cog is already set by AppCommand
276
- # self.params is already set by AppCommand
277
-
278
- # Ensure the MRO is handled correctly. Python's MRO (C3 linearization)
279
- # should call SlashCommand's __init__ then AppCommand's __init__.
280
- # PrefixCommand.__init__ won't be called automatically unless we explicitly call it.
281
- # By setting attributes directly, we avoid potential issues with multiple __init__ calls
282
- # if their logic overlaps too much (e.g., both trying to set self.params).
283
-
284
- # We might need to override invoke if the context or argument passing differs significantly
285
- # between app command invocation and prefix command invocation.
286
- # For now, SlashCommand.invoke and PrefixCommand.invoke are separate.
287
- # The correct one will be called depending on how the command is dispatched.
288
- # The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
289
- # The prefix CommandHandler will use PrefixCommand.invoke.
290
- # This seems acceptable.
291
205
 
292
206
 
293
207
  class AppCommandGroup:
@@ -26,8 +26,8 @@ from .commands import (
26
26
  MessageCommand,
27
27
  AppCommand,
28
28
  AppCommandGroup,
29
- HybridCommand,
30
29
  )
30
+ from .hybrid import HybridCommand
31
31
  from disagreement.interactions import (
32
32
  ApplicationCommandOption,
33
33
  ApplicationCommandOptionChoice,
@@ -0,0 +1,61 @@
1
+ # disagreement/ext/app_commands/hybrid.py
2
+
3
+ from typing import Any, Callable, List, Optional
4
+
5
+ from .commands import SlashCommand
6
+ from disagreement.ext.commands.core import PrefixCommand
7
+
8
+
9
+ class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
10
+ """
11
+ Represents a command that can be invoked as both a slash command
12
+ and a traditional prefix-based command.
13
+ """
14
+
15
+ def __init__(self, callback: Callable[..., Any], **kwargs: Any):
16
+ # Initialize SlashCommand part (which calls AppCommand.__init__)
17
+ # We need to ensure 'type' is correctly passed for AppCommand
18
+ # kwargs for SlashCommand: name, description, guild_ids, default_member_permissions, nsfw, parent, cog, etc.
19
+ # kwargs for PrefixCommand: name, aliases, brief, description, cog
20
+
21
+ # Pop prefix-specific args before passing to SlashCommand constructor
22
+ prefix_aliases = kwargs.pop("aliases", [])
23
+ prefix_brief = kwargs.pop("brief", None)
24
+ # Description is used by both, AppCommand's constructor will handle it.
25
+ # Name is used by both. Cog is used by both.
26
+
27
+ # Call SlashCommand's __init__
28
+ # This will set up name, description, callback, type=CHAT_INPUT, options, etc.
29
+ super().__init__(callback, **kwargs) # This is SlashCommand.__init__
30
+
31
+ # Now, explicitly initialize the PrefixCommand parts that SlashCommand didn't cover
32
+ # or that need specific values for the prefix version.
33
+ # PrefixCommand.__init__(self, callback, name=self.name, aliases=prefix_aliases, brief=prefix_brief, description=self.description, cog=self.cog)
34
+ # However, PrefixCommand.__init__ also sets self.params, which AppCommand already did.
35
+ # We need to be careful not to re-initialize things unnecessarily or incorrectly.
36
+ # Let's manually set the distinct attributes for the PrefixCommand aspect.
37
+
38
+ # Attributes from PrefixCommand:
39
+ # self.callback is already set by AppCommand
40
+ # self.name is already set by AppCommand
41
+ self.aliases: List[str] = (
42
+ prefix_aliases # This was specific to HybridCommand before, now aligns with PrefixCommand
43
+ )
44
+ self.brief: Optional[str] = prefix_brief
45
+ # self.description is already set by AppCommand (SlashCommand ensures it exists)
46
+ # self.cog is already set by AppCommand
47
+ # self.params is already set by AppCommand
48
+
49
+ # Ensure the MRO is handled correctly. Python's MRO (C3 linearization)
50
+ # should call SlashCommand's __init__ then AppCommand's __init__.
51
+ # PrefixCommand.__init__ won't be called automatically unless we explicitly call it.
52
+ # By setting attributes directly, we avoid potential issues with multiple __init__ calls
53
+ # if their logic overlaps too much (e.g., both trying to set self.params).
54
+
55
+ # We might need to override invoke if the context or argument passing differs significantly
56
+ # between app command invocation and prefix command invocation.
57
+ # For now, SlashCommand.invoke and PrefixCommand.invoke are separate.
58
+ # The correct one will be called depending on how the command is dispatched.
59
+ # The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
60
+ # The prefix CommandHandler will use PrefixCommand.invoke.
61
+ # This seems acceptable.
@@ -1,5 +1,7 @@
1
1
  # disagreement/ext/commands/core.py
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import asyncio
4
6
  import inspect
5
7
  from typing import (
@@ -27,10 +29,10 @@ from .errors import (
27
29
  CommandInvokeError,
28
30
  )
29
31
  from .converters import run_converters, DEFAULT_CONVERTERS, Converter
30
- from .cog import Cog
31
32
  from disagreement.typing import Typing
32
33
 
33
34
  if TYPE_CHECKING:
35
+ from .cog import Cog
34
36
  from disagreement.client import Client
35
37
  from disagreement.models import Message, User
36
38
 
@@ -86,6 +88,9 @@ class Command:
86
88
  await self.callback(ctx, *args, **kwargs)
87
89
 
88
90
 
91
+ PrefixCommand = Command # Alias for clarity in hybrid commands
92
+
93
+
89
94
  class CommandContext:
90
95
  """
91
96
  Represents the context in which a command is being invoked.
@@ -123,7 +128,7 @@ class CommandContext:
123
128
 
124
129
  async def reply(
125
130
  self,
126
- content: str,
131
+ content: Optional[str] = None,
127
132
  *,
128
133
  mention_author: Optional[bool] = None,
129
134
  **kwargs: Any,
@@ -235,6 +240,7 @@ class CommandHandler:
235
240
  return self.commands.get(name.lower())
236
241
 
237
242
  def add_cog(self, cog_to_add: "Cog") -> None:
243
+ from .cog import Cog
238
244
  if not isinstance(cog_to_add, Cog):
239
245
  raise TypeError("Argument must be a subclass of Cog.")
240
246
 
disagreement/ext/tasks.py CHANGED
@@ -18,6 +18,8 @@ class Task:
18
18
  delta: Optional[datetime.timedelta] = None,
19
19
  time_of_day: Optional[datetime.time] = None,
20
20
  on_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
21
+ before_loop: Optional[Callable[[], Awaitable[None] | None]] = None,
22
+ after_loop: Optional[Callable[[], Awaitable[None] | None]] = None,
21
23
  ) -> None:
22
24
  self._coro = coro
23
25
  self._task: Optional[asyncio.Task[None]] = None
@@ -36,6 +38,8 @@ class Task:
36
38
  self._seconds = float(interval_seconds)
37
39
  self._time_of_day = time_of_day
38
40
  self._on_error = on_error
41
+ self._before_loop = before_loop
42
+ self._after_loop = after_loop
39
43
 
40
44
  def _seconds_until_time(self) -> float:
41
45
  assert self._time_of_day is not None
@@ -47,6 +51,9 @@ class Task:
47
51
 
48
52
  async def _run(self, *args: Any, **kwargs: Any) -> None:
49
53
  try:
54
+ if self._before_loop is not None:
55
+ await _maybe_call_no_args(self._before_loop)
56
+
50
57
  first = True
51
58
  while True:
52
59
  if self._time_of_day is not None:
@@ -65,6 +72,9 @@ class Task:
65
72
  first = False
66
73
  except asyncio.CancelledError:
67
74
  pass
75
+ finally:
76
+ if self._after_loop is not None:
77
+ await _maybe_call_no_args(self._after_loop)
68
78
 
69
79
  def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
70
80
  if self._task is None or self._task.done():
@@ -89,6 +99,12 @@ async def _maybe_call(
89
99
  await result
90
100
 
91
101
 
102
+ async def _maybe_call_no_args(func: Callable[[], Awaitable[None] | None]) -> None:
103
+ result = func()
104
+ if asyncio.iscoroutine(result):
105
+ await result
106
+
107
+
92
108
  class _Loop:
93
109
  def __init__(
94
110
  self,
@@ -110,6 +126,8 @@ class _Loop:
110
126
  self.on_error = on_error
111
127
  self._task: Optional[Task] = None
112
128
  self._owner: Any = None
129
+ self._before_loop: Optional[Callable[..., Awaitable[Any]]] = None
130
+ self._after_loop: Optional[Callable[..., Awaitable[Any]]] = None
113
131
 
114
132
  def __get__(self, obj: Any, objtype: Any) -> "_BoundLoop":
115
133
  return _BoundLoop(self, obj)
@@ -119,7 +137,33 @@ class _Loop:
119
137
  return self.func(*args, **kwargs)
120
138
  return self.func(self._owner, *args, **kwargs)
121
139
 
140
+ def before_loop(
141
+ self, func: Callable[..., Awaitable[Any]]
142
+ ) -> Callable[..., Awaitable[Any]]:
143
+ self._before_loop = func
144
+ return func
145
+
146
+ def after_loop(
147
+ self, func: Callable[..., Awaitable[Any]]
148
+ ) -> Callable[..., Awaitable[Any]]:
149
+ self._after_loop = func
150
+ return func
151
+
122
152
  def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
153
+ def call_before() -> Awaitable[None] | None:
154
+ if self._before_loop is None:
155
+ return None
156
+ if self._owner is not None:
157
+ return self._before_loop(self._owner)
158
+ return self._before_loop()
159
+
160
+ def call_after() -> Awaitable[None] | None:
161
+ if self._after_loop is None:
162
+ return None
163
+ if self._owner is not None:
164
+ return self._after_loop(self._owner)
165
+ return self._after_loop()
166
+
123
167
  self._task = Task(
124
168
  self._coro,
125
169
  seconds=self.seconds,
@@ -128,6 +172,8 @@ class _Loop:
128
172
  delta=self.delta,
129
173
  time_of_day=self.time_of_day,
130
174
  on_error=self.on_error,
175
+ before_loop=call_before,
176
+ after_loop=call_after,
131
177
  )
132
178
  return self._task.start(*args, **kwargs)
133
179
 
disagreement/http.py CHANGED
@@ -17,11 +17,13 @@ from .errors import (
17
17
  DisagreementException,
18
18
  )
19
19
  from . import __version__ # For User-Agent
20
+ from .rate_limiter import RateLimiter
21
+ from .interactions import InteractionResponsePayload
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  from .client import Client
23
25
  from .models import Message, Webhook, File
24
- from .interactions import ApplicationCommand, InteractionResponsePayload, Snowflake
26
+ from .interactions import ApplicationCommand, Snowflake
25
27
 
26
28
  # Discord API constants
27
29
  API_BASE_URL = "https://discord.com/api/v10" # Using API v10
@@ -44,8 +46,7 @@ class HTTPClient:
44
46
 
45
47
  self.verbose = verbose
46
48
 
47
- self._global_rate_limit_lock = asyncio.Event()
48
- self._global_rate_limit_lock.set() # Initially unlocked
49
+ self._rate_limiter = RateLimiter()
49
50
 
50
51
  async def _ensure_session(self):
51
52
  if self._session is None or self._session.closed:
@@ -87,10 +88,10 @@ class HTTPClient:
87
88
  if self.verbose:
88
89
  print(f"HTTP REQUEST: {method} {url} | payload={payload} params={params}")
89
90
 
90
- # Global rate limit handling
91
- await self._global_rate_limit_lock.wait()
91
+ route = f"{method.upper()}:{endpoint}"
92
92
 
93
93
  for attempt in range(5): # Max 5 retries for rate limits
94
+ await self._rate_limiter.acquire(route)
94
95
  assert self._session is not None, "ClientSession not initialized"
95
96
  async with self._session.request(
96
97
  method,
@@ -120,6 +121,8 @@ class HTTPClient:
120
121
  if self.verbose:
121
122
  print(f"HTTP RESPONSE: {response.status} {url} | {data}")
122
123
 
124
+ self._rate_limiter.release(route, response.headers)
125
+
123
126
  if 200 <= response.status < 300:
124
127
  if response.status == 204:
125
128
  return None
@@ -142,12 +145,9 @@ class HTTPClient:
142
145
  if data and isinstance(data, dict) and "message" in data:
143
146
  error_message += f" Discord says: {data['message']}"
144
147
 
145
- if is_global:
146
- self._global_rate_limit_lock.clear()
147
- await asyncio.sleep(retry_after)
148
- self._global_rate_limit_lock.set()
149
- else:
150
- await asyncio.sleep(retry_after)
148
+ await self._rate_limiter.handle_rate_limit(
149
+ route, retry_after, is_global
150
+ )
151
151
 
152
152
  if attempt < 4: # Don't log on the last attempt before raising
153
153
  print(
@@ -348,6 +348,16 @@ class HTTPClient:
348
348
  f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}",
349
349
  )
350
350
 
351
+ async def clear_reactions(
352
+ self, channel_id: "Snowflake", message_id: "Snowflake"
353
+ ) -> None:
354
+ """Removes all reactions from a message."""
355
+
356
+ await self.request(
357
+ "DELETE",
358
+ f"/channels/{channel_id}/messages/{message_id}/reactions",
359
+ )
360
+
351
361
  async def bulk_delete_messages(
352
362
  self, channel_id: "Snowflake", messages: List["Snowflake"]
353
363
  ) -> List["Snowflake"]:
@@ -411,6 +421,92 @@ class HTTPClient:
411
421
 
412
422
  await self.request("DELETE", f"/webhooks/{webhook_id}")
413
423
 
424
+ async def execute_webhook(
425
+ self,
426
+ webhook_id: "Snowflake",
427
+ token: str,
428
+ *,
429
+ content: Optional[str] = None,
430
+ tts: bool = False,
431
+ embeds: Optional[List[Dict[str, Any]]] = None,
432
+ components: Optional[List[Dict[str, Any]]] = None,
433
+ allowed_mentions: Optional[dict] = None,
434
+ attachments: Optional[List[Any]] = None,
435
+ files: Optional[List[Any]] = None,
436
+ flags: Optional[int] = None,
437
+ username: Optional[str] = None,
438
+ avatar_url: Optional[str] = None,
439
+ ) -> Dict[str, Any]:
440
+ """Executes a webhook and returns the created message."""
441
+
442
+ payload: Dict[str, Any] = {}
443
+ if content is not None:
444
+ payload["content"] = content
445
+ if tts:
446
+ payload["tts"] = True
447
+ if embeds:
448
+ payload["embeds"] = embeds
449
+ if components:
450
+ payload["components"] = components
451
+ if allowed_mentions:
452
+ payload["allowed_mentions"] = allowed_mentions
453
+ if username:
454
+ payload["username"] = username
455
+ if avatar_url:
456
+ payload["avatar_url"] = avatar_url
457
+
458
+ all_files: List["File"] = []
459
+ if attachments is not None:
460
+ payload["attachments"] = []
461
+ for a in attachments:
462
+ if hasattr(a, "data") and hasattr(a, "filename"):
463
+ idx = len(all_files)
464
+ all_files.append(a)
465
+ payload["attachments"].append({"id": idx, "filename": a.filename})
466
+ else:
467
+ payload["attachments"].append(
468
+ a.to_dict() if hasattr(a, "to_dict") else a
469
+ )
470
+ if files is not None:
471
+ for f in files:
472
+ if hasattr(f, "data") and hasattr(f, "filename"):
473
+ idx = len(all_files)
474
+ all_files.append(f)
475
+ if "attachments" not in payload:
476
+ payload["attachments"] = []
477
+ payload["attachments"].append({"id": idx, "filename": f.filename})
478
+ else:
479
+ raise TypeError("files must be File objects")
480
+ if flags:
481
+ payload["flags"] = flags
482
+
483
+ if all_files:
484
+ form = aiohttp.FormData()
485
+ form.add_field(
486
+ "payload_json", json.dumps(payload), content_type="application/json"
487
+ )
488
+ for idx, f in enumerate(all_files):
489
+ form.add_field(
490
+ f"files[{idx}]",
491
+ f.data,
492
+ filename=f.filename,
493
+ content_type="application/octet-stream",
494
+ )
495
+ return await self.request(
496
+ "POST",
497
+ f"/webhooks/{webhook_id}/{token}",
498
+ payload=form,
499
+ is_json=False,
500
+ use_auth_header=False,
501
+ )
502
+
503
+ return await self.request(
504
+ "POST",
505
+ f"/webhooks/{webhook_id}/{token}",
506
+ payload=payload,
507
+ use_auth_header=False,
508
+ )
509
+
414
510
  async def get_user(self, user_id: "Snowflake") -> Dict[str, Any]:
415
511
  """Fetches a user object for a given user ID."""
416
512
  return await self.request("GET", f"/users/{user_id}")
@@ -657,7 +753,7 @@ class HTTPClient:
657
753
  self,
658
754
  interaction_id: "Snowflake",
659
755
  interaction_token: str,
660
- payload: "InteractionResponsePayload",
756
+ payload: Union["InteractionResponsePayload", Dict[str, Any]],
661
757
  *,
662
758
  ephemeral: bool = False,
663
759
  ) -> None:
@@ -670,10 +766,16 @@ class HTTPClient:
670
766
  """
671
767
  # Interaction responses do not use the bot token in the Authorization header.
672
768
  # They are authenticated by the interaction_token in the URL.
769
+ payload_data: Dict[str, Any]
770
+ if isinstance(payload, InteractionResponsePayload):
771
+ payload_data = payload.to_dict()
772
+ else:
773
+ payload_data = payload
774
+
673
775
  await self.request(
674
776
  "POST",
675
777
  f"/interactions/{interaction_id}/{interaction_token}/callback",
676
- payload=payload.to_dict(),
778
+ payload=payload_data,
677
779
  use_auth_header=False,
678
780
  )
679
781
 
@@ -395,17 +395,14 @@ class Interaction:
395
395
 
396
396
  async def respond_modal(self, modal: "Modal") -> None:
397
397
  """|coro| Send a modal in response to this interaction."""
398
-
399
- from typing import Any, cast
400
-
401
- payload = {
402
- "type": InteractionCallbackType.MODAL.value,
403
- "data": modal.to_dict(),
404
- }
398
+ payload = InteractionResponsePayload(
399
+ type=InteractionCallbackType.MODAL,
400
+ data=modal.to_dict(),
401
+ )
405
402
  await self._client._http.create_interaction_response(
406
403
  interaction_id=self.id,
407
404
  interaction_token=self.token,
408
- payload=cast(Any, payload),
405
+ payload=payload,
409
406
  )
410
407
 
411
408
  async def edit(
@@ -489,7 +486,7 @@ class InteractionResponse:
489
486
  """Sends a modal response."""
490
487
  payload = InteractionResponsePayload(
491
488
  type=InteractionCallbackType.MODAL,
492
- data=InteractionCallbackData(modal.to_dict()),
489
+ data=modal.to_dict(),
493
490
  )
494
491
  await self._interaction._client._http.create_interaction_response(
495
492
  self._interaction.id,
@@ -510,7 +507,7 @@ class InteractionCallbackData:
510
507
  )
511
508
  self.allowed_mentions: Optional[AllowedMentions] = (
512
509
  AllowedMentions(data["allowed_mentions"])
513
- if data.get("allowed_mentions")
510
+ if "allowed_mentions" in data
514
511
  else None
515
512
  )
516
513
  self.flags: Optional[int] = data.get("flags") # MessageFlags enum could be used
@@ -557,16 +554,22 @@ class InteractionResponsePayload:
557
554
  def __init__(
558
555
  self,
559
556
  type: InteractionCallbackType,
560
- data: Optional[InteractionCallbackData] = None,
557
+ data: Optional[Union[InteractionCallbackData, Dict[str, Any]]] = None,
561
558
  ):
562
- self.type: InteractionCallbackType = type
563
- self.data: Optional[InteractionCallbackData] = data
559
+ self.type = type
560
+ self.data = data
564
561
 
565
562
  def to_dict(self) -> Dict[str, Any]:
566
563
  payload: Dict[str, Any] = {"type": self.type.value}
567
564
  if self.data:
568
- payload["data"] = self.data.to_dict()
565
+ if isinstance(self.data, dict):
566
+ payload["data"] = self.data
567
+ else:
568
+ payload["data"] = self.data.to_dict()
569
569
  return payload
570
570
 
571
571
  def __repr__(self) -> str:
572
572
  return f"<InteractionResponsePayload type={self.type!r}>"
573
+
574
+ def __getitem__(self, key: str) -> Any:
575
+ return self.to_dict()[key]
disagreement/models.py CHANGED
@@ -8,7 +8,7 @@ import json
8
8
  import asyncio
9
9
  import aiohttp # pylint: disable=import-error
10
10
  import asyncio
11
- from typing import Optional, TYPE_CHECKING, List, Dict, Any, Union
11
+ from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union
12
12
 
13
13
  from .errors import DisagreementException, HTTPException
14
14
  from .enums import ( # These enums will need to be defined in disagreement/enums.py
@@ -25,6 +25,7 @@ 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
28
29
 
29
30
 
30
31
  if TYPE_CHECKING:
@@ -201,6 +202,21 @@ class Message:
201
202
  view=view,
202
203
  )
203
204
 
205
+ async def add_reaction(self, emoji: str) -> None:
206
+ """|coro| Add a reaction to this message."""
207
+
208
+ await self._client.add_reaction(self.channel_id, self.id, emoji)
209
+
210
+ async def remove_reaction(self, emoji: str) -> None:
211
+ """|coro| Remove the bot's reaction from this message."""
212
+
213
+ await self._client.remove_reaction(self.channel_id, self.id, emoji)
214
+
215
+ async def clear_reactions(self) -> None:
216
+ """|coro| Remove all reactions from this message."""
217
+
218
+ await self._client.clear_reactions(self.channel_id, self.id)
219
+
204
220
  async def delete(self, delay: Optional[float] = None) -> None:
205
221
  """|coro|
206
222
 
@@ -312,7 +328,7 @@ class Embed:
312
328
  self.description: Optional[str] = data.get("description")
313
329
  self.url: Optional[str] = data.get("url")
314
330
  self.timestamp: Optional[str] = data.get("timestamp") # ISO8601 timestamp
315
- self.color: Optional[int] = data.get("color")
331
+ self.color = Color.parse(data.get("color"))
316
332
 
317
333
  self.footer: Optional[EmbedFooter] = (
318
334
  EmbedFooter(data["footer"]) if data.get("footer") else None
@@ -342,7 +358,7 @@ class Embed:
342
358
  if self.timestamp:
343
359
  payload["timestamp"] = self.timestamp
344
360
  if self.color is not None:
345
- payload["color"] = self.color
361
+ payload["color"] = self.color.value
346
362
  if self.footer:
347
363
  payload["footer"] = self.footer.to_dict()
348
364
  if self.image:
@@ -528,6 +544,12 @@ class Member(User): # Member inherits from User
528
544
  def __repr__(self) -> str:
529
545
  return f"<Member id='{self.id}' username='{self.username}' nick='{self.nick}'>"
530
546
 
547
+ @property
548
+ def display_name(self) -> str:
549
+ """Return the nickname if set, otherwise the username."""
550
+
551
+ return self.nick or self.username
552
+
531
553
  async def kick(self, *, reason: Optional[str] = None) -> None:
532
554
  if not self.guild_id or not self._client:
533
555
  raise DisagreementException("Member.kick requires guild_id and client")
@@ -1065,6 +1087,20 @@ class TextChannel(Channel):
1065
1087
  )
1066
1088
  self.last_pin_timestamp: Optional[str] = data.get("last_pin_timestamp")
1067
1089
 
1090
+
1091
+ def history(
1092
+ self,
1093
+ *,
1094
+ limit: Optional[int] = None,
1095
+ before: Optional[str] = None,
1096
+ after: Optional[str] = None,
1097
+ ) -> AsyncIterator["Message"]:
1098
+ """Return an async iterator over this channel's messages."""
1099
+
1100
+ from .utils import message_pager
1101
+
1102
+ return message_pager(self, limit=limit, before=before, after=after)
1103
+
1068
1104
  async def send(
1069
1105
  self,
1070
1106
  content: Optional[str] = None,
@@ -1107,6 +1143,7 @@ class TextChannel(Channel):
1107
1143
  self._client._messages.pop(mid, None)
1108
1144
  return ids
1109
1145
 
1146
+
1110
1147
  def __repr__(self) -> str:
1111
1148
  return f"<TextChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1112
1149
 
@@ -1224,6 +1261,36 @@ class DMChannel(Channel):
1224
1261
  components=components,
1225
1262
  )
1226
1263
 
1264
+ async def history(
1265
+ self,
1266
+ *,
1267
+ limit: Optional[int] = 100,
1268
+ before: "Snowflake | None" = None,
1269
+ ):
1270
+ """An async iterator over messages in this DM."""
1271
+
1272
+ params: Dict[str, Union[int, str]] = {}
1273
+ if before is not None:
1274
+ params["before"] = before
1275
+
1276
+ fetched = 0
1277
+ while True:
1278
+ to_fetch = 100 if limit is None else min(100, limit - fetched)
1279
+ if to_fetch <= 0:
1280
+ break
1281
+ params["limit"] = to_fetch
1282
+ messages = await self._client._http.request(
1283
+ "GET", f"/channels/{self.id}/messages", params=params.copy()
1284
+ )
1285
+ if not messages:
1286
+ break
1287
+ params["before"] = messages[-1]["id"]
1288
+ for msg in messages:
1289
+ yield Message(msg, self._client)
1290
+ fetched += 1
1291
+ if limit is not None and fetched >= limit:
1292
+ return
1293
+
1227
1294
  def __repr__(self) -> str:
1228
1295
  recipient_repr = self.recipient.username if self.recipient else "Unknown"
1229
1296
  return f"<DMChannel id='{self.id}' recipient='{recipient_repr}'>"
@@ -1317,6 +1384,57 @@ class Webhook:
1317
1384
 
1318
1385
  return cls({"id": webhook_id, "token": token, "url": url})
1319
1386
 
1387
+ async def send(
1388
+ self,
1389
+ content: Optional[str] = None,
1390
+ *,
1391
+ username: Optional[str] = None,
1392
+ avatar_url: Optional[str] = None,
1393
+ tts: bool = False,
1394
+ embed: Optional["Embed"] = None,
1395
+ embeds: Optional[List["Embed"]] = None,
1396
+ components: Optional[List["ActionRow"]] = None,
1397
+ allowed_mentions: Optional[Dict[str, Any]] = None,
1398
+ attachments: Optional[List[Any]] = None,
1399
+ files: Optional[List[Any]] = None,
1400
+ flags: Optional[int] = None,
1401
+ ) -> "Message":
1402
+ """Send a message using this webhook."""
1403
+
1404
+ if not self._client:
1405
+ raise DisagreementException("Webhook is not bound to a Client")
1406
+ assert self.token is not None, "Webhook token missing"
1407
+
1408
+ if embed and embeds:
1409
+ raise ValueError("Cannot provide both embed and embeds.")
1410
+
1411
+ final_embeds_payload: Optional[List[Dict[str, Any]]] = None
1412
+ if embed:
1413
+ final_embeds_payload = [embed.to_dict()]
1414
+ elif embeds:
1415
+ final_embeds_payload = [e.to_dict() for e in embeds]
1416
+
1417
+ components_payload: Optional[List[Dict[str, Any]]] = None
1418
+ if components:
1419
+ components_payload = [c.to_dict() for c in components]
1420
+
1421
+ message_data = await self._client._http.execute_webhook(
1422
+ self.id,
1423
+ self.token,
1424
+ content=content,
1425
+ tts=tts,
1426
+ embeds=final_embeds_payload,
1427
+ components=components_payload,
1428
+ allowed_mentions=allowed_mentions,
1429
+ attachments=attachments,
1430
+ files=files,
1431
+ flags=flags,
1432
+ username=username,
1433
+ avatar_url=avatar_url,
1434
+ )
1435
+
1436
+ return self._client.parse_message(message_data)
1437
+
1320
1438
 
1321
1439
  # --- Message Components ---
1322
1440
 
@@ -1708,13 +1826,13 @@ class Container(Component):
1708
1826
  def __init__(
1709
1827
  self,
1710
1828
  components: List[Component],
1711
- accent_color: Optional[int] = None,
1829
+ accent_color: Color | int | str | None = None,
1712
1830
  spoiler: bool = False,
1713
1831
  id: Optional[int] = None,
1714
1832
  ):
1715
1833
  super().__init__(ComponentType.CONTAINER)
1716
1834
  self.components = components
1717
- self.accent_color = accent_color
1835
+ self.accent_color = Color.parse(accent_color)
1718
1836
  self.spoiler = spoiler
1719
1837
  self.id = id
1720
1838
 
@@ -1722,7 +1840,7 @@ class Container(Component):
1722
1840
  payload = super().to_dict()
1723
1841
  payload["components"] = [c.to_dict() for c in self.components]
1724
1842
  if self.accent_color:
1725
- payload["accent_color"] = self.accent_color
1843
+ payload["accent_color"] = self.accent_color.value
1726
1844
  if self.spoiler:
1727
1845
  payload["spoiler"] = self.spoiler
1728
1846
  if self.id is not None:
disagreement/ui/modal.py CHANGED
@@ -74,7 +74,7 @@ def text_input(
74
74
 
75
75
  item = TextInput(
76
76
  label=label,
77
- custom_id=custom_id,
77
+ custom_id=custom_id or func.__name__,
78
78
  style=style,
79
79
  placeholder=placeholder,
80
80
  required=required,
disagreement/utils.py CHANGED
@@ -3,8 +3,71 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import datetime, timezone
6
+ from typing import Any, AsyncIterator, Dict, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING: # pragma: no cover - for type hinting only
9
+ from .models import Message, TextChannel
6
10
 
7
11
 
8
12
  def utcnow() -> datetime:
9
13
  """Return the current timezone-aware UTC time."""
10
14
  return datetime.now(timezone.utc)
15
+
16
+
17
+ async def message_pager(
18
+ channel: "TextChannel",
19
+ *,
20
+ limit: Optional[int] = None,
21
+ before: Optional[str] = None,
22
+ after: Optional[str] = None,
23
+ ) -> AsyncIterator["Message"]:
24
+ """Asynchronously paginate a channel's messages.
25
+
26
+ Parameters
27
+ ----------
28
+ channel:
29
+ The :class:`TextChannel` to fetch messages from.
30
+ limit:
31
+ The maximum number of messages to yield. ``None`` fetches until no
32
+ more messages are returned.
33
+ before:
34
+ Fetch messages with IDs less than this snowflake.
35
+ after:
36
+ Fetch messages with IDs greater than this snowflake.
37
+
38
+ Yields
39
+ ------
40
+ Message
41
+ Messages in the channel, oldest first.
42
+ """
43
+
44
+ remaining = limit
45
+ last_id = before
46
+ while remaining is None or remaining > 0:
47
+ fetch_limit = 100
48
+ if remaining is not None:
49
+ fetch_limit = min(fetch_limit, remaining)
50
+
51
+ params: Dict[str, Any] = {"limit": fetch_limit}
52
+ if last_id is not None:
53
+ params["before"] = last_id
54
+ if after is not None:
55
+ params["after"] = after
56
+
57
+ data = await channel._client._http.request( # type: ignore[attr-defined]
58
+ "GET",
59
+ f"/channels/{channel.id}/messages",
60
+ params=params,
61
+ )
62
+
63
+ if not data:
64
+ break
65
+
66
+ for raw in data:
67
+ msg = channel._client.parse_message(raw) # type: ignore[attr-defined]
68
+ yield msg
69
+ last_id = msg.id
70
+ if remaining is not None:
71
+ remaining -= 1
72
+ if remaining == 0:
73
+ return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.1.0rc1
3
+ Version: 0.1.0rc2
4
4
  Summary: A Python library for the Discord API.
5
5
  Author-email: Slipstream <me@slipstreamm.dev>
6
6
  License: BSD 3-Clause
@@ -12,22 +12,23 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: BSD License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
15
16
  Classifier: Programming Language :: Python :: 3.11
16
17
  Classifier: Programming Language :: Python :: 3.12
17
18
  Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Software Development :: Libraries
19
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
21
  Classifier: Topic :: Internet
21
- Requires-Python: >=3.11
22
+ Requires-Python: >=3.10
22
23
  Description-Content-Type: text/markdown
23
24
  License-File: LICENSE
24
25
  Requires-Dist: aiohttp<4.0.0,>=3.9.0
25
26
  Provides-Extra: test
26
27
  Requires-Dist: pytest>=8.0.0; extra == "test"
27
28
  Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
28
- Requires-Dist: hypothesis>=6.89.0; extra == "test"
29
+ Requires-Dist: hypothesis>=6.132.0; extra == "test"
29
30
  Provides-Extra: dev
30
- Requires-Dist: dotenv>=0.0.5; extra == "dev"
31
+ Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
31
32
  Dynamic: license-file
32
33
 
33
34
  # Disagreement
@@ -53,7 +54,7 @@ pip install disagreement
53
54
  pip install -e .
54
55
  ```
55
56
 
56
- Requires Python 3.11 or newer.
57
+ Requires Python 3.10 or newer.
57
58
 
58
59
  ## Basic Usage
59
60
 
@@ -1,40 +1,41 @@
1
- disagreement/__init__.py,sha256=hGvjtT1V8PPnxXYN_86hYZkHBvyP6u-tIoKD_4ZaJwo,1069
1
+ disagreement/__init__.py,sha256=4T6_19N6SjwgFXFONo6GjB3VbgODPZUc5soxJoVS_zY,1084
2
2
  disagreement/audio.py,sha256=P6inobI8CNhNVkaRKU58RMYtLq1RrSREioF0Mui5VlA,3351
3
3
  disagreement/cache.py,sha256=juabGFl4naQih5OUIVN2aN-vAfw2ZC2cI38s4nGEn8U,1525
4
- disagreement/client.py,sha256=hwZtargXv4awVylzQMQUlb1WNWpvuICf5sIBK8Tn0bc,51858
5
- disagreement/color.py,sha256=XK0Cw-q0_R5JMbHI1PNQeh1885TSplUHcLe2xF-oGho,1443
4
+ disagreement/client.py,sha256=7or564FS7fXALDyRvNrt-H32mtV1VLysuIs3CtQ9L3s,53182
5
+ disagreement/color.py,sha256=g-1ynMGCUbY0f6jJXzMLS1aJFoZg91bdMetFkZgaCC0,2387
6
6
  disagreement/components.py,sha256=tEYJ2RHVpIFtZuPPxZ0v8ssUw_x7ybhYBzHNsRiXXvU,5250
7
- disagreement/enums.py,sha256=LLeXdYKcx4TUhlojNV5X4NDuvscMbnteWRNW79d0C2c,9668
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
11
  disagreement/gateway.py,sha256=mhFtBm_YPtbleQJklv3ph3DXE4LZxC1BhtNkd7Y-akQ,23113
12
- disagreement/http.py,sha256=uFKowJZYmguz_fe9PWEh6tfq2rvcEUUy2Abbqx9h-T0,27534
12
+ disagreement/http.py,sha256=4FnVMIRjrNSpQ9IUEeWQGP6jYAhv3YTTANqFDNv0SdY,31046
13
13
  disagreement/hybrid_context.py,sha256=VYCmcreTZdPBU9v-Cy48W38vgWO2t8nM2ulC6_z4HjU,1095
14
14
  disagreement/i18n.py,sha256=1L4rcFuKP0XjHk9dVwbNh4BkLk2ZlxxZ_-tecGWa9S0,718
15
- disagreement/interactions.py,sha256=corwLVsbWM2JXHk5u8VR_Qp3GINLcKFo2y7dyI53QFA,21645
15
+ disagreement/interactions.py,sha256=aUZwwEOLsEds15i6G-rxmSSDCDmaxz_cfoTYS4tv6Ao,21735
16
16
  disagreement/logging_config.py,sha256=4q6baQPE6X_0lfaBTFMU1uqc03x5SbJqo2hsApdDFac,686
17
- disagreement/models.py,sha256=v_BfG7waYmzwouJUP6w3V94Y1lRR6AgyGvAaiZX_K1Y,71173
17
+ disagreement/models.py,sha256=JeHKJWrc7mN70oFNgd7Iic9_SgHfDlgz7aTRgMA-5PA,75070
18
18
  disagreement/oauth.py,sha256=TfDdCwg1J7osM9wDi61dtNBA5BrQk5DeQrrHsYycH34,2810
19
19
  disagreement/permissions.py,sha256=7g5cIlg-evHXOL0-pmtT5EwqcB-stXot1HZSLz724sE,3008
20
20
  disagreement/rate_limiter.py,sha256=ubwR_UTPs2MotipBdtqpgwQKx0IHt2I3cdfFcXTFv7g,2521
21
21
  disagreement/shard_manager.py,sha256=e9F8tx_4IEOlTX3-S3t51lfJToc6Ue3RVBzoNAiVKxs,2161
22
22
  disagreement/typing.py,sha256=_1oFWfZ4HyH5Q3bnF7CO24s79z-3_B5Qb69kWiwLhhU,1242
23
- disagreement/utils.py,sha256=Nj6rx--KnWCaamRHj1B2OXsZCznaMLrNdce-XY3YDg0,220
23
+ disagreement/utils.py,sha256=mz7foTCOAmUv9n8EcdZeiFarwqB14xHOG8o0p8tFuKA,2014
24
24
  disagreement/voice_client.py,sha256=i_67gJ-SQWi9YH-pgtFM8N0lCYznyuQImyL-mf2O7KQ,5384
25
25
  disagreement/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  disagreement/ext/loader.py,sha256=9_uULvNAa-a6UiaeQhWglwgIrHEPKbf9bnWtSL1KV5Q,1408
27
- disagreement/ext/tasks.py,sha256=4MueuF6r9iPJ-2cEOw1A8tgn6SCetQc6Sr442-g8OrQ,5591
28
- disagreement/ext/app_commands/__init__.py,sha256=DQ3vHz2EaJ_hTlvFRpRSofPlCuFueRoLgLriECI5xFg,991
29
- disagreement/ext/app_commands/commands.py,sha256=cY9gyXovnyBIEWmhUPe8YZfw_WXCA1qXWDWTMmX_Vm8,24095
27
+ disagreement/ext/tasks.py,sha256=b14KI-btikbrjPlD76md3Ggt6znrxPqr7TDarU4PYBg,7269
28
+ disagreement/ext/app_commands/__init__.py,sha256=mnQLIuGP9SzqGMPEn5YgOh2eIU7lcYoDXP06vtXZfTA,1014
29
+ disagreement/ext/app_commands/commands.py,sha256=0O5fJQg2esTQzx2FyEpM2ZrrLckNmv8fs3TIPnr1Q_s,19020
30
30
  disagreement/ext/app_commands/context.py,sha256=Xcm4Ka5K5uTQGviixF5LeCDdOdF9YQS5F7lZi2m--8s,20831
31
31
  disagreement/ext/app_commands/converters.py,sha256=J1VEmo-7H9K7kGPJodu5FX4RmFFI1BuzhlQAEs2MsD4,21036
32
- disagreement/ext/app_commands/decorators.py,sha256=smgx5RHedmgn7wxhBza78rQo5Guz8PEDDOvK5niJlMc,23236
32
+ disagreement/ext/app_commands/decorators.py,sha256=dKiD4ZEsafRoPvfgn9zuQ9vvXXo2qYTMquHvyUM1604,23251
33
33
  disagreement/ext/app_commands/handler.py,sha256=XO9yLgcV7aIxzhTMgFcQ1Tbr4GRZRfDBzkIAkiu6mw8,26045
34
+ disagreement/ext/app_commands/hybrid.py,sha256=yRDnlnOqgo79X669WhBHQ6LJCCuFDXKUbmlC_u3aXR0,3329
34
35
  disagreement/ext/commands/__init__.py,sha256=HxZWVfc4qvP_bCRbKTVZoMqXFq19Gj4mQvRumvQiApQ,1130
35
36
  disagreement/ext/commands/cog.py,sha256=U57yMrUpqj3_-W1-koyfGgH43MZG_JzJOl46kTur7iA,6636
36
37
  disagreement/ext/commands/converters.py,sha256=mh8xJr1FIiah6bdYy0KsdccfYcPii2Yc_IdhzCTw5uE,5864
37
- disagreement/ext/commands/core.py,sha256=SAHvmhFKmsPWP-h3SzZ_sS2U8BSyM0clURqtfrGV8T0,19144
38
+ disagreement/ext/commands/core.py,sha256=vwsj3GR9wrEy0y-1HJv16Hg9-9xnm61tvT9b14QlYEI,19296
38
39
  disagreement/ext/commands/decorators.py,sha256=Ox_D9KCFtMa-RiljFjOcsPb3stmDStRKeLw1DVeOdAw,6608
39
40
  disagreement/ext/commands/errors.py,sha256=cG5sPA-osUq2gts5scrl5yT-BHEYVHLTb4TULjAmbaY,2065
40
41
  disagreement/ext/commands/help.py,sha256=yw0ydupOsPwmnhsIIoxa93xjj9MAcBcGfD8BXa7V8G8,1456
@@ -42,11 +43,11 @@ disagreement/ext/commands/view.py,sha256=3Wo4gGJX9fb65qw8yHFwMjnAeJvMJAx19rZNHz-
42
43
  disagreement/ui/__init__.py,sha256=PLA6eHiq9cu7JDOKS-7MKtaFhlqswjbI4AEUlpnbgO0,307
43
44
  disagreement/ui/button.py,sha256=GHbY3-yMrvv6u674-qYONocuC1e2a0flEWjPKwJXKDo,3163
44
45
  disagreement/ui/item.py,sha256=bm-EmQEZpe8Kt8JrRw-o0uQdccgjErORcFsBqaXOcV8,1112
45
- disagreement/ui/modal.py,sha256=FLWFy_VkZ9UAPumX3Q_bT0q7M06O1Q7XzBLhCZyhYhI,4120
46
+ disagreement/ui/modal.py,sha256=w0ZEVslXzx2-RWUP4jdVB54zDuT81jpueVWZ70byFnI,4137
46
47
  disagreement/ui/select.py,sha256=XjWRlWkA09QZaDDLn-wDDOWIuj0Mb4VCWJEOAaExZXw,3018
47
48
  disagreement/ui/view.py,sha256=QhWoYt39QKXwl1X6Mkm5gNNEqd8bt7O505lSpiG0L04,5567
48
- disagreement-0.1.0rc1.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
49
- disagreement-0.1.0rc1.dist-info/METADATA,sha256=cFNTwfEbF3wsGzTBw16ZjYgNZbjDOexCsLlI3bSsxxE,4830
50
- disagreement-0.1.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- disagreement-0.1.0rc1.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
52
- disagreement-0.1.0rc1.dist-info/RECORD,,
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,,