telekit 2.2.0a3__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.2.0a3 → telekit-2.3.0b2}/PKG-INFO +138 -38
  2. {telekit-2.2.0a3 → telekit-2.3.0b2}/README.md +2 -1
  3. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/__init__.py +2 -0
  4. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_callback_query_handler.py +18 -6
  5. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_chain_inline_keyboards_logic.py +1 -5
  6. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_handler.py +7 -0
  7. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_inline_buttons.py +103 -11
  8. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_version.py +1 -1
  9. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/entry.py +5 -1
  10. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/spells.py +12 -13
  11. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/text_document.py +6 -2
  12. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/inline_buttons.py +8 -2
  13. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/senders.py +130 -39
  14. telekit-2.3.0b2/telekit/traits/__init__.py +2 -0
  15. telekit-2.3.0b2/telekit/traits/paginated_choice.py +145 -0
  16. telekit-2.3.0b2/telekit/traits/track_handoff_origin.py +99 -0
  17. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/utils.py +190 -15
  18. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit.egg-info/PKG-INFO +138 -38
  19. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit.egg-info/SOURCES.txt +4 -1
  20. {telekit-2.2.0a3 → telekit-2.3.0b2}/LICENSE +0 -0
  21. {telekit-2.2.0a3 → telekit-2.3.0b2}/setup.cfg +0 -0
  22. {telekit-2.2.0a3 → telekit-2.3.0b2}/setup.py +0 -0
  23. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_buildtext/__init__.py +0 -0
  24. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_buildtext/formatter.py +0 -0
  25. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_buildtext/styles.py +0 -0
  26. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_chain.py +0 -0
  27. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_chain_base.py +0 -0
  28. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_chain_entry_logic.py +0 -0
  29. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_chapters/__init__.py +0 -0
  30. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_chapters/chapters.py +0 -0
  31. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_init.py +0 -0
  32. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_input_handler.py +0 -0
  33. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_logger.py +0 -0
  34. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_on.py +0 -0
  35. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_snapvault/__init__.py +0 -0
  36. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_snapvault/snapcode.py +0 -0
  37. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_snapvault/snapvault.py +0 -0
  38. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_state.py +0 -0
  39. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/__init__.py +0 -0
  40. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/mixin.py +0 -0
  41. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  42. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/builder.py +0 -0
  43. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  44. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  45. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  46. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/parser.py +0 -0
  47. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/parser/token.py +0 -0
  48. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  49. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  50. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_timeout.py +0 -0
  51. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/_user.py +0 -0
  52. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/dices.py +0 -0
  53. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/__init__.py +0 -0
  54. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/__init__.py +0 -0
  55. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/complete_hotel.py +0 -0
  56. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/counter.py +0 -0
  57. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/dsl.py +0 -0
  58. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/faq.py +0 -0
  59. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/hotel.py +0 -0
  60. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/on_text.py +0 -0
  61. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/pages.py +0 -0
  62. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/pyapi.py +0 -0
  63. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/qr.py +0 -0
  64. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/quiz.py +0 -0
  65. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_handlers/start.py +0 -0
  66. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/example/example_server.py +0 -0
  67. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/parameters.py +0 -0
  68. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/server.py +0 -0
  69. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/styles.py +0 -0
  70. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit/types.py +0 -0
  71. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit.egg-info/dependency_links.txt +0 -0
  72. {telekit-2.2.0a3 → telekit-2.3.0b2}/telekit.egg-info/requires.txt +0 -0
  73. {telekit-2.2.0a3 → 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.2.0a3
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
@@ -101,7 +101,8 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
101
101
  - [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
102
102
  - [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
103
103
  - [Counter](https://github.com/Romashkaa/telekit/blob/main/docs/examples/counter.md)
104
- - [Quiz (Telekit DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
104
+ - [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
105
+ - [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
105
106
  - [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
106
107
 
107
108
  ## Overview
@@ -378,49 +379,148 @@ It tries to make Telegram bot development easier.
378
379
 
379
380
  ---
380
381
 
381
- # Changes in version 2.2.0a3
382
+ # Changes in version 2.3.0b2
382
383
 
383
- ## New Button Types
384
+ ## Inline Buttons
384
385
 
385
- | **Name** | **Description** |
386
- |----------------------|----------------------------------------------------------|
387
- | `AlertButton` | A callback button that shows a popup alert when pressed. |
388
- | `NotificationButton` | A callback button that shows a notification. |
389
- | `InvokeButton` | A callback button that calls the object method. |
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
390
 
391
- ## User Improvements
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
392
394
 
393
- | **Name** | **Description** |
394
- | ---------------------- | --------------------------------------------------------- |
395
- | `bio` | Bio of the user or description of the chat. |
396
- | `birthdate` | Birthdate of the user, if set and visible. |
397
- | `description` | Description of the group or channel. |
398
- | `mention` | `tg://user?id=` deep link, works even without a username. |
399
- | `is_private` | Whether the message was sent in a private chat. |
400
- | `is_group` | Whether the message was sent in a group. |
401
- | `is_supergroup` | Whether the message was sent in a supergroup. |
402
- | `is_channel` | Whether the message was sent in a channel. |
403
- | `avatar` | File ID of the user's most recent profile photo. |
404
- | `profile_photos_count` | Total number of profile photos the user has set. |
395
+ ## Traits
405
396
 
406
- - Refactor: `User` now accepts a `Message` object instead of `chat_id` + `from_user`. All properties are derived from `_sender` (`from_user` or `chat`) and migrated to `cached_property`. Fixed broken `get_id` and `get_full_name` references.
407
- - Added `__repr__`.
397
+ | **Name** | **Description** |
398
+ | -------------------------- | --------------------------------------------------------------------- |
399
+ | `TrackHandoffOrigin` | Tracks which handler transferred control to this one via `handoff()`. |
400
+ | `PaginatedChoice` | Adds a paginated inline keyboard for choosing from a list of items. |
408
401
 
409
- ## Sender Improvements
402
+ ### TrackHandoffOrigin
410
403
 
411
- | **Name** | **Description** |
412
- | -------------------------- | ------------------------------------------------------ |
413
- | `sent_message` | The last message sent by this sender instance. |
414
- | `disable_notification` | Disables notification sound when the message is sent. |
415
- | `protect_content` | Protects the message contents from forwarding and saving. |
416
- | `reply_parameters` | Reply parameters for the message to be sent. |
417
- | `link_preview_options` | Link preview options for the message to be sent. |
418
- | `show_caption_above_media` | Shows the caption above the media instead of below. |
404
+ `TrackHandoffOrigin` adds three members to any handler that inherits it:
419
405
 
420
- - Refactored sending system.
406
+ | **Member** | **Description** |
407
+ | ------------------- | -------------------------------------------------------------------------------- |
408
+ | `handoff_origin` | The handler instance that handed off to this one, or `None` if invoked directly. |
409
+ | `is_handed_off` | `True` if this handler was reached via `handoff()`, `False` otherwise. |
410
+ | `handoff_back()` | Transfer control back to the origin handler via `self.handoff_origin.handle()` |
411
+ | `handoff_back_or(handler)` | Like `handoff_back()`, but falls back to `handler` on fail. |
421
412
 
422
- ## Chain Improvements
413
+ Set `TRACK_HANDOFF_ORIGIN = False` on any subclass to opt out of tracking.
423
414
 
424
- | **Name** | **Description** |
425
- | ----------- | ------------------------------------------------------------ |
426
- | `received` | The message received from the user during entry processing. |
415
+ ```python
416
+ class MyHandler(TrackHandoffOrigin, telekit.Handler):
417
+
418
+ def handle(self):
419
+ ...
420
+ self.chain.set_inline_keyboard({
421
+ "« Back": self.handoff_back_or(StartHandler)
422
+ })
423
+ self.chain.edit()
424
+ ```
425
+
426
+ ### PaginatedChoice
427
+
428
+ Renders a paginated inline keyboard from any dict or iterable.
429
+ Navigation buttons (`« Back`, `Next »`) are added automatically.
430
+
431
+ | **Member** | **Description** |
432
+ | -------------------- | ---------------------------------------------------------------------------- |
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
+
438
+
439
+ ```python
440
+ self.chain.sender.set_title("🔤 What is your initial?")
441
+ self.chain.sender.set_message("Pick the first letter of your name")
442
+ self.chain.sender.set_remove_text(False)
443
+
444
+ self.paginated_choice(
445
+ choices="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
446
+ on_choice=self.handle_letter,
447
+ row_width=5
448
+ )
449
+ ```
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
+
460
+ <details>
461
+ <summary>(Click to see the result)</summary>
462
+ <table>
463
+ <tr>
464
+ <td><img src="https://github.com/Romashkaa/telekit/blob/main/docs/images/paginated_choice.png?raw=true" alt="Example" width="300"></td>
465
+ </tr>
466
+ </table>
467
+ </details>
468
+
469
+ `choices` accepts a `dict[str, T]`, or any `Iterable[T]`.
470
+ If only one item is present, `on_choice` is called immediately without rendering a keyboard.
471
+
472
+ ## Utils
473
+
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
481
+
482
+ - `read_token` and `read_canvas_path` now support reading from `.env` files.
483
+ Pass `".env"` to use the default key, or `".env:KEY"` to specify a custom one:
484
+
485
+ ```python
486
+ read_token(".env") # reads TOKEN
487
+ read_token(".env:BOT_TOKEN") # reads BOT_TOKEN
488
+
489
+ read_canvas_path(".env") # reads CANVAS_PATH
490
+ read_canvas_path(".env:MY_CANVAS") # reads MY_CANVAS
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
+ ```
@@ -65,7 +65,8 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
65
65
  - [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
66
66
  - [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
67
67
  - [Counter](https://github.com/Romashkaa/telekit/blob/main/docs/examples/counter.md)
68
- - [Quiz (Telekit DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
68
+ - [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
69
+ - [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
69
70
  - [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
70
71
 
71
72
  ## Overview
@@ -35,12 +35,14 @@ from . import parameters
35
35
  from . import inline_buttons
36
36
  from . import dices
37
37
  from . import utils
38
+ from . import traits
38
39
 
39
40
  Styles = styles.Styles
40
41
 
41
42
  from ._version import __version__
42
43
 
43
44
  __all__ = [
45
+ "traits",
44
46
  "utils",
45
47
  "senders",
46
48
  "types",
@@ -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:
@@ -225,9 +225,16 @@ class Handler:
225
225
 
226
226
  handler_instance = handler(self.message)
227
227
  handler_instance.chain._set_previous_message(self.chain.get_previous_message())
228
+ handler_instance._on_handoff(self)
228
229
 
229
230
  return handler_instance
230
231
 
232
+
233
+ def _on_handoff(self, origin: "Handler") -> None:
234
+ """Called when this handler is reached via handoff(). Override to customize."""
235
+ pass
236
+
237
+
231
238
  def freeze(self, func, *args):
232
239
  """
233
240
  Return a zero-argument callback that invokes the given function
@@ -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
  """
@@ -339,8 +358,8 @@ class CallbackButton(InlineButton):
339
358
 
340
359
  def __call__(self, call: CallbackQuery):
341
360
  self._invoke_chain_callback()
342
- self._invoke_callback()
343
361
  self._answer_callback_query(call)
362
+ self._invoke_callback()
344
363
 
345
364
  def __init__(
346
365
  self,
@@ -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.2.0a3"
6
+ __version__ = "2.3.0b2"
@@ -44,8 +44,12 @@ class EntryHandler(telekit.Handler):
44
44
  # Handling Logic
45
45
  # ------------------------------------------
46
46
 
47
+ def __init__(self, *args, **kwargs):
48
+ super().__init__(*args, **kwargs)
49
+
50
+ self._user_data = UserData(self.user.id)
51
+
47
52
  def handle(self) -> None:
48
- self._user_data = UserData(self.message.chat.id)
49
53
  self.chain.disable_timeout_warnings()
50
54
  self.entry_name()
51
55
 
@@ -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()
@@ -1,11 +1,10 @@
1
- from telebot.types import Message
2
1
  from telekit.types import (
3
2
  TextDocument, CopyTextButton, ParseMode
4
3
  )
5
4
  from telekit.styles import Quote, Escape
6
5
  import telekit
7
6
 
8
- class TextDocumentHandler(telekit.Handler):
7
+ class TextDocumentHandler(telekit.traits.TrackHandoffOrigin, telekit.Handler):
9
8
 
10
9
  @classmethod
11
10
  def init_handler(cls) -> None:
@@ -26,6 +25,11 @@ class TextDocumentHandler(telekit.Handler):
26
25
  self.handle_text_document,
27
26
  allowed_extensions=(".txt", ".py", ".md", ".html")
28
27
  )
28
+ self.chain.set_inline_keyboard(
29
+ {
30
+ "« Back": self.handoff_back_or("StartHandler")
31
+ }
32
+ )
29
33
 
30
34
  self.chain.disable_timeout_warnings()
31
35
  self.chain.edit()