Files
imagefind/scripts/live-pagination-acceptance.py

163 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Read-only live acceptance for upload pagination and core API latency."""
from __future__ import annotations
import argparse
import json
import statistics
import sys
import time
from pathlib import Path
from typing import Any
import httpx
def call(client: httpx.Client, path: str) -> tuple[Any, float]:
started = time.perf_counter()
response = client.get(path)
elapsed_ms = (time.perf_counter() - started) * 1000
if response.is_error:
raise RuntimeError(f"GET {path} -> {response.status_code}: {response.text[:500]}")
return response.json(), elapsed_ms
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True)
auth = parser.add_mutually_exclusive_group(required=True)
auth.add_argument("--token-file", type=Path)
auth.add_argument("--password-stdin", action="store_true")
parser.add_argument("--report", type=Path)
args = parser.parse_args()
headers: dict[str, str] = {}
if args.token_file:
headers["Authorization"] = f"Bearer {args.token_file.read_text(encoding='utf-8').strip()}"
client = httpx.Client(
base_url=args.base_url.rstrip("/"),
headers=headers,
timeout=httpx.Timeout(30, connect=10),
)
if args.password_stdin:
password = sys.stdin.readline().rstrip("\r\n")
login = client.post(
"/api/v1/auth/login",
json={"password": password, "remember_device": False},
)
password = ""
if login.is_error:
raise RuntimeError(f"login -> {login.status_code}: {login.text[:500]}")
status, status_ms = call(client, "/api/v1/status")
if status.get("version") != "0.5.45" or not status.get("configured"):
raise AssertionError(f"unexpected status: {status}")
legacy, legacy_ms = call(client, "/api/v1/uploads?limit=4")
if not isinstance(legacy, list) or len(legacy) > 4:
raise AssertionError("legacy upload response is no longer a bounded array")
first, first_ms = call(client, "/api/v1/uploads?page=1&page_size=10")
required = {
"items",
"status_items",
"active_count",
"failed_count",
"page",
"page_size",
"total",
"pages",
}
if not isinstance(first, dict) or not required.issubset(first):
actual = sorted(first) if isinstance(first, dict) else type(first)
raise AssertionError(f"upload page contract mismatch: {actual}")
if first["page"] != 1 or first["page_size"] != 10 or len(first["items"]) > 10:
raise AssertionError("upload first page bounds are invalid")
if first["pages"] < 1 or first["total"] < len(first["items"]):
raise AssertionError("upload pagination totals are invalid")
order = [(str(item.get("created_at") or ""), str(item["id"])) for item in first["items"]]
if order != sorted(order, reverse=True):
raise AssertionError("upload page ordering is unstable")
allowed_statuses = {"receiving", "queued", "transferring", "indexing", "failed"}
if any(item.get("status") not in allowed_statuses for item in first["status_items"]):
raise AssertionError("upload status summary contains terminal history")
active_visible = sum(item.get("status") != "failed" for item in first["status_items"])
failed_visible = sum(item.get("status") == "failed" for item in first["status_items"])
if first["active_count"] < active_visible or first["failed_count"] < failed_visible:
raise AssertionError("upload status counters are smaller than their visible summaries")
second_ids: list[str] = []
second_ms: float | None = None
if first["pages"] > 1:
second, second_ms = call(client, "/api/v1/uploads?page=2&page_size=10")
second_ids = [str(item["id"]) for item in second["items"]]
if set(second_ids) & {str(item["id"]) for item in first["items"]}:
raise AssertionError("upload pages overlap")
latency_paths = {
"uploads": "/api/v1/uploads?page=1&page_size=10",
"videos": "/api/v1/videos?limit=20",
"profile": "/api/v1/profile",
"resources": "/api/v1/system/resources",
}
latency: dict[str, dict[str, float]] = {}
resource_state: dict[str, Any] = {}
for name, path in latency_paths.items():
samples: list[float] = []
value: Any = None
for _ in range(3):
value, elapsed_ms = call(client, path)
samples.append(elapsed_ms)
latency[name] = {
"median_ms": round(statistics.median(samples), 2),
"max_ms": round(max(samples), 2),
}
if name == "resources" and isinstance(value, dict):
database = value.get("database") or {}
resource_state = {
"cpu_percent": value.get("cpu_percent"),
"memory_available_bytes": value.get("memory_available_bytes"),
"running_jobs": value.get("running_jobs"),
"paused_jobs": value.get("paused_jobs"),
"database": {
"engine": database.get("engine"),
"pool_in_use": database.get("pool_in_use"),
"pool_waiters": database.get("pool_waiters"),
"slow_transaction_count": database.get("slow_transaction_count"),
},
}
report = {
"status": status,
"contract": {
"legacy_count": len(legacy),
"page": first["page"],
"page_size": first["page_size"],
"page_items": len(first["items"]),
"total": first["total"],
"pages": first["pages"],
"second_page_items": len(second_ids),
"status_items": len(first["status_items"]),
"active_count": first["active_count"],
"failed_count": first["failed_count"],
},
"initial_latency_ms": {
"status": round(status_ms, 2),
"legacy_uploads": round(legacy_ms, 2),
"first_page": round(first_ms, 2),
"second_page": round(second_ms, 2) if second_ms is not None else None,
},
"latency": latency,
"resources": resource_state,
}
encoded = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(encoded, encoding="utf-8")
print(encoded, end="")
if __name__ == "__main__":
main()