@@ -0,0 +1,71 @@
|
||||
"""Детерминированный content_hash для сущностей с полем content_hash (sync pull/push)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
# Не входят в хеш: версия/метаданные синка и аудит (иначе хеш «прыгает» без смены данных).
|
||||
_EXCLUDED_FROM_HASH = frozenset(
|
||||
{
|
||||
"version",
|
||||
"content_hash",
|
||||
"sync_timestamp",
|
||||
"sync_status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"client_id",
|
||||
"server_synced",
|
||||
"is_deleted",
|
||||
"deleted_at",
|
||||
"deleted_by",
|
||||
"deleted_reason",
|
||||
"restored_at",
|
||||
"restored_by",
|
||||
"restored_reason",
|
||||
"delete_restore_count",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def stable_payload_for_hash(data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in data.items() if k not in _EXCLUDED_FROM_HASH}
|
||||
|
||||
|
||||
def compute_content_hash_hex(payload: dict[str, Any]) -> str:
|
||||
raw = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str).encode("utf-8")
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def compute_content_hash_for_object(obj: Any) -> str:
|
||||
from app.services.sync_record_data import get_object_data
|
||||
|
||||
data = get_object_data(obj)
|
||||
if not isinstance(data, dict):
|
||||
return ""
|
||||
stable = stable_payload_for_hash(data)
|
||||
return compute_content_hash_hex(stable)
|
||||
|
||||
|
||||
def refresh_content_hash_if_applicable(obj: Any) -> None:
|
||||
if not hasattr(obj, "content_hash"):
|
||||
return
|
||||
new_hash = compute_content_hash_for_object(obj)
|
||||
if new_hash and getattr(obj, "content_hash", None) != new_hash:
|
||||
obj.content_hash = new_hash
|
||||
|
||||
|
||||
def refresh_content_hashes_for_session(session) -> None:
|
||||
"""Вызывать из before_flush: новые и изменённые строки с content_hash."""
|
||||
seen: set[int] = set()
|
||||
for obj in list(session.new) + list(session.dirty):
|
||||
oid = id(obj)
|
||||
if oid in seen:
|
||||
continue
|
||||
seen.add(oid)
|
||||
if not hasattr(obj, "content_hash"):
|
||||
continue
|
||||
refresh_content_hash_if_applicable(obj)
|
||||
Reference in New Issue
Block a user