Back to all work
AiogramX background
2025Released on PyPI2 min read

AiogramX

A toolkit of paginators, calendars, checkboxes, and time pickers with FlipCache-backed storage so Telegram bots only register one handler per widget type.

WidgetBase stores instances in an LRU dict, collapsing thousands of callbacks into a single handler.

Project Metrics

Widgets shipped6 reusable UIs
Callback handlers1 per widget type
PythonAiogram 3FlipCacheTelegram API

AiogramX

Why I Built It

  • Production bots I maintain (especially Tahrirchi) kept re‑implementing the same inline UIs—paginators, calendars, time pickers—with slightly different edge cases every time.
  • Most third-party widget libraries register one callback handler per instance; after a busy day the router list explodes and callbacks slow down.
  • AiogramX packages the components I kept rewriting into a reusable toolkit while leaning on flipcache’s LRU containers so widgets stay fast no matter how many users are hammering them.

Event Flow

AiogramX widget lifecycle

  1. Bot routers register widgets once via WidgetBase.register(dp); each widget type wires a single callback entry point.
  2. When a user interacts with an inline button, Aiogram parses callback data into a _cb model that always contains a short-lived key.
  3. The WidgetBase lookup pulls the instance from an LRUDict(max_items=1000) so stale widgets fall out automatically.
  4. The widget-specific process_cb implementation updates state, re-renders the keyboard, and emits domain-friendly responses (e.g. date, time, CheckboxResult).

Widget Suite

ComponentWhat it solvesHighlights
PaginatorBrowsing large datasets lazilySupports eager lists, async loaders, row/page sizing and per-page callbacks
CalendarDate selection with guard railsLocalised quick-jump buttons, max-range enforcement, expiring widgets
CheckboxCollecting multi-select inputSupplies structured dict payloads and back button hooks
TimeSelectorGrid / TimeSelectorModernTime picking UXEnforces allowed windows, 5 min offsets, custom labels, carry-over logic
ReplyKeyboardMetaStatic reply menusDefine layouts via class attributes, produces ready-to-use ReplyKeyboardMarkup

Implementation Highlights

  • WidgetMeta ensures every widget exposes a _cb CallbackData class with a key field; missing pieces raise at import time instead of failing in production.
  • WidgetBase seeds a class-level LRUDict (from FlipCache) so only the most recent ~1000 widget instances stay resident, keeping callback lookups O(1).
  • watchdog style “expired” responses are localised (en, ru, uz) via get_expired_text, and stale keyboards auto-clear to avoid dangling inline buttons.
  • Utilities such as gen_key guarantee key uniqueness across thousands of concurrent widget instances without hitting Redis or Postgres.
  • The project is packaged as aiogramx==3.1.3 (Python ≥3.9), ships typed hints, and re-exports the widget classes from the package root for frictionless imports.

Release Notes Snapshots

  • Jun 2025 · v3.1.3 — Added ReplyKeyboardMeta, carry-over logic for time selectors, and automated PyPI publishing via CI.
  • May 2025 · v3.1.2 — Fixed stray awaits, relaxed type hints for localisation, and introduced a live demo bot.
  • May 2025 · v3.1.0 — Landed Calendar + Checkbox suites, refactored the widget core, and renamed the time selector API.

Example

Python
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import CallbackQuery, Message
from aiogramx import Paginator

router = Router()
Paginator.register(router)

async def fetch_rows(page: int, per_page: int):
    rows = await repo.fetch_batch(page=page, size=per_page)
    return [row.as_button() for row in rows]

@router.message(Command("browse"))
async def browse_handler(message: Message):
    paginator = Paginator(
        per_page=12,
        per_row=3,
        lazy_data=fetch_rows,
        on_select=lambda cq, payload: cq.answer(f"Picked {payload}"),
    )
    await message.answer("Choose an item", reply_markup=await paginator.render_kb())

@router.callback_query(F.data.startswith("item:"))
async def legacy_handler(callback: CallbackQuery):
    await callback.answer("Legacy callbacks still coexist with AiogramX")

What’s Next

  • Expand localisation bundles for widgets (currently en, ru, uz) and auto-detect right-to-left layouts.
  • Introduce a form builder that chains multiple widgets into a single conversation with built-in validation.
  • Publish typed stub files so editors can auto-complete callback payload structures out-of-the-box.

Explore another build

More systems I am growing in the open.