Skip to content

Types

The data models, enums, and exceptions that underpin the library. All models are built on Pydantic v2.

telegram_menu_builder.types

Core type definitions for the menu builder library.

This module contains all the fundamental data structures used throughout the library, implemented with Pydantic v2 for validation and type safety.

HandlerFunc module-attribute

HandlerFunc = Callable[[Any, Any, dict[str, Any]], Awaitable[None]]

Type alias for async handler functions.

A handler is an async callable that receives the Telegram Update, the bot Context, and the dict of decoded parameters from the callback data, and returns None.

StorageStrategy

Bases: StrEnum

Strategy for storing callback data based on size and persistence requirements.

Attributes:

Name Type Description
INLINE

Store directly in callback_data (< 60 bytes)

SHORT

Store in temporary storage with TTL (60-500 bytes)

PERSISTENT

Store in permanent storage (> 500 bytes or long-lived)

Source code in src/telegram_menu_builder/types.py
class StorageStrategy(StrEnum):
    """Strategy for storing callback data based on size and persistence requirements.

    Attributes:
        INLINE: Store directly in callback_data (< 60 bytes)
        SHORT: Store in temporary storage with TTL (60-500 bytes)
        PERSISTENT: Store in permanent storage (> 500 bytes or long-lived)
    """

    INLINE = "inline"
    SHORT = "short"
    PERSISTENT = "persistent"

MenuAction

Bases: BaseModel

Represents an action to be executed when a menu item is selected.

This class handles the encoding and decoding of callback data, intelligently choosing the appropriate storage strategy based on data size.

Attributes:

Name Type Description
handler str

Name of the handler function to call

params dict[str, Any]

Dictionary of parameters to pass to the handler

strategy StorageStrategy | None

Storage strategy (auto-selected if None)

ttl int

Time-to-live in seconds for SHORT strategy storage

Example

action = MenuAction( ... handler="edit_user", ... params={"user_id": 123, "field": "email"} ... )

Source code in src/telegram_menu_builder/types.py
class MenuAction(BaseModel):
    """Represents an action to be executed when a menu item is selected.

    This class handles the encoding and decoding of callback data, intelligently
    choosing the appropriate storage strategy based on data size.

    Attributes:
        handler: Name of the handler function to call
        params: Dictionary of parameters to pass to the handler
        strategy: Storage strategy (auto-selected if None)
        ttl: Time-to-live in seconds for SHORT strategy storage

    Example:
        >>> action = MenuAction(
        ...     handler="edit_user",
        ...     params={"user_id": 123, "field": "email"}
        ... )
    """

    model_config = ConfigDict(frozen=False, arbitrary_types_allowed=True)

    handler: str = Field(..., min_length=1, max_length=100, description="Handler function name")
    params: dict[str, Any] = Field(
        default_factory=dict, description="Parameters to pass to handler"
    )
    strategy: StorageStrategy | None = Field(
        default=None, description="Storage strategy (auto-selected if None)"
    )
    ttl: int = Field(default=3600, ge=60, le=86400, description="TTL in seconds for SHORT strategy")

    @field_validator("handler")
    @classmethod
    def validate_handler_name(cls, v: str) -> str:
        """Validate handler name follows Python identifier rules."""
        if not v.replace("_", "").replace(".", "").isalnum():
            raise ValueError(
                f"Handler name '{v}' must be a valid Python identifier or dot-separated path"
            )
        return v

    @field_validator("params")
    @classmethod
    def validate_params_serializable(cls, v: dict[str, Any]) -> dict[str, Any]:
        """Validate that params contain only JSON-serializable values."""
        try:
            json.dumps(v)
        except (TypeError, ValueError) as e:
            raise ValueError(f"Params must be JSON-serializable: {e}") from e
        return v

validate_handler_name classmethod

validate_handler_name(v: str) -> str

Validate handler name follows Python identifier rules.

Source code in src/telegram_menu_builder/types.py
@field_validator("handler")
@classmethod
def validate_handler_name(cls, v: str) -> str:
    """Validate handler name follows Python identifier rules."""
    if not v.replace("_", "").replace(".", "").isalnum():
        raise ValueError(
            f"Handler name '{v}' must be a valid Python identifier or dot-separated path"
        )
    return v

validate_params_serializable classmethod

validate_params_serializable(v: dict[str, Any]) -> dict[str, Any]

Validate that params contain only JSON-serializable values.

Source code in src/telegram_menu_builder/types.py
@field_validator("params")
@classmethod
def validate_params_serializable(cls, v: dict[str, Any]) -> dict[str, Any]:
    """Validate that params contain only JSON-serializable values."""
    try:
        json.dumps(v)
    except (TypeError, ValueError) as e:
        raise ValueError(f"Params must be JSON-serializable: {e}") from e
    return v

MenuItem

Bases: BaseModel

Represents a single item in an inline keyboard menu.

Attributes:

Name Type Description
text str

Display text for the button

callback_data str | None

Encoded callback data (max 64 bytes for Telegram)

url str | None

Optional URL for URL buttons

Example

item = MenuItem( ... text="⚙️ Settings", ... callback_data="encoded_data_here" ... )

Source code in src/telegram_menu_builder/types.py
class MenuItem(BaseModel):
    """Represents a single item in an inline keyboard menu.

    Attributes:
        text: Display text for the button
        callback_data: Encoded callback data (max 64 bytes for Telegram)
        url: Optional URL for URL buttons

    Example:
        >>> item = MenuItem(
        ...     text="⚙️ Settings",
        ...     callback_data="encoded_data_here"
        ... )
    """

    model_config = ConfigDict(frozen=True)

    text: str = Field(
        ..., min_length=1, max_length=100, description="Button text displayed to user"
    )
    callback_data: str | None = Field(
        default=None, max_length=64, description="Callback data (Telegram limit: 64 bytes)"
    )
    url: str | None = Field(default=None, description="URL for URL-type buttons")

    @field_validator("callback_data")
    @classmethod
    def validate_callback_size(cls, v: str | None) -> str | None:
        """Ensure callback_data doesn't exceed Telegram's 64-byte limit."""
        if v is not None and len(v.encode("utf-8")) > 64:
            raise ValueError(
                f"callback_data exceeds Telegram's 64-byte limit: {len(v.encode('utf-8'))} bytes"
            )
        return v

    def to_telegram_button(self) -> InlineKeyboardButton:
        """Convert to telegram.InlineKeyboardButton.

        Returns:
            InlineKeyboardButton instance ready for use
        """
        if self.url:
            return InlineKeyboardButton(text=self.text, url=self.url)
        return InlineKeyboardButton(text=self.text, callback_data=self.callback_data or "")

validate_callback_size classmethod

validate_callback_size(v: str | None) -> str | None

Ensure callback_data doesn't exceed Telegram's 64-byte limit.

Source code in src/telegram_menu_builder/types.py
@field_validator("callback_data")
@classmethod
def validate_callback_size(cls, v: str | None) -> str | None:
    """Ensure callback_data doesn't exceed Telegram's 64-byte limit."""
    if v is not None and len(v.encode("utf-8")) > 64:
        raise ValueError(
            f"callback_data exceeds Telegram's 64-byte limit: {len(v.encode('utf-8'))} bytes"
        )
    return v

to_telegram_button

to_telegram_button() -> InlineKeyboardButton

Convert to telegram.InlineKeyboardButton.

Returns:

Type Description
InlineKeyboardButton

InlineKeyboardButton instance ready for use

Source code in src/telegram_menu_builder/types.py
def to_telegram_button(self) -> InlineKeyboardButton:
    """Convert to telegram.InlineKeyboardButton.

    Returns:
        InlineKeyboardButton instance ready for use
    """
    if self.url:
        return InlineKeyboardButton(text=self.text, url=self.url)
    return InlineKeyboardButton(text=self.text, callback_data=self.callback_data or "")

LayoutConfig

Bases: BaseModel

Configuration for menu layout and button arrangement.

Attributes:

Name Type Description
columns int

Number of columns in the grid layout

max_rows int | None

Maximum number of rows (None for unlimited)

fill_last_row bool

Whether to fill the last row or center items

button_width_balance bool

Try to balance button widths in same row

Example

layout = LayoutConfig(columns=3, max_rows=5)

Source code in src/telegram_menu_builder/types.py
class LayoutConfig(BaseModel):
    """Configuration for menu layout and button arrangement.

    Attributes:
        columns: Number of columns in the grid layout
        max_rows: Maximum number of rows (None for unlimited)
        fill_last_row: Whether to fill the last row or center items
        button_width_balance: Try to balance button widths in same row

    Example:
        >>> layout = LayoutConfig(columns=3, max_rows=5)
    """

    model_config = ConfigDict(frozen=False)

    columns: int = Field(default=3, ge=1, le=8, description="Number of buttons per row")
    max_rows: int | None = Field(
        default=None, ge=1, description="Maximum number of rows (None = unlimited)"
    )
    fill_last_row: bool = Field(default=True, description="Fill last row or center remaining items")
    button_width_balance: bool = Field(
        default=False, description="Try to balance button text widths"
    )

NavigationButton

Bases: BaseModel

Configuration for a navigation button (back, next, exit).

Attributes:

Name Type Description
text str

Button text (emoji + label)

handler str

Handler to call when clicked

params dict[str, Any]

Additional parameters for the handler

position Literal['top', 'bottom', 'inline']

Where to place the button

Source code in src/telegram_menu_builder/types.py
class NavigationButton(BaseModel):
    """Configuration for a navigation button (back, next, exit).

    Attributes:
        text: Button text (emoji + label)
        handler: Handler to call when clicked
        params: Additional parameters for the handler
        position: Where to place the button
    """

    model_config = ConfigDict(frozen=False)

    text: str = Field(..., min_length=1, max_length=50, description="Button text")
    handler: str = Field(..., min_length=1, description="Handler function name")
    params: dict[str, Any] = Field(default_factory=dict, description="Handler parameters")
    position: Literal["top", "bottom", "inline"] = Field(
        default="bottom", description="Button position in menu"
    )

NavigationConfig

Bases: BaseModel

Configuration for navigation buttons (back, next, exit).

Attributes:

Name Type Description
back_button NavigationButton | None

Back button configuration

next_button NavigationButton | None

Next button configuration

exit_button NavigationButton | None

Exit button configuration

cancel_button NavigationButton | None

Cancel button configuration

Example

nav = NavigationConfig( ... back_button=NavigationButton( ... text="🔙 Back", ... handler="go_back" ... ) ... )

Source code in src/telegram_menu_builder/types.py
class NavigationConfig(BaseModel):
    """Configuration for navigation buttons (back, next, exit).

    Attributes:
        back_button: Back button configuration
        next_button: Next button configuration
        exit_button: Exit button configuration
        cancel_button: Cancel button configuration

    Example:
        >>> nav = NavigationConfig(
        ...     back_button=NavigationButton(
        ...         text="🔙 Back",
        ...         handler="go_back"
        ...     )
        ... )
    """

    model_config = ConfigDict(frozen=False)

    back_button: NavigationButton | None = Field(
        default=None, description="Back button configuration"
    )
    next_button: NavigationButton | None = Field(
        default=None, description="Next button configuration"
    )
    exit_button: NavigationButton | None = Field(
        default=None, description="Exit/Close button configuration"
    )
    cancel_button: NavigationButton | None = Field(
        default=None, description="Cancel button configuration"
    )

    @model_validator(mode="after")
    def validate_exclusive_buttons(self) -> "NavigationConfig":
        """Ensure exit and cancel buttons are not used together."""
        if self.exit_button is not None and self.cancel_button is not None:
            raise ValueError("Cannot have both exit_button and cancel_button")
        return self

validate_exclusive_buttons

validate_exclusive_buttons() -> NavigationConfig

Ensure exit and cancel buttons are not used together.

Source code in src/telegram_menu_builder/types.py
@model_validator(mode="after")
def validate_exclusive_buttons(self) -> "NavigationConfig":
    """Ensure exit and cancel buttons are not used together."""
    if self.exit_button is not None and self.cancel_button is not None:
        raise ValueError("Cannot have both exit_button and cancel_button")
    return self

CallbackData

Bases: BaseModel

Complete callback data structure for menu interactions.

This is the internal representation of all data associated with a callback. It gets encoded into the callback_data string or stored externally.

Attributes:

Name Type Description
action MenuAction

The menu action to execute

menu_id str | None

Unique identifier for the menu instance

timestamp float | None

When the callback data was created

metadata dict[str, Any]

Additional metadata (breadcrumb, context, etc.)

Example

callback = CallbackData( ... action=MenuAction(handler="test", params={"id": 1}), ... menu_id="main_menu" ... )

Source code in src/telegram_menu_builder/types.py
class CallbackData(BaseModel):
    """Complete callback data structure for menu interactions.

    This is the internal representation of all data associated with a callback.
    It gets encoded into the callback_data string or stored externally.

    Attributes:
        action: The menu action to execute
        menu_id: Unique identifier for the menu instance
        timestamp: When the callback data was created
        metadata: Additional metadata (breadcrumb, context, etc.)

    Example:
        >>> callback = CallbackData(
        ...     action=MenuAction(handler="test", params={"id": 1}),
        ...     menu_id="main_menu"
        ... )
    """

    model_config = ConfigDict(frozen=False)

    action: MenuAction = Field(..., description="Action to execute")
    menu_id: str | None = Field(default=None, max_length=50, description="Menu identifier")
    timestamp: float | None = Field(default=None, description="Creation timestamp")
    metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")

MenuBuilderError

Bases: Exception

Base exception for menu builder errors.

Source code in src/telegram_menu_builder/types.py
class MenuBuilderError(Exception):
    """Base exception for menu builder errors."""

EncodingError

Bases: MenuBuilderError

Raised when callback data encoding fails.

Source code in src/telegram_menu_builder/types.py
class EncodingError(MenuBuilderError):
    """Raised when callback data encoding fails."""

DecodingError

Bases: MenuBuilderError

Raised when callback data decoding fails.

Source code in src/telegram_menu_builder/types.py
class DecodingError(MenuBuilderError):
    """Raised when callback data decoding fails."""

StorageError

Bases: MenuBuilderError

Raised when storage operations fail.

Source code in src/telegram_menu_builder/types.py
class StorageError(MenuBuilderError):
    """Raised when storage operations fail."""

ValidationError

Bases: MenuBuilderError

Raised when menu validation fails.

Source code in src/telegram_menu_builder/types.py
class ValidationError(MenuBuilderError):
    """Raised when menu validation fails."""