7 documents covering stack, integrations, architecture, structure, conventions, testing, and concerns.
6.9 KiB
Coding Conventions
Analysis Date: 2026-04-01
Linting and Formatting
Tool: ruff (configured in pyproject.toml)
Settings:
- Line length: 100 characters
- Target: Python 3.11
- Active rule sets:
E(pycodestyle errors),F(pyflakes),I(isort),UP(pyupgrade),B(bugbear)
Type checking: mypy (available as dev dependency; not enforced in CI at this time)
Run linting:
ruff check .
ruff format .
File Naming
- Module files:
snake_case.py(e.g.,room_router.py,test_dispatcher.py) - Each module starts with a comment declaring its path:
# core/handler.py - Test files:
test_<module>.py(e.g.,test_store.py,test_converter.py) - No index/barrel files except
__init__.pyfor package registration
Class Naming
PascalCasefor all classes (e.g.,EventDispatcher,MockPlatformClient,MatrixBot)- Protocol/interface classes named after the capability:
StateStore,PlatformClient,WebhookReceiver - Manager classes suffixed with
Manager:ChatManager,AuthManager,SettingsManager - Dataclasses follow the same
PascalCaserule:IncomingMessage,OutgoingUI,MatrixRuntime
Function and Method Naming
snake_casefor all functions and methods- Private helpers prefixed with single underscore:
_to_dict,_from_dict,_routing_key,_latency - Handler functions named
handle_<action>:handle_start,handle_message,handle_new_chat - Builder functions named
build_<thing>:build_runtime,build_event_dispatcher,build_skills_text - Converter functions named
from_<source>:from_room_event,from_command,from_reaction - Predicate functions named
is_<state>:is_authenticated,is_new
Variable Naming
snake_casefor all variables and parameters- Internal state attributes prefixed with
_:self._store,self._platform,self._handlers - Store key prefixes are module-level constants in
UPPER_SNAKE_CASE:ROOM_META_PREFIX = "matrix_room:" USER_META_PREFIX = "matrix_user:" - Constants for reaction strings are module-level:
CONFIRM_REACTION = "👍",PLATFORM = "matrix"
Type Annotations
All files use from __future__ import annotations at the top for deferred evaluation.
Annotation style:
- Use built-in generics (
list[str],dict[str, Any]) — notList,Dictfromtyping - Union types written with
|:str | None,IncomingCallback | None - Type aliases at module level:
IncomingEvent = IncomingMessage | IncomingCommand | IncomingCallback - Callable types use
typing.Callableandtyping.Awaitable:HandlerFn = Callable[..., Awaitable[list[OutgoingEvent]]] - Handler functions use loose
listreturn type without generics (consistent acrosscore/handlers/) - Protocol classes use
...as body for abstract methods:async def get(self, key: str) -> dict | None: ...
Pydantic vs dataclasses:
core/protocol.py— plain@dataclasswithfield(default_factory=...)for mutable defaultssdk/interface.py— PydanticBaseModelfor all SDK-facing models (User,MessageResponse,UserSettings)- Choose
@dataclassfor internal protocol structs,BaseModelfor SDK boundary models
Import Organization
Order (enforced by ruff I rules):
from __future__ import annotations- Standard library imports (grouped)
- Third-party imports (grouped)
- Local imports from project packages (grouped)
Example from adapter/matrix/bot.py:
from __future__ import annotations
import asyncio
import os
from dataclasses import dataclass
from pathlib import Path
import structlog
from nio import AsyncClient, ...
from dotenv import load_dotenv
from adapter.matrix.converter import from_reaction, from_room_event
from core.auth import AuthManager
from core.protocol import OutgoingEvent, ...
from sdk.mock import MockPlatformClient
No relative imports; all imports use absolute package paths from the project root.
Async Patterns
All I/O methods are async def. There are no sync wrappers around async code.
Handler signature pattern (used uniformly across core/handlers/):
async def handle_<action>(event: IncomingEvent, auth_mgr, platform, chat_mgr, settings_mgr) -> list:
Note: manager parameters are untyped in handler signatures (accepted as **kwargs at call site in EventDispatcher.dispatch).
Awaiting store calls:
stored = await self._store.get(f"auth:{user_id}")
await self._store.set(f"auth:{user_id}", _to_dict(flow))
SQLiteStore uses sync sqlite3 inside async def methods — blocking I/O is not off-loaded to a thread executor. This is a known limitation (see CONCERNS.md).
Mock latency simulation:
await self._latency(200, 600) # min_ms, max_ms
Logging
Library: structlog
Pattern:
import structlog
logger = structlog.get_logger(__name__)
logger.info("Chat created", chat_id=chat_id, user_id=user_id)
logger.warning("No handler registered", event_type=event_type.__name__, key=key)
- Always pass structured keyword arguments — never use f-strings in log calls
- Logger created at module level with
structlog.get_logger(__name__)
Error Handling
- Raise
ValueErrorfor invalid domain state (e.g., chat not found inChatManager.rename) sdk/interface.pydefinesPlatformError(Exception)with acodefield for SDK-level errors- Handler functions never raise — they return
[]or a fallbackOutgoingMessage - No
try/exceptblocks in core handlers; errors from the platform are expected to propagate
Comments
- Module-level comment declaring file path at top:
# core/handler.py - Docstrings for classes with non-obvious behavior:
class MockPlatformClient: """ Заглушка SDK платформы Lambda. ... """ - Inline comments for non-obvious blocks:
# Scan by chat_id suffix when user_id unknown (slower) - Comments in Russian are normal and acceptable throughout the codebase
Serialization Pattern
Dataclasses are serialized/deserialized via private module-level functions, not class methods:
def _to_dict(ctx: ChatContext) -> dict:
return { "chat_id": ctx.chat_id, ... }
def _from_dict(d: dict) -> ChatContext:
return ChatContext(chat_id=d["chat_id"], ...)
This pattern is used in core/auth.py and core/chat.py. Follow this pattern for any new manager that persists to StateStore.
Module Design
- No barrel
__init__.pyexports exceptcore/handlers/__init__.pywhich exposesregister_all - Manager classes take
(platform, store)as constructor args;platformis often stored asobjector not stored at all if unused @dataclassis preferred for plain data containers, not NamedTuple or TypedDict- Store key namespacing follows
<namespace>:<user_id>:<entity_id>pattern:"chat:u1:C1","auth:u1","matrix_room:!r:m.org"
Convention analysis: 2026-04-01