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.
Files changed (46) hide show
  1. arc/__init__.py +55 -5
  2. arc/abc/__init__.py +32 -1
  3. arc/abc/client.py +207 -67
  4. arc/abc/command.py +245 -34
  5. arc/abc/error_handler.py +33 -2
  6. arc/abc/hookable.py +24 -0
  7. arc/abc/limiter.py +61 -0
  8. arc/abc/option.py +73 -5
  9. arc/abc/plugin.py +185 -29
  10. arc/client.py +103 -33
  11. arc/command/message.py +21 -18
  12. arc/command/option/attachment.py +9 -5
  13. arc/command/option/bool.py +9 -6
  14. arc/command/option/channel.py +9 -5
  15. arc/command/option/float.py +11 -7
  16. arc/command/option/int.py +11 -7
  17. arc/command/option/mentionable.py +9 -5
  18. arc/command/option/role.py +9 -5
  19. arc/command/option/str.py +11 -7
  20. arc/command/option/user.py +9 -5
  21. arc/command/slash.py +222 -197
  22. arc/command/user.py +20 -17
  23. arc/context/autocomplete.py +1 -0
  24. arc/context/base.py +216 -105
  25. arc/errors.py +52 -10
  26. arc/events.py +5 -1
  27. arc/extension.py +23 -0
  28. arc/internal/about.py +1 -1
  29. arc/internal/deprecation.py +3 -4
  30. arc/internal/options.py +106 -0
  31. arc/internal/sigparse.py +19 -1
  32. arc/internal/sync.py +13 -10
  33. arc/internal/types.py +34 -15
  34. arc/locale.py +28 -0
  35. arc/plugin.py +56 -5
  36. arc/utils/__init__.py +53 -2
  37. arc/utils/hooks/__init__.py +25 -0
  38. arc/utils/{hooks.py → hooks/basic.py} +28 -1
  39. arc/utils/hooks/limiters.py +217 -0
  40. arc/utils/ratelimiter.py +243 -0
  41. {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/METADATA +13 -8
  42. hikari_arc-0.6.0.dist-info/RECORD +52 -0
  43. hikari_arc-0.4.0.dist-info/RECORD +0 -47
  44. {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/LICENSE +0 -0
  45. {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/WHEEL +0 -0
  46. {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 bot_has_permissions, dm_only, guild_only, has_permissions, owner_only
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__ = ("guild_only", "owner_only", "dm_only", "has_permissions", "bot_has_permissions")
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.
@@ -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.4.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.344 ; extra == 'dev'
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.7.5 ; extra == 'docs'
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(name="hi", description="Say hi!")
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(description="The user to say hi to.")]
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