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.
Files changed (73) hide show
  1. {telekit-2.3.0b1 → telekit-2.3.0b2}/PKG-INFO +69 -7
  2. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_callback_query_handler.py +18 -6
  3. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain_inline_keyboards_logic.py +1 -5
  4. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_inline_buttons.py +102 -10
  5. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_version.py +1 -1
  6. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/spells.py +12 -13
  7. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/inline_buttons.py +8 -2
  8. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/traits/paginated_choice.py +68 -32
  9. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/utils.py +74 -1
  10. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/PKG-INFO +69 -7
  11. {telekit-2.3.0b1 → telekit-2.3.0b2}/LICENSE +0 -0
  12. {telekit-2.3.0b1 → telekit-2.3.0b2}/README.md +0 -0
  13. {telekit-2.3.0b1 → telekit-2.3.0b2}/setup.cfg +0 -0
  14. {telekit-2.3.0b1 → telekit-2.3.0b2}/setup.py +0 -0
  15. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/__init__.py +0 -0
  16. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_buildtext/__init__.py +0 -0
  17. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_buildtext/formatter.py +0 -0
  18. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_buildtext/styles.py +0 -0
  19. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain.py +0 -0
  20. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain_base.py +0 -0
  21. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chain_entry_logic.py +0 -0
  22. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chapters/__init__.py +0 -0
  23. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_chapters/chapters.py +0 -0
  24. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_handler.py +0 -0
  25. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_init.py +0 -0
  26. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_input_handler.py +0 -0
  27. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_logger.py +0 -0
  28. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_on.py +0 -0
  29. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_snapvault/__init__.py +0 -0
  30. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_snapvault/snapcode.py +0 -0
  31. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_snapvault/snapvault.py +0 -0
  32. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_state.py +0 -0
  33. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/__init__.py +0 -0
  34. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/mixin.py +0 -0
  35. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  36. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/builder.py +0 -0
  37. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  38. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  39. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  40. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/parser.py +0 -0
  41. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/token.py +0 -0
  42. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  43. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  44. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_timeout.py +0 -0
  45. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/_user.py +0 -0
  46. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/dices.py +0 -0
  47. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/__init__.py +0 -0
  48. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/__init__.py +0 -0
  49. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/complete_hotel.py +0 -0
  50. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/counter.py +0 -0
  51. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/dsl.py +0 -0
  52. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/entry.py +0 -0
  53. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/faq.py +0 -0
  54. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/hotel.py +0 -0
  55. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/on_text.py +0 -0
  56. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/pages.py +0 -0
  57. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/pyapi.py +0 -0
  58. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/qr.py +0 -0
  59. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/quiz.py +0 -0
  60. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/start.py +0 -0
  61. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_handlers/text_document.py +0 -0
  62. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/example/example_server.py +0 -0
  63. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/parameters.py +0 -0
  64. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/senders.py +0 -0
  65. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/server.py +0 -0
  66. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/styles.py +0 -0
  67. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/traits/__init__.py +0 -0
  68. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/traits/track_handoff_origin.py +0 -0
  69. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit/types.py +0 -0
  70. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/SOURCES.txt +0 -0
  71. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/dependency_links.txt +0 -0
  72. {telekit-2.3.0b1 → telekit-2.3.0b2}/telekit.egg-info/requires.txt +0 -0
  73. {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.0b1
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.0b1
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, on_update, row_width)` | Display a paginated choice keyboard. |
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** | **Description** |
451
- | ---------------- | ------------------------------------------------------------------ |
452
- | `load_env` | Load all key-value pairs from a `.env` file into a dictionary. |
453
- | `read_env_var` | Read a single variable by name from a `.env` file. |
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[[telebot.types.CallbackQuery], None]]] = {}
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: telebot.types.CallbackQuery) -> None:
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: telebot.types.CallbackQuery):
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
- cls.remove_user_button_callbacks(call.from_user.id)
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, Callable[[], Any] | InlineButton | 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
- - `AlertButton`
61
- - `NotificationButton`
62
- - `InvokeButton`
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 AlertButton(CallbackButton):
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(CallbackButton):
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
@@ -3,4 +3,4 @@
3
3
  # PyPI history: https://pypi.org/project/telekit/#history
4
4
  # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
5
5
 
6
- __version__ = "2.3.0b1"
6
+ __version__ = "2.3.0b2"
@@ -1,25 +1,21 @@
1
1
  import telekit
2
2
 
3
- from telekit.inline_buttons import CallbackButton
3
+ from telekit.inline_buttons import AlertButton
4
4
 
5
5
  spells_text = """
6
- # Expelliarmus
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: CallbackButton(self.display_spells, answer_text=content)
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 dataclasses import dataclass
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
- # ── entry point ───────────────────────────────────────────────
33
+ .. list-table::
34
+ :widths: 30 70
13
35
 
14
- def __init__(self, *args, **kwargs):
15
- super().__init__(*args, **kwargs)
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
- keyboard: dict[str, T | _NavButton] = {}
69
- keyboard.update(page_items)
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["« Back"] = _NavButton(start - page_size)
79
- if has_back and has_next:
80
- nav[f"({page}/{pages})" ] = _NavButton(start)
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["Next »"] = _NavButton(start + page_size)
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
- keyboard,
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.0b1
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.0b1
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, on_update, row_width)` | Display a paginated choice keyboard. |
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** | **Description** |
451
- | ---------------- | ------------------------------------------------------------------ |
452
- | `load_env` | Load all key-value pairs from a `.env` file into a dictionary. |
453
- | `read_env_var` | Read a single variable by name from a `.env` file. |
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