Skip to main content

Core library for Telegram bots

Project description

tg-core

Базовое ядро для Telegram-ботов на aiogram 3.x. Устанавливается как pip-пакет, расширяется на уровне шаблона — ядро никогда не трогается.

Содержание


Требования

  • Python 3.11+
  • PostgreSQL 14+ или SQLite 3.x

Установка

Последняя версия:

pip install gambojo-tgcore

Локальная разработка ядра — изменения применяются сразу без переустановки:

pip install -e /path/to/tg_core

Быстрый старт

Минимальный рабочий бот поверх ядра:

my-bot/
├── alembic/
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
├── db/
│   └── models.py
├── handlers/
│   └── start.py
├── alembic.ini
├── config.py
├── main.py
├── requirements.txt
└── .env

requirements.txt:

gambojo-tgcore
aiogram>=3.7.0
psycopg2-binary

alembic.ini:

[alembic]
script_location = alembic

alembic/env.py:

import db.models
from config import settings
from tg_core.alembic_env import run
run(settings.DATABASE_URL)

alembic/script.py.mako:

"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}

def upgrade() -> None:
    ${upgrades if upgrades else "pass"}

def downgrade() -> None:
    ${downgrades if downgrades else "pass"}

config.py:

from tg_core import Settings

class BotSettings(Settings):
    pass

settings = BotSettings()

db/models.py:

from tg_core import Base, UserMixin

class User(UserMixin, Base):
    pass

handlers/start.py:

from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message

router = Router()

@router.message(CommandStart())
async def cmd_start(message: Message, user) -> None:
    await message.answer(f"Привет, {user.full_name}!")

main.py:

import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from tg_core import (
    AntiSpamMiddleware, UserMiddleware,
    create_admin_router, init_db, run_migrations,
)
from config import settings
from db.models import User
from handlers.start import router as start_router

logging.basicConfig(level=logging.INFO)

async def main() -> None:
    bot = Bot(
        token=settings.BOT_TOKEN,
        default=DefaultBotProperties(parse_mode=ParseMode.HTML),
    )
    dp = Dispatcher(storage=MemoryStorage())

    dp.update.middleware(AntiSpamMiddleware(
        limit=settings.RATE_LIMIT,
        window=settings.RATE_LIMIT_WINDOW,
    ))
    dp.update.middleware(UserMiddleware(
        user_model=User,
        admin_ids=settings.ADMIN_IDS,
    ))

    dp.include_router(create_admin_router(user_model=User))
    dp.include_router(start_router)

    run_migrations()
    await init_db(
        database_url=settings.DATABASE_URL,
        create_tables=False,
        user_model=User,
        admin_ids=settings.ADMIN_IDS,
    )

    try:
        await dp.start_polling(
            bot,
            allowed_updates=dp.resolve_used_update_types(),
        )
    finally:
        await bot.session.close()

if __name__ == "__main__":
    asyncio.run(main())

.env:

BOT_TOKEN=123456:ABC...
ADMIN_IDS=123456789
DATABASE_URL=sqlite+aiosqlite:///bot.db
DEBUG=true

Запуск:

pip install -r requirements.txt
python main.py

Архитектура

Принцип односторонней зависимости

ядро → шаблон:     ЗАПРЕЩЕНО   ядро ничего не знает о шаблонах
шаблон → ядро:     РАЗРЕШЕНО   шаблон импортирует из ядра
шаблон → шаблон:   ЗАПРЕЩЕНО   шаблоны независимы друг от друга

Публичный API

from tg_core import (
    # database
    Base, get_session, get_session_factory,
    init_db, ensure_admins, run_migrations,
    # models
    UserMixin, TimestampMixin,
    # config
    Settings,
    # middleware
    AntiSpamMiddleware, UserMiddleware,
    # keyboards
    paginate, confirm_cancel, back_button, main_menu, url_button,
    # admin
    create_admin_router,
)

Разделение ответственности

Фаза         Инструмент              Ответственность
─────────    ──────────────────────  ──────────────────────────────────
deploy       run_migrations()        создание и обновление схемы БД
runtime      init_db()               инициализация соединения с БД
runtime      UserMiddleware          авторизация и инжект пользователя
runtime      AntiSpamMiddleware      защита от спама
runtime      handler'ы шаблона      бизнес-логика

Компоненты

Settings

Базовый класс конфигурации на pydantic-settings. Читает переменные из .env файла и окружения.

Базовые поля:

Поле               Тип        Дефолт                      Описание
BOT_TOKEN          str        —                           Токен бота от @BotFather
ADMIN_IDS          list[int]  []                          Telegram ID администраторов
DATABASE_URL       str        sqlite+aiosqlite:///bot.db  URL подключения к БД
RATE_LIMIT         int        30                          Максимум сообщений за окно
RATE_LIMIT_WINDOW  int        60                          Размер окна в секундах
DEBUG              bool       False                       Режим отладки
TIMEZONE           str        Europe/Moscow               Временная зона
REFERRAL_BONUS     int        10                          Бонус за реферала

Расширение:

from tg_core import Settings

class MySettings(Settings):
    OPENAI_API_KEY: str
    MAX_HISTORY: int = 20
    FEATURE_FLAG: bool = False

settings = MySettings()

Формат ADMIN_IDS в .env — все три варианта валидны:

ADMIN_IDS=123456789
ADMIN_IDS=123456789,987654321
ADMIN_IDS=[123456789, 987654321]

Database

Асинхронный слой работы с БД на SQLAlchemy 2.x.

run_migrations():

Запускает alembic upgrade head при старте бота. Если папка versions/ пустая — автоматически генерирует начальную миграцию. Вызывать до init_db().

# main.py — обязательный порядок
run_migrations()
await init_db(database_url=settings.DATABASE_URL, ...)

init_db(database_url, create_tables, user_model, admin_ids):

# продакшн — схема управляется через alembic
await init_db(
    database_url=settings.DATABASE_URL,
    create_tables=False,
    user_model=User,
    admin_ids=settings.ADMIN_IDS,
)

# разработка — создаёт таблицы автоматически без alembic
await init_db(
    database_url=settings.DATABASE_URL,
    create_tables=True,
)

ensure_admins(user_model, admin_ids):

Назначает is_admin=True пользователям из ADMIN_IDS если они уже есть в БД. Вызывается автоматически внутри init_db() если переданы user_model и admin_ids.

get_session():

# автоматически через middleware (рекомендуется)
async def my_handler(message: Message, session: AsyncSession) -> None:
    result = await session.execute(select(User))

# вручную — только если нет доступа к middleware
from tg_core import get_session

async def some_service() -> None:
    async for session in get_session():
        result = await session.execute(select(User))

Поддерживаемые СУБД:

# SQLite — разработка
DATABASE_URL=sqlite+aiosqlite:///bot.db

# PostgreSQL — продакшн
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/dbname

Alembic

Для работы run_migrations() шаблон должен содержать:

alembic/
├── env.py          — 3 строки, вызывает tg_core.alembic_env.run()
├── script.py.mako  — шаблон файлов миграций
└── versions/       — файлы миграций (может быть пустой при первом запуске)
alembic.ini         — минимальный, только script_location

alembic/env.py:

import db.models                        # регистрирует модели в Base.metadata
from config import settings
from tg_core.alembic_env import run
run(settings.DATABASE_URL)

alembic.ini:

[alembic]
script_location = alembic

Ручное управление миграциями:

alembic revision --autogenerate -m "add payments table"
alembic upgrade head
alembic downgrade -1

Models

UserMixin — базовые поля пользователя:

Поле         Тип           Описание
id           int           Первичный ключ
telegram_id  int           Уникальный Telegram ID
username     str | None    @username
full_name    str           Имя и фамилия
is_banned    bool          Заблокирован
is_admin     bool          Администратор
last_active  datetime      Последняя активность
referred_by  int | None    Telegram ID реферера
created_at   datetime      Дата регистрации
updated_at   datetime      Дата обновления

TimestampMixin — добавляет created_at и updated_at к любой модели.

Создание модели User:

from tg_core import Base, UserMixin

# минимально
class User(UserMixin, Base):
    pass

# с расширением
class User(UserMixin, Base):
    phone: Mapped[str | None]
    balance: Mapped[int] = mapped_column(default=0)

Важно — никогда не создавай второй Base:

# правильно
from tg_core import Base
class Product(Base): ...

# неправильно — сломает alembic и create_all
from sqlalchemy.orm import DeclarativeBase
class MyBase(DeclarativeBase): pass

Middleware

AntiSpamMiddleware(limit, window):

dp.update.middleware(AntiSpamMiddleware(limit=30, window=60))

UserMiddleware(user_model, admin_ids):

dp.update.middleware(UserMiddleware(
    user_model=User,
    admin_ids=settings.ADMIN_IDS,
))

Поведение:

  • Создаёт пользователя при первом обращении

  • При создании назначает is_admin=True если telegram_id есть в admin_ids

  • Инжектит data["user"], data["session"], data["is_new_user"] в каждый handler

  • Обрабатывает race condition при одновременных запросах через savepoint

    handler получает пользователя автоматически

    async def my_handler( message: Message, user: User, session: AsyncSession, is_new_user: bool, ) -> None: if user.is_banned: await message.answer("Вы заблокированы.")

Реферальная система — установи referred_by до вызова middleware:

# handlers/start.py
@router.message(CommandStart())
async def cmd_start(message, command, user, session, is_new_user):
    if is_new_user and command.args and command.args.isdigit():
        referrer_id = int(command.args)
        # логика начисления бонуса рефереру

Порядок регистрации важен:

# правильно — AntiSpam первым
dp.update.middleware(AntiSpamMiddleware(...))
dp.update.middleware(UserMiddleware(...))

# неправильно — запрос в БД для каждого спамера
dp.update.middleware(UserMiddleware(...))
dp.update.middleware(AntiSpamMiddleware(...))

Keyboards

paginate(items, page, callback_prefix, page_size=8):

products = [("Товар 1", "product:1"), ("Товар 2", "product:2")]
kb = paginate(products, page=0, callback_prefix="catalog")
await message.answer("Каталог:", reply_markup=kb)

confirm_cancel(...):

kb = confirm_cancel(
    confirm_text="✅ Оформить заказ",
    cancel_text="❌ Отмена",
    confirm_callback="checkout:confirm",
    cancel_callback="checkout:cancel",
)

back_button(callback, text="← Назад"):

kb = back_button(callback="catalog:page:0")

main_menu(buttons, resize=True, one_time=False):

kb = main_menu(["🛒 Каталог", "🛍 Корзина", "📦 Заказы"])

url_button(text, url):

kb = url_button("🌐 Открыть сайт", "https://example.com")

Admin

create_admin_router(user_model, extra_routers, stats_callback):

Базовые команды:

Команда         Описание
/admin          Панель администратора
/stats          Статистика + вызывает stats_callback
/broadcast      Рассылка всем пользователям
/ban <id>       Заблокировать пользователя
/unban <id>     Разблокировать пользователя
/user <id>      Информация о пользователе

Минимальное подключение:

dp.include_router(create_admin_router(user_model=User))

Расширение статистики:

async def get_shop_stats() -> str:
    return f"📦 Заказов сегодня: <b>{await count_orders_today()}</b>"

dp.include_router(create_admin_router(
    user_model=User,
    stats_callback=get_shop_stats,
))

Добавление своих команд:

# my-bot/admin.py
router = Router()

@router.message(Command("orders"))
async def cmd_orders(message: Message, user, session) -> None:
    if not user.is_admin:
        return
    ...

# main.py
dp.include_router(create_admin_router(
    user_model=User,
    extra_routers=[router],
))

Создание шаблона

Структура проекта:

my-template/
├── alembic/
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
├── db/
│   ├── __init__.py
│   └── models.py
├── handlers/
│   ├── __init__.py
│   └── start.py
├── admin.py
├── config.py
├── main.py
├── alembic.ini
├── requirements.txt
├── .env.example
└── .gitignore

Зависимости в requirements.txt:

gambojo-tgcore
psycopg2-binary

Правила расширения

Можно:

# наследовать Settings
class MySettings(Settings): ...

# наследовать UserMixin
class User(UserMixin, Base): ...

# наследовать TimestampMixin
class MyModel(TimestampMixin, Base): ...

# передавать параметры в фабрики
create_admin_router(user_model=User, extra_routers=[my_router])

# создавать свои клавиатуры используя функции ядра
from tg_core import back_button

Нельзя:

# импортировать из внутренних модулей
from tg_core._session import ...

# создавать второй Base
from sqlalchemy.orm import DeclarativeBase
class MyBase(DeclarativeBase): pass

# вызывать init_db() больше одного раза

# менять порядок middleware (AntiSpam должен быть первым)

# вызывать init_db() до run_migrations()

Версионирование

Проект следует Semantic Versioning (https://semver.org):

MAJOR — breaking changes в публичном API
MINOR — новые возможности, обратно совместимые
PATCH — исправления багов

Рабочий процесс обновления ядра:

# 1. внёс изменения в код
# 2. описал в CHANGELOG.md под [Unreleased]
# 3. обновил версию
bump-my-version bump minor

# 4. пуш — PyPI обновляется автоматически через CI
git push origin main --tags

# 5. шаблоны обновляют зависимость
pip install --upgrade gambojo-tgcore

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gambojo_tgcore-1.0.0.tar.gz (16.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gambojo_tgcore-1.0.0-py3-none-any.whl (7.1 kB view details)

Uploaded Python 3

File details

Details for the file gambojo_tgcore-1.0.0.tar.gz.

File metadata

  • Download URL: gambojo_tgcore-1.0.0.tar.gz
  • Upload date:
  • Size: 16.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for gambojo_tgcore-1.0.0.tar.gz
Algorithm Hash digest
SHA256 f629f92997f15cd3ffe064772eaaa568bf25b607d14e68109d4205a6c4f0c451
MD5 7158a800e0bd1e454e3805dd940ac1df
BLAKE2b-256 43a9fd2f0f212f9e2c203443d60f4f7e8f23b043a947037576e4da08f3f70aab

See more details on using hashes here.

File details

Details for the file gambojo_tgcore-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: gambojo_tgcore-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 7.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for gambojo_tgcore-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5e8e0becb491692f4dc2726c20362d33afe589bd6560f438d42b8fd9284dc379
MD5 4211a7ece9951e83587dcccfaef0fec4
BLAKE2b-256 f5a1f667e9a94a8c9555583b771a9d230bafcab902206fdf48d59b2488b24973

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page