feat(bot): модель данных, миграции Alembic, FSM в БД, TTL-чистка

- app/db/models.py: 13 таблиц (ARCHITECTURE §2.10); перечисления — VARCHAR + CHECK,
  JSON → JSONB на Postgres, BIGINT/INTEGER-ключи для SQLite
- миграция 0001 (автогенерация + ручная чистка: импорт sa.Text, дубли CHECK)
- миграции применяются при старте бота программно (app.db.migrate), script_location из пакета
- SqlStorage: FSM aiogram поверх fsm_state — состояние переживает рестарт (перенесено из #4)
- cleanup_loop: истёкшие черновики, снимки контейнеров и попытки входа старше суток
- тесты: upgrade/downgrade, дрейф моделей и миграций, CHECK, CASCADE, FSM, чистка

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 18:45:16 +03:00
co-authored by Claude Opus 5.5
parent b70797c805
commit 282920e06b
24 changed files with 1071 additions and 16 deletions
+35
View File
@@ -0,0 +1,35 @@
"""Async engine and session factory."""
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
SessionMaker = async_sessionmaker[AsyncSession]
def make_engine(url: str) -> AsyncEngine:
if url.startswith("sqlite"):
# In-memory SQLite lives per connection: share one connection so every session sees the same DB.
kwargs = (
{"poolclass": StaticPool, "connect_args": {"check_same_thread": False}}
if ":memory:" in url or url.endswith("://")
else {}
)
engine = create_async_engine(url, **kwargs)
event.listen(engine.sync_engine, "connect", _sqlite_foreign_keys)
return engine
return create_async_engine(url, pool_pre_ping=True)
def _sqlite_foreign_keys(dbapi_conn: object, _record: object) -> None:
# SQLite ignores FOREIGN KEY / ON DELETE CASCADE unless asked per connection
cursor = dbapi_conn.cursor() # type: ignore[attr-defined]
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
def make_sessionmaker(engine: AsyncEngine) -> SessionMaker:
return async_sessionmaker(engine, expire_on_commit=False)
__all__ = ["SessionMaker", "make_engine", "make_sessionmaker"]