Исходный код raito.plugins.keyboards.static

from collections.abc import Callable, Sequence
from functools import wraps
from typing import Literal, TypeAlias, cast, overload

from aiogram.types import (
    InlineKeyboardButton,
    InlineKeyboardMarkup,
    KeyboardButton,
    ReplyKeyboardMarkup,
)
from aiogram.utils.keyboard import (
    InlineKeyboardBuilder,
    ReplyKeyboardBuilder,
)
from typing_extensions import ParamSpec, TypeVar

__all__ = ("static_keyboard",)


P = ParamSpec("P")
R = TypeVar("R")

ButtonData: TypeAlias = str | tuple[str] | tuple[str, str] | list[str]
LayoutRow: TypeAlias = Sequence[ButtonData]
LayoutReturn: TypeAlias = Sequence[ButtonData | LayoutRow]

InlineSyncFn: TypeAlias = Callable[P, InlineKeyboardMarkup]
ReplySyncFn: TypeAlias = Callable[P, ReplyKeyboardMarkup]

KeyboardMarkupT: TypeAlias = InlineKeyboardMarkup | ReplyKeyboardMarkup
BuilderFn: TypeAlias = Callable[P, KeyboardMarkupT]


@overload
def _get_button(data: ButtonData, *, inline: Literal[True]) -> InlineKeyboardButton: ...
@overload
def _get_button(data: ButtonData, *, inline: Literal[False]) -> KeyboardButton: ...
@overload
def _get_button(data: ButtonData, *, inline: bool) -> InlineKeyboardButton | KeyboardButton: ...
def _get_button(data: ButtonData, *, inline: bool) -> InlineKeyboardButton | KeyboardButton:
    """Convert button data to the appropriate button type.

    :param data: Button representation (e.g. :code:`("Text", "callback_data")`, or :code:`"Text"` for ReplyKeyboardMarkup)
    :param inline: Whether to create InlineKeyboardButton or KeyboardButton
    :return: An instance of aiogram button
    :raises ValueError: if inline button data is invalid
    """
    if not inline:
        return KeyboardButton(text=data if isinstance(data, str) else data[0])

    if isinstance(data, str) or len(data) != 2:
        raise ValueError("InlineKeyboardButton must be tuple of (text, callback_data)")

    name, value = data

    url: str | None = None
    if value.startswith("t.me/"):
        url = "https://" + value
    elif value.startswith(
        (
            "http://",
            "https://",
            "tg://",
        )
    ):
        url = value

    if url:
        return InlineKeyboardButton(text=name, url=url)
    return InlineKeyboardButton(text=name, callback_data=value)


def _is_button(row: LayoutRow) -> bool:
    """Determine whether a given layout row represents a single button.

    A row is considered a single button if:
    - It is a `str` (used as the text for a reply button), or
    - It is a `tuple[str]` or `tuple[str, str]` (used for inline buttons with callback data)

    :param row: A row from the layout, which may be a button or a list of buttons
    :return: True if the row is a single button, False if it is a row of buttons
    """
    if (
        isinstance(row, tuple | list)
        and 0 < len(row) <= 2
        and all(isinstance(item, str) for item in row)
    ):
        return True
    return isinstance(row, str)


def _inject_layout(
    builder: InlineKeyboardBuilder | ReplyKeyboardBuilder,
    layout: LayoutReturn,
    *,
    inline: bool,
) -> None:
    """Add declarative layout to the keyboard builder.

    :param builder: The builder instance
    :param layout: List of button data or rows
    :param inline: Whether inline buttons are expected
    :raises TypeError: if the builder doesn't match the requested keyboard kind
    """
    expected_type = InlineKeyboardBuilder if inline else ReplyKeyboardBuilder
    if not isinstance(builder, expected_type):
        kind = "Inline" if inline else "Reply"
        raise TypeError(f"{kind} buttons are not supported for {type(builder).__name__}")

    for row in layout:
        if _is_button(row):
            button = _get_button(cast(ButtonData, row), inline=inline)
            builder.row(button, width=1)  # type: ignore[arg-type]
        else:
            buttons = [_get_button(data, inline=inline) for data in row]
            builder.row(*buttons, width=min(8, len(row)))  # type: ignore[arg-type]


@overload
def static_keyboard(
    inline: Literal[True] = True, **builder_kwargs: object
) -> Callable[[Callable[P, LayoutReturn]], InlineSyncFn[P]]: ...
@overload
def static_keyboard(
    inline: Literal[False], **builder_kwargs: object
) -> Callable[[Callable[P, LayoutReturn]], ReplySyncFn[P]]: ...
[документация] def static_keyboard( inline: bool = True, **builder_kwargs: object ) -> Callable[[Callable[P, LayoutReturn]], BuilderFn[P]]: """Decorator to build inline or reply keyboards via layout style. Example: .. code-block:: python @keyboard(inline=True) def markup(): return [ ("Top Button", "top_callback_data"), [("Left", "left"), ("Right", "right")], ] :param inline: If True, builds InlineKeyboardMarkup :param builder_kwargs: Extra args passed to `as_markup()` :returns: A wrapped function returning KeyboardMarkup """ if not inline: builder_kwargs.setdefault("resize_keyboard", True) Builder = InlineKeyboardBuilder if inline else ReplyKeyboardBuilder def wrapper(fn: Callable[P, LayoutReturn]) -> BuilderFn[P]: @wraps(fn) def sync_fn(*args: P.args, **kwargs: P.kwargs) -> KeyboardMarkupT: value = fn(*args, **kwargs) if not isinstance(value, list): msg = "Function must return a list" raise TypeError(msg) builder = Builder() _inject_layout(builder, value, inline=inline) return builder.as_markup(**builder_kwargs) return sync_fn return wrapper