disagreement 0.0.1__py3-none-any.whl → 0.0.2__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.0.1"
17
+ __version__ = "0.0.2"
18
18
 
19
19
  from .client import Client
20
20
  from .models import Message, User
File without changes
@@ -0,0 +1,46 @@
1
+ # disagreement/ext/app_commands/__init__.py
2
+
3
+ """
4
+ Application Commands Extension for Disagreement.
5
+
6
+ This package provides the framework for creating and handling
7
+ Discord Application Commands (slash commands, user commands, message commands).
8
+ """
9
+
10
+ from .commands import (
11
+ AppCommand,
12
+ SlashCommand,
13
+ UserCommand,
14
+ MessageCommand,
15
+ AppCommandGroup,
16
+ )
17
+ from .decorators import (
18
+ slash_command,
19
+ user_command,
20
+ message_command,
21
+ hybrid_command,
22
+ group,
23
+ subcommand,
24
+ subcommand_group,
25
+ OptionMetadata,
26
+ )
27
+ from .context import AppCommandContext
28
+
29
+ # from .handler import AppCommandHandler # Will be imported when defined
30
+
31
+ __all__ = [
32
+ "AppCommand",
33
+ "SlashCommand",
34
+ "UserCommand",
35
+ "MessageCommand",
36
+ "AppCommandGroup", # To be defined
37
+ "slash_command",
38
+ "user_command",
39
+ "message_command",
40
+ "hybrid_command",
41
+ "group",
42
+ "subcommand",
43
+ "subcommand_group",
44
+ "OptionMetadata",
45
+ "AppCommandContext", # To be defined
46
+ ]
@@ -0,0 +1,513 @@
1
+ # disagreement/ext/app_commands/commands.py
2
+
3
+ import inspect
4
+ from typing import Callable, Optional, List, Dict, Any, Union, TYPE_CHECKING
5
+
6
+
7
+ 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
21
+ 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
+ try:
37
+ 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
56
+
57
+
58
+ class AppCommand:
59
+ """
60
+ Base class for an application command.
61
+
62
+ Attributes:
63
+ name (str): The name of the command.
64
+ description (Optional[str]): The description of the command.
65
+ Required for CHAT_INPUT, empty for USER and MESSAGE commands.
66
+ callback (Callable[..., Any]): The coroutine function that will be called when the command is executed.
67
+ type (ApplicationCommandType): The type of the application command.
68
+ options (Optional[List[ApplicationCommandOption]]): Parameters for the command. Populated by decorators.
69
+ guild_ids (Optional[List[Snowflake]]): List of guild IDs where this command is active. None for global.
70
+ default_member_permissions (Optional[str]): Bitwise permissions required by default for users to run the command.
71
+ nsfw (bool): Whether the command is age-restricted.
72
+ parent (Optional['AppCommandGroup']): The parent group if this is a subcommand.
73
+ cog (Optional[Cog]): The cog this command belongs to, if any.
74
+ _full_description (Optional[str]): Stores the original full description, e.g. from docstring,
75
+ even if the payload description is different (like for User/Message commands).
76
+ name_localizations (Optional[Dict[str, str]]): Localizations for the command's name.
77
+ description_localizations (Optional[Dict[str, str]]): Localizations for the command's description.
78
+ integration_types (Optional[List[IntegrationType]]): Installation contexts.
79
+ contexts (Optional[List[InteractionContextType]]): Interaction contexts.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ callback: Callable[..., Any],
85
+ *,
86
+ name: str,
87
+ description: Optional[str] = None,
88
+ locale: Optional[str] = None,
89
+ type: "ApplicationCommandType",
90
+ guild_ids: Optional[List["Snowflake"]] = None,
91
+ default_member_permissions: Optional[str] = None,
92
+ nsfw: bool = False,
93
+ parent: Optional["AppCommandGroup"] = None,
94
+ cog: Optional[
95
+ Any
96
+ ] = None, # Changed 'Cog' to Any to avoid runtime import issues if Cog is complex
97
+ name_localizations: Optional[Dict[str, str]] = None,
98
+ description_localizations: Optional[Dict[str, str]] = None,
99
+ integration_types: Optional[List["IntegrationType"]] = None,
100
+ contexts: Optional[List["InteractionContextType"]] = None,
101
+ ):
102
+ if not asyncio.iscoroutinefunction(callback):
103
+ raise TypeError(
104
+ "Application command callback must be a coroutine function."
105
+ )
106
+
107
+ if locale:
108
+ from disagreement import i18n
109
+
110
+ translate = i18n.translate
111
+
112
+ self.name = translate(name, locale)
113
+ self.description = (
114
+ translate(description, locale) if description is not None else None
115
+ )
116
+ else:
117
+ self.name = name
118
+ self.description = description
119
+ self.locale: Optional[str] = locale
120
+ self.callback: Callable[..., Any] = callback
121
+ self.type: "ApplicationCommandType" = type
122
+ self.options: List["ApplicationCommandOption"] = [] # Populated by decorator
123
+ self.guild_ids: Optional[List["Snowflake"]] = guild_ids
124
+ self.default_member_permissions: Optional[str] = default_member_permissions
125
+ self.nsfw: bool = nsfw
126
+ self.parent: Optional["AppCommandGroup"] = parent
127
+ self.cog: Optional[Any] = cog # Changed 'Cog' to Any
128
+ self.name_localizations: Optional[Dict[str, str]] = name_localizations
129
+ self.description_localizations: Optional[Dict[str, str]] = (
130
+ description_localizations
131
+ )
132
+ self.integration_types: Optional[List["IntegrationType"]] = integration_types
133
+ self.contexts: Optional[List["InteractionContextType"]] = contexts
134
+ self._full_description: Optional[str] = (
135
+ None # Initialized by decorator if needed
136
+ )
137
+
138
+ # Signature for argument parsing by decorators/handlers
139
+ self.params = inspect.signature(callback).parameters
140
+
141
+ async def invoke(
142
+ self, context: "AppCommandContext", *args: Any, **kwargs: Any
143
+ ) -> None:
144
+ """Invokes the command's callback with the given context and arguments."""
145
+ # Similar to Command.invoke, handle cog if present
146
+ actual_args = []
147
+ if self.cog:
148
+ actual_args.append(self.cog)
149
+ actual_args.append(context)
150
+ actual_args.extend(args)
151
+
152
+ await self.callback(*actual_args, **kwargs)
153
+
154
+ def to_dict(self) -> Dict[str, Any]:
155
+ """Converts the command to a dictionary payload for Discord API."""
156
+ payload: Dict[str, Any] = {
157
+ "name": self.name,
158
+ "type": self.type.value,
159
+ # CHAT_INPUT commands require a description.
160
+ # USER and MESSAGE commands must have an empty description in the payload if not omitted.
161
+ # The constructor for UserCommand/MessageCommand already sets self.description to ""
162
+ "description": (
163
+ self.description
164
+ if self.type == ApplicationCommandType.CHAT_INPUT
165
+ else ""
166
+ ),
167
+ }
168
+
169
+ # For CHAT_INPUT commands, options are its parameters.
170
+ # For USER/MESSAGE commands, options should be empty or not present.
171
+ if self.type == ApplicationCommandType.CHAT_INPUT and self.options:
172
+ payload["options"] = [opt.to_dict() for opt in self.options]
173
+
174
+ if self.default_member_permissions is not None: # Can be "0" for no permissions
175
+ payload["default_member_permissions"] = str(self.default_member_permissions)
176
+
177
+ # nsfw defaults to False, only include if True
178
+ if self.nsfw:
179
+ payload["nsfw"] = True
180
+
181
+ if self.name_localizations:
182
+ payload["name_localizations"] = self.name_localizations
183
+
184
+ # Description localizations only apply if there's a description (CHAT_INPUT commands)
185
+ if (
186
+ self.type == ApplicationCommandType.CHAT_INPUT
187
+ and self.description
188
+ and self.description_localizations
189
+ ):
190
+ payload["description_localizations"] = self.description_localizations
191
+
192
+ if self.integration_types:
193
+ payload["integration_types"] = [it.value for it in self.integration_types]
194
+
195
+ if self.contexts:
196
+ payload["contexts"] = [ict.value for ict in self.contexts]
197
+
198
+ # According to Discord API, guild_id is not part of this payload,
199
+ # it's used in the URL path for guild-specific command registration.
200
+ # However, the global command registration takes an 'application_id' in the payload,
201
+ # but that's handled by the HTTPClient.
202
+
203
+ return payload
204
+
205
+ def __repr__(self) -> str:
206
+ return f"<{self.__class__.__name__} name='{self.name}' type={self.type!r}>"
207
+
208
+
209
+ class SlashCommand(AppCommand):
210
+ """Represents a CHAT_INPUT (slash) command."""
211
+
212
+ def __init__(self, callback: Callable[..., Any], **kwargs: Any):
213
+ if not kwargs.get("description"):
214
+ raise ValueError("SlashCommand requires a description.")
215
+ super().__init__(callback, type=ApplicationCommandType.CHAT_INPUT, **kwargs)
216
+
217
+
218
+ class UserCommand(AppCommand):
219
+ """Represents a USER context menu command."""
220
+
221
+ def __init__(self, callback: Callable[..., Any], **kwargs: Any):
222
+ # Description is not allowed by Discord API for User Commands, but can be set to empty string.
223
+ kwargs["description"] = kwargs.get(
224
+ "description", ""
225
+ ) # Ensure it's empty or not present in payload
226
+ super().__init__(callback, type=ApplicationCommandType.USER, **kwargs)
227
+
228
+
229
+ class MessageCommand(AppCommand):
230
+ """Represents a MESSAGE context menu command."""
231
+
232
+ def __init__(self, callback: Callable[..., Any], **kwargs: Any):
233
+ # Description is not allowed by Discord API for Message Commands.
234
+ kwargs["description"] = kwargs.get("description", "")
235
+ super().__init__(callback, type=ApplicationCommandType.MESSAGE, **kwargs)
236
+
237
+
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
+
292
+
293
+ class AppCommandGroup:
294
+ """
295
+ Represents a group of application commands (subcommands or subcommand groups).
296
+ This itself is not directly callable but acts as a namespace.
297
+ """
298
+
299
+ def __init__(
300
+ self,
301
+ name: str,
302
+ description: Optional[
303
+ str
304
+ ] = None, # Required for top-level groups that form part of a slash command
305
+ guild_ids: Optional[List["Snowflake"]] = None,
306
+ parent: Optional["AppCommandGroup"] = None,
307
+ default_member_permissions: Optional[str] = None,
308
+ nsfw: bool = False,
309
+ name_localizations: Optional[Dict[str, str]] = None,
310
+ description_localizations: Optional[Dict[str, str]] = None,
311
+ integration_types: Optional[List["IntegrationType"]] = None,
312
+ contexts: Optional[List["InteractionContextType"]] = None,
313
+ ):
314
+ self.name: str = name
315
+ self.description: Optional[str] = description
316
+ self.guild_ids: Optional[List["Snowflake"]] = guild_ids
317
+ self.parent: Optional["AppCommandGroup"] = parent
318
+ self.commands: Dict[str, Union[AppCommand, "AppCommandGroup"]] = {}
319
+ self.default_member_permissions: Optional[str] = default_member_permissions
320
+ self.nsfw: bool = nsfw
321
+ self.name_localizations: Optional[Dict[str, str]] = name_localizations
322
+ self.description_localizations: Optional[Dict[str, str]] = (
323
+ description_localizations
324
+ )
325
+ self.integration_types: Optional[List["IntegrationType"]] = integration_types
326
+ self.contexts: Optional[List["InteractionContextType"]] = contexts
327
+ # A group itself doesn't have a cog directly, its commands do.
328
+
329
+ def add_command(self, command: Union[AppCommand, "AppCommandGroup"]) -> None:
330
+ if command.name in self.commands:
331
+ raise ValueError(
332
+ f"Command or group '{command.name}' already exists in group '{self.name}'."
333
+ )
334
+ command.parent = self
335
+ self.commands[command.name] = command
336
+
337
+ def get_command(self, name: str) -> Optional[Union[AppCommand, "AppCommandGroup"]]:
338
+ return self.commands.get(name)
339
+
340
+ def command(self, *d_args: Any, **d_kwargs: Any):
341
+ d_kwargs.setdefault("parent", self)
342
+ from .decorators import slash_command
343
+
344
+ return slash_command(*d_args, **d_kwargs)
345
+
346
+ def group(
347
+ self,
348
+ name: str,
349
+ description: Optional[str] = None,
350
+ **kwargs: Any,
351
+ ):
352
+ sub_group = AppCommandGroup(
353
+ name=name,
354
+ description=description,
355
+ parent=self,
356
+ guild_ids=kwargs.get("guild_ids"),
357
+ default_member_permissions=kwargs.get("default_member_permissions"),
358
+ nsfw=kwargs.get("nsfw", False),
359
+ name_localizations=kwargs.get("name_localizations"),
360
+ description_localizations=kwargs.get("description_localizations"),
361
+ integration_types=kwargs.get("integration_types"),
362
+ contexts=kwargs.get("contexts"),
363
+ )
364
+ self.add_command(sub_group)
365
+
366
+ def decorator(func: Optional[Callable[..., Any]] = None):
367
+ if func is not None:
368
+ setattr(func, "__app_command_object__", sub_group)
369
+ return sub_group
370
+ return sub_group
371
+
372
+ return decorator
373
+
374
+ def __repr__(self) -> str:
375
+ return f"<AppCommandGroup name='{self.name}' commands={len(self.commands)}>"
376
+
377
+ def to_dict(self) -> Dict[str, Any]:
378
+ """
379
+ Converts the command group to a dictionary payload for Discord API.
380
+ This represents a top-level command that has subcommands/subcommand groups.
381
+ """
382
+ payload: Dict[str, Any] = {
383
+ "name": self.name,
384
+ "type": ApplicationCommandType.CHAT_INPUT.value, # Groups are implicitly CHAT_INPUT
385
+ "description": self.description
386
+ or "No description provided", # Top-level groups require a description
387
+ "options": [],
388
+ }
389
+
390
+ if self.default_member_permissions is not None:
391
+ payload["default_member_permissions"] = str(self.default_member_permissions)
392
+ if self.nsfw:
393
+ payload["nsfw"] = True
394
+ if self.name_localizations:
395
+ payload["name_localizations"] = self.name_localizations
396
+ if (
397
+ self.description and self.description_localizations
398
+ ): # Only if description is not empty
399
+ payload["description_localizations"] = self.description_localizations
400
+ if self.integration_types:
401
+ payload["integration_types"] = [it.value for it in self.integration_types]
402
+ if self.contexts:
403
+ payload["contexts"] = [ict.value for ict in self.contexts]
404
+
405
+ # guild_ids are handled at the registration level, not in this specific payload part.
406
+
407
+ options_payload: List[Dict[str, Any]] = []
408
+ for cmd_name, command_or_group in self.commands.items():
409
+ if isinstance(command_or_group, AppCommand): # This is a Subcommand
410
+ # Subcommands use their own options (parameters)
411
+ sub_options = (
412
+ [opt.to_dict() for opt in command_or_group.options]
413
+ if command_or_group.options
414
+ else []
415
+ )
416
+ option_dict = {
417
+ "type": ApplicationCommandOptionType.SUB_COMMAND.value,
418
+ "name": command_or_group.name,
419
+ "description": command_or_group.description
420
+ or "No description provided",
421
+ "options": sub_options,
422
+ }
423
+ # Add localization for subcommand name and description if available
424
+ if command_or_group.name_localizations:
425
+ option_dict["name_localizations"] = (
426
+ command_or_group.name_localizations
427
+ )
428
+ if (
429
+ command_or_group.description
430
+ and command_or_group.description_localizations
431
+ ):
432
+ option_dict["description_localizations"] = (
433
+ command_or_group.description_localizations
434
+ )
435
+ options_payload.append(option_dict)
436
+
437
+ elif isinstance(
438
+ command_or_group, AppCommandGroup
439
+ ): # This is a Subcommand Group
440
+ # Subcommand groups have their subcommands/groups as options
441
+ sub_group_options: List[Dict[str, Any]] = []
442
+ for sub_cmd_name, sub_command in command_or_group.commands.items():
443
+ # Nested groups can only contain subcommands, not further nested groups as per Discord rules.
444
+ # So, sub_command here must be an AppCommand.
445
+ if isinstance(
446
+ sub_command, AppCommand
447
+ ): # Should always be AppCommand if structure is valid
448
+ sub_cmd_options = (
449
+ [opt.to_dict() for opt in sub_command.options]
450
+ if sub_command.options
451
+ else []
452
+ )
453
+ sub_group_option_entry = {
454
+ "type": ApplicationCommandOptionType.SUB_COMMAND.value,
455
+ "name": sub_command.name,
456
+ "description": sub_command.description
457
+ or "No description provided",
458
+ "options": sub_cmd_options,
459
+ }
460
+ # Add localization for subcommand name and description if available
461
+ if sub_command.name_localizations:
462
+ sub_group_option_entry["name_localizations"] = (
463
+ sub_command.name_localizations
464
+ )
465
+ if (
466
+ sub_command.description
467
+ and sub_command.description_localizations
468
+ ):
469
+ sub_group_option_entry["description_localizations"] = (
470
+ sub_command.description_localizations
471
+ )
472
+ sub_group_options.append(sub_group_option_entry)
473
+ # else:
474
+ # # This case implies a group nested inside a group, which then contains another group.
475
+ # # Discord's structure is:
476
+ # # command -> option (SUB_COMMAND_GROUP) -> option (SUB_COMMAND) -> option (param)
477
+ # # This should be caught by validation logic in decorators or add_command.
478
+ # # For now, we assume valid structure where AppCommandGroup's commands are AppCommands.
479
+ # pass
480
+
481
+ option_dict = {
482
+ "type": ApplicationCommandOptionType.SUB_COMMAND_GROUP.value,
483
+ "name": command_or_group.name,
484
+ "description": command_or_group.description
485
+ or "No description provided",
486
+ "options": sub_group_options, # These are the SUB_COMMANDs
487
+ }
488
+ # Add localization for subcommand group name and description if available
489
+ if command_or_group.name_localizations:
490
+ option_dict["name_localizations"] = (
491
+ command_or_group.name_localizations
492
+ )
493
+ if (
494
+ command_or_group.description
495
+ and command_or_group.description_localizations
496
+ ):
497
+ option_dict["description_localizations"] = (
498
+ command_or_group.description_localizations
499
+ )
500
+ options_payload.append(option_dict)
501
+
502
+ payload["options"] = options_payload
503
+ return payload
504
+
505
+
506
+ # Need to import asyncio for iscoroutinefunction check
507
+ import asyncio
508
+
509
+ if TYPE_CHECKING:
510
+ from .context import AppCommandContext # For type hint in AppCommand.invoke
511
+
512
+ # Ensure ApplicationCommandOptionType is available for the to_dict method
513
+ from disagreement.enums import ApplicationCommandOptionType