migration: preserve screener and tracking slice

This commit is contained in:
leefer
2026-07-31 03:57:07 +08:00
parent cf2aad28ec
commit 4bab921d14
28 changed files with 4810 additions and 4152 deletions
+99 -10
View File
@@ -14,13 +14,14 @@ def request_json(
opener: urllib.request.OpenerDirector,
url: str,
payload: dict[str, Any] | None = None,
method: str = "GET",
) -> tuple[int, Any]:
data = None
headers = {"Accept": "application/json"}
if payload is not None:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
request = urllib.request.Request(url, data=data, headers=headers)
request = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with opener.open(request, timeout=90) as response:
return response.status, json.loads(response.read().decode("utf-8"))
@@ -36,9 +37,13 @@ def session(base_url: str, username: str, password: str) -> urllib.request.Opene
opener,
f"{base_url.rstrip('/')}/api/auth/login",
{"username": username, "password": password},
"POST",
)
if status != 200 or not body.get("ok"):
raise RuntimeError(f"Login failed for {base_url}: HTTP {status} {body}")
csrf_token = str(body.get("csrf_token") or "")
if csrf_token:
opener.addheaders.append(("X-CSRF-Token", csrf_token))
return opener
@@ -49,18 +54,74 @@ def digest(value: Any) -> str:
return hashlib.sha256(content).hexdigest()
def comparable(value: Any) -> Any:
def comparable(
value: Any,
excluded_paths: set[str] | None = None,
sorted_lists: dict[str, str] | None = None,
path: str = "$",
) -> Any:
excluded_paths = excluded_paths or set()
sorted_lists = sorted_lists or {}
if isinstance(value, dict):
return {
key: comparable(item)
key: comparable(
item,
excluded_paths,
sorted_lists,
f"{path}.{key}",
)
for key, item in value.items()
if key != "request_id"
and f"{path}.{key}" not in excluded_paths
}
if isinstance(value, list):
return [comparable(item) for item in value]
normalized = [
comparable(item, excluded_paths, sorted_lists, f"{path}[]")
for item in value
]
sort_key = sorted_lists.get(path)
if sort_key:
normalized.sort(
key=lambda item: (
str(item.get(sort_key) or "")
if isinstance(item, dict)
else json.dumps(item, ensure_ascii=False, sort_keys=True, default=str)
)
)
return normalized
return value
def first_difference(original: Any, migrated: Any, path: str = "$") -> dict[str, Any] | None:
if type(original) is not type(migrated):
return {"path": path, "original": original, "migrated": migrated}
if isinstance(original, dict):
for key in sorted(set(original) | set(migrated)):
if key not in original or key not in migrated:
return {
"path": f"{path}.{key}",
"original": original.get(key, "<missing>"),
"migrated": migrated.get(key, "<missing>"),
}
difference = first_difference(original[key], migrated[key], f"{path}.{key}")
if difference:
return difference
return None
if isinstance(original, list):
if len(original) != len(migrated):
return {"path": f"{path}.length", "original": len(original), "migrated": len(migrated)}
for index, (original_item, migrated_item) in enumerate(zip(original, migrated)):
difference = first_difference(
original_item, migrated_item, f"{path}[{index}]"
)
if difference:
return difference
return None
if original != migrated:
return {"path": path, "original": original, "migrated": migrated}
return None
def main() -> None:
parser = argparse.ArgumentParser(description="Compare authenticated preservation APIs")
parser.add_argument("--original", required=True)
@@ -68,32 +129,60 @@ def main() -> None:
parser.add_argument("--username", required=True)
parser.add_argument("--password", required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("endpoints", nargs="+")
parser.add_argument("--requests-file", type=Path)
parser.add_argument("endpoints", nargs="*")
args = parser.parse_args()
original = session(args.original, args.username, args.password)
migrated = session(args.migrated, args.username, args.password)
rows = []
all_equal = True
for endpoint in args.endpoints:
requests = [
{"name": endpoint, "method": "GET", "endpoint": endpoint, "payload": None}
for endpoint in args.endpoints
]
if args.requests_file:
requests.extend(json.loads(args.requests_file.read_text(encoding="utf-8")))
if not requests:
parser.error("provide at least one endpoint or --requests-file")
for item in requests:
endpoint = str(item["endpoint"])
method = str(item.get("method") or "GET").upper()
payload = item.get("payload")
excluded_paths = {str(path) for path in item.get("exclude_paths") or []}
sorted_lists = {
str(path): str(key)
for path, key in (item.get("sort_lists") or {}).items()
}
original_status, original_body = request_json(
original, f"{args.original.rstrip('/')}{endpoint}"
original, f"{args.original.rstrip('/')}{endpoint}", payload, method
)
migrated_status, migrated_body = request_json(
migrated, f"{args.migrated.rstrip('/')}{endpoint}"
migrated, f"{args.migrated.rstrip('/')}{endpoint}", payload, method
)
original_comparable = comparable(
original_body, excluded_paths, sorted_lists
)
migrated_comparable = comparable(
migrated_body, excluded_paths, sorted_lists
)
original_comparable = comparable(original_body)
migrated_comparable = comparable(migrated_body)
equal = original_status == migrated_status and original_comparable == migrated_comparable
all_equal = all_equal and equal
rows.append(
{
"name": str(item.get("name") or endpoint),
"method": method,
"endpoint": endpoint,
"original_status": original_status,
"migrated_status": migrated_status,
"original_sha256": digest(original_comparable),
"migrated_sha256": digest(migrated_comparable),
"equal": equal,
"first_difference": (
None
if equal
else first_difference(original_comparable, migrated_comparable)
),
}
)