Skip to main content

gramix

PyPI Python Downloads License Code style: black Typed GitHub stars GitHub issues

A fast, clean, fully-typed Python framework for building Telegram bots. Supports synchronous and asynchronous execution, finite state machines, middleware, inline keyboards, webhooks, and file handling — with zero boilerplate.

⭐ GitHub Repository


Table of Contents


Installation

pip install gramix

Optional extras for webhook support:

pip install gramix[aiohttp]   # aiohttp webhook backend
pip install gramix[fastapi]   # FastAPI + uvicorn webhook backend

Requirements: Python 3.10+


Quick Start

Create a .env file:

BOT_TOKEN=your_token_here

Create bot.py:

from gramix import Bot, Dispatcher, Router, load_env

load_env()

bot = Bot()
dp  = Dispatcher(bot)
rt  = Router()
dp.include(rt)

@rt.message("/start")
def on_start(msg):
    msg.answer(f"Hello, {msg.from_user.full_name}!")

dp.run()
python bot.py

Core Concepts

Bot handles all direct Telegram API calls. Pass parse_mode=ParseMode.HTML once at initialization and every subsequent answer(), reply(), edit(), send_photo(), etc. will inherit it automatically — no need to repeat it per call.

Dispatcher drives the polling or webhook loop, dispatches incoming updates to routers, and manages middleware and lifecycle hooks.

Router declares handlers for messages, callbacks, inline queries, and FSM states. Multiple routers can be included in one dispatcher.

F is a filter shortcut object for common conditions: F.photo, F.document, F.sticker, F.voice, F.text, F.reply, F.forward, F.private, F.group, F.supergroup, F.channel.

State / Step define FSM flows. Steps are declared as class attributes and traversed with ctx.next(), ctx.prev(), or ctx.finish().


Examples

Commands & Text Filters

from gramix import Bot, Dispatcher, Router, load_env, F

load_env()
bot = Bot()
dp  = Dispatcher(bot)
rt  = Router()
dp.include(rt)

@rt.message("/start")
def on_start(msg):
    msg.answer("Welcome! Send me anything.")

@rt.message("/help")
def on_help(msg):
    msg.answer("Commands: /start, /help, /about")

# Exact text match (case-insensitive by default)
@rt.message("ping")
def on_ping(msg):
    msg.answer("pong")

# Regex filter — matches any 4-digit number
@rt.message(regex=r"^\d{4}$")
def on_four_digits(msg):
    msg.answer(f"You sent a 4-digit number: {msg.text}")

# Catch all text messages
@rt.message(F.text)
def on_text(msg):
    msg.answer(f"You said: {msg.text}")

dp.run()

Reply Keyboards

from gramix import Reply, RemoveKeyboard

@rt.message("/start")
def on_start(msg):
    kb = (
        Reply(resize=True)
        .button("📋 Menu")
        .button("ℹ️ Info")
        .row()
        .button("⚙️ Settings")
    )
    msg.answer("Choose an option:", keyboard=kb)

@rt.message("/remove")
def on_remove(msg):
    msg.answer("Keyboard removed.", keyboard=RemoveKeyboard())

Inline Keyboards & Callbacks

from gramix import Inline

@rt.message("/vote")
def on_vote(msg):
    kb = (
        Inline()
        .button("👍 Like", callback="vote:like")
        .button("👎 Dislike", callback="vote:dislike")
        .row()
        .button("🔗 Source", url="https://github.com/riokzyofficial-debug/gramix")
    )
    msg.answer("Cast your vote:", keyboard=kb)

@rt.callback("vote:like", "vote:dislike")
def on_vote_result(cb):
    label = "👍 Like" if cb.data == "vote:like" else "👎 Dislike"
    cb.answer(f"You chose {label}", show_alert=True)
    cb.message.edit(f"You voted: {label}")

# Handle callbacks by prefix
@rt.callback(prefix="item:")
def on_item(cb):
    item_id = cb.data.split(":")[1]
    cb.answer(f"Selected item #{item_id}")
    cb.message.edit(f"You picked item #{item_id}.")

Finite State Machine (FSM)

from gramix import State, Step, MemoryStorage, Router, RemoveKeyboard

rt = Router(storage=MemoryStorage())

class Registration(State):
    name = Step()
    age  = Step()
    city = Step()

@rt.message("/register")
def on_register(msg):
    ctx = rt.fsm.get(msg.from_user.id)
    ctx.set(Registration.name)
    msg.answer("What is your name?", keyboard=RemoveKeyboard())

@rt.state(Registration.name)
def get_name(msg, ctx):
    ctx.data["name"] = msg.text
    ctx.next()
    msg.answer("How old are you?")

@rt.state(Registration.age)
def get_age(msg, ctx):
    if not msg.text.isdigit():
        msg.answer("Please enter a valid number.")
        return
    ctx.data["age"] = int(msg.text)
    ctx.next()
    msg.answer("Which city are you from?")

@rt.state(Registration.city)
def get_city(msg, ctx):
    ctx.data["city"] = msg.text
    ctx.finish()
    msg.answer(
        f"Done! Name: {ctx.data['name']}, "
        f"Age: {ctx.data['age']}, City: {ctx.data['city']}"
    )

SQLite FSM Storage

Use SQLiteStorage to persist FSM state across bot restarts:

from gramix import SQLiteStorage, Router

rt = Router(storage=SQLiteStorage("state.db"))

# All @rt.state() handlers work identically — storage is transparent.
# SQLiteStorage also supports context manager usage:
with SQLiteStorage("state.db") as storage:
    rt = Router(storage=storage)

Media Handling

from gramix import F

@rt.message(F.photo)
def on_photo(msg):
    largest = msg.photo[-1]
    msg.answer(f"Photo: {largest.width}×{largest.height}px")

@rt.message(F.document)
def on_document(msg):
    msg.answer(f"Document: {msg.document.file_name} ({msg.document.file_size} bytes)")

@rt.message(F.voice)
def on_voice(msg):
    msg.answer(f"Voice message: {msg.voice.duration}s")

Sending Media

@rt.message("/photo")
def on_send_photo(msg):
    msg.reply_photo("https://picsum.photos/800/600", caption="A random photo")

@rt.message("/video")
def on_send_video(msg):
    msg.reply_video("https://example.com/sample.mp4", caption="Sample video")

@rt.message("/document")
def on_send_document(msg):
    msg.reply_document("<file_id>", caption="Here is your file")

File Download

@rt.message(F.document)
def on_document(msg):
    file_bytes = bot.download_file(msg.document.file_id)
    with open(msg.document.file_name, "wb") as f:
        f.write(file_bytes)
    msg.answer(f"Saved: {msg.document.file_name}")

Middleware

import time

@dp.middleware
def timing_middleware(msg, call_next):
    start = time.monotonic()
    call_next()
    elapsed = time.monotonic() - start
    print(f"[{msg.from_user.id}] handled in {elapsed:.3f}s")

@dp.middleware
def auth_middleware(msg, call_next):
    ALLOWED = {123456789, 987654321}
    if msg.from_user.id not in ALLOWED:
        msg.answer("Access denied.")
        return
    call_next()

Inline Queries

from gramix import InlineQueryResultArticle

@rt.inline()
def on_inline(query):
    results = [
        InlineQueryResultArticle(
            id="1",
            title="gramix",
            message_text="gramix — Telegram bot framework: https://pypi.org/project/gramix",
            description="Python Telegram bot framework",
        ),
    ]
    query.answer(results, cache_time=10)

Chat Member Events

@rt.chat_member()
def on_member_update(update):
    if update.joined:
        print(f"{update.user.full_name} joined {update.chat.display_name}")
    elif update.left:
        print(f"{update.user.full_name} left {update.chat.display_name}")

Polls & Quiz

@rt.message("/poll")
def send_poll(msg):
    bot.send_poll(
        chat_id=msg.chat.id,
        question="What is your favourite language?",
        options=["Python", "TypeScript", "Rust", "Go"],
        is_anonymous=True,
    )

@rt.poll_answer()
def on_vote(answer):
    print(f"{answer.user.full_name} chose options {answer.option_ids}")

Location & Venue

@rt.message("/location")
def cmd_location(msg):
    bot.send_location(chat_id=msg.chat.id, latitude=55.7558, longitude=37.6173)

@rt.message(F.location)
def on_location(msg):
    loc = msg.location
    msg.answer(f"Received point: {loc.latitude}, {loc.longitude}")

Payments

from gramix import LabeledPrice

@rt.message("/buy")
def cmd_buy(msg):
    bot.send_invoice(
        chat_id=msg.chat.id,
        title="Premium доступ",
        description="30 дней Premium.",
        payload="premium_30d",
        provider_token="YOUR_PROVIDER_TOKEN",
        currency="RUB",
        prices=[LabeledPrice(label="Premium", amount=29900)],
    )

@rt.pre_checkout_query()
def on_pre_checkout(query):
    bot.answer_pre_checkout_query(query.id, ok=True)

@rt.successful_payment()
def on_payment(msg):
    p = msg.successful_payment
    msg.answer(f"Оплата прошла! {p.amount_decimal} {p.currency}")

Parse Mode & HTML Formatting

from gramix import Bot, ParseMode

bot = Bot(parse_mode=ParseMode.HTML)

@rt.message("/start")
def on_start(msg):
    msg.answer(
        "<b>Bold</b>, <i>italic</i>, <code>inline code</code>\n"
        f"Hello, <b>{msg.from_user.full_name}</b>!"
    )

Telegram Games

from gramix import Inline

@rt.message("/game")
def on_game(msg):
    kb = Inline().button("🎮 Play", callback="game_play")
    bot.send_game(chat_id=msg.chat.id, game_short_name="mygame", keyboard=kb)

@rt.game_callback()
def on_game_play(cb):
    cb.answer(url=f"https://yourdomain.com/game?user={cb.from_user.id}")

@rt.message("/scores")
def on_scores(msg):
    scores = bot.get_game_high_scores(
        user_id=msg.from_user.id,
        chat_id=msg.chat.id,
        message_id=msg.reply_to_message.message_id,
    )
    for entry in scores:
        msg.answer(f"#{entry.position} {entry.user.full_name}: {entry.score}")

Rate Limiting

from gramix import ThrottlingMiddleware

# Silent drop — allow one message per second per user
dp.middleware(ThrottlingMiddleware(rate=1.0))

# With a custom response when throttled
def on_throttle(msg):
    msg.answer("⚠️ Too many requests. Please wait a moment.")

dp.middleware(ThrottlingMiddleware(rate=1.0, on_throttle=on_throttle))

Works in both dp.run() (sync) and dp.run_async() (async) without any changes.


Async Mode

from gramix import Bot, Dispatcher, Router, load_env, F, ParseMode

load_env()
bot = Bot(parse_mode=ParseMode.HTML)
dp  = Dispatcher(bot)
rt  = Router()
dp.include(rt)

@rt.message("/start")
async def on_start(msg):
    msg.answer("Hello from async!")

@dp.on_startup
async def on_startup():
    print("Bot started.")

dp.run_async()

Webhook Mode

# Raw socket backend (no extra dependencies)
dp.run(webhook=True, webhook_url="https://yourdomain.com/", port=8080)

# With secret token validation
dp.run(webhook=True, webhook_url="https://yourdomain.com/", secret_token="mysecret", port=8080)

# aiohttp backend — pip install gramix[aiohttp]
dp.run(webhook=True, webhook_url="https://yourdomain.com/", backend="aiohttp", port=8080)

# FastAPI + uvicorn backend — pip install gramix[fastapi]
dp.run(webhook=True, webhook_url="https://yourdomain.com/", backend="fastapi", port=8080)

Lifecycle Hooks

@dp.on_startup
def on_startup():
    print("Bot is online.")

@dp.on_shutdown
def on_shutdown():
    print("Bot is shutting down.")

@dp.on_startup
async def async_startup():
    await db.connect()

API Reference

Full API reference is available on GitHub.

Bot — key methods

Method Description
send_message(chat_id, text, *, keyboard, parse_mode, disable_preview, auto_split) Send a text message.
send_photo(chat_id, photo, *, caption, keyboard, parse_mode) Send a photo by file_id or URL.
send_video(chat_id, video, *, caption, keyboard, parse_mode) Send a video.
send_game(chat_id, game_short_name, *, keyboard) Send a Telegram game.
set_game_score(user_id, score, *, chat_id, message_id, force, disable_edit_return) Set a user's score in a game.
get_game_high_scores(user_id, *, chat_id, message_id) Get the high score table; returns list[GameHighScore].
edit_message_text(chat_id, message_id, text, *, keyboard, parse_mode) Edit a message.
delete_message(chat_id, message_id) Delete a message.
download_file(file_id) Download file content as bytes.
send_poll(chat_id, question, options, *, poll_type, ...) Send a regular poll or quiz.
send_location(chat_id, latitude, longitude, *, live_period, keyboard) Send a location.
send_invoice(chat_id, title, description, payload, provider_token, currency, prices, ...) Send a payment invoice.
set_webhook(url, *, secret_token) Register a webhook URL.
close() Close the HTTP client.

ThrottlingMiddleware

Parameter Type Default Description
rate float 1.0 Minimum seconds between accepted messages per user.
on_throttle Callable | None None Optional callback (update) -> None (sync or async) invoked on throttle.

ParseMode

Constant Value
ParseMode.HTML "HTML"
ParseMode.MARKDOWN "MarkdownV2"
ParseMode.MARKDOWN_LEGACY "Markdown"

F — Filter Shortcuts

Filter Matches when
F.text Message has text
F.photo Message has a photo
F.document Message has a document
F.video Message has a video
F.audio Message has an audio file
F.voice Message has a voice message
F.sticker Message has a sticker
F.reply Message is a reply
F.forward Message is forwarded
F.private Chat type is private
F.group Chat type is group or supergroup
F.supergroup Chat type is supergroup
F.channel Chat type is channel
F.poll Message contains a forwarded poll
F.location Message contains a location
F.venue Message contains a venue

License

MIT © riokzy

Metadata

Release files for gramix 0.1.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gramix 0.1.9
File Size Uploaded
gramix-0.1.9.tar.gz 41.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gramix 0.1.9
File Interpreter ABI Platform
gramix-0.1.9-py3-none-any.whl Python 3 none any Details

Total release size: 79.7 kB

Release files / gramix-0.1.9.tar.gz

Download URL gramix-0.1.9.tar.gz
Size 41.1 kB
Tags Source
SHA-256 checksum
How to use checksums
5612d6f38811dd1256f7e0c71f8edad7d1512095a890f316c852d0afc31fb727
BLAKE2b-256 checksum
How to use checksums
16941e0582c545ad7b41406129e9ed4683711c231ec0db0e953429db299c979b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.8.0 pkginfo/1.12.1.2 readme-renderer/36.0 requests/2.32.5 requests-toolbelt/1.0.0 urllib3/2.6.3 tqdm/4.67.3 importlib-metadata/8.7.1 keyring/25.7.0 rfc3986/2.0.0 colorama/0.4.6 CPython/3.13.12

Release files / gramix-0.1.9-py3-none-any.whl

Download URL gramix-0.1.9-py3-none-any.whl
Size 38.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0bab3939b405f1f96fad11e422d4643c383839e86798e7f668217c8617a39714
BLAKE2b-256 checksum
How to use checksums
6f35cb2701109ac78fdda01db2a1a92591940a835f78db46cceb46d4e9a75543
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.8.0 pkginfo/1.12.1.2 readme-renderer/36.0 requests/2.32.5 requests-toolbelt/1.0.0 urllib3/2.6.3 tqdm/4.67.3 importlib-metadata/8.7.1 keyring/25.7.0 rfc3986/2.0.0 colorama/0.4.6 CPython/3.13.12

Release history Release notifications | RSS feed

This release

0.1.9 This release

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page