11 β Loyiha tuzilishi va konfiguratsiya¶
β¬ οΈ Oldingi: 10 β Ma'lumotlar bazasi bilan ishlash Β· π README Β· Keyingi: 12 β Maxsus xususiyatlar β‘οΈ
Bu bobda: shu paytgacha biz hamma narsani bitta
main.pyfaylga yozib keldik β bu o'rganish uchun qulay, lekin bot kattalashgani sayin bitta fayl o'qib bo'lmas darajaga yetadi. Bu bobda botni professional, modullashtirilgan ko'rinishga keltiramiz:handlers/,keyboards/,states/,services/,middlewares/paketlari vaconfig.py. Maxfiy ma'lumotlarni (token!) kodga emas,.envfayliga olib chiqamiz va unipydantic-settingsbilan o'qiymiz. Dispatcher'ningworkflow_datalug'ati orqali dependency injection (DI) β ya'ni bog'liqliklarni (DB ulanishi, sozlamalar, servislar) handlerlarga avtomatik uzatishni o'rganamiz. Va nihoyat,botbilandpni to'g'ri boshlaydiganmain.pynaqshini va scaling (kengayish) tushunchalarini ko'rib chiqamiz.Halol eslatma: bu bobdagi tuzilish, routerlarni biriktirish,
pydantic-settingsconfig,workflow_dataDI va handler yo'naltirish mantig'ifeed_updatemock Update orqali offline tekshirilgan (token va internet kerak emas). Faqatawait dp.start_polling(bot)vaawait message.answer(...)jonli β ular haqiqiy ishlashi uchun BotFather token va internet talab qiladi; shuning uchun offline testlardamessage.answero'rniga natijani ro'yxatga yozamiz va bu qatorlarni "illustrativ" deb belgilab qo'yamiz.
Nima uchun bitta fayl yetmaydi¶
8-10-boblarda yozgan botingiz allaqachon 200-300 qatorga yetgandir. Hozircha bardosh berish mumkin. Ammo real botda:
- 30-50 ta handler bo'ladi (har bir buyruq, har bir tugma, har bir holat),
- klaviaturalar o'nlab joyda ishlatiladi,
- FSM holatlari ko'p,
- DB so'rovlari, tashqi API chaqiruvlari, biznes-logika qo'shiladi.
Hamma narsa bitta main.py da bo'lsa, fayl 2000+ qatorga o'sadi va siz unda kerakli joyni topolmay qolasiz. Yechim β modullashtirish: bog'liq kodni alohida fayllarga bo'lib qo'yish.
Eslatma: bu xuddi Python loyihalarida paketlarga bo'lish bilan bir xil g'oya β agar paket, modul va
__init__.pytushunchalari xira bo'lsa, Python qo'llanmasi ga qaytib qarang. Node.js'da bot loyihasi qanday bo'linishini esa Node.js qo'llanmasi da ko'rishingiz mumkin.
Tavsiya etilgan loyiha tuzilishi¶
Mana o'rta-katta aiogram 3.x boti uchun amaliy tuzilish:
mybot/
βββ .env # maxfiy sozlamalar (git ga KIRMAYDI)
βββ .env.example # namuna (git ga kiradi, token YO'Q)
βββ .gitignore
βββ requirements.txt
βββ config.py # Settings β .env ni o'qiydi
βββ main.py # bot + dp ni boshlaydi (kirish nuqtasi)
βββ bot/
βββ __init__.py
βββ handlers/
β βββ __init__.py # barcha routerlarni yig'adi
β βββ start.py # /start, /help
β βββ common.py # umumiy xabarlar, echo
β βββ admin.py # faqat admin buyruqlari
βββ keyboards/
β βββ __init__.py
β βββ inline.py # InlineKeyboardBuilder funksiyalari
β βββ reply.py # ReplyKeyboardBuilder funksiyalari
βββ states/
β βββ __init__.py
β βββ registration.py # StatesGroup lar
βββ services/
β βββ __init__.py
β βββ users.py # biznes-logika, DB bilan ishlash
βββ middlewares/
βββ __init__.py
βββ logging.py # middleware lar
Har bir papkaning vazifasi:
| Papka | Nima saqlanadi | Misol |
|---|---|---|
handlers/ |
Router'lar va handler funksiyalari (kelgan update'ga javob) | @router.message(CommandStart()) |
keyboards/ |
Klaviatura quruvchi funksiyalar | def main_menu() -> InlineKeyboardMarkup |
states/ |
FSM StatesGroup lar |
class Registration(StatesGroup) |
services/ |
Biznes-logika, DB, tashqi API β Telegram'ga bog'liq emas | async def create_user(...) |
middlewares/ |
BaseMiddleware voris klasslari |
throttling, logging, DB sessiya |
config.py |
Sozlamalar klassi | class Settings(BaseSettings) |
main.py |
Kirish nuqtasi β hammasini ulaydi va polling boshlaydi | asyncio.run(main()) |
Asosiy qoida: services/ Telegram haqida bilmasligi kerak. U faqat ma'lumot bilan ishlaydi (foydalanuvchini bazaga yozish, narxni hisoblash). Telegram'ga javob berishni handlers/ qiladi. Bu ajratish kodni test qilishni va keyinroq, masalan, web-versiya qo'shishni osonlashtiradi.
Routerlar bilan modullashtirish¶
aiogram 3.x da modullashtirishning kaliti β Router. Har bir fayl o'z Router ini yaratadi, handlerlarni o'sha router'ga ulaydi, so'ng main.py barcha router'larni Dispatcher ga biriktiradi.
handlers/start.py¶
from aiogram import Router
from aiogram.filters import CommandStart, Command
from aiogram.types import Message
router = Router(name="start") # name β log'da qaysi router ishlaganini ko'rish uchun
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
await message.answer("Salom! Men modullashtirilgan botman.")
@router.message(Command("help"))
async def cmd_help(message: Message) -> None:
await message.answer("Mavjud buyruqlar: /start, /help")
handlers/common.py¶
from aiogram import Router, F
from aiogram.types import Message
router = Router(name="common")
@router.message(F.text)
async def echo(message: Message) -> None:
await message.answer(f"Siz yozdingiz: {message.text}")
handlers/admin.py¶
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
router = Router(name="admin")
@router.message(Command("stats"))
async def cmd_stats(message: Message) -> None:
# Bu yerda faqat admin uchun filtr keyinroq qo'shiladi (middleware orqali)
await message.answer("Statistika: ...")
handlers/init.py β routerlarni yig'ish¶
Eng qulay naqsh β handlers paketi ichida bitta "asosiy router" yaratib, unga barcha sub-router'larni biriktirish. Shunda main.py faqat bitta narsani import qiladi:
from aiogram import Router
from .start import router as start_router
from .common import router as common_router
from .admin import router as admin_router
def get_handlers_router() -> Router:
router = Router(name="handlers")
# DIQQAT: tartib muhim β yuqoridagi router birinchi tekshiriladi.
# admin va start ni common (echo) dan OLDIN qo'shamiz, aks holda
# echo barcha matnni "yutib" yuboradi.
router.include_routers(admin_router, start_router, common_router)
return router
include_routers(*routers) β bir nechta router'ni bir martaga biriktiradi (aiogram 3.x). Bitta-bitta qo'shmoqchi bo'lsangiz, include_router(r) ni ishlatasiz.
Tartib nega muhim? Dispatcher update kelganida router'larni biriktirilgan tartibda birma-bir tekshiradi. Birinchi mos kelgan handler ishlaydi va qolganlar tekshirilmaydi.
common.pydagiF.texthar qanday matnga mos keladi, shu sababli uni oxirida qo'yamiz, aks holda/startham unga tushib qoladi.
Konfiguratsiya: token kodda emas, .env da¶
Eng katta xavfsizlik xatosi β token (yoki DB parol, API kalit) ni to'g'ridan-to'g'ri kodga yozish:
Agar bu kodni GitHub'ga yuborsangiz, tokeningiz o'g'irlanadi va botingizni boshqalar boshqaradi. To'g'ri yo'l β maxfiy ma'lumotlarni .env fayliga olish va uni .gitignore ga qo'shish.
.env fayli¶
.gitignore¶
.env.example (git ga kiradi, qiymatlarsiz)¶
.env.example ni git'ga qo'yasiz, shunda loyihani ko'chirib olgan kishi qaysi sozlamalar kerakligini biladi, lekin haqiqiy tokeningizni ko'rmaydi.
config.py β pydantic-settings¶
.env ni o'qish uchun pydantic-settings paketini ishlatamiz. U .env dagi qatorlarni o'qiydi, tiplarni avtomatik tekshiradi (masalan ADMIN_IDS ni list[int] ga aylantiradi) va kerakli sozlama yo'q bo'lsa darrov xato beradi.
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
bot_token: str # majburiy β yo'q bo'lsa xato
admin_ids: list[int] = [] # ixtiyoriy, standart bo'sh ro'yxat
db_path: str = "bot.db"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore", # .env dagi notanish qatorlarni e'tiborsiz qoldir
)
settings = Settings() # import paytida bir marta yuklanadi
Diqqat qiling: maydon nomi bot_token, .env da esa BOT_TOKEN. pydantic-settings registrga befarq (case-insensitive) ishlaydi, shuning uchun mos keladi. list[int] maydon uchun .env da JSON ko'rinishida ([111,222]) yoziladi.
Endi kodning istalgan joyida:
from config import settings
print(settings.bot_token) # .env dan o'qilgan
print(settings.admin_ids) # [111111111, 222222222] β int ro'yxati
Offline tekshirildi:
Settings()ni yuqoridagi.env(fake token bilan) dan o'qib,settings.bot_token,settings.admin_ids,settings.db_pathqiymatlari to'g'ri tip va qiymatda kelganini tasdiqladim.
Dependency Injection: workflow_data¶
Tasavvur qiling, handleringizga DB ulanishi (pool), sozlamalar (settings) yoki biron servis kerak. Eski, qo'pol yo'l β bularni global o'zgaruvchi qilish. Bu testlashni qiyinlashtiradi va kodni chalkashtiradi.
aiogram 3.x dependency injection (DI) ni o'zida taqdim etadi: siz bog'liqlikni bir marta Dispatcher ga qo'yasiz, handler esa uni parametr nomi orqali avtomatik oladi.
Ishlashi:
main.pydadp["repo"] = repodeb yozasiz (yokidp.workflow_data["repo"] = repo).- Dispatcher buni
workflow_datalug'atida saqlaydi. - Update kelganda, dispatcher handler funksiyasining parametrlarini ko'radi.
- Agar parametr nomi
workflow_datadagi kalitga mos kelsa, qiymatni kwarg sifatida uzatadi.
# handlers/start.py da:
@router.message(CommandStart())
async def cmd_start(message: Message, repo, settings) -> None:
# repo va settings AVTOMATIK keladi β parametr nomi kalitga mos
count = await repo.add_user(message.from_user.id)
is_admin = message.from_user.id in settings.admin_ids
await message.answer(f"Siz {count}-foydalanuvchisiz. Admin: {is_admin}")
Handlerga repo va settings ni hech kim qo'lda uzatmaydi β aiogram ularni nom bo'yicha topib beradi. Agar handlerga bu kerak bo'lmasa, parametrni umuman yozmaysiz; dispatcher ortiqcha narsa uzatmaydi.
Offline tekshirildi: DI mantig'ini sinash uchun
dp["repo"] = repovadp["settings_obj"] = settingsqo'yib,feed_updateorqali mock/startupdate'ni uzatdim (handlermessage.answero'rniga natijani o'zgaruvchiga yozadi β yuqoridagiOUTnaqshi kabi, chunkimessage.answerjonli token talab qiladi). Handlerrepovasettings_objni kwarg sifatida oldi,repo.add_userchaqirildi va admin tekshiruvi to'g'ri natija (count=1,is_admin=True) qaytardi.
start_polling orqali ham uzatish mumkin¶
dp["..."]=... o'rniga bog'liqliklarni to'g'ridan-to'g'ri start_polling ga ham berish mumkin β ular ham workflow_data ga qo'shiladi:
Ikkala usul ham bir xil natija beradi. Odatda dp["key"]=value aniqroq va o'qishga qulayroq.
main.py β bot va dp ni boshlash naqshi¶
Endi hammasini bir joyga yig'amiz. main.py β botning kirish nuqtasi. Uning vazifasi: config yuklash, bot va dispatcher yaratish, bog'liqliklarni qo'shish (DI), routerlarni biriktirish va polling boshlash.
# 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 config import settings
from bot.handlers import get_handlers_router
from bot.services.users import UserRepo
async def main() -> None:
logging.basicConfig(level=logging.INFO)
# 1. Bot β default parse_mode HTML
bot = Bot(
token=settings.bot_token,
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)
# 2. Dispatcher + storage (FSM uchun)
dp = Dispatcher(storage=MemoryStorage())
# 3. DI: bog'liqliklarni qo'shish
repo = UserRepo(settings.db_path)
dp["repo"] = repo
dp["settings"] = settings
# 4. Routerlarni biriktirish
dp.include_router(get_handlers_router())
# 5. Polling boshlash (jonli Telegram talab qiladi)
try:
await dp.start_polling(bot)
finally:
await bot.session.close()
if __name__ == "__main__":
asyncio.run(main())
Ba'zi muhim nuqtalar:
Bot(token=..., default=DefaultBotProperties(parse_mode=ParseMode.HTML))β aiogram 3.x daparse_modeto'g'ridan-to'g'riBot()ga emas,DefaultBotPropertiesorqali beriladi.ParseModeaiogram.enumsdan keladi. (2.x dagiBot(parse_mode="HTML")endi ishlamaydi.)MemoryStorage()β FSM holatlarini xotirada saqlaydi. Bot qayta ishga tushganda holatlar yo'qoladi; production'daRedisStorageishlatiladi (pastdagi scaling bo'limiga qarang).finally: await bot.session.close()β bot HTTP sessiyasini toza yopadi.asyncio.run(main())β asyncmain()ni ishga tushiradi.
Halol eslatma:
await dp.start_polling(bot)qatori illustrativ β u haqiqatan ishlashi uchun BotFather'dan olingan token va internet kerak. Qolgan barcha qatorlar (config, bot/dp yaratish, DI, router biriktirish) offline tekshirildi.
Hammasini ulaydigan to'liq, ishlaydigan misol¶
Quyidagi misol bitta faylda, lekin bobning barcha naqshlarini (config, modul router, DI, feed_update) ko'rsatadi. U haqiqatan offline ishlaydi β start_polling o'rniga mock update uzatamiz.
Muhim nuqta:
feed_updateorqali sinaganda handlerawait message.answer(...)chaqirsa, aiogram darrov Telegram API'ga so'rov yuboradi va fake token bilanTelegramUnauthorizedErrorberadi. Shuning uchun offline testda handler javobni Telegram'ga yubormasdan, natijani bir ro'yxatga (OUT) yozadi. Jonli botda esa shu satr o'rnigaawait message.answer(...)turadi (kod izohida ko'rsatilgan).
import asyncio
from datetime import datetime
from aiogram import Bot, Dispatcher, Router, F
from aiogram.filters import CommandStart
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Update, Message, Chat, User
# --- "services/users.py" o'rnida (biznes-logika) ---
class UserRepo:
def __init__(self):
self.users: list[int] = []
async def add_user(self, user_id: int) -> int:
if user_id not in self.users:
self.users.append(user_id)
return len(self.users)
# Offline test natijasi shu yerga yig'iladi (message.answer o'rniga,
# chunki feed_update'da message.answer Telegram API'ga uradi va token talab qiladi).
OUT: list[tuple] = []
# --- "handlers/start.py" o'rnida ---
start_router = Router(name="start")
@start_router.message(CommandStart())
async def cmd_start(message: Message, repo: UserRepo) -> None:
count = await repo.add_user(message.from_user.id)
OUT.append(("start", count))
# Jonli botda yuqoridagi satr o'rniga:
# await message.answer(f"Salom! Siz {count}-foydalanuvchisiz.")
# --- "handlers/common.py" o'rnida ---
common_router = Router(name="common")
@common_router.message(F.text)
async def echo(message: Message) -> None:
OUT.append(("echo", message.text))
# Jonli botda: await message.answer(f"Echo: {message.text}")
async def main() -> None:
bot = Bot(token="123456:AAH-FakeTest_abc") # OFFLINE: fake token
dp = Dispatcher(storage=MemoryStorage())
dp["repo"] = UserRepo() # DI
dp.include_routers(start_router, common_router) # modul routerlar (tartib!)
# OFFLINE test: start_polling o'rniga mock update uzatamiz.
# Handler message.answer chaqirmagani uchun Telegram API'ga urilmaydi.
def make_update(text: str) -> Update:
msg = Message(
message_id=1, date=datetime.now(),
chat=Chat(id=1, type="private"),
from_user=User(id=1, is_bot=False, first_name="Test"),
text=text,
)
return Update(update_id=1, message=msg)
await dp.feed_update(bot, make_update("/start")) # -> start_router
await dp.feed_update(bot, make_update("salom")) # -> common_router
await bot.session.close()
# Routing va DI ni tasdiqlaymiz:
assert ("start", 1) in OUT # /start start_router ga tushdi (DI bilan repo keldi)
assert ("echo", "salom") in OUT # oddiy matn common_router (echo) ga tushdi
print("OK routing + DI:", OUT)
if __name__ == "__main__":
asyncio.run(main())
Offline tekshirildi: yuqoridagi naqsh (modul router + DI +
feed_update)feed_updateorqali ishga tushirildi (token va internet kerak emas):/startstart_routerga, oddiy matncommon_routerga to'g'ri yo'naltirildi va DI bilanrepohandlerga yetib bordi β ikkalaassertham o'tdi. Halol eslatma: offline testda handlermessage.answero'rnigaOUTro'yxatiga yozadi, chunkifeed_update'damessage.answerjonli token+internet talab qiladi (TelegramUnauthorizedErrorberadi). Jonli botdaOUT.append(...)o'rnigaawait message.answer(...),feed_update(...)o'rniga esaawait dp.start_polling(bot)turadi.
Middleware'larni modullashtirish¶
Middleware β har bir update handler'ga yetib borishidan oldin (va keyin) ishlaydigan kod. Ularni middlewares/ paketiga ajratamiz. (Middleware'lar haqida to'liqroq keyingi boblarda.)
# middlewares/logging.py
from typing import Any, Awaitable, Callable
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
class StructLogMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
# handler dan OLDIN
print(f"[LOG] kelgan update: {type(event).__name__}")
# data ga qo'shsangiz, u ham DI orqali handlerga yetadi!
data["request_id"] = "abc-123"
result = await handler(event, data)
# handler dan KEYIN
print("[LOG] handler tugadi")
return result
main.py da ulash:
from bot.middlewares.logging import StructLogMiddleware
dp.update.middleware(StructLogMiddleware()) # barcha update'larga
Diqqat: middleware data lug'atiga qo'shgan narsa (data["request_id"]) ham handlerga DI orqali keladi β handler async def h(message, request_id) deb yozsa, qiymatni oladi.
Offline tekshirildi: router'ga
StructLogMiddlewareulab,feed_updateorqali/startuzatdim. Middleware handler'dan oldin va keyin ishladi,data["..."]ga qo'ygan qiymat handlerga kwarg sifatida yetib bordi.
Scaling: bot kattalashganda¶
Botingiz o'sgani sayin quyidagilarni o'ylash kerak bo'ladi.
1. Storage: MemoryStorage emas, Redis. MemoryStorage FSM holatlarini RAM'da saqlaydi β bot qayta ishga tushsa, hammasi yo'qoladi va bir nechta nusxa (process) o'zaro holatni ko'rishmaydi. Production'da RedisStorage ishlating:
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://localhost:6379/0")
dp = Dispatcher(storage=storage)
2. Polling emas, webhook. Long-polling kichik botlar uchun yaxshi, lekin Telegram'dan doimo so'rab turadi. Yuqori yuklamada webhook (Telegram o'zi sizning serveringizga so'rov yuboradi) tejamliroq. Bu deploy va public HTTPS URL talab qiladi β keyingi boblarda va Git/GitHub bo'limi dagi deploy mavzularida.
3. SQLite emas, kuchli DB. Kichik bot uchun aiosqlite yetarli, lekin ko'p foydalanuvchili botda PostgreSQL (masalan asyncpg yoki async SQLAlchemy 2.0 bilan) tavsiya etiladi. Ulanish pool'ini DI orqali handlerlarga uzating (dp["pool"] = pool). Baza dizayni bo'yicha SQL qo'llanmasi ga qarang.
4. Bir nechta nusxa (horizontal scaling). Bir process'da bot CPU/I-O bo'yicha cheklangan. Webhook + tashqi storage (Redis) + tashqi DB bilan bir nechta nusxa ishga tushirish mumkin. Bu holda holat va navbat hamma nusxalar uchun umumiy bo'ladi.
5. Konfiguratsiya muhitlar bo'yicha. dev va prod uchun alohida .env fayllar (.env.dev, .env.prod) yoki muhit o'zgaruvchilari ishlatiladi. pydantic-settings muhit o'zgaruvchilarini .env dan ustun qo'yadi, shu sababli serverda BOT_TOKEN ni muhit o'zgaruvchisi sifatida berishingiz mumkin β fayl umuman kerak bo'lmaydi.
Bu bo'limning ko'p qismi (webhook, Redis, bir nechta nusxa) jonli muhit talab qiladi β token, server, public URL. Shuning uchun ular bu yerda tushuncha sifatida berildi; amaliy deploy keyingi boblarda.
Eng ko'p uchraydigan xatolar¶
echohamma narsani yutadi.F.textrouter'ini boshqalardan oldin qo'ysangiz,/startham unga tushadi. Yechim: umumiy/echo router'ni oxirida biriktiring.- Token kodda.
Bot(token="123:AAA...")β hech qachon. Doim.env+config.py. .envni git'ga yuborish..gitignorega.envni qo'shishni unutmang. Agar bir marta yuborib qo'ygan bo'lsangiz, tokenni @BotFather'da darhol bekor qiling (/revoke).- DI parametr nomini noto'g'ri yozish.
dp["repo"]qo'yib, handlerdaasync def h(message, repository)deb yozsangiz,repositorykaliti yo'q va xato chiqadi. Nom aynan mos kelishi kerak. - 2.x sintaksisi.
Bot(parse_mode="HTML"),executor.start_polling,@dp.message_handlerβ bularning hammasi 2.x. 3.x daDefaultBotProperties,dp.start_polling,@router.messageishlating.
Mashqlar¶
Oson¶
- Loyiha tuzilishini chizing:
mybot/ostida qaysi papkalar bo'lishi va har biriga qanday fayl tushishini yozib chiqing (handlers, keyboards, states, services, middlewares). .envfaylidaBOT_TOKEN,ADMIN_IDS(ikkita ID),DB_PATHni yozing. So'ng.gitignorega.envni qo'shish nima uchun zarurligini bir jumlada tushuntiring.config.pydaSettings(BaseSettings)yarating:bot_token: str,db_path: str = "bot.db".model_configdaenv_file=".env"ko'rsating.handlers/start.pymodulini yozing:router = Router(name="start")va/startuchun handler. Bu fayldaDispatcherishlatilmasligini tekshiring (faqatRouter).handlers/__init__.pydastart_routervacommon_routerni bittaRoutergainclude_routersbilan yig'adiganget_handlers_router()funksiyasini yozing.- Eski 2.x kodini 3.x ga aylantiring:
bot = Bot(token=TOKEN, parse_mode="HTML")ni to'g'ri 3.x ko'rinishiga keltiring.
O'rta¶
config.pydaadmin_ids: list[int]maydon qo'shing va.envdagiADMIN_IDS=[111,222]to'g'rilist[int]ga aylantirilishini kichik skript bilan tekshiring (chop eting).main.pynaqshining besh qadamini (config, bot/dp, DI, router, polling) to'g'ri tartibda yozing. Har qadamga bitta qator izoh qo'ying.dp["repo"] = repoqo'yib,cmd_start(message, repo)handlerini yozing.repo.add_user()ni chaqirib, foydalanuvchi sonini qaytaring. (Jonli javob o'rnigaprintishlatishingiz mumkin.)- Routerlarni biriktirish tartibi muhimligini ko'rsatadigan misol yozing:
common(echo) nistartdan oldin qo'ying va nima yuz berishini tushuntiring. start_polling(bot, repo=repo)orqali DI uzatishnidp["repo"]=repobilan solishtiring: ikkalasi ham handlergareponi qanday yetkazadi?services/users.pydaUserRepoklassini yozing (add_user,count). Bu klass Telegram haqida hech narsa bilmasligi kerakligini tekshiring (import aiogrambo'lmasin).
Qiyin¶
- To'liq mini-loyiha yozing:
config.py,handlers/start.py,handlers/common.py,handlers/__init__.py(router yig'uvchi),services/users.py. So'ngfeed_updateorqali/startva oddiy matnni offline test qiling β har biri to'g'ri router'ga tushishiniassertbilan tasdiqlang. - Middleware yozing (
StructLogMiddleware): udata["request_id"] = "..."qo'shsin. Handlerasync def h(message, request_id)deb yozib, qiymatni olishinifeed_updatebilan tekshiring. pydantic-settingsda prod/dev muhitlarini qo'llab-quvvatlash uchun fikrlang: muhit o'zgaruvchisi.envfaylidan ustun turishini kichik tajriba bilan ko'rsating (masalanos.environ["BOT_TOKEN"]qo'yib,Settings()qaysi qiymatni olishini tekshiring).
Yechimlar
1.
mybot/
βββ .env, .gitignore, requirements.txt
βββ config.py # Settings
βββ main.py # kirish nuqtasi
βββ bot/
βββ handlers/ (start.py, common.py, admin.py, __init__.py)
βββ keyboards/ (inline.py, reply.py)
βββ states/ (registration.py)
βββ services/ (users.py β biznes-logika, DB)
βββ middlewares/(logging.py)
handlers/ β update'ga javob; keyboards/ β klaviatura quruvchilar; states/ β FSM StatesGroup; services/ β Telegram'dan mustaqil biznes-logika; middlewares/ β handler oldidan/keyin ishlaydigan kod.
2.
.gitignore ga .env qo'shamiz, chunki u tokenni saqlaydi β agar git'ga (ayniqsa public repo'ga) tushsa, token o'g'irlanadi va botni begonalar boshqaradi.
3.
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
bot_token: str
db_path: str = "bot.db"
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
settings = Settings()
4.
# handlers/start.py
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message
router = Router(name="start") # Dispatcher YO'Q β faqat Router
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
await message.answer("Salom!")
Dispatcher bo'lmaydi β u faqat main.py da yaratiladi. Modul faqat o'z Router ini eksport qiladi.
5.
# handlers/__init__.py
from aiogram import Router
from .start import router as start_router
from .common import router as common_router
def get_handlers_router() -> Router:
router = Router(name="handlers")
router.include_routers(start_router, common_router) # start oldin, echo oxirida
return router
6.
# β 2.x:
# bot = Bot(token=TOKEN, parse_mode="HTML")
# β
3.x:
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
7.
# .env: ADMIN_IDS=[111,222]
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
admin_ids: list[int] = []
bot_token: str = "x"
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
s = Settings()
print(s.admin_ids, type(s.admin_ids[0])) # [111, 222] <class 'int'>
assert s.admin_ids == [111, 222]
assert isinstance(s.admin_ids[0], int)
pydantic-settings list[int] maydonni JSON sifatida o'qib, har elementni int ga aylantiradi.
8.
async def main() -> None:
bot = Bot(token=settings.bot_token,
default=DefaultBotProperties(parse_mode=ParseMode.HTML)) # 1+2
dp = Dispatcher(storage=MemoryStorage()) # 2
dp["repo"] = UserRepo(settings.db_path) # 3 DI
dp["settings"] = settings # 3 DI
dp.include_router(get_handlers_router()) # 4 routerlar
try:
await dp.start_polling(bot) # 5 polling (jonli)
finally:
await bot.session.close()
9.
class UserRepo:
def __init__(self):
self.users = []
async def add_user(self, uid):
if uid not in self.users:
self.users.append(uid)
return len(self.users)
@router.message(CommandStart())
async def cmd_start(message: Message, repo: UserRepo) -> None:
count = await repo.add_user(message.from_user.id)
print(f"{count}-foydalanuvchi") # jonli botda: await message.answer(...)
repo dp["repo"]=repo orqali DI bilan keladi β qo'lda uzatilmaydi.
10.
Bu holda/start ham F.text ga mos keladi va common_router dagi echo handler birinchi ishlaydi β /start handleriga yetib bormaydi. Shu sababli echo doim oxirida turishi kerak.
11. Ikkalasi ham workflow_data ga repo ni qo'yadi. dp["repo"]=repo β yaratish paytida; start_polling(bot, repo=repo) β polling boshlanganda. Natija bir xil: handler repo nomli parametr orqali qiymatni oladi. Ko'pincha dp["repo"]=repo o'qishga aniqroq.
12.
# services/users.py β import aiogram YO'Q!
class UserRepo:
def __init__(self, db_path: str = "bot.db"):
self.db_path = db_path
self._users: list[int] = []
async def add_user(self, uid: int) -> int:
if uid not in self._users:
self._users.append(uid)
return len(self._users)
def count(self) -> int:
return len(self._users)
services/ Telegram'ga bog'liq emas β shu sababli uni alohida (hatto web-versiyada) qayta ishlatish va test qilish oson.
13.
import asyncio
from datetime import datetime
from aiogram import Bot, Dispatcher, Router, F
from aiogram.filters import CommandStart
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Update, Message, Chat, User
class UserRepo:
def __init__(self): self.users = []
async def add_user(self, uid):
self.users.append(uid); return len(self.users)
start_router = Router(name="start")
OUT = []
@start_router.message(CommandStart())
async def cmd_start(message: Message, repo: UserRepo):
n = await repo.add_user(message.from_user.id)
OUT.append(("start", n))
common_router = Router(name="common")
@common_router.message(F.text)
async def echo(message: Message):
OUT.append(("echo", message.text))
def get_handlers_router():
r = Router(name="handlers")
r.include_routers(start_router, common_router) # echo oxirida
return r
async def main():
bot = Bot(token="123456:AAH-FakeTest_abc")
dp = Dispatcher(storage=MemoryStorage())
dp["repo"] = UserRepo()
dp.include_router(get_handlers_router())
def upd(t):
m = Message(message_id=1, date=datetime.now(),
chat=Chat(id=7, type="private"),
from_user=User(id=7, is_bot=False, first_name="T"), text=t)
return Update(update_id=1, message=m)
await dp.feed_update(bot, upd("/start"))
await dp.feed_update(bot, upd("salom"))
await bot.session.close()
assert ("start", 1) in OUT
assert ("echo", "salom") in OUT
print("OK", OUT)
asyncio.run(main())
14.
import asyncio
from datetime import datetime
from typing import Any, Awaitable, Callable
from aiogram import Bot, Dispatcher, Router, BaseMiddleware
from aiogram.filters import CommandStart
from aiogram.types import Update, Message, Chat, User, TelegramObject
class StructLogMiddleware(BaseMiddleware):
async def __call__(self, handler: Callable[[TelegramObject, dict], Awaitable[Any]],
event: TelegramObject, data: dict[str, Any]) -> Any:
data["request_id"] = "abc-123"
return await handler(event, data)
r = Router()
GOT = []
@r.message(CommandStart())
async def h(message: Message, request_id):
GOT.append(request_id)
async def main():
bot = Bot(token="123456:AAH-FakeTest_abc")
dp = Dispatcher()
r.message.middleware(StructLogMiddleware())
dp.include_router(r)
m = Message(message_id=1, date=datetime.now(), chat=Chat(id=1, type="private"),
from_user=User(id=1, is_bot=False, first_name="T"), text="/start")
await dp.feed_update(bot, Update(update_id=1, message=m))
await bot.session.close()
assert GOT == ["abc-123"]
print("OK middleware DI:", GOT)
asyncio.run(main())
data ga qo'shgan qiymat handlerga DI orqali yetadi.
15.
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
bot_token: str
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
# .env da: BOT_TOKEN=123456:AAH-FakeTest_abc
os.environ["BOT_TOKEN"] = "999:OVERRIDE"
s = Settings()
print(s.bot_token) # 999:OVERRIDE β muhit o'zgaruvchisi .env dan USTUN
.env faylsiz ham BOT_TOKEN ni muhit o'zgaruvchisi sifatida berib qo'yish mumkin β bu xavfsizroq.
β¬ οΈ Oldingi: 10 β Ma'lumotlar bazasi bilan ishlash Β· π README Β· Keyingi: 12 β Maxsus xususiyatlar β‘οΈ