初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
SOURCE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(SOURCE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from app.persistence import atomic_write_many_bytes
|
||||
|
||||
|
||||
def _read_json(path: Path) -> tuple[dict[str, Any], bytes]:
|
||||
content = path.read_bytes()
|
||||
value = json.loads(content.decode("utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} must contain a JSON object")
|
||||
return value, content
|
||||
|
||||
|
||||
def _json_bytes(value: dict[str, Any]) -> bytes:
|
||||
return (json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def build_migration(
|
||||
draft: dict[str, Any],
|
||||
old_catalog: dict[str, Any],
|
||||
old_catalog_bytes: bytes,
|
||||
new_catalog: dict[str, Any],
|
||||
new_catalog_bytes: bytes,
|
||||
*,
|
||||
excluded_copy_ids: set[str],
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
if old_catalog.get("schema_version") != 1:
|
||||
raise ValueError("old catalog must use schema v1")
|
||||
if new_catalog.get("schema_version") != 2:
|
||||
raise ValueError("new catalog must use schema v2")
|
||||
if draft.get("schema_version") != 1:
|
||||
raise ValueError("draft must use schema v1")
|
||||
old_digest = hashlib.sha256(old_catalog_bytes).hexdigest()
|
||||
if draft.get("base_catalog_digest") != old_digest:
|
||||
raise ValueError("draft does not match the supplied old catalog")
|
||||
overrides = draft.get("overrides")
|
||||
insertions = draft.get("insertions")
|
||||
if not isinstance(overrides, dict) or not isinstance(insertions, list):
|
||||
raise ValueError("draft collections are invalid")
|
||||
|
||||
new_items = new_catalog.get("items")
|
||||
new_anchors = new_catalog.get("anchors")
|
||||
if not isinstance(new_items, dict) or not isinstance(new_anchors, dict):
|
||||
raise ValueError("new catalog collections are invalid")
|
||||
|
||||
retained_overrides: dict[str, Any] = {}
|
||||
removed_overrides: list[dict[str, Any]] = []
|
||||
for copy_id, override in overrides.items():
|
||||
if copy_id in excluded_copy_ids:
|
||||
removed_overrides.append({"copy_id": copy_id, "reason": "explicitly_excluded", "override": override})
|
||||
elif copy_id not in new_items:
|
||||
removed_overrides.append({"copy_id": copy_id, "reason": "not_registered_in_v2", "override": override})
|
||||
else:
|
||||
retained_overrides[copy_id] = override
|
||||
|
||||
retained_insertions: list[dict[str, Any]] = []
|
||||
removed_insertions: list[dict[str, Any]] = []
|
||||
for insertion in insertions:
|
||||
anchor = new_anchors.get(insertion.get("anchor_id")) if isinstance(insertion, dict) else None
|
||||
if not isinstance(anchor, dict) or anchor.get("scope") != insertion.get("workspace_id"):
|
||||
removed_insertions.append({"reason": "anchor_not_registered_in_v2", "insertion": insertion})
|
||||
else:
|
||||
retained_insertions.append(insertion)
|
||||
|
||||
migrated_at = _now()
|
||||
new_digest = hashlib.sha256(new_catalog_bytes).hexdigest()
|
||||
migrated = {
|
||||
"schema_version": 1,
|
||||
"base_catalog_digest": new_digest,
|
||||
"revision": int(draft.get("revision", -1)) + 1,
|
||||
"updated_at": migrated_at,
|
||||
"overrides": retained_overrides,
|
||||
"insertions": retained_insertions,
|
||||
}
|
||||
report = {
|
||||
"schema_version": 1,
|
||||
"migrated_at": migrated_at,
|
||||
"from_catalog_digest": old_digest,
|
||||
"to_catalog_digest": new_digest,
|
||||
"from_revision": draft.get("revision"),
|
||||
"to_revision": migrated["revision"],
|
||||
"retained_override_ids": sorted(retained_overrides),
|
||||
"removed_overrides": removed_overrides,
|
||||
"retained_insertion_ids": [item.get("id") for item in retained_insertions],
|
||||
"removed_insertions": removed_insertions,
|
||||
}
|
||||
return migrated, report
|
||||
|
||||
|
||||
def migrate_files(
|
||||
draft_path: Path,
|
||||
old_catalog_path: Path,
|
||||
new_catalog_path: Path,
|
||||
report_path: Path,
|
||||
*,
|
||||
excluded_copy_ids: set[str],
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
draft, _draft_bytes = _read_json(draft_path)
|
||||
old_catalog, old_bytes = _read_json(old_catalog_path)
|
||||
new_catalog, new_bytes = _read_json(new_catalog_path)
|
||||
migrated, report = build_migration(
|
||||
draft,
|
||||
old_catalog,
|
||||
old_bytes,
|
||||
new_catalog,
|
||||
new_bytes,
|
||||
excluded_copy_ids=excluded_copy_ids,
|
||||
)
|
||||
atomic_write_many_bytes(
|
||||
(
|
||||
(draft_path, _json_bytes(migrated)),
|
||||
(report_path, _json_bytes(report)),
|
||||
)
|
||||
)
|
||||
return migrated, report
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Migrate a UI copy draft from catalog v1 to catalog v2")
|
||||
parser.add_argument("--draft", type=Path, required=True)
|
||||
parser.add_argument("--old-catalog", type=Path, required=True)
|
||||
parser.add_argument("--new-catalog", type=Path, required=True)
|
||||
parser.add_argument("--report", type=Path, required=True)
|
||||
parser.add_argument("--exclude-copy-id", action="append", default=[])
|
||||
args = parser.parse_args()
|
||||
migrated, report = migrate_files(
|
||||
args.draft,
|
||||
args.old_catalog,
|
||||
args.new_catalog,
|
||||
args.report,
|
||||
excluded_copy_ids=set(args.exclude_copy_id),
|
||||
)
|
||||
print(json.dumps({
|
||||
"revision": migrated["revision"],
|
||||
"retained_overrides": len(report["retained_override_ids"]),
|
||||
"removed_overrides": len(report["removed_overrides"]),
|
||||
}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user