Extract import sync preview service
This commit is contained in:
@@ -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
|
||||||
@@ -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", []))
|
||||||
|
|||||||
Reference in New Issue
Block a user