18 β Yakuniy kapston: to'liq bot¶
β¬ οΈ Oldingi: 17 β Production va deploy Β· π README Β· Keyingi: 19 β Guruhlarda ishlash β‘οΈ
Bu bobda: kitob davomida o'rgangan hamma narsani bitta to'liq botda birlashtiramiz β Vazifa-menejeri bot (TaskBot). U foydalanuvchidan vazifa qabul qiladi (FSM forma), uni ma'lumotlar bazasiga saqlaydi, inline tugmalar orqali "bajarildi" / "o'chir" amallarini bajaradi, reply-klaviatura menyusi bilan ishlaydi,
DbMiddlewareorqali DB ni har handlerga uzatadi (DI),ThrottleMiddlewarebilan spamni cheklaydi, faqat admin ko'radigan/statsva/broadcastbuyruqlariga ega, va APScheduler bilan vaqti yetgan vazifalar uchun rejali eslatma yuboradi. Oxirida botni VPS servergasystemdbilan 24/7 ishlash uchun joylashtirish yo'riqnomasini ko'ramiz. Bu bob0 -> ekspertyo'lini yakunlaydi: shu paytgacha bo'lak-bo'lak o'rgangan handlerlar, filtrlar (4-bob), formatlash (5-bob), klaviaturalar (6-bob), callback (7-bob), FSM (8-bob), middleware (9-bob), DB (10-bob) va loyiha tuzilishi (11-bob) endi yagona ishlaydigan mahsulotga aylanadi.Halol eslatma: bu bobdagi biznes-logika (
parse_due/fmt_due), ma'lumotlar bazasi (aiosqlite),CallbackDatapack/unpack, handler yo'naltirish (FSM forma,/start, callbacklar),DbMiddleware/ThrottleMiddleware/IsAdminmiddlewarelari, klaviatura quruvchilar va APScheduler eslatma sikli token va internetsiz, offline tekshirilgan (mockfeed_updateva mock session bilan β Telegramga hech narsa yuborilmagan). Faqatdp.start_polling(bot),bot.set_my_commands(...), realbot.send_message(...)va serverga deploy bo'limlari jonli β ular BotFather token va internet (deploy uchun VPS) talab qiladi. Bularni matnda "illustrativ" deb belgilaymiz; soxta "bot ishladi" deb yozmaymiz.
Nima quramiz: TaskBot¶
TaskBot β shaxsiy vazifalar ro'yxatini boshqaradigan oddiy, lekin to'liq bot. Foydalanuvchi tajribasi shunday ko'rinadi (illustrativ β jonli botda token+internet kerak):
Foydalanuvchi: /start
Bot: Xush kelibsiz, Oqil! Men vazifa-menejer botman. [Yangi vazifa] [Vazifalarim]
Foydalanuvchi: [Yangi vazifa]
Bot: Vazifa nomini yozing (/cancel β bekor):
Foydalanuvchi: Hisobot tayyorlash
Bot: Muddatni kiriting: '30m', '2h', '1d', '2026-06-20 14:30' yoki 'yoq'
Foydalanuvchi: 2h
Bot: Saqlandi! #1: Hisobot tayyorlash | Muddat: 2026-06-13 12:00
... 2 soatdan keyin ...
Bot: Eslatma: "Hisobot tayyorlash" vazifasi muddati yetdi!
Bot ichida quyidagi qatlamlar bor β biz ularni alohida-alohida qurib, keyin birlashtiramiz:
Tuzilish 11-bobdagi modullashtirilgan naqshga amal qiladi:
taskbot/
βββ .env # BOT_TOKEN, ADMIN_IDS (git ga KIRMAYDI)
βββ .env.example
βββ requirements.txt
βββ config.py # Settings
βββ db.py # Database (aiosqlite repository)
βββ utils.py # parse_due / fmt_due (sof logika)
βββ bot_app.py # routerlar, FSM, klaviatura, middleware
βββ scheduler.py # APScheduler eslatma sikli
βββ main.py # kirish nuqtasi: bot + dp + scheduler
Agar paket/modul tuzilishi xira bo'lsa, Python qo'llanmasi ga, SQL asoslari uchun SQL qo'llanmasi ga qarang. Bir xil botni Node.js'da qanday qurish mumkinligini Node.js qo'llanmasi da ko'rishingiz mumkin.
1. Konfiguratsiya: config.py va .env¶
Token va admin ID lari hech qachon kodga yozilmaydi. Ularni .env da saqlaymiz va pydantic-settings bilan o'qiymiz (11-bobdagidek).
# config.py
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
bot_token: str
admin_ids: str = "" # ".env" da: ADMIN_IDS=9001,9002
@property
def admin_id_set(self) -> set[int]:
return {int(x) for x in self.admin_ids.split(",") if x.strip()}
settings = Settings() # type: ignore[call-arg]
Tokenni @BotFather da /newbot buyrug'i bilan olasiz β bu jonli qadam, internet talab qiladi.
2. Ma'lumotlar bazasi qatlami: db.py¶
aiosqlite ustida yupqa repository quramiz (10-bob). Ikki jadval: users (foydalanuvchilar) va tasks (vazifalar). Diqqat: har bir DB metodi user_id ni tekshiradi β bir foydalanuvchi boshqasining vazifasini o'chira olmaydi (egalik tekshiruvi).
# db.py
from __future__ import annotations
import datetime as dt
from dataclasses import dataclass
import aiosqlite
CREATE_SQL = """
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
username TEXT,
created_at TEXT NOT NULL,
is_blocked INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
due_at TEXT,
is_done INTEGER NOT NULL DEFAULT 0,
remind_sent INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
"""
@dataclass
class Task:
id: int
user_id: int
title: str
due_at: str | None
is_done: bool
class Database:
def __init__(self, path: str):
self._path = path
async def connect(self) -> None:
self._conn = await aiosqlite.connect(self._path)
self._conn.row_factory = aiosqlite.Row
await self._conn.executescript(CREATE_SQL)
await self._conn.commit()
async def close(self) -> None:
await self._conn.close()
# --- foydalanuvchilar ---
async def upsert_user(self, user_id: int, full_name: str, username: str | None) -> bool:
"""Foydalanuvchini saqlaydi. Yangi bo'lsa True qaytaradi."""
cur = await self._conn.execute("SELECT 1 FROM users WHERE user_id = ?", (user_id,))
if await cur.fetchone():
await self._conn.execute(
"UPDATE users SET full_name = ?, username = ? WHERE user_id = ?",
(full_name, username, user_id),
)
await self._conn.commit()
return False
await self._conn.execute(
"INSERT INTO users (user_id, full_name, username, created_at) VALUES (?, ?, ?, ?)",
(user_id, full_name, username, dt.datetime.now().isoformat()),
)
await self._conn.commit()
return True
async def count_users(self) -> int:
cur = await self._conn.execute("SELECT COUNT(*) AS c FROM users")
return (await cur.fetchone())["c"]
async def all_user_ids(self) -> list[int]:
cur = await self._conn.execute("SELECT user_id FROM users WHERE is_blocked = 0")
return [r["user_id"] async for r in cur]
# --- vazifalar ---
async def add_task(self, user_id: int, title: str, due_at: str | None) -> int:
cur = await self._conn.execute(
"INSERT INTO tasks (user_id, title, due_at, created_at) VALUES (?, ?, ?, ?)",
(user_id, title, due_at, dt.datetime.now().isoformat()),
)
await self._conn.commit()
return cur.lastrowid
async def list_tasks(self, user_id: int, only_open: bool = True) -> list[Task]:
sql = "SELECT * FROM tasks WHERE user_id = ?"
if only_open:
sql += " AND is_done = 0"
sql += " ORDER BY id"
cur = await self._conn.execute(sql, (user_id,))
rows = await cur.fetchall()
return [Task(r["id"], r["user_id"], r["title"], r["due_at"], bool(r["is_done"])) for r in rows]
async def get_task(self, task_id: int, user_id: int) -> Task | None:
cur = await self._conn.execute(
"SELECT * FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id)
)
r = await cur.fetchone()
return Task(r["id"], r["user_id"], r["title"], r["due_at"], bool(r["is_done"])) if r else None
async def mark_done(self, task_id: int, user_id: int) -> bool:
cur = await self._conn.execute(
"UPDATE tasks SET is_done = 1 WHERE id = ? AND user_id = ? AND is_done = 0",
(task_id, user_id),
)
await self._conn.commit()
return cur.rowcount > 0
async def delete_task(self, task_id: int, user_id: int) -> bool:
cur = await self._conn.execute(
"DELETE FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id)
)
await self._conn.commit()
return cur.rowcount > 0
async def count_tasks(self) -> int:
cur = await self._conn.execute("SELECT COUNT(*) AS c FROM tasks")
return (await cur.fetchone())["c"]
async def due_unsent(self, now_iso: str) -> list[tuple[int, int, str]]:
"""Vaqti yetgan va hali eslatma yuborilmagan vazifalar."""
cur = await self._conn.execute(
"SELECT id, user_id, title FROM tasks "
"WHERE is_done = 0 AND remind_sent = 0 AND due_at IS NOT NULL AND due_at <= ?",
(now_iso,),
)
return [(r["id"], r["user_id"], r["title"]) async for r in cur]
async def mark_remind_sent(self, task_id: int) -> None:
await self._conn.execute("UPDATE tasks SET remind_sent = 1 WHERE id = ?", (task_id,))
await self._conn.commit()
Nega
mark_donevadelete_taskboolqaytaradi?cur.rowcountβ yangilangan/o'chirilgan qatorlar soni. Agaruser_idmos kelmasa, 0 qator o'zgaradi va bizFalseqaytaramiz β bu xavfsizlik: foydalanuvchi faqat o'z vazifasini boshqaradi. Biz buni offline test qildik:await db.delete_task(tid2, 7777) is False.
3. Sof biznes-logika: utils.py¶
Muddat parslash β Telegramdan mutlaqo mustaqil kod. Shuning uchun uni eng oson va eng to'liq test qilamiz (token kerak emas, oddiy funksiya chaqiruvi).
# utils.py
from __future__ import annotations
import datetime as dt
def parse_due(text: str, now: dt.datetime | None = None) -> dt.datetime | None:
"""'30m'/'2h'/'1d' (nisbiy), '2026-06-20 14:30' (aniq), 'yoq' (None). Xato -> ValueError."""
now = now or dt.datetime.now()
text = text.strip().lower()
if text in ("yoq", "yo'q", "-", "skip"):
return None
if len(text) >= 2 and text[-1] in "mhd" and text[:-1].isdigit():
n = int(text[:-1])
if text[-1] == "m":
return now + dt.timedelta(minutes=n)
if text[-1] == "h":
return now + dt.timedelta(hours=n)
return now + dt.timedelta(days=n)
for fmt in ("%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
return dt.datetime.strptime(text, fmt)
except ValueError:
continue
raise ValueError("Muddat formati noto'g'ri")
def fmt_due(due_iso: str | None) -> str:
if not due_iso:
return "muddatsiz"
try:
d = dt.datetime.fromisoformat(due_iso)
except ValueError:
return due_iso
return d.strftime("%Y-%m-%d %H:%M")
Bu funksiyani biz quyidagicha offline tasdiqladik (token yo'q, internet yo'q):
now = dt.datetime(2026, 6, 13, 10, 0)
assert parse_due("30m", now) == now + dt.timedelta(minutes=30)
assert parse_due("yoq", now) is None
assert parse_due("2026-06-20 14:30", now) == dt.datetime(2026, 6, 20, 14, 30)
# "xato-format" -> ValueError
4. FSM holatlari va CallbackData¶
Vazifa qo'shish β ikki bosqichli forma (8-bob). Avval nom, keyin muddat:
# bot_app.py (boshi)
from aiogram.fsm.state import State, StatesGroup
from aiogram.filters.callback_data import CallbackData
class AddTask(StatesGroup):
waiting_title = State()
waiting_due = State()
class TaskCB(CallbackData, prefix="task"):
action: str # "done" | "del"
task_id: int
Forma quyidagi holat diagrammasiga ko'ra ishlaydi:
TaskCB β tipli callback ma'lumot (7-bob). .pack() uni stringga, .filter(...) esa handlerga yo'naltirish uchun ishlatiladi. Buni offline tasdiqladik:
assert TaskCB(action="done", task_id=42).pack() == "task:done:42"
u = TaskCB.unpack("task:done:42")
assert u.action == "done" and u.task_id == 42
3.x vs 2.x. Eski 2.x da callback ma'lumot odatda qo'lda string ajratilardi (
callback_data.split(":")). 3.x daCallbackDatafactory buni tipli va xavfsiz qiladi β bu zamonaviy idiomadir. Quyidagicha YOZMANG:
5. Klaviaturalar¶
Reply-menyu (asosiy tugmalar) va inline-klaviatura (har vazifa uchun amallar). Ikkalasi ham Builder orqali quriladi (6-bob):
# bot_app.py
from aiogram.types import InlineKeyboardMarkup, ReplyKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
def main_menu() -> ReplyKeyboardMarkup:
b = ReplyKeyboardBuilder()
b.button(text="Yangi vazifa")
b.button(text="Vazifalarim")
b.adjust(2)
return b.as_markup(resize_keyboard=True)
def tasks_keyboard(tasks) -> InlineKeyboardMarkup:
b = InlineKeyboardBuilder()
for t in tasks:
b.button(text=f"Bajardim: {t.title[:20]}", callback_data=TaskCB(action="done", task_id=t.id))
b.button(text="O'chir", callback_data=TaskCB(action="del", task_id=t.id))
b.adjust(2) # har qatorda 2 tugma
return b.as_markup()
Offline tasdiq: main_menu() -> ReplyKeyboardMarkup, tasks_keyboard([...]) -> InlineKeyboardMarkup, va birinchi tugmaning callback_data aynan "task:done:1" bo'ldi.
3.x vs 2.x. Eski 2.x da
ReplyKeyboardMarkup(row_width=2).add(KeyboardButton("..."))uslubi ishlatilardi. 3.x daReplyKeyboardBuilder/InlineKeyboardBuilder+.button(...)+.adjust(...)+.as_markup(...)ishlatamiz. Eski uslubni YOZMANG.
6. Middleware: DB injection, throttle va admin filtri¶
Uchta middleware (9-bob). DbMiddleware β har handlerga db ni avtomatik uzatadi (DI), ThrottleMiddleware β spamni cheklaydi, IsAdmin β faqat admin_router uchun.
# bot_app.py
import datetime as dt
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
class DbMiddleware(BaseMiddleware):
def __init__(self, db: "Database"):
self.db = db
async def __call__(self, handler, event: TelegramObject, data: dict[str, Any]) -> Any:
data["db"] = self.db # endi handler `db: Database` argumentini oladi
return await handler(event, data)
class ThrottleMiddleware(BaseMiddleware):
def __init__(self, rate: float = 0.5):
self.rate = rate
self._last: dict[int, float] = {}
async def __call__(self, handler, event: TelegramObject, data: dict[str, Any]) -> Any:
user = data.get("event_from_user")
if user is not None:
now = dt.datetime.now().timestamp()
if now - self._last.get(user.id, 0.0) < self.rate:
return None # bloklandi β handler chaqirilmaydi
self._last[user.id] = now
return await handler(event, data)
class IsAdmin(BaseMiddleware):
def __init__(self, admin_ids: set[int]):
self.admin_ids = admin_ids
async def __call__(self, handler, event: TelegramObject, data: dict[str, Any]) -> Any:
user = data.get("event_from_user")
if user is None or user.id not in self.admin_ids:
return None # admin emas β handler chaqirilmaydi
return await handler(event, data)
Middleware zanjiri shunday ishlaydi: update avval middlewarelardan o'tadi, keyin routerga yetadi. Buni offline tasdiqladik β oddiy user /stats ga javob olmadi (bo'sh call ro'yxati), admin esa oldi; va ketma-ket 2 ta xabardan faqat 1tasi handlerga yetdi (throttle).
event_from_userqayerdan keladi? aiogram dispatcher har update uchundatalug'atiga avtomatikevent_from_user(xabar/callback egasiUser),event_chatva boshqalarni qo'shadi. Biz uni hech qayerda qo'lda to'ldirmaymiz.
7. Handlerlar va routerlar¶
Endi hammasini handlerlarda bog'laymiz. Ikki router: user_router (oddiy foydalanuvchilar) va admin_router (faqat admin). db: Database argumenti DbMiddleware tufayli avtomatik keladi.
# bot_app.py
from aiogram import Bot, Router, F
from aiogram.filters import Command, CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
user_router = Router()
admin_router = Router()
@user_router.message(CommandStart())
async def cmd_start(message: Message, db: "Database") -> None:
u = message.from_user
is_new = await db.upsert_user(u.id, u.full_name, u.username)
salom = "Xush kelibsiz" if is_new else "Yana xush kelibsiz"
await message.answer(
f"{salom}, {u.full_name}!\nMen vazifa-menejer botman. Quyidagi menyudan foydalaning.",
reply_markup=main_menu(),
)
@user_router.message(Command("help"))
async def cmd_help(message: Message) -> None:
await message.answer(
"Buyruqlar:\n/start β boshlash\n/help β yordam\n"
"/cancel β formani bekor qilish\n\nTugmalar: 'Yangi vazifa' va 'Vazifalarim'."
)
@user_router.message(Command("cancel"))
@user_router.message(F.text.casefold() == "bekor")
async def cmd_cancel(message: Message, state: FSMContext) -> None:
if await state.get_state() is None:
await message.answer("Hozir bekor qiladigan amal yo'q.")
return
await state.clear()
await message.answer("Bekor qilindi.", reply_markup=main_menu())
Vazifa qo'shish formasi (FSM)¶
@user_router.message(F.text == "Yangi vazifa")
async def add_start(message: Message, state: FSMContext) -> None:
await state.set_state(AddTask.waiting_title)
await message.answer("Vazifa nomini yozing (/cancel β bekor):")
@user_router.message(AddTask.waiting_title, F.text)
async def add_title(message: Message, state: FSMContext) -> None:
title = message.text.strip()
if len(title) < 2:
await message.answer("Juda qisqa. Iltimos to'liqroq yozing.")
return
await state.update_data(title=title)
await state.set_state(AddTask.waiting_due)
await message.answer(
"Muddatni kiriting:\n- '30m', '2h', '1d' (nisbiy)\n"
"- '2026-06-20 14:30' (aniq)\n- 'yoq' (muddatsiz)"
)
@user_router.message(AddTask.waiting_due, F.text)
async def add_due(message: Message, state: FSMContext, db: "Database") -> None:
try:
due = parse_due(message.text)
except ValueError:
await message.answer("Muddat formati noto'g'ri. Yana urinib ko'ring (yoki 'yoq').")
return
data = await state.get_data()
due_iso = due.isoformat() if due else None
task_id = await db.add_task(message.from_user.id, data["title"], due_iso)
await state.clear()
await message.answer(
f"Saqlandi! #{task_id}: {data['title']}\nMuddat: {fmt_due(due_iso)}",
reply_markup=main_menu(),
)
Diqqat: @user_router.message(AddTask.waiting_title, F.text) β bu holat filtri (faqat shu holatda) va matn filtri birga. Foydalanuvchi formada bo'lmasa, bu handler ishlamaydi. Buni offline tasdiqladik: "Yangi vazifa" -> holat AddTask:waiting_title, nom yuborildi -> AddTask:waiting_due, muddat yuborildi -> holat tozalandi va vazifa DB ga tushdi.
Vazifalarni ko'rsatish va callbacklar¶
@user_router.message(F.text == "Vazifalarim")
@user_router.message(Command("list"))
async def show_tasks(message: Message, db: "Database") -> None:
tasks = await db.list_tasks(message.from_user.id)
if not tasks:
await message.answer("Ochiq vazifalar yo'q. 'Yangi vazifa' tugmasini bosing.")
return
lines = [f"#{t.id} {t.title} ({fmt_due(t.due_at)})" for t in tasks]
await message.answer("Vazifalaringiz:\n" + "\n".join(lines), reply_markup=tasks_keyboard(tasks))
@user_router.callback_query(TaskCB.filter(F.action == "done"))
async def cb_done(query: CallbackQuery, callback_data: TaskCB, db: "Database") -> None:
ok = await db.mark_done(callback_data.task_id, query.from_user.id)
await query.answer("Bajarildi!" if ok else "Topilmadi yoki allaqachon bajarilgan.")
if ok:
tasks = await db.list_tasks(query.from_user.id)
if tasks:
await query.message.edit_text(
"Vazifalaringiz:\n" + "\n".join(f"#{t.id} {t.title}" for t in tasks),
reply_markup=tasks_keyboard(tasks),
)
else:
await query.message.edit_text("Barcha vazifalar bajarildi! Tabriklaymiz.")
@user_router.callback_query(TaskCB.filter(F.action == "del"))
async def cb_del(query: CallbackQuery, callback_data: TaskCB, db: "Database") -> None:
ok = await db.delete_task(callback_data.task_id, query.from_user.id)
await query.answer("O'chirildi." if ok else "Topilmadi.")
if ok:
tasks = await db.list_tasks(query.from_user.id)
text = ("Vazifalaringiz:\n" + "\n".join(f"#{t.id} {t.title}" for t in tasks)
if tasks else "Vazifalar yo'q.")
await query.message.edit_text(text, reply_markup=tasks_keyboard(tasks) if tasks else None)
callback_data: TaskCB argumenti β aiogram TaskCB.filter(...) mos kelganda callback stringni avtomatik unpack qilib beradi. query.answer(...) β tugma bosilganda chiqadigan kichik "toast" xabari. Offline tasdiq: done callback AnswerCallbackQuery ni chaqirdi va DB da vazifa is_done=True bo'ldi.
Admin buyruqlari¶
@admin_router.message(Command("stats"))
async def cmd_stats(message: Message, db: "Database") -> None:
users = await db.count_users()
tasks = await db.count_tasks()
await message.answer(f"Statistika:\nFoydalanuvchilar: {users}\nVazifalar: {tasks}")
@admin_router.message(Command("broadcast"))
async def cmd_broadcast(message: Message, db: "Database", bot: Bot) -> None:
text = message.text.removeprefix("/broadcast").strip()
if not text:
await message.answer("Foydalanish: /broadcast <xabar>")
return
ids = await db.all_user_ids()
sent = 0
for uid in ids:
try:
await bot.send_message(uid, f"Xabar:\n{text}") # JONLI: token+internet kerak
sent += 1
except Exception:
continue # bloklagan foydalanuvchini o'tkazib yuboramiz
await message.answer(f"{sent} ta foydalanuvchiga yuborildi.")
/broadcast ichidagi bot.send_message(...) β jonli chaqiruv (illustrativ): real ishlashi uchun token va internet kerak. try/except muhim: ba'zi foydalanuvchilar botni bloklagan bo'lishi mumkin, ularda xato chiqadi, biz uni yutib, davom etamiz.
8. Hammasini yig'ish: build_dispatcher¶
# bot_app.py
from aiogram import Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
def build_dispatcher(db: "Database", admin_ids: set[int], throttle_rate: float = 0.5) -> Dispatcher:
dp = Dispatcher(storage=MemoryStorage())
dp.update.middleware(DbMiddleware(db)) # DI β har update'ga
dp.message.middleware(ThrottleMiddleware(throttle_rate))
admin_router.message.middleware(IsAdmin(admin_ids)) # faqat admin_router'ni himoyalaydi
dp.include_router(admin_router) # admin AVVAL ulanadi (ustuvor)
dp.include_router(user_router)
return dp
Router tartibi muhim. admin_router avval ulanadi, shuning uchun admin yuborgan /stats avval uni ko'radi. IsAdmin middleware admin_router'ga biriktirilgani uchun oddiy foydalanuvchining /stats i bu yerda to'xtaydi (None) va user_router da /stats handleri yo'qligi sababli hech narsa bo'lmaydi.
Bitta muhim nozik nuqta:
user_routervaadmin_routermodul darajasidagi global obyektlar. Bir router faqat bitta Dispatcher'ga biriktirilishi mumkin β uni ikkinchidp.include_router(...)ga berishga urinsangiz,RuntimeError: Router is already attachedchiqadi. Bitta ishlaydigan botda bu muammo emas (bitta dp), lekin ko'p dispatcher yaratadigan testlarda middlewareni alohida birlik sifatida tekshirgan ma'qul (bizThrottleMiddlewareni shunday test qildik).
9. Rejali eslatma: APScheduler¶
Bot vazifa muddati yetganda foydalanuvchiga eslatma yuborishi kerak. Buni APScheduler ning AsyncIOScheduler i bilan qilamiz β u botning event loop'ida ishlaydigan job'larni belgilangan oraliqda chaqiradi.
# scheduler.py
import datetime as dt
from aiogram import Bot
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from db import Database
async def check_reminders(bot: Bot, db: Database) -> None:
"""Har chaqirilganda vaqti yetgan vazifalarni topib, eslatma yuboradi."""
now_iso = dt.datetime.now().isoformat()
for task_id, user_id, title in await db.due_unsent(now_iso):
try:
await bot.send_message(user_id, f"Eslatma: \"{title}\" vazifasi muddati yetdi!") # JONLI
except Exception:
pass # bloklagan foydalanuvchi
await db.mark_remind_sent(task_id) # ikki marta yubormaslik uchun belgilaymiz
def setup_scheduler(bot: Bot, db: Database) -> AsyncIOScheduler:
scheduler = AsyncIOScheduler()
scheduler.add_job(check_reminders, "interval", minutes=1, args=[bot, db])
scheduler.start()
return scheduler
due_unsent SELECT i ikki shartni birlashtiradi: due_at <= hozir (vaqti yetgan) va remind_sent = 0 (hali yuborilmagan). Yuborgandan keyin mark_remind_sent bilan belgilaymiz β shuning uchun har bir vazifa uchun eslatma faqat bir marta ketadi. Buni offline tasdiqladik (jonli send_message o'rniga logga yozib): vaqti yetgan vazifa tanlandi, kelajakdagisi tanlanmadi, va mark_remind_sent dan keyin due_unsent bo'sh qaytdi. Job mexanizmining o'zini ham AsyncIOScheduler ni 1 soniyalik interval bilan ishga tushirib tekshirdik β eslatma haqiqatan chaqirildi.
10. Kirish nuqtasi: main.py¶
# main.py
import asyncio
import logging
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.types import BotCommand, BotCommandScopeDefault
from config import settings
from db import Database
from bot_app import build_dispatcher
from scheduler import setup_scheduler
async def set_commands(bot: Bot) -> None:
await bot.set_my_commands(
commands=[
BotCommand(command="start", description="Boshlash"),
BotCommand(command="help", description="Yordam"),
BotCommand(command="list", description="Vazifalarim"),
BotCommand(command="cancel", description="Bekor qilish"),
],
scope=BotCommandScopeDefault(),
)
async def main() -> None:
logging.basicConfig(level=logging.INFO)
db = Database("tasks.db")
await db.connect()
bot = Bot(
token=settings.bot_token,
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)
dp = build_dispatcher(db, admin_ids=settings.admin_id_set)
scheduler = setup_scheduler(bot, db) # rejali eslatma
await set_commands(bot) # JONLI: menyu buyruqlari
try:
await dp.start_polling(bot) # JONLI: long polling
finally:
scheduler.shutdown(wait=False)
await db.close()
await bot.session.close()
if __name__ == "__main__":
asyncio.run(main())
Bot(token=..., default=DefaultBotProperties(parse_mode=ParseMode.HTML)) β bu 3.x da parse_mode ni belgilash usuli (ParseMode aiogram.enums dan keladi). dp.start_polling(bot), set_my_commands va start_polling β jonli qadamlar (illustrativ): ular BotFather token va internet talab qiladi. Kodning to'g'riligini biz build_dispatcher va undagi hamma handlerni mock feed_update bilan tasdiqladik.
3.x vs 2.x. Eski 2.x da
executor.start_polling(dp)vafrom aiogram import executorishlatilardi, hamdaBot(parse_mode="HTML"). 3.x da bu butunlay o'zgargan:await dp.start_polling(bot)vadefault=DefaultBotProperties(parse_mode=ParseMode.HTML). Eski uslubni YOZMANG.
11. Botni offline sinash (token kerak emas)¶
Botingiz to'g'ri ishlashini token va internetsiz ham tekshirish mumkin β bu juda qimmatli, chunki har o'zgarishdan keyin botni jonli ishga tushirmasdan sinab ko'rasiz. Siri β mock session (Telegramga hech narsa yubormaydigan) va dp.feed_update(...) (qo'lda yasalgan Update'ni dispatcher'ga uzatish).
# test_offline.py (qisqartirilgan)
import asyncio, datetime as dt
from aiogram import Bot
from aiogram.client.session.base import BaseSession
from aiogram.types import Chat, Message, Update, User
from bot_app import build_dispatcher, TaskCB
from db import Database
FAKE_TOKEN = "123456:AAH-FakeTest_abcdefghijklmnopqrstuvwxyz"
class MockSession(BaseSession):
def __init__(self):
super().__init__()
self.calls = []
async def close(self): ...
async def make_request(self, bot, method, timeout=None):
self.calls.append(type(method).__name__) # Telegramga BORMAYDI
return True
async def stream_content(self, *a, **kw):
yield b""
def msg(text, uid=1001):
return Update(update_id=1, message=Message(
message_id=1, date=dt.datetime.now(),
chat=Chat(id=uid, type="private"),
from_user=User(id=uid, is_bot=False, first_name="Test"),
text=text))
async def main():
db = Database(":memory:")
await db.connect()
session = MockSession()
bot = Bot(token=FAKE_TOKEN, session=session)
dp = build_dispatcher(db, admin_ids={9001}, throttle_rate=0)
await dp.feed_update(bot, msg("/start"))
assert "SendMessage" in session.calls # /start javob berdi
assert await db.count_users() == 1 # foydalanuvchi saqlandi
print("OFFLINE TEST O'TDI")
await bot.session.close()
await db.close()
asyncio.run(main())
Bizning to'liq test to'plamimiz quyidagilarni offline tasdiqladi (token yo'q, internet yo'q β make_request Telegramga hech narsa yubormaydi):
[OK] biznes-logika: parse_due / fmt_due
[OK] DB: upsert / add / list / mark_done / delete + egalik tekshiruvi
[OK] CallbackData pack/unpack: task:done:42
[OK] /start -> SendMessage, foydalanuvchi DB ga yozildi
[OK] FSM forma: title -> due -> DB ga saqlandi, holat tozalandi
[OK] /cancel formani bekor qildi
[OK] callback 'done' -> AnswerCallbackQuery + DB yangilandi
[OK] admin filtri: oddiy user bloklandi, admin o'tdi
[OK] throttle: ketma-ket 2 xabardan faqat 1tasi handlerga yetdi
[OK] eslatma: faqat vaqti yetgan vazifa tanlandi, ikki marta yuborilmaydi
Halol chegara: bu testlar handler mantig'ini, routing'ni, FSM holatlarni, DB ni va middlewareni tekshiradi. Ular jonli yuborishni tekshirmaydi β chunki
make_requestni mock qilganmiz. "Xabar foydalanuvchiga yetib bordimi" degan savolga faqat jonli bot (token + internet) javob bera oladi.
12. Deploy: VPS serverga 24/7 ishlatish¶
Bot kompyuteringizda python main.py bilan ishlaydi, lekin kompyuter o'chsa bot ham o'chadi. Real botni VPS server da systemd xizmati sifatida 24/7 ishlatamiz (17-bobdagi deploy g'oyalarining amaliy yakuni).
Quyidagi qadamlar jonli (illustrativ β VPS va internet talab qiladi):
# 1. Serverda (Ubuntu) Python va venv tayyorlash
sudo apt update && sudo apt install -y python3 python3-venv git
git clone https://github.com/foydalanuvchi/taskbot.git
cd taskbot
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 2. .env ni yaratish (token!) β git ga KIRMAYDI
nano .env # BOT_TOKEN=... va ADMIN_IDS=... yozasiz
requirements.txt:
systemd xizma fayli β bot qulasa avtomatik qayta ishga tushadi:
# /etc/systemd/system/taskbot.service
[Unit]
Description=TaskBot (aiogram)
After=network.target
[Service]
Type=simple
User=botuser
WorkingDirectory=/home/botuser/taskbot
ExecStart=/home/botuser/taskbot/venv/bin/python main.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
# 3. Xizmatni yoqish va ishga tushirish
sudo systemctl daemon-reload
sudo systemctl enable taskbot
sudo systemctl start taskbot
sudo systemctl status taskbot # holatni ko'rish
sudo journalctl -u taskbot -f # loglarni jonli kuzatish
Bir nechta amaliy maslahat:
- Backup. SQLite fayli (
tasks.db) β bu sizning barcha ma'lumotingiz.cronbilan kunlik nusxa oling:0 3 * * * cp /home/botuser/taskbot/tasks.db /home/botuser/backups/tasks-$(date +\%F).db. - Token xavfsizligi.
.envni hech qachon git ga commit qilmang..gitignorega.envva*.dbni qo'shing. Token sizib chiqsa, BotFather'da/revokeqiling. - Polling vs webhook. Bu bobda polling ishlatdik β u VPS uchun yetarli va sodda. Yuqori yuklamali botda webhook samaraliroq (17-bobga qarang); webhook public HTTPS URL talab qiladi.
- Loglash.
logging.basicConfig(level=logging.INFO)qo'ydik βjournalctlorqali ko'rinadi. Production'da xatolarni alohida faylga yoki Sentry'ga yuboring.
Deploy bo'yicha git/CI tafsilotlari uchun Git va GitHub qo'llanmasi ga qarang.
Yakun: 0 dan ekspertgacha yo'l tugadi¶
Tabriklaymiz! Siz bu kitobda Telegram botni noldan ekspert darajasigacha o'rgandingiz:
- 1-2-bob: Bot API arxitekturasi, BotFather, birinchi echo bot.
- 3-4-bob: Router, handlerlar, filtrlar va buyruqlar.
- 5-7-bob: Xabar formatlash, media, klaviaturalar, callback va inline.
- 8-bob: FSM β ko'p bosqichli formalar.
- 9-10-bob: Middleware (DI, throttle) va ma'lumotlar bazasi.
- 11-17-bob: Loyiha tuzilishi, maxsus xususiyatlar va production deploy.
- 18-bob (shu): Hamma narsani birlashtirgan to'liq TaskBot β FSM, DB, klaviatura, callback, middleware, admin, rejali eslatma va deploy.
Endi sizda real, ishlaydigan, kengaytiriladigan bot bor. Uni o'zingizning g'oyangizga moslang: mini-do'kon, quiz, eslatma-bot β asoslar bir xil. Omad!
Mashqlar¶
Quyidagi mashqlarni TaskBot kodi ustida ishlang. Iloji boricha har o'zgarishni 11-bo'limdagi offline test naqshi (mock session + feed_update) bilan tekshiring.
Oson¶
- Yangi buyruq.
/aboutbuyrug'ini qo'shing β u bot haqida qisqa ma'lumot va muallif havolasini chiqarsin.user_routergaCommand("about")handleri yozing. - Vazifalar soni.
show_tasksda, vazifalar ro'yxati tepasiga "Jami: N ta ochiq vazifa" qatorini qo'shing. - Bo'sh ro'yxat tugmasi.
show_tasksda ochiq vazifa yo'q bo'lsa, javobga "Yangi vazifa" inline tugmasini ham qo'shing (callback emas, oddiy reply menyu yetarli β yoki inline tugma bilan urinib ko'ring). fmt_dueuchun test.parse_due("3d", now)natijasinifmt_due(...)ga berib, kutilgan stringniassertbilan tekshiradigan kichik test yozing./cancelxabari. Forma yo'q paytda/cancelbosilganda chiqadigan matnni o'zgartiring va offlinefeed_updatebilan tekshiring.
O'rta¶
- Muddat tahrirlash. Vazifa uchun "Muddatni o'zgartir" callback tugmasini qo'shing: u FSM ga o'tib, yangi muddat so'rasin va DB da yangilasin. (
Database.update_due(task_id, user_id, due_iso)metodini yozishingiz kerak.) - Bajarilganlarni ko'rish.
/donebuyrug'ini qo'shing β ulist_tasks(only_open=False)dan faqat bajarilgan vazifalarni chiqarsin. - Throttle xabari.
ThrottleMiddlewarebloklaganda foydalanuvchiga "Sekinroq, iltimos" deb javob bering (hozir u shunchakiNoneqaytaradi). Maslahat: middleware ichidaevent.answer(...)chaqiring (lekin faqatMessageuchun). - Admin: foydalanuvchini bloklash.
/block <user_id>admin buyrug'ini qo'shing βDatabase.set_blocked(user_id, True)ni chaqirsin. Bloklangan foydalanuvchi botdan foydalana olmasin (DbMiddlewaredan keyin ishlaydigan yangi middleware yoki tekshiruv qo'shing). - Vazifa cheklovi. Bir foydalanuvchi 20 tadan ko'p ochiq vazifa qo'sha olmasin.
add_duedalist_taskssonini tekshiring.
Qiyin¶
- Pagination. Foydalanuvchida 50 ta vazifa bo'lsa, hammasi bitta xabarga sig'maydi.
show_tasksga sahifalashni qo'shing: har sahifada 5 ta vazifa, "Oldingi"/"Keyingi" inline tugmalari bilan (7-bobdagi pagination naqshi).PageCB(CallbackData, prefix="page")ishlating. - Takrorlanuvchi vazifa. "Har kuni" turidagi vazifalarni qo'llab-quvvatlang: bajarilganda yoki muddati o'tganda avtomatik keyingi kunga ko'chsin. DB ga
repeatustuni qo'shing vacheck_remindersni moslang. - SQLAlchemy ga ko'chirish.
db.pyni xom SQL o'rniga SQLAlchemy 2.0 (async) bilan qayta yozing (10-bobdagi naqsh). Mavjud offline testlar o'zgarishsiz o'tishi kerak.
Yechimlar
1. /about buyrug'i¶
@user_router.message(Command("about"))
async def cmd_about(message: Message) -> None:
await message.answer(
"TaskBot β vazifa-menejer bot.\n"
"aiogram 3.x bilan qurilgan.\n"
"Muallif: https://ioqil.uz"
)
Offline tekshiruv: await dp.feed_update(bot, msg("/about")) -> session.calls da "SendMessage" bo'lishi kerak.
2. Vazifalar soni¶
@user_router.message(F.text == "Vazifalarim")
@user_router.message(Command("list"))
async def show_tasks(message: Message, db: "Database") -> None:
tasks = await db.list_tasks(message.from_user.id)
if not tasks:
await message.answer("Ochiq vazifalar yo'q. 'Yangi vazifa' tugmasini bosing.")
return
header = f"Jami: {len(tasks)} ta ochiq vazifa\n"
lines = [f"#{t.id} {t.title} ({fmt_due(t.due_at)})" for t in tasks]
await message.answer(header + "\n".join(lines), reply_markup=tasks_keyboard(tasks))
3. Bo'sh ro'yxat tugmasi¶
Eng sodda yo'l β reply-menyu allaqachon "Yangi vazifa" tugmasiga ega, shuning uchun foydalanuvchiga eslatish kifoya:
if not tasks:
await message.answer(
"Ochiq vazifalar yo'q. Pastdagi 'Yangi vazifa' tugmasini bosing.",
reply_markup=main_menu(),
)
return
Inline variant uchun maxsus callback_data="new_task" tugma va unga handler kerak bo'ladi.
4. fmt_due testi¶
import datetime as dt
from utils import parse_due, fmt_due
now = dt.datetime(2026, 6, 13, 10, 0)
due = parse_due("3d", now) # 2026-06-16 10:00
assert fmt_due(due.isoformat()) == "2026-06-16 10:00"
print("[OK] 3d -> fmt_due")
5. /cancel xabarini o'zgartirish¶
@user_router.message(Command("cancel"))
async def cmd_cancel(message: Message, state: FSMContext) -> None:
if await state.get_state() is None:
await message.answer("Hech narsa bekor qilinmadi β siz menyudasiz.")
return
await state.clear()
await message.answer("Bekor qilindi.", reply_markup=main_menu())
Offline: forma boshlamasdan /cancel -> javobda yangi matn; feed_update bilan tekshiring.
6. Muddat tahrirlash¶
Avval DB metodi:
# db.py ga
async def update_due(self, task_id: int, user_id: int, due_iso: str | None) -> bool:
cur = await self._conn.execute(
"UPDATE tasks SET due_at = ? WHERE id = ? AND user_id = ?",
(due_iso, task_id, user_id),
)
await self._conn.commit()
return cur.rowcount > 0
Keyin FSM va callback:
class EditDue(StatesGroup):
waiting_new_due = State()
class TaskCB(CallbackData, prefix="task"):
action: str # "done" | "del" | "editdue"
task_id: int
@user_router.callback_query(TaskCB.filter(F.action == "editdue"))
async def cb_edit_due(query: CallbackQuery, callback_data: TaskCB, state: FSMContext) -> None:
await state.update_data(task_id=callback_data.task_id)
await state.set_state(EditDue.waiting_new_due)
await query.message.answer("Yangi muddatni kiriting ('2h', '2026-06-20 14:30' yoki 'yoq'):")
await query.answer()
@user_router.message(EditDue.waiting_new_due, F.text)
async def set_new_due(message: Message, state: FSMContext, db: "Database") -> None:
try:
due = parse_due(message.text)
except ValueError:
await message.answer("Format noto'g'ri. Yana urinib ko'ring.")
return
data = await state.get_data()
ok = await db.update_due(data["task_id"], message.from_user.id, due.isoformat() if due else None)
await state.clear()
await message.answer("Muddat yangilandi." if ok else "Vazifa topilmadi.", reply_markup=main_menu())
tasks_keyboard ga "Muddatni o'zgartir" tugmasini ham qo'shasiz: b.button(text="Muddat", callback_data=TaskCB(action="editdue", task_id=t.id)).
7. Bajarilganlarni ko'rish¶
@user_router.message(Command("done"))
async def show_done(message: Message, db: "Database") -> None:
all_tasks = await db.list_tasks(message.from_user.id, only_open=False)
done = [t for t in all_tasks if t.is_done]
if not done:
await message.answer("Hali bajarilgan vazifa yo'q.")
return
await message.answer("Bajarilgan:\n" + "\n".join(f"#{t.id} {t.title}" for t in done))
8. Throttle xabari¶
from aiogram.types import Message as TgMessage
class ThrottleMiddleware(BaseMiddleware):
def __init__(self, rate: float = 0.5):
self.rate = rate
self._last: dict[int, float] = {}
self._warned: set[int] = set()
async def __call__(self, handler, event, data):
user = data.get("event_from_user")
if user is not None:
now = dt.datetime.now().timestamp()
if now - self._last.get(user.id, 0.0) < self.rate:
# faqat Message bo'lsa va birinchi marta bo'lsa ogohlantiramiz
if isinstance(event, TgMessage) and user.id not in self._warned:
self._warned.add(user.id)
await event.answer("Sekinroq, iltimos.")
return None
self._last[user.id] = now
self._warned.discard(user.id)
return await handler(event, data)
Eslatma: dp.message.middleware(...) da event aynan Message bo'ladi, shuning uchun isinstance tekshiruvi xavfsiz.
9. Admin: foydalanuvchini bloklash¶
@admin_router.message(Command("block"))
async def cmd_block(message: Message, db: "Database") -> None:
parts = message.text.split()
if len(parts) != 2 or not parts[1].isdigit():
await message.answer("Foydalanish: /block <user_id>")
return
await db.set_blocked(int(parts[1]), True)
await message.answer(f"Foydalanuvchi {parts[1]} bloklandi.")
Bloklangan foydalanuvchini to'xtatuvchi middleware (DbMiddleware dan keyin ulanadi):
class BlockGuard(BaseMiddleware):
async def __call__(self, handler, event, data):
db = data.get("db")
user = data.get("event_from_user")
if db is not None and user is not None and await db.is_blocked(user.id):
return None # bloklangan β handler chaqirilmaydi
return await handler(event, data)
# build_dispatcher ichida:
# dp.message.middleware(BlockGuard()) # DbMiddleware dp.update da bo'lgani uchun db mavjud
is_blocked db.py da allaqachon bor.
10. Vazifa cheklovi¶
@user_router.message(AddTask.waiting_due, F.text)
async def add_due(message: Message, state: FSMContext, db: "Database") -> None:
open_count = len(await db.list_tasks(message.from_user.id))
if open_count >= 20:
await state.clear()
await message.answer("Sizda 20 ta ochiq vazifa bor. Avval bir nechtasini bajaring.")
return
try:
due = parse_due(message.text)
except ValueError:
await message.answer("Muddat formati noto'g'ri. Yana urinib ko'ring (yoki 'yoq').")
return
data = await state.get_data()
due_iso = due.isoformat() if due else None
task_id = await db.add_task(message.from_user.id, data["title"], due_iso)
await state.clear()
await message.answer(
f"Saqlandi! #{task_id}: {data['title']}\nMuddat: {fmt_due(due_iso)}",
reply_markup=main_menu(),
)
11. Pagination¶
PAGE_SIZE = 5
class PageCB(CallbackData, prefix="page"):
offset: int
def tasks_page_keyboard(tasks, offset: int) -> InlineKeyboardMarkup:
b = InlineKeyboardBuilder()
page = tasks[offset:offset + PAGE_SIZE]
for t in page:
b.button(text=f"Bajardim: {t.title[:18]}", callback_data=TaskCB(action="done", task_id=t.id))
b.adjust(1)
nav = InlineKeyboardBuilder()
if offset > 0:
nav.button(text="Oldingi", callback_data=PageCB(offset=max(0, offset - PAGE_SIZE)))
if offset + PAGE_SIZE < len(tasks):
nav.button(text="Keyingi", callback_data=PageCB(offset=offset + PAGE_SIZE))
b.attach(nav)
return b.as_markup()
def _render(tasks, offset):
page = tasks[offset:offset + PAGE_SIZE]
total_pages = (len(tasks) + PAGE_SIZE - 1) // PAGE_SIZE
cur = offset // PAGE_SIZE + 1
text = f"Vazifalar ({cur}/{total_pages}):\n" + "\n".join(
f"#{t.id} {t.title} ({fmt_due(t.due_at)})" for t in page
)
return text
@user_router.message(F.text == "Vazifalarim")
async def show_tasks_paged(message: Message, db: "Database") -> None:
tasks = await db.list_tasks(message.from_user.id)
if not tasks:
await message.answer("Ochiq vazifalar yo'q.")
return
await message.answer(_render(tasks, 0), reply_markup=tasks_page_keyboard(tasks, 0))
@user_router.callback_query(PageCB.filter())
async def page_nav(query: CallbackQuery, callback_data: PageCB, db: "Database") -> None:
tasks = await db.list_tasks(query.from_user.id)
off = callback_data.offset
await query.message.edit_text(_render(tasks, off), reply_markup=tasks_page_keyboard(tasks, off))
await query.answer()
12. Takrorlanuvchi vazifa¶
DB ga repeat ustuni qo'shing (ALTER TABLE yoki yangi CREATE): repeat TEXT ("daily" yoki NULL). check_reminders ni moslang:
async def check_reminders(bot, db):
now = dt.datetime.now()
for task_id, user_id, title in await db.due_unsent(now.isoformat()):
try:
await bot.send_message(user_id, f"Eslatma: \"{title}\"")
except Exception:
pass
task_repeat = await db.get_repeat(task_id) # yangi metod
if task_repeat == "daily":
# keyingi kunga ko'chiramiz va remind_sent ni 0 qoldiramiz
await db.reschedule(task_id, (now + dt.timedelta(days=1)).isoformat())
else:
await db.mark_remind_sent(task_id)
reschedule due_at ni yangilab, remind_sent = 0 qiladi. Shunda ertasi kuni yana eslatma ketadi.
13. SQLAlchemy ga ko'chirish¶
10-bobdagi async naqsh. Qisqacha skelet:
from sqlalchemy import String, Integer, Boolean, ForeignKey
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
user_id: Mapped[int] = mapped_column(Integer, primary_key=True)
full_name: Mapped[str] = mapped_column(String)
is_blocked: Mapped[bool] = mapped_column(Boolean, default=False)
class TaskRow(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.user_id"))
title: Mapped[str] = mapped_column(String)
due_at: Mapped[str | None] = mapped_column(String, nullable=True)
is_done: Mapped[bool] = mapped_column(Boolean, default=False)
class Database:
def __init__(self, url: str):
self._engine = create_async_engine(url)
self._sm = async_sessionmaker(self._engine, expire_on_commit=False)
async def connect(self):
async with self._engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def add_task(self, user_id, title, due_at) -> int:
async with self._sm() as s:
row = TaskRow(user_id=user_id, title=title, due_at=due_at)
s.add(row)
await s.commit()
return row.id
# ... qolgan metodlar shu naqshda
URL: "sqlite+aiosqlite:///tasks.db". Handler kodi (db.add_task(...) chaqiruvi) o'zgarmaydi β repository interfeysi bir xil, shuning uchun mavjud offline testlaringiz o'zgarishsiz o'tadi.
β¬ οΈ Oldingi: 17 β Production va deploy Β· π README Β· Keyingi: 19 β Guruhlarda ishlash β‘οΈ