Extract import sync preview service
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-17 22:10:06 +03:00
parent 8c19410da1
commit 6dd4d69163
3 changed files with 100 additions and 88 deletions
@@ -0,0 +1,25 @@
from __future__ import annotations
from pydantic import BaseModel, Field
class ImportSyncDiffItem(BaseModel):
qualified_name: str
name: str
object_kind: str
group_name: str | None = None
change_kind: str
before_hash: str | None = None
after_hash: str | None = None
class ImportSyncPreview(BaseModel):
mode: str = "SYNC_PREVIEW"
applied: bool = False
status: str = "preview_only"
message: str
added_count: int = 0
removed_count: int = 0
changed_count: int = 0
unchanged_count: int = 0
items: list[ImportSyncDiffItem] = Field(default_factory=list)
@@ -0,0 +1,73 @@
from __future__ import annotations
import json
from api_server.import_sync_models import ImportSyncDiffItem, ImportSyncPreview
from one_c_normalizer import NormalizedProject
from sir import stable_hash
def build_import_sync_preview(
current: NormalizedProject | None,
incoming: NormalizedProject | None,
) -> ImportSyncPreview:
current_index = normalized_object_hash_index(current)
incoming_index = normalized_object_hash_index(incoming)
items: list[ImportSyncDiffItem] = []
for qualified_name in sorted(set(current_index) | set(incoming_index)):
before = current_index.get(qualified_name)
after = incoming_index.get(qualified_name)
if before is None and after is not None:
change_kind = "ADD"
elif before is not None and after is None:
change_kind = "REMOVE"
elif before is not None and after is not None and before["hash"] != after["hash"]:
change_kind = "UPDATE"
else:
change_kind = "UNCHANGED"
source = after or before or {}
items.append(
ImportSyncDiffItem(
qualified_name=qualified_name,
name=str(source.get("name", qualified_name)),
object_kind=str(source.get("object_kind", "UNKNOWN")),
group_name=source.get("group_name"),
change_kind=change_kind,
before_hash=before["hash"] if before is not None else None,
after_hash=after["hash"] if after is not None else None,
)
)
added_count = sum(1 for item in items if item.change_kind == "ADD")
removed_count = sum(1 for item in items if item.change_kind == "REMOVE")
changed_count = sum(1 for item in items if item.change_kind == "UPDATE")
unchanged_count = sum(1 for item in items if item.change_kind == "UNCHANGED")
return ImportSyncPreview(
message=(
"Synchronization is not applied yet. This preview shows what would change; "
"use FULL_REPLACE to replace current project data."
),
added_count=added_count,
removed_count=removed_count,
changed_count=changed_count,
unchanged_count=unchanged_count,
items=[item for item in items if item.change_kind != "UNCHANGED"],
)
def normalized_object_hash_index(normalized: NormalizedProject | None) -> dict[str, dict]:
if normalized is None:
return {}
result: dict[str, dict] = {}
for group in normalized.configuration.groups:
for item in group.objects:
payload = item.model_dump(mode="json")
result[item.qualified_name] = {
"name": item.name,
"object_kind": item.object_kind,
"group_name": group.name,
"hash": stable_hash(json.dumps(payload, ensure_ascii=False, sort_keys=True)),
}
return result
+2 -88
View File
@@ -96,6 +96,8 @@ from api_server.html5_setup_controller import (
) )
from api_server.import_quality_models import ImportQualityResponse from api_server.import_quality_models import ImportQualityResponse
from api_server.import_quality_service import import_quality_response as _build_import_quality_response from api_server.import_quality_service import import_quality_response as _build_import_quality_response
from api_server.import_sync_models import ImportSyncPreview
from api_server.import_sync_service import build_import_sync_preview as _build_import_sync_preview
from api_server.metadata_tree_controller import ( from api_server.metadata_tree_controller import (
metadata_tree as _metadata_tree, metadata_tree as _metadata_tree,
metadata_tree_children as _metadata_tree_children, metadata_tree_children as _metadata_tree_children,
@@ -923,28 +925,6 @@ class ImportRequest(BaseModel):
mode: ImportMode = ImportMode.FULL_REPLACE mode: ImportMode = ImportMode.FULL_REPLACE
class ImportSyncDiffItem(BaseModel):
qualified_name: str
name: str
object_kind: str
group_name: str | None = None
change_kind: str
before_hash: str | None = None
after_hash: str | None = None
class ImportSyncPreview(BaseModel):
mode: ImportMode = ImportMode.SYNC_PREVIEW
applied: bool = False
status: str = "preview_only"
message: str
added_count: int = 0
removed_count: int = 0
changed_count: int = 0
unchanged_count: int = 0
items: list[ImportSyncDiffItem] = Field(default_factory=list)
class ImportSummary(BaseModel): class ImportSummary(BaseModel):
source: ImportSourceKind source: ImportSourceKind
mode: ImportMode = ImportMode.FULL_REPLACE mode: ImportMode = ImportMode.FULL_REPLACE
@@ -7249,72 +7229,6 @@ def _import_project_sync_preview(project_id: str, request: ImportRequest) -> Imp
return summary return summary
def _build_import_sync_preview(
current: NormalizedProject | None,
incoming: NormalizedProject | None,
) -> ImportSyncPreview:
current_index = _normalized_object_hash_index(current)
incoming_index = _normalized_object_hash_index(incoming)
items: list[ImportSyncDiffItem] = []
for qualified_name in sorted(set(current_index) | set(incoming_index)):
before = current_index.get(qualified_name)
after = incoming_index.get(qualified_name)
if before is None and after is not None:
change_kind = "ADD"
elif before is not None and after is None:
change_kind = "REMOVE"
elif before is not None and after is not None and before["hash"] != after["hash"]:
change_kind = "UPDATE"
else:
change_kind = "UNCHANGED"
source = after or before or {}
items.append(
ImportSyncDiffItem(
qualified_name=qualified_name,
name=str(source.get("name", qualified_name)),
object_kind=str(source.get("object_kind", "UNKNOWN")),
group_name=source.get("group_name"),
change_kind=change_kind,
before_hash=before["hash"] if before is not None else None,
after_hash=after["hash"] if after is not None else None,
)
)
added_count = sum(1 for item in items if item.change_kind == "ADD")
removed_count = sum(1 for item in items if item.change_kind == "REMOVE")
changed_count = sum(1 for item in items if item.change_kind == "UPDATE")
unchanged_count = sum(1 for item in items if item.change_kind == "UNCHANGED")
return ImportSyncPreview(
message=(
"Synchronization is not applied yet. This preview shows what would change; "
"use FULL_REPLACE to replace current project data."
),
added_count=added_count,
removed_count=removed_count,
changed_count=changed_count,
unchanged_count=unchanged_count,
items=[item for item in items if item.change_kind != "UNCHANGED"],
)
def _normalized_object_hash_index(normalized: NormalizedProject | None) -> dict[str, dict]:
if normalized is None:
return {}
result: dict[str, dict] = {}
for group in normalized.configuration.groups:
for item in group.objects:
payload = item.model_dump(mode="json")
result[item.qualified_name] = {
"name": item.name,
"object_kind": item.object_kind,
"group_name": group.name,
"hash": stable_hash(json.dumps(payload, ensure_ascii=False, sort_keys=True)),
}
return result
def _append_import_history(project_id: str, summary: ImportSummary) -> None: def _append_import_history(project_id: str, summary: ImportSummary) -> None:
state = _project_setup.setdefault(project_id, {}) state = _project_setup.setdefault(project_id, {})
history = list(state.get("import_history", [])) history = list(state.get("import_history", []))