migration: preserve screener and tracking slice
This commit is contained in:
@@ -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)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user