262 lines
9.6 KiB
Python
262 lines
9.6 KiB
Python
"""Build UI catalog / method payloads from OpenStack contract packs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from app.contracts.examples import path_param_example
|
|
from app.openstack.contract_loader import (
|
|
ensure_loaded,
|
|
load_series_pack,
|
|
major_for_series,
|
|
series_for_major,
|
|
)
|
|
from app.openstack.request_examples import body_fields_from_example, schema_example
|
|
|
|
_PATH_PARAM = re.compile(r"\{([^{}]+)\}")
|
|
|
|
|
|
def openstack_catalog_payload(major: int) -> dict[str, Any]:
|
|
series = series_for_major(major)
|
|
ensure_loaded(series)
|
|
packs = load_series_pack(series)
|
|
categories: list[dict[str, Any]] = []
|
|
path_count = 0
|
|
method_count = 0
|
|
for name, pack in sorted(packs.items(), key=lambda item: item[0]):
|
|
by_path: dict[str, list[dict[str, Any]]] = {}
|
|
for op in pack.operations:
|
|
by_path.setdefault(op.path, []).append(
|
|
{
|
|
"verb": op.method,
|
|
"name": op.operation_id,
|
|
"description": op.notes or f"{op.kind} {op.resource_type}",
|
|
"protected": op.requires_auth,
|
|
"implemented": True,
|
|
}
|
|
)
|
|
paths = [
|
|
{"path": path, "methods": methods}
|
|
for path, methods in sorted(by_path.items(), key=lambda item: item[0])
|
|
]
|
|
path_count += len(paths)
|
|
method_count += sum(len(item["methods"]) for item in paths)
|
|
categories.append({"tag": name, "paths": paths})
|
|
return {
|
|
"major": major,
|
|
"series": {
|
|
"yoga": "Yoga",
|
|
"antelope": "Antelope",
|
|
"caracal": "Caracal",
|
|
"dalmatian": "Dalmatian",
|
|
}.get(series, series.title()),
|
|
"source_version": f"openstack-{series}",
|
|
"latest_version": series,
|
|
"artifact_url": f"contracts/openstack/{series}",
|
|
"bundled": True,
|
|
"path_count": path_count,
|
|
"method_count": method_count,
|
|
"categories": categories,
|
|
"catalog_kind": "openstack",
|
|
}
|
|
|
|
|
|
def openstack_method_payload(
|
|
*,
|
|
major: int,
|
|
path: str,
|
|
verb: str,
|
|
runtime_version: str | None,
|
|
) -> dict[str, Any]:
|
|
series = series_for_major(major)
|
|
packs = load_series_pack(series)
|
|
verb_u = verb.upper()
|
|
for pack in packs.values():
|
|
for op in pack.operations:
|
|
if op.path == path and op.method == verb_u:
|
|
path_params = _PATH_PARAM.findall(path)
|
|
path_fields = [
|
|
{
|
|
"name": name,
|
|
"type": "string",
|
|
"description": f"Path parameter {name}",
|
|
"optional": False,
|
|
"enum": [],
|
|
"example": path_param_example(name) or name,
|
|
}
|
|
for name in path_params
|
|
]
|
|
body_fields: list[dict[str, Any]] = []
|
|
body_example: dict[str, Any] = {}
|
|
if op.method in {"POST", "PUT", "PATCH"}:
|
|
if op.request_schema:
|
|
example = schema_example(op.request_schema)
|
|
body_example = example if isinstance(example, dict) else {}
|
|
# PARAM drawer: nested leaves from body_example (oVirt-style).
|
|
body_fields = body_fields_from_example(body_example)
|
|
else:
|
|
# Schemas are required for write ops; keep a minimal
|
|
# envelope only as a last-resort safety net.
|
|
key = op.item_key or op.collection_key or "resource"
|
|
if op.kind == "action":
|
|
action = op.action_name or "os-start"
|
|
body_example = {action: None}
|
|
body_fields = body_fields_from_example(body_example)
|
|
else:
|
|
body_example = {key: {"name": "example"}}
|
|
body_fields = body_fields_from_example(body_example)
|
|
return {
|
|
"major": major,
|
|
"series": series,
|
|
"path": path,
|
|
"verb": verb_u,
|
|
"name": op.operation_id,
|
|
"description": op.notes or f"{pack.name} {op.resource_type}",
|
|
"protected": op.requires_auth,
|
|
"implemented": True,
|
|
"runtime_version": runtime_version,
|
|
"path_fields": path_fields,
|
|
"query_fields": [
|
|
{
|
|
"name": "limit",
|
|
"type": "integer",
|
|
"description": "Max items",
|
|
"optional": True,
|
|
"enum": [],
|
|
"example": 25,
|
|
},
|
|
{
|
|
"name": "marker",
|
|
"type": "string",
|
|
"description": "Pagination marker (id)",
|
|
"optional": True,
|
|
"enum": [],
|
|
"example": "",
|
|
},
|
|
]
|
|
if op.method == "GET" and op.kind in {"collection", "detail"}
|
|
else [],
|
|
"body_fields": body_fields,
|
|
"body_example": body_example,
|
|
"returns": {"type": "object"},
|
|
"permissions": [],
|
|
"service": pack.name,
|
|
"port": pack.port,
|
|
}
|
|
raise KeyError(f"{verb} {path}")
|
|
|
|
|
|
def openstack_series_majors(runtime_version: str | None = None) -> dict[str, object]:
|
|
from app.openstack.contract_loader import list_series
|
|
|
|
series = list_series()
|
|
return {
|
|
"runtime_version": runtime_version,
|
|
"majors": [
|
|
{
|
|
"major": item["major"],
|
|
"series": str(item["series"]).title(),
|
|
"latest_version": item["series"],
|
|
"artifact_url": f"contracts/openstack/{item['series']}",
|
|
"bundled": True,
|
|
"operation_count": item["operation_count"],
|
|
"microversions": item.get("microversions") or [],
|
|
}
|
|
for item in sorted(series, key=lambda row: row["major"])
|
|
],
|
|
}
|
|
|
|
|
|
def openstack_compatibility_payload(
|
|
major: int,
|
|
*,
|
|
runtime_version: str | None = None,
|
|
schema_ops_mounted: int | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Compatibility summary from OpenStack pack ops (surface-complete packs)."""
|
|
|
|
series = series_for_major(major)
|
|
packs = load_series_pack(series)
|
|
# Count every pack operation (same basis as pack operation_count). Path+/verb
|
|
# alone is not unique across services (e.g. GET /v1).
|
|
method_names: list[str] = []
|
|
groups: dict[str, dict[str, int]] = {}
|
|
for name, pack in sorted(packs.items(), key=lambda item: item[0]):
|
|
counters = groups.setdefault(name, {"declared": 0, "implemented": 0, "verified": 0})
|
|
for op in pack.operations:
|
|
method_names.append(f"{op.method.upper()} [{name}] {op.path}")
|
|
counters["declared"] += 1
|
|
# Pack operations are mounted via specialized routers + schema engine.
|
|
counters["implemented"] += 1
|
|
|
|
method_names.sort()
|
|
total = len(method_names)
|
|
score = 1.0 if total else 1.0
|
|
mounted = schema_ops_mounted if schema_ops_mounted is not None else total
|
|
|
|
methods_by_verb: dict[str, int] = {}
|
|
for name, pack in packs.items():
|
|
_ = name
|
|
for op in pack.operations:
|
|
verb = op.method.upper()
|
|
methods_by_verb[verb] = methods_by_verb.get(verb, 0) + 1
|
|
|
|
def _level(count: int, methods: list[str] | None = None) -> dict[str, Any]:
|
|
return {
|
|
"count": count,
|
|
"score": (count / total) if total else 1.0,
|
|
"methods": methods if methods is not None else [],
|
|
}
|
|
|
|
# Compact method samples for UI (full list is large).
|
|
sample = method_names[:40]
|
|
|
|
return {
|
|
"source_version": f"openstack-{series}",
|
|
"catalog_version": f"openstack-{series}",
|
|
"latest_version": series,
|
|
"major": major,
|
|
"series": series,
|
|
"catalog_kind": "openstack",
|
|
"runtime_version": runtime_version or f"openstack-{series}",
|
|
"evidence_scope": "catalog",
|
|
"total_declared": total,
|
|
"schema_ops_mounted": mounted,
|
|
"service_count": len(packs),
|
|
"methods_by_verb": dict(sorted(methods_by_verb.items())),
|
|
"levels": {
|
|
"declared": _level(total, sample),
|
|
"schema_only": _level(0),
|
|
"implemented": _level(total, sample),
|
|
"observed": _level(0),
|
|
"verified": _level(0),
|
|
},
|
|
"groups": dict(sorted(groups.items())),
|
|
"dimension_groups": {},
|
|
"classifications": {
|
|
# Store counts only — length of full method lists is expensive in the UI.
|
|
"fully_compatible_count": total,
|
|
"partially_compatible_count": 0,
|
|
"incompatible_count": 0,
|
|
"regressions_count": 0,
|
|
"unsupported_count": 0,
|
|
"fully_compatible": [],
|
|
"partially_compatible": [],
|
|
"incompatible": [],
|
|
"regressions": [],
|
|
"unsupported": [],
|
|
},
|
|
"dimensions": {
|
|
"route_method": {
|
|
"count": total,
|
|
"score": score,
|
|
"methods": sample,
|
|
}
|
|
},
|
|
}
|
|
|
|
|
|
# silence unused import warning helpers
|
|
_ = major_for_series
|