telethon-floodgate 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.
- telethon_floodgate-0.1.0/.github/workflows/ci.yml +38 -0
- telethon_floodgate-0.1.0/.gitignore +19 -0
- telethon_floodgate-0.1.0/CHANGELOG.md +15 -0
- telethon_floodgate-0.1.0/LICENSE +21 -0
- telethon_floodgate-0.1.0/PKG-INFO +182 -0
- telethon_floodgate-0.1.0/README.md +148 -0
- telethon_floodgate-0.1.0/pyproject.toml +63 -0
- telethon_floodgate-0.1.0/scripts/release_pypi.sh +117 -0
- telethon_floodgate-0.1.0/setup.cfg +4 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/__init__.py +97 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/_datetime.py +37 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/flood_breaker.py +238 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/flood_wait.py +222 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/peer.py +93 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/py.typed +0 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/rate_limit_gate.py +251 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate/rate_limiter.py +134 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate.egg-info/PKG-INFO +182 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate.egg-info/SOURCES.txt +25 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate.egg-info/dependency_links.txt +1 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate.egg-info/requires.txt +10 -0
- telethon_floodgate-0.1.0/src/telethon_floodgate.egg-info/top_level.txt +1 -0
- telethon_floodgate-0.1.0/tests/test_flood_breaker.py +206 -0
- telethon_floodgate-0.1.0/tests/test_flood_wait.py +231 -0
- telethon_floodgate-0.1.0/tests/test_peer.py +76 -0
- telethon_floodgate-0.1.0/tests/test_rate_limit_gate.py +220 -0
- telethon_floodgate-0.1.0/tests/test_resolve_rate_limiter.py +110 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.11"
|
|
16
|
+
- name: Install
|
|
17
|
+
run: pip install -e ".[dev]"
|
|
18
|
+
- name: Ruff
|
|
19
|
+
run: ruff check src tests
|
|
20
|
+
- name: Build + twine check
|
|
21
|
+
run: |
|
|
22
|
+
python -m build
|
|
23
|
+
python -m twine check dist/*
|
|
24
|
+
|
|
25
|
+
tests:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
strategy:
|
|
28
|
+
matrix:
|
|
29
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: ${{ matrix.python-version }}
|
|
35
|
+
- name: Install
|
|
36
|
+
run: pip install -e ".[dev]"
|
|
37
|
+
- name: Pytest
|
|
38
|
+
run: python -m pytest -q
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
Initial release — 1:1 extraction of the flood stack from the tg_content_factory
|
|
6
|
+
project, plus per-peer send limits.
|
|
7
|
+
|
|
8
|
+
- `TelegramRateLimitGate`: proactive per-(phone, category) sliding windows
|
|
9
|
+
plus per-peer send buckets `(phone, category, peer)` — independent from the
|
|
10
|
+
category budget, with `peer_key()` extraction from Telethon entities
|
|
11
|
+
(`peer.py`) and `TelegramPeerRateLimitedError`
|
|
12
|
+
- `FloodCircuitBreaker`: pybreaker-based suspension per (operation, phone)
|
|
13
|
+
- `ResolveRateLimiter`: sliding-window limiter for `auth.resolveUsername`
|
|
14
|
+
- flood-wait helpers: `run_with_flood_wait`, `run_with_flood_wait_retry`,
|
|
15
|
+
transient/blocking classification, sleep helpers, `FloodWaitInfo`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axisrow
|
|
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,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: telethon-floodgate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flood-wait handling, proactive rate-limit gate and circuit breaker for Telethon user accounts
|
|
5
|
+
Author: Axisrow
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/axisrow/telethon-floodgate
|
|
8
|
+
Project-URL: Repository, https://github.com/axisrow/telethon-floodgate
|
|
9
|
+
Project-URL: Changelog, https://github.com/axisrow/telethon-floodgate/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: telegram,telethon,flood-wait,rate-limit,floodwait,mtproto
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Communications :: Chat
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: telethon<2,>=1.36
|
|
25
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
26
|
+
Requires-Dist: pybreaker>=1.4.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=6.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# telethon-floodgate
|
|
36
|
+
|
|
37
|
+
Flood-wait handling for Telethon user accounts: a proactive rate-limit gate, a
|
|
38
|
+
reactive circuit breaker, and flood-wait sleep/retry helpers — three layers
|
|
39
|
+
that keep a working account out of Telegram's multi-hour FLOOD_WAIT bans.
|
|
40
|
+
|
|
41
|
+
Telegram does not publish rate limits. This library is the production flood
|
|
42
|
+
stack of a multi-account Telegram content collector, calibrated against real
|
|
43
|
+
incidents: a fresh session firing 160+ `auth.resolveUsername` calls in seconds
|
|
44
|
+
escalated into 15–18 hour bans, and repeated `getDialogs` sweeps ran a account
|
|
45
|
+
into a 14.8-hour ban. The defaults are deliberately conservative and every
|
|
46
|
+
bucket is configurable.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install telethon-floodgate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires Python 3.11+, [Telethon](https://github.com/LonamiWebs/Telethon) 1.x,
|
|
55
|
+
pydantic 2.x and pybreaker.
|
|
56
|
+
|
|
57
|
+
## The three layers
|
|
58
|
+
|
|
59
|
+
### 1. Proactive rate-limit gate — before the call
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from telethon_floodgate import TelegramRateLimitGate, TelegramRateLimitedError
|
|
63
|
+
|
|
64
|
+
gate = TelegramRateLimitGate() # create once, next to your client pool
|
|
65
|
+
|
|
66
|
+
def before_telegram_call(phone: str, operation: str) -> None:
|
|
67
|
+
category = gate.category_for(operation) # "send", "history", "dialogs", ...
|
|
68
|
+
retry_after = gate.try_acquire(phone, category)
|
|
69
|
+
if retry_after > 0:
|
|
70
|
+
raise TelegramRateLimitedError(phone, category, retry_after)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Buckets are independent sliding windows keyed by `(phone, category)`:
|
|
74
|
+
`dialogs` (1/min — the #1330 incident), `dialog_sweep`, `history`,
|
|
75
|
+
`admin_action`, `send` (30/min), `channel_lifecycle` (3 per 5 min), plus a
|
|
76
|
+
permissive `default`. Override any of them:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from telethon_floodgate import RateLimitSpec, TelegramRateLimitGate
|
|
80
|
+
|
|
81
|
+
gate = TelegramRateLimitGate(
|
|
82
|
+
category_limits={"send": RateLimitSpec(max_calls=10, window_sec=60.0)}
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`resolve` and `reaction` are intentionally no-op categories: in the origin
|
|
87
|
+
project those paths have dedicated limiters (see `ResolveRateLimiter`, also
|
|
88
|
+
shipped here).
|
|
89
|
+
|
|
90
|
+
### Per-peer send limits
|
|
91
|
+
|
|
92
|
+
Telegram throttles *sending* per peer, not just per account: roughly one
|
|
93
|
+
message per second to the same private chat and about twenty per minute into
|
|
94
|
+
the same group or channel. Pass a peer key to `try_acquire` and the gate
|
|
95
|
+
checks a second, independent `(phone, category, peer)` bucket before the
|
|
96
|
+
category one:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from telethon_floodgate import peer_key
|
|
100
|
+
|
|
101
|
+
retry_after = gate.try_acquire(phone, "send", peer=peer_key(entity))
|
|
102
|
+
if retry_after > 0:
|
|
103
|
+
raise TelegramPeerRateLimitedError(phone, peer_key(entity), retry_after)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- A per-peer refusal does **not** consume the account-wide category slot, so a
|
|
107
|
+
burst aimed at one peer cannot burn the account budget.
|
|
108
|
+
- `peer_key()` derives `"user:123"` / `"channel:-100123"` / `"chat:-456"` /
|
|
109
|
+
`"username:durov"` / `"id:123"` from ints, strings, Telethon TL peers, input
|
|
110
|
+
peers and full entities — purely local attribute access, never a network
|
|
111
|
+
round-trip.
|
|
112
|
+
- Only `send:user`, `send:channel` and `send:chat` are configured by default;
|
|
113
|
+
unknown kinds pass through unthrottled. Override or disable via `peer_limits`
|
|
114
|
+
(a `"send:user"` entry with a permissive spec effectively turns it off) and
|
|
115
|
+
cap memory with `peer_max_buckets` (LRU, default 4096).
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
gate = TelegramRateLimitGate(
|
|
119
|
+
peer_limits={"send:user": RateLimitSpec(max_calls=1, window_sec=5.0)},
|
|
120
|
+
peer_max_buckets=4096,
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`TelegramPeerRateLimitedError` subclasses `TelegramRateLimitedError`, so one
|
|
125
|
+
"operation unavailable, move on" handler covers every layer.
|
|
126
|
+
|
|
127
|
+
### 2. Reactive circuit breaker — when Telegram answers anyway
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from telethon_floodgate import FloodCircuitBreaker
|
|
131
|
+
|
|
132
|
+
breaker = FloodCircuitBreaker(threshold=3, cooldown_seconds=300)
|
|
133
|
+
|
|
134
|
+
breaker.check(operation, phone) # raises TelegramOperationSuspendedError
|
|
135
|
+
# while the pair is suspended
|
|
136
|
+
try:
|
|
137
|
+
await client.some_call()
|
|
138
|
+
breaker.record_success(operation, phone)
|
|
139
|
+
except telethon.errors.FloodWaitError:
|
|
140
|
+
breaker.record_flood(operation, phone)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
After `threshold` flood waits on the same `(operation, phone)` the pair is
|
|
144
|
+
suspended for `cooldown_seconds`, then exactly one half-open trial call is
|
|
145
|
+
allowed. `TelegramOperationSuspendedError` subclasses
|
|
146
|
+
`TelegramRateLimitedError`, so a single "operation unavailable, move on"
|
|
147
|
+
handler covers both layers.
|
|
148
|
+
|
|
149
|
+
### 3. Flood-wait helpers — classify, sleep, retry, report
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from telethon_floodgate import (
|
|
153
|
+
HandledFloodWaitError, FloodWaitInfo,
|
|
154
|
+
handle_flood_wait, run_with_flood_wait, run_with_flood_wait_retry,
|
|
155
|
+
is_transient_flood_wait_seconds, is_blocking_flood_wait_until,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# wraps one awaitable: FloodWaitError -> HandledFloodWaitError (+ pool report)
|
|
159
|
+
await run_with_flood_wait(client.get_dialogs(), operation="warm", phone=phone, pool=pool)
|
|
160
|
+
|
|
161
|
+
# retries transient waits (<=60s) under a total budget (120s default)
|
|
162
|
+
await run_with_flood_wait_retry(factory, operation="fetch", phone=phone, pool=pool)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`handle_flood_wait` reports the wait back to the pool via
|
|
166
|
+
`await pool.report_flood(phone, seconds)` so account rotation can skip the
|
|
167
|
+
flooded account — persist that to your own storage; the library is
|
|
168
|
+
storage-agnostic.
|
|
169
|
+
|
|
170
|
+
## Design notes
|
|
171
|
+
|
|
172
|
+
- Everything runs on one event loop; the limiter state is plain in-memory
|
|
173
|
+
deques, no locks, no DB.
|
|
174
|
+
- `try_acquire` never sleeps and never raises — it returns seconds to defer,
|
|
175
|
+
so callers decide whether to reschedule, skip or await.
|
|
176
|
+
- Atomic multi-slot reservations (`try_acquire(..., slots=n)`) are supported
|
|
177
|
+
for compound operations.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT — see [LICENSE](LICENSE). Unofficial third-party library; not affiliated
|
|
182
|
+
with the Telethon project.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# telethon-floodgate
|
|
2
|
+
|
|
3
|
+
Flood-wait handling for Telethon user accounts: a proactive rate-limit gate, a
|
|
4
|
+
reactive circuit breaker, and flood-wait sleep/retry helpers — three layers
|
|
5
|
+
that keep a working account out of Telegram's multi-hour FLOOD_WAIT bans.
|
|
6
|
+
|
|
7
|
+
Telegram does not publish rate limits. This library is the production flood
|
|
8
|
+
stack of a multi-account Telegram content collector, calibrated against real
|
|
9
|
+
incidents: a fresh session firing 160+ `auth.resolveUsername` calls in seconds
|
|
10
|
+
escalated into 15–18 hour bans, and repeated `getDialogs` sweeps ran a account
|
|
11
|
+
into a 14.8-hour ban. The defaults are deliberately conservative and every
|
|
12
|
+
bucket is configurable.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install telethon-floodgate
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Python 3.11+, [Telethon](https://github.com/LonamiWebs/Telethon) 1.x,
|
|
21
|
+
pydantic 2.x and pybreaker.
|
|
22
|
+
|
|
23
|
+
## The three layers
|
|
24
|
+
|
|
25
|
+
### 1. Proactive rate-limit gate — before the call
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from telethon_floodgate import TelegramRateLimitGate, TelegramRateLimitedError
|
|
29
|
+
|
|
30
|
+
gate = TelegramRateLimitGate() # create once, next to your client pool
|
|
31
|
+
|
|
32
|
+
def before_telegram_call(phone: str, operation: str) -> None:
|
|
33
|
+
category = gate.category_for(operation) # "send", "history", "dialogs", ...
|
|
34
|
+
retry_after = gate.try_acquire(phone, category)
|
|
35
|
+
if retry_after > 0:
|
|
36
|
+
raise TelegramRateLimitedError(phone, category, retry_after)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Buckets are independent sliding windows keyed by `(phone, category)`:
|
|
40
|
+
`dialogs` (1/min — the #1330 incident), `dialog_sweep`, `history`,
|
|
41
|
+
`admin_action`, `send` (30/min), `channel_lifecycle` (3 per 5 min), plus a
|
|
42
|
+
permissive `default`. Override any of them:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from telethon_floodgate import RateLimitSpec, TelegramRateLimitGate
|
|
46
|
+
|
|
47
|
+
gate = TelegramRateLimitGate(
|
|
48
|
+
category_limits={"send": RateLimitSpec(max_calls=10, window_sec=60.0)}
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`resolve` and `reaction` are intentionally no-op categories: in the origin
|
|
53
|
+
project those paths have dedicated limiters (see `ResolveRateLimiter`, also
|
|
54
|
+
shipped here).
|
|
55
|
+
|
|
56
|
+
### Per-peer send limits
|
|
57
|
+
|
|
58
|
+
Telegram throttles *sending* per peer, not just per account: roughly one
|
|
59
|
+
message per second to the same private chat and about twenty per minute into
|
|
60
|
+
the same group or channel. Pass a peer key to `try_acquire` and the gate
|
|
61
|
+
checks a second, independent `(phone, category, peer)` bucket before the
|
|
62
|
+
category one:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from telethon_floodgate import peer_key
|
|
66
|
+
|
|
67
|
+
retry_after = gate.try_acquire(phone, "send", peer=peer_key(entity))
|
|
68
|
+
if retry_after > 0:
|
|
69
|
+
raise TelegramPeerRateLimitedError(phone, peer_key(entity), retry_after)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- A per-peer refusal does **not** consume the account-wide category slot, so a
|
|
73
|
+
burst aimed at one peer cannot burn the account budget.
|
|
74
|
+
- `peer_key()` derives `"user:123"` / `"channel:-100123"` / `"chat:-456"` /
|
|
75
|
+
`"username:durov"` / `"id:123"` from ints, strings, Telethon TL peers, input
|
|
76
|
+
peers and full entities — purely local attribute access, never a network
|
|
77
|
+
round-trip.
|
|
78
|
+
- Only `send:user`, `send:channel` and `send:chat` are configured by default;
|
|
79
|
+
unknown kinds pass through unthrottled. Override or disable via `peer_limits`
|
|
80
|
+
(a `"send:user"` entry with a permissive spec effectively turns it off) and
|
|
81
|
+
cap memory with `peer_max_buckets` (LRU, default 4096).
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
gate = TelegramRateLimitGate(
|
|
85
|
+
peer_limits={"send:user": RateLimitSpec(max_calls=1, window_sec=5.0)},
|
|
86
|
+
peer_max_buckets=4096,
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`TelegramPeerRateLimitedError` subclasses `TelegramRateLimitedError`, so one
|
|
91
|
+
"operation unavailable, move on" handler covers every layer.
|
|
92
|
+
|
|
93
|
+
### 2. Reactive circuit breaker — when Telegram answers anyway
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from telethon_floodgate import FloodCircuitBreaker
|
|
97
|
+
|
|
98
|
+
breaker = FloodCircuitBreaker(threshold=3, cooldown_seconds=300)
|
|
99
|
+
|
|
100
|
+
breaker.check(operation, phone) # raises TelegramOperationSuspendedError
|
|
101
|
+
# while the pair is suspended
|
|
102
|
+
try:
|
|
103
|
+
await client.some_call()
|
|
104
|
+
breaker.record_success(operation, phone)
|
|
105
|
+
except telethon.errors.FloodWaitError:
|
|
106
|
+
breaker.record_flood(operation, phone)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
After `threshold` flood waits on the same `(operation, phone)` the pair is
|
|
110
|
+
suspended for `cooldown_seconds`, then exactly one half-open trial call is
|
|
111
|
+
allowed. `TelegramOperationSuspendedError` subclasses
|
|
112
|
+
`TelegramRateLimitedError`, so a single "operation unavailable, move on"
|
|
113
|
+
handler covers both layers.
|
|
114
|
+
|
|
115
|
+
### 3. Flood-wait helpers — classify, sleep, retry, report
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from telethon_floodgate import (
|
|
119
|
+
HandledFloodWaitError, FloodWaitInfo,
|
|
120
|
+
handle_flood_wait, run_with_flood_wait, run_with_flood_wait_retry,
|
|
121
|
+
is_transient_flood_wait_seconds, is_blocking_flood_wait_until,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# wraps one awaitable: FloodWaitError -> HandledFloodWaitError (+ pool report)
|
|
125
|
+
await run_with_flood_wait(client.get_dialogs(), operation="warm", phone=phone, pool=pool)
|
|
126
|
+
|
|
127
|
+
# retries transient waits (<=60s) under a total budget (120s default)
|
|
128
|
+
await run_with_flood_wait_retry(factory, operation="fetch", phone=phone, pool=pool)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`handle_flood_wait` reports the wait back to the pool via
|
|
132
|
+
`await pool.report_flood(phone, seconds)` so account rotation can skip the
|
|
133
|
+
flooded account — persist that to your own storage; the library is
|
|
134
|
+
storage-agnostic.
|
|
135
|
+
|
|
136
|
+
## Design notes
|
|
137
|
+
|
|
138
|
+
- Everything runs on one event loop; the limiter state is plain in-memory
|
|
139
|
+
deques, no locks, no DB.
|
|
140
|
+
- `try_acquire` never sleeps and never raises — it returns seconds to defer,
|
|
141
|
+
so callers decide whether to reschedule, skip or await.
|
|
142
|
+
- Atomic multi-slot reservations (`try_acquire(..., slots=n)`) are supported
|
|
143
|
+
for compound operations.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT — see [LICENSE](LICENSE). Unofficial third-party library; not affiliated
|
|
148
|
+
with the Telethon project.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "telethon-floodgate"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Flood-wait handling, proactive rate-limit gate and circuit breaker for Telethon user accounts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [{name = "Axisrow"}]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
keywords = ["telegram", "telethon", "flood-wait", "rate-limit", "floodwait", "mtproto"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Communications :: Chat",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"telethon>=1.36,<2",
|
|
28
|
+
"pydantic>=2.0,<3",
|
|
29
|
+
"pybreaker>=1.4.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/axisrow/telethon-floodgate"
|
|
34
|
+
Repository = "https://github.com/axisrow/telethon-floodgate"
|
|
35
|
+
Changelog = "https://github.com/axisrow/telethon-floodgate/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"pytest-asyncio>=0.24",
|
|
41
|
+
"ruff>=0.8",
|
|
42
|
+
"build>=1.2",
|
|
43
|
+
"twine>=6.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
telethon_floodgate = ["py.typed"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
target-version = "py311"
|
|
54
|
+
line-length = 120
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "N", "W"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
asyncio_mode = "auto"
|
|
61
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
filterwarnings = ["error"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
ENV_FILE="${ROOT_DIR}/.env"
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<'EOF'
|
|
10
|
+
Usage:
|
|
11
|
+
scripts/release_pypi.sh testpypi
|
|
12
|
+
scripts/release_pypi.sh pypi
|
|
13
|
+
scripts/release_pypi.sh all
|
|
14
|
+
|
|
15
|
+
Behavior:
|
|
16
|
+
- loads .env from the repository root when present
|
|
17
|
+
- rebuilds dist artifacts from scratch
|
|
18
|
+
- runs twine checks before upload
|
|
19
|
+
- uploads to TestPyPI, PyPI, or both
|
|
20
|
+
|
|
21
|
+
Required .env variables:
|
|
22
|
+
TWINE_USERNAME=__token__
|
|
23
|
+
TEST_PYPI_TOKEN=pypi-... # for testpypi/all
|
|
24
|
+
PYPI_TOKEN=pypi-... # for pypi/all
|
|
25
|
+
EOF
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if [[ $# -ne 1 ]]; then
|
|
29
|
+
usage
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
TARGET="$1"
|
|
34
|
+
|
|
35
|
+
case "${TARGET}" in
|
|
36
|
+
testpypi|pypi|all)
|
|
37
|
+
;;
|
|
38
|
+
*)
|
|
39
|
+
usage
|
|
40
|
+
exit 1
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
44
|
+
if [[ -f "${ENV_FILE}" ]]; then
|
|
45
|
+
set -a
|
|
46
|
+
# shellcheck disable=SC1090
|
|
47
|
+
source "${ENV_FILE}"
|
|
48
|
+
set +a
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
TWINE_USERNAME="${TWINE_USERNAME:-__token__}"
|
|
52
|
+
|
|
53
|
+
require_var() {
|
|
54
|
+
local name="$1"
|
|
55
|
+
if [[ -z "${!name:-}" ]]; then
|
|
56
|
+
echo "Missing required environment variable: ${name}" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
require_command() {
|
|
62
|
+
local name="$1"
|
|
63
|
+
if ! command -v "${name}" >/dev/null 2>&1; then
|
|
64
|
+
echo "Required command not found: ${name}" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
build_artifacts() {
|
|
70
|
+
require_command python3
|
|
71
|
+
|
|
72
|
+
echo "Cleaning old build artifacts"
|
|
73
|
+
rm -rf "${ROOT_DIR}/dist" "${ROOT_DIR}/build" "${ROOT_DIR}"/*.egg-info
|
|
74
|
+
|
|
75
|
+
echo "Building package"
|
|
76
|
+
(
|
|
77
|
+
cd "${ROOT_DIR}"
|
|
78
|
+
python3 -m build --no-isolation
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
echo "Checking artifacts with twine"
|
|
82
|
+
(
|
|
83
|
+
cd "${ROOT_DIR}"
|
|
84
|
+
python3 -m twine check dist/*
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
upload_target() {
|
|
89
|
+
local repo_url="$1"
|
|
90
|
+
local password_var="$2"
|
|
91
|
+
local label="$3"
|
|
92
|
+
|
|
93
|
+
require_var "${password_var}"
|
|
94
|
+
|
|
95
|
+
echo "Uploading to ${label}"
|
|
96
|
+
(
|
|
97
|
+
cd "${ROOT_DIR}"
|
|
98
|
+
TWINE_USERNAME="${TWINE_USERNAME}" \
|
|
99
|
+
TWINE_PASSWORD="${!password_var}" \
|
|
100
|
+
python3 -m twine upload --non-interactive --repository-url "${repo_url}" dist/*
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
build_artifacts
|
|
105
|
+
|
|
106
|
+
case "${TARGET}" in
|
|
107
|
+
testpypi)
|
|
108
|
+
upload_target "https://test.pypi.org/legacy/" "TEST_PYPI_TOKEN" "TestPyPI"
|
|
109
|
+
;;
|
|
110
|
+
pypi)
|
|
111
|
+
upload_target "https://upload.pypi.org/legacy/" "PYPI_TOKEN" "PyPI"
|
|
112
|
+
;;
|
|
113
|
+
all)
|
|
114
|
+
upload_target "https://test.pypi.org/legacy/" "TEST_PYPI_TOKEN" "TestPyPI"
|
|
115
|
+
upload_target "https://upload.pypi.org/legacy/" "PYPI_TOKEN" "PyPI"
|
|
116
|
+
;;
|
|
117
|
+
esac
|