scurry-kit 0.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.
scurry_kit/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ # scurry_kit
2
+
3
+ from .client import ScurryKit
4
+
5
+ __all__ = [
6
+ "ScurryKit"
7
+ ]
8
+
9
+ from .addons import *
@@ -0,0 +1,9 @@
1
+ # easybot/addons
2
+
3
+ from .component_builder import ComponentBuilder
4
+ from .embed_builder import EmbedBuilder
5
+
6
+ __all__ = [
7
+ "ComponentBuilder",
8
+ "EmbedBuilder"
9
+ ]
@@ -0,0 +1,13 @@
1
+ # easybot/addons/cache
2
+
3
+ from .bot_emojis import BotEmojisCacheAddon
4
+ from .channel import GuildChannelCacheAddon
5
+ from .guild_emojis import GuildEmojiCacheAddon
6
+ from .role import RoleCacheAddon
7
+
8
+ __all__ = [
9
+ 'BotEmojisCacheAddon',
10
+ 'GuildChannelCacheAddon',
11
+ 'GuildEmojiCacheAddon',
12
+ 'RoleCacheAddon'
13
+ ]
@@ -0,0 +1,34 @@
1
+ from scurrypy import (
2
+ Addon,
3
+ Client,
4
+ EmojiModel
5
+ )
6
+
7
+ class BotEmojisCacheAddon(Addon):
8
+ """Defines caching bot emojis and lookup."""
9
+
10
+ def __init__(self, client: Client):
11
+ self.bot = client
12
+ self.emojis: dict[str, EmojiModel] = {} # index by unique name
13
+ client.addons.add(self)
14
+
15
+ def setup(self):
16
+ self.bot.add_startup_hook(self.load_bot_emojis)
17
+
18
+ async def load_bot_emojis(self):
19
+ """Fetch all bot's emojis and add them to the cache."""
20
+ emojis = await self.bot.bot_emoji().fetch_all()
21
+
22
+ for emoji in emojis:
23
+ self.emojis[emoji.name] = emoji
24
+
25
+ def get_emoji(self, name: str):
26
+ """Get an emoji from the cache.
27
+
28
+ Args:
29
+ name (str): name of the emoji
30
+
31
+ Returns:
32
+ (EmojiModel | None): the emoji object if found else None
33
+ """
34
+ return self.emojis.get(name)
@@ -0,0 +1,113 @@
1
+ from scurrypy import (
2
+ Addon,
3
+ Client,
4
+ ChannelModel,
5
+ GuildCreateEvent, GuildDeleteEvent,
6
+ GuildChannelCreateEvent, GuildChannelUpdateEvent, GuildChannelDeleteEvent
7
+ )
8
+
9
+ class GuildChannelCacheAddon(Addon):
10
+ """Defines caching channels and lookup."""
11
+
12
+ def __init__(self, client: Client):
13
+ self.bot = client
14
+
15
+ self.channels: dict[int, dict[int, ChannelModel]] = {} # stores OBJECTS
16
+ self.channel_index: dict[int, ChannelModel] = {} # stores REFERENCES
17
+
18
+ client.addons.add(self)
19
+
20
+ def setup(self):
21
+ self.bot.add_startup_hook(self.on_startup)
22
+
23
+ def on_startup(self):
24
+ self.bot.add_event_listener('GUILD_CREATE', self.on_guild_create)
25
+ self.bot.add_event_listener('GUILD_DELETE', self.on_guild_delete)
26
+
27
+ self.bot.add_event_listener('CHANNEL_CREATE', self.on_channel_create)
28
+ self.bot.add_event_listener('CHANNEL_UPDATE', self.on_channel_update)
29
+ self.bot.add_event_listener('CHANNEL_DELETE', self.on_channel_delete)
30
+
31
+ def on_guild_create(self, event: GuildCreateEvent):
32
+ """Append new guild channels to cache. Also add channels to index.
33
+
34
+ Args:
35
+ event (GuildCreateEvent): the GUILD_CREATE event
36
+ """
37
+ guild_dict = self.channels.setdefault(event.id, {})
38
+
39
+ for ch in event.channels:
40
+ guild_dict[ch.id] = ch
41
+ self.channel_index[ch.id] = ch
42
+
43
+ def on_guild_delete(self, event: GuildDeleteEvent):
44
+ """Remove guild channels from cache. Also remove channels from index
45
+
46
+ Args:
47
+ event (GuildDeleteEvent): the GUILD_DELETE event
48
+ """
49
+ removed_channels = self.channels.pop(event.id, {})
50
+
51
+ for ch in removed_channels.values():
52
+ self.channel_index.pop(ch.id, None)
53
+
54
+ def on_channel_create(self, event: GuildChannelCreateEvent):
55
+ """Append channel to guild key. Also append channel to index.
56
+
57
+ Args:
58
+ event (GuildChannelCreateEvent): the CHANNEL_CREATE event
59
+ """
60
+ model = ChannelModel.from_dict(event.raw)
61
+ guild_dict = self.channels.setdefault(event.guild_id, {})
62
+
63
+ guild_dict[event.id] = model
64
+ self.channel_index[event.id] = model
65
+
66
+ def on_channel_update(self, event: GuildChannelUpdateEvent):
67
+ """Replace channel in guild key. Also replace channel in index.
68
+
69
+ Args:
70
+ event (GuildChannelUpdateEvent): the CHANNEL_UPDATE event
71
+ """
72
+ model = ChannelModel.from_dict(event.raw)
73
+ guild_dict = self.channels.setdefault(event.guild_id, {})
74
+
75
+ guild_dict[event.id] = model
76
+ self.channel_index[event.id] = model
77
+
78
+ def on_channel_delete(self, event: GuildChannelDeleteEvent):
79
+ """Remove channel from guild key. Also remove channel from index.
80
+
81
+ Args:
82
+ event (GuildChannelDeleteEvent): the CHANNEL_DELETE event
83
+ """
84
+ model = self.channel_index.pop(event.id, None)
85
+ if model:
86
+ self.channels.get(event.guild_id, {}).pop(event.id, None)
87
+
88
+ def get_channel(self, channel_id: int):
89
+ """Get a channel from the cache.
90
+
91
+ Args:
92
+ channel_id (int): ID of the channel
93
+
94
+ Returns:
95
+ (ChannelModel | None): the channel object if found else None
96
+ """
97
+ return self.channel_index.get(channel_id)
98
+
99
+ def put(self, channel: ChannelModel):
100
+ """Put a new channel into the cache.
101
+
102
+ Args:
103
+ channel (ChannelModel): the channel object
104
+
105
+ Raises:
106
+ ValueError: missing `guild_id`
107
+ """
108
+ if channel.guild_id is None:
109
+ raise ValueError("Cannot cache a channel without a guild_id.")
110
+
111
+ guild_dict = self.channels.setdefault(channel.guild_id, {})
112
+ guild_dict[channel.id] = channel
113
+ self.channel_index[channel.id] = channel
@@ -0,0 +1,92 @@
1
+ from scurrypy import (
2
+ Addon,
3
+ Client,
4
+ Intents,
5
+ EmojiModel,
6
+ GuildCreateEvent, GuildDeleteEvent,
7
+ GuildEmojisUpdateEvent
8
+ )
9
+
10
+ class GuildEmojiCacheAddon(Addon):
11
+ """Defines caching guild emojis and lookup.
12
+
13
+ !!! important
14
+ This cache requires `Intents.GUILD_EMOJIS_AND_STICKERS` to keep up-to-date.
15
+ """
16
+
17
+ def __init__(self, client: Client):
18
+ self.bot = client
19
+
20
+ if not Intents.has(client.intents, Intents.GUILD_EMOJIS_AND_STICKERS):
21
+ raise ValueError("GuildEmojiCache requires Intents.GUILD_EMOJIS_AND_STICKERS for GUILD_EMOJIS_UPDATE event.")
22
+
23
+ self.guild_emojis: dict[int, dict[int, EmojiModel]] = {} # owns emoji objects
24
+ self.guild_emoji_index: dict[int, EmojiModel] = {} # index by ID (reference)
25
+
26
+ client.addons.add(self)
27
+
28
+ def setup(self):
29
+ self.bot.add_startup_hook(self.on_startup)
30
+
31
+ def on_startup(self):
32
+ self.bot.add_event_listener('GUILD_CREATE', self.on_guild_create)
33
+ self.bot.add_event_listener('GUILD_DELETE', self.on_guild_delete)
34
+
35
+ self.bot.add_event_listener('GUILD_EMOJIS_UPDATE', self.on_emojis_update)
36
+
37
+ def on_guild_create(self, event: GuildCreateEvent):
38
+ """Append new guild emojis to cache. Also add emojis to index.
39
+
40
+ Args:
41
+ event (GuildCreateEvent): the GUILD_CREATE event
42
+ """
43
+ guild_dict = self.guild_emojis.setdefault(event.id, {})
44
+
45
+ # event.emojis is already hydrated
46
+ for emoji in event.emojis:
47
+ guild_dict[emoji.id] = emoji
48
+ self.guild_emoji_index[emoji.id] = emoji
49
+
50
+ def on_guild_delete(self, event: GuildDeleteEvent):
51
+ """Remove guild emojis from cache. Also remove emojis from index
52
+
53
+ Args:
54
+ event (GuildDeleteEvent): the GUILD_DELETE event
55
+ """
56
+ removed = self.guild_emojis.pop(event.id, {})
57
+
58
+ for emoji in removed.values():
59
+ self.guild_emoji_index.pop(emoji.id, None)
60
+
61
+ def on_emojis_update(self, event: GuildEmojisUpdateEvent):
62
+ """Refresh guild emojis with new list. Also refresh the index
63
+
64
+ Args:
65
+ event (GuildEmojisUpdateEvent): the GUILD_EMOJIS_UPDATE event
66
+ """
67
+ guild_id = event.guild_id
68
+
69
+ # remove old emojis
70
+ removed = self.guild_emojis.pop(guild_id, {})
71
+
72
+ for emoji in removed.values():
73
+ self.guild_emoji_index.pop(emoji.id, None)
74
+
75
+ # add new emoji set (full replacement)
76
+ guild_dict = self.guild_emojis.setdefault(guild_id, {})
77
+
78
+ for emoji in event.emojis:
79
+ guild_dict[emoji.id] = emoji
80
+ self.guild_emoji_index[emoji.id] = emoji
81
+
82
+ def get_emoji(self, emoji_id: int):
83
+ """Get an emoji from the cache.
84
+
85
+ Args:
86
+ emoji_id (int): ID of the emoji
87
+
88
+ Returns:
89
+ (EmojiModel | None): the Emoji object if found, else None
90
+ """
91
+ return self.guild_emoji_index.get(emoji_id, None)
92
+
@@ -0,0 +1,108 @@
1
+ from scurrypy import (
2
+ Addon,
3
+ Client,
4
+ RoleModel,
5
+ GuildCreateEvent, GuildDeleteEvent,
6
+ RoleCreateEvent, RoleUpdateEvent, RoleDeleteEvent
7
+ )
8
+
9
+ class RoleCacheAddon(Addon):
10
+ """Defines caching guild roles and lookup."""
11
+
12
+ def __init__(self, client: Client):
13
+ self.bot = client
14
+
15
+ self.roles: dict[int, dict[int, RoleModel]] = {} # stores OBJECTS
16
+ self.role_index: dict[int, RoleModel] = {} # stores REFERENCES
17
+
18
+ client.addons.add(self)
19
+
20
+ def setup(self):
21
+ self.bot.add_startup_hook(self.on_startup)
22
+
23
+ def on_startup(self):
24
+ self.bot.add_event_listener('GUILD_CREATE', self.on_guild_create)
25
+ self.bot.add_event_listener('GUILD_DELETE', self.on_guild_delete)
26
+
27
+ self.bot.add_event_listener('ROLE_CREATE', self.on_role_create)
28
+ self.bot.add_event_listener('ROLE_UPDATE', self.on_role_update)
29
+ self.bot.add_event_listener('ROLE_DELETE', self.on_role_delete)
30
+
31
+ def on_guild_create(self, event: GuildCreateEvent):
32
+ """Append new guild roles to cache. Also add roles to index.
33
+
34
+ Args:
35
+ event (GuildCreateEvent): the GUILD_CREATE event
36
+ """
37
+ guild_dict = self.roles.setdefault(event.id, {})
38
+
39
+ for role in event.roles:
40
+ guild_dict[role.id] = role
41
+ self.role_index[role.id] = role
42
+
43
+ def on_guild_delete(self, event: GuildDeleteEvent):
44
+ """Remove guild roles from cache. Also remove roles from index
45
+
46
+ Args:
47
+ event (GuildDeleteEvent): the GUILD_DELETE event
48
+ """
49
+ removed_roles = self.roles.pop(event.id, {})
50
+
51
+ for role in removed_roles.values():
52
+ self.role_index.pop(role.id, None)
53
+
54
+ def on_role_create(self, event: RoleCreateEvent):
55
+ """Append role to guild key. Also append role to index.
56
+
57
+ Args:
58
+ event (RoleCreateEvent): the ROLE_CREATE event
59
+ """
60
+ model = RoleModel.from_dict(event.raw)
61
+ guild_dict = self.roles.setdefault(event.guild_id, {})
62
+
63
+ guild_dict[event.role.id] = model
64
+ self.role_index[event.role.id] = model
65
+
66
+ def on_role_update(self, event: RoleUpdateEvent):
67
+ """Replace role in guild key. Also replace role in index.
68
+
69
+ Args:
70
+ event (RoleUpdateEvent): the ROLE_UPDATE event
71
+ """
72
+ model = RoleModel.from_dict(event.raw)
73
+ guild_dict = self.roles.setdefault(event.guild_id, {})
74
+
75
+ guild_dict[event.role.id] = model
76
+ self.role_index[event.role.id] = model
77
+
78
+ def on_role_delete(self, event: RoleDeleteEvent):
79
+ """Remove role from guild key. Also remove role from index.
80
+
81
+ Args:
82
+ event (RoleDeleteEvent): the ROLE_DELETE event
83
+ """
84
+ model = self.role_index.pop(event.role_id, None)
85
+ if model:
86
+ self.roles.get(event.guild_id, {}).pop(event.role_id, None)
87
+
88
+ def get_role(self, role_id: int):
89
+ """Get a role from the cache.
90
+
91
+ Args:
92
+ role_id (int): ID of the role
93
+
94
+ Returns:
95
+ (RoleModel | None): the role object if found else None
96
+ """
97
+ return self.role_index.get(role_id)
98
+
99
+ def put(self, guild_id: int, role: RoleModel):
100
+ """Put a new role into the cache.
101
+
102
+ Args:
103
+ guild_id (int): guild ID of the role
104
+ role (RoleModel): the role object
105
+ """
106
+ guild_dict = self.roles.setdefault(guild_id, {})
107
+ guild_dict[role.id] = role
108
+ self.role_index[role.id] = role
@@ -0,0 +1,290 @@
1
+ from scurrypy import (
2
+ Addon,
3
+ EmojiModel,
4
+ Button, ButtonStyles,
5
+ ActionRowChild, ActionRowPart,
6
+ SelectOption,
7
+ TextInput, TextInputStyles,
8
+ DefaultValue
9
+ )
10
+
11
+ class ComponentBuilder(Addon):
12
+
13
+ @staticmethod
14
+ def _basic_button(
15
+ style: int,
16
+ custom_id: str,
17
+ label: str = None,
18
+ emoji: str | EmojiModel = None,
19
+ disabled: bool = False
20
+ ):
21
+ if emoji:
22
+ if isinstance(emoji, str):
23
+ emoji = EmojiModel(name=emoji)
24
+ elif not isinstance(emoji, EmojiModel):
25
+ raise TypeError(f"EasyBot.primary expects type str or EmojiModel, got {type(emoji).__name__}")
26
+
27
+ return Button(
28
+ style=style,
29
+ custom_id=custom_id,
30
+ label=label,
31
+ emoji=emoji ,
32
+ disabled=disabled
33
+ )
34
+
35
+ @staticmethod
36
+ def primary(
37
+ custom_id: str,
38
+ label: str = None,
39
+ emoji: str | EmojiModel = None,
40
+ disabled: bool = False
41
+ ):
42
+ """Builds a primary button
43
+
44
+ Args:
45
+ custom_id (str): unique button identifier
46
+ label (str, optional): user-facing label
47
+ emoji (str | EmojiModel, optional): emoji icon as str or EmojiModel if custom
48
+ disabled (bool, optional): Whether the button should be disabled. Defaults to False.
49
+
50
+ Returns:
51
+ (Button): the button object
52
+ """
53
+ return ComponentBuilder._basic_button(ButtonStyles.PRIMARY, custom_id, label, emoji, disabled)
54
+
55
+ @staticmethod
56
+ def secondary(
57
+ custom_id: str,
58
+ label: str = None,
59
+ emoji: str | EmojiModel = None,
60
+ disabled: bool = False
61
+ ):
62
+ """Builds a secondary button
63
+
64
+ Args:
65
+ custom_id (str): unique button identifier
66
+ label (str, optional): user-facing label
67
+ emoji (str | EmojiModel, optional): emoji icon as str or EmojiModel if custom
68
+ disabled (bool, optional): Whether the button should be disabled. Defaults to False.
69
+
70
+ Returns:
71
+ (Button): the button object
72
+ """
73
+ return ComponentBuilder._basic_button(ButtonStyles.SECONDARY, custom_id, label, emoji, disabled)
74
+
75
+ @staticmethod
76
+ def success(
77
+ custom_id: str,
78
+ label: str = None,
79
+ emoji: str | EmojiModel = None,
80
+ disabled: bool = False
81
+ ):
82
+ """Builds a success button
83
+
84
+ Args:
85
+ custom_id (str): unique button identifier
86
+ label (str, optional): user-facing label
87
+ emoji (str | EmojiModel, optional): emoji icon as str or EmojiModel if custom
88
+ disabled (bool, optional): Whether the button should be disabled. Defaults to False.
89
+
90
+ Returns:
91
+ (Button): the button object
92
+ """
93
+ return ComponentBuilder._basic_button(ButtonStyles.SUCCESS, custom_id, label, emoji, disabled)
94
+
95
+ @staticmethod
96
+ def danger(
97
+ custom_id: str,
98
+ label: str = None,
99
+ emoji: str | EmojiModel = None,
100
+ disabled: bool = False
101
+ ):
102
+ """Builds a danger button
103
+
104
+ Args:
105
+ custom_id (str): unique button identifier
106
+ label (str, optional): user-facing label
107
+ emoji (str | EmojiModel, optional): emoji icon as str or EmojiModel if custom
108
+ disabled (bool, optional): Whether the button should be disabled. Defaults to False.
109
+
110
+ Returns:
111
+ (Button): the button object
112
+ """
113
+ return ComponentBuilder._basic_button(ButtonStyles.DANGER, custom_id, label, emoji, disabled)
114
+
115
+ @staticmethod
116
+ def link(
117
+ url: str,
118
+ label: str = None,
119
+ emoji: str | EmojiModel = None,
120
+ disabled: bool = False
121
+ ):
122
+ """Builds a link button
123
+
124
+ Args:
125
+ url (str): button URL to open
126
+ label (str, optional): user-facing label
127
+ emoji (str | EmojiModel, optional): emoji icon as str or EmojiModel if custom
128
+ disabled (bool, optional): Whether the button should be disabled. Defaults to False.
129
+
130
+ Returns:
131
+ (Button): the button object
132
+ """
133
+ btn = ComponentBuilder._basic_button(ButtonStyles.LINK, label, emoji, disabled)
134
+ btn.url = url
135
+ return btn
136
+
137
+ @staticmethod
138
+ def row(components: list[ActionRowChild]):
139
+ """Shorthand for action row.
140
+
141
+ Args:
142
+ components (list[ActionRowChild]): the action row objects.
143
+
144
+ Returns:
145
+ (ActionRowPart): the action row object
146
+ """
147
+ if not isinstance(components, list):
148
+ components = [components]
149
+
150
+ return ActionRowPart(components)
151
+
152
+ @staticmethod
153
+ def option(
154
+ label: str,
155
+ value: str,
156
+ description: str = None,
157
+ emoji: EmojiModel | str = None,
158
+ default: bool = False
159
+ ):
160
+ """Builds a string menu select option.
161
+
162
+ Args:
163
+ label (str): user-facing label
164
+ value (str): unique identifier
165
+ description (str, optional): option descriptor
166
+ emoji (EmojiModel | str, optional): emoji icon as str or EmojiModel if custom
167
+ default (bool, optional): Whether this value should be the default if none is selected. Defaults to False.
168
+
169
+ Raises:
170
+ (TypeError): invalid `emoji` type
171
+
172
+ Returns:
173
+ (SelectOption): the SelectOption object
174
+ """
175
+ if emoji:
176
+ if isinstance(emoji, str):
177
+ emoji = EmojiModel(name=emoji)
178
+ elif not isinstance(emoji, EmojiModel):
179
+ raise TypeError(f"EasyBot.primary expects type str or EmojiModel, got {type(emoji).__name__}")
180
+
181
+ return SelectOption(
182
+ label=label,
183
+ value=value,
184
+ description=description,
185
+ emoji=emoji,
186
+ default=default
187
+ )
188
+
189
+ @staticmethod
190
+ def short_text(
191
+ custom_id: str,
192
+ required: bool = True,
193
+ placeholder: str | None = None,
194
+ min_length: int | None = None,
195
+ max_length: int | None = None
196
+ ):
197
+ """Builds a TextInput with `TextInputStyles.SHORT` style.
198
+
199
+ Args:
200
+ custom_id (str): unique identifier
201
+ required (bool, optional): Whether this field is required. Defaults to True.
202
+ placeholder (str | None, optional): default text if nothing is entered
203
+ min_length (int | None, optional): minimum input length
204
+ max_length (int | None, optional): maximum input length
205
+
206
+ Returns:
207
+ (TextInput): the TextInput object
208
+ """
209
+ return TextInput(
210
+ style=TextInputStyles.SHORT,
211
+ custom_id=custom_id,
212
+ required=required,
213
+ placeholder=placeholder,
214
+ min_length=min_length,
215
+ max_length=max_length
216
+ )
217
+
218
+ @staticmethod
219
+ def long_text(
220
+ custom_id: str,
221
+ required: bool = True,
222
+ placeholder: str | None = None,
223
+ min_length: int | None = None,
224
+ max_length: int | None = None
225
+ ):
226
+ """Builds a TextInput with `TextInputStyles.PARAGRAPH` style.
227
+
228
+ Args:
229
+ custom_id (str): unique identifier
230
+ required (bool, optional): Whether this field is required. Defaults to True.
231
+ placeholder (str | None, optional): default text if nothing is entered
232
+ min_length (int | None, optional): minimum input length
233
+ max_length (int | None, optional): maximum input length
234
+
235
+ Returns:
236
+ (TextInput): the TextInput object
237
+ """
238
+ return TextInput(
239
+ style=TextInputStyles.PARAGRAPH,
240
+ custom_id=custom_id,
241
+ required=required,
242
+ placeholder=placeholder,
243
+ min_length=min_length,
244
+ max_length=max_length
245
+ )
246
+
247
+ @staticmethod
248
+ def role_value(id: int):
249
+ """Builds a default role value.
250
+
251
+ Args:
252
+ id (int): role ID
253
+
254
+ Returns:
255
+ (DefaultValue): the DefaultValue object
256
+ """
257
+ return DefaultValue(
258
+ id=id,
259
+ type='role'
260
+ )
261
+
262
+ @staticmethod
263
+ def user_value(id: int):
264
+ """Builds a default user value.
265
+
266
+ Args:
267
+ id (int): user ID
268
+
269
+ Returns:
270
+ (DefaultValue): the DefaultValue object
271
+ """
272
+ return DefaultValue(
273
+ id=id,
274
+ type='user'
275
+ )
276
+
277
+ @staticmethod
278
+ def channel_value(id: int):
279
+ """Builds a default channel value.
280
+
281
+ Args:
282
+ id (int): channel ID
283
+
284
+ Returns:
285
+ (DefaultValue): the DefaultValue object
286
+ """
287
+ return DefaultValue(
288
+ id=id,
289
+ type='channel'
290
+ )