pyroflow 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.
- pyroflow-0.1.0/LICENSE +21 -0
- pyroflow-0.1.0/PKG-INFO +271 -0
- pyroflow-0.1.0/README.md +237 -0
- pyroflow-0.1.0/pyproject.toml +78 -0
- pyroflow-0.1.0/pyroflow/__init__.py +14 -0
- pyroflow-0.1.0/pyroflow/__meta__.py +1 -0
- pyroflow-0.1.0/pyroflow/client.py +413 -0
- pyroflow-0.1.0/pyroflow/dispatcher.py +503 -0
- pyroflow-0.1.0/pyroflow/enums.py +10 -0
- pyroflow-0.1.0/pyroflow/errors.py +20 -0
- pyroflow-0.1.0/pyroflow/listener_coordinator/__init__.py +11 -0
- pyroflow-0.1.0/pyroflow/listener_coordinator/listener_coordinator.py +151 -0
- pyroflow-0.1.0/pyroflow/listener_coordinator/memory_listener_coordinator.py +74 -0
- pyroflow-0.1.0/pyroflow/listener_coordinator/redis_listener_coordinator.py +280 -0
- pyroflow-0.1.0/pyroflow/models.py +64 -0
- pyroflow-0.1.0/pyroflow/types.py +304 -0
- pyroflow-0.1.0/pyroflow/typings.py +15 -0
- pyroflow-0.1.0/pyroflow/update_coordinated/__init__.py +11 -0
- pyroflow-0.1.0/pyroflow/update_coordinated/callback_query_coordinated.py +31 -0
- pyroflow-0.1.0/pyroflow/update_coordinated/message_coordinated.py +38 -0
- pyroflow-0.1.0/pyroflow/update_coordinated/update_coordinated.py +204 -0
- pyroflow-0.1.0/pyroflow/update_coordinator/__init__.py +11 -0
- pyroflow-0.1.0/pyroflow/update_coordinator/memory_update_coordinator.py +43 -0
- pyroflow-0.1.0/pyroflow/update_coordinator/redis_update_coordinator.py +54 -0
- pyroflow-0.1.0/pyroflow/update_coordinator/update_coordinator.py +118 -0
- pyroflow-0.1.0/pyroflow/update_history/__init__.py +12 -0
- pyroflow-0.1.0/pyroflow/update_history/callback_query_history.py +56 -0
- pyroflow-0.1.0/pyroflow/update_history/message_history.py +6 -0
- pyroflow-0.1.0/pyroflow/update_history/update_history.py +306 -0
- pyroflow-0.1.0/pyroflow/update_history_store/__init__.py +9 -0
- pyroflow-0.1.0/pyroflow/update_history_store/memory_update_history_store.py +117 -0
- pyroflow-0.1.0/pyroflow/update_history_store/update_history_store.py +93 -0
- pyroflow-0.1.0/pyroflow/update_listener/__init__.py +11 -0
- pyroflow-0.1.0/pyroflow/update_listener/callback_query_listener.py +82 -0
- pyroflow-0.1.0/pyroflow/update_listener/message_listener.py +48 -0
- pyroflow-0.1.0/pyroflow/update_listener/update_listener.py +391 -0
- pyroflow-0.1.0/pyroflow/utils/__init__.py +19 -0
- pyroflow-0.1.0/pyroflow/utils/async_tools.py +220 -0
- pyroflow-0.1.0/pyroflow/utils/classes.py +100 -0
- pyroflow-0.1.0/pyroflow/utils/enums.py +15 -0
- pyroflow-0.1.0/pyroflow/utils/iter_tools.py +30 -0
- pyroflow-0.1.0/pyroflow/utils/misc_tools.py +67 -0
- pyroflow-0.1.0/pyroflow/utils/typings.py +88 -0
- pyroflow-0.1.0/pyroflow/utils/validate_tools.py +30 -0
pyroflow-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abdullah (github.com/eeeob)
|
|
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.
|
pyroflow-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyroflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Conversation-oriented Pyrogram extension with per-update listeners and multi-session coordination
|
|
5
|
+
Project-URL: Homepage, https://github.com/eeeob/pyroflow
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/eeeob/pyroflow/issues
|
|
7
|
+
Author-email: Abdullah <aldheeb01@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: async,bot,conversation,coordinator,listener,pyrogram,telegram
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Communications :: Chat
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: cachetools
|
|
27
|
+
Requires-Dist: kurigram>=2.2.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: hatch<=1.16.5; extra == 'dev'
|
|
30
|
+
Requires-Dist: twine<=6.2.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: redis
|
|
32
|
+
Requires-Dist: redis>=4.2.0; extra == 'redis'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# pyroflow
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/pyroflow/)
|
|
38
|
+
[](https://pypi.org/project/pyroflow/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
**Conversation-oriented Pyrogram extension with per-update listeners and multi-session coordination.**
|
|
42
|
+
|
|
43
|
+
pyroflow builds on top of [Pyrogram](https://github.com/pyrogram/pyrogram) / [Kurigram](https://github.com/KurimuzonAkuma/pyrogram) to replace the handler-based model with a conversation-first API — `await` a specific reply from a specific user instead of wiring up global handlers and managing state machines by hand.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pip install pyroflow
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For Redis-backed coordination:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pip install pyroflow[redis]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Why pyroflow?
|
|
58
|
+
|
|
59
|
+
Pyrogram fires your handler for **every** incoming update of a given type. The moment you need a back-and-forth conversation you end up writing state machines, storing `user_id → step` in a dict, and hoping two updates don't race each other.
|
|
60
|
+
|
|
61
|
+
pyroflow solves all three problems:
|
|
62
|
+
|
|
63
|
+
| Problem | pyroflow solution |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Waiting for a specific reply | `UpdateListener` — `await` the next update from a user |
|
|
66
|
+
| Duplicate processing across multiple bot sessions | `UpdateCoordinated` — distributed lock per update |
|
|
67
|
+
| Replaying or inspecting previous handler steps | `UpdateHistory` — per-update-type handler record |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
**Minimum requirements:** Python 3.9+
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
pip install pyroflow # core
|
|
77
|
+
pip install pyroflow[redis] # + Redis coordinator support
|
|
78
|
+
pip install pyroflow[dev] # + development tools (hatch, twine)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from pyroflow import Client, MessageListener
|
|
87
|
+
|
|
88
|
+
client = Client("my_session")
|
|
89
|
+
client.register_listener(MessageListener())
|
|
90
|
+
|
|
91
|
+
@client.on_message()
|
|
92
|
+
async def on_start(client, message):
|
|
93
|
+
if message.text != "/start":
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
answer = await message.ask(
|
|
97
|
+
chat_id=message.chat.id,
|
|
98
|
+
text="What is your name?",
|
|
99
|
+
listen_user_id=message.from_user.id,
|
|
100
|
+
timeout=60,
|
|
101
|
+
)
|
|
102
|
+
await answer.reply(f"Hello, {answer.text}!")
|
|
103
|
+
|
|
104
|
+
client.run()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Core concepts
|
|
110
|
+
|
|
111
|
+
### Listeners
|
|
112
|
+
|
|
113
|
+
A `UpdateListener` is a typed queue bound to a Pyrogram update type. Any coroutine can `await` the next matching update from a specific user or chat. An update claimed by a listener **never reaches the normal handler pipeline**.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from pyroflow import Client, MessageListener
|
|
117
|
+
from pyroflow.errors import ListenerTimeout
|
|
118
|
+
|
|
119
|
+
client = Client("my_session")
|
|
120
|
+
client.register_listener(MessageListener())
|
|
121
|
+
|
|
122
|
+
@client.on_message()
|
|
123
|
+
async def on_confirm(client, message):
|
|
124
|
+
if message.text != "/confirm":
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
await message.reply("Send your confirmation code:")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
code_msg = await client.message_listen(
|
|
131
|
+
chat_id=message.chat.id,
|
|
132
|
+
user_id=message.from_user.id,
|
|
133
|
+
timeout=120,
|
|
134
|
+
)
|
|
135
|
+
except ListenerTimeout:
|
|
136
|
+
await message.reply("Timed out. Please try again.")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
await code_msg.reply(f"Code received: {code_msg.text}")
|
|
140
|
+
|
|
141
|
+
client.run()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Shortcuts for the two most common listener types:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
client.message_listen # UpdateListener[Message]
|
|
148
|
+
client.callback_listen # UpdateListener[CallbackQuery]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### ask()
|
|
154
|
+
|
|
155
|
+
`ask()` is the high-level wrapper around listeners. It sends (or edits) a message and then suspends until a matching reply arrives — all in one `await`.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Send a new message, then wait for reply
|
|
159
|
+
answer = await client.ask(
|
|
160
|
+
chat_id=chat_id,
|
|
161
|
+
text="Choose an option:",
|
|
162
|
+
reply_markup=keyboard,
|
|
163
|
+
listen_user_id=user_id,
|
|
164
|
+
timeout=30,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Edit an existing message, then wait for a callback query
|
|
168
|
+
callback = await client.ask(
|
|
169
|
+
chat_id=chat_id,
|
|
170
|
+
text="Updated — choose again:",
|
|
171
|
+
message_id=sent_msg.id,
|
|
172
|
+
listen_user_id=user_id,
|
|
173
|
+
timeout=30,
|
|
174
|
+
update_type=CallbackQuery,
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Parameters:**
|
|
179
|
+
|
|
180
|
+
| Parameter | Description |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `chat_id` | Target chat |
|
|
183
|
+
| `text` | Message text |
|
|
184
|
+
| `message_id` | If provided, edits the message instead of sending a new one |
|
|
185
|
+
| `listen_user_id` | Filter the awaited update by user |
|
|
186
|
+
| `listen_message_id` | Filter the awaited update by message |
|
|
187
|
+
| `timeout` | Seconds to wait before raising `ListenerTimeout` |
|
|
188
|
+
| `update_type` | Update type to wait for — determines the return type (default: `Message`) |
|
|
189
|
+
| `meta` | Arbitrary metadata attached to the listener |
|
|
190
|
+
|
|
191
|
+
**Raises:**
|
|
192
|
+
- `ListenerTimeout` — no reply arrived within `timeout` seconds
|
|
193
|
+
- `ListenerCancelled` — the listener was cancelled while waiting
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Coordinators
|
|
198
|
+
|
|
199
|
+
A `UpdateCoordinated` acquires a **distributed lock** before processing an update. This ensures the same update is handled by exactly one session when the bot runs on multiple servers simultaneously.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from functools import partial
|
|
203
|
+
from redis.asyncio import Redis
|
|
204
|
+
from pyroflow import Client, MessageCoordinated, RedisUpdateCoordinator
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
client = Client("my_session")
|
|
208
|
+
redis = Redis(db=client.name)
|
|
209
|
+
coordinator_factory = partial(RedisUpdateCoordinator, redis)
|
|
210
|
+
coordinated = MessageCoordinated(coordinator_factory)
|
|
211
|
+
client.register_coordinated(coordinated)
|
|
212
|
+
|
|
213
|
+
client.run()
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Supported backends:**
|
|
217
|
+
|
|
218
|
+
| Backend | Extra |
|
|
219
|
+
|---|---|
|
|
220
|
+
| Redis | `pip install pyroflow[redis]` |
|
|
221
|
+
|
|
222
|
+
**Lock states:**
|
|
223
|
+
|
|
224
|
+
- `HANDLED` — at least one handler completed without error; lock is released and other sessions skip the update.
|
|
225
|
+
- `None` — no handler ran successfully; lock is released so another session may retry.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### Histories
|
|
230
|
+
|
|
231
|
+
A `UpdateHistory` records which handlers ran successfully for each update. This enables features like `back` buttons that replay or inspect previous processing steps.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from pyroflow import Client, MessageHistory
|
|
235
|
+
|
|
236
|
+
client = Client("my_session")
|
|
237
|
+
client.register_history(MessageHistory())
|
|
238
|
+
|
|
239
|
+
client.run()
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
They can also be removed at runtime:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
await client.unregister_listener(Message)
|
|
248
|
+
await client.unregister_coordinated(Message)
|
|
249
|
+
await client.unregister_history(Message)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Error handling
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from pyroflow.errors import ListenerTimeout, ListenerCancelled
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
reply = await client.ask(chat_id, "Your input?", listen_user_id=uid, timeout=30)
|
|
261
|
+
except ListenerTimeout:
|
|
262
|
+
await client.send_message(chat_id, "You took too long. Try again.")
|
|
263
|
+
except ListenerCancelled:
|
|
264
|
+
await client.send_message(chat_id, "Session was cancelled.")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
pyroflow-0.1.0/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# pyroflow
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pyroflow/)
|
|
4
|
+
[](https://pypi.org/project/pyroflow/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Conversation-oriented Pyrogram extension with per-update listeners and multi-session coordination.**
|
|
8
|
+
|
|
9
|
+
pyroflow builds on top of [Pyrogram](https://github.com/pyrogram/pyrogram) / [Kurigram](https://github.com/KurimuzonAkuma/pyrogram) to replace the handler-based model with a conversation-first API — `await` a specific reply from a specific user instead of wiring up global handlers and managing state machines by hand.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install pyroflow
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For Redis-backed coordination:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install pyroflow[redis]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why pyroflow?
|
|
24
|
+
|
|
25
|
+
Pyrogram fires your handler for **every** incoming update of a given type. The moment you need a back-and-forth conversation you end up writing state machines, storing `user_id → step` in a dict, and hoping two updates don't race each other.
|
|
26
|
+
|
|
27
|
+
pyroflow solves all three problems:
|
|
28
|
+
|
|
29
|
+
| Problem | pyroflow solution |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Waiting for a specific reply | `UpdateListener` — `await` the next update from a user |
|
|
32
|
+
| Duplicate processing across multiple bot sessions | `UpdateCoordinated` — distributed lock per update |
|
|
33
|
+
| Replaying or inspecting previous handler steps | `UpdateHistory` — per-update-type handler record |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
**Minimum requirements:** Python 3.9+
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install pyroflow # core
|
|
43
|
+
pip install pyroflow[redis] # + Redis coordinator support
|
|
44
|
+
pip install pyroflow[dev] # + development tools (hatch, twine)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pyroflow import Client, MessageListener
|
|
53
|
+
|
|
54
|
+
client = Client("my_session")
|
|
55
|
+
client.register_listener(MessageListener())
|
|
56
|
+
|
|
57
|
+
@client.on_message()
|
|
58
|
+
async def on_start(client, message):
|
|
59
|
+
if message.text != "/start":
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
answer = await message.ask(
|
|
63
|
+
chat_id=message.chat.id,
|
|
64
|
+
text="What is your name?",
|
|
65
|
+
listen_user_id=message.from_user.id,
|
|
66
|
+
timeout=60,
|
|
67
|
+
)
|
|
68
|
+
await answer.reply(f"Hello, {answer.text}!")
|
|
69
|
+
|
|
70
|
+
client.run()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Core concepts
|
|
76
|
+
|
|
77
|
+
### Listeners
|
|
78
|
+
|
|
79
|
+
A `UpdateListener` is a typed queue bound to a Pyrogram update type. Any coroutine can `await` the next matching update from a specific user or chat. An update claimed by a listener **never reaches the normal handler pipeline**.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from pyroflow import Client, MessageListener
|
|
83
|
+
from pyroflow.errors import ListenerTimeout
|
|
84
|
+
|
|
85
|
+
client = Client("my_session")
|
|
86
|
+
client.register_listener(MessageListener())
|
|
87
|
+
|
|
88
|
+
@client.on_message()
|
|
89
|
+
async def on_confirm(client, message):
|
|
90
|
+
if message.text != "/confirm":
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
await message.reply("Send your confirmation code:")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
code_msg = await client.message_listen(
|
|
97
|
+
chat_id=message.chat.id,
|
|
98
|
+
user_id=message.from_user.id,
|
|
99
|
+
timeout=120,
|
|
100
|
+
)
|
|
101
|
+
except ListenerTimeout:
|
|
102
|
+
await message.reply("Timed out. Please try again.")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
await code_msg.reply(f"Code received: {code_msg.text}")
|
|
106
|
+
|
|
107
|
+
client.run()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Shortcuts for the two most common listener types:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
client.message_listen # UpdateListener[Message]
|
|
114
|
+
client.callback_listen # UpdateListener[CallbackQuery]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### ask()
|
|
120
|
+
|
|
121
|
+
`ask()` is the high-level wrapper around listeners. It sends (or edits) a message and then suspends until a matching reply arrives — all in one `await`.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# Send a new message, then wait for reply
|
|
125
|
+
answer = await client.ask(
|
|
126
|
+
chat_id=chat_id,
|
|
127
|
+
text="Choose an option:",
|
|
128
|
+
reply_markup=keyboard,
|
|
129
|
+
listen_user_id=user_id,
|
|
130
|
+
timeout=30,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Edit an existing message, then wait for a callback query
|
|
134
|
+
callback = await client.ask(
|
|
135
|
+
chat_id=chat_id,
|
|
136
|
+
text="Updated — choose again:",
|
|
137
|
+
message_id=sent_msg.id,
|
|
138
|
+
listen_user_id=user_id,
|
|
139
|
+
timeout=30,
|
|
140
|
+
update_type=CallbackQuery,
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Parameters:**
|
|
145
|
+
|
|
146
|
+
| Parameter | Description |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `chat_id` | Target chat |
|
|
149
|
+
| `text` | Message text |
|
|
150
|
+
| `message_id` | If provided, edits the message instead of sending a new one |
|
|
151
|
+
| `listen_user_id` | Filter the awaited update by user |
|
|
152
|
+
| `listen_message_id` | Filter the awaited update by message |
|
|
153
|
+
| `timeout` | Seconds to wait before raising `ListenerTimeout` |
|
|
154
|
+
| `update_type` | Update type to wait for — determines the return type (default: `Message`) |
|
|
155
|
+
| `meta` | Arbitrary metadata attached to the listener |
|
|
156
|
+
|
|
157
|
+
**Raises:**
|
|
158
|
+
- `ListenerTimeout` — no reply arrived within `timeout` seconds
|
|
159
|
+
- `ListenerCancelled` — the listener was cancelled while waiting
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### Coordinators
|
|
164
|
+
|
|
165
|
+
A `UpdateCoordinated` acquires a **distributed lock** before processing an update. This ensures the same update is handled by exactly one session when the bot runs on multiple servers simultaneously.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from functools import partial
|
|
169
|
+
from redis.asyncio import Redis
|
|
170
|
+
from pyroflow import Client, MessageCoordinated, RedisUpdateCoordinator
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
client = Client("my_session")
|
|
174
|
+
redis = Redis(db=client.name)
|
|
175
|
+
coordinator_factory = partial(RedisUpdateCoordinator, redis)
|
|
176
|
+
coordinated = MessageCoordinated(coordinator_factory)
|
|
177
|
+
client.register_coordinated(coordinated)
|
|
178
|
+
|
|
179
|
+
client.run()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Supported backends:**
|
|
183
|
+
|
|
184
|
+
| Backend | Extra |
|
|
185
|
+
|---|---|
|
|
186
|
+
| Redis | `pip install pyroflow[redis]` |
|
|
187
|
+
|
|
188
|
+
**Lock states:**
|
|
189
|
+
|
|
190
|
+
- `HANDLED` — at least one handler completed without error; lock is released and other sessions skip the update.
|
|
191
|
+
- `None` — no handler ran successfully; lock is released so another session may retry.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### Histories
|
|
196
|
+
|
|
197
|
+
A `UpdateHistory` records which handlers ran successfully for each update. This enables features like `back` buttons that replay or inspect previous processing steps.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from pyroflow import Client, MessageHistory
|
|
201
|
+
|
|
202
|
+
client = Client("my_session")
|
|
203
|
+
client.register_history(MessageHistory())
|
|
204
|
+
|
|
205
|
+
client.run()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
They can also be removed at runtime:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
await client.unregister_listener(Message)
|
|
214
|
+
await client.unregister_coordinated(Message)
|
|
215
|
+
await client.unregister_history(Message)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Error handling
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from pyroflow.errors import ListenerTimeout, ListenerCancelled
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
reply = await client.ask(chat_id, "Your input?", listen_user_id=uid, timeout=30)
|
|
227
|
+
except ListenerTimeout:
|
|
228
|
+
await client.send_message(chat_id, "You took too long. Try again.")
|
|
229
|
+
except ListenerCancelled:
|
|
230
|
+
await client.send_message(chat_id, "Session was cancelled.")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "pyroflow"
|
|
8
|
+
dynamic = ["version"]
|
|
9
|
+
description = "Conversation-oriented Pyrogram extension with per-update listeners and multi-session coordination"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Abdullah", email = "aldheeb01@gmail.com" }]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
|
|
15
|
+
keywords = [
|
|
16
|
+
"pyrogram",
|
|
17
|
+
"telegram",
|
|
18
|
+
"bot",
|
|
19
|
+
"async",
|
|
20
|
+
"conversation",
|
|
21
|
+
"listener",
|
|
22
|
+
"coordinator",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Programming Language :: Python :: 3.14",
|
|
36
|
+
"Topic :: Communications :: Chat",
|
|
37
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
38
|
+
"Framework :: AsyncIO",
|
|
39
|
+
"Typing :: Typed",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
dependencies = [
|
|
43
|
+
"cachetools",
|
|
44
|
+
"kurigram>=2.2.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
"Homepage" = "https://github.com/eeeob/pyroflow"
|
|
50
|
+
"Bug Tracker" = "https://github.com/eeeob/pyroflow/issues"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
[project.optional-dependencies]
|
|
54
|
+
dev = [
|
|
55
|
+
"hatch<=1.16.5",
|
|
56
|
+
"twine<=6.2.0",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
redis = [
|
|
60
|
+
"redis>=4.2.0",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.sdist]
|
|
65
|
+
exclude = [
|
|
66
|
+
".github/",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.hatch.version]
|
|
70
|
+
path = "pyroflow/__meta__.py"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
[tool.hatch.metadata]
|
|
74
|
+
allow-direct-references = true
|
|
75
|
+
|
|
76
|
+
[tool.hatch.build.targets.wheel]
|
|
77
|
+
ignore-vcs = true
|
|
78
|
+
packages = ["pyroflow"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .listener_coordinator import *
|
|
2
|
+
from .update_coordinated import *
|
|
3
|
+
from .update_coordinator import *
|
|
4
|
+
from .update_history import *
|
|
5
|
+
from .update_history_store import *
|
|
6
|
+
from .update_listener import *
|
|
7
|
+
|
|
8
|
+
from .client import Client
|
|
9
|
+
from .dispatcher import Dispatcher
|
|
10
|
+
|
|
11
|
+
from .types import Message, CallbackQuery
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|