Metadata-Version: 2.4
Name: telekit
Version: 2.6.0a1
Summary: Declarative, developer-friendly library for building Telegram bots
Home-page: https://github.com/Romashkaa/telekit
Author: romashka
Author-email: notromashka@gmail.com
License: GPLv3
Project-URL: GitHub, https://github.com/Romashkaa/telekit
Project-URL: Telegram, https://t.me/TelekitLib
Keywords: telegram bot api declarative tools bot-api
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: charset_normalizer==3.4.2
Requires-Dist: Jinja2==3.1.6
Requires-Dist: pyTelegramBotAPI==4.31.0
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

![TeleKit](https://github.com/Romashkaa/images/blob/main/TeleKitWide.png?raw=true)

[![PyPI](https://img.shields.io/pypi/v/telekit.svg)](https://pypi.org/project/telekit/)
[![Python](https://img.shields.io/pypi/pyversions/telekit.svg)](https://pypi.org/project/telekit/)
[![PyPI Downloads](https://static.pepy.tech/badge/telekit)](https://pepy.tech/project/telekit)

# Telekit

**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.

```py
import telekit

class MyStartHandler(telekit.Handler):
    @classmethod
    def init_handler(cls):
        cls.on.command('start').invoke(cls.handle_start)

    def handle_start(self):
        self.chain.sender.set_text("Hello!")
        self.chain.sender.set_photo("robot.png")
        self.chain.send()

telekit.Server("BOT_TOKEN").polling()
```

> Send "Hello!" with a photo on `/start`

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.

```js
@ main {
    title   = "🎉 Fun Facts Quiz";
    message = "Test your knowledge with 10 fun questions!";

    buttons {
        question_1("Start Quiz");
    }
}
```

> See the [full example](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)

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.

**Key features:**  
- **Chains** handle complex conversations without state machines
- [Ready-to-use DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md) for FAQs and interactive scripts
- Automatic [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**
- **Deep Linking** with type-checked [Command Parameters](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/command_trigger_parameters.md)
- Built-in **Permission** and **Logging** system
- Reusable **Traits** for pluggable behavior modules
- Works with [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI)

[GitHub](https://github.com/Romashkaa/telekit)
[PyPI](https://pypi.org/project/telekit/)
[Telegram](https://t.me/NotRomashka)
[Community](https://t.me/+wu-dFrOBFIwyNzc0)

## Contents

- 🌟 [Tutorial](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/0_tutorial.md)
- 🎆 [Gallery](https://github.com/Romashkaa/telekit/blob/main/docs/documentation/gallery.md)
- 👀 [Examples](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
    - [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
    - [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
    - [Counter](https://github.com/Romashkaa/telekit/blob/main/docs/examples/counter.md)
    - [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
    - [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
    - [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)

## Overview

In **Telekit**, dialogs read like normal method calls. You point to the next step. Telekit calls it when the user replies.

### Entries

No state machines. Just tell Telekit which method should handle the next user message.

```py
def handle(self):
    self.chain.sender.set_text("👋 Hello! What is your name?")
    self.chain.set_entry_text(self.handle_name)
    self.chain.send()

def handle_name(self, name: str):
    self.chain.sender.set_text(f"Nice to meet you, {name}!")
    self.chain.send()
```

`handle` sends a message and registers `handle_name` as the next step with `set_entry_text`. When the user replies, Telekit calls `handle_name` and passes the reply as a plain `str`.

> That's it. No enums. No manual state tracking. No boilerplate.

### Inline Keyboards

The fastest way to add buttons to a message. Pass a `dict` where each key is the button label and each value is the callback to run when pressed:

```python
self.chain.set_inline_keyboard(
    {
        "✏️ Change": self.change_name,
        "❌ Delete": self.delete,
    }
)
```

`row_width` controls how many buttons appear per row:

```python
self.chain.set_inline_keyboard(
    {
        "One":   self.one,
        "Two":   self.two,
        "Three": self.three,
        "Four":  self.four,
        "Five":  self.five,
    },
    row_width=(3, 2)  # first row: 3 buttons, second row: 2
)
```

```
╭──────────┬──────────┬──────────╮
│   One    │   Two    │  Three   │
├──────────┴──┬───────┴──────────┤
│    Four     │       Five       │
╰─────────────┴──────────────────╯
```

For precise row layout or conditional buttons, use `InlineKeyboard`, a builder you compose step by step:

```python
self.chain.set_keyboard(
    InlineKeyboard()
        .add_callback("-", self.decrement, style="danger")
        .add_callback("+", self.increment, style="success")
    .row()
        .add_callback("↺ Reset", self.reset)
)
```

```
╭──────────┬──────────╮
│    -     │    +     │
├──────────┴──────────┤
│       ↺ Reset       │
╰─────────────────────╯
```

> `InlineKeyboard` is built by chaining method calls. Just call `.row()` to start a new row.

**Not just callback buttons**

| **Method**            | **Description**                                                  |
| --------------------- | ---------------------------------------------------------------- |
| `add_callback(...)`   | Button that fires a callback function.                           |
| `add_link(...)`       | Button that opens a URL.                                         |
| `add_copy(...)`       | Button that copies text to the clipboard.                        |
| `add_alert(...)`      | Button that shows a popup alert dialog.                          |
| `add_notification(...)` | Button that shows a brief top-of-chat notification.            |
| `add_static(...)`     | Decorative button with no action.                                |
| `add_webapp(...)`     | Button that opens a Telegram Mini App.                           |
| `add_suggest(...)`    | Button that simulates the user sending a message.                |

### Reply Keyboards

Reply keyboards replace the system keyboard with buttons at the bottom of the chat. Tapping one sends its text as a message, or triggers a system action like sharing a phone number or location.

```python
self.chain.set_keyboard(
    ReplyKeyboard(one_time_keyboard=True)
        .add_text("Hello!")
        .add_text("Hi")
    .row()
        .add_contact("📱 Share phone")
        .add_location("📍 Share location")
)
```

### Command Parameters

Telekit can parse and validate command parameters for you.

```py
from telekit.parameters import *

class GreetHandler(telekit.Handler):
    @classmethod
    def init_handler(cls) -> None:
        cls.on.command("greet", params=[Int(), Str()]).invoke(cls.handle)

    def handle(self, age: int | None = None, name: str | None = None):
        if age is None or name is None:
            self.chain.sender.set_text("Usage: /greet <age> <name>")
        else:
            self.chain.sender.set_text(f"Hello, {name}! You are {age} years old. Next year you'll turn {age + 1} 😅")
        self.chain.send()
```

Now `/greet 128 Dracula` or even `/greet 64 "Alice Reingold"` are parsed automatically.

> [!NOTE]
> If arguments are invalid or missing, you simply receive `None` and decide how to respond.

### Dialogue

Dialogs are built as a chain of steps. Each method waits for the user before continuing.

```py
class DialogueHandler(telekit.Handler):

    @classmethod
    def init_handler(cls) -> None:
        cls.on.text("hello", "hi", "hey").invoke(cls.handle_hello)

    def handle_hello(self) -> None:
        self.chain.sender.set_text("👋 Hello! What is your name?")
        if self.user.first_name:
            self.chain.set_entry_suggestions([self.user.first_name])
        self.chain.set_entry_text(self.handle_name)
        self.chain.send()

    def handle_name(self, name: str) -> None:
        self.user_name = name
        self.chain.sender.set_text("Nice! How are you feeling today?")
        self.chain.set_entry_text(self.handle_feeling)
        self.chain.send()

    def handle_feeling(self, feeling: str) -> None:
        self.chain.sender.set_text(f"Got it, {self.user_name.title()}! You feel: {feeling}")
        self.chain.set_inline_keyboard({"↺ Restart": self.handle_hello})
        self.chain.send()
```

How it works:

- The handler reacts to `"hello"`, `"hi"`, or `"hey"` in any case.
- `handle_hello` asks for the user's name.ч
- `set_entry_suggestions` attaches the user's Telegram `first_name` as a suggestion button.
- `handle_name` stores the name in `self.user_name`.
- `handle_feeling` closes the flow and adds a "↺ Restart" button that routes back to the start.

It reads like regular Python because it is regular Python.

### Sender

Want to attach an image, document or add an effect in a single line?

```python
self.chain.sender.set_effect(Effect.HEART) # Add effect to message. Use enum or string
self.chain.sender.set_photo("robot.png") # Attach photo. URL, file_id, or path
self.chain.sender.set_document("README.md") # Attach document. URL, file_id, or path
self.chain.sender.set_text_as_document("Hello, this is a text document!") # Convert string to text document
self.chain.sender.send_chat_action(ChatAction.TYPING) # Send chat action. Use enum or string
```

> [!NOTE]
> Telekit picks `bot.send_message` or `bot.send_photo` based on the content you attach.

### Styles

Describe formatting as objects instead of writing raw HTML or Markdown.

```py
from telekit.styles import *

def handle(self) -> None:
    self.chain.sender.set_text(
        Bold("Text style examples:\n"),
        Stack(
            Bold("Bold text"),
            Italic("Italic text"),
            Bold(Italic("Bold + italic")),
            Link("Link", url="https://example.com"),
            BotLink("Deep link", username="MyBot", start="promo_42"),
            start="- {{index}}. ",
            sep=".\n",
        )
    )
    self.chain.send()
```

You describe structure. Telekit turns that structure into HTML or MarkdownV2:

```html
<b>Text style examples:</b>

- 1. <b>Bold text</b>.
- 2. <i>Italic text</i>.
- 3. <b><i>Bold + italic</i></b>.
- 4. <a href="https://example.com">Link</a>.
- 5. <a href="https://t.me/MyBot?start=promo_42">Deep link</a>
```

You skip manual escaping and the broken formatting one stray character causes.

### Telekit DSL

Prefer not to write dialog logic in Python? Use the built-in DSL with Jinja support.

```py
import telekit

class QuizHandler(telekit.DSLHandler):
    @classmethod
    def init_handler(cls) -> None:
        cls.analyze_string(script)
        cls.on.command("start").invoke(cls.start_script)

script = """
$ timeout {
    time = 20; // 20 sec.
}

@ main {
    title   = "🎉 Fun Facts Quiz";
    message = "Test your knowledge with 10 fun questions!";

    buttons {
        next("Start Quiz");
    }
}

@ question_1 {
    title   = "🐶 Question 1";
    message = "Which animal is the fastest on land?";
    buttons {
        _lose("Elephant");
        next("Cheetah");       // correct answer
        _lose("Horse");
        _lose("Lion");
    }
}

/* ... */
"""

telekit.Server(BOT_TOKEN).polling()
```

**Key features of the Telekit DSL:**

- Scene-based architecture
- Anonymous scenes
- Automatic navigation stack management
- Input handling
- Images support and link buttons
- Template variables
- Custom variables
- Hooks (Python API integration)
- Jinja template engine

<details>
  <summary>🎆 Click to see what you can do with the DSL</summary>
  <table>
    <tr>
      <td><img src="./docs/images/telekit_example_7.jpg" alt="Telekit Example 7" width="300"></td>
      <td><img src="./docs/images/telekit_example_8.jpg" alt="Telekit Example 8" width="300"></td>
    </tr>
    <tr>
      <td><img src="./docs/images/telekit_example_6.jpg" alt="Telekit Example 7" width="300"></td>
      <td><img src="./docs/images/telekit_example_1.jpg" alt="Telekit Example 8" width="300"></td>
    </tr>
  </table>
</details>

> [!TIP]
> 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.

### Traits

Traits are reusable behavior modules you can mix into any handler.

Here's the built-in `CalendarPick` trait: a user picks a date from an inline calendar, and a callback handles the result.

```py
from telekit.traits import CalendarPick

class CalendarHandler(CalendarPick, telekit.Handler):

    @classmethod
    def init_handler(cls) -> None:
        cls.on.command("calendar").invoke(cls.handle)

    def handle(self) -> None:
        self.chain.sender.set_title("📅 Choose a date")
        self.chain.sender.set_message("Select any date — past or future:")
        self.chain.sender.set_remove_text(False)

        self.calendar_pick(self.handle_date) # HERE

    def handle_date(self, date: datetime.date) -> None:
        self.chain.sender.set_text(f"You picked: {date}")
        self.chain.send()
```

<details>
  <summary>Result</summary>
  <table>
    <tr>
      <td><img src="./docs/images/calendar.png" alt="Telekit Calendar Example" width="500"></td>
    </tr>
  </table>
</details>

### Example Bot

Run this to launch an example bot:

```py
import telekit

telekit.example(YOUR_BOT_TOKEN)
```

It includes example commands, dialogs, keyboards, and style usage.

## Why Telekit

- Chains instead of an FSM.
- Declarative, behavior-focused bot logic with minimal boilerplate.
- Automatic callback routing and input handling.
- A Styles API for rich text (`Bold`, `Italic`, links) with automatic escaping.
- Deep linking and typed command parameters.
- A built-in DSL for menus, FAQs, and simple bots.
- Reusable Traits for composable behavior, including a built-in calendar picker.
- Zero-code mode [Obsidian Canvas](https://github.com/Romashkaa/telekit/blob/main/docs/examples/canvas_faq.md) mode.
- Works with `pyTelegramBotAPI`.

Telekit focuses on one job: making Telegram bot development easier.

> [!TIP]
> Interested? Start with the [Tutorial](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/0_tutorial.md).

---

# Changes in version 2.6.0a1

### v2.5.4 `(bug-fix)`
- Fix: Update condition to check for `None` instead of truthy value in `DSLHandler`
### v2.5.3 `(bug-fix)`
- Fix: Remove `_` parameters from `_filter_entry` and `_handle_entry` methods in `DSLHandler`
### v2.5.2
- Add `random_` variable prefix in `DSLHandler` — resolves to a random choice from a `list`/`tuple`/`str` static variable

### v2.5.1 `(bug-fix)`
- Implement `TelegramMarkdownV2Sanitizer` for improved `MarkdownV2` handling

### v2.5.0 `(final)`
- Refactor `CalendarPick` trait to use `set_keyboard`

### v2.5.0`b3`
- Added `utils.Markers` class
- Added `HTMLText` class for handling Telegram HTML strings with tag-aware indexing and slicing.
- Added `PaginatedText` trait for displaying long HTML text in a paginated format, supporting navigation and smart splitting.
- Added `__radd__` to `TextEntity`: `"Regular" + Bold(" and Bold")`
- Added `__mul__` to `TextEntity`: `Bold("Text") * 3`
- Added `enabled=` parameter to `TextEntity`: `Bold("bold text", enabled=is_text_bold)`
- Added `TextBuilder` class – a fluent message composition API mirroring `InlineKeyboard`'s builder pattern
- Added styles to `telekit.types`
- Added `utils.CyclicList`
- Fixed `_answer_callback_query` to always call `bot.answer_callback_query()`, even without a popup text

### v2.5.0`b2`
- Added the `escape` parameter to `telekit.utils.*`:
  - `make_user_link`
  - `make_bot_link`
- `Handler.handlers_dict` now excludes private handlers (classes whose names start with `_`).
- Added `Debug.duplicate_handler_warnings` to warn about duplicate handler names during initialization.
- Added `Handler.chat` object (BETA)

### v2.5.0`b1`
- Added `Sender.send_message` method.
- Added `utils.make_mention` utility for generating `tg://user?id=` mention links.
- Added new inline button types to `inline_buttons`:
  - `ContactButton` — mentions a user by Telegram ID via `tg://user?id=`.
  - `UserLinkButton` — opens a user profile by username; supports pre-filled message text.
  - `BotLinkButton` — opens a bot by username; supports deep-link `?start=` payload.
- Added new methods to `InlineKeyboard`:
  - `add_contact` — adds a `ContactButton`.
  - `add_user_link` — adds a `UserLinkButton`.
  - `add_bot_link` — adds a `BotLinkButton`.

### v2.5.0`b0`
- Added support for t-strings (PEP 750, Python 3.14+) in `TextEntity`.
