scurrypy 0.3.3__py3-none-any.whl → 0.3.4.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.

Potentially problematic release.


This version of scurrypy might be problematic. Click here for more details.

discord/client.py CHANGED
@@ -1,3 +1,5 @@
1
+ import asyncio
2
+
1
3
  from .config import BaseConfig
2
4
  from .intents import Intents
3
5
  from .error import DiscordError
@@ -29,6 +31,11 @@ class Client(ClientLike):
29
31
  prefix (str, optional): set message prefix if using command prefixes
30
32
  quiet (bool, optional): if INFO, DEBUG, and WARN should be logged
31
33
  """
34
+ if not token:
35
+ raise ValueError("Token is required")
36
+ if not application_id:
37
+ raise ValueError("Application ID is required")
38
+
32
39
  from .logger import Logger
33
40
  from .gateway import GatewayClient
34
41
  from .http import HTTPClient
@@ -82,30 +89,41 @@ class Client(ClientLike):
82
89
  return func
83
90
  return decorator
84
91
 
85
- def command(self, command: SlashCommand | MessageCommand | UserCommand, guild_id: int = 0):
86
- """Decorator registers a function for a command handler.
92
+ def command(self, command: SlashCommand | MessageCommand | UserCommand, guild_ids: list[int] | None = None):
93
+ """Decorator to register a function as a command handler.
87
94
 
88
95
  Args:
89
- command (SlashCommand | MessageCommand | UserCommand): command to register
90
- guild_id (int): ID of guild in which to register command (if a guild command)
96
+ command (SlashCommand | MessageCommand | UserCommand): The command to register.
97
+ guild_ids (list[int] | None): Guild IDs for guild-specific commands. None for global commands.
91
98
  """
92
99
  def decorator(func):
93
- # hash out command type
94
- if isinstance(command, MessageCommand):
95
- self.command_dispatcher.message_command(command.name, func)
96
- elif isinstance(command, UserCommand):
97
- self.command_dispatcher.user_command(command.name, func)
98
- elif isinstance(command, SlashCommand):
99
- self.command_dispatcher.command(command.name, func)
100
+ # Map command types to dispatcher registration functions
101
+ handler_map = {
102
+ MessageCommand: self.command_dispatcher.message_command,
103
+ UserCommand: self.command_dispatcher.user_command,
104
+ SlashCommand: self.command_dispatcher.command,
105
+ }
106
+
107
+ # Resolve dispatcher method based on command type
108
+ for cls, handler in handler_map.items():
109
+ if isinstance(command, cls):
110
+ handler(command.name, func)
111
+ break
100
112
  else:
101
- raise ValueError(f'Command {command.name} expected to be of type SlashCommand, UserCommand, MessageCommand; \
102
- got {type(command).__name__}.')
103
-
104
- # then hash out if this command should be guild or global level
105
- if guild_id != 0:
106
- self._guild_commands.setdefault(guild_id, []).append(command)
113
+ raise ValueError(
114
+ f"Command {getattr(command, 'name', '<unnamed>')} must be one of "
115
+ f"SlashCommand, UserCommand, MessageCommand; got {type(command).__name__}."
116
+ )
117
+
118
+ # Queue command for later registration
119
+ if guild_ids:
120
+ gids = [guild_ids] if isinstance(guild_ids, int) else guild_ids
121
+ for gid in gids:
122
+ self._guild_commands.setdefault(gid, []).append(command)
107
123
  else:
108
124
  self._global_commands.append(command)
125
+
126
+ return func # ensure original function is preserved
109
127
  return decorator
110
128
 
111
129
  def event(self, event_name: str):
@@ -137,7 +155,7 @@ class Client(ClientLike):
137
155
  """
138
156
  self._shutdown_hooks.append(func)
139
157
 
140
- def application_from_id(self, application_id: int):
158
+ def fetch_application(self, application_id: int):
141
159
  """Creates an interactable application resource.
142
160
 
143
161
  Args:
@@ -150,7 +168,7 @@ class Client(ClientLike):
150
168
 
151
169
  return Application(application_id, self._http)
152
170
 
153
- def guild_from_id(self, guild_id: int):
171
+ def fetch_guild(self, guild_id: int):
154
172
  """Creates an interactable guild resource.
155
173
 
156
174
  Args:
@@ -163,7 +181,7 @@ class Client(ClientLike):
163
181
 
164
182
  return Guild(guild_id, self._http)
165
183
 
166
- def channel_from_id(self, channel_id: int):
184
+ def fetch_channel(self, channel_id: int):
167
185
  """Creates an interactable channel resource.
168
186
 
169
187
  Args:
@@ -176,7 +194,7 @@ class Client(ClientLike):
176
194
 
177
195
  return Channel(channel_id, self._http)
178
196
 
179
- def message_from_id(self, channel_id: int, message_id: int):
197
+ def fetch_message(self, channel_id: int, message_id: int):
180
198
  """Creates an interactable message resource.
181
199
 
182
200
  Args:
@@ -190,7 +208,7 @@ class Client(ClientLike):
190
208
 
191
209
  return Message(message_id, channel_id, self._http)
192
210
 
193
- def user_from_id(self, user_id: int):
211
+ def fetch_user(self, user_id: int):
194
212
  """Creates an interactable user resource.
195
213
 
196
214
  Args:
@@ -213,13 +231,11 @@ class Client(ClientLike):
213
231
  self._logger.log_info(f"Guild {guild_id} already queued, skipping clear.")
214
232
  return
215
233
 
216
- await self.command_dispatcher._register_guild_commands({guild_id: []})
234
+ self._guild_commands[guild_id] = []
217
235
 
218
236
  async def _listen(self):
219
237
  """Main event loop for incoming gateway requests."""
220
- import asyncio
221
-
222
- while True:
238
+ while self._ws.is_connected():
223
239
  try:
224
240
  message = await self._ws.receive()
225
241
  if not message:
@@ -227,17 +243,17 @@ class Client(ClientLike):
227
243
 
228
244
  op_code = message.get('op')
229
245
 
230
- if op_code == 0:
231
- dispatch_type = message.get('t')
232
- self._logger.log_info(f"DISPATCH -> {dispatch_type}")
233
- event_data = message.get('d')
234
- self._ws.sequence = message.get('s') or self._ws.sequence
246
+ match op_code:
247
+ case 0:
248
+ dispatch_type = message.get('t')
249
+ self._logger.log_info(f"DISPATCH -> {dispatch_type}")
250
+ event_data = message.get('d')
251
+ self._ws.sequence = message.get('s') or self._ws.sequence
235
252
 
236
- if dispatch_type == "READY":
237
- self._ws.session_id = event_data.get("session_id")
238
- self._ws.connect_url = event_data.get("resume_gateway_url", self._ws.connect_url)
253
+ if dispatch_type == "READY":
254
+ self._ws.session_id = event_data.get("session_id")
255
+ self._ws.connect_url = event_data.get("resume_gateway_url", self._ws.connect_url)
239
256
 
240
- try:
241
257
  if self.prefix_dispatcher.prefix and dispatch_type == 'MESSAGE_CREATE':
242
258
  await self.prefix_dispatcher.dispatch(event_data)
243
259
 
@@ -245,107 +261,98 @@ class Client(ClientLike):
245
261
  await self.command_dispatcher.dispatch(event_data)
246
262
 
247
263
  await self.dispatcher.dispatch(dispatch_type, event_data)
248
- except DiscordError as e:
249
- if e.fatal:
250
- raise # let run() handle fatal errors
251
- else:
252
- self._logger.log_warn(f"Recoverable DiscordError: {e}")
253
- continue # keep listening
254
-
255
- elif op_code == 7:
256
- raise ConnectionError("Reconnect requested by server.")
257
- elif op_code == 9:
258
- self._ws.session_id = None
259
- self._ws.sequence = None
260
- raise ConnectionError("Invalid session.")
264
+ case 7:
265
+ raise ConnectionError("Reconnect requested by server.")
266
+ case 9:
267
+ self._ws.session_id = None
268
+ self._ws.sequence = None
269
+ raise ConnectionError("Invalid session.")
270
+ case 11:
271
+ self._logger.log_debug("Heartbeat ACK received")
272
+
261
273
  except asyncio.CancelledError:
262
- raise
274
+ break
263
275
  except DiscordError as e:
264
276
  if e.fatal:
265
- raise # propagate fatal errors
277
+ self._logger.log_error(f"Fatal DiscordError: {e}")
278
+ break
266
279
  else:
267
280
  self._logger.log_warn(f"Recoverable DiscordError: {e}")
268
281
  except ConnectionError as e:
269
282
  self._logger.log_warn(f"Connection lost: {e}")
270
- await self._ws.close()
271
- await asyncio.sleep(2)
283
+ raise
272
284
 
273
285
  async def start(self):
274
286
  """Runs the main lifecycle of the bot.
275
287
  Handles connection setup, heartbeat management, event loop, and automatic reconnects.
276
288
  """
277
- import asyncio
278
-
279
- while True:
280
- try:
281
- await self._http.start_session()
282
- await self._ws.connect()
283
- await self._ws.start_heartbeat()
289
+ try:
290
+ await self._http.start_session()
291
+ await self._ws.connect()
292
+ await self._ws.start_heartbeat()
284
293
 
294
+ while self._ws.is_connected():
285
295
  if self._ws.session_id and self._ws.sequence:
286
296
  await self._ws.reconnect()
287
297
  else:
288
298
  await self._ws.identify()
289
299
 
290
300
  if not self._is_set_up:
291
- if self._setup_hooks:
292
- for hook in self._setup_hooks:
293
- self._logger.log_info(f"Setting hook {hook.__name__}")
294
- await hook(self)
295
- self._logger.log_high_priority("Hooks set up.")
301
+ await self._startup()
302
+ self._is_set_up = True
296
303
 
297
- # register GUILD commands
298
- await self.command_dispatcher._register_guild_commands(self._guild_commands)
304
+ await self._listen()
305
+
306
+ # If we get here, connection was lost - reconnect
307
+ await self._ws.close()
308
+ await asyncio.sleep(5)
309
+ await self._ws.connect()
310
+ await self._ws.start_heartbeat()
299
311
 
300
- # register GLOBAL commands
301
- await self.command_dispatcher._register_global_commands(self._global_commands)
312
+ except asyncio.CancelledError:
313
+ self._logger.log_high_priority("Connection cancelled via KeyboardInterrupt.")
314
+ except Exception as e:
315
+ self._logger.log_error(f"{type(e).__name__} - {e}")
316
+ finally:
317
+ await self._close()
302
318
 
303
- self._is_set_up = True
319
+ async def _startup(self):
320
+ """Start up bot by registering user-defined hooks and commands."""
321
+ try:
322
+ if self._setup_hooks:
323
+ for hook in self._setup_hooks:
324
+ self._logger.log_info(f"Setting hook {hook.__name__}")
325
+ await hook(self)
326
+ self._logger.log_high_priority("Hooks set up.")
304
327
 
305
- await self._listen()
328
+ # register GUILD commands
329
+ await self.command_dispatcher._register_guild_commands(self._guild_commands)
306
330
 
307
- except ConnectionError as e:
308
- self._logger.log_warn("Connection lost. Attempting reconnect...")
309
- await self._ws.close()
310
- await asyncio.sleep(2)
311
- continue
312
- except asyncio.CancelledError:
313
- self._logger.log_info("Cancelling connection...")
314
- break
315
- except DiscordError as e:
316
- self._logger.log_error(f"Fatal DiscordError: {e}")
317
- break
318
- except Exception as e:
319
- self._logger.log_error(f"Unspecified Error Type {type(e).__name__} - {e}")
320
- break
321
- finally:
322
- # Run hooks (with safe catching)
323
- for hook in self._shutdown_hooks:
324
- try:
325
- self._logger.log_info(f"Executing shutdown hook {hook.__name__}")
326
- await hook(self)
327
- except Exception as e:
328
- self._logger.log_error(f"{type(e).__name__}: {e}")
329
-
330
- # Always close resources
331
- try:
332
- await self._ws.close()
333
- except Exception as e:
334
- self._logger.log_warn(f"WebSocket close failed: {e}")
335
-
336
- try:
337
- await self._http.close_session()
338
- except Exception as e:
339
- self._logger.log_warn(f"HTTP session close failed: {e}")
331
+ # register GLOBAL commands
332
+ await self.command_dispatcher._register_global_commands(self._global_commands)
333
+
334
+ self._logger.log_high_priority("Commands set up.")
335
+ except Exception:
336
+ raise
340
337
 
338
+ async def _close(self):
339
+ """Gracefully close HTTP and websocket connections."""
340
+ # Close HTTP first since it's more important
341
+ self._logger.log_debug("Closing HTTP session...")
342
+ await self._http.close_session()
341
343
 
344
+ # Then try websocket with short timeout
345
+ try:
346
+ self._logger.log_debug("Closing websocket connection...")
347
+ await asyncio.wait_for(self._ws.close(), timeout=1.0)
348
+ except:
349
+ pass # Don't care if websocket won't close
350
+
342
351
  def run(self):
343
352
  """Starts the bot.
344
353
  Handles starting the session, WS, and heartbeat, reconnection logic,
345
354
  setting up emojis and hooks, and then listens for gateway events.
346
- """
347
- import asyncio
348
-
355
+ """
349
356
  try:
350
357
  asyncio.run(self.start())
351
358
  except KeyboardInterrupt:
@@ -1,4 +1,4 @@
1
- from importlib import import_module
1
+ import importlib
2
2
  from ..client_like import ClientLike
3
3
 
4
4
  from ..resources.message import Message
@@ -9,8 +9,24 @@ class EventDispatcher:
9
9
  RESOURCE_MAP = { # maps discord events to their respective dataclass (lazy loading)
10
10
  'READY': ('discord.events.ready_event', 'ReadyEvent'),
11
11
 
12
- 'MESSAGE_CREATE': ('discord.events.message_events', 'MessageCreateEvent')
12
+ 'GUILD_CREATE': ('discord.events.guild_events', 'GuildCreateEvent'),
13
+ 'GUILD_UPDATE': ('discord.events.guild_events', 'GuildUpdateEvent'),
14
+ 'GUILD_DELETE': ('discord.events.guild_events', 'GuildDeleteEvent'),
15
+
16
+ 'CHANNEL_CREATE': ('discord.events.channel_events', 'GuildChannelCreateEvent'),
17
+ 'CHANNEL_UPDATE': ('discord.events.channel_events', 'GuildChannelUpdateEvent'),
18
+ 'CHANNEL_DELETE': ('discord.events.channel_events', 'GuildChannelDeleteEvent'),
19
+ 'CHANNEL_PINS_UPDATE': ('discord.events.channel_events', 'ChannelPinsUpdateEvent'),
20
+
21
+ 'MESSAGE_CREATE': ('discord.events.message_events', 'MessageCreateEvent'),
22
+ 'MESSAGE_UPDATE': ('discord.events.message_events', 'MessageUpdateEvent'),
23
+ 'MESSAGE_DELETE': ('discord.events.message_events', 'MessageDeleteEvent'),
13
24
 
25
+ 'MESSAGE_REACTION_ADD': ('discord.events.reaction_events', 'ReactionAddEvent'),
26
+ 'MESSAGE_REACTION_REMOVE': ('discord.events.reaction_events', 'ReactionRemoveEvent'),
27
+ 'MESSAGE_REACTION_REMOVE_ALL': ('discord.events.reaction_events', 'ReactionRemoveAllEvent'),
28
+ 'MESSAGE_REACTION_REMOVE_EMOJI': ('discord.events.reaction_events', 'ReactionRemoveEmojiEvent'),
29
+
14
30
  # and other events...
15
31
  }
16
32
 
@@ -59,7 +75,7 @@ class EventDispatcher:
59
75
 
60
76
  module_name, class_name = module_info
61
77
 
62
- module = import_module(module_name)
78
+ module = importlib.import_module(module_name)
63
79
  if not module:
64
80
  print(f"Cannot find module '{module_name}'!")
65
81
  return
@@ -1,8 +1,9 @@
1
1
  from dataclasses import dataclass
2
+ from ..model import DataModel
2
3
  from typing import Optional
3
4
 
4
5
  @dataclass
5
- class GuildChannelEvent:
6
+ class GuildChannelEvent(DataModel):
6
7
  """Base guild channel event."""
7
8
 
8
9
  id: int
@@ -48,5 +49,10 @@ class GuildChannelDeleteEvent(GuildChannelEvent):
48
49
  class ChannelPinsUpdateEvent:
49
50
  """Pin update event."""
50
51
  channel_id: int
52
+ """ID of channel where the pins were updated."""
53
+
51
54
  guild_id: Optional[int]
55
+ """ID of the guild where the pins were updated."""
56
+
52
57
  last_pin_timestamp: Optional[str] # ISO8601 timestamp of last pinned message
58
+ """ISO8601 formatted timestamp of the last pinned message in the channel."""
@@ -1,9 +1,11 @@
1
1
  from dataclasses import dataclass
2
+ from ..model import DataModel
3
+
2
4
  from ..models.member import MemberModel
3
5
  from ..resources.channel import Channel
4
6
 
5
7
  @dataclass
6
- class GuildEvent:
8
+ class GuildEvent(DataModel):
7
9
  """Base guild event."""
8
10
  joined_at: str
9
11
  """ISO8601 timestamp of when app joined the guild."""
@@ -1,11 +1,12 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Optional
3
+ from ..model import DataModel
3
4
 
4
5
  from ..resources.message import Message
5
6
  from ..models.member import MemberModel
6
7
 
7
8
  @dataclass
8
- class MessageCreateEvent:
9
+ class MessageCreateEvent(DataModel):
9
10
  """Received when a message is created."""
10
11
  message: Message
11
12
  """Message resource object. See [`Resource.Message`][discord.resources.message.Message]."""
@@ -17,7 +18,7 @@ class MessageCreateEvent:
17
18
  """Partial Member object of the author of the message. See [`MemberModel`][discord.models.member.MemberModel]."""
18
19
 
19
20
  @dataclass
20
- class MessageUpdateEvent:
21
+ class MessageUpdateEvent(DataModel):
21
22
  """Received when a message is updated."""
22
23
  message: Message
23
24
  """Message resource object. See [`Resource.Message`][discord.resources.message.Message]."""
@@ -29,7 +30,7 @@ class MessageUpdateEvent:
29
30
  """Partial Member object of the author of the message. See [`MemberModel`][discord.models.member.MemberModel]."""
30
31
 
31
32
  @dataclass
32
- class MessageDeleteEvent:
33
+ class MessageDeleteEvent(DataModel):
33
34
  """Received when a message is deleted."""
34
35
 
35
36
  id: int
@@ -1,5 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Optional
3
+ from ..model import DataModel
3
4
 
4
5
  from ..models.member import MemberModel
5
6
  from ..models.emoji import EmojiModel
@@ -14,7 +15,7 @@ class ReactionType:
14
15
  """A super emoji."""
15
16
 
16
17
  @dataclass
17
- class ReactionAddEvent:
18
+ class ReactionAddEvent(DataModel):
18
19
  """Reaction added event."""
19
20
 
20
21
  type: int
@@ -45,7 +46,7 @@ class ReactionAddEvent:
45
46
  """ID of the user who sent the message where the reaction was added."""
46
47
 
47
48
  @dataclass
48
- class ReactionRemoveEvent:
49
+ class ReactionRemoveEvent(DataModel):
49
50
  """Reaction removed event."""
50
51
 
51
52
  type: int
@@ -69,7 +70,7 @@ class ReactionRemoveEvent:
69
70
  burst: bool
70
71
  """If the emoji of the removed reaction is super."""
71
72
 
72
- class ReactionRemoveAllEvent:
73
+ class ReactionRemoveAllEvent(DataModel):
73
74
  """Remove all reactions event."""
74
75
 
75
76
  channel_id: int
@@ -82,7 +83,7 @@ class ReactionRemoveAllEvent:
82
83
  """ID of the guild where all reaction were removed (if in a guild)."""
83
84
 
84
85
  @dataclass
85
- class ReactionRemoveEmojiEvent:
86
+ class ReactionRemoveEmojiEvent(DataModel):
86
87
  """All reactions of a specific emoji removed."""
87
88
 
88
89
  emoji: EmojiModel
discord/gateway.py CHANGED
@@ -53,57 +53,46 @@ class GatewayClient:
53
53
 
54
54
  async def connect(self):
55
55
  """Established websocket connection to Discord."""
56
- try:
57
- self.ws = await websockets.connect(self.connect_url + self.url_params)
58
- self._logger.log_high_priority("Connected to Discord.")
59
- except Exception as e:
60
- self._logger.log_error(f"Websocket Connection Error {type(e).__name__} - {e}")
61
-
56
+ self.ws = await websockets.connect(self.connect_url + self.url_params)
57
+ self._logger.log_high_priority("Connected to Discord.")
58
+
62
59
  async def receive(self):
63
60
  """Receives and logs messages from the gateway.
64
61
 
65
62
  Returns:
66
63
  (dict): parsed JSON data
67
64
  """
68
- try:
69
- message = await self.ws.recv()
70
- if message:
71
- data: dict = json.loads(message)
72
- self._logger.log_debug(f"Received: {DISCORD_OP_CODES.get(data.get('op'))} - {json.dumps(data, indent=4)}")
73
- self._logger.log_info(f"Received: {DISCORD_OP_CODES.get(data.get('op'))}")
74
- return data
75
- else:
76
- return None
77
- except Exception as e:
78
- self._logger.log_error(f"Error on receive: {type(e).__name__} - {e}")
79
-
65
+
66
+ message = await asyncio.wait_for(self.ws.recv(), timeout=60)
67
+
68
+ if message:
69
+ data: dict = json.loads(message)
70
+ self._logger.log_debug(f"Received: {DISCORD_OP_CODES.get(data.get('op'))} - {json.dumps(data, indent=4)}")
71
+ self._logger.log_info(f"Received: {DISCORD_OP_CODES.get(data.get('op'))}")
72
+ return data
73
+
74
+ return None
75
+
80
76
  async def send(self, message: dict):
81
77
  """Sends a JSON-encoded message to the gateway.
82
78
 
83
79
  Args:
84
80
  message (dict): the message to send
85
81
  """
86
- try:
87
- await self.ws.send(json.dumps(message))
88
- except Exception as e:
89
- self._logger.log_error(f"Error on send: {type(e).__name__} - {e}")
90
-
82
+ self._logger.log_debug(f"Sending payload: {message}")
83
+ await self.ws.send(json.dumps(message))
84
+
91
85
  async def send_heartbeat_loop(self):
92
86
  """Background task that sends heartbeat payloads in regular intervals.
93
87
  Retries until cancelled.
94
88
  """
95
- try:
96
- while True:
97
- await asyncio.sleep(self.heartbeat_interval / 1000)
98
- hb_data = {"op": 1, "d": self.sequence}
99
- await self.send(hb_data)
100
- self._logger.log_debug(f"Sending: {hb_data}")
101
- self._logger.log_info("Heartbeat sent.")
102
- except asyncio.CancelledError:
103
- self._logger.log_debug("Heartbeat task cancelled")
104
- except Exception as e:
105
- self._logger.log_error(f"Error on heartbeat send: {type(e).__name__} - {e}")
106
-
89
+ while self.ws:
90
+ await asyncio.sleep(self.heartbeat_interval / 1000)
91
+ hb_data = {"op": 1, "d": self.sequence}
92
+ await self.send(hb_data)
93
+ self._logger.log_debug(f"Sending: {hb_data}")
94
+ self._logger.log_info("Heartbeat sent.")
95
+
107
96
  async def identify(self):
108
97
  """Sends the IDENIFY payload (token, intents, connection properties).
109
98
  Must be sent after connecting to the WS.
@@ -127,15 +116,12 @@ class GatewayClient:
127
116
 
128
117
  async def start_heartbeat(self):
129
118
  """Waits for initial HELLO event, hydrates the HelloEvent class, and begins the heartbeat."""
130
- try:
131
- data = await self.receive()
132
- hello = HelloEvent.from_dict(data.get('d'))
133
- self.heartbeat_interval = hello.heartbeat_interval
134
- self.heartbeat = asyncio.create_task(self.send_heartbeat_loop())
135
- self._logger.log_high_priority("Heartbeat started.")
136
- except Exception as e:
137
- self._logger.log_error(f"Heartbeat Task Error: {type(e).__name__} - {e}")
138
-
119
+ data = await self.receive()
120
+ hello = HelloEvent.from_dict(data.get('d'))
121
+ self.heartbeat_interval = hello.heartbeat_interval
122
+ self.heartbeat = asyncio.create_task(self.send_heartbeat_loop())
123
+ self._logger.log_high_priority("Heartbeat started.")
124
+
139
125
  async def reconnect(self):
140
126
  """Sends RESUME payload to reconnect with the same session ID and sequence number
141
127
  as provided by Discord.
@@ -156,20 +142,14 @@ class GatewayClient:
156
142
  if self.heartbeat:
157
143
  self._logger.log_high_priority(f"Cancelling heartbeat...")
158
144
  self.heartbeat.cancel()
159
- try:
160
- await asyncio.wait_for(self.heartbeat, timeout=3) # Add timeout to avoid hanging
161
- except asyncio.CancelledError:
162
- self._logger.log_debug("Heartbeat cancelled by CancelledError.")
163
- except asyncio.TimeoutError:
164
- self._logger.log_error("Heartbeat cancel timed out.")
165
- except Exception as e:
166
- self._logger.log_error(f"Unexpected error cancelling heartbeat: {type(e).__name__} - {e}")
145
+ await asyncio.wait_for(self.heartbeat, timeout=3) # Add timeout to avoid hanging
167
146
  self.heartbeat = None
168
147
 
169
148
  if self.ws:
170
- try:
171
- await self.ws.close()
172
- self._logger.log_high_priority("WebSocket closed.")
173
- except Exception as e:
174
- self._logger.log_error(f"Error while closing websocket: {type(e).__name__} - {e}")
149
+ await self.ws.close()
150
+ self._logger.log_high_priority("WebSocket closed.")
175
151
  self.ws = None
152
+
153
+ def is_connected(self):
154
+ """Helper function to tell whether the websocket is still active."""
155
+ return self.ws is not None
discord/http.py CHANGED
@@ -269,24 +269,12 @@ class HTTPClient:
269
269
  if now < self.global_reset:
270
270
  await asyncio.sleep(self.global_reset - now)
271
271
 
272
- async def close_session(self):
273
- """Gracefully shuts down all workes and closes aiohttp session."""
274
- # Stop all bucket workers
272
+ async def close_session(self):
273
+ """Gracefully shuts down all workes and closes aiohttp session."""
274
+ # Stop workers
275
275
  for q in self.bucket_queues.values():
276
276
  await q.queue.put(self._sentinel)
277
-
278
- # Stop pending worker
279
277
  await self.pending_queue.put(self._sentinel)
280
-
281
- # Cancel all tasks except the current one
282
- tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
283
- for task in tasks:
284
- task.cancel()
285
-
286
- # Wait for tasks to acknowledge cancellation
287
- await asyncio.gather(*tasks, return_exceptions=True)
288
-
289
- # Close the aiohttp session
278
+
290
279
  if self.session and not self.session.closed:
291
280
  await self.session.close()
292
- self._logger.log_debug("Session closed successfully.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scurrypy
3
- Version: 0.3.3
3
+ Version: 0.3.4.2
4
4
  Summary: Discord API Wrapper in Python
5
5
  Author: Furmissile
6
6
  Requires-Python: >=3.10
@@ -1,24 +1,24 @@
1
1
  discord/__init__.py,sha256=cETkxHmm0s9YkSJgn-1daQhnbL96fuD7L9SIg2t5vBg,6823
2
- discord/client.py,sha256=v-phlPbUe1Rr-zg5Posbw_jyv8GxvwmvNbjgAjCZW1Y,13566
2
+ discord/client.py,sha256=yrUFRVZ-k5FDLfmFaHic9COZxFJMlq9LBeHi-9QuMx4,13662
3
3
  discord/client_like.py,sha256=JyJq0XBq0vKuPBJ_ZnYf5yAAuX1zz_2B1TZBQE-BYbQ,473
4
4
  discord/config.py,sha256=OH1A2mNKhDlGvQYASEsVUx2pNxP1YQ2a7a7z-IM5xFg,200
5
5
  discord/error.py,sha256=AlislRTna554cM6KC0KrwKugzYDYtx_9C8_3QFe4XDc,2070
6
- discord/gateway.py,sha256=H_WaUrpK8rl3rGlT3qNexpru7R6O6Y6GQPkQcDt_KX8,6555
7
- discord/http.py,sha256=cGFhGEeNebf1sgSUB4Xfnlj00twWLFi9AwO85gAAUDA,10955
6
+ discord/gateway.py,sha256=1TVUKrd3JovoM4df5-GlMZ0kz15Xls5V48ShXDSlK3Q,5334
7
+ discord/http.py,sha256=BI7bCjedh8zuu5pdOLhI-XwMZfdVj5qP9ZZY6WJqtgo,10511
8
8
  discord/intents.py,sha256=Lf2fogFFDqilZeKJv7tcUgKmMW3D7ykK4bBNi-zDzYA,2866
9
9
  discord/logger.py,sha256=qAmOc3geCcCCqPhdi61SVWzMDewmM8Q_KWhTcjO46j8,4726
10
10
  discord/model.py,sha256=CmuxyoWLWokE_UvCQ9M7U9Cr7JH9R7ULMv9KMwzXjDQ,3105
11
11
  discord/dispatch/__init__.py,sha256=m7ixrbNhOV9QRORXPw6LSwxofQMAvLmPFBweBZu9ACc,20
12
12
  discord/dispatch/command_dispatcher.py,sha256=pyJOQaZLZYrHUEs6HEWp8XMKTMZX4SBrwTizGKIeUG8,5904
13
- discord/dispatch/event_dispatcher.py,sha256=tgU5FXJT2NAluT0aIlMhtMjz6hHcJRN6us1vAG639RU,2612
13
+ discord/dispatch/event_dispatcher.py,sha256=1A7Qof_IzTi5_14IMPxIQDpvo3-Sj-X0KZWOuVGH53k,3764
14
14
  discord/dispatch/prefix_dispatcher.py,sha256=4mkn3cuXTjdEChbewkbZQqd_sMKm4jePSFKKOPbt12g,2065
15
15
  discord/events/__init__.py,sha256=xE8YtJ7NKZkm7MLnohDQIbezh3ColmLR-3BMiZabt3k,18
16
- discord/events/channel_events.py,sha256=NJbu7oXRdtkUQZ68XbX6fBXINaJmavAkNZStFVA3rzs,1318
17
- discord/events/guild_events.py,sha256=HA4-loifH5x3j-zAif3H5sJpKX_HbZz4ORgBFvPMe70,852
16
+ discord/events/channel_events.py,sha256=t9UL4JjDqulAP_XepQ8MRMW54pNRqCbIK3M8tauzf9I,1556
17
+ discord/events/guild_events.py,sha256=Ok9tW3tjcwtbiqJgbe-42d9-R3-2RzqmIgBHEP-2Pcc,896
18
18
  discord/events/hello_event.py,sha256=O8Ketu_N943cnGaFkGsAHfWhgKXFQCYCqSD3EqdsXjA,225
19
19
  discord/events/interaction_events.py,sha256=5hlYOsV1ROiRXISAGCKcZo8vfk6oqiZcNzzZjSteiag,4361
20
- discord/events/message_events.py,sha256=uovC_n73LuERwjqJHsXqW-IktuIzQTfolQSAzYzlzpc,1411
21
- discord/events/reaction_events.py,sha256=Mwj3sUWDBl5YKIs4URylXqsrLyMhtQk7d-hF1mYhFbo,2568
20
+ discord/events/message_events.py,sha256=M5xdaJH1zRzdZk0oN0Jykaeu9k09EjgZjeiIT_EkL1A,1475
21
+ discord/events/reaction_events.py,sha256=xx7GD-fqakhJmS-X-HbuAUg9pg6Gqo_KRtLTdPJu7UE,2643
22
22
  discord/events/ready_event.py,sha256=c3Pf4ndNYV2byuliADi8pUxpuvKXa9FLKNz_uzIWGso,794
23
23
  discord/models/__init__.py,sha256=ZKhFO5eX4GbTRdvi4CU4z2hO-HQU29WZw2x4DujvARY,18
24
24
  discord/models/application.py,sha256=2sXtRysUc2TJ40FjdcrWgosmwMrp_h3ybddubQMixKM,924
@@ -48,8 +48,8 @@ discord/resources/guild.py,sha256=Unld1lWY3XynmRHU2FCi3-LA9VNp2thMI2BlILUTTxk,81
48
48
  discord/resources/interaction.py,sha256=mr4kLecQpl3AtgnNeqnj3eZIwQ73Clutdi-gnoSMWVU,5237
49
49
  discord/resources/message.py,sha256=RtvcCRx0lwW-mHPl3aNYoEvGffrvtpLsQ2fVWckywVI,7527
50
50
  discord/resources/user.py,sha256=vk89TnCVi-6ZgbDs_TZTCXrx_NfFS5Q9Wi_itYoaoyg,3085
51
- scurrypy-0.3.3.dist-info/licenses/LICENSE,sha256=NtspfRMAlryd1Eev4BYi9EFbKhvdmlCJJ2-ADUoEBoI,426
52
- scurrypy-0.3.3.dist-info/METADATA,sha256=QQHxDvVbl4i51dJwTCr_atvGEXOK9mITtWrK-tdpNZg,2954
53
- scurrypy-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- scurrypy-0.3.3.dist-info/top_level.txt,sha256=fJkrNbR-_8ubMBUcDEJBcfkpECrvSEmMrNKgvLlQFoM,8
55
- scurrypy-0.3.3.dist-info/RECORD,,
51
+ scurrypy-0.3.4.2.dist-info/licenses/LICENSE,sha256=NtspfRMAlryd1Eev4BYi9EFbKhvdmlCJJ2-ADUoEBoI,426
52
+ scurrypy-0.3.4.2.dist-info/METADATA,sha256=4woer2KUB0UBUp-fQnL2xJm3CF0JyZTRqvTuENDs_Vs,2956
53
+ scurrypy-0.3.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
+ scurrypy-0.3.4.2.dist-info/top_level.txt,sha256=fJkrNbR-_8ubMBUcDEJBcfkpECrvSEmMrNKgvLlQFoM,8
55
+ scurrypy-0.3.4.2.dist-info/RECORD,,