scurrypy 0.3.2__tar.gz → 0.7.2__tar.gz

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 (89) hide show
  1. scurrypy-0.7.2/LICENSE +16 -0
  2. scurrypy-0.7.2/PKG-INFO +114 -0
  3. scurrypy-0.7.2/README.md +102 -0
  4. scurrypy-0.7.2/pyproject.toml +27 -0
  5. scurrypy-0.7.2/scurrypy/__init__.py +16 -0
  6. scurrypy-0.7.2/scurrypy/client.py +335 -0
  7. scurrypy-0.7.2/scurrypy/core/__init__.py +16 -0
  8. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/client_like.py +8 -1
  9. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/error.py +6 -18
  10. scurrypy-0.7.2/scurrypy/core/gateway.py +183 -0
  11. scurrypy-0.7.2/scurrypy/core/http.py +310 -0
  12. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/intents.py +5 -7
  13. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/logger.py +14 -60
  14. scurrypy-0.7.2/scurrypy/core/model.py +71 -0
  15. scurrypy-0.7.2/scurrypy/dispatch/__init__.py +7 -0
  16. scurrypy-0.7.2/scurrypy/dispatch/command_dispatcher.py +205 -0
  17. scurrypy-0.7.2/scurrypy/dispatch/event_dispatcher.py +99 -0
  18. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/dispatch/prefix_dispatcher.py +32 -13
  19. scurrypy-0.7.2/scurrypy/events/__init__.py +62 -0
  20. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/channel_events.py +9 -2
  21. scurrypy-0.7.2/scurrypy/events/gateway_events.py +31 -0
  22. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/guild_events.py +24 -1
  23. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/hello_event.py +1 -1
  24. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/interaction_events.py +35 -18
  25. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/message_events.py +12 -8
  26. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/reaction_events.py +6 -6
  27. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/ready_event.py +2 -4
  28. scurrypy-0.7.2/scurrypy/models/__init__.py +15 -0
  29. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/emoji.py +18 -2
  30. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/guild.py +5 -3
  31. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/interaction.py +6 -1
  32. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/role.py +2 -1
  33. scurrypy-0.7.2/scurrypy/models/user.py +95 -0
  34. scurrypy-0.7.2/scurrypy/parts/__init__.py +79 -0
  35. scurrypy-0.7.2/scurrypy/parts/channel.py +42 -0
  36. scurrypy-0.7.2/scurrypy/parts/command.py +90 -0
  37. scurrypy-0.7.2/scurrypy/parts/components.py +224 -0
  38. scurrypy-0.7.2/scurrypy/parts/components_v2.py +144 -0
  39. scurrypy-0.7.2/scurrypy/parts/embed.py +83 -0
  40. scurrypy-0.7.2/scurrypy/parts/message.py +134 -0
  41. scurrypy-0.7.2/scurrypy/parts/modal.py +16 -0
  42. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/parts/role.py +2 -14
  43. scurrypy-0.7.2/scurrypy/resources/__init__.py +45 -0
  44. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/application.py +3 -4
  45. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/bot_emojis.py +2 -2
  46. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/channel.py +11 -10
  47. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/guild.py +16 -18
  48. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/interaction.py +59 -38
  49. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/message.py +25 -18
  50. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/user.py +5 -6
  51. scurrypy-0.7.2/scurrypy.egg-info/PKG-INFO +114 -0
  52. scurrypy-0.7.2/scurrypy.egg-info/SOURCES.txt +56 -0
  53. scurrypy-0.7.2/scurrypy.egg-info/requires.txt +2 -0
  54. scurrypy-0.7.2/scurrypy.egg-info/top_level.txt +1 -0
  55. scurrypy-0.3.2/LICENSE +0 -5
  56. scurrypy-0.3.2/PKG-INFO +0 -85
  57. scurrypy-0.3.2/README.md +0 -75
  58. scurrypy-0.3.2/discord/__init__.py +0 -10
  59. scurrypy-0.3.2/discord/client.py +0 -349
  60. scurrypy-0.3.2/discord/dispatch/__init__.py +0 -1
  61. scurrypy-0.3.2/discord/dispatch/command_dispatcher.py +0 -163
  62. scurrypy-0.3.2/discord/dispatch/event_dispatcher.py +0 -91
  63. scurrypy-0.3.2/discord/events/__init__.py +0 -33
  64. scurrypy-0.3.2/discord/gateway.py +0 -175
  65. scurrypy-0.3.2/discord/http.py +0 -292
  66. scurrypy-0.3.2/discord/model.py +0 -90
  67. scurrypy-0.3.2/discord/models/__init__.py +0 -8
  68. scurrypy-0.3.2/discord/models/application.py +0 -37
  69. scurrypy-0.3.2/discord/models/integration.py +0 -23
  70. scurrypy-0.3.2/discord/models/member.py +0 -27
  71. scurrypy-0.3.2/discord/models/user.py +0 -15
  72. scurrypy-0.3.2/discord/parts/__init__.py +0 -28
  73. scurrypy-0.3.2/discord/parts/action_row.py +0 -257
  74. scurrypy-0.3.2/discord/parts/attachment.py +0 -18
  75. scurrypy-0.3.2/discord/parts/channel.py +0 -20
  76. scurrypy-0.3.2/discord/parts/command.py +0 -102
  77. scurrypy-0.3.2/discord/parts/components_v2.py +0 -270
  78. scurrypy-0.3.2/discord/parts/embed.py +0 -154
  79. scurrypy-0.3.2/discord/parts/message.py +0 -179
  80. scurrypy-0.3.2/discord/parts/modal.py +0 -21
  81. scurrypy-0.3.2/discord/resources/__init__.py +0 -10
  82. scurrypy-0.3.2/pyproject.toml +0 -23
  83. scurrypy-0.3.2/scurrypy.egg-info/PKG-INFO +0 -85
  84. scurrypy-0.3.2/scurrypy.egg-info/SOURCES.txt +0 -57
  85. scurrypy-0.3.2/scurrypy.egg-info/top_level.txt +0 -1
  86. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/config.py +0 -0
  87. {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/parts/component_types.py +0 -0
  88. {scurrypy-0.3.2 → scurrypy-0.7.2}/scurrypy.egg-info/dependency_links.txt +0 -0
  89. {scurrypy-0.3.2 → scurrypy-0.7.2}/setup.cfg +0 -0
scurrypy-0.7.2/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright (c) 2025 Furmissile. All rights reserved.
2
+
3
+ Permission is granted to view, use, modify, and distribute copies of this software
4
+ and its source code, provided that:
5
+
6
+ 1. Attribution to the original author, Furmissile, is preserved in all copies and
7
+ derivative works.
8
+ 2. The name "ScurryPy" and associated branding may not be used to promote derived
9
+ projects without explicit permission.
10
+ 3. This license and copyright notice must be included in all copies or substantial
11
+ portions of the software.
12
+ 4. This software is provided "as is", without warranty of any kind, express or
13
+ implied. The author assumes no liability for any damages arising from its use.
14
+
15
+ 5. This software may not be used for commercial purposes without written consent
16
+ from the author.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: scurrypy
3
+ Version: 0.7.2
4
+ Summary: Dataclass-driven Discord API Wrapper in Python
5
+ Author: Furmissile
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: aiohttp>=3.8.0
10
+ Requires-Dist: websockets>=11.0.0
11
+ Dynamic: license-file
12
+
13
+ ## __<center> ScurryPy </center>__
14
+
15
+ [![PyPI version](https://badge.fury.io/py/scurrypy.svg)](https://badge.fury.io/py/scurrypy)
16
+
17
+ A lightweight, fully readable Discord API framework built to accommodate everything from basic bots to custom frameworks.
18
+
19
+ While ScurryPy powers many squirrel-related shenanigans, it works just as well for game bots, interactive components, and educational projects.
20
+
21
+ ## Features
22
+ * Easy to extend and build frameworks on top
23
+ * Lightweight core (<1000 lines)
24
+ * Command, and event handling
25
+ * Unix shell-style wildcards for component routing
26
+ * Declarative style using decorators
27
+ * Supports both legacy and new features
28
+ * Respects Discord's rate limits
29
+ * No `__future__` hacks to avoid circular import
30
+ * Capable of sharding
31
+
32
+ ## Getting Started
33
+
34
+ *Note: This section also appears in the documentation, but here are complete examples ready to use with your bot credentials.*
35
+
36
+ ### Installation
37
+
38
+ To install the ScurryPy package, run:
39
+
40
+ ```bash
41
+ pip install scurrypy
42
+ ```
43
+
44
+ ## Minimal Slash Command
45
+
46
+ The following demonstrates building and responding to a slash command.
47
+
48
+ ```py
49
+ import scurrypy
50
+
51
+ client = scurrypy.Client(
52
+ token='your-token',
53
+ application_id=APPLICATION_ID # your bot's application ID
54
+ )
55
+
56
+ @client.command(
57
+ scurrypy.SlashCommand('example', 'Demonstrate the minimal slash command!'),
58
+ GUILD_ID # must be a guild ID your bot is in
59
+ )
60
+ async def example(bot: scurrypy.Client, event: scurrypy.InteractionEvent):
61
+ await event.interaction.respond(f'Hello, {event.interaction.member.user.username}!')
62
+
63
+ client.run()
64
+ ```
65
+
66
+ ## Minimal Prefix Command (Legacy)
67
+
68
+ The following demonstrates building and responding to a message prefix command.
69
+
70
+ ```py
71
+ import scurrypy
72
+
73
+ client = scurrypy.Client(
74
+ token='your-token',
75
+ application_id=APPLICATION_ID, # your bot's application ID
76
+ intents=scurrypy.set_intents(message_content=True),
77
+ prefix='!' # your custom prefix
78
+ )
79
+
80
+ @client.prefix_command("ping")
81
+ async def on_ping(bot: scurrypy.Client, event: scurrypy.MessageCreateEvent):
82
+ await event.message.send("Pong!")
83
+
84
+ client.run()
85
+ ```
86
+
87
+ ## Building on Top of ScurryPy
88
+
89
+ ScurryPy is designed to be easy to extend with your own abstractions.
90
+
91
+ The following demonstrates integrating a custom cache into your client configuration:
92
+
93
+ ```py
94
+ class CacheProtocol(Protocol):
95
+ async def get_user(self, user_id: int) ...
96
+
97
+ # and the rest...
98
+
99
+ class MyCache(CacheProtocol):
100
+ # your implementation...
101
+
102
+ class MyConfig(BaseConfig):
103
+ cache: MyCache
104
+ # other stuff here...
105
+
106
+ client = scurrypy.Client(
107
+ token = 'your-token',
108
+ application_id = 123456789012345,
109
+ config = MyConfig()
110
+ )
111
+ ```
112
+
113
+ ## Like What You See?
114
+ Explore the full [documentation](https://furmissile.github.io/scurrypy) for more examples, guides, and API reference.
@@ -0,0 +1,102 @@
1
+ ## __<center> ScurryPy </center>__
2
+
3
+ [![PyPI version](https://badge.fury.io/py/scurrypy.svg)](https://badge.fury.io/py/scurrypy)
4
+
5
+ A lightweight, fully readable Discord API framework built to accommodate everything from basic bots to custom frameworks.
6
+
7
+ While ScurryPy powers many squirrel-related shenanigans, it works just as well for game bots, interactive components, and educational projects.
8
+
9
+ ## Features
10
+ * Easy to extend and build frameworks on top
11
+ * Lightweight core (<1000 lines)
12
+ * Command, and event handling
13
+ * Unix shell-style wildcards for component routing
14
+ * Declarative style using decorators
15
+ * Supports both legacy and new features
16
+ * Respects Discord's rate limits
17
+ * No `__future__` hacks to avoid circular import
18
+ * Capable of sharding
19
+
20
+ ## Getting Started
21
+
22
+ *Note: This section also appears in the documentation, but here are complete examples ready to use with your bot credentials.*
23
+
24
+ ### Installation
25
+
26
+ To install the ScurryPy package, run:
27
+
28
+ ```bash
29
+ pip install scurrypy
30
+ ```
31
+
32
+ ## Minimal Slash Command
33
+
34
+ The following demonstrates building and responding to a slash command.
35
+
36
+ ```py
37
+ import scurrypy
38
+
39
+ client = scurrypy.Client(
40
+ token='your-token',
41
+ application_id=APPLICATION_ID # your bot's application ID
42
+ )
43
+
44
+ @client.command(
45
+ scurrypy.SlashCommand('example', 'Demonstrate the minimal slash command!'),
46
+ GUILD_ID # must be a guild ID your bot is in
47
+ )
48
+ async def example(bot: scurrypy.Client, event: scurrypy.InteractionEvent):
49
+ await event.interaction.respond(f'Hello, {event.interaction.member.user.username}!')
50
+
51
+ client.run()
52
+ ```
53
+
54
+ ## Minimal Prefix Command (Legacy)
55
+
56
+ The following demonstrates building and responding to a message prefix command.
57
+
58
+ ```py
59
+ import scurrypy
60
+
61
+ client = scurrypy.Client(
62
+ token='your-token',
63
+ application_id=APPLICATION_ID, # your bot's application ID
64
+ intents=scurrypy.set_intents(message_content=True),
65
+ prefix='!' # your custom prefix
66
+ )
67
+
68
+ @client.prefix_command("ping")
69
+ async def on_ping(bot: scurrypy.Client, event: scurrypy.MessageCreateEvent):
70
+ await event.message.send("Pong!")
71
+
72
+ client.run()
73
+ ```
74
+
75
+ ## Building on Top of ScurryPy
76
+
77
+ ScurryPy is designed to be easy to extend with your own abstractions.
78
+
79
+ The following demonstrates integrating a custom cache into your client configuration:
80
+
81
+ ```py
82
+ class CacheProtocol(Protocol):
83
+ async def get_user(self, user_id: int) ...
84
+
85
+ # and the rest...
86
+
87
+ class MyCache(CacheProtocol):
88
+ # your implementation...
89
+
90
+ class MyConfig(BaseConfig):
91
+ cache: MyCache
92
+ # other stuff here...
93
+
94
+ client = scurrypy.Client(
95
+ token = 'your-token',
96
+ application_id = 123456789012345,
97
+ config = MyConfig()
98
+ )
99
+ ```
100
+
101
+ ## Like What You See?
102
+ Explore the full [documentation](https://furmissile.github.io/scurrypy) for more examples, guides, and API reference.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scurrypy"
7
+ version = "0.7.2"
8
+
9
+ description = "Dataclass-driven Discord API Wrapper in Python"
10
+ readme = "README.md"
11
+ authors = [{ name = "Furmissile" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+ "aiohttp>=3.8.0",
15
+ "websockets>=11.0.0"
16
+ ]
17
+
18
+ [tool.setuptools]
19
+ packages = [
20
+ "scurrypy",
21
+ "scurrypy.dispatch",
22
+ "scurrypy.events",
23
+ "scurrypy.resources",
24
+ "scurrypy.parts",
25
+ "scurrypy.core",
26
+ "scurrypy.models"
27
+ ]
@@ -0,0 +1,16 @@
1
+ # scurrypy
2
+
3
+ from .client import Client
4
+
5
+ __all__ = [
6
+ # top-level modules
7
+ "Client"
8
+ ]
9
+
10
+ # imports listed __all__ libs
11
+ from .events import *
12
+ from .parts import *
13
+ from .resources import *
14
+ from .dispatch import *
15
+ from .models import *
16
+ from .core import *
@@ -0,0 +1,335 @@
1
+ import asyncio
2
+
3
+ from .core.config import BaseConfig
4
+ from .core.intents import Intents
5
+ from .core.gateway import GatewayClient
6
+ from .core.client_like import ClientLike
7
+
8
+ from .parts.command import SlashCommand, MessageCommand, UserCommand
9
+
10
+ class Client(ClientLike):
11
+ """Main entry point for Discord bots.
12
+ Ties together the moving parts: gateway, HTTP, event dispatching, command handling, and resource managers.
13
+ """
14
+ def __init__(self,
15
+ *,
16
+ token: str,
17
+ application_id: int,
18
+ intents: int = Intents.DEFAULT,
19
+ config: BaseConfig = None,
20
+ debug_mode: bool = False,
21
+ sync_commands: bool = True,
22
+ prefix = None,
23
+ quiet: bool = False
24
+ ):
25
+ """
26
+ Args:
27
+ token (str): the bot's token
28
+ application_id (int): the bot's user ID
29
+ intents (int, optional): gateway intents. Defaults to Intents.DEFAULT.
30
+ config (BaseConfig, optional): user-defined config data
31
+ sync_commands (bool, optional): toggle registering commands. Defaults to True.
32
+ debug_mode (bool, optional): toggle debug messages. Defaults to False.
33
+ prefix (str, optional): set message prefix if using command prefixes
34
+ quiet (bool, optional): if INFO, DEBUG, and WARN should be logged
35
+ """
36
+ if not token:
37
+ raise ValueError("Token is required")
38
+ if not application_id:
39
+ raise ValueError("Application ID is required")
40
+
41
+ from .core.logger import Logger
42
+ from .core.http import HTTPClient
43
+ from .resources.bot_emojis import BotEmojis
44
+ from .dispatch.event_dispatcher import EventDispatcher
45
+ from .dispatch.prefix_dispatcher import PrefixDispatcher
46
+ from .dispatch.command_dispatcher import CommandDispatcher
47
+
48
+ self.token = token
49
+ self.intents = intents
50
+ self.application_id = application_id
51
+ self.config = config
52
+ self.sync_commands = sync_commands
53
+
54
+ self._logger = Logger(debug_mode, quiet)
55
+
56
+ self._http = HTTPClient(self._logger)
57
+
58
+ self.shards: list[GatewayClient] = []
59
+ self.dispatcher = EventDispatcher(self)
60
+ self.prefix_dispatcher = PrefixDispatcher(self, prefix)
61
+ self.command_dispatcher = CommandDispatcher(self)
62
+
63
+ self._setup_hooks = []
64
+ self._shutdown_hooks = []
65
+
66
+ self.emojis = BotEmojis(self._http, self.application_id)
67
+
68
+ def prefix_command(self, name: str):
69
+ """Decorator registers prefix commands by the name of the function.
70
+
71
+ Args:
72
+ name (str): name of the command
73
+ !!! warning "Important"
74
+ Prefix commands are CASE-INSENSITIVE.
75
+ """
76
+ def decorator(func):
77
+ self.prefix_dispatcher.register(name.lower(), func)
78
+ return func
79
+ return decorator
80
+
81
+ def component(self, custom_id: str):
82
+ """Decorator registers a function for a component handler.
83
+
84
+ Args:
85
+ custom_id (str): Identifier of the component. Must match the `custom_id` set where the component was created.
86
+ """
87
+ def decorator(func):
88
+ self.command_dispatcher.component(func, custom_id)
89
+ return func
90
+ return decorator
91
+
92
+ def command(self, command: SlashCommand | MessageCommand | UserCommand, guild_ids: list[int] = None):
93
+ """Decorator registers a function to a command handler.
94
+
95
+ Args:
96
+ command (SlashCommand | MessageCommand | UserCommand): the command object
97
+ guild_ids (list[int], optional): Guild IDs to register command to (if any). If omitted, the command is **global**.
98
+ """
99
+ def decorator(func):
100
+ if not isinstance(command, (SlashCommand, MessageCommand, UserCommand)):
101
+ raise ValueError(f"Expected SlashCommand, MessageCommand, or UserCommand; got {type(command).__name__}")
102
+
103
+ # maps command type -> command registry
104
+ handler_map = {
105
+ SlashCommand: self.command_dispatcher.add_slash_command,
106
+ MessageCommand: self.command_dispatcher.add_message_command,
107
+ UserCommand: self.command_dispatcher.add_user_command
108
+ }
109
+
110
+ # can guarantee at this point command is one of SlashCommand | MessageCommand | UserCommand
111
+ handler = handler_map[type(command)]
112
+
113
+ handler(command, func, guild_ids)
114
+ return func
115
+ return decorator
116
+
117
+ def event(self, event_name: str):
118
+ """Decorator registers a function for an event handler.
119
+
120
+ Args:
121
+ event_name (str): event name (must be a valid event)
122
+ """
123
+ def decorator(func):
124
+ self.dispatcher.register(event_name, func)
125
+ return func
126
+ return decorator
127
+
128
+ def setup_hook(self, func):
129
+ """Decorator registers a setup hook.
130
+ (Runs once before the bot starts listening)
131
+
132
+ Args:
133
+ func (callable): callback to the setup function
134
+ """
135
+ self._setup_hooks.append(func)
136
+
137
+ def shutdown_hook(self, func):
138
+ """Decorator registers a shutdown hook.
139
+ (Runs once before the bot exits the loop)
140
+
141
+ Args:
142
+ func (callable): callback to the shutdown function
143
+ """
144
+ self._shutdown_hooks.append(func)
145
+
146
+ def fetch_application(self, application_id: int):
147
+ """Creates an interactable application resource.
148
+
149
+ Args:
150
+ application_id (int): ID of target application
151
+
152
+ Returns:
153
+ (Application): the Application resource
154
+ """
155
+ from .resources.application import Application
156
+
157
+ return Application(application_id, self._http)
158
+
159
+ def fetch_guild(self, guild_id: int):
160
+ """Creates an interactable guild resource.
161
+
162
+ Args:
163
+ guild_id (int): ID of target guild
164
+
165
+ Returns:
166
+ (Guild): the Guild resource
167
+ """
168
+ from .resources.guild import Guild
169
+
170
+ return Guild(guild_id, self._http)
171
+
172
+ def fetch_channel(self, channel_id: int):
173
+ """Creates an interactable channel resource.
174
+
175
+ Args:
176
+ channel_id (int): ID of target channel
177
+
178
+ Returns:
179
+ (Channel): the Channel resource
180
+ """
181
+ from .resources.channel import Channel
182
+
183
+ return Channel(channel_id, self._http)
184
+
185
+ def fetch_message(self, channel_id: int, message_id: int):
186
+ """Creates an interactable message resource.
187
+
188
+ Args:
189
+ message_id (int): ID of target message
190
+ channel_id (int): channel ID of target message
191
+
192
+ Returns:
193
+ (Message): the Message resource
194
+ """
195
+ from .resources.message import Message
196
+
197
+ return Message(message_id, channel_id, self._http)
198
+
199
+ def fetch_user(self, user_id: int):
200
+ """Creates an interactable user resource.
201
+
202
+ Args:
203
+ user_id (int): ID of target user
204
+
205
+ Returns:
206
+ (User): the User resource
207
+ """
208
+ from .resources.user import User
209
+
210
+ return User(user_id, self._http)
211
+
212
+ async def clear_commands(self, guild_ids: list[int] = None):
213
+ """Clear a guild's or global commands (all types).
214
+
215
+ Args:
216
+ guild_ids (list[int]): ID of the target guild. If omitted, **global** commands will be cleared.
217
+ """
218
+ self.command_dispatcher.clear_commands(guild_ids)
219
+
220
+ async def _start_shards(self):
221
+ """Starts all shards batching by max_concurrency."""
222
+
223
+ from .events.gateway_events import GatewayEvent
224
+
225
+ data = await self._http.request('GET', '/gateway/bot')
226
+
227
+ gateway = GatewayEvent.from_dict(data)
228
+
229
+ # pull important values for easier access
230
+ total_shards = gateway.shards
231
+ batch_size = gateway.session_start_limit.max_concurrency
232
+
233
+ tasks = []
234
+
235
+ for batch_start in range(0, total_shards, batch_size):
236
+ batch_end = min(batch_start + batch_size, total_shards)
237
+
238
+ self._logger.log_info(f"Starting shards {batch_start}-{batch_end - 1} of {total_shards}")
239
+
240
+ for shard_id in range(batch_start, batch_end):
241
+ shard = GatewayClient(self, gateway.url, shard_id, total_shards)
242
+ self.shards.append(shard)
243
+
244
+ # fire and forget
245
+ tasks.append(asyncio.create_task(shard.start()))
246
+ tasks.append(asyncio.create_task(self._listen_shard(shard)))
247
+
248
+ # wait before next batch to respect identify rate limit
249
+ await asyncio.sleep(5)
250
+
251
+ return tasks
252
+
253
+ async def _listen_shard(self, shard: GatewayClient):
254
+ """Listen to websocket queue for events. Only OP code 0 passes!
255
+
256
+ Args:
257
+ shard (GatewayClient): the shard or gateway to listen on
258
+ """
259
+ while True:
260
+ try:
261
+ dispatch_type, event_data = await shard.event_queue.get()
262
+
263
+ # check prefix first (only if a prefix is set)
264
+ if self.prefix_dispatcher.prefix and dispatch_type == 'MESSAGE_CREATE':
265
+ await self.prefix_dispatcher.dispatch(event_data)
266
+
267
+ # then try interaction
268
+ elif dispatch_type == 'INTERACTION_CREATE':
269
+ await self.command_dispatcher.dispatch(event_data)
270
+
271
+ # otherwise this must be an event!
272
+ await self.dispatcher.dispatch(dispatch_type, event_data)
273
+ except:
274
+ break # stop task if an error occurred
275
+
276
+ async def _start(self):
277
+ """Starts the HTTP/Websocket client, run startup hooks, and registers commands."""
278
+
279
+ try:
280
+ await self._http.start(self.token)
281
+
282
+ if self._setup_hooks:
283
+ for hook in self._setup_hooks:
284
+ self._logger.log_info(f"Setting hook {hook.__name__}")
285
+ await hook(self)
286
+ self._logger.log_high_priority("Hooks set up.")
287
+
288
+ if self.sync_commands:
289
+ await self.command_dispatcher.register_guild_commands()
290
+
291
+ await self.command_dispatcher.register_global_commands()
292
+
293
+ self._logger.log_high_priority("Commands set up.")
294
+
295
+ tasks = await asyncio.create_task(self._start_shards())
296
+
297
+ # end all ongoing tasks
298
+ await asyncio.gather(*tasks)
299
+
300
+ except asyncio.CancelledError:
301
+ self._logger.log_high_priority("Connection cancelled via KeyboardInterrupt.")
302
+ except Exception as e:
303
+ self._logger.log_error(f"{type(e).__name__} - {e}")
304
+ finally:
305
+ await self._close()
306
+
307
+ async def _close(self):
308
+ """Gracefully close HTTP session, websocket connections, and run shutdown hooks."""
309
+
310
+ for hook in self._shutdown_hooks:
311
+ try:
312
+ self._logger.log_info(f"Executing shutdown hook {hook.__name__}")
313
+ await hook(self)
314
+ except Exception as e:
315
+ self._logger.log_error(f"Shutdown hook failed: {type(e).__name__}: {e}")
316
+
317
+ self._logger.log_info("Closing HTTP session...")
318
+ await self._http.close()
319
+
320
+ # close each connection or shard
321
+ for shard in self.shards:
322
+ await shard.close_ws()
323
+
324
+ def run(self):
325
+ """User-facing entry point for starting the client."""
326
+
327
+ try:
328
+ asyncio.run(self._start())
329
+ except KeyboardInterrupt:
330
+ self._logger.log_info("Shutdown requested via KeyboardInterrupt.")
331
+ except Exception as e:
332
+ self._logger.log_error(f"{type(e).__name__} {e}")
333
+ finally:
334
+ self._logger.log_high_priority("Bot shutting down.")
335
+ self._logger.close()
@@ -0,0 +1,16 @@
1
+ # scurrypy/core
2
+
3
+ # from .client_like import ClientLike
4
+ from .config import BaseConfig
5
+ # from .error import DiscordError
6
+ # from .gateway import GatewayClient
7
+ # from .http import HTTPClient
8
+ from .intents import Intents, set_intents
9
+ from .logger import Logger
10
+ # from .model import DataModel
11
+
12
+ __all__ = [
13
+ "BaseConfig",
14
+ "Intents", 'set_intents',
15
+ "Logger"
16
+ ]
@@ -5,10 +5,17 @@ from .http import HTTPClient
5
5
  from .logger import Logger
6
6
 
7
7
  class ClientLike(Protocol):
8
- """Exposes a common interface for [`Client`][discord.client.Client]."""
8
+ """Exposes a common interface for [`Client`][scurrypy.client.Client]."""
9
+
10
+ token: str
11
+ """Bot's token."""
12
+
9
13
  application_id: int
10
14
  """Bot's application ID."""
11
15
 
16
+ intents: int
17
+ """Bot intents for listening to events."""
18
+
12
19
  config: BaseConfig
13
20
  """User-defined config."""
14
21
 
@@ -1,12 +1,6 @@
1
- METHOD_SUCCESS_CODES = {
2
- "GET": (200),
3
- "POST": (200, 201),
4
- "PATCH": (200, 201),
5
- "DELETE": (200, 204)
6
- }
7
-
8
1
  class DiscordError(Exception):
9
2
  """Represents a Discord API error."""
3
+
10
4
  def __init__(self, status: int, data: dict):
11
5
  """Initialize the error with Discord's response.
12
6
  Extracts reason, code, and walks the nested errors.
@@ -15,22 +9,14 @@ class DiscordError(Exception):
15
9
  data (dict): Discord's error JSON
16
10
  """
17
11
  self.data = data
18
- """Raw error data."""
19
-
12
+ self.status = status
20
13
  self.reason = data.get('message', 'Unknown Error')
21
- """Discord-generated reason for error."""
22
-
23
- self.code = data.get('code', '???')
24
- """Discord-generated code of error."""
14
+ self.code = data.get('code', 'Unknown Code')
25
15
 
26
16
  self.error_data = data.get('errors', {})
27
- """Error-specific data."""
28
-
29
17
  self.details = self.walk(self.error_data)
30
- """Error details."""
31
18
 
32
- self.fatal = status in (401, 403)
33
- """If this error is considered fatal."""
19
+ self.is_fatal = status in (401, 403)
34
20
 
35
21
  errors = [f"→ {path}: {reason}" for path, reason in self.details]
36
22
  full_message = f"{self.reason} ({self.code})"
@@ -58,6 +44,8 @@ class DiscordError(Exception):
58
44
  if key == '_errors' and isinstance(value, list):
59
45
  msg = value[0].get('message', 'Unknown error')
60
46
  result.append(('.'.join(path), msg))
47
+
48
+ # the value should not be a dict -- keep going
61
49
  elif isinstance(value, dict):
62
50
  result.extend(self.walk(value, path + [key]))
63
51
  return result