Интегрирован 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
+289
View File
@@ -0,0 +1,289 @@
from datetime import datetime
from sqlalchemy import event
from sqlalchemy.orm import Session
# Импортируем модели, чтобы метаданные SQLAlchemy были доступны при инициализации.
from .component import Component, Ingredient # noqa: F401
from .recipe import Recipe, UnloadingGroup, PeriodRecipe # noqa: F401
from .report import ( # noqa: F401
LoadingReport,
LoadingReportComponent,
ComponentLoadingTime,
UnloadingReport,
UnloadingReportGroup,
)
from .feed_alert import FeedAlert # noqa: F401
from .feed_quality_settings import FeedQualitySettings # noqa: F401
from .org_settings import OrgSettings # noqa: F401
from .daily_component_norm_adjustment import DailyComponentNormAdjustment # noqa: F401
from .daily_trip_skip import DailyTripSkip # noqa: F401
from .daily_ingredient_skip import DailyIngredientSkip # noqa: F401
from .daily_ingredient_replacement import DailyIngredientReplacement # noqa: F401
from .daily_unloading_group_skip import DailyUnloadingGroupSkip # noqa: F401
from .equipment import ( # noqa: F401
FeedMixer,
FeedDispenser,
FeedingLocation,
FeedingPeriod,
FeedingPoint,
Trip,
)
from .sklad import ComponentStock # noqa: F401
from .hardware_setting import HardwareSetting # noqa: F401
from .kiosk import KioskDevice, KioskPairToken # noqa: F401
from .user import WebUser # noqa: F401
from .auto_update_settings import AutoUpdateSettings # noqa: F401
from .zootech_notification import ZootechNotification # noqa: F401
from .sync import ( # noqa: F401
SyncMetadata,
SyncClient,
SyncClientDisplayName,
SyncEngineState,
SyncQueue,
SyncDelivery,
SyncConflict,
)
from .base import TimestampMixin, SoftDeleteMixin # noqa: F401
def _cascade_on_parent_soft_delete(mapper, connection, target) -> None: # type: ignore[override]
if not getattr(target, "is_deleted", False):
return
table = getattr(target, "__tablename__", "")
if not table:
return
from sqlalchemy.orm import object_session
from app.services.sync_cascade import CASCADE_CHILDREN, cascade_soft_delete
if table not in CASCADE_CHILDREN:
return
sess = object_session(target)
suppress_enqueue = bool(
sess is not None and sess.info.get(WESP_SUPPRESS_SYNC_ENQUEUE)
)
cascade_soft_delete(
table,
str(getattr(target, "id", "")),
deleted_by=str(getattr(target, "deleted_by", None) or f"cascade:{table}"),
enqueue=not suppress_enqueue,
defer_enqueue=True,
)
for _parent_table in (
"recipe",
"component",
"feed_dispenser",
"feeding_period",
"feed_mixer",
"loading_report",
"unloading_report",
):
_parent_model = {
"recipe": Recipe,
"component": Component,
"feed_dispenser": FeedDispenser,
"feeding_period": FeedingPeriod,
"feed_mixer": FeedMixer,
"loading_report": LoadingReport,
"unloading_report": UnloadingReport,
}[_parent_table]
event.listen(_parent_model, "after_update", _cascade_on_parent_soft_delete)
def _sync_record_id(target) -> str:
table = getattr(target, "__tablename__", "")
if table == "period_recipes":
return f"{target.period_id}:{target.recipe_id}"
return target.id
_SYNC_ENQUEUE_INFO_KEY = "_wesp_sync_enqueue_pending"
# session.info: применение pull на клиенте (sync_client.apply_batch) — не ставить задачи в sync_queue.
WESP_SUPPRESS_SYNC_ENQUEUE = "_wesp_suppress_sync_enqueue"
def _schedule_sync_enqueue_from_listener(target, action: str, priority: int) -> None:
"""Не вызывать enqueue_sync_queue_task из after_insert/after_update — это внутри flush."""
from sqlalchemy.orm import object_session
sess = object_session(target)
if sess is not None and sess.info.get(WESP_SUPPRESS_SYNC_ENQUEUE):
return
rid = _sync_record_id(target)
row = (target.__tablename__, str(rid), action, priority, None)
if sess is None:
from app.services.sync_manager import enqueue_sync_queue_task
tname, rec_id, act, pri, tid = row
enqueue_sync_queue_task(tname, rec_id, act, priority=pri, target_node_id=tid)
return
sess.info.setdefault(_SYNC_ENQUEUE_INFO_KEY, []).append(row)
@event.listens_for(Session, "after_flush")
def _flush_pending_sync_enqueues(session, flush_context) -> None: # type: ignore[override]
from app.services.sync_cascade import CASCADE_ENQUEUE_INFO_KEY
pending = session.info.pop(_SYNC_ENQUEUE_INFO_KEY, None)
cascade_pending = session.info.pop(CASCADE_ENQUEUE_INFO_KEY, None)
from app.services.sync_manager import enqueue_sync_queue_task
if cascade_pending:
merged_cascade: set[tuple[str, str]] = set()
for tname, rid in cascade_pending:
merged_cascade.add((tname, rid))
with session.no_autoflush:
for tname, rid in merged_cascade:
enqueue_sync_queue_task(tname, rid, "update", priority=1)
if not pending:
return
merged: dict[tuple[str, str, str, str | None], tuple] = {}
for table_name, record_id, action, priority, target_node_id in pending:
key = (table_name, record_id, action, target_node_id)
pri = int(priority)
prev = merged.get(key)
if prev is None or pri > prev[3]:
merged[key] = (table_name, record_id, action, pri, target_node_id)
with session.no_autoflush:
for tname, rec_id, act, pri, tid in merged.values():
enqueue_sync_queue_task(tname, rec_id, act, priority=pri, target_node_id=tid)
def _sync_task_after_insert(mapper, connection, target): # type: ignore[override]
"""Универсальная постановка в sync_queue после insert (паритет с legacy sync_models)."""
priority_map = {
"component": 2,
"recipe": 2,
"ingredient": 3,
"unloading_group": 4,
"loading_report": 4,
"loading_report_component": 4,
"component_loading_time": 4,
"unloading_report": 4,
"unloading_report_group": 4,
"feed_mixer": 3,
"feeding_location": 3,
"feeding_period": 3,
"feeding_point": 3,
"trip": 4,
"feed_dispenser": 3,
"period_recipes": 3,
"daily_trip_skip": 2,
"daily_ingredient_skip": 2,
"daily_unloading_group_skip": 2,
"daily_ingredient_replacement": 2,
"daily_component_norm_adjustment": 2,
}
priority = priority_map.get(target.__tablename__, 3)
_schedule_sync_enqueue_from_listener(target, "create", priority)
def _sync_task_after_update(mapper, connection, target): # type: ignore[override]
# soft_delete / каскад: update в очередь не ставим — delete или cascade_soft_delete уже поставили задачи
if getattr(target, "is_deleted", False):
return
priority_map = {
"component": 3,
"recipe": 3,
"ingredient": 4,
"unloading_group": 4,
"loading_report": 4,
"loading_report_component": 4,
"component_loading_time": 4,
"unloading_report": 4,
"unloading_report_group": 4,
"feed_mixer": 4,
"feeding_location": 4,
"feeding_period": 4,
"feeding_point": 4,
"trip": 4,
"feed_dispenser": 4,
"period_recipes": 4,
"daily_trip_skip": 3,
"daily_ingredient_skip": 3,
"daily_unloading_group_skip": 3,
"daily_ingredient_replacement": 3,
"daily_component_norm_adjustment": 3,
}
priority = priority_map.get(target.__tablename__, 4)
_schedule_sync_enqueue_from_listener(target, "update", priority)
_sync_models = (
Component,
Recipe,
Ingredient,
UnloadingGroup,
LoadingReport,
LoadingReportComponent,
ComponentLoadingTime,
UnloadingReport,
UnloadingReportGroup,
FeedMixer,
FeedingLocation,
FeedingPeriod,
FeedingPoint,
Trip,
FeedDispenser,
PeriodRecipe,
DailyTripSkip,
DailyIngredientSkip,
DailyUnloadingGroupSkip,
DailyIngredientReplacement,
DailyComponentNormAdjustment,
)
def _model_has_content_hash_column(cls) -> bool:
try:
return "content_hash" in cls.__table__.columns
except Exception:
return False
def _sync_content_hash_after_insert(mapper, connection, target) -> None: # type: ignore[override]
"""После INSERT строка уже содержит значения по умолчанию из БД — хеш совпадает с get_object_data."""
from sqlalchemy import and_, update
from app.services.sync_content_hash import compute_content_hash_for_object
if not hasattr(target, "content_hash"):
return
h = compute_content_hash_for_object(target)
if not h:
return
table = mapper.local_table
pk_cols = list(table.primary_key.columns)
stmt = update(table).values(content_hash=h)
if len(pk_cols) == 1:
pk = pk_cols[0]
stmt = stmt.where(pk == getattr(target, pk.key))
else:
stmt = stmt.where(and_(*(c == getattr(target, c.key) for c in pk_cols)))
connection.execute(stmt)
target.content_hash = h
for _sm in _sync_models:
if _model_has_content_hash_column(_sm):
event.listen(_sm, "after_insert", _sync_content_hash_after_insert)
for _sm in _sync_models:
event.listen(_sm, "after_insert", _sync_task_after_insert)
event.listen(_sm, "after_update", _sync_task_after_update)
@event.listens_for(Session, "before_flush")
def _wesp_recompute_content_hashes_before_flush(session, flush_context, instances) -> None: # type: ignore[override]
from app.services.sync_content_hash import refresh_content_hash_if_applicable
for obj in list(session.dirty):
if hasattr(obj, "content_hash"):
refresh_content_hash_if_applicable(obj)