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.
- requestflow-0.1.0/LICENSE +21 -0
- requestflow-0.1.0/PKG-INFO +207 -0
- requestflow-0.1.0/README.md +172 -0
- requestflow-0.1.0/pyproject.toml +73 -0
- requestflow-0.1.0/setup.cfg +4 -0
- requestflow-0.1.0/src/requestflow/__init__.py +15 -0
- requestflow-0.1.0/src/requestflow/deduplication.py +104 -0
- requestflow-0.1.0/src/requestflow/exceptions.py +43 -0
- requestflow-0.1.0/src/requestflow/executor.py +89 -0
- requestflow-0.1.0/src/requestflow/flow.py +73 -0
- requestflow-0.1.0/src/requestflow/integrations/__init__.py +2 -0
- requestflow-0.1.0/src/requestflow/integrations/discord_py.py +115 -0
- requestflow-0.1.0/src/requestflow/metrics.py +104 -0
- requestflow-0.1.0/src/requestflow/models.py +75 -0
- requestflow-0.1.0/src/requestflow/policies.py +39 -0
- requestflow-0.1.0/src/requestflow/priorities.py +49 -0
- requestflow-0.1.0/src/requestflow/py.typed +1 -0
- requestflow-0.1.0/src/requestflow/queue.py +236 -0
- requestflow-0.1.0/src/requestflow/ratelimit.py +188 -0
- requestflow-0.1.0/src/requestflow/scheduler.py +131 -0
- requestflow-0.1.0/src/requestflow.egg-info/PKG-INFO +207 -0
- requestflow-0.1.0/src/requestflow.egg-info/SOURCES.txt +34 -0
- requestflow-0.1.0/src/requestflow.egg-info/dependency_links.txt +1 -0
- requestflow-0.1.0/src/requestflow.egg-info/requires.txt +8 -0
- requestflow-0.1.0/src/requestflow.egg-info/top_level.txt +1 -0
- requestflow-0.1.0/tests/test_burst_stress.py +61 -0
- requestflow-0.1.0/tests/test_cancellation.py +41 -0
- requestflow-0.1.0/tests/test_deduplication.py +50 -0
- requestflow-0.1.0/tests/test_discord_integration.py +66 -0
- requestflow-0.1.0/tests/test_executor.py +84 -0
- requestflow-0.1.0/tests/test_fairness.py +48 -0
- requestflow-0.1.0/tests/test_lifecycle.py +62 -0
- requestflow-0.1.0/tests/test_metrics.py +50 -0
- requestflow-0.1.0/tests/test_priorities.py +28 -0
- requestflow-0.1.0/tests/test_queue.py +115 -0
- 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
|
+
[](https://pypi.org/project/requestflow/)
|
|
41
|
+
[](https://pypi.org/project/requestflow/)
|
|
42
|
+
[](https://github.com/snipe1337/requestflow)
|
|
43
|
+
[](https://github.com/Rapptz/discord.py)
|
|
44
|
+
[](https://discord.gg/cse)
|
|
45
|
+
[](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
|
+
[](https://pypi.org/project/requestflow/)
|
|
6
|
+
[](https://pypi.org/project/requestflow/)
|
|
7
|
+
[](https://github.com/snipe1337/requestflow)
|
|
8
|
+
[](https://github.com/Rapptz/discord.py)
|
|
9
|
+
[](https://discord.gg/cse)
|
|
10
|
+
[](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,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
|