Files
Cloud-Tour-to-Libo/scripts/verify_graph_file_import.py

168 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Exercise a real JSON file through multipart + streaming validation.
Optional --import-falkor writes only a UUID-named local test graph and removes
it in finally. System project metadata/auth are test doubles; this is NOT a
production importer, nor evidence of a browser/proxy/production deployment test.
"""
from __future__ import annotations
import argparse
import asyncio
import json
from pathlib import Path
import resource
import shutil
import sys
import tempfile
import time
import uuid
from unittest.mock import patch
import httpx
from fastapi import FastAPI
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.api import graph_file_import as file_api
from app.api.plaza import map_pois
from app.auth import get_current_user
from app.config import settings
from app.project_context import ProjectContext
from app.project_lifecycle import _falkor_client
def graph_names(db) -> set[str]:
return {name.decode() if isinstance(name, bytes) else str(name) for name in db.list_graphs()}
async def verify(args):
if settings.falkordb_host not in {"localhost", "127.0.0.1", "::1"}:
raise RuntimeError("验证仅允许本机 FalkorDB,不连接生产服务器")
if args.report.exists():
raise FileExistsError("报告已存在,请选择新路径")
if args.schema.stat().st_size > file_api.SCHEMA_BYTES:
raise ValueError("Schema 超过大小上限")
schema = json.loads(args.schema.read_text(encoding="utf-8"))
scratch = "codex_upload_verify_" + uuid.uuid4().hex
metadata = {"project_id": scratch, "display_name": "城市知识图谱-贵州*贵阳(本地导入验证)",
"schema": schema, "spatial_map": {"enabled": True, "scope": "guizhou",
"region_name": "贵阳市", "region_adcode": "520100", "region_level": "city"}}
app = FastAPI()
app.include_router(file_api.router, prefix="/v1/admin")
# Isolated in-process test app only; never bypass the running app's login.
app.dependency_overrides[get_current_user] = lambda: {"username": "test_only", "roles": ["admin"]}
@app.get("/health")
async def health():
return {"ok": True}
report = {"file_bytes": args.data.stat().st_size, "metadata_and_auth": "isolated_test_doubles",
"transport": "HTTPX ASGI multipart", "falkordb_import": bool(args.import_falkor),
"temp_free_before_bytes": shutil.disk_usage(tempfile.gettempdir()).free}
created = False
db = _falkor_client() if args.import_falkor else None
existing_graphs = graph_names(db) if db else set()
if scratch in existing_graphs:
raise FileExistsError("临时图名冲突,未开始测试")
if db:
report["existing_graphs_before"] = len(existing_graphs)
real_validator = file_api.validate_graph_file
last_print = time.monotonic()
def validate(source, staging, body, progress):
def observed(phase, nodes, relations):
nonlocal last_print
size = staging.stat().st_size if staging.exists() else 0
report["peak_staging_bytes"] = max(report.get("peak_staging_bytes", 0), size)
report["last_progress"] = {"phase": phase, "nodes": nodes, "relations": relations,
"staging_bytes": size, "temp_free_bytes": shutil.disk_usage(tempfile.gettempdir()).free}
progress(phase, nodes, relations)
if time.monotonic() - last_print > 8:
print(json.dumps({"phase": phase, "nodes": nodes, "relations": relations}, ensure_ascii=False), flush=True)
last_print = time.monotonic()
return real_validator(source, staging, body, observed)
async def publish(payload, user, *, graph_importer):
nonlocal created
report["validated_counts"] = payload["counts"]
report["file_sha256"] = payload["file_sha256"]
if db:
created = True
counts = await asyncio.to_thread(graph_importer, scratch, payload["graph_data"])
report["actual_graph_counts"] = counts
assert counts["nodes"] == payload["counts"]["nodes"]
assert counts["relations"] == payload["counts"]["relations"]
return {"project": {"project_id": scratch}, "counts": payload["counts"]}
started = time.monotonic()
latencies = []
failure = None
try:
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://local-test", timeout=None) as client:
with args.data.open("rb") as source, patch.object(file_api, "validate_graph_file", side_effect=validate), patch(
"app.api.projects.provision_validated_project", side_effect=publish,
):
upload = asyncio.create_task(client.post(
"/v1/admin/projects/provision-file", data={"metadata": json.dumps(metadata, ensure_ascii=False)},
files={"file": (args.data.name, source, "application/json")},
))
while not upload.done():
t = time.monotonic()
health = await client.get("/health")
assert health.status_code == 200
latencies.append(round((time.monotonic() - t) * 1000, 2))
await asyncio.sleep(0.5)
response = await upload
report["status_code"] = response.status_code
if response.status_code != 200:
raise RuntimeError(response.text[:1500])
messages = [json.loads(line) for line in response.text.splitlines()]
report["response_events"] = len(messages)
if not messages or messages[-1]["event"] != "result":
raise RuntimeError(str(messages[-1] if messages else "没有返回结果"))
if db:
pois = await map_pois(context=ProjectContext("local_test", scratch, scratch), _user=None)
report["map_pois"] = pois["total"]
assert pois["source"] == "falkordb"
assert pois["total"] == report["validated_counts"]["map_pois"]
del pois
report["passed"] = True
except Exception as exc:
failure = exc
report.update(passed=False, error=str(exc)[:2000])
finally:
if db:
try:
if created and scratch in graph_names(db):
db.select_graph(scratch).delete()
final_graphs = graph_names(db)
if scratch in final_graphs:
raise RuntimeError("UUID 临时图清理失败")
if not existing_graphs.issubset(final_graphs):
raise RuntimeError("验证期间发现原有图谱缺失")
report.update(test_graph_removed=True, existing_graphs_preserved=len(existing_graphs))
finally:
db.close()
report.update(seconds=round(time.monotonic() - started, 2),
peak_rss_bytes=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * (1 if sys.platform == "darwin" else 1024),
health_checks=len(latencies), max_health_latency_ms=max(latencies, default=0),
temp_free_after_bytes=shutil.disk_usage(tempfile.gettempdir()).free)
args.report.parent.mkdir(parents=True, exist_ok=True)
with args.report.open("x", encoding="utf-8") as stream:
json.dump(report, stream, ensure_ascii=False, indent=2)
print(json.dumps(report, ensure_ascii=False, indent=2), flush=True)
if failure:
raise RuntimeError("验证未通过,原因及资源占用已写入报告") from failure
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--schema", type=Path, required=True)
parser.add_argument("--data", type=Path, required=True)
parser.add_argument("--report", type=Path, required=True)
parser.add_argument("--import-falkor", action="store_true")
asyncio.run(verify(parser.parse_args()))