requestflow 0.1.0__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 (36) hide show
  1. requestflow-0.1.0/LICENSE +21 -0
  2. requestflow-0.1.0/PKG-INFO +207 -0
  3. requestflow-0.1.0/README.md +172 -0
  4. requestflow-0.1.0/pyproject.toml +73 -0
  5. requestflow-0.1.0/setup.cfg +4 -0
  6. requestflow-0.1.0/src/requestflow/__init__.py +15 -0
  7. requestflow-0.1.0/src/requestflow/deduplication.py +104 -0
  8. requestflow-0.1.0/src/requestflow/exceptions.py +43 -0
  9. requestflow-0.1.0/src/requestflow/executor.py +89 -0
  10. requestflow-0.1.0/src/requestflow/flow.py +73 -0
  11. requestflow-0.1.0/src/requestflow/integrations/__init__.py +2 -0
  12. requestflow-0.1.0/src/requestflow/integrations/discord_py.py +115 -0
  13. requestflow-0.1.0/src/requestflow/metrics.py +104 -0
  14. requestflow-0.1.0/src/requestflow/models.py +75 -0
  15. requestflow-0.1.0/src/requestflow/policies.py +39 -0
  16. requestflow-0.1.0/src/requestflow/priorities.py +49 -0
  17. requestflow-0.1.0/src/requestflow/py.typed +1 -0
  18. requestflow-0.1.0/src/requestflow/queue.py +236 -0
  19. requestflow-0.1.0/src/requestflow/ratelimit.py +188 -0
  20. requestflow-0.1.0/src/requestflow/scheduler.py +131 -0
  21. requestflow-0.1.0/src/requestflow.egg-info/PKG-INFO +207 -0
  22. requestflow-0.1.0/src/requestflow.egg-info/SOURCES.txt +34 -0
  23. requestflow-0.1.0/src/requestflow.egg-info/dependency_links.txt +1 -0
  24. requestflow-0.1.0/src/requestflow.egg-info/requires.txt +8 -0
  25. requestflow-0.1.0/src/requestflow.egg-info/top_level.txt +1 -0
  26. requestflow-0.1.0/tests/test_burst_stress.py +61 -0
  27. requestflow-0.1.0/tests/test_cancellation.py +41 -0
  28. requestflow-0.1.0/tests/test_deduplication.py +50 -0
  29. requestflow-0.1.0/tests/test_discord_integration.py +66 -0
  30. requestflow-0.1.0/tests/test_executor.py +84 -0
  31. requestflow-0.1.0/tests/test_fairness.py +48 -0
  32. requestflow-0.1.0/tests/test_lifecycle.py +62 -0
  33. requestflow-0.1.0/tests/test_metrics.py +50 -0
  34. requestflow-0.1.0/tests/test_priorities.py +28 -0
  35. requestflow-0.1.0/tests/test_queue.py +115 -0
  36. requestflow-0.1.0/tests/test_ratelimit.py +89 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RequestFlow Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: requestflow
3
+ Version: 0.1.0
4
+ Summary: stop getting 429 ratelimited like an amateur. high-performance http scheduling and pacing for discord.py.
5
+ Author: requestflow
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/snipe1337/requestflow
8
+ Project-URL: Issues, https://github.com/snipe1337/requestflow/issues
9
+ Project-URL: Community, https://discord.gg/cse
10
+ Project-URL: Support, https://discord.gg/cse
11
+ Keywords: discord,discord.py,http,ratelimit,queue,scheduler,asyncio,performance
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: discord.py>=2.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
31
+ Requires-Dist: coverage>=7.0.0; extra == "dev"
32
+ Requires-Dist: setuptools>=61.0.0; extra == "dev"
33
+ Requires-Dist: wheel; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # requestflow
37
+
38
+ > stop getting 429 ratelimited like an amateur. intelligent http scheduling and pacing for discord.py.
39
+
40
+ [![pypi](https://img.shields.io/pypi/v/requestflow?color=black&style=flat-square)](https://pypi.org/project/requestflow/)
41
+ [![python](https://img.shields.io/badge/python-3.11+-black?style=flat-square)](https://pypi.org/project/requestflow/)
42
+ [![stars](https://img.shields.io/github/stars/snipe1337/requestflow?style=flat-square&color=black)](https://github.com/snipe1337/requestflow)
43
+ [![discord.py](https://img.shields.io/badge/discord.py-v2-black?style=flat-square)](https://github.com/Rapptz/discord.py)
44
+ [![community](https://img.shields.io/badge/community-discord.gg%2Fcse-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/cse)
45
+ [![license](https://img.shields.io/badge/license-MIT-black?style=flat-square)](LICENSE)
46
+
47
+ ---
48
+
49
+ ### why this exists
50
+
51
+ when your bot grows, commands collide, background loops spam discord, and unmanaged `asyncio.gather` blasts 300 requests in 10 milliseconds.
52
+
53
+ discord hits back with `HTTP 429: Global rate limit hit`. your whole bot freezes for everyone. commands fail. messages get dropped.
54
+
55
+ **requestflow fixes this.** it sits between discord.py and the discord api, intelligently pacing your requests with gcra rate-limiting, cross-guild fairness, and priority queuing.
56
+
57
+ ### raw discord.py vs requestflow (real 300-channel burst test)
58
+
59
+ ```text
60
+ raw discord.py (asyncio.gather):
61
+ - requested: 300
62
+ - created: 240
63
+ - dropped/failed: 60 (20% failure rate)
64
+ - terminal: spammed dozens of "WARNING Global rate limit hit. Retrying in 0.30s"
65
+
66
+ requestflow:
67
+ - requested: 300
68
+ - created: 300 (100% success rate)
69
+ - dropped/failed: 0
70
+ - terminal: dead silent. zero 429s. zero bans.
71
+ ```
72
+
73
+ ---
74
+
75
+ ### install
76
+
77
+ ```bash
78
+ pip install requestflow
79
+ ```
80
+
81
+ ---
82
+
83
+ ### quickstart (2 lines)
84
+
85
+ just attach it to your bot. every single `channel.send()`, `message.delete()`, cog, and slash command is automatically scheduled, paced, and protected.
86
+
87
+ ```python
88
+ import discord
89
+ from discord.ext import commands
90
+ from requestflow import RequestFlow
91
+
92
+ bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
93
+
94
+ # 1. initialize requestflow
95
+ flow = RequestFlow(bot, global_rate_limit=40.0, max_concurrency=16)
96
+
97
+ @bot.event
98
+ async def setup_hook():
99
+ # 2. start the engine
100
+ await flow.start()
101
+
102
+ @bot.command()
103
+ async def ping(ctx):
104
+ # automatically prioritized and paced without changing your code
105
+ await ctx.send("pong")
106
+
107
+ bot.run("TOKEN")
108
+ ```
109
+
110
+ ---
111
+
112
+ ### or use managed priorities
113
+
114
+ want your slash commands and moderation actions to jump ahead of massive background sweeps?
115
+
116
+ ```python
117
+ from requestflow import RequestFlow, Priority
118
+
119
+ async with RequestFlow() as flow:
120
+ # critical interaction responses jump to the front of the queue
121
+ await flow.submit(
122
+ interaction.response.send_message,
123
+ "fast response",
124
+ priority=Priority.CRITICAL,
125
+ is_interaction=True
126
+ )
127
+
128
+ # background audits run smoothly without blocking users
129
+ await flow.submit(
130
+ guild.fetch_members,
131
+ limit=1000,
132
+ priority=Priority.LOW,
133
+ guild_id=guild.id
134
+ )
135
+ ```
136
+
137
+ ---
138
+
139
+ ### features
140
+
141
+ - **gcra rate pacing**: smooths request bursts before discord gets angry (defaults to 40 req/s, 50 req/s ceiling).
142
+ - **zero 429 stampedes**: eliminates the thundering herd problem when rate limits unlock.
143
+ - **cross-guild fairness**: deficit round-robin ensures one spammy server never starves your other servers.
144
+ - **priority queues**: critical slash commands and moderator bans bypass background syncs.
145
+ - **starvation prevention**: low-priority jobs age up over time so they never get stuck forever.
146
+ - **in-flight deduplication**: coalesces duplicate reads so identical queries only hit discord once.
147
+ - **clean metrics**: real-time p50/p95 latency and queue stats with zero external telemetry.
148
+
149
+ ---
150
+
151
+ ### live metrics
152
+
153
+ inspect what your bot is doing anytime:
154
+
155
+ ```python
156
+ print(flow.stats)
157
+ ```
158
+
159
+ ```text
160
+ MetricsSnapshot(
161
+ submitted=300,
162
+ completed=300,
163
+ failed=0,
164
+ queue_wait_p50=5.0ms,
165
+ exec_latency_p50=32.0ms,
166
+ active_guilds=1,
167
+ rate_limit_429_count=0
168
+ )
169
+ ```
170
+
171
+ ---
172
+
173
+ ### test it yourself
174
+
175
+ run the included stress test bot against your own discord server:
176
+
177
+ ```bash
178
+ python stress_bot.py --token YOUR_TOKEN
179
+ ```
180
+
181
+ in discord:
182
+ - `!burst_raw 350` — watch raw discord.py drop requests and spam 429 warnings
183
+ - `!cleanup` — clear the test channels
184
+ - `!burst_flow 350` — watch requestflow create all 200 with 100% success and 0 errors
185
+ - `!stats` — see real-time latency and queue percentiles
186
+
187
+ ---
188
+
189
+ ### community & dev
190
+
191
+ need help, got feature ideas, or building massive bots? join us:
192
+
193
+ <div align="center">
194
+ <a href="https://discord.gg/cse">
195
+ <img src="https://lanyard.cnrad.dev/api/1420036436505268244?theme=dark&borderRadius=10px&idleMessage=chilling%20with%20requestflow" alt="discord status" />
196
+ </a>
197
+ <br/><br/>
198
+ <a href="https://discord.gg/cse">
199
+ <img src="https://img.shields.io/badge/discord-join%20cse%20community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="cse discord" />
200
+ </a>
201
+ </div>
202
+
203
+ ---
204
+
205
+ ### license
206
+
207
+ MIT
@@ -0,0 +1,172 @@
1
+ # requestflow
2
+
3
+ > stop getting 429 ratelimited like an amateur. intelligent http scheduling and pacing for discord.py.
4
+
5
+ [![pypi](https://img.shields.io/pypi/v/requestflow?color=black&style=flat-square)](https://pypi.org/project/requestflow/)
6
+ [![python](https://img.shields.io/badge/python-3.11+-black?style=flat-square)](https://pypi.org/project/requestflow/)
7
+ [![stars](https://img.shields.io/github/stars/snipe1337/requestflow?style=flat-square&color=black)](https://github.com/snipe1337/requestflow)
8
+ [![discord.py](https://img.shields.io/badge/discord.py-v2-black?style=flat-square)](https://github.com/Rapptz/discord.py)
9
+ [![community](https://img.shields.io/badge/community-discord.gg%2Fcse-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/cse)
10
+ [![license](https://img.shields.io/badge/license-MIT-black?style=flat-square)](LICENSE)
11
+
12
+ ---
13
+
14
+ ### why this exists
15
+
16
+ when your bot grows, commands collide, background loops spam discord, and unmanaged `asyncio.gather` blasts 300 requests in 10 milliseconds.
17
+
18
+ discord hits back with `HTTP 429: Global rate limit hit`. your whole bot freezes for everyone. commands fail. messages get dropped.
19
+
20
+ **requestflow fixes this.** it sits between discord.py and the discord api, intelligently pacing your requests with gcra rate-limiting, cross-guild fairness, and priority queuing.
21
+
22
+ ### raw discord.py vs requestflow (real 300-channel burst test)
23
+
24
+ ```text
25
+ raw discord.py (asyncio.gather):
26
+ - requested: 300
27
+ - created: 240
28
+ - dropped/failed: 60 (20% failure rate)
29
+ - terminal: spammed dozens of "WARNING Global rate limit hit. Retrying in 0.30s"
30
+
31
+ requestflow:
32
+ - requested: 300
33
+ - created: 300 (100% success rate)
34
+ - dropped/failed: 0
35
+ - terminal: dead silent. zero 429s. zero bans.
36
+ ```
37
+
38
+ ---
39
+
40
+ ### install
41
+
42
+ ```bash
43
+ pip install requestflow
44
+ ```
45
+
46
+ ---
47
+
48
+ ### quickstart (2 lines)
49
+
50
+ just attach it to your bot. every single `channel.send()`, `message.delete()`, cog, and slash command is automatically scheduled, paced, and protected.
51
+
52
+ ```python
53
+ import discord
54
+ from discord.ext import commands
55
+ from requestflow import RequestFlow
56
+
57
+ bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
58
+
59
+ # 1. initialize requestflow
60
+ flow = RequestFlow(bot, global_rate_limit=40.0, max_concurrency=16)
61
+
62
+ @bot.event
63
+ async def setup_hook():
64
+ # 2. start the engine
65
+ await flow.start()
66
+
67
+ @bot.command()
68
+ async def ping(ctx):
69
+ # automatically prioritized and paced without changing your code
70
+ await ctx.send("pong")
71
+
72
+ bot.run("TOKEN")
73
+ ```
74
+
75
+ ---
76
+
77
+ ### or use managed priorities
78
+
79
+ want your slash commands and moderation actions to jump ahead of massive background sweeps?
80
+
81
+ ```python
82
+ from requestflow import RequestFlow, Priority
83
+
84
+ async with RequestFlow() as flow:
85
+ # critical interaction responses jump to the front of the queue
86
+ await flow.submit(
87
+ interaction.response.send_message,
88
+ "fast response",
89
+ priority=Priority.CRITICAL,
90
+ is_interaction=True
91
+ )
92
+
93
+ # background audits run smoothly without blocking users
94
+ await flow.submit(
95
+ guild.fetch_members,
96
+ limit=1000,
97
+ priority=Priority.LOW,
98
+ guild_id=guild.id
99
+ )
100
+ ```
101
+
102
+ ---
103
+
104
+ ### features
105
+
106
+ - **gcra rate pacing**: smooths request bursts before discord gets angry (defaults to 40 req/s, 50 req/s ceiling).
107
+ - **zero 429 stampedes**: eliminates the thundering herd problem when rate limits unlock.
108
+ - **cross-guild fairness**: deficit round-robin ensures one spammy server never starves your other servers.
109
+ - **priority queues**: critical slash commands and moderator bans bypass background syncs.
110
+ - **starvation prevention**: low-priority jobs age up over time so they never get stuck forever.
111
+ - **in-flight deduplication**: coalesces duplicate reads so identical queries only hit discord once.
112
+ - **clean metrics**: real-time p50/p95 latency and queue stats with zero external telemetry.
113
+
114
+ ---
115
+
116
+ ### live metrics
117
+
118
+ inspect what your bot is doing anytime:
119
+
120
+ ```python
121
+ print(flow.stats)
122
+ ```
123
+
124
+ ```text
125
+ MetricsSnapshot(
126
+ submitted=300,
127
+ completed=300,
128
+ failed=0,
129
+ queue_wait_p50=5.0ms,
130
+ exec_latency_p50=32.0ms,
131
+ active_guilds=1,
132
+ rate_limit_429_count=0
133
+ )
134
+ ```
135
+
136
+ ---
137
+
138
+ ### test it yourself
139
+
140
+ run the included stress test bot against your own discord server:
141
+
142
+ ```bash
143
+ python stress_bot.py --token YOUR_TOKEN
144
+ ```
145
+
146
+ in discord:
147
+ - `!burst_raw 350` — watch raw discord.py drop requests and spam 429 warnings
148
+ - `!cleanup` — clear the test channels
149
+ - `!burst_flow 350` — watch requestflow create all 200 with 100% success and 0 errors
150
+ - `!stats` — see real-time latency and queue percentiles
151
+
152
+ ---
153
+
154
+ ### community & dev
155
+
156
+ need help, got feature ideas, or building massive bots? join us:
157
+
158
+ <div align="center">
159
+ <a href="https://discord.gg/cse">
160
+ <img src="https://lanyard.cnrad.dev/api/1420036436505268244?theme=dark&borderRadius=10px&idleMessage=chilling%20with%20requestflow" alt="discord status" />
161
+ </a>
162
+ <br/><br/>
163
+ <a href="https://discord.gg/cse">
164
+ <img src="https://img.shields.io/badge/discord-join%20cse%20community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="cse discord" />
165
+ </a>
166
+ </div>
167
+
168
+ ---
169
+
170
+ ### license
171
+
172
+ MIT
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "requestflow"
7
+ version = "0.1.0"
8
+ description = "stop getting 429 ratelimited like an amateur. high-performance http scheduling and pacing for discord.py."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "requestflow" }
14
+ ]
15
+ keywords = [
16
+ "discord",
17
+ "discord.py",
18
+ "http",
19
+ "ratelimit",
20
+ "queue",
21
+ "scheduler",
22
+ "asyncio",
23
+ "performance"
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 5 - Production/Stable",
27
+ "Framework :: AsyncIO",
28
+ "Intended Audience :: Developers",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Topic :: Communications :: Chat",
35
+ "Topic :: Internet :: WWW/HTTP",
36
+ "Topic :: Software Development :: Libraries :: Python Modules",
37
+ "Typing :: Typed"
38
+ ]
39
+ dependencies = [
40
+ "discord.py>=2.0.0"
41
+ ]
42
+
43
+ [project.optional-dependencies]
44
+ dev = [
45
+ "pytest>=8.0.0",
46
+ "pytest-asyncio>=0.23.0",
47
+ "coverage>=7.0.0",
48
+ "setuptools>=61.0.0",
49
+ "wheel"
50
+ ]
51
+
52
+ [project.urls]
53
+ Repository = "https://github.com/snipe1337/requestflow"
54
+ Issues = "https://github.com/snipe1337/requestflow/issues"
55
+ Community = "https://discord.gg/cse"
56
+ Support = "https://discord.gg/cse"
57
+
58
+ [tool.setuptools.packages.find]
59
+ where = ["src"]
60
+
61
+ [tool.setuptools.package-data]
62
+ requestflow = ["py.typed"]
63
+
64
+ [tool.pytest.ini_options]
65
+ minversion = "8.0"
66
+ testpaths = ["tests"]
67
+ pythonpath = ["src"]
68
+ asyncio_mode = "auto"
69
+ asyncio_default_fixture_loop_scope = "function"
70
+ filterwarnings = [
71
+ "ignore::DeprecationWarning",
72
+ "ignore::UserWarning"
73
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from requestflow.deduplication import InFlightDeduplicator, ResultCache
2
+ from requestflow.exceptions import IntegrationError, OperationCancelledError, QueueFullError, RateLimitExceededError, RequestFlowError, RequestTimeoutError, SchedulerClosedError, SchedulerNotStartedError, UnsupportedDiscordVersionError
3
+ from requestflow.executor import RequestExecutor
4
+ from requestflow.flow import RequestFlow
5
+ from requestflow.integrations.discord_py import DiscordHTTPAdapter, attach_to_bot
6
+ from requestflow.metrics import RequestMetrics
7
+ from requestflow.models import MetricsSnapshot, RequestItem
8
+ from requestflow.policies import OverloadPolicy, PacingStrategy, RetryPolicy
9
+ from requestflow.priorities import Priority
10
+ from requestflow.queue import RequestQueue
11
+ from requestflow.ratelimit import GlobalRateLimiter, RateLimitCoordinator, RouteBucketCoordinator
12
+ from requestflow.scheduler import RequestScheduler
13
+ __version__ = '0.1.0'
14
+ __all__ = ['__version__', 'requestflow', 'priority', 'overloadpolicy', 'pacingstrategy', 'retrypolicy', 'requestmetrics', 'metricssnapshot', 'requestitem', 'requestscheduler', 'requestqueue', 'requestexecutor', 'ratelimitcoordinator', 'globalratelimiter', 'routebucketcoordinator',
15
+ 'inflightdeduplicator', 'resultcache', 'discordhttpadapter', 'attach_to_bot', 'requestflowerror', 'queuefullerror', 'schedulerclosederror', 'schedulernotstartederror', 'requesttimeouterror', 'ratelimitexceedederror', 'integrationerror', 'unsupporteddiscordversionerror', 'operationcancellederror']
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ import collections
3
+ import time
4
+ from typing import Any, Callable, Coroutine, Dict, Generic, List, Optional, Tuple, TypeVar
5
+ T = TypeVar('t')
6
+
7
+
8
+ class InFlightDeduplicator:
9
+ def __init__(self) -> None:
10
+ self._waiters: Dict[str, List[asyncio.Future]] = {}
11
+ self._tasks: Dict[str, asyncio.Task] = {}
12
+ self._lock: asyncio.Lock = asyncio.Lock()
13
+
14
+ async def execute_or_join(self, dedup_key: str, runner: Callable[[], Coroutine[Any, Any, T]]) -> T:
15
+ loop = asyncio.get_running_loop()
16
+ caller_future: asyncio.Future = loop.create_future()
17
+ async with self._lock:
18
+ if dedup_key in self._waiters:
19
+ self._waiters[dedup_key].append(caller_future)
20
+ is_leader = False
21
+ else:
22
+ self._waiters[dedup_key] = [caller_future]
23
+ is_leader = True
24
+ task = asyncio.create_task(self._run_leader(dedup_key, runner))
25
+ self._tasks[dedup_key] = task
26
+ try:
27
+ return await caller_future
28
+ except asyncio.CancelledError:
29
+ async with self._lock:
30
+ if dedup_key in self._waiters:
31
+ waiters = self._waiters[dedup_key]
32
+ if caller_future in waiters:
33
+ waiters.remove(caller_future)
34
+ if len(waiters) == 0 and dedup_key in self._tasks:
35
+ task = self._tasks.pop(dedup_key, None)
36
+ if task and (not task.done()):
37
+ task.cancel()
38
+ self._waiters.pop(dedup_key, None)
39
+ raise
40
+
41
+ async def _run_leader(self, dedup_key: str, runner: Callable[[], Coroutine[Any, Any, T]]) -> None:
42
+ result: Any = None
43
+ error: Optional[BaseException] = None
44
+ try:
45
+ result = await runner()
46
+ except asyncio.CancelledError as exc:
47
+ error = exc
48
+ except BaseException as exc:
49
+ error = exc
50
+ async with self._lock:
51
+ waiters = self._waiters.pop(dedup_key, [])
52
+ self._tasks.pop(dedup_key, None)
53
+ for fut in waiters:
54
+ if not fut.done():
55
+ if error is not None:
56
+ if isinstance(error, asyncio.CancelledError):
57
+ fut.cancel()
58
+ else:
59
+ fut.set_exception(error)
60
+ else:
61
+ fut.set_result(result)
62
+
63
+
64
+ class ResultCache(Generic[T]):
65
+ def __init__(self, max_capacity: int = 1000, default_ttl_seconds: float = 60.0) -> None:
66
+ if max_capacity <= 0:
67
+ raise ValueError('max_capacity must be > 0')
68
+ self._max_capacity: int = max_capacity
69
+ self._default_ttl: float = default_ttl_seconds
70
+ self._entries: collections.OrderedDict[str,
71
+ Tuple[T, float]] = collections.OrderedDict()
72
+ self._lock: asyncio.Lock = asyncio.Lock()
73
+
74
+ async def get(self, key: str) -> Optional[T]:
75
+ async with self._lock:
76
+ if key not in self._entries:
77
+ return None
78
+ val, expiry = self._entries[key]
79
+ if time.monotonic() > expiry:
80
+ del self._entries[key]
81
+ return None
82
+ self._entries.move_to_end(key)
83
+ return val
84
+
85
+ async def set(self, key: str, value: T, ttl_seconds: Optional[float] = None) -> None:
86
+ ttl = ttl_seconds if ttl_seconds is not None else self._default_ttl
87
+ expiry = time.monotonic() + ttl
88
+ async with self._lock:
89
+ if key in self._entries:
90
+ del self._entries[key]
91
+ elif len(self._entries) >= self._max_capacity:
92
+ self._entries.popitem(last=False)
93
+ self._entries[key] = (value, expiry)
94
+
95
+ async def invalidate(self, key: str) -> bool:
96
+ async with self._lock:
97
+ if key in self._entries:
98
+ del self._entries[key]
99
+ return True
100
+ return False
101
+
102
+ async def clear(self) -> None:
103
+ async with self._lock:
104
+ self._entries.clear()
@@ -0,0 +1,43 @@
1
+ from typing import Optional
2
+
3
+
4
+ class RequestFlowError(Exception):
5
+ pass
6
+
7
+
8
+ class QueueFullError(RequestFlowError):
9
+ pass
10
+
11
+
12
+ class SchedulerClosedError(RequestFlowError):
13
+ pass
14
+
15
+
16
+ class SchedulerNotStartedError(RequestFlowError):
17
+ pass
18
+
19
+
20
+ class RequestTimeoutError(RequestFlowError):
21
+ def __init__(self, message: str, *, timeout_seconds: float) -> None:
22
+ super().__init__(message)
23
+ self.timeout_seconds: float = timeout_seconds
24
+
25
+
26
+ class RateLimitExceededError(RequestFlowError):
27
+ def __init__(self, message: str, *, retry_after: Optional[float] = None) -> None:
28
+ super().__init__(message)
29
+ self.retry_after: Optional[float] = retry_after
30
+
31
+
32
+ class IntegrationError(RequestFlowError):
33
+ pass
34
+
35
+
36
+ class UnsupportedDiscordVersionError(IntegrationError):
37
+ def __init__(self, message: str, *, detected_version: Optional[str] = None) -> None:
38
+ super().__init__(message)
39
+ self.detected_version: Optional[str] = detected_version
40
+
41
+
42
+ class OperationCancelledError(RequestFlowError):
43
+ pass