183 lines
5.8 KiB
Python
183 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Read-only smoke check for the catalog-first D-InSAR architecture.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Any, Dict
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
|
|
def _project_root() -> str:
|
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
PROJECT_ROOT = _project_root()
|
|
if PROJECT_ROOT not in sys.path:
|
|
sys.path.insert(0, PROJECT_ROOT)
|
|
|
|
|
|
from backend.app import database
|
|
from backend.app.config import ensure_project_env_loaded, settings
|
|
from backend.app.db_maintenance import ensure_database_ready
|
|
from backend.app.models import AiDiagnosisORM, HazardPointORM, ResultProductORM
|
|
from backend.app.services.dinsar_read_service import dinsar_read_service
|
|
from backend.app.services.health_service import get_health_status
|
|
from backend.app.services.spatial_service import spatial_service
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run a read-only smoke check for D-InSAR catalog/compat integration."
|
|
)
|
|
parser.add_argument(
|
|
"--skip-maintenance",
|
|
action="store_true",
|
|
help="Do not run database self-maintenance before the smoke check.",
|
|
)
|
|
parser.add_argument(
|
|
"--include-external",
|
|
action="store_true",
|
|
help="Include external checks such as nginx and Ollama.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-refresh",
|
|
action="store_true",
|
|
help="Do not force a schema refresh inside health_service.",
|
|
)
|
|
parser.add_argument(
|
|
"--compact",
|
|
action="store_true",
|
|
help="Print compact JSON instead of pretty JSON.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def _print_json(payload: Dict[str, Any], compact: bool) -> None:
|
|
if compact:
|
|
print(json.dumps(payload, ensure_ascii=False, default=str))
|
|
return
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
|
|
|
|
|
|
async def _run_smoke(include_external: bool, refresh: bool) -> Dict[str, Any]:
|
|
session_factory = database.AsyncSessionLocal
|
|
if session_factory is None:
|
|
raise RuntimeError("AsyncSessionLocal is not initialized.")
|
|
|
|
health = await get_health_status(
|
|
include_external=include_external,
|
|
include_details=True,
|
|
full=True,
|
|
refresh=refresh,
|
|
)
|
|
|
|
async with session_factory() as db:
|
|
catalog_records = await dinsar_read_service.list_catalog_records(db)
|
|
compat_count = await dinsar_read_service.count_compat_records(db)
|
|
compat_records = await dinsar_read_service.list_compat_records(db, limit=5, offset=0)
|
|
|
|
diagnosis_total = int(
|
|
(await db.execute(select(func.count(AiDiagnosisORM.id)))).scalar_one() or 0
|
|
)
|
|
hazard_id = (
|
|
await db.execute(
|
|
select(HazardPointORM.id).order_by(HazardPointORM.id.asc()).limit(1)
|
|
)
|
|
).scalar_one_or_none()
|
|
spatial_count = None
|
|
if hazard_id is not None:
|
|
spatial_records = await spatial_service.find_dinsar_results_near_hazard(
|
|
db,
|
|
int(hazard_id),
|
|
)
|
|
spatial_count = len(spatial_records)
|
|
|
|
product_count = int(
|
|
(
|
|
await db.execute(
|
|
select(func.count(ResultProductORM.id)).where(
|
|
ResultProductORM.catalog_name == "dinsar"
|
|
)
|
|
)
|
|
).scalar_one()
|
|
or 0
|
|
)
|
|
|
|
return {
|
|
"health_ok": bool(health.get("ok")),
|
|
"database": {
|
|
"ok": bool(health.get("database", {}).get("ok")),
|
|
"schema_ok": bool(health.get("database", {}).get("schema_ok")),
|
|
"postgis_ok": bool(health.get("database", {}).get("postgis_ok")),
|
|
},
|
|
"worker": {
|
|
"ok": bool(health.get("worker", {}).get("ok")),
|
|
"worker_count": int(health.get("worker", {}).get("worker_count") or 0),
|
|
},
|
|
"dinsar_result_catalog": health.get("dinsar_result_catalog", {}),
|
|
"dinsar_bridge": health.get("dinsar_bridge", {}),
|
|
"source_roots": health.get("source_roots", {}),
|
|
"reads": {
|
|
"catalog_records": len(catalog_records),
|
|
"compat_records": compat_count,
|
|
"compat_preview_records": len(compat_records),
|
|
"sample_public_ids": [
|
|
int(record.compat_row.id)
|
|
for record in compat_records
|
|
if getattr(record, "compat_row", None) is not None
|
|
][:5],
|
|
},
|
|
"diagnosis_total": diagnosis_total,
|
|
"spatial_count_for_first_hazard": spatial_count,
|
|
"product_count_direct": product_count,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
ensure_project_env_loaded()
|
|
database_url = settings.DATABASE_URL
|
|
if not database_url:
|
|
print("[ERROR] DATABASE_URL not found in .env")
|
|
return 1
|
|
|
|
maintenance = None
|
|
if not args.skip_maintenance:
|
|
maintenance = ensure_database_ready(
|
|
database_url,
|
|
bootstrap_admin=True,
|
|
seed_hazard=True,
|
|
)
|
|
|
|
database.init_db()
|
|
if database.AsyncSessionLocal is None:
|
|
print("[ERROR] Failed to initialize async database session factory.")
|
|
return 1
|
|
|
|
payload = {
|
|
"maintenance": maintenance,
|
|
"smoke": asyncio.run(
|
|
_run_smoke(
|
|
include_external=args.include_external,
|
|
refresh=not args.no_refresh,
|
|
)
|
|
),
|
|
}
|
|
_print_json(payload, args.compact)
|
|
|
|
smoke = payload["smoke"]
|
|
root_blocked = int(smoke.get("source_roots", {}).get("inaccessible_count") or 0) > 0
|
|
worker_missing = not bool(smoke.get("worker", {}).get("ok"))
|
|
return 2 if root_blocked or worker_missing else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|