hikari-arc 0.4.0__py3-none-any.whl → 0.6.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.
- arc/__init__.py +55 -5
- arc/abc/__init__.py +32 -1
- arc/abc/client.py +207 -67
- arc/abc/command.py +245 -34
- arc/abc/error_handler.py +33 -2
- arc/abc/hookable.py +24 -0
- arc/abc/limiter.py +61 -0
- arc/abc/option.py +73 -5
- arc/abc/plugin.py +185 -29
- arc/client.py +103 -33
- arc/command/message.py +21 -18
- arc/command/option/attachment.py +9 -5
- arc/command/option/bool.py +9 -6
- arc/command/option/channel.py +9 -5
- arc/command/option/float.py +11 -7
- arc/command/option/int.py +11 -7
- arc/command/option/mentionable.py +9 -5
- arc/command/option/role.py +9 -5
- arc/command/option/str.py +11 -7
- arc/command/option/user.py +9 -5
- arc/command/slash.py +222 -197
- arc/command/user.py +20 -17
- arc/context/autocomplete.py +1 -0
- arc/context/base.py +216 -105
- arc/errors.py +52 -10
- arc/events.py +5 -1
- arc/extension.py +23 -0
- arc/internal/about.py +1 -1
- arc/internal/deprecation.py +3 -4
- arc/internal/options.py +106 -0
- arc/internal/sigparse.py +19 -1
- arc/internal/sync.py +13 -10
- arc/internal/types.py +34 -15
- arc/locale.py +28 -0
- arc/plugin.py +56 -5
- arc/utils/__init__.py +53 -2
- arc/utils/hooks/__init__.py +25 -0
- arc/utils/{hooks.py → hooks/basic.py} +28 -1
- arc/utils/hooks/limiters.py +217 -0
- arc/utils/ratelimiter.py +243 -0
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/METADATA +13 -8
- hikari_arc-0.6.0.dist-info/RECORD +52 -0
- hikari_arc-0.4.0.dist-info/RECORD +0 -47
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/LICENSE +0 -0
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/WHEEL +0 -0
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/top_level.txt +0 -0
arc/utils/__init__.py
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
from .hooks import
|
|
1
|
+
from .hooks import (
|
|
2
|
+
LimiterHook,
|
|
3
|
+
bot_has_permissions,
|
|
4
|
+
channel_limiter,
|
|
5
|
+
custom_limiter,
|
|
6
|
+
dm_only,
|
|
7
|
+
global_limiter,
|
|
8
|
+
guild_limiter,
|
|
9
|
+
guild_only,
|
|
10
|
+
has_permissions,
|
|
11
|
+
member_limiter,
|
|
12
|
+
owner_only,
|
|
13
|
+
user_limiter,
|
|
14
|
+
)
|
|
15
|
+
from .ratelimiter import RateLimiter, RateLimiterExhaustedError
|
|
2
16
|
|
|
3
|
-
__all__ = (
|
|
17
|
+
__all__ = (
|
|
18
|
+
"guild_only",
|
|
19
|
+
"owner_only",
|
|
20
|
+
"dm_only",
|
|
21
|
+
"has_permissions",
|
|
22
|
+
"bot_has_permissions",
|
|
23
|
+
"global_limiter",
|
|
24
|
+
"guild_limiter",
|
|
25
|
+
"user_limiter",
|
|
26
|
+
"member_limiter",
|
|
27
|
+
"channel_limiter",
|
|
28
|
+
"custom_limiter",
|
|
29
|
+
"LimiterHook",
|
|
30
|
+
"RateLimiter",
|
|
31
|
+
"RateLimiterExhaustedError",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# MIT License
|
|
35
|
+
#
|
|
36
|
+
# Copyright (c) 2023-present hypergonial
|
|
37
|
+
#
|
|
38
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
39
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
40
|
+
# in the Software without restriction, including without limitation the rights
|
|
41
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
42
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
43
|
+
# furnished to do so, subject to the following conditions:
|
|
44
|
+
#
|
|
45
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
46
|
+
# copies or substantial portions of the Software.
|
|
47
|
+
#
|
|
48
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
49
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
50
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
51
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
52
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
53
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
54
|
+
# SOFTWARE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .basic import bot_has_permissions, dm_only, guild_only, has_permissions, owner_only
|
|
2
|
+
from .limiters import (
|
|
3
|
+
LimiterHook,
|
|
4
|
+
channel_limiter,
|
|
5
|
+
custom_limiter,
|
|
6
|
+
global_limiter,
|
|
7
|
+
guild_limiter,
|
|
8
|
+
member_limiter,
|
|
9
|
+
user_limiter,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
"guild_only",
|
|
14
|
+
"owner_only",
|
|
15
|
+
"dm_only",
|
|
16
|
+
"has_permissions",
|
|
17
|
+
"bot_has_permissions",
|
|
18
|
+
"global_limiter",
|
|
19
|
+
"guild_limiter",
|
|
20
|
+
"channel_limiter",
|
|
21
|
+
"user_limiter",
|
|
22
|
+
"member_limiter",
|
|
23
|
+
"custom_limiter",
|
|
24
|
+
"LimiterHook",
|
|
25
|
+
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import typing as t
|
|
2
4
|
|
|
3
5
|
import hikari
|
|
4
6
|
|
|
5
7
|
from arc.abc.hookable import HookResult
|
|
6
|
-
from arc.context import Context
|
|
7
8
|
from arc.errors import (
|
|
8
9
|
BotMissingPermissionsError,
|
|
9
10
|
DMOnlyError,
|
|
@@ -12,6 +13,9 @@ from arc.errors import (
|
|
|
12
13
|
NotOwnerError,
|
|
13
14
|
)
|
|
14
15
|
|
|
16
|
+
if t.TYPE_CHECKING:
|
|
17
|
+
from arc.context import Context
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
def guild_only(ctx: Context[t.Any]) -> HookResult:
|
|
17
21
|
"""A pre-execution hook that aborts the execution of a command if it is invoked outside of a guild.
|
|
@@ -127,3 +131,26 @@ def bot_has_permissions(perms: hikari.Permissions) -> t.Callable[[Context[t.Any]
|
|
|
127
131
|
This hook requires the command to be invoked in a guild, and implies the [`guild_only`][arc.utils.hooks.guild_only] hook.
|
|
128
132
|
"""
|
|
129
133
|
return lambda ctx: _bot_has_permissions(ctx, perms)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# MIT License
|
|
137
|
+
#
|
|
138
|
+
# Copyright (c) 2023-present hypergonial
|
|
139
|
+
#
|
|
140
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
141
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
142
|
+
# in the Software without restriction, including without limitation the rights
|
|
143
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
144
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
145
|
+
# furnished to do so, subject to the following conditions:
|
|
146
|
+
#
|
|
147
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
148
|
+
# copies or substantial portions of the Software.
|
|
149
|
+
#
|
|
150
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
151
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
152
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
153
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
154
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
155
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
156
|
+
# SOFTWARE.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from arc.abc.limiter import LimiterProto
|
|
6
|
+
from arc.context.base import Context
|
|
7
|
+
from arc.errors import UnderCooldownError
|
|
8
|
+
from arc.internal.types import ClientT
|
|
9
|
+
from arc.utils.ratelimiter import RateLimiter, RateLimiterExhaustedError
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"LimiterHook",
|
|
13
|
+
"global_limiter",
|
|
14
|
+
"guild_limiter",
|
|
15
|
+
"channel_limiter",
|
|
16
|
+
"user_limiter",
|
|
17
|
+
"member_limiter",
|
|
18
|
+
"custom_limiter",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LimiterHook(RateLimiter[Context[ClientT]], LimiterProto[ClientT]):
|
|
23
|
+
"""The default implementation of a ratelimiter that can be used as a hook.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
period : float
|
|
28
|
+
The period, in seconds, after which the bucket resets.
|
|
29
|
+
limit : int
|
|
30
|
+
The amount of requests allowed in a bucket.
|
|
31
|
+
get_key_with : t.Callable[[Context[t.Any]], str]
|
|
32
|
+
A callable that returns a key for the ratelimiter bucket.
|
|
33
|
+
|
|
34
|
+
See Also
|
|
35
|
+
--------
|
|
36
|
+
- [`global_limiter`][arc.utils.hooks.limiters.global_limiter]
|
|
37
|
+
- [`guild_limiter`][arc.utils.hooks.limiters.guild_limiter]
|
|
38
|
+
- [`channel_limiter`][arc.utils.hooks.limiters.channel_limiter]
|
|
39
|
+
- [`user_limiter`][arc.utils.hooks.limiters.user_limiter]
|
|
40
|
+
- [`member_limiter`][arc.utils.hooks.limiters.member_limiter]
|
|
41
|
+
- [`custom_limiter`][arc.utils.hooks.limiters.custom_limiter]
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
async def acquire(self, ctx: Context[ClientT], *, wait: bool = True) -> None:
|
|
45
|
+
"""Acquire a bucket, block execution if ratelimited and wait is True.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
ctx : Context[t.Any]
|
|
50
|
+
The context to evaluate the ratelimit under.
|
|
51
|
+
wait : bool
|
|
52
|
+
Determines if this call should block in
|
|
53
|
+
case of hitting a ratelimit.
|
|
54
|
+
|
|
55
|
+
Raises
|
|
56
|
+
------
|
|
57
|
+
UnderCooldownError
|
|
58
|
+
If the ratelimiter is ratelimited and wait is False.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
return await super().acquire(ctx, wait=wait)
|
|
62
|
+
except RateLimiterExhaustedError as exc:
|
|
63
|
+
raise UnderCooldownError(
|
|
64
|
+
self, exc.retry_after, f"Command is under cooldown for '{exc.retry_after}' seconds."
|
|
65
|
+
) from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def global_limiter(period: float, limit: int) -> LimiterHook[t.Any]:
|
|
69
|
+
"""Create a global ratelimiter.
|
|
70
|
+
|
|
71
|
+
This ratelimiter is shared across all invocation contexts.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
period : float
|
|
76
|
+
The period, in seconds, after which the bucket resets.
|
|
77
|
+
limit : int
|
|
78
|
+
The amount of requests allowed in a bucket.
|
|
79
|
+
|
|
80
|
+
Usage
|
|
81
|
+
-----
|
|
82
|
+
```py
|
|
83
|
+
@arc.with_hook(arc.global_limiter(5.0, 1))
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
return LimiterHook(period, limit, get_key_with=lambda _: "amongus")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def guild_limiter(period: float, limit: int) -> LimiterHook[t.Any]:
|
|
90
|
+
"""Create a guild ratelimiter.
|
|
91
|
+
|
|
92
|
+
This ratelimiter is shared across all contexts in a guild.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
period : float
|
|
97
|
+
The period, in seconds, after which the bucket resets.
|
|
98
|
+
limit : int
|
|
99
|
+
The amount of requests allowed in a bucket.
|
|
100
|
+
|
|
101
|
+
Usage
|
|
102
|
+
-----
|
|
103
|
+
```py
|
|
104
|
+
@arc.with_hook(arc.guild_limiter(5.0, 1))
|
|
105
|
+
```
|
|
106
|
+
"""
|
|
107
|
+
return LimiterHook(period, limit, get_key_with=lambda ctx: str(ctx.guild_id))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def channel_limiter(period: float, limit: int) -> LimiterHook[t.Any]:
|
|
111
|
+
"""Create a channel ratelimiter.
|
|
112
|
+
|
|
113
|
+
This ratelimiter is shared across all contexts in a channel.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
period : float
|
|
118
|
+
The period, in seconds, after which the bucket resets.
|
|
119
|
+
limit : int
|
|
120
|
+
The amount of requests allowed in a bucket.
|
|
121
|
+
|
|
122
|
+
Usage
|
|
123
|
+
-----
|
|
124
|
+
```py
|
|
125
|
+
@arc.with_hook(arc.channel_limiter(5.0, 1))
|
|
126
|
+
```
|
|
127
|
+
"""
|
|
128
|
+
return LimiterHook(period, limit, get_key_with=lambda ctx: str(ctx.channel_id))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def user_limiter(period: float, limit: int) -> LimiterHook[t.Any]:
|
|
132
|
+
"""Create a user ratelimiter.
|
|
133
|
+
|
|
134
|
+
This ratelimiter is shared across all contexts by a user.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
period : float
|
|
139
|
+
The period, in seconds, after which the bucket resets.
|
|
140
|
+
limit : int
|
|
141
|
+
The amount of requests allowed in a bucket.
|
|
142
|
+
|
|
143
|
+
Usage
|
|
144
|
+
-----
|
|
145
|
+
```py
|
|
146
|
+
@arc.with_hook(arc.user_limiter(5.0, 1))
|
|
147
|
+
```
|
|
148
|
+
"""
|
|
149
|
+
return LimiterHook(period, limit, get_key_with=lambda ctx: str(ctx.author.id))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def member_limiter(period: float, limit: int) -> LimiterHook[t.Any]:
|
|
153
|
+
"""Create a member ratelimiter.
|
|
154
|
+
|
|
155
|
+
This ratelimiter is shared across all contexts by a member in a guild.
|
|
156
|
+
The same user in a different guild will be assigned a different bucket.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
period : float
|
|
161
|
+
The period, in seconds, after which the bucket resets.
|
|
162
|
+
limit : int
|
|
163
|
+
The amount of requests allowed in a bucket.
|
|
164
|
+
|
|
165
|
+
Usage
|
|
166
|
+
-----
|
|
167
|
+
```py
|
|
168
|
+
@arc.with_hook(arc.member_limiter(5.0, 1))
|
|
169
|
+
```
|
|
170
|
+
"""
|
|
171
|
+
return LimiterHook(period, limit, get_key_with=lambda ctx: f"{ctx.author.id}:{ctx.guild_id}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def custom_limiter(period: float, limit: int, get_key_with: t.Callable[[Context[t.Any]], str]) -> LimiterHook[t.Any]:
|
|
175
|
+
"""Create a ratelimiter with a custom key extraction function.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
period : float
|
|
180
|
+
The period, in seconds, after which the bucket resets.
|
|
181
|
+
limit : int
|
|
182
|
+
The amount of requests allowed in a bucket.
|
|
183
|
+
get_key_with : t.Callable[[Context[t.Any]], str]
|
|
184
|
+
A callable that returns a key for the ratelimiter bucket. This key is used to identify the bucket.
|
|
185
|
+
For instance, to create a ratelimiter that is shared across all contexts in a guild,
|
|
186
|
+
you would use `lambda ctx: str(ctx.guild_id)`.
|
|
187
|
+
|
|
188
|
+
Usage
|
|
189
|
+
-----
|
|
190
|
+
```py
|
|
191
|
+
@arc.with_hook(arc.custom_limiter(5.0, 1, lambda ctx: str(ctx.guild_id)))
|
|
192
|
+
```
|
|
193
|
+
"""
|
|
194
|
+
return LimiterHook(period, limit, get_key_with=get_key_with)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# MIT License
|
|
198
|
+
#
|
|
199
|
+
# Copyright (c) 2023-present hypergonial
|
|
200
|
+
#
|
|
201
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
202
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
203
|
+
# in the Software without restriction, including without limitation the rights
|
|
204
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
205
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
206
|
+
# furnished to do so, subject to the following conditions:
|
|
207
|
+
#
|
|
208
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
209
|
+
# copies or substantial portions of the Software.
|
|
210
|
+
#
|
|
211
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
212
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
213
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
214
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
215
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
216
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
217
|
+
# SOFTWARE.
|
arc/utils/ratelimiter.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
import typing as t
|
|
8
|
+
from collections import deque
|
|
9
|
+
|
|
10
|
+
import attr
|
|
11
|
+
|
|
12
|
+
from arc.abc.hookable import HookResult
|
|
13
|
+
|
|
14
|
+
KeyT = t.TypeVar("KeyT")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@attr.define(slots=True, kw_only=True)
|
|
18
|
+
class _Bucket(t.Generic[KeyT]):
|
|
19
|
+
"""Handles the ratelimiting of a single item. (E.g. a single user or a channel)."""
|
|
20
|
+
|
|
21
|
+
key: str
|
|
22
|
+
"""The key of the bucket."""
|
|
23
|
+
|
|
24
|
+
reset_at: float
|
|
25
|
+
"""The time at which the bucket resets."""
|
|
26
|
+
|
|
27
|
+
limiter: RateLimiter[KeyT]
|
|
28
|
+
"""The limiter this bucket belongs to."""
|
|
29
|
+
|
|
30
|
+
_remaining: int = attr.field(alias="remaining")
|
|
31
|
+
"""The amount of requests remaining until the bucket is exhausted."""
|
|
32
|
+
|
|
33
|
+
_queue: deque[asyncio.Event] = attr.field(factory=deque, init=False)
|
|
34
|
+
"""A list of events to set as the iter task proceeds."""
|
|
35
|
+
|
|
36
|
+
_task: asyncio.Task[None] | None = attr.field(default=None, init=False)
|
|
37
|
+
"""The task that is currently iterating over the queue."""
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def for_limiter(cls, key: str, limiter: RateLimiter[KeyT]) -> _Bucket[KeyT]:
|
|
41
|
+
"""Create a new bucket for a RateLimiter."""
|
|
42
|
+
return cls(key=key, limiter=limiter, reset_at=time.monotonic() + limiter.period, remaining=limiter.limit)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def remaining(self) -> int:
|
|
46
|
+
"""The amount of requests remaining until the bucket is exhausted."""
|
|
47
|
+
if self.reset_at <= time.monotonic():
|
|
48
|
+
self.reset()
|
|
49
|
+
return self._remaining
|
|
50
|
+
|
|
51
|
+
@remaining.setter
|
|
52
|
+
def remaining(self, value: int) -> None:
|
|
53
|
+
self._remaining = value
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_exhausted(self) -> bool:
|
|
57
|
+
"""Return a boolean determining if the bucket is exhausted."""
|
|
58
|
+
return self.remaining <= 0 and self.reset_at > time.monotonic()
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_stale(self) -> bool:
|
|
62
|
+
"""Return a boolean determining if the bucket is stale.
|
|
63
|
+
If a bucket is stale, it is no longer in use and can be purged.
|
|
64
|
+
"""
|
|
65
|
+
return not self._queue and self.remaining == self.limiter.limit and (self._task is None or self._task.done())
|
|
66
|
+
|
|
67
|
+
def start_queue(self) -> None:
|
|
68
|
+
"""Start the queue of a bucket.
|
|
69
|
+
This will start setting events in the queue until the bucket is ratelimited.
|
|
70
|
+
"""
|
|
71
|
+
if self._task is None or self._task.done():
|
|
72
|
+
self._task = asyncio.create_task(self._iter_queue())
|
|
73
|
+
|
|
74
|
+
def reset(self) -> None:
|
|
75
|
+
"""Reset the bucket."""
|
|
76
|
+
self.reset_at = time.monotonic() + self.limiter.period
|
|
77
|
+
self._remaining = self.limiter.limit
|
|
78
|
+
|
|
79
|
+
async def _iter_queue(self) -> None:
|
|
80
|
+
"""Iterate over the queue and set events until exhausted."""
|
|
81
|
+
try:
|
|
82
|
+
while self._queue:
|
|
83
|
+
if self.remaining <= 0 and self.reset_at > time.monotonic():
|
|
84
|
+
# Sleep until ratelimit expires
|
|
85
|
+
await asyncio.sleep(self.reset_at - time.monotonic())
|
|
86
|
+
self.reset()
|
|
87
|
+
|
|
88
|
+
# Set events while not ratelimited
|
|
89
|
+
while self.remaining > 0 and self._queue:
|
|
90
|
+
self._queue.popleft().set()
|
|
91
|
+
self._remaining -= 1
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print(f"Task Exception was never retrieved: {e}", file=sys.stderr)
|
|
95
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RateLimiter(t.Generic[KeyT]):
|
|
99
|
+
"""A fixed window ratelimiter implementation.
|
|
100
|
+
|
|
101
|
+
It is generic over the type of object it extracts the key from (`KeyT`), and must also be given a key extraction function.
|
|
102
|
+
It can be used independently for any purpose, or can be used as a hook via [`LimiterHook`][arc.utils.hooks.limiters.LimiterHook].
|
|
103
|
+
|
|
104
|
+
!!! tip
|
|
105
|
+
If you're looking to use this ratelimiter implementation as a hook, see [`LimiterHook`][arc.utils.hooks.limiters.LimiterHook]
|
|
106
|
+
along with all the other built-in limiter hooks.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
period : float
|
|
111
|
+
The period, in seconds, after which the bucket resets.
|
|
112
|
+
limit : int
|
|
113
|
+
The amount of requests allowed in a bucket.
|
|
114
|
+
get_key_with : t.Callable[[Context[t.Any]], str]
|
|
115
|
+
A callable that returns a key for the ratelimiter bucket.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
__slots__ = ("period", "limit", "_buckets", "_get_key", "_gc_task")
|
|
119
|
+
|
|
120
|
+
def __init__(self, period: float, limit: int, *, get_key_with: t.Callable[[KeyT], str]) -> None:
|
|
121
|
+
self.period: float = period
|
|
122
|
+
self.limit: int = limit
|
|
123
|
+
self._buckets: t.Dict[str, _Bucket[KeyT]] = {}
|
|
124
|
+
self._get_key: t.Callable[[KeyT], str] = get_key_with
|
|
125
|
+
self._gc_task: asyncio.Task[None] | None = None
|
|
126
|
+
|
|
127
|
+
def get_key(self, ctx: KeyT) -> str:
|
|
128
|
+
"""Get key for ratelimiter bucket."""
|
|
129
|
+
return self._get_key(ctx)
|
|
130
|
+
|
|
131
|
+
def is_rate_limited(self, ctx: KeyT) -> bool:
|
|
132
|
+
"""Returns a boolean determining if the ratelimiter is ratelimited or not.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
ctx : Context[t.Any]
|
|
137
|
+
The context to evaluate the ratelimit under.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
bool
|
|
142
|
+
A boolean determining if the ratelimiter is ratelimited or not.
|
|
143
|
+
"""
|
|
144
|
+
now = time.monotonic()
|
|
145
|
+
|
|
146
|
+
if data := self._buckets.get(self.get_key(ctx)):
|
|
147
|
+
if data.reset_at <= now:
|
|
148
|
+
return False
|
|
149
|
+
return data._remaining <= 0
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def _start_gc(self) -> None:
|
|
153
|
+
"""Start the garbage collector task if one is not running."""
|
|
154
|
+
if self._gc_task is None or self._gc_task.done():
|
|
155
|
+
self._gc_task = asyncio.create_task(self._gc())
|
|
156
|
+
|
|
157
|
+
async def _gc(self) -> None:
|
|
158
|
+
"""Purge stale buckets."""
|
|
159
|
+
while self._buckets:
|
|
160
|
+
await asyncio.sleep(self.period + 1.0)
|
|
161
|
+
for bucket in list(self._buckets.values()):
|
|
162
|
+
if bucket.is_stale:
|
|
163
|
+
del self._buckets[bucket.key]
|
|
164
|
+
|
|
165
|
+
async def acquire(self, ctx: KeyT, *, wait: bool = True) -> None:
|
|
166
|
+
"""Acquire a bucket, block execution if ratelimited and wait is True.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
ctx : Context[t.Any]
|
|
171
|
+
The context to evaluate the ratelimit under.
|
|
172
|
+
wait : bool
|
|
173
|
+
Determines if this call should block in
|
|
174
|
+
case of hitting a ratelimit.
|
|
175
|
+
|
|
176
|
+
Raises
|
|
177
|
+
------
|
|
178
|
+
RateLimiterExhaustedError
|
|
179
|
+
If the ratelimiter is ratelimited and wait is False.
|
|
180
|
+
"""
|
|
181
|
+
event = asyncio.Event()
|
|
182
|
+
|
|
183
|
+
key = self.get_key(ctx)
|
|
184
|
+
# Get existing or insert new bucket
|
|
185
|
+
bucket = self._buckets.setdefault(key, _Bucket.for_limiter(key, self))
|
|
186
|
+
|
|
187
|
+
if bucket.is_exhausted and not wait:
|
|
188
|
+
raise RateLimiterExhaustedError(
|
|
189
|
+
self,
|
|
190
|
+
bucket.reset_at - time.monotonic(),
|
|
191
|
+
f"Ratelimited for {bucket.reset_at - time.monotonic()} seconds.",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
bucket._queue.append(event)
|
|
195
|
+
bucket.start_queue()
|
|
196
|
+
self._start_gc()
|
|
197
|
+
|
|
198
|
+
if wait:
|
|
199
|
+
await event.wait()
|
|
200
|
+
|
|
201
|
+
async def __call__(self, ctx: KeyT) -> HookResult:
|
|
202
|
+
"""Acquire a ratelimit, fail if ratelimited.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
ctx : Context[t.Any]
|
|
207
|
+
The context to evaluate the ratelimit under.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
HookResult
|
|
212
|
+
A hook result to conform to the limiter protocol.
|
|
213
|
+
|
|
214
|
+
Raises
|
|
215
|
+
------
|
|
216
|
+
UnderCooldownError
|
|
217
|
+
If the ratelimiter is ratelimited.
|
|
218
|
+
"""
|
|
219
|
+
await self.acquire(ctx, wait=False)
|
|
220
|
+
return HookResult()
|
|
221
|
+
|
|
222
|
+
def reset(self, ctx: KeyT) -> None:
|
|
223
|
+
"""Reset the ratelimit for a given context."""
|
|
224
|
+
if bucket := self._buckets.get(self.get_key(ctx)):
|
|
225
|
+
bucket.reset()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class RateLimiterExhaustedError(Exception):
|
|
229
|
+
"""Raised when a [RateLimiter][arc.utils.ratelimiter.RateLimiter] is acquired while it is exhausted, and the
|
|
230
|
+
`wait` parameter is set to `False`.
|
|
231
|
+
|
|
232
|
+
Attributes
|
|
233
|
+
----------
|
|
234
|
+
retry_after : float
|
|
235
|
+
The amount of time in seconds until the command is off cooldown.
|
|
236
|
+
limiter : arc.abc.limiter.LimiterProto
|
|
237
|
+
The limiter that was rate limited.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(self, limiter: RateLimiter[t.Any], retry_after: float, *args: t.Any) -> None:
|
|
241
|
+
self.retry_after = retry_after
|
|
242
|
+
self.limiter = limiter
|
|
243
|
+
super().__init__(*args)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hikari-arc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A command handler for hikari with a focus on type-safety and correctness.
|
|
5
5
|
Home-page: https://github.com/hypergonial/hikari-arc
|
|
6
6
|
Author: hypergonial
|
|
@@ -29,12 +29,17 @@ Requires-Dist: attrs >=23.1
|
|
|
29
29
|
Requires-Dist: colorama ; sys_platform=="win32"
|
|
30
30
|
Provides-Extra: dev
|
|
31
31
|
Requires-Dist: ruff ==0.1.11 ; extra == 'dev'
|
|
32
|
-
Requires-Dist: pyright ==1.1.
|
|
32
|
+
Requires-Dist: pyright ==1.1.345 ; extra == 'dev'
|
|
33
33
|
Requires-Dist: nox ==2023.4.22 ; extra == 'dev'
|
|
34
|
+
Requires-Dist: typing-extensions ==4.9.0 ; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest ==7.4.4 ; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio ==0.23.3 ; extra == 'dev'
|
|
34
37
|
Provides-Extra: docs
|
|
35
38
|
Requires-Dist: mkdocs-material[imaging] ~=9.5.3 ; extra == 'docs'
|
|
36
39
|
Requires-Dist: mkdocs ~=1.5.3 ; extra == 'docs'
|
|
37
|
-
Requires-Dist: mkdocstrings-python ~=1.
|
|
40
|
+
Requires-Dist: mkdocstrings-python ~=1.8.0 ; extra == 'docs'
|
|
41
|
+
Requires-Dist: black ~=23.12.1 ; extra == 'docs'
|
|
42
|
+
Requires-Dist: griffe-inherited-docstrings ~=1.0.0 ; extra == 'docs'
|
|
38
43
|
Provides-Extra: rest
|
|
39
44
|
Requires-Dist: hikari[server] >=2.0.0.dev122 ; extra == 'rest'
|
|
40
45
|
|
|
@@ -81,10 +86,10 @@ bot = hikari.GatewayBot("TOKEN") # or hikari.RESTBot
|
|
|
81
86
|
client = arc.GatewayClient(bot) # or arc.RESTClient
|
|
82
87
|
|
|
83
88
|
@client.include
|
|
84
|
-
@arc.slash_command(
|
|
89
|
+
@arc.slash_command("hi", "Say hi!")
|
|
85
90
|
async def ping(
|
|
86
91
|
ctx: arc.GatewayContext,
|
|
87
|
-
user: arc.Option[hikari.User, arc.UserParams(
|
|
92
|
+
user: arc.Option[hikari.User, arc.UserParams("The user to say hi to.")]
|
|
88
93
|
) -> None:
|
|
89
94
|
await ctx.respond(f"Hey {user.mention}!")
|
|
90
95
|
|
|
@@ -105,9 +110,9 @@ See [Contributing](./CONTRIBUTING.md)
|
|
|
105
110
|
`arc` is in large part a combination of all the parts I like in other command handlers, with my own spin on it. The following projects have inspired me and aided me greatly in the design of this library:
|
|
106
111
|
|
|
107
112
|
- [`hikari-lightbulb`](https://github.com/tandemdude/hikari-lightbulb) - The library initially started as a reimagination of lightbulb, it inherits a similar project structure and terminology.
|
|
108
|
-
- [`Tanjun`](https://github.com/FasterSpeeding/Tanjun) - For the idea of using `typing.Annotated` and dependency injection in a command handler. `arc` also uses the same dependency injection library, [`Alluka`](https://github.com/FasterSpeeding/Alluka), under the hood.
|
|
109
|
-
- [`hikari-crescent`](https://github.com/hikari-crescent/hikari-crescent) The design of hooks is largely inspired by `crescent`.
|
|
110
|
-
- [`FastAPI`](https://github.com/tiangolo/fastapi) - Some design ideas and most of the documentation configuration derives from `FastAPI`.
|
|
113
|
+
- [`Tanjun`](https://github.com/FasterSpeeding/Tanjun) - For the idea of using `typing.Annotated` and [dependency injection](https://arc.hypergonial.com/guides/dependency_injection/) in a command handler. `arc` also uses the same dependency injection library, [`Alluka`](https://github.com/FasterSpeeding/Alluka), under the hood.
|
|
114
|
+
- [`hikari-crescent`](https://github.com/hikari-crescent/hikari-crescent) The design of [hooks](https://arc.hypergonial.com/guides/hooks/) is largely inspired by `crescent`.
|
|
115
|
+
- [`FastAPI`](https://github.com/tiangolo/fastapi) - Some design ideas and most of the [documentation](https://arc.hypergonial.com/) [configuration](https://github.com/hypergonial/hikari-arc/blob/main/mkdocs.yml) derives from `FastAPI`.
|
|
111
116
|
|
|
112
117
|
## Links
|
|
113
118
|
|