generated from coulomb/repo-seed
408 lines
13 KiB
Python
408 lines
13 KiB
Python
import json
|
|
import re
|
|
from typing import Annotated, Any
|
|
|
|
from fastapi import APIRouter, Depends, Query, Request, status
|
|
from fastapi.responses import PlainTextResponse, RedirectResponse
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core_hub.crypto import new_api_key
|
|
from core_hub.db import session_scope
|
|
from core_hub.models import (
|
|
ApiConsumer,
|
|
ApiKey,
|
|
Hub,
|
|
HubCapabilityManifest,
|
|
InteractionEvent,
|
|
Widget,
|
|
)
|
|
from core_hub.schemas import CatalogItem
|
|
from core_hub.security import require_api_token
|
|
from core_hub.seeds import (
|
|
ANNOTATION_CATEGORIES,
|
|
EVENT_TYPES,
|
|
POLICY_SCOPES,
|
|
WIDGET_TYPES,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/v2", tags=["api-v2"])
|
|
TokenDependency = Annotated[str, Depends(require_api_token)]
|
|
SessionDependency = Annotated[AsyncSession, Depends(session_scope)]
|
|
|
|
|
|
def slugify(value: str) -> str:
|
|
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
return slug or "resource"
|
|
|
|
|
|
def encode_body(payload: dict[str, Any]) -> str:
|
|
return json.dumps(payload, sort_keys=True)
|
|
|
|
|
|
def decode_body(raw: str | None) -> dict[str, Any]:
|
|
if not raw:
|
|
return {}
|
|
value = json.loads(raw)
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
def page(data: list[dict[str, Any]]) -> dict[str, Any]:
|
|
return {"data": data, "count": len(data)}
|
|
|
|
|
|
def serialize_hub(row: Hub) -> dict[str, Any]:
|
|
payload = decode_body(row.body_json)
|
|
payload.update(
|
|
{
|
|
"id": row.id,
|
|
"slug": row.slug,
|
|
"name": row.name,
|
|
"domain": row.domain,
|
|
"hubKind": row.hub_kind,
|
|
"hubFamily": row.hub_family,
|
|
"vsmFunction": row.vsm_function,
|
|
"vsmSystem": row.vsm_system,
|
|
"status": row.status,
|
|
}
|
|
)
|
|
if row.description is not None:
|
|
payload["description"] = row.description
|
|
return {key: value for key, value in payload.items() if value is not None}
|
|
|
|
|
|
def serialize_manifest(row: HubCapabilityManifest) -> dict[str, Any]:
|
|
payload = decode_body(row.body_json)
|
|
payload.update(
|
|
{
|
|
"id": row.id,
|
|
"hubId": row.hub_id,
|
|
"hubSlug": row.hub_slug,
|
|
"manifestVersion": row.manifest_version,
|
|
"status": row.status,
|
|
}
|
|
)
|
|
return {key: value for key, value in payload.items() if value is not None}
|
|
|
|
|
|
def serialize_consumer(row: ApiConsumer) -> dict[str, Any]:
|
|
payload = decode_body(row.body_json)
|
|
payload.update(
|
|
{
|
|
"id": row.id,
|
|
"slug": row.slug,
|
|
"name": row.name,
|
|
"description": row.description,
|
|
"hubCapabilityManifestId": row.hub_capability_manifest_id,
|
|
"rateLimitPerMinute": row.rate_limit_per_minute,
|
|
"quotaPerDay": row.quota_per_day,
|
|
"keyPrefix": row.key_prefix,
|
|
"status": row.status,
|
|
}
|
|
)
|
|
return {key: value for key, value in payload.items() if value is not None}
|
|
|
|
|
|
def serialize_widget(row: Widget) -> dict[str, Any]:
|
|
payload = decode_body(row.body_json)
|
|
payload.update(
|
|
{
|
|
"id": row.id,
|
|
"hubId": row.hub_id,
|
|
"name": row.name,
|
|
"widgetType": row.widget_type,
|
|
"capabilityRef": row.capability_ref,
|
|
"viewContext": row.view_context,
|
|
"policyScope": row.policy_scope,
|
|
"status": row.status,
|
|
}
|
|
)
|
|
return {key: value for key, value in payload.items() if value is not None}
|
|
|
|
|
|
def serialize_event(row: InteractionEvent) -> dict[str, Any]:
|
|
payload = decode_body(row.body_json)
|
|
payload.update(
|
|
{
|
|
"id": row.id,
|
|
"widgetId": row.widget_id,
|
|
"eventType": row.event_type,
|
|
"viewContext": row.view_context,
|
|
"metadata": decode_body(row.metadata_json),
|
|
}
|
|
)
|
|
return {key: value for key, value in payload.items() if value is not None}
|
|
|
|
|
|
def compatibility_openapi(request: Request) -> dict[str, Any]:
|
|
document = request.app.openapi()
|
|
paths = document.setdefault("paths", {})
|
|
for path, operations in list(paths.items()):
|
|
if path.startswith("/api/v2/"):
|
|
paths.setdefault(path.removeprefix("/api/v2"), operations)
|
|
return document
|
|
|
|
|
|
@router.get("/hubs")
|
|
async def list_hubs(_: TokenDependency, session: SessionDependency) -> dict[str, Any]:
|
|
result = await session.execute(select(Hub).order_by(Hub.slug))
|
|
return page([serialize_hub(row) for row in result.scalars()])
|
|
|
|
|
|
@router.post("/hubs", status_code=status.HTTP_201_CREATED)
|
|
async def create_hub(
|
|
body: dict[str, Any], _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
row = Hub(
|
|
slug=body["slug"],
|
|
name=body["name"],
|
|
domain=body.get("domain"),
|
|
hub_kind=body.get("hubKind"),
|
|
hub_family=body.get("hubFamily"),
|
|
vsm_function=body.get("vsmFunction"),
|
|
vsm_system=body.get("vsmSystem"),
|
|
status=body.get("status", "active"),
|
|
description=body.get("description"),
|
|
body_json=encode_body(body),
|
|
)
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return serialize_hub(row)
|
|
|
|
|
|
@router.get("/hub-capability-manifests")
|
|
async def list_hub_capability_manifests(
|
|
_: TokenDependency,
|
|
session: SessionDependency,
|
|
hub_id: Annotated[str | None, Query(alias="hubId")] = None,
|
|
) -> dict[str, Any]:
|
|
statement = select(HubCapabilityManifest).order_by(HubCapabilityManifest.created_at)
|
|
if hub_id:
|
|
statement = statement.where(HubCapabilityManifest.hub_id == hub_id)
|
|
result = await session.execute(statement)
|
|
return page([serialize_manifest(row) for row in result.scalars()])
|
|
|
|
|
|
@router.post("/hub-capability-manifests", status_code=status.HTTP_201_CREATED)
|
|
async def create_hub_capability_manifest(
|
|
body: dict[str, Any], _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
hub_id = body.get("hubId")
|
|
hub_slug = None
|
|
if hub_id:
|
|
hub = await session.get(Hub, hub_id)
|
|
hub_slug = hub.slug if hub else None
|
|
row = HubCapabilityManifest(
|
|
hub_id=hub_id,
|
|
hub_slug=hub_slug,
|
|
manifest_version=body.get("manifestVersion", "0.1.0"),
|
|
status=body.get("status", "draft"),
|
|
body_json=encode_body(body),
|
|
)
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return serialize_manifest(row)
|
|
|
|
|
|
@router.patch("/hub-capability-manifests/{manifest_id}")
|
|
async def update_hub_capability_manifest(
|
|
manifest_id: str, body: dict[str, Any], _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
row = await session.get(HubCapabilityManifest, manifest_id)
|
|
if row is None:
|
|
return {"id": manifest_id, "status": "missing"}
|
|
payload = decode_body(row.body_json)
|
|
payload.update(body)
|
|
row.manifest_version = payload.get("manifestVersion", row.manifest_version)
|
|
row.body_json = encode_body(payload)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return serialize_manifest(row)
|
|
|
|
|
|
@router.post("/hub-capability-manifests/{manifest_id}/activate")
|
|
async def activate_hub_capability_manifest(
|
|
manifest_id: str, _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
row = await session.get(HubCapabilityManifest, manifest_id)
|
|
if row is None:
|
|
return {"id": manifest_id, "status": "missing"}
|
|
row.status = "active"
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return serialize_manifest(row)
|
|
|
|
|
|
@router.get("/api-consumers")
|
|
async def list_api_consumers(_: TokenDependency, session: SessionDependency) -> dict[str, Any]:
|
|
result = await session.execute(select(ApiConsumer).order_by(ApiConsumer.name))
|
|
return page([serialize_consumer(row) for row in result.scalars()])
|
|
|
|
|
|
@router.post("/api-consumers", status_code=status.HTTP_201_CREATED)
|
|
async def create_api_consumer(
|
|
body: dict[str, Any], _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
row = ApiConsumer(
|
|
slug=body.get("slug") or slugify(body["name"]),
|
|
name=body["name"],
|
|
description=body.get("description"),
|
|
hub_capability_manifest_id=body.get("hubCapabilityManifestId"),
|
|
rate_limit_per_minute=body.get("rateLimitPerMinute"),
|
|
quota_per_day=body.get("quotaPerDay"),
|
|
status=body.get("status", "active"),
|
|
body_json=encode_body(body),
|
|
)
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return serialize_consumer(row)
|
|
|
|
|
|
@router.post("/api-consumers/{consumer_id}/api-keys", status_code=status.HTTP_201_CREATED)
|
|
async def create_api_key(
|
|
consumer_id: str, body: dict[str, Any], _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
full_key, key_prefix, key_hash = new_api_key()
|
|
row = ApiKey(
|
|
api_consumer_id=consumer_id,
|
|
key_prefix=key_prefix,
|
|
key_hash=key_hash,
|
|
scopes=body.get("scopes"),
|
|
)
|
|
session.add(row)
|
|
consumer = await session.get(ApiConsumer, consumer_id)
|
|
if consumer:
|
|
consumer.key_prefix = key_prefix
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return {
|
|
"fullKey": full_key,
|
|
"apiKey": {
|
|
"id": row.id,
|
|
"apiConsumerId": consumer_id,
|
|
"keyPrefix": key_prefix,
|
|
"scopes": row.scopes,
|
|
"status": row.status,
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/widgets")
|
|
async def list_widgets(_: TokenDependency, session: SessionDependency) -> dict[str, Any]:
|
|
result = await session.execute(select(Widget).order_by(Widget.name))
|
|
return page([serialize_widget(row) for row in result.scalars()])
|
|
|
|
|
|
@router.post("/widgets", status_code=status.HTTP_201_CREATED)
|
|
async def create_widget(
|
|
body: dict[str, Any], _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
row = Widget(
|
|
hub_id=body["hubId"],
|
|
name=body["name"],
|
|
widget_type=body.get("widgetType"),
|
|
capability_ref=body.get("capabilityRef"),
|
|
view_context=body.get("viewContext"),
|
|
policy_scope=body.get("policyScope"),
|
|
status=body.get("status", "active"),
|
|
body_json=encode_body(body),
|
|
)
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return serialize_widget(row)
|
|
|
|
|
|
@router.get("/interaction-events")
|
|
async def list_interaction_events(_: TokenDependency, session: SessionDependency) -> dict[str, Any]:
|
|
result = await session.execute(select(InteractionEvent).order_by(InteractionEvent.created_at))
|
|
return page([serialize_event(row) for row in result.scalars()])
|
|
|
|
|
|
@router.post("/interaction-events", status_code=status.HTTP_201_CREATED)
|
|
async def create_interaction_event(
|
|
body: dict[str, Any], _: TokenDependency, session: SessionDependency
|
|
) -> dict[str, Any]:
|
|
row = InteractionEvent(
|
|
widget_id=body["widgetId"],
|
|
event_type=body["eventType"],
|
|
view_context=body.get("viewContext"),
|
|
metadata_json=encode_body(body.get("metadata", {})),
|
|
body_json=encode_body(body),
|
|
)
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return serialize_event(row)
|
|
|
|
|
|
@router.get("/hub-registry")
|
|
async def hub_registry(_: TokenDependency, session: SessionDependency) -> dict[str, Any]:
|
|
hubs = await list_hubs(_, session)
|
|
manifests = await list_hub_capability_manifests(_, session)
|
|
return {"data": {"hubs": hubs["data"], "hubCapabilityManifests": manifests["data"]}}
|
|
|
|
|
|
@router.get("/annotations")
|
|
@router.post("/annotations", status_code=status.HTTP_201_CREATED)
|
|
@router.get("/requirement-candidates")
|
|
@router.post("/requirement-candidates", status_code=status.HTTP_201_CREATED)
|
|
@router.get("/decision-records")
|
|
@router.post("/decision-records", status_code=status.HTTP_201_CREATED)
|
|
@router.get("/deployment-records")
|
|
@router.post("/deployment-records", status_code=status.HTTP_201_CREATED)
|
|
@router.get("/outcome-signals")
|
|
@router.post("/outcome-signals", status_code=status.HTTP_201_CREATED)
|
|
async def deferred_collection(_: TokenDependency) -> dict[str, list]:
|
|
return {"data": []}
|
|
|
|
|
|
@router.post("/token", tags=["api-v2-protected"], operation_id="post_token")
|
|
async def issue_token(_: TokenDependency) -> dict[str, str]:
|
|
return {"access_token": "already-authenticated", "token_type": "bearer"}
|
|
|
|
|
|
@router.get("/widget-types", response_model=list[CatalogItem])
|
|
def list_widget_types() -> list[CatalogItem]:
|
|
return WIDGET_TYPES
|
|
|
|
|
|
@router.get("/event-types", response_model=list[CatalogItem])
|
|
def list_event_types() -> list[CatalogItem]:
|
|
return EVENT_TYPES
|
|
|
|
|
|
@router.get("/annotation-categories", response_model=list[CatalogItem])
|
|
def list_annotation_categories() -> list[CatalogItem]:
|
|
return ANNOTATION_CATEGORIES
|
|
|
|
|
|
@router.get("/policy-scopes", response_model=list[CatalogItem])
|
|
def list_policy_scopes() -> list[CatalogItem]:
|
|
return POLICY_SCOPES
|
|
|
|
|
|
@router.get("/openapi.json", include_in_schema=False)
|
|
def openapi_json(request: Request) -> dict[str, Any]:
|
|
return compatibility_openapi(request)
|
|
|
|
|
|
@router.get("/openapi.yaml", include_in_schema=False)
|
|
def openapi_yaml(request: Request) -> PlainTextResponse:
|
|
info = compatibility_openapi(request).get("info", {})
|
|
body = (
|
|
"openapi: 3.1.0\n"
|
|
"info:\n"
|
|
f" title: {info.get('title', 'Core Hub API')}\n"
|
|
f" version: {info.get('version', '0.1.0')}\n"
|
|
)
|
|
return PlainTextResponse(body, media_type="application/yaml")
|
|
|
|
|
|
@router.get("/docs", include_in_schema=False)
|
|
def docs_redirect() -> RedirectResponse:
|
|
return RedirectResponse(url="/docs")
|