"""Functions router — governed serverless function catalogue and invocation.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, HTTPException from pydantic import BaseModel from gsap_broker.functions.base import FunctionContext from gsap_broker.functions.registry import FunctionRegistry from gsap_broker.functions.runtime import FunctionRuntime from gsap_broker.functions.examples.billing import BillingProcessor from gsap_broker.functions.examples.echo import EchoFunction router = APIRouter() # Module-level registry and runtime _registry = FunctionRegistry() _runtime = FunctionRuntime(registry=_registry) # Register built-in functions _registry.register(BillingProcessor()) _registry.register(EchoFunction()) class InvokeRequest(BaseModel): event: dict[str, Any] = {} trigger_event_kind: str = "" trigger_event_cid: str = "" chronicle_session_id: str = "" gsap_context_id: str = "" pipeline_run_id: str = "" class DispatchRequest(BaseModel): event_kind: str event_data: dict[str, Any] = {} trigger_cid: str = "" class InvokeResponse(BaseModel): success: bool data: Any = None error: str | None = None lineage_cid: str = "" function_id: str = "" duration_ms: float = 0.0 class DispatchResponse(BaseModel): event_kind: str dispatched_count: int results: list[dict[str, Any]] @router.get("/") async def catalogue() -> dict: return { "functions": _registry.catalogue(), "trigger_index": _registry.trigger_index(), } @router.get("/{function_id}/") async def get_descriptor(function_id: str) -> dict: function = _registry.get(function_id) if function is None: raise HTTPException(status_code=404, detail=f"Function not found: {function_id}") return function.descriptor() @router.get("/{function_id}/manifest/") async def knative_manifest(function_id: str, image: str = "gcr.io/default/function:latest", namespace: str = "default") -> dict: function = _registry.get(function_id) if function is None: raise HTTPException(status_code=404, detail=f"Function not found: {function_id}") return function.knative_manifest(image=image, namespace=namespace) @router.post("/{function_id}/invoke/") async def invoke_function(function_id: str, body: InvokeRequest) -> InvokeResponse: function = _registry.get(function_id) if function is None: raise HTTPException(status_code=404, detail=f"Function not found: {function_id}") ctx = FunctionContext( trigger_event_kind=body.trigger_event_kind, trigger_event_cid=body.trigger_event_cid, chronicle_session_id=body.chronicle_session_id, gsap_context_id=body.gsap_context_id, pipeline_run_id=body.pipeline_run_id, ) result = await _runtime.invoke(function_id, body.event, ctx) return InvokeResponse( success=result.success, data=result.data, error=result.error, lineage_cid=result.lineage_cid, function_id=function_id, duration_ms=result.duration_ms, ) @router.post("/dispatch/") async def dispatch_event(body: DispatchRequest) -> DispatchResponse: results = await _runtime.dispatch(body.event_kind, body.event_data, body.trigger_cid) return DispatchResponse( event_kind=body.event_kind, dispatched_count=len(results), results=results, ) @router.get("/{function_id}/health/") async def health_check(function_id: str) -> dict: function = _registry.get(function_id) if function is None: raise HTTPException(status_code=404, detail=f"Function not found: {function_id}") healthy = function.health_check() return {"function_id": function_id, "healthy": healthy}