Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
+102
View File
@@ -0,0 +1,102 @@
"""
Идемпотентная схема WESP 2.0: недостающие таблицы (ORM) + недостающие колонки.
Используется:
- ревизией Alembic wesp_2_0;
- после каждого upgrade head в create_app — чтобы починить БД после stamp / частичного прогона.
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
from alembic.operations import Operations
def _load_legacy_align():
root = Path(__file__).resolve().parent.parent / "migrations" / "legacy_orm_schema_align.py"
spec = importlib.util.spec_from_file_location("legacy_orm_schema_align", root)
if spec is None or spec.loader is None:
raise RuntimeError(f"Не найден {root}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _table_names_for_bind(*, reports_only: bool) -> set[str]:
from app import db # noqa: WPS433
import app.models # noqa: F401, WPS433
names: set[str] = set()
for mapper in db.Model.registry.mappers:
cls = mapper.class_
if cls.__dict__.get("__abstract__", False):
continue
bk = getattr(cls, "__bind_key__", None)
if reports_only:
if bk != "reports":
continue
else:
if bk == "reports":
continue
tab = getattr(cls, "__tablename__", None)
if tab:
names.add(tab)
return names
def run_wesp20_idempotent_sync(op: Operations, kw: dict) -> None:
from app import db # noqa: WPS433
import app.models # noqa: F401, WPS433
tag = kw.get("tag")
reports_only = tag == "reports"
bind = op.get_bind()
allowed = _table_names_for_bind(reports_only=reports_only)
# У Flask-SQLAlchemy отдельный MetaData на каждый bind; db.metadata — только основной.
meta = db.metadatas["reports"] if reports_only else db.metadatas[None]
from app.services.migration_pace import migration_pause, migration_pause_step
for table in meta.sorted_tables:
if table.name not in allowed:
continue
table.create(bind, checkfirst=True)
migration_pause_step(f"table:{table.name}")
_load_legacy_align().apply_legacy_orm_gaps(op, kw)
migration_pause("legacy_align")
if not reports_only:
repair_legacy_sync_client_snapshot_cursor(op)
migration_pause("sync_clients_repair")
def repair_legacy_sync_client_snapshot_cursor(op: Operations) -> int:
"""Старые узлы: personal_snapshot_cursor=0 при завершённом enqueue — новая логика ждёт cursor>=total."""
from sqlalchemy import inspect, text
from app.services.sync_manager import SNAPSHOT_MODELS
bind = op.get_bind()
if bind.dialect.name != "sqlite":
return 0
insp = inspect(bind)
if not insp.has_table("sync_clients"):
return 0
total = len(SNAPSHOT_MODELS)
result = bind.execute(
text(
"""
UPDATE sync_clients
SET personal_snapshot_cursor = :total
WHERE personal_snapshot_completed_at IS NOT NULL
AND COALESCE(personal_snapshot_cursor, 0) < :total
"""
),
{"total": total},
)
return int(getattr(result, "rowcount", 0) or 0)