telekit 2.3.0b3__tar.gz → 2.4.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 (83) hide show
  1. telekit-2.4.0/PKG-INFO +672 -0
  2. telekit-2.3.0b3/telekit.egg-info/PKG-INFO → telekit-2.4.0/README.md +128 -72
  3. {telekit-2.3.0b3 → telekit-2.4.0}/setup.py +4 -3
  4. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/__init__.py +9 -1
  5. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_callback_query_handler.py +15 -3
  6. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_chain.py +2 -2
  7. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_chain_base.py +1 -2
  8. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_chain_entry_logic.py +4 -4
  9. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_chain_inline_keyboards_logic.py +71 -2
  10. telekit-2.4.0/telekit/_inline_keyboard.py +597 -0
  11. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_input_handler.py +3 -2
  12. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_logger.py +46 -41
  13. telekit-2.4.0/telekit/_reply_keyboard.py +521 -0
  14. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/mixin.py +479 -5
  15. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_version.py +1 -1
  16. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/debug.py +1 -0
  17. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/calendar.py +11 -79
  18. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/counter.py +23 -19
  19. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/start.py +0 -1
  20. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/inline_buttons.py +4 -0
  21. telekit-2.4.0/telekit/reply_buttons.py +254 -0
  22. telekit-2.4.0/telekit/scheduler.py +167 -0
  23. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/senders.py +24 -6
  24. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/server.py +4 -6
  25. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/types.py +31 -1
  26. telekit-2.4.0/telekit.egg-info/PKG-INFO +672 -0
  27. {telekit-2.3.0b3 → telekit-2.4.0}/telekit.egg-info/SOURCES.txt +4 -0
  28. telekit-2.3.0b3/PKG-INFO +0 -384
  29. telekit-2.3.0b3/README.md +0 -342
  30. {telekit-2.3.0b3 → telekit-2.4.0}/LICENSE +0 -0
  31. {telekit-2.3.0b3 → telekit-2.4.0}/setup.cfg +0 -0
  32. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_buildtext/__init__.py +0 -0
  33. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_buildtext/formatter.py +0 -0
  34. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_buildtext/styles.py +0 -0
  35. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_chapters/__init__.py +0 -0
  36. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_chapters/chapters.py +0 -0
  37. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_handler.py +0 -0
  38. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_init.py +0 -0
  39. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_inline_buttons.py +0 -0
  40. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_on.py +0 -0
  41. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_snapvault/__init__.py +0 -0
  42. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_snapvault/snapcode.py +0 -0
  43. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_snapvault/snapvault.py +0 -0
  44. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_state.py +0 -0
  45. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/__init__.py +0 -0
  46. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  47. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/parser/builder.py +0 -0
  48. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  49. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  50. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  51. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/parser/parser.py +0 -0
  52. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/parser/token.py +0 -0
  53. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  54. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  55. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_timeout.py +0 -0
  56. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_trait.py +0 -0
  57. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/_user.py +0 -0
  58. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/dices.py +0 -0
  59. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/__init__.py +0 -0
  60. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/__init__.py +0 -0
  61. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/complete_hotel.py +0 -0
  62. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/dsl.py +0 -0
  63. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/entry.py +0 -0
  64. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/faq.py +0 -0
  65. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/hotel.py +0 -0
  66. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/on_text.py +0 -0
  67. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/pages.py +0 -0
  68. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/pyapi.py +0 -0
  69. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/qr.py +0 -0
  70. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/quiz.py +0 -0
  71. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/spells.py +0 -0
  72. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_handlers/text_document.py +0 -0
  73. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/example/example_server.py +0 -0
  74. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/parameters.py +0 -0
  75. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/styles.py +0 -0
  76. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/traits/__init__.py +0 -0
  77. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/traits/calendar_pick.py +0 -0
  78. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/traits/paginated_choice.py +0 -0
  79. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/traits/track_handoff_origin.py +0 -0
  80. {telekit-2.3.0b3 → telekit-2.4.0}/telekit/utils.py +0 -0
  81. {telekit-2.3.0b3 → telekit-2.4.0}/telekit.egg-info/dependency_links.txt +0 -0
  82. {telekit-2.3.0b3 → telekit-2.4.0}/telekit.egg-info/requires.txt +0 -0
  83. {telekit-2.3.0b3 → telekit-2.4.0}/telekit.egg-info/top_level.txt +0 -0
telekit-2.4.0/PKG-INFO ADDED
@@ -0,0 +1,672 @@
1
+ Metadata-Version: 2.4
2
+ Name: telekit
3
+ Version: 2.4.0
4
+ Summary: Declarative, developer-friendly library for building Telegram bots
5
+ Home-page: https://github.com/Romashkaa/telekit
6
+ Author: romashka
7
+ Author-email: notromashka@gmail.com
8
+ License: GPLv3
9
+ Project-URL: GitHub, https://github.com/Romashkaa/telekit
10
+ Project-URL: Telegram, https://t.me/TelekitLib
11
+ Keywords: telegram bot api declarative tools bot-api
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
17
+ Classifier: Operating System :: OS Independent
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: charset_normalizer==3.4.2
22
+ Requires-Dist: Jinja2==3.1.6
23
+ Requires-Dist: pyTelegramBotAPI==4.31.0
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: keywords
31
+ Dynamic: license-file
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
36
+
37
+ ![TeleKit](https://github.com/Romashkaa/images/blob/main/TeleKitWide.png?raw=true)
38
+
39
+ [![PyPI](https://img.shields.io/pypi/v/telekit.svg)](https://pypi.org/project/telekit/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/telekit.svg)](https://pypi.org/project/telekit/)
41
+ [![PyPI Downloads](https://static.pepy.tech/badge/telekit)](https://pepy.tech/project/telekit)
42
+
43
+ # Telekit
44
+
45
+ **Telekit** is a declarative, developer-friendly library for building Telegram bots. It gives developers a dedicated Sender for composing and sending messages and a Chain for handling dialogue between the user and the bot. The library also handles inline keyboards and callback routing automatically, letting you focus on the bot's behavior instead of repetitive tasks.
46
+
47
+ ```py
48
+ import telekit
49
+
50
+ class MyStartHandler(telekit.Handler):
51
+ @classmethod
52
+ def init_handler(cls):
53
+ cls.on.command('start').invoke(cls.handle_start)
54
+
55
+ def handle_start(self):
56
+ self.chain.sender.set_text("Hello!")
57
+ self.chain.sender.set_photo("robot.png")
58
+ self.chain.send()
59
+
60
+ telekit.Server("BOT_TOKEN").polling()
61
+ ```
62
+
63
+ > Send "Hello!" with a photo on `/start`
64
+
65
+ Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md), allowing developers to create fully interactive bots with minimal code. It also integrates [**Jinja**](https://github.com/Romashkaa/telekit/blob/main/docs/examples/jinja_engine.md), giving you loops, conditionals, expressions, and filters to generate dynamic content.
66
+
67
+ ```js
68
+ @ main {
69
+ title = "🎉 Fun Facts Quiz";
70
+ message = "Test your knowledge with 10 fun questions!";
71
+
72
+ buttons {
73
+ question_1("Start Quiz");
74
+ }
75
+ }
76
+ ```
77
+
78
+ > See the [full example](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
79
+
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
+
82
+ **Key features:**
83
+ - Declarative bot logic with **chains** for effortless handling of complex conversations
84
+ - [Ready-to-use DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md) for FAQs and other interactive scripts
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
+ - **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
+ - Built-in **Permission** and **Logging** system for user management
88
+ - Reusable **Traits** system for pluggable, self-contained behavior modules
89
+ - Seamless integration with [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI)
90
+ - Fast to develop and easy-to-extend code
91
+
92
+ [GitHub](https://github.com/Romashkaa/telekit)
93
+ [PyPI](https://pypi.org/project/telekit/)
94
+ [Telegram](https://t.me/NotRomashka)
95
+ [Community](https://t.me/+wu-dFrOBFIwyNzc0)
96
+
97
+ ## Contents
98
+
99
+ - 🌟 [Tutorial](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/0_tutorial.md)
100
+ - 🎆 [Gallery](https://github.com/Romashkaa/telekit/blob/main/docs/documentation/gallery.md)
101
+ - 👀 [Examples](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
102
+ - [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
103
+ - [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
104
+ - [Counter](https://github.com/Romashkaa/telekit/blob/main/docs/examples/counter.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)
107
+ - [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
108
+
109
+ ## Overview
110
+
111
+ **Telekit** is a library for building Telegram bots where dialogs look like normal method calls. No bulky state machines. No scattered handlers.
112
+
113
+ The idea is simple: you point to the next step — Telekit calls it when the user replies.
114
+
115
+ ### Entries
116
+
117
+ No state machines. Just tell Telekit which method should handle the next user message.
118
+
119
+ ```py
120
+ def handle(self):
121
+ self.chain.sender.set_text("👋 Hello! What is your name?")
122
+ self.chain.set_entry_text(self.handle_name)
123
+ self.chain.send()
124
+
125
+ def handle_name(self, name: str):
126
+ self.chain.sender.set_text(f"Nice to meet you, {name}!")
127
+ self.chain.send()
128
+ ```
129
+
130
+ The `handle` method sends a message and registers `handle_name` as the next step using `set_entry_text`. When the user replies, Telekit automatically calls `handle_name` and passes the user's message as a plain `str` argument.
131
+
132
+ > That's it. No enums. No manual state tracking. No boilerplate.
133
+
134
+ ### Inline Keyboards
135
+
136
+ The fastest way to add buttons to a message. Pass a plain `dict` where each key is the button label and each value is the callback to invoke when pressed:
137
+
138
+ ```python
139
+ self.chain.set_inline_keyboard(
140
+ {
141
+ "✏️ Change": self.change_name,
142
+ "❌ Delete": self.delete,
143
+ }
144
+ )
145
+ ```
146
+
147
+ `row_width` controls how many buttons appear per row:
148
+
149
+ ```python
150
+ self.chain.set_inline_keyboard(
151
+ {
152
+ "One": self.one,
153
+ "Two": self.two,
154
+ "Three": self.three,
155
+ "Four": self.four,
156
+ "Five": self.five,
157
+ },
158
+ row_width=(3, 2) # first row: 3 buttons, second row: 2
159
+ )
160
+ ```
161
+
162
+ ```
163
+ ╭──────────┬──────────┬──────────╮
164
+ │ One │ Two │ Three │
165
+ ├──────────┴──┬───────┴──────────┤
166
+ │ Four │ Five │
167
+ ╰─────────────┴──────────────────╯
168
+ ```
169
+
170
+ **Need more control?**
171
+
172
+ When you need precise row layout or conditional buttons, use `InlineKeyboard` — a fluent builder that composes keyboards step by step:
173
+
174
+ ```python
175
+ self.chain.set_keyboard(
176
+ InlineKeyboard()
177
+ .add_callback("-", self.decrement, style="danger")
178
+ .add_callback("+", self.increment, style="success")
179
+ .row()
180
+ .add_callback("↺ Reset", self.reset)
181
+ )
182
+ ```
183
+
184
+ ```
185
+ ╭──────────┬──────────╮
186
+ │ - │ + │
187
+ ├──────────┴──────────┤
188
+ │ ↺ Reset │
189
+ ╰─────────────────────╯
190
+ ```
191
+
192
+ > `InlineKeyboard` is built by chaining method calls. Just call `.row()` to start a new row.
193
+
194
+ **Not just callback buttons**
195
+
196
+ | **Method** | **Description** |
197
+ | --------------------- | ---------------------------------------------------------------- |
198
+ | `add_callback(...)` | Button that fires a callback function. |
199
+ | `add_link(...)` | Button that opens a URL. |
200
+ | `add_copy(...)` | Button that copies text to the clipboard. |
201
+ | `add_alert(...)` | Button that shows a popup alert dialog. |
202
+ | `add_notification(...)` | Button that shows a brief top-of-chat notification. |
203
+ | `add_static(...)` | Decorative button with no action. |
204
+ | `add_webapp(...)` | Button that opens a Telegram Mini App. |
205
+ | `add_suggest(...)` | Button that simulates the user sending a message. |
206
+
207
+ ### Reply Keyboards
208
+
209
+ Unlike inline keyboards, reply keyboards replace the user's system keyboard with buttons shown at the bottom of the chat. Tapping a button either sends its text as a regular message or triggers a system action, such as sharing a phone number or location.
210
+
211
+ ```python
212
+ self.chain.set_keyboard(
213
+ ReplyKeyboard(one_time_keyboard=True)
214
+ .add_text("Hello!")
215
+ .add_text("Hi")
216
+ .row()
217
+ .add_contact("📱 Share phone")
218
+ .add_location("📍 Share location")
219
+ )
220
+ ```
221
+
222
+ ### Command Parameters
223
+
224
+ Telekit can parse and validate command parameters for you.
225
+
226
+ ```py
227
+ from telekit.parameters import *
228
+
229
+ class GreetHandler(telekit.Handler):
230
+ @classmethod
231
+ def init_handler(cls) -> None:
232
+ cls.on.command("greet", params=[Int(), Str()]).invoke(cls.handle)
233
+
234
+ def handle(self, age: int | None = None, name: str | None = None):
235
+ if age is None or name is None:
236
+ self.chain.sender.set_text("Usage: /greet <age> <name>")
237
+ else:
238
+ self.chain.sender.set_text(f"Hello, {name}! You are {age} years old. Next year you'll turn {age + 1} 😅")
239
+ self.chain.send()
240
+ ```
241
+
242
+ Now `/greet 64 "Alice Reingold"` or `/greet 128 Dracula` are parsed automatically.
243
+
244
+ > [!NOTE]
245
+ > If arguments are invalid or missing, you simply receive `None` and decide how to respond.
246
+
247
+ ### Dialogue
248
+
249
+ Dialogs are built as a chain of steps. Each method waits for the user before continuing.
250
+
251
+ ```py
252
+ class DialogueHandler(telekit.Handler):
253
+
254
+ @classmethod
255
+ def init_handler(cls) -> None:
256
+ cls.on.text("hello", "hi", "hey").invoke(cls.handle_hello)
257
+
258
+ def handle_hello(self) -> None:
259
+ self.chain.sender.set_text("👋 Hello! What is your name?")
260
+ if self.user.first_name:
261
+ self.chain.set_entry_suggestions([self.user.first_name])
262
+ self.chain.set_entry_text(self.handle_name)
263
+ self.chain.send()
264
+
265
+ def handle_name(self, name: str) -> None:
266
+ self.user_name = name
267
+ self.chain.sender.set_text("Nice! How are you feeling today?")
268
+ self.chain.set_entry_text(self.handle_feeling)
269
+ self.chain.send()
270
+
271
+ def handle_feeling(self, feeling: str) -> None:
272
+ self.chain.sender.set_text(f"Got it, {self.user_name.title()}! You feel: {feeling}")
273
+ self.chain.set_inline_keyboard({"↺ Restart": self.handle_hello})
274
+ self.chain.send()
275
+ ```
276
+
277
+ How it works:
278
+
279
+ - The handler reacts to "hello", "hi", or "hey" (lowercase, UPPERCASE, or mixed).
280
+ - `handle_hello` asks for the user's name.
281
+ - `set_entry_suggestions` attaches the user's Telegram `first_name` as a suggestion button.
282
+ - `handle_name` stores the name in `self.user_name`.
283
+ - `handle_feeling` completes the flow and adds a `"↺ Restart"` button that routes back to the beginning.
284
+
285
+ It looks like regular Python. And reads like it too.
286
+
287
+ ### Sender
288
+
289
+ Want to add an image, document or an effect in a single line?
290
+
291
+ ```python
292
+ self.chain.sender.set_effect(Effect.HEART) # Add effect to message. Use enum or string
293
+ self.chain.sender.set_photo("robot.png") # Attach photo. URL, file_id, or path
294
+ self.chain.sender.set_document("README.md") # Attach document. URL, file_id, or path
295
+ self.chain.sender.set_text_as_document("Hello, this is a text document!") # Convert string to text document
296
+ self.chain.sender.send_chat_action(ChatAction.TYPING) # Send chat action. Use enum or string
297
+ ```
298
+
299
+ > [!NOTE]
300
+ > Telekit automatically decides whether to use `bot.send_message` or `bot.send_photo` based on the content
301
+
302
+ ### Styles
303
+
304
+ Telekit lets you describe formatting as objects instead of writing raw HTML or Markdown.
305
+
306
+ ```py
307
+ from telekit.styles import *
308
+
309
+ def handle(self) -> None:
310
+ self.chain.sender.set_text(
311
+ Bold("Text style examples:\n"),
312
+ Stack(
313
+ Bold("Bold text"),
314
+ Italic("Italic text"),
315
+ Bold(Italic("Bold + italic")),
316
+ Link("Link", url="https://example.com"),
317
+ BotLink("Deep link", username="MyBot", start="promo_42"),
318
+ start="- {{index}}. ",
319
+ sep=".\n",
320
+ )
321
+ )
322
+ self.chain.send()
323
+ ```
324
+
325
+ You describe structure. Telekit generates HTML or MarkdownV2 automatically:
326
+
327
+ ```html
328
+ <b>Text style examples:</b>
329
+
330
+ - 1. <b>Bold text</b>.
331
+ - 2. <i>Italic text</i>.
332
+ - 3. <b><i>Bold + italic</i></b>.
333
+ - 4. <a href="https://example.com">Link</a>.
334
+ - 5. <a href="https://t.me/MyBot?start=promo_42">Deep link</a>
335
+ ```
336
+
337
+ No manual escaping. No broken formatting because of one missing character.
338
+
339
+ ### Telekit DSL
340
+
341
+ If you prefer not to write dialog logic in Python, you can use the built-in DSL with Jinja support.
342
+
343
+ ```py
344
+ import telekit
345
+
346
+ class QuizHandler(telekit.DSLHandler):
347
+ @classmethod
348
+ def init_handler(cls) -> None:
349
+ cls.analyze_string(script)
350
+ cls.on.command("start").invoke(cls.start_script)
351
+
352
+ script = """
353
+ $ timeout {
354
+ time = 20; // 20 sec.
355
+ }
356
+
357
+ @ main {
358
+ title = "🎉 Fun Facts Quiz";
359
+ message = "Test your knowledge with 10 fun questions!";
360
+
361
+ buttons {
362
+ next("Start Quiz");
363
+ }
364
+ }
365
+
366
+ @ question_1 {
367
+ title = "🐶 Question 1";
368
+ message = "Which animal is the fastest on land?";
369
+ buttons {
370
+ _lose("Elephant");
371
+ next("Cheetah"); // correct answer
372
+ _lose("Horse");
373
+ _lose("Lion");
374
+ }
375
+ }
376
+
377
+ /* ... */
378
+ """
379
+
380
+ telekit.Server(BOT_TOKEN).polling()
381
+ ```
382
+
383
+ **Key features of the Telekit DSL:**
384
+
385
+ - Scene-based architecture
386
+ - Anonymous scenes
387
+ - Automatic navigation stack management
388
+ - Input handling
389
+ - Images support and link buttons
390
+ - Template variables
391
+ - Custom variables
392
+ - Hooks (Python API integration)
393
+ - Jinja template engine
394
+
395
+ <details>
396
+ <summary>🎆 Click to see what you can do with the DSL</summary>
397
+ <table>
398
+ <tr>
399
+ <td><img src="./docs/images/telekit_example_7.jpg" alt="Telekit Example 7" width="300"></td>
400
+ <td><img src="./docs/images/telekit_example_8.jpg" alt="Telekit Example 8" width="300"></td>
401
+ </tr>
402
+ <tr>
403
+ <td><img src="./docs/images/telekit_example_6.jpg" alt="Telekit Example 7" width="300"></td>
404
+ <td><img src="./docs/images/telekit_example_1.jpg" alt="Telekit Example 8" width="300"></td>
405
+ </tr>
406
+ </table>
407
+ </details>
408
+
409
+ > [!TIP]
410
+ > 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.
411
+
412
+ ### Traits
413
+
414
+ Traits are reusable behavior modules you can mix into any handler.
415
+
416
+ 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.
417
+
418
+ ```py
419
+ from telekit.traits import CalendarPick
420
+
421
+ class CalendarHandler(CalendarPick, telekit.Handler):
422
+
423
+ @classmethod
424
+ def init_handler(cls) -> None:
425
+ cls.on.command("calendar").invoke(cls.handle)
426
+
427
+ def handle(self) -> None:
428
+ self.chain.sender.set_title("📅 Choose a date")
429
+ self.chain.sender.set_message("Select any date — past or future:")
430
+ self.chain.sender.set_remove_text(False)
431
+
432
+ self.calendar_pick(self.handle_date) # HERE
433
+
434
+ def handle_date(self, date: datetime.date) -> None:
435
+ self.chain.sender.set_text(f"You picked: {date}")
436
+ self.chain.send()
437
+ ```
438
+
439
+ <details>
440
+ <summary>Result</summary>
441
+ <table>
442
+ <tr>
443
+ <td><img src="./docs/images/calendar.png" alt="Telekit Calendar Example" width="500"></td>
444
+ </tr>
445
+ </table>
446
+ </details>
447
+
448
+ ### Example Bot
449
+
450
+ You can launch an example bot by **running the following code**:
451
+
452
+ ```py
453
+ import telekit
454
+
455
+ telekit.example(YOUR_BOT_TOKEN)
456
+ ```
457
+
458
+ It includes example commands, dialogs, keyboards, and style usage.
459
+
460
+ ## Why Telekit
461
+
462
+ - No FSM — just **chains**.
463
+ - Declarative, behavior-focused bot logic with minimal boilerplate.
464
+ - Automatic **callback routing** and **input handling**.
465
+ - **Styles API** for rich text (`Bold`, `Italic`, `Links`) with **automatic escaping**.
466
+ - Deep linking and **typed command parameters**.
467
+ - **Built-in DSL** for menus, FAQs, and simple bots.
468
+ - Reusable **Traits** for composable, plug-and-play behavior (for example, a built-in declarative calendar picker).
469
+ - **Zero-code** [Obsidian Canvas](https://github.com/Romashkaa/telekit/blob/main/docs/examples/canvas_faq.md) mode.
470
+ - Seamless integration with **pyTelegramBotAPI**.
471
+
472
+ Telekit doesn't try to be everything.
473
+ It tries to make Telegram bot development easier.
474
+
475
+ > [!TIP]
476
+ > If you're interested and want to learn more, check out the [Tutorial](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/0_tutorial.md)
477
+
478
+ ---
479
+
480
+ # Changes in version 2.4.0
481
+
482
+ ## Inline and Reply Keyboards
483
+
484
+ ### `chain.set_keyboard()`
485
+
486
+ Universal method for attaching a keyboard to the current chain message.
487
+ Accepts either an `InlineKeyboard` or a `ReplyKeyboard` instance.
488
+
489
+ ```python
490
+ # Inline keyboard
491
+ self.chain.set_keyboard(
492
+ InlineKeyboard()
493
+ .add_callback("Click Me!", self.handle)
494
+ .row()
495
+ .add_link("YouTube", "https://youtube.com")
496
+ )
497
+
498
+ # Reply keyboard
499
+ self.chain.set_keyboard(
500
+ ReplyKeyboard(one_time_keyboard=True)
501
+ .add_text("Hello!")
502
+ .add_text("Hi")
503
+ .row()
504
+ .add_contact("📱 Share phone")
505
+ )
506
+ ```
507
+
508
+ > [!NOTE]
509
+ > Inline and reply keyboards are mutually exclusive in Telegram.
510
+ > Only one keyboard can be displayed at a time per message.
511
+ > Passing an `InlineKeyboard` attaches buttons directly to the message;
512
+ > passing a `ReplyKeyboard` replaces the user's system keyboard below the input field.
513
+
514
+ ### `InlineKeyboard`
515
+
516
+ Fluent builder for Telegram inline keyboards. Supports all standard inline button types, conditional rendering via `when=`, and row/column layout helpers.
517
+
518
+ **Layout helpers**
519
+
520
+ | **Method** | **Description** |
521
+ | -------------- | --------------- |
522
+ | `row()` | Finalize the current row and start a new one. |
523
+ | `column()` | Every subsequent button gets its own row. Alias for `grid(1)`. |
524
+ | `column_end()` | Exit column mode and flush the current row. Alias for `grid_end()`. |
525
+ | `grid(width)` | Every subsequent button is automatically split into rows of `width` buttons. |
526
+ | `grid_end()` | Exit grid mode and flush the current row. |
527
+
528
+ **Button methods**
529
+
530
+ | **Method** | **Description** |
531
+ |------------|-----------------|
532
+ | `add(text, button, when=)` | Attach any `InlineButton` instance. |
533
+ | `add_callback(text, callback, pass_args, pass_kwargs, answer_text, answer_as_alert, style, when=)` | Button that fires a callback function. |
534
+ | `add_link(text, url, style, when=)` | Button that opens a URL or `tg://` link. |
535
+ | `add_webapp(text, url, style, when=)` | Button that opens a Telegram Mini App. |
536
+ | `add_suggest(text, suggestion, style, strict, when=)` | Button that simulates the user sending a message. |
537
+ | `add_copy(text, copy_text, style, strict, when=)` | Button that copies text to the clipboard. |
538
+ | `add_static(text, style, when=)` | Decorative button with no action. |
539
+ | `add_alert(text, alert_text, persistent, style, when=)` | Button that shows a popup alert dialog. |
540
+ | `add_notification(text, notification_text, persistent, style, when=)` | Button that shows a brief top-of-chat notification. |
541
+ | `add_invoke(text, obj, invoke, pass_args, pass_kwargs, answer_text, answer_as_alert, style, when=)` | Button that calls a named method on an arbitrary object. |
542
+
543
+ **Bulk helpers**
544
+
545
+ | **Method** | **Description** |
546
+ |------------|-----------------|
547
+ | `extend(buttons, column=, when=)` | Add multiple buttons from a `dict[str, InlineButton \| None]` or `list[str]`. |
548
+ | `extend_rows(*rows, when=)` | Append one or more pre-built `list[tuple[str, InlineButton]]` rows. |
549
+
550
+ All button methods accept a `style` parameter: `"danger"` (red), `"success"` (green), or `"primary"` (blue).
551
+
552
+ ```python
553
+ InlineKeyboard()
554
+ .add_link("YouTube", "https://youtube.com")
555
+ .add_copy("Copy Me", "copied!")
556
+ .add_alert("Info", "This is an alert")
557
+ .row()
558
+ .add_callback("Click Me!", self.handle)
559
+ .column()
560
+ .add_link("A", "https://example.com")
561
+ .add_callback("B", self.handle_b)
562
+ .column_end()
563
+ .extend({"Alert": AlertButton("Hi!"), "Notify": NotificationButton("Hey")}, column=True)
564
+ ```
565
+
566
+ ### `ReplyKeyboard`
567
+
568
+ Fluent builder for Telegram reply keyboards. Mirrors the `InlineKeyboard` layout API (`row`, `column`, `column_end`, `extend`, `extend_rows`).
569
+
570
+ **Constructor parameters**
571
+
572
+ | **Parameter** | **Default** | **Description** |
573
+ |---|---|---|
574
+ | `resize_keyboard` | `True` | Shrink the keyboard to fit its buttons. |
575
+ | `one_time_keyboard` | `False` | Hide the keyboard after the first press. |
576
+ | `input_field_placeholder` | `None` | Placeholder text shown while the keyboard is active (max 64 chars). |
577
+ | `selective` | `False` | Show only to mentioned users or the original sender. |
578
+ | `is_persistent` | `False` | Always show the keyboard; do not collapse it to an icon. |
579
+
580
+ **Button methods**
581
+
582
+ | **Method** | **Description** |
583
+ |---|---|
584
+ | `add(text, button, when=)` | Attach any `ReplyButton` instance. |
585
+ | `add_text(text, when=)` | Plain text button — sends the label as a message. |
586
+ | `add_contact(text, when=)` | Prompts the user to share their phone number (private chats only). |
587
+ | `add_location(text, when=)` | Prompts the user to share their geolocation (private chats only). |
588
+ | `add_poll(text, poll_type, when=)` | Opens the poll creation dialog; `poll_type` can be `"quiz"`, `"regular"`, or `None`. |
589
+ | `add_webapp(text, url, when=)` | Opens a Telegram Mini App. |
590
+ | `add_request_user(text, request_id, user_is_bot, user_is_premium, when=)` | Lets the user pick a Telegram user; result returned as a service message. |
591
+ | `add_request_chat(text, request_id, chat_is_channel, chat_is_forum, chat_has_username, chat_is_created, user_administrator_rights, bot_administrator_rights, bot_is_member, when=)` | Lets the user pick a chat; result returned as a service message. |
592
+
593
+ ```python
594
+ ReplyKeyboard(input_field_placeholder="Choose:", one_time_keyboard=True)
595
+ .add_text("Hello!")
596
+ .add_text("Hi")
597
+ .row()
598
+ .add_contact("📱 Phone")
599
+ .add_location("📍 Location")
600
+ .add_poll("📊 Poll")
601
+ .row()
602
+ .add_request_user("Pick user", request_id=1)
603
+ .add_request_chat("Pick chat", request_id=2)
604
+ ```
605
+
606
+ ## Telekit DSL
607
+
608
+ ### `InstanceDSLHandler`
609
+
610
+ Instance-oriented variant of `DSLHandler` where each instance carries its own
611
+ `executable_model`, `_script_data_factory`, and `_jinja_env` — allowing multiple
612
+ instances to run completely independent scripts simultaneously.
613
+
614
+ Use the `*_locally` instance methods instead of the class-level ones:
615
+
616
+ ```python
617
+ class MyHandler(telekit.InstanceDSLHandler):
618
+ @classmethod
619
+ def init_handler(cls) -> None:
620
+ cls.on.message().invoke(cls.handle)
621
+
622
+ def handle(self):
623
+ script = fetch_script_from_db(self.user.id) # per-user DSL
624
+ self.analyze_string_locally(script)
625
+ self.start_script()
626
+ ```
627
+
628
+ | **Method** | **Description** |
629
+ | --------------------------------------- | ---------------------------------------------------- |
630
+ | `analyze_file_locally(path, encoding)` | Analyse a script file on this instance. |
631
+ | `analyze_string_locally(script)` | Analyse a DSL string on this instance. |
632
+ | `analyze_canvas_locally(file_path)` | Analyse an Obsidian `.canvas` file on this instance. |
633
+ | `analyze_executable_model_locally(model)` | Load a pre-built model dict on this instance. |
634
+
635
+ **Security**
636
+
637
+ When accepting scripts from untrusted users, restrict dangerous features via the
638
+ `RESTRICTED` class attribute. Set `DEFAULT_TIMEOUT` to control the fallback timeout,
639
+ and `DEFAULT_CONFIG` to provide a safe base config.
640
+
641
+ ```python
642
+ class SafeDSL(telekit.InstanceDSLHandler):
643
+ RESTRICTED: list[RestrictedToken] = ["hook", "jinja", "redirect", "handoff", "config"]
644
+ DEFAULT_TIMEOUT = 120
645
+ DEFAULT_CONFIG = {"template": "vars"}
646
+ ```
647
+
648
+ | **Token** | **Effect** |
649
+ | -------------- | ----------------------------------------------------------------------------------------------- |
650
+ | `"handoff"` | Disables `handoff` button type (cross-handler transitions). |
651
+ | `"redirect"` | Disables `redirect` button type (simulated user messages). |
652
+ | `"hook"` | Removes all `on_enter`, `on_enter_once`, `on_exit`, `on_timeout` hooks. |
653
+ | `"jinja"` | Forces template engine to `"vars"`; Jinja is never executed. |
654
+ | `"timeout"` | Ignores per-script `timeout_time`; uses `DEFAULT_TIMEOUT` only. |
655
+ | `"config"` | Replaces script config with `DEFAULT_CONFIG`; `vars_*` keys are preserved unless `"vars"` is also set. |
656
+ | `"vars"` | Removes all `vars_*` keys and disables `{{variable}}` substitution. |
657
+ | `"images"` | Strips `image` field from every scene. |
658
+ | `"links"` | Disables `link` button type (external URLs). |
659
+ | `"suggest"` | Disables `suggest` button type (pre-filled entry suggestions). |
660
+ | `"entry"` | Disables entry handlers (free-text input routing). |
661
+ | `"next"` | Disables `next` magic scene navigation. |
662
+ | `"back"` | Disables `back` magic scene navigation. |
663
+
664
+ ## Scheduler
665
+
666
+ - Added `every` decorator for scheduling functions in a background daemon thread.
667
+ - Implemented `PeriodicTask` class to manage periodic execution and error handling.
668
+
669
+ ## Others
670
+
671
+ - Enhanced `Debug` class with callback query tracing functionality.
672
+ - Refactored `BaseSender` to use `send_or_handle_error`.