114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate contracts/openstack/request_bodies/*.json for all pack write ops."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "tools"))
|
|
|
|
from os_api_inventory.request_body_catalog import schema_for_operation # noqa: E402
|
|
|
|
SERIES_DEFAULT = "dalmatian"
|
|
OUT_DIR = ROOT / "contracts" / "openstack" / "request_bodies"
|
|
|
|
|
|
def _load_ops(series: str) -> dict[str, list[dict]]:
|
|
series_dir = ROOT / "contracts" / "openstack" / series
|
|
by_service: dict[str, list[dict]] = {}
|
|
for api in sorted(series_dir.glob("*/api.json")):
|
|
data = json.loads(api.read_text())
|
|
service = str(data["service"])
|
|
writes = [
|
|
op
|
|
for op in data.get("operations") or []
|
|
if op.get("method") in {"POST", "PUT", "PATCH"}
|
|
]
|
|
by_service[service] = writes
|
|
return by_service
|
|
|
|
|
|
def generate(series: str, out_dir: Path) -> dict[str, int]:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
counts: dict[str, int] = {}
|
|
# Merge ops across series for max coverage of operation_ids.
|
|
merged: dict[str, dict[str, dict]] = defaultdict(dict)
|
|
for series_name in ("yoga", "antelope", "caracal", "dalmatian"):
|
|
if series and series != "all" and series_name != series:
|
|
continue
|
|
for service, ops in _load_ops(series_name).items():
|
|
for op in ops:
|
|
oid = op["operation_id"]
|
|
path_key = f"{op['method']} {op['path']}"
|
|
merged[service][oid] = op
|
|
# also keep path index later
|
|
_ = path_key
|
|
|
|
for service, by_oid in sorted(merged.items()):
|
|
operations: dict[str, dict] = {}
|
|
by_path: dict[str, dict] = {}
|
|
for oid, op in sorted(by_oid.items()):
|
|
schema = schema_for_operation(op)
|
|
operations[oid] = schema
|
|
by_path[f"{op['method']} {op['path']}"] = schema
|
|
payload = {
|
|
"service": service,
|
|
"source": "generated-from-api-ref-catalog",
|
|
"operations": operations,
|
|
"by_path": by_path,
|
|
}
|
|
path = out_dir / f"{service}.json"
|
|
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
counts[service] = len(operations)
|
|
return counts
|
|
|
|
|
|
def coverage_report(series: str) -> int:
|
|
from app.openstack.contract_loader import load_series_pack
|
|
from app.openstack.request_bodies import clear_request_body_cache, missing_write_schemas
|
|
|
|
clear_request_body_cache()
|
|
packs = load_series_pack(series)
|
|
missing = missing_write_schemas(packs)
|
|
print(f"series={series} missing={len(missing)}")
|
|
for service, method, path, oid in missing[:50]:
|
|
print(f" {service} {method} {path} ({oid})")
|
|
if len(missing) > 50:
|
|
print(f" ... and {len(missing) - 50} more")
|
|
return len(missing)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--series", default="all", help="Series to scan or 'all'")
|
|
parser.add_argument("--coverage", action="store_true", help="Report missing schemas only")
|
|
parser.add_argument(
|
|
"--coverage-series",
|
|
default=SERIES_DEFAULT,
|
|
help="Series for coverage check (default: dalmatian)",
|
|
)
|
|
args = parser.parse_args()
|
|
if args.coverage:
|
|
missing = coverage_report(args.coverage_series)
|
|
return 1 if missing else 0
|
|
series = None if args.series == "all" else args.series
|
|
counts = generate(series or "all", OUT_DIR)
|
|
total = sum(counts.values())
|
|
print(f"Wrote {len(counts)} services, {total} operation schemas → {OUT_DIR}")
|
|
for name, count in sorted(counts.items()):
|
|
print(f" {name}: {count}")
|
|
if args.series == "all":
|
|
# Coverage check requires the project venv (Python 3.13 dataclasses).
|
|
print("Run coverage with: python tools/os_api_inventory/generate_request_bodies.py --coverage")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|