telekit 2.3.0b2__tar.gz → 2.4.0b0__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 (79) hide show
  1. {telekit-2.3.0b2 → telekit-2.4.0b0}/PKG-INFO +97 -131
  2. {telekit-2.3.0b2 → telekit-2.4.0b0}/README.md +45 -3
  3. {telekit-2.3.0b2 → telekit-2.4.0b0}/setup.py +2 -2
  4. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/__init__.py +8 -1
  5. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_callback_query_handler.py +15 -3
  6. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_chain.py +4 -3
  7. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_chain_base.py +4 -5
  8. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_chain_inline_keyboards_logic.py +2 -2
  9. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_init.py +1 -1
  10. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_inline_buttons.py +11 -0
  11. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_logger.py +46 -41
  12. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_state.py +15 -2
  13. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/mixin.py +478 -4
  14. telekit-2.4.0b0/telekit/_trait.py +10 -0
  15. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_version.py +1 -1
  16. telekit-2.4.0b0/telekit/debug.py +9 -0
  17. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/__init__.py +2 -1
  18. telekit-2.4.0b0/telekit/example/example_handlers/calendar.py +155 -0
  19. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/start.py +2 -1
  20. telekit-2.4.0b0/telekit/scheduler.py +167 -0
  21. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/senders.py +13 -4
  22. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/server.py +7 -7
  23. telekit-2.4.0b0/telekit/traits/__init__.py +3 -0
  24. telekit-2.4.0b0/telekit/traits/calendar_pick.py +807 -0
  25. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/traits/paginated_choice.py +1 -1
  26. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/traits/track_handoff_origin.py +1 -1
  27. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit.egg-info/PKG-INFO +97 -131
  28. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit.egg-info/SOURCES.txt +5 -0
  29. telekit-2.3.0b2/telekit/traits/__init__.py +0 -2
  30. {telekit-2.3.0b2 → telekit-2.4.0b0}/LICENSE +0 -0
  31. {telekit-2.3.0b2 → telekit-2.4.0b0}/setup.cfg +0 -0
  32. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_buildtext/__init__.py +0 -0
  33. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_buildtext/formatter.py +0 -0
  34. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_buildtext/styles.py +0 -0
  35. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_chain_entry_logic.py +0 -0
  36. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_chapters/__init__.py +0 -0
  37. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_chapters/chapters.py +0 -0
  38. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_handler.py +0 -0
  39. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_input_handler.py +0 -0
  40. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_on.py +0 -0
  41. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_snapvault/__init__.py +0 -0
  42. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_snapvault/snapcode.py +0 -0
  43. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_snapvault/snapvault.py +0 -0
  44. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/__init__.py +0 -0
  45. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  46. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/parser/builder.py +0 -0
  47. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  48. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  49. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  50. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/parser/parser.py +0 -0
  51. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/parser/token.py +0 -0
  52. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  53. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  54. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_timeout.py +0 -0
  55. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/_user.py +0 -0
  56. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/dices.py +0 -0
  57. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/__init__.py +0 -0
  58. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/complete_hotel.py +0 -0
  59. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/counter.py +0 -0
  60. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/dsl.py +0 -0
  61. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/entry.py +0 -0
  62. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/faq.py +0 -0
  63. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/hotel.py +0 -0
  64. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/on_text.py +0 -0
  65. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/pages.py +0 -0
  66. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/pyapi.py +0 -0
  67. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/qr.py +0 -0
  68. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/quiz.py +0 -0
  69. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/spells.py +0 -0
  70. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_handlers/text_document.py +0 -0
  71. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/example/example_server.py +0 -0
  72. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/inline_buttons.py +0 -0
  73. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/parameters.py +0 -0
  74. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/styles.py +0 -0
  75. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/types.py +0 -0
  76. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit/utils.py +0 -0
  77. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit.egg-info/dependency_links.txt +0 -0
  78. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit.egg-info/requires.txt +0 -0
  79. {telekit-2.3.0b2 → telekit-2.4.0b0}/telekit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: telekit
3
- Version: 2.3.0b2
3
+ Version: 2.4.0b0
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
 
@@ -340,14 +341,54 @@ telekit.Server(BOT_TOKEN).polling()
340
341
  <summary>Click to see what you can do with the DSL</summary>
341
342
  <table>
342
343
  <tr>
343
- <td><img src="../images/telekit_example_7.jpg" alt="Telekit Example 7" width="300"></td>
344
- <td><img src="../images/telekit_example_8.jpg" alt="Telekit Example 8" width="300"></td>
344
+ <td><img src="./docs/images/telekit_example_7.jpg" alt="Telekit Example 7" width="300"></td>
345
+ <td><img src="./docs/images/telekit_example_8.jpg" alt="Telekit Example 8" width="300"></td>
346
+ </tr>
347
+ <tr>
348
+ <td><img src="./docs/images/telekit_example_6.jpg" alt="Telekit Example 7" width="300"></td>
349
+ <td><img src="./docs/images/telekit_example_1.jpg" alt="Telekit Example 8" width="300"></td>
345
350
  </tr>
346
351
  </table>
347
352
  </details>
348
353
 
349
354
  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.
350
355
 
356
+ ### Traits
357
+
358
+ Traits are reusable behavior modules you can mix into any handler.
359
+
360
+ 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.
361
+
362
+ ```py
363
+ from telekit.traits import CalendarPick
364
+
365
+ class CalendarHandler(CalendarPick, telekit.Handler):
366
+
367
+ @classmethod
368
+ def init_handler(cls) -> None:
369
+ cls.on.command("calendar").invoke(cls.handle)
370
+
371
+ def handle(self) -> None:
372
+ self.chain.sender.set_title("📅 Choose a date")
373
+ self.chain.sender.set_message("Select any date — past or future:")
374
+ self.chain.sender.set_remove_text(False)
375
+
376
+ self.calendar_pick(self.handle_date) # HERE
377
+
378
+ def handle_date(self, date: datetime.date) -> None:
379
+ self.chain.sender.set_text(f"You picked: {date}")
380
+ self.chain.send()
381
+ ```
382
+
383
+ <details>
384
+ <summary>Result</summary>
385
+ <table>
386
+ <tr>
387
+ <td><img src="./docs/images/calendar.png" alt="Telekit Calendar Example" width="500"></td>
388
+ </tr>
389
+ </table>
390
+ </details>
391
+
351
392
  ### Example Bot
352
393
 
353
394
  You can launch an example bot by **running the following code**:
@@ -368,6 +409,7 @@ It includes example commands, dialogs, keyboards, and style usage.
368
409
  - **Styles API** for rich text (`Bold`, `Italic`, `Links`) with **automatic escaping**.
369
410
  - Deep linking and **typed command parameters**.
370
411
  - **Built-in DSL** for menus, FAQs, and simple bots.
412
+ - Reusable **Traits** for composable, plug-and-play behavior (for example, a built-in declarative calendar picker).
371
413
  - **Zero-code** [Obsidian Canvas](https://github.com/Romashkaa/telekit/blob/main/docs/examples/canvas_faq.md) mode.
372
414
  - Seamless integration with **pyTelegramBotAPI**.
373
415
 
@@ -379,148 +421,72 @@ It tries to make Telegram bot development easier.
379
421
 
380
422
  ---
381
423
 
382
- # Changes in version 2.3.0b2
383
-
384
- ## Inline Buttons
385
-
386
- | **Name** | **Description** |
387
- | ---------------- | ------------------------------------------------------------------------------- |
388
- | `StaticButton` | A non-interactive inline button that performs no action when pressed. |
389
- | `AnswerButton` | A button that responds to a callback query with a notification or alert without executing custom logic. |
390
-
391
- - `AnswerButton` and its subclasses (`AlertButton`, `NotificationButton`) now support the `persistent` parameter (Defauts to `True`):
392
- - `True` — non-blocking hint (does not affect further interactions)
393
- - `False` — terminates interaction after click
424
+ # Changes in version 2.4.0b0
394
425
 
395
- ## Traits
426
+ ## Scheduler
396
427
 
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. |
428
+ - Added `every` decorator for scheduling functions in a background daemon thread.
429
+ - Implemented `PeriodicTask` class to manage periodic execution and error handling.
401
430
 
402
- ### TrackHandoffOrigin
431
+ ## Telekit DSL
403
432
 
404
- `TrackHandoffOrigin` adds three members to any handler that inherits it:
433
+ ### `InstanceDSLHandler`
405
434
 
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. |
435
+ Instance-oriented variant of `DSLHandler` where each instance carries its own
436
+ `executable_model`, `_script_data_factory`, and `_jinja_env` — allowing multiple
437
+ instances to run completely independent scripts simultaneously.
412
438
 
413
- Set `TRACK_HANDOFF_ORIGIN = False` on any subclass to opt out of tracking.
439
+ Use the `*_locally` instance methods instead of the class-level ones:
414
440
 
415
441
  ```python
416
- class MyHandler(TrackHandoffOrigin, telekit.Handler):
442
+ class MyHandler(telekit.InstanceDSLHandler):
443
+ @classmethod
444
+ def init_handler(cls) -> None:
445
+ cls.on.message().invoke(cls.handle)
417
446
 
418
447
  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
- )
448
+ script = fetch_script_from_db(self.user.id) # per-user DSL
449
+ self.analyze_string_locally(script)
450
+ self.start_script()
449
451
  ```
450
452
 
451
- Override labels per handler to localise or restyle navigation:
453
+ | **Method** | **Description** |
454
+ | --------------------------------------- | ---------------------------------------------------- |
455
+ | `analyze_file_locally(path, encoding)` | Analyse a script file on this instance. |
456
+ | `analyze_string_locally(script)` | Analyse a DSL string on this instance. |
457
+ | `analyze_canvas_locally(file_path)` | Analyse an Obsidian `.canvas` file on this instance. |
458
+ | `analyze_executable_model_locally(model)` | Load a pre-built model dict on this instance. |
452
459
 
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
- ```
460
+ **Security**
459
461
 
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:
462
+ When accepting scripts from untrusted users, restrict dangerous features via the
463
+ `RESTRICTED` class attribute. Set `DEFAULT_TIMEOUT` to control the fallback timeout,
464
+ and `DEFAULT_CONFIG` to provide a safe base config.
484
465
 
485
466
  ```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
467
+ class SafeDSL(telekit.InstanceDSLHandler):
468
+ RESTRICTED: list[RestrictedToken] = ["hook", "jinja", "redirect", "handoff", "config"]
469
+ DEFAULT_TIMEOUT = 120
470
+ DEFAULT_CONFIG = {"template": "vars"}
491
471
  ```
492
472
 
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
- ```
473
+ | **Token** | **Effect** |
474
+ | -------------- | ----------------------------------------------------------------------------------------------- |
475
+ | `"handoff"` | Disables `handoff` button type (cross-handler transitions). |
476
+ | `"redirect"` | Disables `redirect` button type (simulated user messages). |
477
+ | `"hook"` | Removes all `on_enter`, `on_enter_once`, `on_exit`, `on_timeout` hooks. |
478
+ | `"jinja"` | Forces template engine to `"vars"`; Jinja is never executed. |
479
+ | `"timeout"` | Ignores per-script `timeout_time`; uses `DEFAULT_TIMEOUT` only. |
480
+ | `"config"` | Replaces script config with `DEFAULT_CONFIG`; `vars_*` keys are preserved unless `"vars"` is also set. |
481
+ | `"vars"` | Removes all `vars_*` keys and disables `{{variable}}` substitution. |
482
+ | `"images"` | Strips `image` field from every scene. |
483
+ | `"links"` | Disables `link` button type (external URLs). |
484
+ | `"suggest"` | Disables `suggest` button type (pre-filled entry suggestions). |
485
+ | `"entry"` | Disables entry handlers (free-text input routing). |
486
+ | `"next"` | Disables `next` magic scene navigation. |
487
+ | `"back"` | Disables `back` magic scene navigation. |
488
+
489
+ ## Others
490
+
491
+ - Enhanced `Debug` class with callback query tracing functionality.
492
+ - Refactored `BaseSender` to use `send_or_handle_error`.
@@ -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
 
@@ -304,14 +305,54 @@ telekit.Server(BOT_TOKEN).polling()
304
305
  <summary>Click to see what you can do with the DSL</summary>
305
306
  <table>
306
307
  <tr>
307
- <td><img src="../images/telekit_example_7.jpg" alt="Telekit Example 7" width="300"></td>
308
- <td><img src="../images/telekit_example_8.jpg" alt="Telekit Example 8" width="300"></td>
308
+ <td><img src="./docs/images/telekit_example_7.jpg" alt="Telekit Example 7" width="300"></td>
309
+ <td><img src="./docs/images/telekit_example_8.jpg" alt="Telekit Example 8" width="300"></td>
310
+ </tr>
311
+ <tr>
312
+ <td><img src="./docs/images/telekit_example_6.jpg" alt="Telekit Example 7" width="300"></td>
313
+ <td><img src="./docs/images/telekit_example_1.jpg" alt="Telekit Example 8" width="300"></td>
309
314
  </tr>
310
315
  </table>
311
316
  </details>
312
317
 
313
318
  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.
314
319
 
320
+ ### Traits
321
+
322
+ Traits are reusable behavior modules you can mix into any handler.
323
+
324
+ 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.
325
+
326
+ ```py
327
+ from telekit.traits import CalendarPick
328
+
329
+ class CalendarHandler(CalendarPick, telekit.Handler):
330
+
331
+ @classmethod
332
+ def init_handler(cls) -> None:
333
+ cls.on.command("calendar").invoke(cls.handle)
334
+
335
+ def handle(self) -> None:
336
+ self.chain.sender.set_title("📅 Choose a date")
337
+ self.chain.sender.set_message("Select any date — past or future:")
338
+ self.chain.sender.set_remove_text(False)
339
+
340
+ self.calendar_pick(self.handle_date) # HERE
341
+
342
+ def handle_date(self, date: datetime.date) -> None:
343
+ self.chain.sender.set_text(f"You picked: {date}")
344
+ self.chain.send()
345
+ ```
346
+
347
+ <details>
348
+ <summary>Result</summary>
349
+ <table>
350
+ <tr>
351
+ <td><img src="./docs/images/calendar.png" alt="Telekit Calendar Example" width="500"></td>
352
+ </tr>
353
+ </table>
354
+ </details>
355
+
315
356
  ### Example Bot
316
357
 
317
358
  You can launch an example bot by **running the following code**:
@@ -332,6 +373,7 @@ It includes example commands, dialogs, keyboards, and style usage.
332
373
  - **Styles API** for rich text (`Bold`, `Italic`, `Links`) with **automatic escaping**.
333
374
  - Deep linking and **typed command parameters**.
334
375
  - **Built-in DSL** for menus, FAQs, and simple bots.
376
+ - Reusable **Traits** for composable, plug-and-play behavior (for example, a built-in declarative calendar picker).
335
377
  - **Zero-code** [Obsidian Canvas](https://github.com/Romashkaa/telekit/blob/main/docs/examples/canvas_faq.md) mode.
336
378
  - Seamless integration with **pyTelegramBotAPI**.
337
379
 
@@ -26,11 +26,11 @@ remove_cache()
26
26
  def readme():
27
27
  with open('README.md', 'r') as f:
28
28
  return f.read()
29
-
29
+
30
30
  def changelog():
31
31
  with open('CHANGELOG.md', 'r') as f:
32
32
  return f.read()
33
-
33
+
34
34
  def install_requires():
35
35
  with open('telekit/requirements.txt', 'r') as f:
36
36
  return f.read().split("\n")
@@ -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
@@ -25,7 +26,7 @@ from ._snapvault import Vault
25
26
  from ._chapters import chapters
26
27
  from ._user import User
27
28
  from ._telekit_dsl.telekit_dsl import TelekitDSL
28
- from ._telekit_dsl.mixin import DSLHandler
29
+ from ._telekit_dsl.mixin import DSLHandler, InstanceDSLHandler
29
30
  from ._logger import enable_file_logging
30
31
 
31
32
  from . import senders
@@ -36,6 +37,8 @@ from . import inline_buttons
36
37
  from . import dices
37
38
  from . import utils
38
39
  from . import traits
40
+ from . import debug
41
+ from . import scheduler
39
42
 
40
43
  Styles = styles.Styles
41
44
 
@@ -50,20 +53,24 @@ __all__ = [
50
53
  "parameters",
51
54
  "inline_buttons",
52
55
  "dices",
56
+ "scheduler",
53
57
 
54
58
  "Styles",
55
59
  "User",
56
60
 
57
61
  "Server",
58
62
  "Chain",
63
+ "Trait",
59
64
  "Handler",
60
65
  "CallbackQueryHandler",
61
66
 
62
67
  "TelekitDSL",
63
68
  "DSLHandler",
69
+ "InstanceDSLHandler",
64
70
 
65
71
  "Vault",
66
72
  "enable_file_logging",
67
73
  "chapters",
68
74
  "example",
75
+ "debug",
69
76
  ]
@@ -26,6 +26,9 @@ from telebot.types import (
26
26
  CallbackQuery
27
27
  )
28
28
 
29
+ from .debug import Debug
30
+ from ._logger import _library
31
+
29
32
 
30
33
  class CallbackQueryHandler:
31
34
 
@@ -44,9 +47,18 @@ class CallbackQueryHandler:
44
47
 
45
48
  @bot.callback_query_handler(func=lambda call: True)
46
49
  def handle(call: CallbackQuery) -> None:
50
+ if Debug.callback_query_tracing:
51
+ _library.info(
52
+ "CallbackQuery("
53
+ f"user_id={call.from_user.id}, "
54
+ f"data={call.data!r},\n"
55
+ f"message_text={call.message.text if call.message else None!r}"
56
+ f") - {Debug.callback_query_tracing=}"
57
+ )
58
+
47
59
  if not call.data:
48
60
  return
49
-
61
+
50
62
  if call.data.startswith(cls.INLINE_BUTTON):
51
63
  cls._handle_inline_button(call)
52
64
  elif call.data.startswith(cls.STATIC_BUTTON):
@@ -180,11 +192,11 @@ class CallbackQueryHandler:
180
192
 
181
193
  # (text: str, is_alert: bool)
182
194
 
183
- _invalid_data_answer: tuple[str, bool] = ("Invalid Call Data (None)", True)
195
+ _invalid_data_answer: tuple[str, bool] = ("Invalid Call Data", True)
184
196
  _button_is_no_active_answer: tuple[str, bool] = ("Button is no longer active", True)
185
197
 
186
198
  @classmethod
187
- def set_invalid_data_answer(cls, answer: str="Invalid Call Data (None)", is_alert: bool=True):
199
+ def set_invalid_data_answer(cls, answer: str="Invalid Call Data", is_alert: bool=True):
188
200
  """
189
201
  Sets the response message and type for cases when the received callback data is invalid or empty.
190
202
 
@@ -26,10 +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
32
  # Chain modules
32
- from ._chain_base import library
33
+ from ._chain_base import _library
33
34
  from ._chain_inline_keyboards_logic import ChainInlineKeyboardLogic
34
35
  from ._chain_entry_logic import ChainEntryLogic, TextDocument
35
36
 
@@ -154,8 +155,8 @@ class Chain(ChainInlineKeyboardLogic, ChainEntryLogic):
154
155
  self.sender.set_edit_message(None)
155
156
  self._previous_message = message
156
157
 
157
- if self._timeout_warnings_enabled and _handler and not _timeout:
158
- library.warning(
158
+ if Debug.timeout_warnings and _handler and not _timeout:
159
+ _library.warning(
159
160
  "Next-message handler is active, but no timeout was set for the chain. "
160
161
  "This may cause the bot to wait indefinitely."
161
162
  )
@@ -31,14 +31,12 @@ from . import _input_handler
31
31
  from . import _timeout
32
32
 
33
33
  # Logging
34
- from ._logger import logger
35
- library = logger.library
34
+ from .debug import Debug
35
+ from ._logger import _library as _library
36
36
 
37
37
  class ChainBase:
38
38
 
39
39
  bot: telebot.TeleBot
40
-
41
- _timeout_warnings_enabled: bool = True
42
40
 
43
41
  @classmethod
44
42
  def _init(cls, bot: telebot.TeleBot):
@@ -206,4 +204,5 @@ class ChainBase:
206
204
  # Timeout API
207
205
 
208
206
  def disable_timeout_warnings(self, value: bool = True) -> None:
209
- self._timeout_warnings_enabled = not value
207
+ """.. deprecated:: Set ``Debug.timeout_warnings`` directly."""
208
+ Debug.timeout_warnings = not value
@@ -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, library
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
@@ -249,7 +249,7 @@ class ChainInlineKeyboardLogic(ChainBase):
249
249
  if not isinstance(choices, dict):
250
250
  for c in choices:
251
251
  if type(c).__str__ is object.__str__:
252
- library.warning(
252
+ _library.warning(
253
253
  f"{type(c).__name__} does not implement __str__. "
254
254
  f"Consider passing a dict with explicit labels.",
255
255
  stacklevel=3
@@ -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)
@@ -26,6 +26,8 @@ from telebot.types import InlineKeyboardButton, CallbackQuery
26
26
  from telebot import TeleBot
27
27
  from ._callback_query_handler import CallbackQueryHandler
28
28
 
29
+ from ._state import TelekitState
30
+
29
31
  __all__ = [
30
32
  "InlineButton",
31
33
 
@@ -122,6 +124,15 @@ class InlineButton:
122
124
  raise ValueError(f"Unknown style: {style!r}. Must be one of {_BUTTON_STYLES_LIST}")
123
125
  raise TypeError(f"Style must be str, ButtonStyle, or None, got {type(style)}")
124
126
 
127
+ def _normalize_custom_emoji_id(self, custom_emoji_id: int | str | None) -> str | None:
128
+ if not custom_emoji_id:
129
+ return None
130
+
131
+ if not TelekitState.is_premium():
132
+ return None
133
+
134
+ return str(custom_emoji_id)
135
+
125
136
  class StaticButton(InlineButton):
126
137
 
127
138
  def __init__(self, *, style: str | None | ButtonStyle = None, **kwargs):