disagreement 0.0.1__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.
@@ -0,0 +1,166 @@
1
+ """Message component utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional, TYPE_CHECKING
6
+
7
+ from .enums import ComponentType, ButtonStyle, ChannelType, TextInputStyle
8
+ from .models import (
9
+ ActionRow,
10
+ Button,
11
+ Component,
12
+ SelectMenu,
13
+ SelectOption,
14
+ PartialEmoji,
15
+ PartialEmoji,
16
+ Section,
17
+ TextDisplay,
18
+ Thumbnail,
19
+ MediaGallery,
20
+ MediaGalleryItem,
21
+ File,
22
+ Separator,
23
+ Container,
24
+ UnfurledMediaItem,
25
+ )
26
+
27
+ if TYPE_CHECKING: # pragma: no cover - optional client for future use
28
+ from .client import Client
29
+
30
+
31
+ def component_factory(
32
+ data: Dict[str, Any], client: Optional["Client"] = None
33
+ ) -> "Component":
34
+ """Create a component object from raw API data."""
35
+ ctype = ComponentType(data["type"])
36
+
37
+ if ctype == ComponentType.ACTION_ROW:
38
+ row = ActionRow()
39
+ for comp in data.get("components", []):
40
+ row.add_component(component_factory(comp, client))
41
+ return row
42
+
43
+ if ctype == ComponentType.BUTTON:
44
+ return Button(
45
+ style=ButtonStyle(data["style"]),
46
+ label=data.get("label"),
47
+ emoji=PartialEmoji(data["emoji"]) if data.get("emoji") else None,
48
+ custom_id=data.get("custom_id"),
49
+ url=data.get("url"),
50
+ disabled=data.get("disabled", False),
51
+ )
52
+
53
+ if ctype in {
54
+ ComponentType.STRING_SELECT,
55
+ ComponentType.USER_SELECT,
56
+ ComponentType.ROLE_SELECT,
57
+ ComponentType.MENTIONABLE_SELECT,
58
+ ComponentType.CHANNEL_SELECT,
59
+ }:
60
+ options = [
61
+ SelectOption(
62
+ label=o["label"],
63
+ value=o["value"],
64
+ description=o.get("description"),
65
+ emoji=PartialEmoji(o["emoji"]) if o.get("emoji") else None,
66
+ default=o.get("default", False),
67
+ )
68
+ for o in data.get("options", [])
69
+ ]
70
+ channel_types = None
71
+ if ctype == ComponentType.CHANNEL_SELECT and data.get("channel_types"):
72
+ channel_types = [ChannelType(ct) for ct in data.get("channel_types", [])]
73
+
74
+ return SelectMenu(
75
+ custom_id=data["custom_id"],
76
+ options=options,
77
+ placeholder=data.get("placeholder"),
78
+ min_values=data.get("min_values", 1),
79
+ max_values=data.get("max_values", 1),
80
+ disabled=data.get("disabled", False),
81
+ channel_types=channel_types,
82
+ type=ctype,
83
+ )
84
+
85
+ if ctype == ComponentType.TEXT_INPUT:
86
+ from .ui.modal import TextInput
87
+
88
+ return TextInput(
89
+ label=data.get("label", ""),
90
+ custom_id=data.get("custom_id"),
91
+ style=TextInputStyle(data.get("style", TextInputStyle.SHORT.value)),
92
+ placeholder=data.get("placeholder"),
93
+ required=data.get("required", True),
94
+ min_length=data.get("min_length"),
95
+ max_length=data.get("max_length"),
96
+ )
97
+
98
+ if ctype == ComponentType.SECTION:
99
+ # The components in a section can only be TextDisplay
100
+ section_components = []
101
+ for c in data.get("components", []):
102
+ comp = component_factory(c, client)
103
+ if isinstance(comp, TextDisplay):
104
+ section_components.append(comp)
105
+
106
+ accessory = None
107
+ if data.get("accessory"):
108
+ acc_comp = component_factory(data["accessory"], client)
109
+ if isinstance(acc_comp, (Thumbnail, Button)):
110
+ accessory = acc_comp
111
+
112
+ return Section(
113
+ components=section_components,
114
+ accessory=accessory,
115
+ id=data.get("id"),
116
+ )
117
+
118
+ if ctype == ComponentType.TEXT_DISPLAY:
119
+ return TextDisplay(content=data["content"], id=data.get("id"))
120
+
121
+ if ctype == ComponentType.THUMBNAIL:
122
+ return Thumbnail(
123
+ media=UnfurledMediaItem(**data["media"]),
124
+ description=data.get("description"),
125
+ spoiler=data.get("spoiler", False),
126
+ id=data.get("id"),
127
+ )
128
+
129
+ if ctype == ComponentType.MEDIA_GALLERY:
130
+ return MediaGallery(
131
+ items=[
132
+ MediaGalleryItem(
133
+ media=UnfurledMediaItem(**i["media"]),
134
+ description=i.get("description"),
135
+ spoiler=i.get("spoiler", False),
136
+ )
137
+ for i in data.get("items", [])
138
+ ],
139
+ id=data.get("id"),
140
+ )
141
+
142
+ if ctype == ComponentType.FILE:
143
+ return File(
144
+ file=UnfurledMediaItem(**data["file"]),
145
+ spoiler=data.get("spoiler", False),
146
+ id=data.get("id"),
147
+ )
148
+
149
+ if ctype == ComponentType.SEPARATOR:
150
+ return Separator(
151
+ divider=data.get("divider", True),
152
+ spacing=data.get("spacing", 1),
153
+ id=data.get("id"),
154
+ )
155
+
156
+ if ctype == ComponentType.CONTAINER:
157
+ return Container(
158
+ components=[
159
+ component_factory(c, client) for c in data.get("components", [])
160
+ ],
161
+ accent_color=data.get("accent_color"),
162
+ spoiler=data.get("spoiler", False),
163
+ id=data.get("id"),
164
+ )
165
+
166
+ raise ValueError(f"Unsupported component type: {ctype}")
disagreement/enums.py ADDED
@@ -0,0 +1,357 @@
1
+ # disagreement/enums.py
2
+
3
+ """
4
+ Enums for Discord constants.
5
+ """
6
+
7
+ from enum import IntEnum, Enum # Import Enum
8
+
9
+
10
+ class GatewayOpcode(IntEnum):
11
+ """Represents a Discord Gateway Opcode."""
12
+
13
+ DISPATCH = 0
14
+ HEARTBEAT = 1
15
+ IDENTIFY = 2
16
+ PRESENCE_UPDATE = 3
17
+ VOICE_STATE_UPDATE = 4
18
+ RESUME = 6
19
+ RECONNECT = 7
20
+ REQUEST_GUILD_MEMBERS = 8
21
+ INVALID_SESSION = 9
22
+ HELLO = 10
23
+ HEARTBEAT_ACK = 11
24
+
25
+
26
+ class GatewayIntent(IntEnum):
27
+ """Represents a Discord Gateway Intent bit.
28
+
29
+ Intents are used to subscribe to specific groups of events from the Gateway.
30
+ """
31
+
32
+ GUILDS = 1 << 0
33
+ GUILD_MEMBERS = 1 << 1 # Privileged
34
+ GUILD_MODERATION = 1 << 2 # Formerly GUILD_BANS
35
+ GUILD_EMOJIS_AND_STICKERS = 1 << 3
36
+ GUILD_INTEGRATIONS = 1 << 4
37
+ GUILD_WEBHOOKS = 1 << 5
38
+ GUILD_INVITES = 1 << 6
39
+ GUILD_VOICE_STATES = 1 << 7
40
+ GUILD_PRESENCES = 1 << 8 # Privileged
41
+ GUILD_MESSAGES = 1 << 9
42
+ GUILD_MESSAGE_REACTIONS = 1 << 10
43
+ GUILD_MESSAGE_TYPING = 1 << 11
44
+ DIRECT_MESSAGES = 1 << 12
45
+ DIRECT_MESSAGE_REACTIONS = 1 << 13
46
+ DIRECT_MESSAGE_TYPING = 1 << 14
47
+ MESSAGE_CONTENT = 1 << 15 # Privileged (as of Aug 31, 2022)
48
+ GUILD_SCHEDULED_EVENTS = 1 << 16
49
+ AUTO_MODERATION_CONFIGURATION = 1 << 20
50
+ AUTO_MODERATION_EXECUTION = 1 << 21
51
+
52
+ @classmethod
53
+ def default(cls) -> int:
54
+ """Returns default intents (excluding privileged ones like members, presences, message content)."""
55
+ return (
56
+ cls.GUILDS
57
+ | cls.GUILD_MODERATION
58
+ | cls.GUILD_EMOJIS_AND_STICKERS
59
+ | cls.GUILD_INTEGRATIONS
60
+ | cls.GUILD_WEBHOOKS
61
+ | cls.GUILD_INVITES
62
+ | cls.GUILD_VOICE_STATES
63
+ | cls.GUILD_MESSAGES
64
+ | cls.GUILD_MESSAGE_REACTIONS
65
+ | cls.GUILD_MESSAGE_TYPING
66
+ | cls.DIRECT_MESSAGES
67
+ | cls.DIRECT_MESSAGE_REACTIONS
68
+ | cls.DIRECT_MESSAGE_TYPING
69
+ | cls.GUILD_SCHEDULED_EVENTS
70
+ | cls.AUTO_MODERATION_CONFIGURATION
71
+ | cls.AUTO_MODERATION_EXECUTION
72
+ )
73
+
74
+ @classmethod
75
+ def all(cls) -> int:
76
+ """Returns all intents, including privileged ones. Use with caution."""
77
+ val = 0
78
+ for intent in cls:
79
+ val |= intent.value
80
+ return val
81
+
82
+ @classmethod
83
+ def privileged(cls) -> int:
84
+ """Returns a bitmask of all privileged intents."""
85
+ return cls.GUILD_MEMBERS | cls.GUILD_PRESENCES | cls.MESSAGE_CONTENT
86
+
87
+
88
+ # --- Application Command Enums ---
89
+
90
+
91
+ class ApplicationCommandType(IntEnum):
92
+ """Type of application command."""
93
+
94
+ CHAT_INPUT = 1
95
+ USER = 2
96
+ MESSAGE = 3
97
+ PRIMARY_ENTRY_POINT = 4
98
+
99
+
100
+ class ApplicationCommandOptionType(IntEnum):
101
+ """Type of application command option."""
102
+
103
+ SUB_COMMAND = 1
104
+ SUB_COMMAND_GROUP = 2
105
+ STRING = 3
106
+ INTEGER = 4 # Any integer between -2^53 and 2^53
107
+ BOOLEAN = 5
108
+ USER = 6
109
+ CHANNEL = 7 # Includes all channel types + categories
110
+ ROLE = 8
111
+ MENTIONABLE = 9 # Includes users and roles
112
+ NUMBER = 10 # Any double between -2^53 and 2^53
113
+ ATTACHMENT = 11
114
+
115
+
116
+ class InteractionType(IntEnum):
117
+ """Type of interaction."""
118
+
119
+ PING = 1
120
+ APPLICATION_COMMAND = 2
121
+ MESSAGE_COMPONENT = 3
122
+ APPLICATION_COMMAND_AUTOCOMPLETE = 4
123
+ MODAL_SUBMIT = 5
124
+
125
+
126
+ class InteractionCallbackType(IntEnum):
127
+ """Type of interaction callback."""
128
+
129
+ PONG = 1
130
+ CHANNEL_MESSAGE_WITH_SOURCE = 4
131
+ DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
132
+ DEFERRED_UPDATE_MESSAGE = 6
133
+ UPDATE_MESSAGE = 7
134
+ APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8
135
+ MODAL = 9 # Response to send a modal
136
+
137
+
138
+ class IntegrationType(IntEnum):
139
+ """
140
+ Installation context(s) where the command is available,
141
+ only for globally-scoped commands.
142
+ """
143
+
144
+ GUILD_INSTALL = (
145
+ 0 # Command is available when the app is installed to a guild (default)
146
+ )
147
+ USER_INSTALL = 1 # Command is available when the app is installed to a user
148
+
149
+
150
+ class InteractionContextType(IntEnum):
151
+ """
152
+ Interaction context(s) where the command can be used,
153
+ only for globally-scoped commands.
154
+ """
155
+
156
+ GUILD = 0 # Command can be used in guilds
157
+ BOT_DM = 1 # Command can be used in DMs with the app's bot user
158
+ PRIVATE_CHANNEL = 2 # Command can be used in Group DMs and DMs (requires USER_INSTALL integration_type)
159
+
160
+
161
+ class MessageFlags(IntEnum):
162
+ """Represents the flags of a message."""
163
+
164
+ CROSSPOSTED = 1 << 0
165
+ IS_CROSSPOST = 1 << 1
166
+ SUPPRESS_EMBEDS = 1 << 2
167
+ SOURCE_MESSAGE_DELETED = 1 << 3
168
+ URGENT = 1 << 4
169
+ HAS_THREAD = 1 << 5
170
+ EPHEMERAL = 1 << 6
171
+ LOADING = 1 << 7
172
+ FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8
173
+ SUPPRESS_NOTIFICATIONS = (
174
+ 1 << 12
175
+ ) # Discord specific, was previously 1 << 4 (IS_VOICE_MESSAGE)
176
+ IS_COMPONENTS_V2 = 1 << 15
177
+
178
+
179
+ # --- Guild Enums ---
180
+
181
+
182
+ class VerificationLevel(IntEnum):
183
+ """Guild verification level."""
184
+
185
+ NONE = 0
186
+ LOW = 1
187
+ MEDIUM = 2
188
+ HIGH = 3
189
+ VERY_HIGH = 4
190
+
191
+
192
+ class MessageNotificationLevel(IntEnum):
193
+ """Default message notification level for a guild."""
194
+
195
+ ALL_MESSAGES = 0
196
+ ONLY_MENTIONS = 1
197
+
198
+
199
+ class ExplicitContentFilterLevel(IntEnum):
200
+ """Explicit content filter level for a guild."""
201
+
202
+ DISABLED = 0
203
+ MEMBERS_WITHOUT_ROLES = 1
204
+ ALL_MEMBERS = 2
205
+
206
+
207
+ class MFALevel(IntEnum):
208
+ """Multi-Factor Authentication level for a guild."""
209
+
210
+ NONE = 0
211
+ ELEVATED = 1
212
+
213
+
214
+ class GuildNSFWLevel(IntEnum):
215
+ """NSFW level of a guild."""
216
+
217
+ DEFAULT = 0
218
+ EXPLICIT = 1
219
+ SAFE = 2
220
+ AGE_RESTRICTED = 3
221
+
222
+
223
+ class PremiumTier(IntEnum):
224
+ """Guild premium tier (boost level)."""
225
+
226
+ NONE = 0
227
+ TIER_1 = 1
228
+ TIER_2 = 2
229
+ TIER_3 = 3
230
+
231
+
232
+ class GuildFeature(str, Enum): # Changed from IntEnum to Enum
233
+ """Features that a guild can have.
234
+
235
+ Note: This is not an exhaustive list and Discord may add more.
236
+ Using str as a base allows for unknown features to be stored as strings.
237
+ """
238
+
239
+ ANIMATED_BANNER = "ANIMATED_BANNER"
240
+ ANIMATED_ICON = "ANIMATED_ICON"
241
+ APPLICATION_COMMAND_PERMISSIONS_V2 = "APPLICATION_COMMAND_PERMISSIONS_V2"
242
+ AUTO_MODERATION = "AUTO_MODERATION"
243
+ BANNER = "BANNER"
244
+ COMMUNITY = "COMMUNITY"
245
+ CREATOR_MONETIZABLE_PROVISIONAL = "CREATOR_MONETIZABLE_PROVISIONAL"
246
+ CREATOR_STORE_PAGE = "CREATOR_STORE_PAGE"
247
+ DEVELOPER_SUPPORT_SERVER = "DEVELOPER_SUPPORT_SERVER"
248
+ DISCOVERABLE = "DISCOVERABLE"
249
+ FEATURABLE = "FEATURABLE"
250
+ INVITES_DISABLED = "INVITES_DISABLED"
251
+ INVITE_SPLASH = "INVITE_SPLASH"
252
+ MEMBER_VERIFICATION_GATE_ENABLED = "MEMBER_VERIFICATION_GATE_ENABLED"
253
+ MORE_STICKERS = "MORE_STICKERS"
254
+ NEWS = "NEWS"
255
+ PARTNERED = "PARTNERED"
256
+ PREVIEW_ENABLED = "PREVIEW_ENABLED"
257
+ RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED"
258
+ ROLE_ICONS = "ROLE_ICONS"
259
+ ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE = (
260
+ "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE"
261
+ )
262
+ ROLE_SUBSCRIPTIONS_ENABLED = "ROLE_SUBSCRIPTIONS_ENABLED"
263
+ TICKETED_EVENTS_ENABLED = "TICKETED_EVENTS_ENABLED"
264
+ VANITY_URL = "VANITY_URL"
265
+ VERIFIED = "VERIFIED"
266
+ VIP_REGIONS = "VIP_REGIONS"
267
+ WELCOME_SCREEN_ENABLED = "WELCOME_SCREEN_ENABLED"
268
+ # Add more as they become known or needed
269
+
270
+ # This allows GuildFeature("UNKNOWN_FEATURE_STRING") to work
271
+ @classmethod
272
+ def _missing_(cls, value): # type: ignore
273
+ return str(value)
274
+
275
+
276
+ # --- Channel Enums ---
277
+
278
+
279
+ class ChannelType(IntEnum):
280
+ """Type of channel."""
281
+
282
+ GUILD_TEXT = 0 # a text channel within a server
283
+ DM = 1 # a direct message between users
284
+ GUILD_VOICE = 2 # a voice channel within a server
285
+ GROUP_DM = 3 # a direct message between multiple users
286
+ GUILD_CATEGORY = 4 # an organizational category that contains up to 50 channels
287
+ GUILD_ANNOUNCEMENT = 5 # a channel that users can follow and crosspost into their own server (formerly GUILD_NEWS)
288
+ ANNOUNCEMENT_THREAD = (
289
+ 10 # a temporary sub-channel within a GUILD_ANNOUNCEMENT channel
290
+ )
291
+ PUBLIC_THREAD = (
292
+ 11 # a temporary sub-channel within a GUILD_TEXT or GUILD_ANNOUNCEMENT channel
293
+ )
294
+ PRIVATE_THREAD = 12 # a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
295
+ GUILD_STAGE_VOICE = (
296
+ 13 # a voice channel for hosting events with speakers and audiences
297
+ )
298
+ GUILD_DIRECTORY = 14 # a channel in a hub containing the listed servers
299
+ GUILD_FORUM = 15 # (Still in development) a channel that can only contain threads
300
+ GUILD_MEDIA = 16 # (Still in development) a channel that can only contain media
301
+
302
+
303
+ class OverwriteType(IntEnum):
304
+ """Type of target for a permission overwrite."""
305
+
306
+ ROLE = 0
307
+ MEMBER = 1
308
+
309
+
310
+ # --- Component Enums ---
311
+
312
+
313
+ class ComponentType(IntEnum):
314
+ """Type of message component."""
315
+
316
+ ACTION_ROW = 1
317
+ BUTTON = 2
318
+ STRING_SELECT = 3 # Formerly SELECT_MENU
319
+ TEXT_INPUT = 4
320
+ USER_SELECT = 5
321
+ ROLE_SELECT = 6
322
+ MENTIONABLE_SELECT = 7
323
+ CHANNEL_SELECT = 8
324
+ SECTION = 9
325
+ TEXT_DISPLAY = 10
326
+ THUMBNAIL = 11
327
+ MEDIA_GALLERY = 12
328
+ FILE = 13
329
+ SEPARATOR = 14
330
+ CONTAINER = 17
331
+
332
+
333
+ class ButtonStyle(IntEnum):
334
+ """Style of a button component."""
335
+
336
+ # Blurple
337
+ PRIMARY = 1
338
+ # Grey
339
+ SECONDARY = 2
340
+ # Green
341
+ SUCCESS = 3
342
+ # Red
343
+ DANGER = 4
344
+ # Grey, navigates to a URL
345
+ LINK = 5
346
+
347
+
348
+ class TextInputStyle(IntEnum):
349
+ """Style of a text input component."""
350
+
351
+ SHORT = 1
352
+ PARAGRAPH = 2
353
+
354
+
355
+ # Example of how you might combine intents:
356
+ # intents = GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES | GatewayIntent.MESSAGE_CONTENT
357
+ # client = Client(token="YOUR_TOKEN", intents=intents)
@@ -0,0 +1,33 @@
1
+ import asyncio
2
+ import logging
3
+ import traceback
4
+ from typing import Optional
5
+
6
+ from .logging_config import setup_logging
7
+
8
+
9
+ def setup_global_error_handler(
10
+ loop: Optional[asyncio.AbstractEventLoop] = None,
11
+ ) -> None:
12
+ """Configure a basic global error handler for the provided loop.
13
+
14
+ The handler logs unhandled exceptions so they don't crash the bot.
15
+ """
16
+ if loop is None:
17
+ loop = asyncio.get_event_loop()
18
+
19
+ if not logging.getLogger().hasHandlers():
20
+ setup_logging(logging.ERROR)
21
+
22
+ def handle_exception(loop: asyncio.AbstractEventLoop, context: dict) -> None:
23
+ exception = context.get("exception")
24
+ if exception:
25
+ logging.error("Unhandled exception in event loop: %s", exception)
26
+ traceback.print_exception(
27
+ type(exception), exception, exception.__traceback__
28
+ )
29
+ else:
30
+ message = context.get("message")
31
+ logging.error("Event loop error: %s", message)
32
+
33
+ loop.set_exception_handler(handle_exception)
disagreement/errors.py ADDED
@@ -0,0 +1,112 @@
1
+ # disagreement/errors.py
2
+
3
+ """
4
+ Custom exceptions for the Disagreement library.
5
+ """
6
+
7
+ from typing import Optional, Any # Add Optional and Any here
8
+
9
+
10
+ class DisagreementException(Exception):
11
+ """Base exception class for all errors raised by this library."""
12
+
13
+ pass
14
+
15
+
16
+ class HTTPException(DisagreementException):
17
+ """Exception raised for HTTP-related errors.
18
+
19
+ Attributes:
20
+ response: The aiohttp response object, if available.
21
+ status: The HTTP status code.
22
+ text: The response text, if available.
23
+ error_code: Discord specific error code, if available.
24
+ """
25
+
26
+ def __init__(
27
+ self, response=None, message=None, *, status=None, text=None, error_code=None
28
+ ):
29
+ self.response = response
30
+ self.status = status or (response.status if response else None)
31
+ self.text = text or (
32
+ response.text if response else None
33
+ ) # Or await response.text() if in async context
34
+ self.error_code = error_code
35
+
36
+ full_message = f"HTTP {self.status}"
37
+ if message:
38
+ full_message += f": {message}"
39
+ elif self.text:
40
+ full_message += f": {self.text}"
41
+ if self.error_code:
42
+ full_message += f" (Discord Error Code: {self.error_code})"
43
+
44
+ super().__init__(full_message)
45
+
46
+
47
+ class GatewayException(DisagreementException):
48
+ """Exception raised for errors related to the Discord Gateway connection or protocol."""
49
+
50
+ pass
51
+
52
+
53
+ class AuthenticationError(DisagreementException):
54
+ """Exception raised for authentication failures (e.g., invalid token)."""
55
+
56
+ pass
57
+
58
+
59
+ class RateLimitError(HTTPException):
60
+ """
61
+ Exception raised when a rate limit is encountered.
62
+
63
+ Attributes:
64
+ retry_after (float): The number of seconds to wait before retrying.
65
+ is_global (bool): Whether this is a global rate limit.
66
+ """
67
+
68
+ def __init__(
69
+ self, response, message=None, *, retry_after: float, is_global: bool = False
70
+ ):
71
+ self.retry_after = retry_after
72
+ self.is_global = is_global
73
+ super().__init__(
74
+ response,
75
+ message
76
+ or f"Rate limited. Retry after: {retry_after}s. Global: {is_global}",
77
+ )
78
+
79
+
80
+ # You can add more specific exceptions as needed, e.g.:
81
+ # class NotFound(HTTPException):
82
+ # """Raised for 404 Not Found errors."""
83
+ # pass
84
+
85
+ # class Forbidden(HTTPException):
86
+ # """Raised for 403 Forbidden errors."""
87
+ # pass
88
+
89
+
90
+ class AppCommandError(DisagreementException):
91
+ """Base exception for application command related errors."""
92
+
93
+ pass
94
+
95
+
96
+ class AppCommandOptionConversionError(AppCommandError):
97
+ """Exception raised when an application command option fails to convert."""
98
+
99
+ def __init__(
100
+ self,
101
+ message: str,
102
+ option_name: Optional[str] = None,
103
+ original_value: Any = None,
104
+ ):
105
+ self.option_name = option_name
106
+ self.original_value = original_value
107
+ full_message = message
108
+ if option_name:
109
+ full_message = f"Failed to convert option '{option_name}': {message}"
110
+ if original_value is not None:
111
+ full_message += f" (Original value: '{original_value}')"
112
+ super().__init__(full_message)