v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""Окружение Alembic. URL берётся из настроек, metadata — из SQLModel."""
|
||||
from __future__ import annotations
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# Регистрируем все модели в SQLModel.metadata.
|
||||
import app.models # noqa: F401
|
||||
|
||||
config = context.config
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = SQLModel.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
context.configure(
|
||||
url=settings.database_url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
render_as_batch=True, # обязательно для SQLite (ALTER через copy-and-recreate)
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
render_as_batch=True,
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,25 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
${imports if imports else ""}
|
||||
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Начальная схема (все таблицы v1).
|
||||
|
||||
Создаём схему из метаданных SQLModel — гарантирует точное соответствие моделям
|
||||
для greenfield-проекта. Последующие изменения схемы — обычными op.* миграциями.
|
||||
|
||||
Revision ID: 0001_initial
|
||||
Revises:
|
||||
Create Date: 2026-06-15
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from alembic import op
|
||||
|
||||
# Импорт моделей наполняет SQLModel.metadata.
|
||||
import app.models # noqa: F401
|
||||
|
||||
revision: str = "0001_initial"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
SQLModel.metadata.create_all(bind)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
SQLModel.metadata.drop_all(bind)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Сидинг справочников: дополнения и фракции (идемпотентно).
|
||||
|
||||
Revision ID: 0002_seed_reference
|
||||
Revises: 0001_initial
|
||||
Create Date: 2026-06-15
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from sqlmodel import Session
|
||||
|
||||
from alembic import op
|
||||
from app.seed.reference_data import seed_reference_data
|
||||
|
||||
revision: str = "0002_seed_reference"
|
||||
down_revision: Union[str, None] = "0001_initial"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
with Session(bind=bind) as session:
|
||||
seed_reference_data(session)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
op.execute("DELETE FROM factions")
|
||||
op.execute("DELETE FROM expansions")
|
||||
_ = bind
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Жизненный цикл партии: статус, тайминг, длительность, причина победы; place → nullable.
|
||||
|
||||
Идемпотентна: на свежей БД (0001 создаёт схему из актуальных моделей через create_all)
|
||||
столбцы уже есть → no-op; на существующей БД (ревизия 0002, старая схема) — добавляет
|
||||
столбцы и делает place nullable. render_as_batch включён в env.py (нужен для SQLite).
|
||||
|
||||
Revision ID: 0003_match_lifecycle
|
||||
Revises: 0002_seed_reference
|
||||
Create Date: 2026-06-15
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0003_match_lifecycle"
|
||||
down_revision: Union[str, None] = "0002_seed_reference"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
insp = inspect(bind)
|
||||
|
||||
match_cols = {c["name"] for c in insp.get_columns("matches")}
|
||||
add = []
|
||||
if "status" not in match_cols:
|
||||
add.append(sa.Column("status", sa.String(16), nullable=False, server_default="finished"))
|
||||
if "started_at" not in match_cols:
|
||||
add.append(sa.Column("started_at", sa.DateTime(), nullable=True))
|
||||
if "finished_at" not in match_cols:
|
||||
add.append(sa.Column("finished_at", sa.DateTime(), nullable=True))
|
||||
if "duration_minutes" not in match_cols:
|
||||
add.append(sa.Column("duration_minutes", sa.Integer(), nullable=True))
|
||||
if "win_reason" not in match_cols:
|
||||
add.append(sa.Column("win_reason", sa.String(16), nullable=True))
|
||||
|
||||
if add:
|
||||
with op.batch_alter_table("matches") as b:
|
||||
for col in add:
|
||||
b.add_column(col)
|
||||
# Бэкфилл для существующих (уже завершённых) партий.
|
||||
op.execute("UPDATE matches SET status = 'finished' WHERE status IS NULL")
|
||||
op.execute("UPDATE matches SET started_at = created_at WHERE started_at IS NULL")
|
||||
op.execute("UPDATE matches SET finished_at = created_at WHERE finished_at IS NULL")
|
||||
|
||||
# place → nullable, если ещё NOT NULL.
|
||||
place_col = next(c for c in insp.get_columns("match_participants") if c["name"] == "place")
|
||||
if not place_col["nullable"]:
|
||||
with op.batch_alter_table("match_participants") as b:
|
||||
b.alter_column("place", existing_type=sa.Integer(), nullable=True)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("matches") as b:
|
||||
for name in ("win_reason", "duration_minutes", "finished_at", "started_at", "status"):
|
||||
try:
|
||||
b.drop_column(name)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
Reference in New Issue
Block a user