pyteleflow 0.1.1__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.
- pyteleflow-0.1.1/LICENSE +21 -0
- pyteleflow-0.1.1/PKG-INFO +258 -0
- pyteleflow-0.1.1/README.md +235 -0
- pyteleflow-0.1.1/pyproject.toml +55 -0
- pyteleflow-0.1.1/pyproject.toml.orig +48 -0
- pyteleflow-0.1.1/pyteleflow/__init__.py +4 -0
- pyteleflow-0.1.1/pyteleflow/_app.py +439 -0
- pyteleflow-0.1.1/pyteleflow/_flow.py +117 -0
- pyteleflow-0.1.1/pyteleflow/_helpers.py +56 -0
- pyteleflow-0.1.1/pyteleflow/_strategy.py +121 -0
- pyteleflow-0.1.1/pyteleflow/_types.py +8 -0
- pyteleflow-0.1.1/pyteleflow/errors.py +21 -0
- pyteleflow-0.1.1/pyteleflow/filters_ext.py +51 -0
- pyteleflow-0.1.1/pyteleflow/middleware.py +17 -0
- pyteleflow-0.1.1/pyteleflow/py.typed +0 -0
- pyteleflow-0.1.1/pyteleflow/widgets/__init__.py +2 -0
- pyteleflow-0.1.1/pyteleflow/widgets/paged_select.py +176 -0
- pyteleflow-0.1.1/pyteleflow/widgets/select.py +150 -0
pyteleflow-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2026] [nebenwelt]
|
|
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,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyteleflow
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Asynchronous framework for building Telegram bots with linear flows
|
|
5
|
+
Keywords: telegram,bot,async,python-telegram-bot,conversation,flow
|
|
6
|
+
Author: nebenwelt
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Topic :: Communications :: Chat
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: python-telegram-bot[rate-limiter]>=22.8
|
|
20
|
+
Requires-Dist: tenacity>=9.1.4
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
  
|
|
25
|
+
|
|
26
|
+
# Introduction
|
|
27
|
+
|
|
28
|
+
`pyteleflow` is an asynchronous Python framework for building Telegram bots. It offers an alternative to the traditional approach based on event handlers and finite-state machines (FSMs).
|
|
29
|
+
|
|
30
|
+
Instead of distributing conversation logic across handlers, wiring them together, and managing states manually, you describe the entire interaction flow in one or more asynchronous functions.
|
|
31
|
+
|
|
32
|
+
With `pyteleflow`, you write code the same way you think about a conversation:
|
|
33
|
+
- ask the user a question
|
|
34
|
+
- wait for a response
|
|
35
|
+
- perform an action
|
|
36
|
+
- ask another question
|
|
37
|
+
- and so on
|
|
38
|
+
|
|
39
|
+
Here is what a minimal example looks like:
|
|
40
|
+
```python
|
|
41
|
+
async def your_flow(ctx: Context):
|
|
42
|
+
# Get the current chat with the user
|
|
43
|
+
chat = ctx.initial_update.effective_chat
|
|
44
|
+
|
|
45
|
+
# Send the user an initial message asking a question
|
|
46
|
+
await chat.send_message("How old are you?")
|
|
47
|
+
|
|
48
|
+
# Wait for the user to send a text message in the chat.
|
|
49
|
+
# Other message types are ignored because we specified the TEXT filter.
|
|
50
|
+
msg = await ctx.message(filters.TEXT)
|
|
51
|
+
age = msg.text
|
|
52
|
+
|
|
53
|
+
# Ask the user to send a photo
|
|
54
|
+
await chat.send_message("Send me a photo of yourself")
|
|
55
|
+
|
|
56
|
+
# Wait until the user sends a photo
|
|
57
|
+
msg = await ctx.message(filters.PHOTO)
|
|
58
|
+
photo = msg.photo
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
And there is more! `pyteleflow` also makes it easy to describe and create widgets:
|
|
62
|
+
```python
|
|
63
|
+
# ...
|
|
64
|
+
result = await widgets.Select(
|
|
65
|
+
ctx=ctx,
|
|
66
|
+
prompt="Which file-sharing service do you use?",
|
|
67
|
+
items=["Skype", "Telegram"],
|
|
68
|
+
).show_as_reply(msg)
|
|
69
|
+
# ...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`pyteleflow` makes code easier to write and review, including when an AI agent writes it for you.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Dependencies
|
|
76
|
+
To access the Telegram Bot API, `pyteleflow` uses the popular [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) wrapper. You can also use the extensive collection of existing filters provided by `python-telegram-bot`.
|
|
77
|
+
|
|
78
|
+
Why not aiogram?
|
|
79
|
+
aiogram uses heavyweight Pydantic models, and its author does not appear to have plans to move away from them.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Quickstart
|
|
83
|
+
## Installation
|
|
84
|
+
Using uv:
|
|
85
|
+
```sh
|
|
86
|
+
uv add pyteleflow
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Using pip:
|
|
90
|
+
```sh
|
|
91
|
+
pip install pyteleflow
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Simple Bot
|
|
96
|
+
> 💡 The code for this example is in [quickstart.py](./examples/quickstart.py)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
1. Import the dependencies and initialize logging:
|
|
101
|
+
```python
|
|
102
|
+
import asyncio as aio
|
|
103
|
+
import os
|
|
104
|
+
import logging
|
|
105
|
+
|
|
106
|
+
from telegram.ext import ExtBot, AIORateLimiter, filters
|
|
107
|
+
from pyteleflow import FlowApp, Context, widgets, ConcurrencyStrategy
|
|
108
|
+
|
|
109
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
110
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
111
|
+
logging.getLogger("telegram").setLevel(logging.WARNING)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
2. Create a Flow that collects a simple user profile:
|
|
115
|
+
```python
|
|
116
|
+
async def quickstart_flow(ctx: Context):
|
|
117
|
+
# Get the current chat with the user
|
|
118
|
+
chat = ctx.initial_update.effective_chat
|
|
119
|
+
|
|
120
|
+
# Send the user an initial message asking a question
|
|
121
|
+
await chat.send_message("What’s your name?")
|
|
122
|
+
|
|
123
|
+
# Wait for the user to send a text message in the chat.
|
|
124
|
+
# Other message types are ignored because we specified the TEXT filter.
|
|
125
|
+
msg = await ctx.message(filters.TEXT)
|
|
126
|
+
user_name = msg.text
|
|
127
|
+
|
|
128
|
+
# Ask the user to send a photo
|
|
129
|
+
await chat.send_message("Send me a photo of yourself")
|
|
130
|
+
|
|
131
|
+
# Wait until the user sends a photo
|
|
132
|
+
msg = await ctx.message(filters.PHOTO)
|
|
133
|
+
user_photo = msg.photo[0]
|
|
134
|
+
|
|
135
|
+
# Show the user a widget with buttons and wait for their selection
|
|
136
|
+
result = await widgets.Select(
|
|
137
|
+
ctx=ctx,
|
|
138
|
+
prompt="Your profile is complete. Would you like to save it?",
|
|
139
|
+
items=["Save", "View profile"],
|
|
140
|
+
direction="horizontal",
|
|
141
|
+
).show(chat.id, cancel_button=True)
|
|
142
|
+
|
|
143
|
+
if not result.canceled:
|
|
144
|
+
if result.item == "Save":
|
|
145
|
+
alert = await chat.send_message("Profile saved")
|
|
146
|
+
await aio.sleep(2)
|
|
147
|
+
await alert.delete()
|
|
148
|
+
elif result.item == "View profile":
|
|
149
|
+
await chat.send_photo(
|
|
150
|
+
photo=user_photo,
|
|
151
|
+
caption=f"PROFILE\nUser name: {user_name}\nUser ID: {msg.from_user.id}",
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
3. Create the application, specifying the Flow to run, and start it:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
async def main():
|
|
159
|
+
bot_token = os.environ.get("BOT_TOKEN")
|
|
160
|
+
|
|
161
|
+
app = FlowApp(
|
|
162
|
+
ExtBot(bot_token, rate_limiter=AIORateLimiter()),
|
|
163
|
+
entry_coro=quickstart_flow,
|
|
164
|
+
spawn_trigger=filters.Text(["/start"]),
|
|
165
|
+
concurrency_strategy=ConcurrencyStrategy.CHAT
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
await app.run_polling()
|
|
170
|
+
finally:
|
|
171
|
+
await app.shutdown()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
aio.run(main())
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
4. To start a conversation with the bot, send the `/start` command in the bot's chat.
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Overview
|
|
182
|
+
## Framework specifics and limitations
|
|
183
|
+
|
|
184
|
+
__Conversation state does not survive restarts.__
|
|
185
|
+
|
|
186
|
+
The simplicity of writing and reading Flows comes at a cost. In the current architecture, active Flow instances are stored only in process memory. If the bot restarts, all conversations will be lost, and users will have to start them over. Python cannot serialize and restore the stack of `asyncio` coroutines. As a result, a coroutine cannot resume from its suspension point after the process restarts. There are theoretically a couple of ways to address this problem, but they would make Flow code more complicated.
|
|
187
|
+
|
|
188
|
+
How critical is this?
|
|
189
|
+
There are different opinions on this. For example, web users are accustomed to a browser page's front-end state being reset when the page is refreshed, while the back-end state (user data) is stored persistently and survives restarts. Telegram bots are also the front-end state of your application.
|
|
190
|
+
|
|
191
|
+
## Widgets
|
|
192
|
+
Currently, `pyteleflow.widgets` includes only a few widgets. More may be added in the future, but for now these widgets serve as examples of how you can implement your own.
|
|
193
|
+
|
|
194
|
+
## Flow cancellation handling
|
|
195
|
+
Spawned Flow instances can be cancelled for several reasons:
|
|
196
|
+
|
|
197
|
+
__Flow inactivity__
|
|
198
|
+
The last activity is considered to be the moment you receive a message through the context, when `ctx.message()` returns a received message that matches the specified filter
|
|
199
|
+
```python
|
|
200
|
+
msg = await ctx.message(filters.PHOTO)
|
|
201
|
+
```
|
|
202
|
+
If the period of inactivity exceeds `flow_inactivity_timeout` (set when creating `FlowApp`), the Flow instance will be cancelled. You can change this timeout for an individual Flow during the conversation through the context:
|
|
203
|
+
```python
|
|
204
|
+
ctx.inactivity_timeout = timedelta(minutes=20)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
__Flow respawn__
|
|
208
|
+
If a filter is configured in `respawn_trigger` (set when creating `FlowApp`), for example `filters.Text(["/start"])`, the active Flow instance will first be cancelled and then restarted from the beginning.
|
|
209
|
+
|
|
210
|
+
__Stopping the bot application__
|
|
211
|
+
Always use the `FlowApp.shutdown()` method when stopping the application. This method initiates cancellation of all active Flow instances.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
When a coroutine is cancelled, an `asyncio.CancelledError` exception is raised inside it. You can handle it if necessary:
|
|
215
|
+
```python
|
|
216
|
+
async def your_flow(ctx: Context):
|
|
217
|
+
try:
|
|
218
|
+
# ...
|
|
219
|
+
msg = ctx.message(filter.PHOTO)
|
|
220
|
+
# ...
|
|
221
|
+
except asyncio.CancelledError:
|
|
222
|
+
# Handle Flow cancellation
|
|
223
|
+
|
|
224
|
+
raise
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Middleware
|
|
228
|
+
|
|
229
|
+
> 💡 The code for this example is in [middleware.py](./examples/middleware.py)
|
|
230
|
+
|
|
231
|
+
Middleware allows you to intercept incoming `Update` objects before they reach a Flow. You can use middleware to restrict access, log incoming updates, modify an `Update` before processing, and more.
|
|
232
|
+
|
|
233
|
+
If a middleware returns `None`, the remaining middleware chain is skipped, and the `Update` is discarded.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from pyteleflow.middleware import Middleware
|
|
237
|
+
|
|
238
|
+
class Authentication(Middleware):
|
|
239
|
+
def __init__(self, allowed_user_ids: Iterable[int]):
|
|
240
|
+
self._allowed_user_ids: set[int] = set(allowed_user_ids)
|
|
241
|
+
|
|
242
|
+
def __call__(self, update: Update | None):
|
|
243
|
+
if update.effective_user is not None and update.effective_user.id in self._allowed_user_ids:
|
|
244
|
+
return update
|
|
245
|
+
|
|
246
|
+
return None
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Middleware is executed in the order in which it is added:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
app.add_pre_middlewares(
|
|
253
|
+
[Authentication(allowed_user_ids=(111111, 222222, 3333333))],
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Background tasks
|
|
258
|
+
`pyteleflow` supports running background tasks. See the example in [background_task.py](./examples/background_task.py)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
  
|
|
2
|
+
|
|
3
|
+
# Introduction
|
|
4
|
+
|
|
5
|
+
`pyteleflow` is an asynchronous Python framework for building Telegram bots. It offers an alternative to the traditional approach based on event handlers and finite-state machines (FSMs).
|
|
6
|
+
|
|
7
|
+
Instead of distributing conversation logic across handlers, wiring them together, and managing states manually, you describe the entire interaction flow in one or more asynchronous functions.
|
|
8
|
+
|
|
9
|
+
With `pyteleflow`, you write code the same way you think about a conversation:
|
|
10
|
+
- ask the user a question
|
|
11
|
+
- wait for a response
|
|
12
|
+
- perform an action
|
|
13
|
+
- ask another question
|
|
14
|
+
- and so on
|
|
15
|
+
|
|
16
|
+
Here is what a minimal example looks like:
|
|
17
|
+
```python
|
|
18
|
+
async def your_flow(ctx: Context):
|
|
19
|
+
# Get the current chat with the user
|
|
20
|
+
chat = ctx.initial_update.effective_chat
|
|
21
|
+
|
|
22
|
+
# Send the user an initial message asking a question
|
|
23
|
+
await chat.send_message("How old are you?")
|
|
24
|
+
|
|
25
|
+
# Wait for the user to send a text message in the chat.
|
|
26
|
+
# Other message types are ignored because we specified the TEXT filter.
|
|
27
|
+
msg = await ctx.message(filters.TEXT)
|
|
28
|
+
age = msg.text
|
|
29
|
+
|
|
30
|
+
# Ask the user to send a photo
|
|
31
|
+
await chat.send_message("Send me a photo of yourself")
|
|
32
|
+
|
|
33
|
+
# Wait until the user sends a photo
|
|
34
|
+
msg = await ctx.message(filters.PHOTO)
|
|
35
|
+
photo = msg.photo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
And there is more! `pyteleflow` also makes it easy to describe and create widgets:
|
|
39
|
+
```python
|
|
40
|
+
# ...
|
|
41
|
+
result = await widgets.Select(
|
|
42
|
+
ctx=ctx,
|
|
43
|
+
prompt="Which file-sharing service do you use?",
|
|
44
|
+
items=["Skype", "Telegram"],
|
|
45
|
+
).show_as_reply(msg)
|
|
46
|
+
# ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`pyteleflow` makes code easier to write and review, including when an AI agent writes it for you.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Dependencies
|
|
53
|
+
To access the Telegram Bot API, `pyteleflow` uses the popular [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) wrapper. You can also use the extensive collection of existing filters provided by `python-telegram-bot`.
|
|
54
|
+
|
|
55
|
+
Why not aiogram?
|
|
56
|
+
aiogram uses heavyweight Pydantic models, and its author does not appear to have plans to move away from them.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Quickstart
|
|
60
|
+
## Installation
|
|
61
|
+
Using uv:
|
|
62
|
+
```sh
|
|
63
|
+
uv add pyteleflow
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Using pip:
|
|
67
|
+
```sh
|
|
68
|
+
pip install pyteleflow
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Simple Bot
|
|
73
|
+
> 💡 The code for this example is in [quickstart.py](./examples/quickstart.py)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
1. Import the dependencies and initialize logging:
|
|
78
|
+
```python
|
|
79
|
+
import asyncio as aio
|
|
80
|
+
import os
|
|
81
|
+
import logging
|
|
82
|
+
|
|
83
|
+
from telegram.ext import ExtBot, AIORateLimiter, filters
|
|
84
|
+
from pyteleflow import FlowApp, Context, widgets, ConcurrencyStrategy
|
|
85
|
+
|
|
86
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
87
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
88
|
+
logging.getLogger("telegram").setLevel(logging.WARNING)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
2. Create a Flow that collects a simple user profile:
|
|
92
|
+
```python
|
|
93
|
+
async def quickstart_flow(ctx: Context):
|
|
94
|
+
# Get the current chat with the user
|
|
95
|
+
chat = ctx.initial_update.effective_chat
|
|
96
|
+
|
|
97
|
+
# Send the user an initial message asking a question
|
|
98
|
+
await chat.send_message("What’s your name?")
|
|
99
|
+
|
|
100
|
+
# Wait for the user to send a text message in the chat.
|
|
101
|
+
# Other message types are ignored because we specified the TEXT filter.
|
|
102
|
+
msg = await ctx.message(filters.TEXT)
|
|
103
|
+
user_name = msg.text
|
|
104
|
+
|
|
105
|
+
# Ask the user to send a photo
|
|
106
|
+
await chat.send_message("Send me a photo of yourself")
|
|
107
|
+
|
|
108
|
+
# Wait until the user sends a photo
|
|
109
|
+
msg = await ctx.message(filters.PHOTO)
|
|
110
|
+
user_photo = msg.photo[0]
|
|
111
|
+
|
|
112
|
+
# Show the user a widget with buttons and wait for their selection
|
|
113
|
+
result = await widgets.Select(
|
|
114
|
+
ctx=ctx,
|
|
115
|
+
prompt="Your profile is complete. Would you like to save it?",
|
|
116
|
+
items=["Save", "View profile"],
|
|
117
|
+
direction="horizontal",
|
|
118
|
+
).show(chat.id, cancel_button=True)
|
|
119
|
+
|
|
120
|
+
if not result.canceled:
|
|
121
|
+
if result.item == "Save":
|
|
122
|
+
alert = await chat.send_message("Profile saved")
|
|
123
|
+
await aio.sleep(2)
|
|
124
|
+
await alert.delete()
|
|
125
|
+
elif result.item == "View profile":
|
|
126
|
+
await chat.send_photo(
|
|
127
|
+
photo=user_photo,
|
|
128
|
+
caption=f"PROFILE\nUser name: {user_name}\nUser ID: {msg.from_user.id}",
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
3. Create the application, specifying the Flow to run, and start it:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
async def main():
|
|
136
|
+
bot_token = os.environ.get("BOT_TOKEN")
|
|
137
|
+
|
|
138
|
+
app = FlowApp(
|
|
139
|
+
ExtBot(bot_token, rate_limiter=AIORateLimiter()),
|
|
140
|
+
entry_coro=quickstart_flow,
|
|
141
|
+
spawn_trigger=filters.Text(["/start"]),
|
|
142
|
+
concurrency_strategy=ConcurrencyStrategy.CHAT
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
await app.run_polling()
|
|
147
|
+
finally:
|
|
148
|
+
await app.shutdown()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
aio.run(main())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
4. To start a conversation with the bot, send the `/start` command in the bot's chat.
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Overview
|
|
159
|
+
## Framework specifics and limitations
|
|
160
|
+
|
|
161
|
+
__Conversation state does not survive restarts.__
|
|
162
|
+
|
|
163
|
+
The simplicity of writing and reading Flows comes at a cost. In the current architecture, active Flow instances are stored only in process memory. If the bot restarts, all conversations will be lost, and users will have to start them over. Python cannot serialize and restore the stack of `asyncio` coroutines. As a result, a coroutine cannot resume from its suspension point after the process restarts. There are theoretically a couple of ways to address this problem, but they would make Flow code more complicated.
|
|
164
|
+
|
|
165
|
+
How critical is this?
|
|
166
|
+
There are different opinions on this. For example, web users are accustomed to a browser page's front-end state being reset when the page is refreshed, while the back-end state (user data) is stored persistently and survives restarts. Telegram bots are also the front-end state of your application.
|
|
167
|
+
|
|
168
|
+
## Widgets
|
|
169
|
+
Currently, `pyteleflow.widgets` includes only a few widgets. More may be added in the future, but for now these widgets serve as examples of how you can implement your own.
|
|
170
|
+
|
|
171
|
+
## Flow cancellation handling
|
|
172
|
+
Spawned Flow instances can be cancelled for several reasons:
|
|
173
|
+
|
|
174
|
+
__Flow inactivity__
|
|
175
|
+
The last activity is considered to be the moment you receive a message through the context, when `ctx.message()` returns a received message that matches the specified filter
|
|
176
|
+
```python
|
|
177
|
+
msg = await ctx.message(filters.PHOTO)
|
|
178
|
+
```
|
|
179
|
+
If the period of inactivity exceeds `flow_inactivity_timeout` (set when creating `FlowApp`), the Flow instance will be cancelled. You can change this timeout for an individual Flow during the conversation through the context:
|
|
180
|
+
```python
|
|
181
|
+
ctx.inactivity_timeout = timedelta(minutes=20)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
__Flow respawn__
|
|
185
|
+
If a filter is configured in `respawn_trigger` (set when creating `FlowApp`), for example `filters.Text(["/start"])`, the active Flow instance will first be cancelled and then restarted from the beginning.
|
|
186
|
+
|
|
187
|
+
__Stopping the bot application__
|
|
188
|
+
Always use the `FlowApp.shutdown()` method when stopping the application. This method initiates cancellation of all active Flow instances.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
When a coroutine is cancelled, an `asyncio.CancelledError` exception is raised inside it. You can handle it if necessary:
|
|
192
|
+
```python
|
|
193
|
+
async def your_flow(ctx: Context):
|
|
194
|
+
try:
|
|
195
|
+
# ...
|
|
196
|
+
msg = ctx.message(filter.PHOTO)
|
|
197
|
+
# ...
|
|
198
|
+
except asyncio.CancelledError:
|
|
199
|
+
# Handle Flow cancellation
|
|
200
|
+
|
|
201
|
+
raise
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Middleware
|
|
205
|
+
|
|
206
|
+
> 💡 The code for this example is in [middleware.py](./examples/middleware.py)
|
|
207
|
+
|
|
208
|
+
Middleware allows you to intercept incoming `Update` objects before they reach a Flow. You can use middleware to restrict access, log incoming updates, modify an `Update` before processing, and more.
|
|
209
|
+
|
|
210
|
+
If a middleware returns `None`, the remaining middleware chain is skipped, and the `Update` is discarded.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from pyteleflow.middleware import Middleware
|
|
214
|
+
|
|
215
|
+
class Authentication(Middleware):
|
|
216
|
+
def __init__(self, allowed_user_ids: Iterable[int]):
|
|
217
|
+
self._allowed_user_ids: set[int] = set(allowed_user_ids)
|
|
218
|
+
|
|
219
|
+
def __call__(self, update: Update | None):
|
|
220
|
+
if update.effective_user is not None and update.effective_user.id in self._allowed_user_ids:
|
|
221
|
+
return update
|
|
222
|
+
|
|
223
|
+
return None
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Middleware is executed in the order in which it is added:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
app.add_pre_middlewares(
|
|
230
|
+
[Authentication(allowed_user_ids=(111111, 222222, 3333333))],
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Background tasks
|
|
235
|
+
`pyteleflow` supports running background tasks. See the example in [background_task.py](./examples/background_task.py)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.13.5,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[tool.uv.build-backend]
|
|
6
|
+
module-root = ""
|
|
7
|
+
module-name = "pyteleflow"
|
|
8
|
+
|
|
9
|
+
[tool.pytest.ini_options]
|
|
10
|
+
log_cli = false
|
|
11
|
+
log_cli_level = "DEBUG"
|
|
12
|
+
|
|
13
|
+
[project]
|
|
14
|
+
name = "pyteleflow"
|
|
15
|
+
version = "0.1.1"
|
|
16
|
+
description = "Asynchronous framework for building Telegram bots with linear flows"
|
|
17
|
+
license = "MIT"
|
|
18
|
+
license-files = ["LICENSE"]
|
|
19
|
+
keywords = [
|
|
20
|
+
"telegram",
|
|
21
|
+
"bot",
|
|
22
|
+
"async",
|
|
23
|
+
"python-telegram-bot",
|
|
24
|
+
"conversation",
|
|
25
|
+
"flow",
|
|
26
|
+
]
|
|
27
|
+
readme = "README.md"
|
|
28
|
+
requires-python = ">=3.12"
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
36
|
+
"Framework :: AsyncIO",
|
|
37
|
+
"Topic :: Communications :: Chat",
|
|
38
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
39
|
+
"Typing :: Typed",
|
|
40
|
+
]
|
|
41
|
+
dependencies = [
|
|
42
|
+
"python-telegram-bot[rate-limiter]>=22.8",
|
|
43
|
+
"tenacity>=9.1.4",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[[project.authors]]
|
|
47
|
+
name = "nebenwelt"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"mypy>=2.4.0",
|
|
52
|
+
"pytest>=9.1.1",
|
|
53
|
+
"pytest-asyncio>=1.4.0",
|
|
54
|
+
"ruff>=0.17.0",
|
|
55
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.13.5,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[tool.uv.build-backend]
|
|
6
|
+
module-root = ""
|
|
7
|
+
module-name = "pyteleflow"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "pyteleflow"
|
|
11
|
+
version = "0.1.1"
|
|
12
|
+
description = "Asynchronous framework for building Telegram bots with linear flows"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [{ name = "nebenwelt" }]
|
|
16
|
+
keywords = ["telegram", "bot", "async", "python-telegram-bot", "conversation", "flow"]
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
requires-python = ">=3.12"
|
|
19
|
+
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Framework :: AsyncIO",
|
|
28
|
+
"Topic :: Communications :: Chat",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
dependencies = [
|
|
34
|
+
"python-telegram-bot[rate-limiter]>=22.8",
|
|
35
|
+
"tenacity>=9.1.4",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"mypy>=2.4.0",
|
|
41
|
+
"pytest>=9.1.1",
|
|
42
|
+
"pytest-asyncio>=1.4.0",
|
|
43
|
+
"ruff>=0.17.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
log_cli = false
|
|
48
|
+
log_cli_level = "DEBUG"
|