disagreement 0.2.0rc1__py3-none-any.whl → 0.4.0__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.
Files changed (38) hide show
  1. disagreement/__init__.py +2 -4
  2. disagreement/audio.py +42 -5
  3. disagreement/cache.py +43 -4
  4. disagreement/caching.py +121 -0
  5. disagreement/client.py +1682 -1535
  6. disagreement/enums.py +10 -3
  7. disagreement/error_handler.py +5 -1
  8. disagreement/errors.py +1341 -3
  9. disagreement/event_dispatcher.py +3 -5
  10. disagreement/ext/__init__.py +1 -0
  11. disagreement/ext/app_commands/__init__.py +0 -2
  12. disagreement/ext/app_commands/commands.py +0 -2
  13. disagreement/ext/app_commands/context.py +0 -2
  14. disagreement/ext/app_commands/converters.py +2 -4
  15. disagreement/ext/app_commands/decorators.py +5 -7
  16. disagreement/ext/app_commands/handler.py +1 -3
  17. disagreement/ext/app_commands/hybrid.py +0 -2
  18. disagreement/ext/commands/__init__.py +63 -61
  19. disagreement/ext/commands/cog.py +0 -2
  20. disagreement/ext/commands/converters.py +16 -5
  21. disagreement/ext/commands/core.py +728 -563
  22. disagreement/ext/commands/decorators.py +294 -219
  23. disagreement/ext/commands/errors.py +0 -2
  24. disagreement/ext/commands/help.py +0 -2
  25. disagreement/ext/commands/view.py +1 -3
  26. disagreement/gateway.py +632 -586
  27. disagreement/http.py +1362 -1041
  28. disagreement/interactions.py +0 -2
  29. disagreement/models.py +2682 -2263
  30. disagreement/shard_manager.py +0 -2
  31. disagreement/ui/view.py +167 -165
  32. disagreement/voice_client.py +263 -162
  33. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/METADATA +33 -6
  34. disagreement-0.4.0.dist-info/RECORD +55 -0
  35. disagreement-0.2.0rc1.dist-info/RECORD +0 -54
  36. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/WHEEL +0 -0
  37. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/licenses/LICENSE +0 -0
  38. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/top_level.txt +0 -0
disagreement/__init__.py CHANGED
@@ -1,5 +1,3 @@
1
- # disagreement/__init__.py
2
-
3
1
  """
4
2
  Disagreement
5
3
  ~~~~~~~~~~~~
@@ -14,7 +12,7 @@ __title__ = "disagreement"
14
12
  __author__ = "Slipstream"
15
13
  __license__ = "BSD 3-Clause License"
16
14
  __copyright__ = "Copyright 2025 Slipstream"
17
- __version__ = "0.2.0rc1"
15
+ __version__ = "0.4.0"
18
16
 
19
17
  from .client import Client, AutoShardedClient
20
18
  from .models import Message, User, Reaction, AuditLogEntry
@@ -31,7 +29,7 @@ from .errors import (
31
29
  )
32
30
  from .color import Color
33
31
  from .utils import utcnow, message_pager
34
- from .enums import GatewayIntent, GatewayOpcode # Export enums
32
+ from .enums import GatewayIntent, GatewayOpcode
35
33
  from .error_handler import setup_global_error_handler
36
34
  from .hybrid_context import HybridContext
37
35
  from .ext import tasks
disagreement/audio.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import contextlib
7
7
  import io
8
+ import shlex
8
9
  from typing import Optional, Union
9
10
 
10
11
 
@@ -35,15 +36,27 @@ class FFmpegAudioSource(AudioSource):
35
36
  A filename, URL, or file-like object to read from.
36
37
  """
37
38
 
38
- def __init__(self, source: Union[str, io.BufferedIOBase]):
39
+ def __init__(
40
+ self,
41
+ source: Union[str, io.BufferedIOBase],
42
+ *,
43
+ before_options: Optional[str] = None,
44
+ options: Optional[str] = None,
45
+ volume: float = 1.0,
46
+ ):
39
47
  self.source = source
48
+ self.before_options = before_options
49
+ self.options = options
50
+ self.volume = volume
40
51
  self.process: Optional[asyncio.subprocess.Process] = None
41
52
  self._feeder: Optional[asyncio.Task] = None
42
53
 
43
54
  async def _spawn(self) -> None:
44
55
  if isinstance(self.source, str):
45
- args = [
46
- "ffmpeg",
56
+ args = ["ffmpeg"]
57
+ if self.before_options:
58
+ args += shlex.split(self.before_options)
59
+ args += [
47
60
  "-i",
48
61
  self.source,
49
62
  "-f",
@@ -54,14 +67,18 @@ class FFmpegAudioSource(AudioSource):
54
67
  "2",
55
68
  "pipe:1",
56
69
  ]
70
+ if self.options:
71
+ args += shlex.split(self.options)
57
72
  self.process = await asyncio.create_subprocess_exec(
58
73
  *args,
59
74
  stdout=asyncio.subprocess.PIPE,
60
75
  stderr=asyncio.subprocess.DEVNULL,
61
76
  )
62
77
  else:
63
- args = [
64
- "ffmpeg",
78
+ args = ["ffmpeg"]
79
+ if self.before_options:
80
+ args += shlex.split(self.before_options)
81
+ args += [
65
82
  "-i",
66
83
  "pipe:0",
67
84
  "-f",
@@ -72,6 +89,8 @@ class FFmpegAudioSource(AudioSource):
72
89
  "2",
73
90
  "pipe:1",
74
91
  ]
92
+ if self.options:
93
+ args += shlex.split(self.options)
75
94
  self.process = await asyncio.create_subprocess_exec(
76
95
  *args,
77
96
  stdin=asyncio.subprocess.PIPE,
@@ -114,3 +133,21 @@ class FFmpegAudioSource(AudioSource):
114
133
  if isinstance(self.source, io.IOBase):
115
134
  with contextlib.suppress(Exception):
116
135
  self.source.close()
136
+
137
+
138
+ class AudioSink:
139
+ """Abstract base class for audio sinks."""
140
+
141
+ def write(self, user, data):
142
+ """Write a chunk of PCM audio.
143
+
144
+ Subclasses must implement this. The data is raw PCM at 48kHz
145
+ stereo.
146
+ """
147
+
148
+ raise NotImplementedError
149
+
150
+ def close(self) -> None:
151
+ """Cleanup the sink when the voice client disconnects."""
152
+
153
+ return None
disagreement/cache.py CHANGED
@@ -2,23 +2,32 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from typing import TYPE_CHECKING, Dict, Generic, Optional, TypeVar
5
+ from collections import OrderedDict
5
6
 
6
7
  if TYPE_CHECKING:
7
- from .models import Channel, Guild
8
+ from .models import Channel, Guild, Member
9
+ from .caching import MemberCacheFlags
8
10
 
9
11
  T = TypeVar("T")
10
12
 
11
13
 
12
14
  class Cache(Generic[T]):
13
- """Simple in-memory cache with optional TTL support."""
15
+ """Simple in-memory cache with optional TTL and max size support."""
14
16
 
15
- def __init__(self, ttl: Optional[float] = None) -> None:
17
+ def __init__(
18
+ self, ttl: Optional[float] = None, maxlen: Optional[int] = None
19
+ ) -> None:
16
20
  self.ttl = ttl
17
- self._data: Dict[str, tuple[T, Optional[float]]] = {}
21
+ self.maxlen = maxlen
22
+ self._data: "OrderedDict[str, tuple[T, Optional[float]]]" = OrderedDict()
18
23
 
19
24
  def set(self, key: str, value: T) -> None:
20
25
  expiry = time.monotonic() + self.ttl if self.ttl is not None else None
26
+ if key in self._data:
27
+ self._data.move_to_end(key)
21
28
  self._data[key] = (value, expiry)
29
+ if self.maxlen is not None and len(self._data) > self.maxlen:
30
+ self._data.popitem(last=False)
22
31
 
23
32
  def get(self, key: str) -> Optional[T]:
24
33
  item = self._data.get(key)
@@ -28,6 +37,7 @@ class Cache(Generic[T]):
28
37
  if expiry is not None and expiry < time.monotonic():
29
38
  self.invalidate(key)
30
39
  return None
40
+ self._data.move_to_end(key)
31
41
  return value
32
42
 
33
43
  def invalidate(self, key: str) -> None:
@@ -53,3 +63,32 @@ class GuildCache(Cache["Guild"]):
53
63
 
54
64
  class ChannelCache(Cache["Channel"]):
55
65
  """Cache specifically for :class:`Channel` objects."""
66
+
67
+
68
+ class MemberCache(Cache["Member"]):
69
+ """
70
+ A cache for :class:`Member` objects that respects :class:`MemberCacheFlags`.
71
+ """
72
+
73
+ def __init__(self, flags: MemberCacheFlags, ttl: Optional[float] = None) -> None:
74
+ super().__init__(ttl)
75
+ self.flags = flags
76
+
77
+ def _should_cache(self, member: Member) -> bool:
78
+ """Determines if a member should be cached based on the flags."""
79
+ if self.flags.all:
80
+ return True
81
+ if self.flags.none:
82
+ return False
83
+
84
+ if self.flags.online and member.status != "offline":
85
+ return True
86
+ if self.flags.voice and member.voice_state is not None:
87
+ return True
88
+ if self.flags.joined and getattr(member, "_just_joined", False):
89
+ return True
90
+ return False
91
+
92
+ def set(self, key: str, value: Member) -> None:
93
+ if self._should_cache(value):
94
+ super().set(key, value)
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import operator
4
+ from typing import Any, Callable, ClassVar, Dict, Iterator, Tuple
5
+
6
+
7
+ class _MemberCacheFlagValue:
8
+ flag: int
9
+
10
+ def __init__(self, func: Callable[[Any], bool]):
11
+ self.flag = getattr(func, "flag", 0)
12
+ self.__doc__ = func.__doc__
13
+
14
+ def __get__(self, instance: "MemberCacheFlags", owner: type) -> Any:
15
+ if instance is None:
16
+ return self
17
+ return instance.value & self.flag != 0
18
+
19
+ def __set__(self, instance: Any, value: bool) -> None:
20
+ if value:
21
+ instance.value |= self.flag
22
+ else:
23
+ instance.value &= ~self.flag
24
+
25
+ def __repr__(self) -> str:
26
+ return f"<{self.__class__.__name__} flag={self.flag}>"
27
+
28
+
29
+ def flag_value(flag: int) -> Callable[[Callable[[Any], bool]], _MemberCacheFlagValue]:
30
+ def decorator(func: Callable[[Any], bool]) -> _MemberCacheFlagValue:
31
+ setattr(func, "flag", flag)
32
+ return _MemberCacheFlagValue(func)
33
+
34
+ return decorator
35
+
36
+
37
+ class MemberCacheFlags:
38
+ __slots__ = ("value",)
39
+
40
+ VALID_FLAGS: ClassVar[Dict[str, int]] = {
41
+ "joined": 1 << 0,
42
+ "voice": 1 << 1,
43
+ "online": 1 << 2,
44
+ }
45
+ DEFAULT_FLAGS: ClassVar[int] = 1 | 2 | 4
46
+ ALL_FLAGS: ClassVar[int] = sum(VALID_FLAGS.values())
47
+
48
+ def __init__(self, **kwargs: bool):
49
+ self.value = self.DEFAULT_FLAGS
50
+ for key, value in kwargs.items():
51
+ if key not in self.VALID_FLAGS:
52
+ raise TypeError(f"{key!r} is not a valid member cache flag.")
53
+ setattr(self, key, value)
54
+
55
+ @classmethod
56
+ def _from_value(cls, value: int) -> MemberCacheFlags:
57
+ self = cls.__new__(cls)
58
+ self.value = value
59
+ return self
60
+
61
+ def __eq__(self, other: object) -> bool:
62
+ return isinstance(other, MemberCacheFlags) and self.value == other.value
63
+
64
+ def __ne__(self, other: object) -> bool:
65
+ return not self.__eq__(other)
66
+
67
+ def __hash__(self) -> int:
68
+ return hash(self.value)
69
+
70
+ def __repr__(self) -> str:
71
+ return f"<MemberCacheFlags value={self.value}>"
72
+
73
+ def __iter__(self) -> Iterator[Tuple[str, bool]]:
74
+ for name in self.VALID_FLAGS:
75
+ yield name, getattr(self, name)
76
+
77
+ def __int__(self) -> int:
78
+ return self.value
79
+
80
+ def __index__(self) -> int:
81
+ return self.value
82
+
83
+ @classmethod
84
+ def all(cls) -> MemberCacheFlags:
85
+ """A factory method that creates a :class:`MemberCacheFlags` with all flags enabled."""
86
+ return cls._from_value(cls.ALL_FLAGS)
87
+
88
+ @classmethod
89
+ def none(cls) -> MemberCacheFlags:
90
+ """A factory method that creates a :class:`MemberCacheFlags` with all flags disabled."""
91
+ return cls._from_value(0)
92
+
93
+ @classmethod
94
+ def only_joined(cls) -> MemberCacheFlags:
95
+ """A factory method that creates a :class:`MemberCacheFlags` with only the `joined` flag enabled."""
96
+ return cls._from_value(cls.VALID_FLAGS["joined"])
97
+
98
+ @classmethod
99
+ def only_voice(cls) -> MemberCacheFlags:
100
+ """A factory method that creates a :class:`MemberCacheFlags` with only the `voice` flag enabled."""
101
+ return cls._from_value(cls.VALID_FLAGS["voice"])
102
+
103
+ @classmethod
104
+ def only_online(cls) -> MemberCacheFlags:
105
+ """A factory method that creates a :class:`MemberCacheFlags` with only the `online` flag enabled."""
106
+ return cls._from_value(cls.VALID_FLAGS["online"])
107
+
108
+ @flag_value(1 << 0)
109
+ def joined(self) -> bool:
110
+ """Whether to cache members that have just joined the guild."""
111
+ return False
112
+
113
+ @flag_value(1 << 1)
114
+ def voice(self) -> bool:
115
+ """Whether to cache members that are in a voice channel."""
116
+ return False
117
+
118
+ @flag_value(1 << 2)
119
+ def online(self) -> bool:
120
+ """Whether to cache members that are online."""
121
+ return False