telekit 2.3.0b1__tar.gz → 2.3.0b2__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.
- {telekit-2.3.0b1 → telekit-2.3.0b2}/PKG-INFO +69 -7
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_callback_query_handler.py +18 -6
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain_inline_keyboards_logic.py +1 -5
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_inline_buttons.py +102 -10
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_version.py +1 -1
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/spells.py +12 -13
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/inline_buttons.py +8 -2
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/traits/paginated_choice.py +68 -32
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/utils.py +74 -1
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/PKG-INFO +69 -7
- {telekit-2.3.0b1 → telekit-2.3.0b2}/LICENSE +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/README.md +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/setup.cfg +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/setup.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_buildtext/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_buildtext/formatter.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_buildtext/styles.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain_base.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain_entry_logic.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chapters/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chapters/chapters.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_handler.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_init.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_input_handler.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_logger.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_on.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_snapvault/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_snapvault/snapcode.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_snapvault/snapvault.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_state.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/mixin.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/builder.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/lexer.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/nodes.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/parser.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/token.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/telekit_orm.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_timeout.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_user.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/dices.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/complete_hotel.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/counter.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/dsl.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/entry.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/faq.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/hotel.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/on_text.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/pages.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/pyapi.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/qr.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/quiz.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/start.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/text_document.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_server.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/parameters.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/senders.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/server.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/styles.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/traits/__init__.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/traits/track_handoff_origin.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/types.py +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/SOURCES.txt +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/dependency_links.txt +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/requires.txt +0 -0
- {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telekit
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.0b2
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -379,7 +379,18 @@ It tries to make Telegram bot development easier.
|
|
|
379
379
|
|
|
380
380
|
---
|
|
381
381
|
|
|
382
|
-
# Changes in version 2.3.
|
|
382
|
+
# Changes in version 2.3.0b2
|
|
383
|
+
|
|
384
|
+
## Inline Buttons
|
|
385
|
+
|
|
386
|
+
| **Name** | **Description** |
|
|
387
|
+
| ---------------- | ------------------------------------------------------------------------------- |
|
|
388
|
+
| `StaticButton` | A non-interactive inline button that performs no action when pressed. |
|
|
389
|
+
| `AnswerButton` | A button that responds to a callback query with a notification or alert without executing custom logic. |
|
|
390
|
+
|
|
391
|
+
- `AnswerButton` and its subclasses (`AlertButton`, `NotificationButton`) now support the `persistent` parameter (Defauts to `True`):
|
|
392
|
+
- `True` — non-blocking hint (does not affect further interactions)
|
|
393
|
+
- `False` — terminates interaction after click
|
|
383
394
|
|
|
384
395
|
## Traits
|
|
385
396
|
|
|
@@ -419,7 +430,11 @@ Navigation buttons (`« Back`, `Next »`) are added automatically.
|
|
|
419
430
|
|
|
420
431
|
| **Member** | **Description** |
|
|
421
432
|
| -------------------- | ---------------------------------------------------------------------------- |
|
|
422
|
-
| `paginated_choice(choices, on_choice,
|
|
433
|
+
| `paginated_choice(choices, on_choice, ...)` | Display a paginated choice keyboard. |
|
|
434
|
+
| `PAGINATED_CHOICE_BACK_LABEL` | Label for the back navigation button. Defaults to `« Back`. |
|
|
435
|
+
| `PAGINATED_CHOICE_NEXT_LABEL` | Label for the next navigation button. Defaults to `Next »`. |
|
|
436
|
+
| `PAGINATED_CHOICE_PAGE_LABEL` | Label template for the page indicator button. Supports `{page}` and `{pages}` placeholders. Set to `None` to hide. Defaults to `{page} / {pages}`. |
|
|
437
|
+
|
|
423
438
|
|
|
424
439
|
```python
|
|
425
440
|
self.chain.sender.set_title("🔤 What is your initial?")
|
|
@@ -433,6 +448,15 @@ self.paginated_choice(
|
|
|
433
448
|
)
|
|
434
449
|
```
|
|
435
450
|
|
|
451
|
+
Override labels per handler to localise or restyle navigation:
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
class MyHandler(PaginatedChoice, telekit.Handler):
|
|
455
|
+
PAGINATED_CHOICE_BACK_LABEL = "⬅️ Назад"
|
|
456
|
+
PAGINATED_CHOICE_NEXT_LABEL = "Далі ➡️"
|
|
457
|
+
PAGINATED_CHOICE_PAGE_LABEL = None # hide page indicator
|
|
458
|
+
```
|
|
459
|
+
|
|
436
460
|
<details>
|
|
437
461
|
<summary>(Click to see the result)</summary>
|
|
438
462
|
<table>
|
|
@@ -447,10 +471,13 @@ If only one item is present, `on_choice` is called immediately without rendering
|
|
|
447
471
|
|
|
448
472
|
## Utils
|
|
449
473
|
|
|
450
|
-
| **Name**
|
|
451
|
-
|
|
|
452
|
-
| `load_env`
|
|
453
|
-
| `read_env_var`
|
|
474
|
+
| **Name** | **Description** |
|
|
475
|
+
| ------------------ | ------------------------------------------------------------------ |
|
|
476
|
+
| `load_env` | Load all key-value pairs from a `.env` file into a dictionary. |
|
|
477
|
+
| `read_env_var` | Read a single variable by name from a `.env` file. |
|
|
478
|
+
| `compose_keyboard` | Merge multiple button groups into a single inline keyboard with computed `row_width`. |
|
|
479
|
+
|
|
480
|
+
### Environment Utils
|
|
454
481
|
|
|
455
482
|
- `read_token` and `read_canvas_path` now support reading from `.env` files.
|
|
456
483
|
Pass `".env"` to use the default key, or `".env:KEY"` to specify a custom one:
|
|
@@ -462,3 +489,38 @@ read_token(".env:BOT_TOKEN") # reads BOT_TOKEN
|
|
|
462
489
|
read_canvas_path(".env") # reads CANVAS_PATH
|
|
463
490
|
read_canvas_path(".env:MY_CANVAS") # reads MY_CANVAS
|
|
464
491
|
```
|
|
492
|
+
|
|
493
|
+
### Inline Keyboard Utils
|
|
494
|
+
|
|
495
|
+
`compose_keyboard` combines multiple button groups into a single keyboard and automatically calculates `row_width` for each group.
|
|
496
|
+
|
|
497
|
+
Each group is laid out independently using its corresponding width from `widths`.
|
|
498
|
+
A width of `-1` means "all buttons in one row" (i.e. ``len(group)``).
|
|
499
|
+
|
|
500
|
+
| **Param** | **Type** | **Description** |
|
|
501
|
+
|-----------|------------------|---------------------------------------------|
|
|
502
|
+
| `groups` | `dict[str, Any]` | One or more button groups |
|
|
503
|
+
| `widths` | `Iterable[int]` | Row width per group or single value for all |
|
|
504
|
+
|
|
505
|
+
| **Returns** | **Type** |
|
|
506
|
+
|------------|----------|
|
|
507
|
+
| `keyboard` | `dict[str, Any]` |
|
|
508
|
+
| `row_width` | `tuple[int, ...]` |
|
|
509
|
+
|
|
510
|
+
```py
|
|
511
|
+
# row_width → (1, 3, 3, 3, 2)
|
|
512
|
+
# layout:
|
|
513
|
+
# | 🆕 Create |
|
|
514
|
+
# | 1 | 2 | 3 |
|
|
515
|
+
# | 4 | 5 | 6 |
|
|
516
|
+
# | 7 | 8 | 9 |
|
|
517
|
+
# | « Back | Next » |
|
|
518
|
+
|
|
519
|
+
keyboard, row_width = compose_keyboard(
|
|
520
|
+
{"🆕 Create": "create"},
|
|
521
|
+
{str(n): str(n) for n in range(1, 10)},
|
|
522
|
+
{"« Back": "back", "Next »": "next"},
|
|
523
|
+
widths=(-1, 3, -1), # or (1, 3, 2)
|
|
524
|
+
)
|
|
525
|
+
self.chain.set_inline_choice(keyboard, row_width)
|
|
526
|
+
```
|
|
@@ -40,15 +40,17 @@ class CallbackQueryHandler:
|
|
|
40
40
|
bot (TeleBot): The Telegram bot instance to be used for sending messages.
|
|
41
41
|
"""
|
|
42
42
|
cls.bot = bot
|
|
43
|
-
cls.user_button_callbacks: dict[int, dict[str, Callable[[
|
|
43
|
+
cls.user_button_callbacks: dict[int, dict[str, Callable[[CallbackQuery], None]]] = {}
|
|
44
44
|
|
|
45
45
|
@bot.callback_query_handler(func=lambda call: True)
|
|
46
|
-
def handle(call:
|
|
46
|
+
def handle(call: CallbackQuery) -> None:
|
|
47
47
|
if not call.data:
|
|
48
48
|
return
|
|
49
49
|
|
|
50
50
|
if call.data.startswith(cls.INLINE_BUTTON):
|
|
51
51
|
cls._handle_inline_button(call)
|
|
52
|
+
elif call.data.startswith(cls.STATIC_BUTTON):
|
|
53
|
+
cls._handle_static_button(call)
|
|
52
54
|
elif call.data.startswith(cls.SUGGEST):
|
|
53
55
|
cls._handle_suggestion(call)
|
|
54
56
|
else:
|
|
@@ -59,7 +61,7 @@ class CallbackQueryHandler:
|
|
|
59
61
|
# –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
60
62
|
|
|
61
63
|
@classmethod
|
|
62
|
-
def _handle_inline_button(cls, call:
|
|
64
|
+
def _handle_inline_button(cls, call: CallbackQuery):
|
|
63
65
|
if not call.data:
|
|
64
66
|
cls.bot.answer_callback_query(call.id, text=cls._invalid_data_answer[0], show_alert=cls._invalid_data_answer[1])
|
|
65
67
|
return
|
|
@@ -76,10 +78,19 @@ class CallbackQueryHandler:
|
|
|
76
78
|
cls.bot.answer_callback_query(call.id, text=cls._button_is_no_active_answer[0], show_alert=cls._button_is_no_active_answer[1])
|
|
77
79
|
return
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
if not getattr(callback, "_persistent", False):
|
|
82
|
+
cls.remove_user_button_callbacks(call.from_user.id)
|
|
80
83
|
|
|
81
84
|
callback(call)
|
|
82
85
|
|
|
86
|
+
# –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
87
|
+
# Static Buttons Handling
|
|
88
|
+
# –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def _handle_static_button(cls, call: CallbackQuery):
|
|
92
|
+
cls.bot.answer_callback_query(call.id, text="")
|
|
93
|
+
|
|
83
94
|
# –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
84
95
|
# Suggestion Handling
|
|
85
96
|
# –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
@@ -87,7 +98,7 @@ class CallbackQueryHandler:
|
|
|
87
98
|
@classmethod
|
|
88
99
|
def _handle_suggestion(cls, call: CallbackQuery):
|
|
89
100
|
text: str = str(call.data)[len(cls.SUGGEST):]
|
|
90
|
-
cls.simulate(call.message, text, from_user=call.from_user)
|
|
101
|
+
cls.simulate(call.message, text, from_user=call.from_user) # pyright: ignore[reportArgumentType]
|
|
91
102
|
cls.bot.answer_callback_query(call.id)
|
|
92
103
|
|
|
93
104
|
@classmethod
|
|
@@ -126,9 +137,10 @@ class CallbackQueryHandler:
|
|
|
126
137
|
# Query Types
|
|
127
138
|
# –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
128
139
|
|
|
140
|
+
STATIC_BUTTON: str = "static_button"
|
|
129
141
|
INLINE_BUTTON: str = "inline_button:"
|
|
130
142
|
SUGGEST: str = "suggest:"
|
|
131
|
-
|
|
143
|
+
|
|
132
144
|
@classmethod
|
|
133
145
|
def suggest(cls, suggestion: str):
|
|
134
146
|
return f"{cls.SUGGEST}{suggestion}"
|
|
@@ -40,8 +40,7 @@ if typing.TYPE_CHECKING:
|
|
|
40
40
|
class ChainInlineKeyboardLogic(ChainBase):
|
|
41
41
|
def set_inline_keyboard(
|
|
42
42
|
self,
|
|
43
|
-
keyboard: dict[str,
|
|
44
|
-
*,
|
|
43
|
+
keyboard: dict[str, Any],
|
|
45
44
|
row_width: int | Iterable[int] = 1
|
|
46
45
|
) -> None:
|
|
47
46
|
"""
|
|
@@ -118,7 +117,6 @@ class ChainInlineKeyboardLogic(ChainBase):
|
|
|
118
117
|
def inline_keyboard[Caption: str, Value](
|
|
119
118
|
self,
|
|
120
119
|
keyboard: dict[Caption, Value],
|
|
121
|
-
*,
|
|
122
120
|
row_width: int | Iterable[int] = 1,
|
|
123
121
|
enable_special_buttons: bool = True
|
|
124
122
|
) -> Callable[[Callable[[Value], None]], None]:
|
|
@@ -183,7 +181,6 @@ class ChainInlineKeyboardLogic(ChainBase):
|
|
|
183
181
|
def inline_choice(
|
|
184
182
|
self,
|
|
185
183
|
choices: list[Any] | tuple[Any, ...] | dict[str, Any | InlineButton],
|
|
186
|
-
*,
|
|
187
184
|
row_width: int | Iterable[int] = 1,
|
|
188
185
|
enable_special_buttons: bool = True
|
|
189
186
|
) -> Callable[[Callable[[Any], None]], None]:
|
|
@@ -219,7 +216,6 @@ class ChainInlineKeyboardLogic(ChainBase):
|
|
|
219
216
|
self,
|
|
220
217
|
func: Callable[[Any], None],
|
|
221
218
|
choices: list[Any] | tuple[Any, ...] | dict[str, Any | InlineButton],
|
|
222
|
-
*,
|
|
223
219
|
row_width: int | Iterable[int] = 1,
|
|
224
220
|
enable_special_buttons: bool = True
|
|
225
221
|
) -> None:
|
|
@@ -27,13 +27,15 @@ from telebot import TeleBot
|
|
|
27
27
|
from ._callback_query_handler import CallbackQueryHandler
|
|
28
28
|
|
|
29
29
|
__all__ = [
|
|
30
|
-
"InlineButton",
|
|
30
|
+
"InlineButton",
|
|
31
31
|
|
|
32
|
+
"StaticButton",
|
|
32
33
|
"LinkButton",
|
|
33
34
|
"WebAppButton",
|
|
34
35
|
"SuggestButton",
|
|
35
36
|
"CopyTextButton",
|
|
36
37
|
"CallbackButton",
|
|
38
|
+
"AnswerButton",
|
|
37
39
|
"AlertButton",
|
|
38
40
|
"NotificationButton",
|
|
39
41
|
"InvokeButton",
|
|
@@ -57,9 +59,11 @@ class InlineButton:
|
|
|
57
59
|
- `SuggestButton`
|
|
58
60
|
- `CopyTextButton`
|
|
59
61
|
- `CallbackButton`
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
- `InvokeButton`
|
|
63
|
+
- `AnswerButton`
|
|
64
|
+
- `AlertButton`
|
|
65
|
+
- `NotificationButton`
|
|
66
|
+
- `StaticButton`
|
|
63
67
|
"""
|
|
64
68
|
|
|
65
69
|
_bot: TeleBot
|
|
@@ -70,12 +74,14 @@ class InlineButton:
|
|
|
70
74
|
|
|
71
75
|
MAX_SIZE: int | None = None
|
|
72
76
|
MIN_SIZE: int | None = None
|
|
73
|
-
|
|
77
|
+
|
|
78
|
+
Static: type["StaticButton"]
|
|
74
79
|
Link: type["LinkButton"]
|
|
75
80
|
WebApp: type["WebAppButton"]
|
|
76
81
|
Suggest: type["SuggestButton"]
|
|
77
82
|
CopyText: type["CopyTextButton"]
|
|
78
83
|
Callback: type["CallbackButton"]
|
|
84
|
+
Answer: type["AnswerButton"]
|
|
79
85
|
Alert: type["AlertButton"]
|
|
80
86
|
Notification: type["NotificationButton"]
|
|
81
87
|
Invoke: type["InvokeButton"]
|
|
@@ -115,7 +121,20 @@ class InlineButton:
|
|
|
115
121
|
return normalized
|
|
116
122
|
raise ValueError(f"Unknown style: {style!r}. Must be one of {_BUTTON_STYLES_LIST}")
|
|
117
123
|
raise TypeError(f"Style must be str, ButtonStyle, or None, got {type(style)}")
|
|
124
|
+
|
|
125
|
+
class StaticButton(InlineButton):
|
|
118
126
|
|
|
127
|
+
def __init__(self, *, style: str | None | ButtonStyle = None, **kwargs):
|
|
128
|
+
self._style = self._normalize_style(style)
|
|
129
|
+
self._kwargs = kwargs
|
|
130
|
+
|
|
131
|
+
def _compile(self, caption: str) -> InlineKeyboardButton:
|
|
132
|
+
return InlineKeyboardButton(
|
|
133
|
+
text=caption,
|
|
134
|
+
callback_data=CallbackQueryHandler.STATIC_BUTTON,
|
|
135
|
+
style=self._style,
|
|
136
|
+
**self._kwargs,
|
|
137
|
+
)
|
|
119
138
|
|
|
120
139
|
class LinkButton(InlineButton):
|
|
121
140
|
"""
|
|
@@ -387,7 +406,77 @@ class CallbackButton(InlineButton):
|
|
|
387
406
|
callback=callback,
|
|
388
407
|
)
|
|
389
408
|
|
|
390
|
-
class
|
|
409
|
+
class AnswerButton(CallbackButton):
|
|
410
|
+
|
|
411
|
+
class _CallbackInvoker(CallbackButton._CallbackInvoker):
|
|
412
|
+
def __init__(
|
|
413
|
+
self,
|
|
414
|
+
chain_callback: Callable[[], None],
|
|
415
|
+
|
|
416
|
+
answer_text: str | None = None,
|
|
417
|
+
answer_as_alert: bool = True,
|
|
418
|
+
|
|
419
|
+
persistent: bool = True,
|
|
420
|
+
|
|
421
|
+
style: str | None | ButtonStyle = None,
|
|
422
|
+
|
|
423
|
+
kwargs: dict[str, Any] = {}
|
|
424
|
+
):
|
|
425
|
+
self._answer_text = answer_text
|
|
426
|
+
self._answer_as_alert = answer_as_alert
|
|
427
|
+
|
|
428
|
+
self._persistent = persistent
|
|
429
|
+
|
|
430
|
+
self._style = style
|
|
431
|
+
|
|
432
|
+
self._kwargs = {"style": style} | kwargs
|
|
433
|
+
|
|
434
|
+
self._chain_callback: Callable[[], None] = chain_callback
|
|
435
|
+
|
|
436
|
+
def _answer_callback_query(self, call: CallbackQuery):
|
|
437
|
+
if self._answer_text:
|
|
438
|
+
InlineButton._bot.answer_callback_query(
|
|
439
|
+
call.id,
|
|
440
|
+
self._answer_text,
|
|
441
|
+
self._answer_as_alert
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def __call__(self, call: CallbackQuery):
|
|
445
|
+
if not self._persistent:
|
|
446
|
+
self._invoke_chain_callback()
|
|
447
|
+
self._answer_callback_query(call)
|
|
448
|
+
|
|
449
|
+
def __init__(
|
|
450
|
+
self,
|
|
451
|
+
answer_text: str | None = None,
|
|
452
|
+
answer_as_alert: bool = True,
|
|
453
|
+
|
|
454
|
+
persistent: bool = True,
|
|
455
|
+
|
|
456
|
+
style: str | None | ButtonStyle = None,
|
|
457
|
+
|
|
458
|
+
**kwargs
|
|
459
|
+
):
|
|
460
|
+
self._answer_text = answer_text
|
|
461
|
+
self._answer_as_alert = answer_as_alert
|
|
462
|
+
|
|
463
|
+
self._persistent = persistent
|
|
464
|
+
|
|
465
|
+
self._style = self._normalize_style(style)
|
|
466
|
+
|
|
467
|
+
self._kwargs = kwargs
|
|
468
|
+
|
|
469
|
+
def build_invoker(self, chain_callback: Callable[[], None]) -> _CallbackInvoker:
|
|
470
|
+
return self._CallbackInvoker(
|
|
471
|
+
chain_callback=chain_callback,
|
|
472
|
+
answer_text=self._answer_text,
|
|
473
|
+
answer_as_alert=self._answer_as_alert,
|
|
474
|
+
persistent=self._persistent,
|
|
475
|
+
style=self._style,
|
|
476
|
+
kwargs=self._kwargs
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
class AlertButton(AnswerButton):
|
|
391
480
|
"""
|
|
392
481
|
An inline keyboard button that shows a popup alert when pressed and terminates the chain.
|
|
393
482
|
|
|
@@ -424,19 +513,19 @@ class AlertButton(CallbackButton):
|
|
|
424
513
|
def __init__(
|
|
425
514
|
self,
|
|
426
515
|
text: str | None = None,
|
|
427
|
-
|
|
516
|
+
persistent: bool = True,
|
|
428
517
|
style: str | None | ButtonStyle = None,
|
|
429
518
|
**kwargs
|
|
430
519
|
):
|
|
431
520
|
super().__init__(
|
|
432
|
-
callback=None,
|
|
433
521
|
answer_text=text,
|
|
434
522
|
answer_as_alert=True,
|
|
523
|
+
persistent=persistent,
|
|
435
524
|
style=style,
|
|
436
525
|
**kwargs
|
|
437
526
|
)
|
|
438
527
|
|
|
439
|
-
class NotificationButton(
|
|
528
|
+
class NotificationButton(AnswerButton):
|
|
440
529
|
"""
|
|
441
530
|
An inline keyboard button that shows a brief notification at the top of the chat screen
|
|
442
531
|
when pressed and terminates the chain.
|
|
@@ -475,13 +564,14 @@ class NotificationButton(CallbackButton):
|
|
|
475
564
|
self,
|
|
476
565
|
text: str | None = None,
|
|
477
566
|
*,
|
|
567
|
+
persistent: bool = True,
|
|
478
568
|
style: str | None | ButtonStyle = None,
|
|
479
569
|
**kwargs
|
|
480
570
|
):
|
|
481
571
|
super().__init__(
|
|
482
|
-
callback=None,
|
|
483
572
|
answer_text=text,
|
|
484
573
|
answer_as_alert=False,
|
|
574
|
+
persistent=persistent,
|
|
485
575
|
style=style,
|
|
486
576
|
**kwargs
|
|
487
577
|
)
|
|
@@ -611,10 +701,12 @@ class InvokeButton(CallbackButton):
|
|
|
611
701
|
)
|
|
612
702
|
|
|
613
703
|
InlineButton.Link = LinkButton
|
|
704
|
+
InlineButton.Static = StaticButton
|
|
614
705
|
InlineButton.WebApp = WebAppButton
|
|
615
706
|
InlineButton.Suggest = SuggestButton
|
|
616
707
|
InlineButton.CopyText = CopyTextButton
|
|
617
708
|
InlineButton.Callback = CallbackButton
|
|
709
|
+
InlineButton.Answer = AnswerButton
|
|
618
710
|
InlineButton.Alert = AlertButton
|
|
619
711
|
InlineButton.Notification = NotificationButton
|
|
620
712
|
InlineButton.Invoke = InvokeButton
|
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import telekit
|
|
2
2
|
|
|
3
|
-
from telekit.inline_buttons import
|
|
3
|
+
from telekit.inline_buttons import AlertButton
|
|
4
4
|
|
|
5
5
|
spells_text = """
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
The Disarming Charm. It causes whatever the victim is holding to fly out of their hand. It became Harry Potter's signature spell.
|
|
9
|
-
|
|
10
|
-
# Wingardium Leviosa
|
|
6
|
+
# 🦋 Wingardium Leviosa
|
|
11
7
|
|
|
12
8
|
The Levitation Charm. Used to make objects fly. As Hermione Granger famously noted, it's "Levi-o-sa, not Levio-sar."
|
|
13
9
|
|
|
14
|
-
# Expecto Patronum
|
|
10
|
+
# 🧌 Expecto Patronum
|
|
15
11
|
|
|
16
12
|
The Patronus Charm. A highly advanced spell that conjures a silver guardian to protect the caster against Dementors.
|
|
17
13
|
|
|
18
|
-
# Alohomora
|
|
14
|
+
# 🗝️ Alohomora
|
|
19
15
|
|
|
20
16
|
The Unlocking Charm. Used to open doors and windows that are not protected by magic.
|
|
21
17
|
|
|
22
|
-
# Lumos
|
|
18
|
+
# 🪄 Lumos
|
|
23
19
|
|
|
24
20
|
The Wand-Lighting Charm. Illuminates the tip of the caster's wand, allowing them to see in the dark.
|
|
25
21
|
"""
|
|
@@ -48,11 +44,14 @@ class SpellsHandler(telekit.Handler):
|
|
|
48
44
|
|
|
49
45
|
self.chain.set_inline_keyboard(
|
|
50
46
|
{
|
|
51
|
-
title:
|
|
47
|
+
title: AlertButton(content)
|
|
52
48
|
for title, content in spells.items()
|
|
53
|
-
}
|
|
49
|
+
} | {"« Back": self.handle_back},
|
|
50
|
+
row_width=2
|
|
54
51
|
)
|
|
55
|
-
self.chain.set_remove_inline_keyboard(False)
|
|
56
52
|
|
|
57
53
|
self.chain.disable_timeout_warnings()
|
|
58
|
-
self.chain.edit()
|
|
54
|
+
self.chain.edit()
|
|
55
|
+
|
|
56
|
+
def handle_back(self):
|
|
57
|
+
self.handoff("StartHandler").handle()
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
from ._inline_buttons import (
|
|
21
21
|
InlineButton,
|
|
22
|
+
StaticButton,
|
|
22
23
|
|
|
23
24
|
LinkButton,
|
|
24
25
|
WebAppButton,
|
|
@@ -26,13 +27,16 @@ from ._inline_buttons import (
|
|
|
26
27
|
CopyTextButton,
|
|
27
28
|
|
|
28
29
|
CallbackButton,
|
|
30
|
+
InvokeButton,
|
|
31
|
+
|
|
32
|
+
AnswerButton,
|
|
29
33
|
AlertButton,
|
|
30
34
|
NotificationButton,
|
|
31
|
-
InvokeButton,
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
__all__ = [
|
|
35
38
|
"InlineButton",
|
|
39
|
+
"StaticButton",
|
|
36
40
|
|
|
37
41
|
"LinkButton",
|
|
38
42
|
"WebAppButton",
|
|
@@ -40,7 +44,9 @@ __all__ = [
|
|
|
40
44
|
"CopyTextButton",
|
|
41
45
|
|
|
42
46
|
"CallbackButton",
|
|
47
|
+
"InvokeButton",
|
|
48
|
+
|
|
49
|
+
"AnswerButton",
|
|
43
50
|
"AlertButton",
|
|
44
51
|
"NotificationButton",
|
|
45
|
-
"InvokeButton",
|
|
46
52
|
]
|
|
@@ -1,18 +1,67 @@
|
|
|
1
1
|
from typing import Callable, Any, Iterable
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
import telekit
|
|
4
|
-
from
|
|
5
|
+
from telekit.inline_buttons import StaticButton
|
|
6
|
+
from telekit.utils import compose_keyboard
|
|
5
7
|
|
|
6
8
|
@dataclass
|
|
7
9
|
class _NavButton:
|
|
8
10
|
start_index: int
|
|
9
11
|
|
|
10
12
|
class PaginatedChoice(telekit.Handler):
|
|
13
|
+
"""
|
|
14
|
+
A trait that adds a paginated inline keyboard to any handler.
|
|
15
|
+
|
|
16
|
+
Inherit alongside ``telekit.Handler`` to get the :meth:`paginated_choice`
|
|
17
|
+
method, which renders a multi-page inline keyboard from any iterable or dict.
|
|
18
|
+
|
|
19
|
+
Navigation buttons (``« Back``, ``Next »``) and an optional page indicator
|
|
20
|
+
are added automatically. Layout is handled via :func:`compose_keyboard`.
|
|
21
|
+
|
|
22
|
+
**Customising navigation labels** - override at the class level:
|
|
23
|
+
|
|
24
|
+
.. code-block:: python
|
|
25
|
+
|
|
26
|
+
class MyHandler(PaginatedChoice, telekit.Handler):
|
|
27
|
+
PAGINATED_CHOICE_BACK_LABEL = "« Back"
|
|
28
|
+
PAGINATED_CHOICE_NEXT_LABEL = "Next »"
|
|
29
|
+
PAGINATED_CHOICE_PAGE_LABEL = "{page} / {pages}"
|
|
30
|
+
|
|
31
|
+
**Class attributes:**
|
|
11
32
|
|
|
12
|
-
|
|
33
|
+
.. list-table::
|
|
34
|
+
:widths: 30 70
|
|
13
35
|
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
* - ``PAGINATED_CHOICE_BACK_LABEL``
|
|
37
|
+
- Label for the back button. Defaults to ``« Back``.
|
|
38
|
+
* - ``PAGINATED_CHOICE_NEXT_LABEL``
|
|
39
|
+
- Label for the next button. Defaults to ``Next »``.
|
|
40
|
+
* - ``PAGINATED_CHOICE_PAGE_LABEL``
|
|
41
|
+
- Label template for the page indicator. Defaults to ``{page} / {pages}``.
|
|
42
|
+
|
|
43
|
+
Example::
|
|
44
|
+
|
|
45
|
+
class LetterPickerHandler(PaginatedChoice, telekit.Handler):
|
|
46
|
+
|
|
47
|
+
def handle(self) -> None:
|
|
48
|
+
self.chain.sender.set_title("🔤 What is your initial?")
|
|
49
|
+
self.chain.sender.set_message("Pick the first letter of your name")
|
|
50
|
+
self.chain.sender.set_remove_text(False)
|
|
51
|
+
|
|
52
|
+
self.paginated_choice(
|
|
53
|
+
choices="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
54
|
+
on_choice=self.handle_letter,
|
|
55
|
+
row_width=5
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def handle_letter(self, letter: str) -> None:
|
|
59
|
+
...
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
PAGINATED_CHOICE_BACK_LABEL: str = "« Back"
|
|
63
|
+
PAGINATED_CHOICE_NEXT_LABEL: str = "Next »"
|
|
64
|
+
PAGINATED_CHOICE_PAGE_LABEL: str | None = "{page} / {pages}" # use .format(page=..., pages=...)
|
|
16
65
|
|
|
17
66
|
def paginated_choice[T](self, choices: dict[str, T] | Iterable[T], on_choice: Callable[[T], Any], on_update: Callable[[], Any] | None = None, row_width: int = 1, page_size: int = 10) -> None:
|
|
18
67
|
"""
|
|
@@ -56,44 +105,32 @@ class PaginatedChoice(telekit.Handler):
|
|
|
56
105
|
self._paginated_choice(0, _choices, on_choice, on_update, row_width, page_size)
|
|
57
106
|
|
|
58
107
|
def _paginated_choice[T](self, start: int, choices: dict[str, T], on_choice: Callable[[T], Any], on_update: Callable[[], Any] | None, row_width: int, page_size: int) -> None:
|
|
108
|
+
if on_update is not None:
|
|
109
|
+
on_update()
|
|
110
|
+
|
|
59
111
|
total = len(choices)
|
|
60
|
-
|
|
112
|
+
|
|
61
113
|
page = start // page_size + 1
|
|
62
114
|
pages = (total + page_size - 1) // page_size
|
|
63
|
-
page_items = list(choices.items())[start : start + page_size]
|
|
64
|
-
|
|
65
|
-
if on_update is not None:
|
|
66
|
-
on_update()
|
|
67
115
|
|
|
68
|
-
|
|
69
|
-
|
|
116
|
+
# choices
|
|
117
|
+
page_items = list(choices.items())[start : start + page_size]
|
|
70
118
|
|
|
71
119
|
# navigation row
|
|
72
120
|
has_back = start - page_size >= 0
|
|
73
121
|
has_next = start + page_size < total
|
|
74
122
|
|
|
75
|
-
nav: dict[str, T | _NavButton] = {}
|
|
123
|
+
nav: dict[str, T | _NavButton | StaticButton] = {}
|
|
76
124
|
|
|
77
125
|
if has_back:
|
|
78
|
-
nav[
|
|
79
|
-
|
|
80
|
-
|
|
126
|
+
nav[self.PAGINATED_CHOICE_BACK_LABEL] = \
|
|
127
|
+
_NavButton(start - page_size)
|
|
128
|
+
if has_back and has_next and self.PAGINATED_CHOICE_PAGE_LABEL:
|
|
129
|
+
nav[self.PAGINATED_CHOICE_PAGE_LABEL.format(page=page, pages=pages)] = \
|
|
130
|
+
StaticButton()
|
|
81
131
|
if has_next:
|
|
82
|
-
nav[
|
|
83
|
-
|
|
84
|
-
# each choice on its own row, nav row at the end
|
|
85
|
-
choices_count = len(keyboard)
|
|
86
|
-
nav_count = len(nav)
|
|
87
|
-
|
|
88
|
-
full_rows = choices_count // row_width
|
|
89
|
-
remainder = choices_count % row_width
|
|
90
|
-
|
|
91
|
-
if remainder > 0:
|
|
92
|
-
_row_width = (*([row_width] * full_rows), remainder, nav_count)
|
|
93
|
-
else:
|
|
94
|
-
_row_width = (*([row_width] * full_rows), nav_count)
|
|
95
|
-
|
|
96
|
-
keyboard.update(nav)
|
|
132
|
+
nav[self.PAGINATED_CHOICE_NEXT_LABEL] = \
|
|
133
|
+
_NavButton(start + page_size)
|
|
97
134
|
|
|
98
135
|
def _on_choice(choice: T | _NavButton):
|
|
99
136
|
if isinstance(choice, _NavButton):
|
|
@@ -103,7 +140,6 @@ class PaginatedChoice(telekit.Handler):
|
|
|
103
140
|
|
|
104
141
|
self.chain.set_inline_choice(
|
|
105
142
|
_on_choice,
|
|
106
|
-
|
|
107
|
-
row_width=_row_width,
|
|
143
|
+
*compose_keyboard(dict(page_items), nav, widths=(row_width, -1))
|
|
108
144
|
)
|
|
109
145
|
self.chain.edit()
|
|
@@ -3,7 +3,7 @@ import re
|
|
|
3
3
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from urllib.parse import urlencode, quote
|
|
6
|
-
from typing import Literal
|
|
6
|
+
from typing import Literal, Any, Iterable
|
|
7
7
|
|
|
8
8
|
ROOT_DIR = Path(__file__).resolve().parent # telekit/
|
|
9
9
|
|
|
@@ -245,6 +245,79 @@ def read_canvas_path(path: str = "canvas_path.txt") -> str:
|
|
|
245
245
|
with open(path) as f:
|
|
246
246
|
return f.readline().strip()
|
|
247
247
|
|
|
248
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
249
|
+
# Inline Keyboards
|
|
250
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
251
|
+
|
|
252
|
+
def compose_keyboard(
|
|
253
|
+
*groups: dict[str, Any],
|
|
254
|
+
widths: Iterable[int] = (1, -1),
|
|
255
|
+
) -> tuple[dict[str, Any], tuple[int, ...]]:
|
|
256
|
+
"""
|
|
257
|
+
Merge multiple button groups into a single keyboard dict with computed row widths.
|
|
258
|
+
|
|
259
|
+
Each group is laid out independently using its corresponding width from ``widths``.
|
|
260
|
+
A width of ``-1`` means "all buttons in one row" (i.e. ``len(group)``).
|
|
261
|
+
|
|
262
|
+
:param groups: One or more dicts mapping button labels to values.
|
|
263
|
+
:type groups: ``dict[str, Any]``
|
|
264
|
+
:param widths: Row width for each group. Must match the number of groups,
|
|
265
|
+
or be a single value applied to all groups.
|
|
266
|
+
Use ``-1`` to fit the entire group on one row.
|
|
267
|
+
:type widths: ``Iterable[int]``
|
|
268
|
+
:return: Merged keyboard dict and the computed row_width tuple.
|
|
269
|
+
:rtype: ``tuple[dict[str, Any], tuple[int, ...]]``
|
|
270
|
+
|
|
271
|
+
Example::
|
|
272
|
+
|
|
273
|
+
keyboard, row_width = compose_keyboard(
|
|
274
|
+
{"🆕 Create": "create"},
|
|
275
|
+
{str(n): str(n) for n in range(1, 10)},
|
|
276
|
+
{"« Back": "back", "Next »": "next"},
|
|
277
|
+
widths=(1, 3, -1),
|
|
278
|
+
)
|
|
279
|
+
chain.set_inline_choice(keyboard, row_width)
|
|
280
|
+
# row_width → (1, 3, 3, 3, 2)
|
|
281
|
+
# layout:
|
|
282
|
+
# | 🆕 Create |
|
|
283
|
+
# | 1 | 2 | 3 |
|
|
284
|
+
# | 4 | 5 | 6 |
|
|
285
|
+
# | 7 | 8 | 9 |
|
|
286
|
+
# | « Back | Next » |
|
|
287
|
+
"""
|
|
288
|
+
widths_list = list(widths)
|
|
289
|
+
|
|
290
|
+
if len(widths_list) == 1:
|
|
291
|
+
widths_list = widths_list * len(groups)
|
|
292
|
+
|
|
293
|
+
if len(widths_list) != len(groups):
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"Number of widths ({len(widths_list)}) must match "
|
|
296
|
+
f"number of groups ({len(groups)}) or be 1."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
keyboard: dict[str, Any] = {}
|
|
300
|
+
row_width: list[int] = []
|
|
301
|
+
|
|
302
|
+
for group, width in zip(groups, widths_list):
|
|
303
|
+
if not group:
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
count = len(group)
|
|
307
|
+
w = count if width == -1 else width
|
|
308
|
+
|
|
309
|
+
full_rows = count // w
|
|
310
|
+
remainder = count % w
|
|
311
|
+
|
|
312
|
+
row_width.extend([w] * full_rows)
|
|
313
|
+
if remainder:
|
|
314
|
+
row_width.append(remainder)
|
|
315
|
+
|
|
316
|
+
keyboard.update(group)
|
|
317
|
+
|
|
318
|
+
return keyboard, tuple(row_width)
|
|
319
|
+
|
|
320
|
+
|
|
248
321
|
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
249
322
|
# Link Generating
|
|
250
323
|
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telekit
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.0b2
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -379,7 +379,18 @@ It tries to make Telegram bot development easier.
|
|
|
379
379
|
|
|
380
380
|
---
|
|
381
381
|
|
|
382
|
-
# Changes in version 2.3.
|
|
382
|
+
# Changes in version 2.3.0b2
|
|
383
|
+
|
|
384
|
+
## Inline Buttons
|
|
385
|
+
|
|
386
|
+
| **Name** | **Description** |
|
|
387
|
+
| ---------------- | ------------------------------------------------------------------------------- |
|
|
388
|
+
| `StaticButton` | A non-interactive inline button that performs no action when pressed. |
|
|
389
|
+
| `AnswerButton` | A button that responds to a callback query with a notification or alert without executing custom logic. |
|
|
390
|
+
|
|
391
|
+
- `AnswerButton` and its subclasses (`AlertButton`, `NotificationButton`) now support the `persistent` parameter (Defauts to `True`):
|
|
392
|
+
- `True` — non-blocking hint (does not affect further interactions)
|
|
393
|
+
- `False` — terminates interaction after click
|
|
383
394
|
|
|
384
395
|
## Traits
|
|
385
396
|
|
|
@@ -419,7 +430,11 @@ Navigation buttons (`« Back`, `Next »`) are added automatically.
|
|
|
419
430
|
|
|
420
431
|
| **Member** | **Description** |
|
|
421
432
|
| -------------------- | ---------------------------------------------------------------------------- |
|
|
422
|
-
| `paginated_choice(choices, on_choice,
|
|
433
|
+
| `paginated_choice(choices, on_choice, ...)` | Display a paginated choice keyboard. |
|
|
434
|
+
| `PAGINATED_CHOICE_BACK_LABEL` | Label for the back navigation button. Defaults to `« Back`. |
|
|
435
|
+
| `PAGINATED_CHOICE_NEXT_LABEL` | Label for the next navigation button. Defaults to `Next »`. |
|
|
436
|
+
| `PAGINATED_CHOICE_PAGE_LABEL` | Label template for the page indicator button. Supports `{page}` and `{pages}` placeholders. Set to `None` to hide. Defaults to `{page} / {pages}`. |
|
|
437
|
+
|
|
423
438
|
|
|
424
439
|
```python
|
|
425
440
|
self.chain.sender.set_title("🔤 What is your initial?")
|
|
@@ -433,6 +448,15 @@ self.paginated_choice(
|
|
|
433
448
|
)
|
|
434
449
|
```
|
|
435
450
|
|
|
451
|
+
Override labels per handler to localise or restyle navigation:
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
class MyHandler(PaginatedChoice, telekit.Handler):
|
|
455
|
+
PAGINATED_CHOICE_BACK_LABEL = "⬅️ Назад"
|
|
456
|
+
PAGINATED_CHOICE_NEXT_LABEL = "Далі ➡️"
|
|
457
|
+
PAGINATED_CHOICE_PAGE_LABEL = None # hide page indicator
|
|
458
|
+
```
|
|
459
|
+
|
|
436
460
|
<details>
|
|
437
461
|
<summary>(Click to see the result)</summary>
|
|
438
462
|
<table>
|
|
@@ -447,10 +471,13 @@ If only one item is present, `on_choice` is called immediately without rendering
|
|
|
447
471
|
|
|
448
472
|
## Utils
|
|
449
473
|
|
|
450
|
-
| **Name**
|
|
451
|
-
|
|
|
452
|
-
| `load_env`
|
|
453
|
-
| `read_env_var`
|
|
474
|
+
| **Name** | **Description** |
|
|
475
|
+
| ------------------ | ------------------------------------------------------------------ |
|
|
476
|
+
| `load_env` | Load all key-value pairs from a `.env` file into a dictionary. |
|
|
477
|
+
| `read_env_var` | Read a single variable by name from a `.env` file. |
|
|
478
|
+
| `compose_keyboard` | Merge multiple button groups into a single inline keyboard with computed `row_width`. |
|
|
479
|
+
|
|
480
|
+
### Environment Utils
|
|
454
481
|
|
|
455
482
|
- `read_token` and `read_canvas_path` now support reading from `.env` files.
|
|
456
483
|
Pass `".env"` to use the default key, or `".env:KEY"` to specify a custom one:
|
|
@@ -462,3 +489,38 @@ read_token(".env:BOT_TOKEN") # reads BOT_TOKEN
|
|
|
462
489
|
read_canvas_path(".env") # reads CANVAS_PATH
|
|
463
490
|
read_canvas_path(".env:MY_CANVAS") # reads MY_CANVAS
|
|
464
491
|
```
|
|
492
|
+
|
|
493
|
+
### Inline Keyboard Utils
|
|
494
|
+
|
|
495
|
+
`compose_keyboard` combines multiple button groups into a single keyboard and automatically calculates `row_width` for each group.
|
|
496
|
+
|
|
497
|
+
Each group is laid out independently using its corresponding width from `widths`.
|
|
498
|
+
A width of `-1` means "all buttons in one row" (i.e. ``len(group)``).
|
|
499
|
+
|
|
500
|
+
| **Param** | **Type** | **Description** |
|
|
501
|
+
|-----------|------------------|---------------------------------------------|
|
|
502
|
+
| `groups` | `dict[str, Any]` | One or more button groups |
|
|
503
|
+
| `widths` | `Iterable[int]` | Row width per group or single value for all |
|
|
504
|
+
|
|
505
|
+
| **Returns** | **Type** |
|
|
506
|
+
|------------|----------|
|
|
507
|
+
| `keyboard` | `dict[str, Any]` |
|
|
508
|
+
| `row_width` | `tuple[int, ...]` |
|
|
509
|
+
|
|
510
|
+
```py
|
|
511
|
+
# row_width → (1, 3, 3, 3, 2)
|
|
512
|
+
# layout:
|
|
513
|
+
# | 🆕 Create |
|
|
514
|
+
# | 1 | 2 | 3 |
|
|
515
|
+
# | 4 | 5 | 6 |
|
|
516
|
+
# | 7 | 8 | 9 |
|
|
517
|
+
# | « Back | Next » |
|
|
518
|
+
|
|
519
|
+
keyboard, row_width = compose_keyboard(
|
|
520
|
+
{"🆕 Create": "create"},
|
|
521
|
+
{str(n): str(n) for n in range(1, 10)},
|
|
522
|
+
{"« Back": "back", "Next »": "next"},
|
|
523
|
+
widths=(-1, 3, -1), # or (1, 3, 2)
|
|
524
|
+
)
|
|
525
|
+
self.chain.set_inline_choice(keyboard, row_width)
|
|
526
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|