This repository has been archived on 2026-07-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core-hub/src/core_hub/api/v2.py

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")