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,243 @@
1
+ # disagreement/event_dispatcher.py
2
+
3
+ """
4
+ Event dispatcher for handling Discord Gateway events.
5
+ """
6
+
7
+ import asyncio
8
+ import inspect
9
+ from collections import defaultdict
10
+ from typing import (
11
+ Callable,
12
+ Coroutine,
13
+ Any,
14
+ Dict,
15
+ List,
16
+ Set,
17
+ TYPE_CHECKING,
18
+ Awaitable,
19
+ Optional,
20
+ )
21
+
22
+ from .models import Message, User # Assuming User might be part of other events
23
+ from .errors import DisagreementException
24
+
25
+ if TYPE_CHECKING:
26
+ from .client import Client # For type hinting to avoid circular imports
27
+ from .interactions import Interaction
28
+
29
+ # Type alias for an event listener
30
+ EventListener = Callable[..., Awaitable[None]]
31
+
32
+
33
+ class EventDispatcher:
34
+ """
35
+ Manages registration and dispatching of event listeners.
36
+ """
37
+
38
+ def __init__(self, client_instance: "Client"):
39
+ self._client: "Client" = client_instance
40
+ self._listeners: Dict[str, List[EventListener]] = defaultdict(list)
41
+ self._waiters: Dict[
42
+ str, List[tuple[asyncio.Future, Optional[Callable[[Any], bool]]]]
43
+ ] = defaultdict(list)
44
+ self.on_dispatch_error: Optional[
45
+ Callable[[str, Exception, EventListener], Awaitable[None]]
46
+ ] = None
47
+ # Pre-defined parsers for specific event types to convert raw data to models
48
+ self._event_parsers: Dict[str, Callable[[Dict[str, Any]], Any]] = {
49
+ "MESSAGE_CREATE": self._parse_message_create,
50
+ "INTERACTION_CREATE": self._parse_interaction_create,
51
+ "GUILD_CREATE": self._parse_guild_create,
52
+ "CHANNEL_CREATE": self._parse_channel_create,
53
+ "PRESENCE_UPDATE": self._parse_presence_update,
54
+ "TYPING_START": self._parse_typing_start,
55
+ }
56
+
57
+ def _parse_message_create(self, data: Dict[str, Any]) -> Message:
58
+ """Parses raw MESSAGE_CREATE data into a Message object."""
59
+ return self._client.parse_message(data)
60
+
61
+ def _parse_interaction_create(self, data: Dict[str, Any]) -> "Interaction":
62
+ """Parses raw INTERACTION_CREATE data into an Interaction object."""
63
+ from .interactions import Interaction
64
+
65
+ return Interaction(data=data, client_instance=self._client)
66
+
67
+ def _parse_guild_create(self, data: Dict[str, Any]):
68
+ """Parses raw GUILD_CREATE data into a Guild object."""
69
+
70
+ return self._client.parse_guild(data)
71
+
72
+ def _parse_channel_create(self, data: Dict[str, Any]):
73
+ """Parses raw CHANNEL_CREATE data into a Channel object."""
74
+
75
+ return self._client.parse_channel(data)
76
+
77
+ def _parse_presence_update(self, data: Dict[str, Any]):
78
+ """Parses raw PRESENCE_UPDATE data into a PresenceUpdate object."""
79
+
80
+ from .models import PresenceUpdate
81
+
82
+ return PresenceUpdate(data, client_instance=self._client)
83
+
84
+ def _parse_typing_start(self, data: Dict[str, Any]):
85
+ """Parses raw TYPING_START data into a TypingStart object."""
86
+
87
+ from .models import TypingStart
88
+
89
+ return TypingStart(data, client_instance=self._client)
90
+
91
+ # Potentially add _parse_user for events that directly provide a full user object
92
+ # def _parse_user_update(self, data: Dict[str, Any]) -> User:
93
+ # return User(data=data)
94
+
95
+ def register(self, event_name: str, coro: EventListener):
96
+ """
97
+ Registers a coroutine function to listen for a specific event.
98
+
99
+ Args:
100
+ event_name (str): The name of the event (e.g., 'MESSAGE_CREATE').
101
+ coro (Callable): The coroutine function to call when the event occurs.
102
+ It should accept arguments appropriate for the event.
103
+
104
+ Raises:
105
+ TypeError: If the provided callback is not a coroutine function.
106
+ """
107
+ if not inspect.iscoroutinefunction(coro):
108
+ raise TypeError(
109
+ f"Event listener for '{event_name}' must be a coroutine function (async def)."
110
+ )
111
+
112
+ # Normalize event name, e.g., 'on_message' -> 'MESSAGE_CREATE'
113
+ # For now, we assume event_name is already the Discord event type string.
114
+ # If using decorators like @client.on_message, the decorator would handle this mapping.
115
+ self._listeners[event_name.upper()].append(coro)
116
+
117
+ def unregister(self, event_name: str, coro: EventListener):
118
+ """
119
+ Unregisters a coroutine function from an event.
120
+
121
+ Args:
122
+ event_name (str): The name of the event.
123
+ coro (Callable): The coroutine function to unregister.
124
+ """
125
+ event_name_upper = event_name.upper()
126
+ if event_name_upper in self._listeners:
127
+ try:
128
+ self._listeners[event_name_upper].remove(coro)
129
+ except ValueError:
130
+ pass # Listener not in list
131
+
132
+ def add_waiter(
133
+ self,
134
+ event_name: str,
135
+ future: asyncio.Future,
136
+ check: Optional[Callable[[Any], bool]] = None,
137
+ ) -> None:
138
+ self._waiters[event_name.upper()].append((future, check))
139
+
140
+ def remove_waiter(self, event_name: str, future: asyncio.Future) -> None:
141
+ waiters = self._waiters.get(event_name.upper())
142
+ if not waiters:
143
+ return
144
+ self._waiters[event_name.upper()] = [
145
+ (f, c) for f, c in waiters if f is not future
146
+ ]
147
+ if not self._waiters[event_name.upper()]:
148
+ self._waiters.pop(event_name.upper(), None)
149
+
150
+ def _resolve_waiters(self, event_name: str, data: Any) -> None:
151
+ waiters = self._waiters.get(event_name)
152
+ if not waiters:
153
+ return
154
+ to_remove: List[tuple[asyncio.Future, Optional[Callable[[Any], bool]]]] = []
155
+ for future, check in waiters:
156
+ if future.cancelled():
157
+ to_remove.append((future, check))
158
+ continue
159
+ try:
160
+ if check is None or check(data):
161
+ future.set_result(data)
162
+ to_remove.append((future, check))
163
+ except Exception as exc:
164
+ future.set_exception(exc)
165
+ to_remove.append((future, check))
166
+ for item in to_remove:
167
+ if item in waiters:
168
+ waiters.remove(item)
169
+ if not waiters:
170
+ self._waiters.pop(event_name, None)
171
+
172
+ async def dispatch(self, event_name: str, raw_data: Dict[str, Any]):
173
+ """
174
+ Dispatches an event to all registered listeners.
175
+
176
+ Args:
177
+ event_name (str): The name of the event (e.g., 'MESSAGE_CREATE').
178
+ raw_data (Dict[str, Any]): The raw data payload from the Discord Gateway for this event.
179
+ """
180
+ event_name_upper = event_name.upper()
181
+ listeners = self._listeners.get(event_name_upper)
182
+
183
+ if not listeners:
184
+ # print(f"No listeners for event {event_name_upper}")
185
+ return
186
+
187
+ parsed_data: Any = raw_data
188
+ if event_name_upper in self._event_parsers:
189
+ try:
190
+ parser = self._event_parsers[event_name_upper]
191
+ parsed_data = parser(raw_data)
192
+ except Exception as e:
193
+ print(f"Error parsing event data for {event_name_upper}: {e}")
194
+ # Optionally, dispatch with raw_data or raise, or log more formally
195
+ # For now, we'll proceed to dispatch with raw_data if parsing fails,
196
+ # or just log and return if parsed_data is critical.
197
+ # Let's assume if a parser exists, its output is critical.
198
+ return
199
+
200
+ self._resolve_waiters(event_name_upper, parsed_data)
201
+ # print(f"Dispatching event {event_name_upper} with data: {parsed_data} to {len(listeners)} listeners.")
202
+ for listener in listeners:
203
+ try:
204
+ # Inspect the listener to see how many arguments it expects
205
+ sig = inspect.signature(listener)
206
+ num_params = len(sig.parameters)
207
+
208
+ if num_params == 0: # Listener takes no arguments
209
+ await listener()
210
+ elif (
211
+ num_params == 1
212
+ ): # Listener takes one argument (the parsed data or model)
213
+ await listener(parsed_data)
214
+ # elif num_params == 2 and event_name_upper == "MESSAGE_CREATE": # Special case for (client, message)
215
+ # await listener(self._client, parsed_data) # This might be too specific here
216
+ else:
217
+ # Fallback or error if signature doesn't match expected patterns
218
+ # For now, assume one arg is the most common for parsed data.
219
+ # Or, if you want to be strict:
220
+ print(
221
+ f"Warning: Listener {listener.__name__} for {event_name_upper} has an unhandled number of parameters ({num_params}). Skipping or attempting with one arg."
222
+ )
223
+ if num_params > 0: # Try with one arg if it takes any
224
+ await listener(parsed_data)
225
+
226
+ except Exception as e:
227
+ callback = self.on_dispatch_error
228
+ if callback is not None:
229
+ try:
230
+ await callback(event_name_upper, e, listener)
231
+
232
+ except Exception as hook_error:
233
+ print(f"Error in on_dispatch_error hook itself: {hook_error}")
234
+ else:
235
+ # Default error handling if no hook is set
236
+ print(
237
+ f"Error in event listener {listener.__name__} for {event_name_upper}: {e}"
238
+ )
239
+ if hasattr(self._client, "on_error"):
240
+ try:
241
+ await self._client.on_error(event_name_upper, e, listener)
242
+ except Exception as client_err_e:
243
+ print(f"Error in client.on_error itself: {client_err_e}")