| β¬ οΈ Back | π Docs Root |
Every feature follows a standardized structure. This document describes the anatomy of a feature and the role of each file.
features/{feature_name}/
βββ feature_setting.py # Feature manifest (States, GC, Menu, Factory)
βββ menu.py # Menu button config (optional)
βββ handlers/
β βββ __init__.py # Must export: router
β βββ handlers.py # aiogram handlers (thin, no logic)
βββ logic/
β βββ __init__.py
β βββ orchestrator.py # Business logic coordinator
β βββ state_manager.py # Draft storage (optional, for multi-step forms)
βββ contracts/
β βββ __init__.py
β βββ contract.py # Protocol interface for data access
βββ ui/
β βββ __init__.py
β βββ ui.py # Pure rendering (data β ViewResultDTO)
βββ resources/
β βββ __init__.py
β βββ texts.py # Text constants
β βββ callbacks.py # CallbackData classes
β βββ keyboards.py # Keyboard builders
β βββ formatters.py # Data formatters
βββ services/ # Feature-specific services (optional)
βββ ...
This is the featureβs declaration file. FeatureDiscoveryService reads it at startup.
from aiogram.fsm.state import State, StatesGroup
# 1. States
class MyFeatureStates(StatesGroup):
main = State()
editing = State()
STATES = MyFeatureStates
# 2. Garbage Collector
GARBAGE_COLLECT = True # Auto-register all STATES
# OR: GARBAGE_STATES = [MyFeatureStates.editing] # Explicit list
# OR: GARBAGE_COLLECT = False # Disabled
# 3. Menu Config (optional β only if feature appears in dashboard)
MENU_CONFIG = {
"key": "my_feature",
"text": "β¨ My Feature",
"description": "Short description for dashboard",
"target_state": "my_feature",
"priority": 50, # Lower = higher in menu
"is_admin": False, # Requires owner role
"is_superuser": False, # Requires developer role
}
# 4. Factory (DI)
def create_orchestrator(container):
# Choose data provider based on mode (API vs Direct)
data_provider = container.some_client
ui = MyFeatureUI()
return MyFeatureOrchestrator(provider=data_provider, ui=ui)
Telegram Update
β Middleware (inject container)
β Handler (thin routing layer)
β Orchestrator (business logic)
β Contract (data provider: API or DB)
β UI (pure rendering)
β UnifiedViewDTO
β ViewSender (send/edit messages)
β Telegram API
Contracts define what data the orchestrator needs, not how to get it:
class MyDataProvider(Protocol):
async def get_items(self, user_id: int) -> list[Item]: ...
async def create_item(self, data: ItemDTO) -> Item: ...
API Mode: Implemented by an HTTP client (calls FastAPI). Direct Mode: Implemented by a repository (calls SQLAlchemy).
The orchestrator receives the contract via DI and does not know which mode is active.
Pure transformation functions. No side effects, no API calls:
class MyFeatureUI:
def render_main(self, items: list[Item]) -> ViewResultDTO:
text = format_items(items)
kb = build_items_keyboard(items)
return ViewResultDTO(text=text, kb=kb)
Every handler follows the same structure:
@router.callback_query(MyCallback.filter(F.action == "action"))
async def handle_action(call, callback_data, state, container):
await call.answer()
# 1. Get orchestrator from container
orchestrator = container.features["my_feature"]
# 2. Set up Director for navigation
director = Director(container, state, call.from_user.id)
orchestrator.set_director(director)
# 3. Call business logic
view_dto = await orchestrator.handle_action(callback_data)
# 4. Send response
state_data = await state.get_data()
sender = ViewSender(call.bot, state, state_data, call.from_user.id)
await sender.send(view_dto)
Handlers contain zero business logic. All decisions happen in the orchestrator.
python -m src.telegram_bot.manage create_feature my_feature
feature_setting.py with States, GC, Menu, Factoryhandlers/__init__.py that exports routerINSTALLED_FEATURES in settings.py