telekit 2.3.0b3__tar.gz → 2.4.0b1__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 (82) hide show
  1. {telekit-2.3.0b3 → telekit-2.4.0b1}/PKG-INFO +246 -6
  2. telekit-2.3.0b3/telekit.egg-info/PKG-INFO → telekit-2.4.0b1/README.md +46 -46
  3. {telekit-2.3.0b3 → telekit-2.4.0b1}/setup.py +4 -3
  4. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/__init__.py +9 -1
  5. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_callback_query_handler.py +15 -3
  6. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_chain.py +2 -2
  7. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_chain_base.py +1 -2
  8. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_chain_inline_keyboards_logic.py +71 -2
  9. telekit-2.4.0b1/telekit/_inline_keyboard.py +545 -0
  10. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_input_handler.py +3 -2
  11. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_logger.py +46 -41
  12. telekit-2.4.0b1/telekit/_reply_keyboard.py +463 -0
  13. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/mixin.py +478 -4
  14. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_version.py +1 -1
  15. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/debug.py +1 -0
  16. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/calendar.py +11 -79
  17. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/inline_buttons.py +4 -0
  18. telekit-2.4.0b1/telekit/reply_buttons.py +254 -0
  19. telekit-2.4.0b1/telekit/scheduler.py +167 -0
  20. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/senders.py +24 -6
  21. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/server.py +4 -6
  22. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/types.py +31 -1
  23. telekit-2.4.0b1/telekit.egg-info/PKG-INFO +624 -0
  24. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit.egg-info/SOURCES.txt +4 -0
  25. telekit-2.3.0b3/README.md +0 -342
  26. {telekit-2.3.0b3 → telekit-2.4.0b1}/LICENSE +0 -0
  27. {telekit-2.3.0b3 → telekit-2.4.0b1}/setup.cfg +0 -0
  28. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_buildtext/__init__.py +0 -0
  29. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_buildtext/formatter.py +0 -0
  30. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_buildtext/styles.py +0 -0
  31. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_chain_entry_logic.py +0 -0
  32. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_chapters/__init__.py +0 -0
  33. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_chapters/chapters.py +0 -0
  34. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_handler.py +0 -0
  35. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_init.py +0 -0
  36. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_inline_buttons.py +0 -0
  37. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_on.py +0 -0
  38. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_snapvault/__init__.py +0 -0
  39. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_snapvault/snapcode.py +0 -0
  40. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_snapvault/snapvault.py +0 -0
  41. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_state.py +0 -0
  42. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/__init__.py +0 -0
  43. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  44. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/parser/builder.py +0 -0
  45. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  46. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  47. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  48. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/parser/parser.py +0 -0
  49. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/parser/token.py +0 -0
  50. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  51. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  52. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_timeout.py +0 -0
  53. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_trait.py +0 -0
  54. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/_user.py +0 -0
  55. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/dices.py +0 -0
  56. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/__init__.py +0 -0
  57. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/__init__.py +0 -0
  58. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/complete_hotel.py +0 -0
  59. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/counter.py +0 -0
  60. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/dsl.py +0 -0
  61. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/entry.py +0 -0
  62. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/faq.py +0 -0
  63. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/hotel.py +0 -0
  64. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/on_text.py +0 -0
  65. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/pages.py +0 -0
  66. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/pyapi.py +0 -0
  67. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/qr.py +0 -0
  68. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/quiz.py +0 -0
  69. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/spells.py +0 -0
  70. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/start.py +0 -0
  71. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_handlers/text_document.py +0 -0
  72. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/example/example_server.py +0 -0
  73. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/parameters.py +0 -0
  74. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/styles.py +0 -0
  75. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/traits/__init__.py +0 -0
  76. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/traits/calendar_pick.py +0 -0
  77. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/traits/paginated_choice.py +0 -0
  78. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/traits/track_handoff_origin.py +0 -0
  79. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit/utils.py +0 -0
  80. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit.egg-info/dependency_links.txt +0 -0
  81. {telekit-2.3.0b3 → telekit-2.4.0b1}/telekit.egg-info/requires.txt +0 -0
  82. {telekit-2.3.0b3 → telekit-2.4.0b1}/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.0b3
3
+ Version: 2.4.0b1
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,6 +421,204 @@ It tries to make Telegram bot development easier.
379
421
 
380
422
  ---
381
423
 
382
- # Changes in version 2.3.0b3
424
+ # Changes in version 2.4.0b1
425
+
426
+ ## Inline and Reply Keyboards
427
+
428
+ ### `chain.set_keyboard()`
429
+
430
+ Universal method for attaching a keyboard to the current chain message.
431
+ Accepts either an `InlineKeyboard` or a `ReplyKeyboard` instance.
432
+
433
+ ```python
434
+ # Inline keyboard
435
+ self.chain.set_keyboard(
436
+ InlineKeyboard()
437
+ .add_callback("Click Me!", self.handle)
438
+ .row()
439
+ .add_link("YouTube", "https://youtube.com")
440
+ )
441
+
442
+ # Reply keyboard
443
+ self.chain.set_keyboard(
444
+ ReplyKeyboard(one_time_keyboard=True)
445
+ .add_text("Hello!")
446
+ .add_text("Hi")
447
+ .row()
448
+ .add_contact("📱 Share phone")
449
+ )
450
+ ```
451
+
452
+ > [!NOTE]
453
+ > Inline and reply keyboards are mutually exclusive in Telegram.
454
+ > Only one keyboard can be displayed at a time per message.
455
+ > Passing an `InlineKeyboard` attaches buttons directly to the message;
456
+ > passing a `ReplyKeyboard` replaces the user's system keyboard below the input field.
457
+
458
+ ### `InlineKeyboard`
459
+
460
+ Fluent builder for Telegram inline keyboards. Supports all standard inline button types, conditional rendering via `when=`, and row/column layout helpers.
461
+
462
+ **Layout helpers**
463
+
464
+ | **Method** | **Description** |
465
+ |------------------|----------------------------------------------------------------|
466
+ | `row(when=)` | Finalize the current row and start a new one. |
467
+ | `column_start()` | Enable column mode — every subsequent button gets its own row. |
468
+ | `column_end()` | Disable column mode and flush the current row. |
469
+
470
+ **Button methods**
471
+
472
+ | **Method** | **Description** |
473
+ |------------|-----------------|
474
+ | `add(text, button, when=)` | Attach any `InlineButton` instance. |
475
+ | `add_callback(text, callback, pass_args, pass_kwargs, answer_text, answer_as_alert, style, when=)` | Button that fires a callback function. |
476
+ | `add_link(text, url, style, when=)` | Button that opens a URL or `tg://` link. |
477
+ | `add_webapp(text, url, style, when=)` | Button that opens a Telegram Mini App. |
478
+ | `add_suggest(text, suggestion, style, strict, when=)` | Button that simulates the user sending a message. |
479
+ | `add_copy(text, copy_text, style, strict, when=)` | Button that copies text to the clipboard. |
480
+ | `add_static(text, style, when=)` | Decorative button with no action. |
481
+ | `add_alert(text, alert_text, persistent, style, when=)` | Button that shows a popup alert dialog. |
482
+ | `add_notification(text, notification_text, persistent, style, when=)` | Button that shows a brief top-of-chat notification. |
483
+ | `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. |
484
+
485
+ **Bulk helpers**
486
+
487
+ | **Method** | **Description** |
488
+ |------------|-----------------|
489
+ | `extend(buttons, column=, when=)` | Add multiple buttons from a `dict[str, InlineButton \| None]` or `list[str]`. |
490
+ | `extend_rows(*rows, when=)` | Append one or more pre-built `list[tuple[str, InlineButton]]` rows. |
491
+
492
+ All button methods accept a `style` parameter: `"danger"` (red), `"success"` (green), or `"primary"` (blue).
493
+
494
+ ```python
495
+ InlineKeyboard()
496
+ .add_link("YouTube", "https://youtube.com")
497
+ .add_copy("Copy Me", "copied!")
498
+ .add_alert("Info", "This is an alert")
499
+ .row()
500
+ .add_callback("Click Me!", self.handle)
501
+ .column_start()
502
+ .add_link("A", "https://example.com")
503
+ .add_callback("B", self.handle_b)
504
+ .column_end()
505
+ .extend({"Alert": AlertButton("Hi!"), "Notify": NotificationButton("Hey")}, column=True)
506
+ ```
507
+
508
+ ### `ReplyKeyboard`
509
+
510
+ Fluent builder for Telegram reply keyboards. Mirrors the `InlineKeyboard` layout API (`row`, `column_start`, `column_end`, `extend`, `extend_rows`).
511
+
512
+ **Constructor parameters**
513
+
514
+ | **Parameter** | **Default** | **Description** |
515
+ |---|---|---|
516
+ | `resize_keyboard` | `True` | Shrink the keyboard to fit its buttons. |
517
+ | `one_time_keyboard` | `False` | Hide the keyboard after the first press. |
518
+ | `input_field_placeholder` | `None` | Placeholder text shown while the keyboard is active (max 64 chars). |
519
+ | `selective` | `False` | Show only to mentioned users or the original sender. |
520
+ | `is_persistent` | `False` | Always show the keyboard; do not collapse it to an icon. |
521
+
522
+ **Button methods**
523
+
524
+ | **Method** | **Description** |
525
+ |---|---|
526
+ | `add(text, button, when=)` | Attach any `ReplyButton` instance. |
527
+ | `add_text(text, when=)` | Plain text button — sends the label as a message. |
528
+ | `add_contact(text, when=)` | Prompts the user to share their phone number (private chats only). |
529
+ | `add_location(text, when=)` | Prompts the user to share their geolocation (private chats only). |
530
+ | `add_poll(text, poll_type, when=)` | Opens the poll creation dialog; `poll_type` can be `"quiz"`, `"regular"`, or `None`. |
531
+ | `add_webapp(text, url, when=)` | Opens a Telegram Mini App. |
532
+ | `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. |
533
+ | `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. |
534
+
535
+ **Layout helpers** — identical to `InlineKeyboard`
536
+
537
+ | **Method** | **Description** |
538
+ |---|---|
539
+ | `row(when=)` | Finalize the current row and start a new one. |
540
+ | `column_start()` | Enable column mode — every subsequent button gets its own row. |
541
+ | `column_end()` | Disable column mode and flush the current row. |
542
+ | `extend(buttons, column=, when=)` | Add multiple buttons from a `dict[str, ReplyButton \| None]` or `list[str]`. |
543
+ | `extend_rows(*rows, when=)` | Append one or more pre-built `list[tuple[str, ReplyButton]]` rows. |
544
+
545
+ ```python
546
+ ReplyKeyboard(input_field_placeholder="Choose:", one_time_keyboard=True)
547
+ .add_text("Hello!")
548
+ .add_text("Hi")
549
+ .row()
550
+ .add_contact("📱 Phone")
551
+ .add_location("📍 Location")
552
+ .add_poll("📊 Poll")
553
+ .row()
554
+ .add_request_user("Pick user", request_id=1)
555
+ .add_request_chat("Pick chat", request_id=2)
556
+ ```
557
+
558
+ ## Telekit DSL
559
+
560
+ ### `InstanceDSLHandler`
561
+
562
+ Instance-oriented variant of `DSLHandler` where each instance carries its own
563
+ `executable_model`, `_script_data_factory`, and `_jinja_env` — allowing multiple
564
+ instances to run completely independent scripts simultaneously.
565
+
566
+ Use the `*_locally` instance methods instead of the class-level ones:
567
+
568
+ ```python
569
+ class MyHandler(telekit.InstanceDSLHandler):
570
+ @classmethod
571
+ def init_handler(cls) -> None:
572
+ cls.on.message().invoke(cls.handle)
573
+
574
+ def handle(self):
575
+ script = fetch_script_from_db(self.user.id) # per-user DSL
576
+ self.analyze_string_locally(script)
577
+ self.start_script()
578
+ ```
579
+
580
+ | **Method** | **Description** |
581
+ | --------------------------------------- | ---------------------------------------------------- |
582
+ | `analyze_file_locally(path, encoding)` | Analyse a script file on this instance. |
583
+ | `analyze_string_locally(script)` | Analyse a DSL string on this instance. |
584
+ | `analyze_canvas_locally(file_path)` | Analyse an Obsidian `.canvas` file on this instance. |
585
+ | `analyze_executable_model_locally(model)` | Load a pre-built model dict on this instance. |
586
+
587
+ **Security**
588
+
589
+ When accepting scripts from untrusted users, restrict dangerous features via the
590
+ `RESTRICTED` class attribute. Set `DEFAULT_TIMEOUT` to control the fallback timeout,
591
+ and `DEFAULT_CONFIG` to provide a safe base config.
592
+
593
+ ```python
594
+ class SafeDSL(telekit.InstanceDSLHandler):
595
+ RESTRICTED: list[RestrictedToken] = ["hook", "jinja", "redirect", "handoff", "config"]
596
+ DEFAULT_TIMEOUT = 120
597
+ DEFAULT_CONFIG = {"template": "vars"}
598
+ ```
383
599
 
384
- [View changelog on GitHub](https://github.com/Romashkaa/telekit/blob/main/CHANGELOG.md)
600
+ | **Token** | **Effect** |
601
+ | -------------- | ----------------------------------------------------------------------------------------------- |
602
+ | `"handoff"` | Disables `handoff` button type (cross-handler transitions). |
603
+ | `"redirect"` | Disables `redirect` button type (simulated user messages). |
604
+ | `"hook"` | Removes all `on_enter`, `on_enter_once`, `on_exit`, `on_timeout` hooks. |
605
+ | `"jinja"` | Forces template engine to `"vars"`; Jinja is never executed. |
606
+ | `"timeout"` | Ignores per-script `timeout_time`; uses `DEFAULT_TIMEOUT` only. |
607
+ | `"config"` | Replaces script config with `DEFAULT_CONFIG`; `vars_*` keys are preserved unless `"vars"` is also set. |
608
+ | `"vars"` | Removes all `vars_*` keys and disables `{{variable}}` substitution. |
609
+ | `"images"` | Strips `image` field from every scene. |
610
+ | `"links"` | Disables `link` button type (external URLs). |
611
+ | `"suggest"` | Disables `suggest` button type (pre-filled entry suggestions). |
612
+ | `"entry"` | Disables entry handlers (free-text input routing). |
613
+ | `"next"` | Disables `next` magic scene navigation. |
614
+ | `"back"` | Disables `back` magic scene navigation. |
615
+
616
+ ## Scheduler
617
+
618
+ - Added `every` decorator for scheduling functions in a background daemon thread.
619
+ - Implemented `PeriodicTask` class to manage periodic execution and error handling.
620
+
621
+ ## Others
622
+
623
+ - Enhanced `Debug` class with callback query tracing functionality.
624
+ - Refactored `BaseSender` to use `send_or_handle_error`.
@@ -1,39 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: telekit
3
- Version: 2.3.0b3
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
1
  ![TeleKit](https://github.com/Romashkaa/images/blob/main/TeleKitWide.png?raw=true)
38
2
 
39
3
  [![PyPI](https://img.shields.io/pypi/v/telekit.svg)](https://pypi.org/project/telekit/)
@@ -77,7 +41,7 @@ Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/ma
77
41
 
78
42
  > See the [full example](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
79
43
 
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.
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.
81
45
 
82
46
  **Key features:**
83
47
  - Declarative bot logic with **chains** for effortless handling of complex conversations
@@ -85,6 +49,7 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
85
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**
86
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
87
51
  - Built-in **Permission** and **Logging** system for user management
52
+ - Reusable **Traits** system for pluggable, self-contained behavior modules
88
53
  - Seamless integration with [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI)
89
54
  - Fast to develop and easy-to-extend code
90
55
 
@@ -340,14 +305,54 @@ telekit.Server(BOT_TOKEN).polling()
340
305
  <summary>Click to see what you can do with the DSL</summary>
341
306
  <table>
342
307
  <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>
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>
345
314
  </tr>
346
315
  </table>
347
316
  </details>
348
317
 
349
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.
350
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
+
351
356
  ### Example Bot
352
357
 
353
358
  You can launch an example bot by **running the following code**:
@@ -368,6 +373,7 @@ It includes example commands, dialogs, keyboards, and style usage.
368
373
  - **Styles API** for rich text (`Bold`, `Italic`, `Links`) with **automatic escaping**.
369
374
  - Deep linking and **typed command parameters**.
370
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).
371
377
  - **Zero-code** [Obsidian Canvas](https://github.com/Romashkaa/telekit/blob/main/docs/examples/canvas_faq.md) mode.
372
378
  - Seamless integration with **pyTelegramBotAPI**.
373
379
 
@@ -375,10 +381,4 @@ Telekit doesn't try to be everything.
375
381
  It tries to make Telegram bot development easier.
376
382
 
377
383
  > [!TIP]
378
- > 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)
379
-
380
- ---
381
-
382
- # Changes in version 2.3.0b3
383
-
384
- [View changelog on GitHub](https://github.com/Romashkaa/telekit/blob/main/CHANGELOG.md)
384
+ > 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)
@@ -26,10 +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
- return f"[View changelog on GitHub](https://github.com/Romashkaa/telekit/blob/main/CHANGELOG.md)"
32
-
31
+ with open('CHANGELOG.md', 'r') as f:
32
+ return f.read()
33
+
33
34
  def install_requires():
34
35
  with open('telekit/requirements.txt', 'r') as f:
35
36
  return f.read().split("\n")
@@ -22,11 +22,13 @@ from ._trait import Trait
22
22
  from ._chain import Chain
23
23
  from ._callback_query_handler import CallbackQueryHandler
24
24
  from .server import Server, example
25
+ from ._inline_keyboard import InlineKeyboard
26
+ from ._reply_keyboard import ReplyKeyboard
25
27
  from ._snapvault import Vault
26
28
  from ._chapters import chapters
27
29
  from ._user import User
28
30
  from ._telekit_dsl.telekit_dsl import TelekitDSL
29
- from ._telekit_dsl.mixin import DSLHandler
31
+ from ._telekit_dsl.mixin import DSLHandler, InstanceDSLHandler
30
32
  from ._logger import enable_file_logging
31
33
 
32
34
  from . import senders
@@ -38,6 +40,7 @@ from . import dices
38
40
  from . import utils
39
41
  from . import traits
40
42
  from . import debug
43
+ from . import scheduler
41
44
 
42
45
  Styles = styles.Styles
43
46
 
@@ -52,6 +55,7 @@ __all__ = [
52
55
  "parameters",
53
56
  "inline_buttons",
54
57
  "dices",
58
+ "scheduler",
55
59
 
56
60
  "Styles",
57
61
  "User",
@@ -62,8 +66,12 @@ __all__ = [
62
66
  "Handler",
63
67
  "CallbackQueryHandler",
64
68
 
69
+ "InlineKeyboard",
70
+ "ReplyKeyboard",
71
+
65
72
  "TelekitDSL",
66
73
  "DSLHandler",
74
+ "InstanceDSLHandler",
67
75
 
68
76
  "Vault",
69
77
  "enable_file_logging",
@@ -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
 
@@ -30,7 +30,7 @@ from .debug import Debug
30
30
  from .styles import TextEntity
31
31
 
32
32
  # Chain modules
33
- from ._chain_base import library
33
+ from ._chain_base import _library
34
34
  from ._chain_inline_keyboards_logic import ChainInlineKeyboardLogic
35
35
  from ._chain_entry_logic import ChainEntryLogic, TextDocument
36
36
 
@@ -156,7 +156,7 @@ class Chain(ChainInlineKeyboardLogic, ChainEntryLogic):
156
156
  self._previous_message = message
157
157
 
158
158
  if Debug.timeout_warnings and _handler and not _timeout:
159
- library.warning(
159
+ _library.warning(
160
160
  "Next-message handler is active, but no timeout was set for the chain. "
161
161
  "This may cause the bot to wait indefinitely."
162
162
  )
@@ -32,8 +32,7 @@ from . import _timeout
32
32
 
33
33
  # Logging
34
34
  from .debug import Debug
35
- from ._logger import logger
36
- library = logger.library
35
+ from ._logger import _library as _library
37
36
 
38
37
  class ChainBase:
39
38
 
@@ -31,8 +31,10 @@ from telebot.types import (
31
31
  )
32
32
 
33
33
  from ._callback_query_handler import CallbackQueryHandler
34
+ from ._inline_keyboard import InlineKeyboard
35
+ from ._reply_keyboard import ReplyKeyboard
34
36
  from ._inline_buttons import InlineButton, CallbackButton
35
- from ._chain_base import ChainBase, library
37
+ from ._chain_base import ChainBase, _library
36
38
 
37
39
  if typing.TYPE_CHECKING:
38
40
  from ._chain import Chain # only for type hints
@@ -249,7 +251,7 @@ class ChainInlineKeyboardLogic(ChainBase):
249
251
  if not isinstance(choices, dict):
250
252
  for c in choices:
251
253
  if type(c).__str__ is object.__str__:
252
- library.warning(
254
+ _library.warning(
253
255
  f"{type(c).__name__} does not implement __str__. "
254
256
  f"Consider passing a dict with explicit labels.",
255
257
  stacklevel=3
@@ -335,6 +337,73 @@ class ChainInlineKeyboardLogic(ChainBase):
335
337
 
336
338
  self.sender.set_reply_markup(markup)
337
339
 
340
+ #
341
+
342
+ def set_keyboard(self, keyboard: InlineKeyboard | ReplyKeyboard) -> None:
343
+ """
344
+ Sets a keyboard for the chain from an `InlineKeyboard` or `ReplyKeyboard` object.
345
+
346
+ Example::
347
+
348
+ from telekit.types import InlineKeyboard, ReplyKeyboard
349
+
350
+ # Inline keyboard
351
+ self.chain.set_keyboard(
352
+ InlineKeyboard()
353
+ .add_callback("Click Me!", self.handle)
354
+ .row()
355
+ .add_link("YouTube", "https://youtube.com")
356
+ )
357
+
358
+ # Reply keyboard
359
+ self.chain.set_keyboard(
360
+ ReplyKeyboard(one_time_keyboard=True)
361
+ .add_text("Hello!")
362
+ .add_text("Hi")
363
+ .row()
364
+ .add_contact("📱 Share phone")
365
+ )
366
+
367
+ :param keyboard: A constructed `InlineKeyboard` instance with buttons and rows.
368
+ :type keyboard: `InlineKeyboard`
369
+ """
370
+ # --- ReplyKeyboard ---
371
+ if isinstance(keyboard, ReplyKeyboard):
372
+ markup = keyboard._compile()
373
+ self.sender.set_reply_markup(markup)
374
+ self._handler.set_button_callbacks(None)
375
+ return
376
+
377
+ # --- InlineKeyboard ---
378
+ button_callbacks: dict[str, Callable[[CallbackQuery], None]] = {}
379
+ markup = InlineKeyboardMarkup()
380
+
381
+ button_index = 0
382
+
383
+ for row in keyboard._compile():
384
+ compiled_row: list[InlineKeyboardButton] = []
385
+ for caption, inline_button in row:
386
+ if isinstance(inline_button, CallbackButton):
387
+ invoker = inline_button.build_invoker(self._cancel_timeout_and_handlers)
388
+ callback_data = CallbackQueryHandler.inline_button(f"{button_index}:{random.randint(1000, 9999)}")
389
+ button_callbacks[callback_data] = invoker
390
+ compiled_row.append(
391
+ InlineKeyboardButton(
392
+ text=caption,
393
+ callback_data=callback_data,
394
+ **invoker._kwargs # pyright: ignore[reportArgumentType]
395
+ )
396
+ )
397
+ else:
398
+ compiled_row.append(inline_button._compile(caption))
399
+ button_index += 1
400
+ markup.keyboard.append(compiled_row)
401
+
402
+ self.sender.set_reply_markup(markup)
403
+ self._handler.set_button_callbacks(button_callbacks)
404
+
405
+ # Utils
406
+
338
407
  def _get_invoker_with_argument(self, callback: Callable, argument: Any, query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
339
408
  def invoker(call: CallbackQuery) -> None:
340
409
  self._cancel_timeout_and_handlers()