Source code for app.main

import asyncio
import logging
import os
import sys
import time

# Configure logging to ensure it shows up in Cloud Run logs immediately
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    stream=sys.stdout,
)
logger = logging.getLogger("kumiho.fastapi.boot")
logger.info("Initializing Kumiho SaaS API...")

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app import __version__
version = "1.0.2" # CI requires a string literal assignment to 'version'
from app.rate_limit import RateLimitMiddleware

from app.correlation import CorrelationIdMiddleware
from app.errors import register_error_handlers
from app.observability import RequestLogMiddleware
from app.otel import setup_otel

from app.dependencies import ensure_tenant_context

# Core API routers (SDK-mirroring)
from app.core import (
    projects_router,
    spaces_router,
    items_router,
    revisions_router,
    artifacts_router,
    edges_router,
    graph_router,
    tenant_router,
    resolve_router,
    bundles_router,
    attributes_router,
    events_router,
    mcp_router,
)

# Apps (domain-specific applications)
from app.apps.blog import router as blog_router

from kumiho.mcp_server import create_mcp_server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager

[docs] @asynccontextmanager async def lifespan(app: FastAPI): """Lifespan for Kumiho SaaS API.""" # Initialize MCP Manager mcp_server = create_mcp_server() mcp_manager = StreamableHTTPSessionManager(mcp_server, stateless=True) # Start MCP Manager in background async with mcp_manager.run(): app.state.mcp_manager = mcp_manager # Warm control plane cache in background async def _warm(): try: await asyncio.sleep(5) if os.getenv("KUMIHO_SERVICE_TOKEN"): await asyncio.get_event_loop().run_in_executor(None, ensure_tenant_context) logger.info("Tenant context warmed successfully.") except Exception as exc: logger.warning(f"Failed to warm tenant context: {exc}") warm_task = asyncio.create_task(_warm()) yield warm_task.cancel()
app = FastAPI( title="Kumiho SaaS API", description="Multi-tenant SaaS API for Kumiho - Build applications with version-controlled asset management", version=__version__, docs_url="/docs", redoc_url="/redoc", lifespan=lifespan ) # Ensure every request/response has a correlation id header. app.add_middleware(CorrelationIdMiddleware) # Basic access logs with correlation id. app.add_middleware(RequestLogMiddleware) # Standardize error responses across the API. register_error_handlers(app) # OpenTelemetry instrumentation (optional). setup_otel(app, service_name="kumiho-fastapi", service_version=__version__) # Configure CORS - environment aware # In production, set CORS_ORIGINS environment variable to specific allowed origins # Example: CORS_ORIGINS=https://yourdomain.com,https://app.yourdomain.com allowed_origins_env = os.getenv("CORS_ORIGINS", "") allowed_origins_regex = os.getenv("CORS_ORIGIN_REGEX", "").strip() if allowed_origins_env: # Production: use specified origins origins = [origin.strip() for origin in allowed_origins_env.split(",")] else: # Development: allow common dev origins origins = [ "http://localhost:3000", # local web app "http://localhost:8080", # gRPC kumiho-server "http://localhost:5678", # n8n localhost "https://kumiho.io", # official kumiho site ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_origin_regex=allowed_origins_regex or None, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["*"] ) # Add rate limiting - configurable via environment # Default: 120 requests per minute per IP # Set RATE_LIMIT_PER_MINUTE=0 to disable rate limiting rate_limit = int(os.getenv("RATE_LIMIT_PER_MINUTE", "120")) if rate_limit > 0: app.add_middleware(RateLimitMiddleware, requests_per_minute=rate_limit) # ============================================================================ # Core API routers - SDK-mirroring CRUD endpoints # These expose the same operations as kumiho-python Client # ============================================================================ app.include_router(projects_router, prefix="/api/v1/projects", tags=["projects"]) app.include_router(spaces_router, prefix="/api/v1/spaces", tags=["spaces"]) app.include_router(items_router, prefix="/api/v1/items", tags=["items"]) app.include_router(revisions_router, prefix="/api/v1/revisions", tags=["revisions"]) app.include_router(artifacts_router, prefix="/api/v1/artifacts", tags=["artifacts"]) app.include_router(edges_router, prefix="/api/v1/edges", tags=["edges"]) app.include_router(graph_router, prefix="/api/v1/graph", tags=["graph"]) app.include_router(tenant_router, prefix="/api/v1/tenant", tags=["tenant"]) app.include_router(resolve_router, prefix="/api/v1/resolve", tags=["resolve"]) app.include_router(bundles_router, prefix="/api/v1/bundles", tags=["bundles"]) app.include_router(attributes_router, prefix="/api/v1/attributes", tags=["attributes"]) app.include_router(events_router, prefix="/api/v1/events", tags=["events"]) app.include_router(mcp_router, prefix="/api/v1/mcp", tags=["mcp"]) # ============================================================================ # Apps - Domain-specific applications built on top of core APIs # ============================================================================ app.include_router(blog_router, prefix="/api/v1/apps/blog", tags=["blog"])
[docs] @app.get("/healthz") async def healthz(): """Simple health check endpoint for Cloud Run""" return {"status": "ok", "timestamp": time.time()}
[docs] @app.get("/") async def root(): """Root endpoint with API information""" return { "service": "Kumiho SaaS API", "version": "1.0.1", "description": "Multi-tenant API for building applications with Kumiho", "documentation": "/docs", "authentication": { "required_header": "X-Kumiho-Token", "get_token": "https://kumiho.io - Sign up to get your service token" }, "endpoints": { "core_api": { "prefix": "/api/v1", "resources": [ "projects", "spaces", "items", "revisions", "artifacts", "edges", "tenant", "resolve", "bundles", "attributes", "events", "mcp" ] }, "apps": { "prefix": "/api/v1/apps", "available": ["blog"] } }, "rate_limit": { "enabled": rate_limit > 0, "requests_per_minute": rate_limit if rate_limit > 0 else "unlimited" } }