disagreement 0.1.0rc3__py3-none-any.whl → 0.3.0b1__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/color.py CHANGED
@@ -46,11 +46,110 @@ class Color:
46
46
  def blue(cls) -> "Color":
47
47
  return cls(0x0000FF)
48
48
 
49
+ # Discord brand colors
50
+ @classmethod
51
+ def blurple(cls) -> "Color":
52
+ """Discord brand blurple (#5865F2)."""
53
+ return cls(0x5865F2)
54
+
55
+ @classmethod
56
+ def light_blurple(cls) -> "Color":
57
+ """Light blurple used by Discord (#E0E3FF)."""
58
+ return cls(0xE0E3FF)
59
+
60
+ @classmethod
61
+ def legacy_blurple(cls) -> "Color":
62
+ """Legacy Discord blurple (#7289DA)."""
63
+ return cls(0x7289DA)
64
+
65
+ # Additional assorted colors
66
+ @classmethod
67
+ def teal(cls) -> "Color":
68
+ return cls(0x1ABC9C)
69
+
70
+ @classmethod
71
+ def dark_teal(cls) -> "Color":
72
+ return cls(0x11806A)
73
+
74
+ @classmethod
75
+ def brand_green(cls) -> "Color":
76
+ return cls(0x57F287)
77
+
78
+ @classmethod
79
+ def dark_green(cls) -> "Color":
80
+ return cls(0x206694)
81
+
82
+ @classmethod
83
+ def orange(cls) -> "Color":
84
+ return cls(0xE67E22)
85
+
86
+ @classmethod
87
+ def dark_orange(cls) -> "Color":
88
+ return cls(0xA84300)
89
+
90
+ @classmethod
91
+ def brand_red(cls) -> "Color":
92
+ return cls(0xED4245)
93
+
94
+ @classmethod
95
+ def dark_red(cls) -> "Color":
96
+ return cls(0x992D22)
97
+
98
+ @classmethod
99
+ def magenta(cls) -> "Color":
100
+ return cls(0xE91E63)
101
+
102
+ @classmethod
103
+ def dark_magenta(cls) -> "Color":
104
+ return cls(0xAD1457)
105
+
106
+ @classmethod
107
+ def purple(cls) -> "Color":
108
+ return cls(0x9B59B6)
109
+
110
+ @classmethod
111
+ def dark_purple(cls) -> "Color":
112
+ return cls(0x71368A)
113
+
114
+ @classmethod
115
+ def yellow(cls) -> "Color":
116
+ return cls(0xF1C40F)
117
+
118
+ @classmethod
119
+ def dark_gold(cls) -> "Color":
120
+ return cls(0xC27C0E)
121
+
122
+ @classmethod
123
+ def light_gray(cls) -> "Color":
124
+ return cls(0x99AAB5)
125
+
126
+ @classmethod
127
+ def dark_gray(cls) -> "Color":
128
+ return cls(0x2C2F33)
129
+
130
+ @classmethod
131
+ def lighter_gray(cls) -> "Color":
132
+ return cls(0xBFBFBF)
133
+
134
+ @classmethod
135
+ def darker_gray(cls) -> "Color":
136
+ return cls(0x23272A)
137
+
138
+ @classmethod
139
+ def black(cls) -> "Color":
140
+ return cls(0x000000)
141
+
142
+ @classmethod
143
+ def white(cls) -> "Color":
144
+ return cls(0xFFFFFF)
145
+
49
146
  def to_rgb(self) -> tuple[int, int, int]:
50
147
  return ((self.value >> 16) & 0xFF, (self.value >> 8) & 0xFF, self.value & 0xFF)
51
148
 
52
149
  @classmethod
53
- def parse(cls, value: "Color | int | str | tuple[int, int, int] | None") -> "Color | None":
150
+ def parse(
151
+ cls, value: "Color | int | str | tuple[int, int, int] | None"
152
+ ) -> "Color | None":
54
153
  """Convert ``value`` to a :class:`Color` instance.
55
154
 
56
155
  Parameters
disagreement/enums.py CHANGED
@@ -278,6 +278,62 @@ class GuildFeature(str, Enum): # Changed from IntEnum to Enum
278
278
  return str(value)
279
279
 
280
280
 
281
+ # --- Guild Scheduled Event Enums ---
282
+
283
+
284
+ class GuildScheduledEventPrivacyLevel(IntEnum):
285
+ """Privacy level for a scheduled event."""
286
+
287
+ GUILD_ONLY = 2
288
+
289
+
290
+ class GuildScheduledEventStatus(IntEnum):
291
+ """Status of a scheduled event."""
292
+
293
+ SCHEDULED = 1
294
+ ACTIVE = 2
295
+ COMPLETED = 3
296
+ CANCELED = 4
297
+
298
+
299
+ class GuildScheduledEventEntityType(IntEnum):
300
+ """Entity type for a scheduled event."""
301
+
302
+ STAGE_INSTANCE = 1
303
+ VOICE = 2
304
+ EXTERNAL = 3
305
+
306
+
307
+ class VoiceRegion(str, Enum):
308
+ """Voice region identifier."""
309
+
310
+ AMSTERDAM = "amsterdam"
311
+ BRAZIL = "brazil"
312
+ DUBAI = "dubai"
313
+ EU_CENTRAL = "eu-central"
314
+ EU_WEST = "eu-west"
315
+ EUROPE = "europe"
316
+ FRANKFURT = "frankfurt"
317
+ HONGKONG = "hongkong"
318
+ INDIA = "india"
319
+ JAPAN = "japan"
320
+ RUSSIA = "russia"
321
+ SINGAPORE = "singapore"
322
+ SOUTHAFRICA = "southafrica"
323
+ SOUTH_KOREA = "south-korea"
324
+ SYDNEY = "sydney"
325
+ US_CENTRAL = "us-central"
326
+ US_EAST = "us-east"
327
+ US_SOUTH = "us-south"
328
+ US_WEST = "us-west"
329
+ VIP_US_EAST = "vip-us-east"
330
+ VIP_US_WEST = "vip-us-west"
331
+
332
+ @classmethod
333
+ def _missing_(cls, value): # type: ignore
334
+ return str(value)
335
+
336
+
281
337
  # --- Channel Enums ---
282
338
 
283
339
 
@@ -305,6 +361,13 @@ class ChannelType(IntEnum):
305
361
  GUILD_MEDIA = 16 # (Still in development) a channel that can only contain media
306
362
 
307
363
 
364
+ class StageInstancePrivacyLevel(IntEnum):
365
+ """Privacy level of a stage instance."""
366
+
367
+ PUBLIC = 1
368
+ GUILD_ONLY = 2
369
+
370
+
308
371
  class OverwriteType(IntEnum):
309
372
  """Type of target for a permission overwrite."""
310
373
 
@@ -76,7 +76,7 @@ class EventDispatcher:
76
76
  """Parses MESSAGE_DELETE and updates message cache."""
77
77
  message_id = data.get("id")
78
78
  if message_id:
79
- self._client._messages.pop(message_id, None)
79
+ self._client._messages.invalidate(message_id)
80
80
  return data
81
81
 
82
82
  def _parse_message_reaction_raw(self, data: Dict[str, Any]) -> Dict[str, Any]:
@@ -124,7 +124,7 @@ class EventDispatcher:
124
124
  """Parses GUILD_MEMBER_ADD into a Member object."""
125
125
 
126
126
  guild_id = str(data.get("guild_id"))
127
- return self._client.parse_member(data, guild_id)
127
+ return self._client.parse_member(data, guild_id, just_joined=True)
128
128
 
129
129
  def _parse_guild_member_remove(self, data: Dict[str, Any]):
130
130
  """Parses GUILD_MEMBER_REMOVE into a GuildMemberRemove model."""
@@ -202,8 +202,6 @@ class MessageCommand(AppCommand):
202
202
  super().__init__(callback, type=ApplicationCommandType.MESSAGE, **kwargs)
203
203
 
204
204
 
205
-
206
-
207
205
  class AppCommandGroup:
208
206
  """
209
207
  Represents a group of application commands (subcommands or subcommand groups).
@@ -1,7 +1,9 @@
1
1
  # disagreement/ext/app_commands/handler.py
2
2
 
3
3
  import inspect
4
+ import json
4
5
  import logging
6
+ import os
5
7
  from typing import (
6
8
  TYPE_CHECKING,
7
9
  Dict,
@@ -67,6 +69,8 @@ if not TYPE_CHECKING:
67
69
 
68
70
  logger = logging.getLogger(__name__)
69
71
 
72
+ COMMANDS_CACHE_FILE = ".disagreement_commands.json"
73
+
70
74
 
71
75
  class AppCommandHandler:
72
76
  """
@@ -84,6 +88,33 @@ class AppCommandHandler:
84
88
  self._app_command_groups: Dict[str, AppCommandGroup] = {}
85
89
  self._converter_registry: Dict[type, type] = {}
86
90
 
91
+ def _load_cached_ids(self) -> Dict[str, Dict[str, str]]:
92
+ try:
93
+ with open(COMMANDS_CACHE_FILE, "r", encoding="utf-8") as fp:
94
+ return json.load(fp)
95
+ except FileNotFoundError:
96
+ return {}
97
+ except json.JSONDecodeError:
98
+ logger.warning("Invalid command cache file. Ignoring.")
99
+ return {}
100
+
101
+ def _save_cached_ids(self, data: Dict[str, Dict[str, str]]) -> None:
102
+ try:
103
+ with open(COMMANDS_CACHE_FILE, "w", encoding="utf-8") as fp:
104
+ json.dump(data, fp, indent=2)
105
+ except Exception as e: # pragma: no cover - logging only
106
+ logger.error("Failed to write command cache: %s", e)
107
+
108
+ def clear_stored_registrations(self) -> None:
109
+ """Remove persisted command registration data."""
110
+ if os.path.exists(COMMANDS_CACHE_FILE):
111
+ os.remove(COMMANDS_CACHE_FILE)
112
+
113
+ def migrate_stored_registrations(self, new_path: str) -> None:
114
+ """Move stored registrations to ``new_path``."""
115
+ if os.path.exists(COMMANDS_CACHE_FILE):
116
+ os.replace(COMMANDS_CACHE_FILE, new_path)
117
+
87
118
  def add_command(self, command: Union["AppCommand", "AppCommandGroup"]) -> None:
88
119
  """Adds an application command or a command group to the handler."""
89
120
  if isinstance(command, AppCommandGroup):
@@ -564,11 +595,13 @@ class AppCommandHandler:
564
595
  Synchronizes (registers/updates) all application commands with Discord.
565
596
  If guild_id is provided, syncs commands for that guild. Otherwise, syncs global commands.
566
597
  """
567
- commands_to_sync: List[Dict[str, Any]] = []
598
+ cache = self._load_cached_ids()
599
+ scope_key = str(guild_id) if guild_id else "global"
600
+ stored = cache.get(scope_key, {})
568
601
 
569
- # Collect commands based on scope (global or specific guild)
570
- # This needs to be more sophisticated to handle guild_ids on commands/groups
602
+ current_payloads: Dict[str, Dict[str, Any]] = {}
571
603
 
604
+ # Collect commands based on scope (global or specific guild)
572
605
  source_commands = (
573
606
  list(self._slash_commands.values())
574
607
  + list(self._user_commands.values())
@@ -577,26 +610,22 @@ class AppCommandHandler:
577
610
  )
578
611
 
579
612
  for cmd_or_group in source_commands:
580
- # Determine if this command/group should be synced for the current scope
581
613
  is_guild_specific_command = (
582
614
  cmd_or_group.guild_ids is not None and len(cmd_or_group.guild_ids) > 0
583
615
  )
584
616
 
585
- if guild_id: # Syncing for a specific guild
586
- # Skip if not a guild-specific command OR if it's for a different guild
617
+ if guild_id:
587
618
  if not is_guild_specific_command or (
588
619
  cmd_or_group.guild_ids is not None
589
620
  and guild_id not in cmd_or_group.guild_ids
590
621
  ):
591
622
  continue
592
- else: # Syncing global commands
623
+ else:
593
624
  if is_guild_specific_command:
594
- continue # Skip guild-specific commands when syncing global
625
+ continue
595
626
 
596
- # Use the to_dict() method from AppCommand or AppCommandGroup
597
627
  try:
598
- payload = cmd_or_group.to_dict()
599
- commands_to_sync.append(payload)
628
+ current_payloads[cmd_or_group.name] = cmd_or_group.to_dict()
600
629
  except AttributeError:
601
630
  logger.warning(
602
631
  "Command or group '%s' does not have a to_dict() method. Skipping.",
@@ -609,32 +638,74 @@ class AppCommandHandler:
609
638
  e,
610
639
  )
611
640
 
612
- if not commands_to_sync:
641
+ if not current_payloads:
613
642
  logger.info(
614
643
  "No commands to sync for %s scope.",
615
644
  f"guild {guild_id}" if guild_id else "global",
616
645
  )
617
646
  return
618
647
 
648
+ names_current = set(current_payloads)
649
+ names_stored = set(stored)
650
+
651
+ to_delete = names_stored - names_current
652
+ to_create = names_current - names_stored
653
+ to_update = names_current & names_stored
654
+
655
+ if not to_delete and not to_create and not to_update:
656
+ logger.info(
657
+ "Application commands already up to date for %s scope.", scope_key
658
+ )
659
+ return
660
+
619
661
  try:
620
- if guild_id:
621
- logger.info(
622
- "Syncing %s commands for guild %s...",
623
- len(commands_to_sync),
624
- guild_id,
625
- )
626
- await self.client._http.bulk_overwrite_guild_application_commands(
627
- application_id, guild_id, commands_to_sync
628
- )
629
- else:
630
- logger.info(
631
- "Syncing %s global commands...",
632
- len(commands_to_sync),
633
- )
634
- await self.client._http.bulk_overwrite_global_application_commands(
635
- application_id, commands_to_sync
636
- )
662
+ for name in to_delete:
663
+ cmd_id = stored[name]
664
+ if guild_id:
665
+ await self.client._http.delete_guild_application_command(
666
+ application_id, guild_id, cmd_id
667
+ )
668
+ else:
669
+ await self.client._http.delete_global_application_command(
670
+ application_id, cmd_id
671
+ )
672
+
673
+ new_ids: Dict[str, str] = {}
674
+ for name in to_create:
675
+ payload = current_payloads[name]
676
+ if guild_id:
677
+ result = await self.client._http.create_guild_application_command(
678
+ application_id, guild_id, payload
679
+ )
680
+ else:
681
+ result = await self.client._http.create_global_application_command(
682
+ application_id, payload
683
+ )
684
+ if result.id:
685
+ new_ids[name] = str(result.id)
686
+
687
+ for name in to_update:
688
+ payload = current_payloads[name]
689
+ cmd_id = stored[name]
690
+ if guild_id:
691
+ await self.client._http.edit_guild_application_command(
692
+ application_id, guild_id, cmd_id, payload
693
+ )
694
+ else:
695
+ await self.client._http.edit_global_application_command(
696
+ application_id, cmd_id, payload
697
+ )
698
+ new_ids[name] = cmd_id
699
+
700
+ final_ids: Dict[str, str] = {}
701
+ for name in names_current:
702
+ if name in new_ids:
703
+ final_ids[name] = new_ids[name]
704
+ else:
705
+ final_ids[name] = stored[name]
706
+
707
+ cache[scope_key] = final_ids
708
+ self._save_cached_ids(cache)
637
709
  logger.info("Command sync successful.")
638
710
  except Exception as e:
639
711
  logger.error("Error syncing application commands: %s", e)
640
- # Consider re-raising or specific error handling
@@ -1,57 +1,65 @@
1
- # disagreement/ext/commands/__init__.py
2
-
3
- """
4
- disagreement.ext.commands - A command framework extension for the Disagreement library.
5
- """
6
-
7
- from .cog import Cog
8
- from .core import (
9
- Command,
10
- CommandContext,
11
- CommandHandler,
12
- ) # CommandHandler might be internal
13
- from .decorators import (
14
- command,
15
- listener,
16
- check,
17
- check_any,
18
- cooldown,
19
- requires_permissions,
20
- )
21
- from .errors import (
22
- CommandError,
23
- CommandNotFound,
24
- BadArgument,
25
- MissingRequiredArgument,
26
- ArgumentParsingError,
27
- CheckFailure,
28
- CheckAnyFailure,
29
- CommandOnCooldown,
30
- CommandInvokeError,
31
- )
32
-
33
- __all__ = [
34
- # Cog
35
- "Cog",
36
- # Core
37
- "Command",
38
- "CommandContext",
39
- # "CommandHandler", # Usually not part of public API for direct use by bot devs
40
- # Decorators
41
- "command",
42
- "listener",
43
- "check",
44
- "check_any",
45
- "cooldown",
46
- "requires_permissions",
47
- # Errors
48
- "CommandError",
49
- "CommandNotFound",
50
- "BadArgument",
51
- "MissingRequiredArgument",
52
- "ArgumentParsingError",
53
- "CheckFailure",
54
- "CheckAnyFailure",
55
- "CommandOnCooldown",
56
- "CommandInvokeError",
57
- ]
1
+ # disagreement/ext/commands/__init__.py
2
+
3
+ """
4
+ disagreement.ext.commands - A command framework extension for the Disagreement library.
5
+ """
6
+
7
+ from .cog import Cog
8
+ from .core import (
9
+ Command,
10
+ CommandContext,
11
+ CommandHandler,
12
+ ) # CommandHandler might be internal
13
+ from .decorators import (
14
+ command,
15
+ listener,
16
+ check,
17
+ check_any,
18
+ cooldown,
19
+ max_concurrency,
20
+ requires_permissions,
21
+ has_role,
22
+ has_any_role,
23
+ )
24
+ from .errors import (
25
+ CommandError,
26
+ CommandNotFound,
27
+ BadArgument,
28
+ MissingRequiredArgument,
29
+ ArgumentParsingError,
30
+ CheckFailure,
31
+ CheckAnyFailure,
32
+ CommandOnCooldown,
33
+ CommandInvokeError,
34
+ MaxConcurrencyReached,
35
+ )
36
+
37
+ __all__ = [
38
+ # Cog
39
+ "Cog",
40
+ # Core
41
+ "Command",
42
+ "CommandContext",
43
+ # "CommandHandler", # Usually not part of public API for direct use by bot devs
44
+ # Decorators
45
+ "command",
46
+ "listener",
47
+ "check",
48
+ "check_any",
49
+ "cooldown",
50
+ "max_concurrency",
51
+ "requires_permissions",
52
+ "has_role",
53
+ "has_any_role",
54
+ # Errors
55
+ "CommandError",
56
+ "CommandNotFound",
57
+ "BadArgument",
58
+ "MissingRequiredArgument",
59
+ "ArgumentParsingError",
60
+ "CheckFailure",
61
+ "CheckAnyFailure",
62
+ "CommandOnCooldown",
63
+ "CommandInvokeError",
64
+ "MaxConcurrencyReached",
65
+ ]