telekit 2.2.0a2__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {telekit-2.2.0a2/telekit.egg-info → telekit-2.3.0}/PKG-INFO +44 -28
  2. {telekit-2.2.0a2 → telekit-2.3.0}/README.md +41 -2
  3. {telekit-2.2.0a2 → telekit-2.3.0}/setup.py +1 -2
  4. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/__init__.py +6 -0
  5. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_callback_query_handler.py +18 -6
  6. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_chain.py +3 -5
  7. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_chain_base.py +9 -3
  8. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_chain_entry_logic.py +12 -0
  9. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_chain_inline_keyboards_logic.py +42 -25
  10. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_handler.py +7 -0
  11. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_init.py +1 -1
  12. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_inline_buttons.py +243 -12
  13. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_logger.py +42 -41
  14. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_state.py +15 -2
  15. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/mixin.py +2 -2
  16. telekit-2.3.0/telekit/_trait.py +10 -0
  17. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_version.py +1 -1
  18. telekit-2.3.0/telekit/debug.py +8 -0
  19. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/__init__.py +2 -1
  20. telekit-2.3.0/telekit/example/example_handlers/calendar.py +155 -0
  21. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/entry.py +5 -1
  22. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/pages.py +12 -5
  23. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/spells.py +12 -13
  24. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/start.py +2 -1
  25. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/text_document.py +6 -2
  26. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/inline_buttons.py +8 -0
  27. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/senders.py +413 -102
  28. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/server.py +7 -5
  29. telekit-2.3.0/telekit/traits/__init__.py +3 -0
  30. telekit-2.3.0/telekit/traits/calendar_pick.py +807 -0
  31. telekit-2.3.0/telekit/traits/paginated_choice.py +145 -0
  32. telekit-2.3.0/telekit/traits/track_handoff_origin.py +99 -0
  33. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/types.py +43 -30
  34. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/utils.py +215 -15
  35. {telekit-2.2.0a2 → telekit-2.3.0/telekit.egg-info}/PKG-INFO +44 -28
  36. {telekit-2.2.0a2 → telekit-2.3.0}/telekit.egg-info/SOURCES.txt +8 -1
  37. {telekit-2.2.0a2 → telekit-2.3.0}/LICENSE +0 -0
  38. {telekit-2.2.0a2 → telekit-2.3.0}/setup.cfg +0 -0
  39. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_buildtext/__init__.py +0 -0
  40. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_buildtext/formatter.py +0 -0
  41. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_buildtext/styles.py +0 -0
  42. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_chapters/__init__.py +0 -0
  43. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_chapters/chapters.py +0 -0
  44. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_input_handler.py +0 -0
  45. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_on.py +0 -0
  46. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_snapvault/__init__.py +0 -0
  47. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_snapvault/snapcode.py +0 -0
  48. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_snapvault/snapvault.py +0 -0
  49. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/__init__.py +0 -0
  50. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  51. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/parser/builder.py +0 -0
  52. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  53. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  54. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  55. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/parser/parser.py +0 -0
  56. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/parser/token.py +0 -0
  57. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  58. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  59. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_timeout.py +0 -0
  60. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/_user.py +0 -0
  61. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/dices.py +0 -0
  62. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/__init__.py +0 -0
  63. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/complete_hotel.py +0 -0
  64. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/counter.py +0 -0
  65. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/dsl.py +0 -0
  66. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/faq.py +0 -0
  67. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/hotel.py +0 -0
  68. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/on_text.py +0 -0
  69. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/pyapi.py +0 -0
  70. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/qr.py +0 -0
  71. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_handlers/quiz.py +0 -0
  72. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/example/example_server.py +0 -0
  73. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/parameters.py +0 -0
  74. {telekit-2.2.0a2 → telekit-2.3.0}/telekit/styles.py +0 -0
  75. {telekit-2.2.0a2 → telekit-2.3.0}/telekit.egg-info/dependency_links.txt +0 -0
  76. {telekit-2.2.0a2 → telekit-2.3.0}/telekit.egg-info/requires.txt +0 -0
  77. {telekit-2.2.0a2 → telekit-2.3.0}/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.0a2
3
+ Version: 2.3.0
4
4
  Summary: Declarative, developer-friendly library for building Telegram bots
5
5
  Home-page: https://github.com/Romashkaa/telekit
6
6
  Author: romashka
@@ -77,7 +77,7 @@ Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/ma
77
77
 
78
78
  > See the [full example](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
79
79
 
80
- Even in its beta stage, Telekit accelerates bot development, offering typed command parameters, text styling via `Bold()`, `Italic()`, built-in emoji game results for `🎲 🎯 🏀 ⚽ 🎳 🎰`, and much more out of the box. Its declarative design makes bots easier to read, maintain, and extend.
80
+ Even in its beta stage, Telekit accelerates bot development, offering typed command parameters, text styling via `Bold()`, `Italic()`, a built-in declarative calendar picker, emoji game results for `🎲 🎯 🏀 ⚽ 🎳 🎰`, and much more out of the box. Its declarative design makes bots easier to read, maintain, and extend.
81
81
 
82
82
  **Key features:**
83
83
  - Declarative bot logic with **chains** for effortless handling of complex conversations
@@ -85,6 +85,7 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
85
85
  - Automatic handling of [message formatting](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/6_styles.md) via [Sender](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/5_senders.md) and **callback routing**
86
86
  - **Deep Linking** support with type-checked [Command Parameters](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/command_trigger_parameters.md) for flexible user input
87
87
  - Built-in **Permission** and **Logging** system for user management
88
+ - Reusable **Traits** system for pluggable, self-contained behavior modules
88
89
  - Seamless integration with [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI)
89
90
  - Fast to develop and easy-to-extend code
90
91
 
@@ -101,7 +102,8 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
101
102
  - [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
102
103
  - [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
103
104
  - [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)
105
+ - [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
106
+ - [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
105
107
  - [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
106
108
 
107
109
  ## Overview
@@ -347,6 +349,42 @@ telekit.Server(BOT_TOKEN).polling()
347
349
 
348
350
  You can find a [full quiz example](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md) and [DSL reference](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md) in the repository.
349
351
 
352
+ ### Traits
353
+
354
+ Traits are reusable behavior modules you can mix into any handler.
355
+
356
+ This example demonstrates the simplest way to use the built-in CalendarPick trait. It allows a user to pick a date from an inline calendar and handles the result via a callback.
357
+
358
+ ```py
359
+ from telekit.traits import CalendarPick
360
+
361
+ class CalendarHandler(CalendarPick, telekit.Handler):
362
+
363
+ @classmethod
364
+ def init_handler(cls) -> None:
365
+ cls.on.command("calendar").invoke(cls.handle)
366
+
367
+ def handle(self) -> None:
368
+ self.chain.sender.set_title("📅 Choose a date")
369
+ self.chain.sender.set_message("Select any date — past or future:")
370
+ self.chain.sender.set_remove_text(False)
371
+
372
+ self.calendar_pick(self.handle_date) # HERE
373
+
374
+ def handle_date(self, date: datetime.date) -> None:
375
+ self.chain.sender.set_text(f"You picked: {date}")
376
+ self.chain.send()
377
+ ```
378
+
379
+ <details>
380
+ <summary>Result</summary>
381
+ <table>
382
+ <tr>
383
+ <td><img src="./docs/images/calendar.png" alt="Telekit Calendar Example" width="500"></td>
384
+ </tr>
385
+ </table>
386
+ </details>
387
+
350
388
  ### Example Bot
351
389
 
352
390
  You can launch an example bot by **running the following code**:
@@ -367,6 +405,7 @@ It includes example commands, dialogs, keyboards, and style usage.
367
405
  - **Styles API** for rich text (`Bold`, `Italic`, `Links`) with **automatic escaping**.
368
406
  - Deep linking and **typed command parameters**.
369
407
  - **Built-in DSL** for menus, FAQs, and simple bots.
408
+ - Reusable **Traits** for composable, plug-and-play behavior (for example, a built-in declarative calendar picker).
370
409
  - **Zero-code** [Obsidian Canvas](https://github.com/Romashkaa/telekit/blob/main/docs/examples/canvas_faq.md) mode.
371
410
  - Seamless integration with **pyTelegramBotAPI**.
372
411
 
@@ -378,29 +417,6 @@ It tries to make Telegram bot development easier.
378
417
 
379
418
  ---
380
419
 
381
- # Changes in version 2.2.0a2
382
-
383
- ## New Button Types
384
-
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
-
390
- ## User Improvements
391
-
392
- | **Name** | **Description** |
393
- | ---------------------- | --------------------------------------------------------- |
394
- | `bio` | Bio of the user or description of the chat. |
395
- | `birthdate` | Birthdate of the user, if set and visible. |
396
- | `description` | Description of the group or channel. |
397
- | `mention` | `tg://user?id=` deep link, works even without a username. |
398
- | `is_private` | Whether the message was sent in a private chat. |
399
- | `is_group` | Whether the message was sent in a group. |
400
- | `is_supergroup` | Whether the message was sent in a supergroup. |
401
- | `is_channel` | Whether the message was sent in a channel. |
402
- | `avatar` | File ID of the user's most recent profile photo. |
403
- | `profile_photos_count` | Total number of profile photos the user has set. |
420
+ # Changes in version 2.3.0
404
421
 
405
- - 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.
406
- - Added `__repr__`.
422
+ [View changelog on GitHub](https://github.com/Romashkaa/telekit/blob/main/CHANGELOG.md)
@@ -41,7 +41,7 @@ Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/ma
41
41
 
42
42
  > See the [full example](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
43
43
 
44
- Even in its beta stage, Telekit accelerates bot development, offering typed command parameters, text styling via `Bold()`, `Italic()`, built-in emoji game results for `🎲 🎯 🏀 ⚽ 🎳 🎰`, and much more out of the box. Its declarative design makes bots easier to read, maintain, and extend.
44
+ Even in its beta stage, Telekit accelerates bot development, offering typed command parameters, text styling via `Bold()`, `Italic()`, a built-in declarative calendar picker, emoji game results for `🎲 🎯 🏀 ⚽ 🎳 🎰`, and much more out of the box. Its declarative design makes bots easier to read, maintain, and extend.
45
45
 
46
46
  **Key features:**
47
47
  - Declarative bot logic with **chains** for effortless handling of complex conversations
@@ -49,6 +49,7 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
49
49
  - Automatic handling of [message formatting](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/6_styles.md) via [Sender](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/5_senders.md) and **callback routing**
50
50
  - **Deep Linking** support with type-checked [Command Parameters](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/command_trigger_parameters.md) for flexible user input
51
51
  - Built-in **Permission** and **Logging** system for user management
52
+ - Reusable **Traits** system for pluggable, self-contained behavior modules
52
53
  - Seamless integration with [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI)
53
54
  - Fast to develop and easy-to-extend code
54
55
 
@@ -65,7 +66,8 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
65
66
  - [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
66
67
  - [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
67
68
  - [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)
69
+ - [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
70
+ - [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
69
71
  - [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
70
72
 
71
73
  ## Overview
@@ -311,6 +313,42 @@ telekit.Server(BOT_TOKEN).polling()
311
313
 
312
314
  You can find a [full quiz example](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md) and [DSL reference](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md) in the repository.
313
315
 
316
+ ### Traits
317
+
318
+ Traits are reusable behavior modules you can mix into any handler.
319
+
320
+ This example demonstrates the simplest way to use the built-in CalendarPick trait. It allows a user to pick a date from an inline calendar and handles the result via a callback.
321
+
322
+ ```py
323
+ from telekit.traits import CalendarPick
324
+
325
+ class CalendarHandler(CalendarPick, telekit.Handler):
326
+
327
+ @classmethod
328
+ def init_handler(cls) -> None:
329
+ cls.on.command("calendar").invoke(cls.handle)
330
+
331
+ def handle(self) -> None:
332
+ self.chain.sender.set_title("📅 Choose a date")
333
+ self.chain.sender.set_message("Select any date — past or future:")
334
+ self.chain.sender.set_remove_text(False)
335
+
336
+ self.calendar_pick(self.handle_date) # HERE
337
+
338
+ def handle_date(self, date: datetime.date) -> None:
339
+ self.chain.sender.set_text(f"You picked: {date}")
340
+ self.chain.send()
341
+ ```
342
+
343
+ <details>
344
+ <summary>Result</summary>
345
+ <table>
346
+ <tr>
347
+ <td><img src="./docs/images/calendar.png" alt="Telekit Calendar Example" width="500"></td>
348
+ </tr>
349
+ </table>
350
+ </details>
351
+
314
352
  ### Example Bot
315
353
 
316
354
  You can launch an example bot by **running the following code**:
@@ -331,6 +369,7 @@ It includes example commands, dialogs, keyboards, and style usage.
331
369
  - **Styles API** for rich text (`Bold`, `Italic`, `Links`) with **automatic escaping**.
332
370
  - Deep linking and **typed command parameters**.
333
371
  - **Built-in DSL** for menus, FAQs, and simple bots.
372
+ - Reusable **Traits** for composable, plug-and-play behavior (for example, a built-in declarative calendar picker).
334
373
  - **Zero-code** [Obsidian Canvas](https://github.com/Romashkaa/telekit/blob/main/docs/examples/canvas_faq.md) mode.
335
374
  - Seamless integration with **pyTelegramBotAPI**.
336
375
 
@@ -28,8 +28,7 @@ def readme():
28
28
  return f.read()
29
29
 
30
30
  def changelog():
31
- with open('CHANGELOG.md', 'r') as f:
32
- return f.read()
31
+ return f"[View changelog on GitHub](https://github.com/Romashkaa/telekit/blob/main/CHANGELOG.md)"
33
32
 
34
33
  def install_requires():
35
34
  with open('telekit/requirements.txt', 'r') as f:
@@ -18,6 +18,7 @@
18
18
  #
19
19
 
20
20
  from ._handler import Handler
21
+ from ._trait import Trait
21
22
  from ._chain import Chain
22
23
  from ._callback_query_handler import CallbackQueryHandler
23
24
  from .server import Server, example
@@ -35,12 +36,15 @@ from . import parameters
35
36
  from . import inline_buttons
36
37
  from . import dices
37
38
  from . import utils
39
+ from . import traits
40
+ from . import debug
38
41
 
39
42
  Styles = styles.Styles
40
43
 
41
44
  from ._version import __version__
42
45
 
43
46
  __all__ = [
47
+ "traits",
44
48
  "utils",
45
49
  "senders",
46
50
  "types",
@@ -54,6 +58,7 @@ __all__ = [
54
58
 
55
59
  "Server",
56
60
  "Chain",
61
+ "Trait",
57
62
  "Handler",
58
63
  "CallbackQueryHandler",
59
64
 
@@ -64,4 +69,5 @@ __all__ = [
64
69
  "enable_file_logging",
65
70
  "chapters",
66
71
  "example",
72
+ "debug",
67
73
  ]
@@ -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}"
@@ -26,13 +26,11 @@ from telebot.types import Message
26
26
 
27
27
  # Local modules
28
28
  from . import senders
29
+ from .debug import Debug
29
30
  from .styles import TextEntity
30
31
 
31
- # Logging
32
- from ._logger import logger
33
- library = logger.library
34
-
35
32
  # Chain modules
33
+ from ._chain_base import library
36
34
  from ._chain_inline_keyboards_logic import ChainInlineKeyboardLogic
37
35
  from ._chain_entry_logic import ChainEntryLogic, TextDocument
38
36
 
@@ -157,7 +155,7 @@ class Chain(ChainInlineKeyboardLogic, ChainEntryLogic):
157
155
  self.sender.set_edit_message(None)
158
156
  self._previous_message = message
159
157
 
160
- if self._timeout_warnings_enabled and _handler and not _timeout:
158
+ if Debug.timeout_warnings and _handler and not _timeout:
161
159
  library.warning(
162
160
  "Next-message handler is active, but no timeout was set for the chain. "
163
161
  "This may cause the bot to wait indefinitely."
@@ -30,11 +30,14 @@ from . import senders
30
30
  from . import _input_handler
31
31
  from . import _timeout
32
32
 
33
+ # Logging
34
+ from .debug import Debug
35
+ from ._logger import logger
36
+ library = logger.library
37
+
33
38
  class ChainBase:
34
39
 
35
40
  bot: telebot.TeleBot
36
-
37
- _timeout_warnings_enabled: bool = True
38
41
 
39
42
  @classmethod
40
43
  def _init(cls, bot: telebot.TeleBot):
@@ -49,6 +52,8 @@ class ChainBase:
49
52
  def __init__(self, chat_id: int, *, previous_message: Message | None = None):
50
53
  self.chat_id = chat_id
51
54
  self.sender = senders.Sender(chat_id)
55
+ self.received = None
56
+
52
57
  self._handler = _input_handler.InputHandler(chat_id)
53
58
  self._previous_message = previous_message
54
59
  self._timeout_handler = _timeout.TimeoutHandler()
@@ -200,4 +205,5 @@ class ChainBase:
200
205
  # Timeout API
201
206
 
202
207
  def disable_timeout_warnings(self, value: bool = True) -> None:
203
- self._timeout_warnings_enabled = not value
208
+ """.. deprecated:: Set ``Debug.timeout_warnings`` directly."""
209
+ Debug.timeout_warnings = not value
@@ -68,6 +68,8 @@ class ChainEntryLogic(ChainBase):
68
68
  """
69
69
 
70
70
  def callback(message: Message) -> bool:
71
+ self.received = message
72
+
71
73
  if delete_user_response:
72
74
  self.sender.delete_message(message, True)
73
75
 
@@ -154,6 +156,8 @@ class ChainEntryLogic(ChainBase):
154
156
  """
155
157
 
156
158
  def callback(message: Message) -> bool:
159
+ self.received = message
160
+
157
161
  if delete_user_response:
158
162
  self.sender.delete_message(message, True)
159
163
 
@@ -246,6 +250,8 @@ class ChainEntryLogic(ChainBase):
246
250
  """
247
251
 
248
252
  def callback(message: Message) -> bool:
253
+ self.received = message
254
+
249
255
  if delete_user_response:
250
256
  self.sender.delete_message(message, True)
251
257
 
@@ -340,6 +346,8 @@ class ChainEntryLogic(ChainBase):
340
346
  """
341
347
 
342
348
  def callback(message: Message) -> bool:
349
+ self.received = message
350
+
343
351
  if delete_user_response:
344
352
  self.sender.delete_message(message, True)
345
353
 
@@ -453,6 +461,8 @@ class ChainEntryLogic(ChainBase):
453
461
  """
454
462
 
455
463
  def callback(message: Message) -> bool:
464
+ self.received = message
465
+
456
466
  if delete_user_response:
457
467
  self.sender.delete_message(message, True)
458
468
 
@@ -592,6 +602,8 @@ class ChainEntryLogic(ChainBase):
592
602
  """
593
603
 
594
604
  def callback(message: Message) -> bool:
605
+ self.received = message
606
+
595
607
  if delete_user_response:
596
608
  self.sender.delete_message(message, True)
597
609
 
@@ -32,7 +32,7 @@ from telebot.types import (
32
32
 
33
33
  from ._callback_query_handler import CallbackQueryHandler
34
34
  from ._inline_buttons import InlineButton, CallbackButton
35
- from ._chain_base import ChainBase
35
+ from ._chain_base import ChainBase, library
36
36
 
37
37
  if typing.TYPE_CHECKING:
38
38
  from ._chain import Chain # only for type hints
@@ -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]:
@@ -151,15 +149,24 @@ class ChainInlineKeyboardLogic(ChainBase):
151
149
  buttons: list[InlineKeyboardButton] = []
152
150
 
153
151
  for index, (caption, value) in enumerate(keyboard.items()):
154
- if enable_special_buttons and isinstance(value, InlineButton):
152
+ if enable_special_buttons and isinstance(value, InlineButton) and not isinstance(value, CallbackButton):
155
153
  buttons.append(value._compile(caption))
156
154
  else:
155
+ if isinstance(value, CallbackButton):
156
+ invoker = value.build_invoker(self._cancel_timeout_and_handlers)
157
+ else:
158
+ invoker = self._get_invoker_with_argument(func, value)
159
+
157
160
  callback_data = CallbackQueryHandler.inline_button(f"{index}:{random.randint(1000, 9999)}")
158
- callback_functions[callback_data] = self._get_callback_with_argument(func, value)
161
+ callback_functions[callback_data] = invoker
162
+
163
+ kwargs: dict[str, Any] = getattr(invoker, "_kwargs", None) or {}
164
+
159
165
  buttons.append(
160
166
  InlineKeyboardButton(
161
167
  text=caption,
162
- callback_data=callback_data
168
+ callback_data=callback_data,
169
+ **kwargs
163
170
  )
164
171
  )
165
172
 
@@ -174,7 +181,6 @@ class ChainInlineKeyboardLogic(ChainBase):
174
181
  def inline_choice(
175
182
  self,
176
183
  choices: list[Any] | tuple[Any, ...] | dict[str, Any | InlineButton],
177
- *,
178
184
  row_width: int | Iterable[int] = 1,
179
185
  enable_special_buttons: bool = True
180
186
  ) -> Callable[[Callable[[Any], None]], None]:
@@ -210,7 +216,6 @@ class ChainInlineKeyboardLogic(ChainBase):
210
216
  self,
211
217
  func: Callable[[Any], None],
212
218
  choices: list[Any] | tuple[Any, ...] | dict[str, Any | InlineButton],
213
- *,
214
219
  row_width: int | Iterable[int] = 1,
215
220
  enable_special_buttons: bool = True
216
221
  ) -> None:
@@ -242,18 +247,34 @@ class ChainInlineKeyboardLogic(ChainBase):
242
247
  buttons: list[InlineKeyboardButton] = []
243
248
 
244
249
  if not isinstance(choices, dict):
250
+ for c in choices:
251
+ if type(c).__str__ is object.__str__:
252
+ library.warning(
253
+ f"{type(c).__name__} does not implement __str__. "
254
+ f"Consider passing a dict with explicit labels.",
255
+ stacklevel=3
256
+ )
245
257
  choices = {str(c): c for c in choices}
246
258
 
247
259
  for index, (caption, value) in enumerate(choices.items()):
248
- if enable_special_buttons and isinstance(value, InlineButton):
260
+ if enable_special_buttons and isinstance(value, InlineButton) and not isinstance(value, CallbackButton):
249
261
  buttons.append(value._compile(caption))
250
262
  else:
263
+ if isinstance(value, CallbackButton):
264
+ invoker = value.build_invoker(self._cancel_timeout_and_handlers)
265
+ else:
266
+ invoker = self._get_invoker_with_argument(func, value)
267
+
251
268
  callback_data = CallbackQueryHandler.inline_button(f"{index}:{random.randint(1000, 9999)}")
252
- callback_functions[callback_data] = self._get_callback_with_argument(func, value)
269
+ callback_functions[callback_data] = invoker
270
+
271
+ kwargs: dict[str, Any] = getattr(invoker, "_kwargs", None) or {}
272
+
253
273
  buttons.append(
254
274
  InlineKeyboardButton(
255
275
  text=caption,
256
- callback_data=callback_data
276
+ callback_data=callback_data,
277
+ **kwargs
257
278
  )
258
279
  )
259
280
 
@@ -314,21 +335,21 @@ class ChainInlineKeyboardLogic(ChainBase):
314
335
 
315
336
  self.sender.set_reply_markup(markup)
316
337
 
317
- def _get_callback_with_argument(self, func: Callable, argument: Any, query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
318
- def callback(call: CallbackQuery) -> None:
338
+ def _get_invoker_with_argument(self, callback: Callable, argument: Any, query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
339
+ def invoker(call: CallbackQuery) -> None:
319
340
  self._cancel_timeout_and_handlers()
320
- func(argument)
341
+ callback(argument)
321
342
  self._answer_callback_query(call, query_answer)
322
343
 
323
- return callback
344
+ return invoker
324
345
 
325
- def _get_callback(self, func: Callable[..., None], query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
326
- def callback(call: CallbackQuery):
346
+ def _get_invoker(self, callback: Callable[..., None], query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
347
+ def invoker(call: CallbackQuery):
327
348
  self._cancel_timeout_and_handlers()
328
- func()
349
+ callback()
329
350
  self._answer_callback_query(call, query_answer)
330
351
 
331
- return callback
352
+ return invoker
332
353
 
333
354
  def _answer_callback_query(self, call: CallbackQuery, query_answer: tuple[str, bool] | None = None):
334
355
  if query_answer is None:
@@ -369,8 +390,4 @@ class ChainInlineKeyboardLogic(ChainBase):
369
390
  rows.append(buttons[index:index + last_width])
370
391
  index += last_width
371
392
 
372
- return rows
373
-
374
- def _is_valid_telegram_callback(self, data: str) -> bool:
375
- byte_size = len(data.encode('utf-8'))
376
- return 1 <= byte_size <= 64
393
+ return rows
@@ -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
@@ -31,7 +31,7 @@ import telebot
31
31
  __all__ = ["init"]
32
32
 
33
33
  def init(bot: telebot.TeleBot) -> None:
34
- TelekitState.init(bot)
34
+ TelekitState._init(bot)
35
35
  BaseSender._init(bot)
36
36
  Handler._init(bot)
37
37
  Chain._init(bot)