72 lines
2.2 KiB
Python
72 lines
2.2 KiB
Python
"""Детерминированный 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)
|