docs: add code quality and type checking workflows to master index
Some checks failed
Documentation Validation / validate-docs (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
API Endpoint Tests / test-api-endpoints (push) Has been cancelled
CLI Tests / test-cli (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-agent-sdk path:packages/py/aitbc-agent-sdk]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-core path:packages/py/aitbc-core]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-crypto path:packages/py/aitbc-crypto]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-sdk path:packages/py/aitbc-sdk]) (push) Has been cancelled
Package Tests / test-javascript-packages (map[name:aitbc-sdk-js path:packages/js/aitbc-sdk]) (push) Has been cancelled
Package Tests / test-javascript-packages (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled
Some checks failed
Documentation Validation / validate-docs (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
API Endpoint Tests / test-api-endpoints (push) Has been cancelled
CLI Tests / test-cli (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-agent-sdk path:packages/py/aitbc-agent-sdk]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-core path:packages/py/aitbc-core]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-crypto path:packages/py/aitbc-crypto]) (push) Has been cancelled
Package Tests / test-python-packages (map[name:aitbc-sdk path:packages/py/aitbc-sdk]) (push) Has been cancelled
Package Tests / test-javascript-packages (map[name:aitbc-sdk-js path:packages/js/aitbc-sdk]) (push) Has been cancelled
Package Tests / test-javascript-packages (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled
- Add Code Quality Module section with pre-commit hooks and quality checks - Add Type Checking CI/CD Module section with MyPy workflow and coverage - Update README with code quality achievements and project structure - Migrate FastAPI apps from deprecated on_event to lifespan context manager - Update pyproject.toml files to reference consolidated dependencies - Remove unused app.py import in coordinator-api - Add type hints to agent
This commit is contained in:
@@ -1,78 +1,73 @@
|
||||
"""Coordinator API main entry point."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Security: Lock sys.path to trusted locations to prevent malicious package shadowing
|
||||
# Keep: site-packages under /opt/aitbc (venv), stdlib paths, our app directory, and crypto/sdk paths
|
||||
_LOCKED_PATH = []
|
||||
for p in sys.path:
|
||||
if 'site-packages' in p and '/opt/aitbc' in p:
|
||||
if "site-packages" in p and "/opt/aitbc" in p:
|
||||
_LOCKED_PATH.append(p)
|
||||
elif 'site-packages' not in p and ('/usr/lib/python' in p or '/usr/local/lib/python' in p):
|
||||
elif "site-packages" not in p and ("/usr/lib/python" in p or "/usr/local/lib/python" in p):
|
||||
_LOCKED_PATH.append(p)
|
||||
elif p.startswith('/opt/aitbc/apps/coordinator-api'): # our app code
|
||||
elif p.startswith("/opt/aitbc/apps/coordinator-api"): # our app code
|
||||
_LOCKED_PATH.append(p)
|
||||
elif p.startswith('/opt/aitbc/packages/py/aitbc-crypto'): # crypto module
|
||||
elif p.startswith("/opt/aitbc/packages/py/aitbc-crypto"): # crypto module
|
||||
_LOCKED_PATH.append(p)
|
||||
elif p.startswith('/opt/aitbc/packages/py/aitbc-sdk'): # sdk module
|
||||
elif p.startswith("/opt/aitbc/packages/py/aitbc-sdk"): # sdk module
|
||||
_LOCKED_PATH.append(p)
|
||||
|
||||
# Add crypto and sdk paths to sys.path
|
||||
sys.path.insert(0, '/opt/aitbc/packages/py/aitbc-crypto/src')
|
||||
sys.path.insert(0, '/opt/aitbc/packages/py/aitbc-sdk/src')
|
||||
sys.path.insert(0, "/opt/aitbc/packages/py/aitbc-crypto/src")
|
||||
sys.path.insert(0, "/opt/aitbc/packages/py/aitbc-sdk/src")
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Annotated
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from fastapi import FastAPI, Request, Depends
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from prometheus_client import Counter, Histogram, generate_latest, make_asgi_app
|
||||
from prometheus_client.core import CollectorRegistry
|
||||
from prometheus_client.exposition import CONTENT_TYPE_LATEST
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
from .config import settings
|
||||
from .storage import init_db
|
||||
from .routers import (
|
||||
client,
|
||||
miner,
|
||||
admin,
|
||||
marketplace,
|
||||
marketplace_gpu,
|
||||
exchange,
|
||||
users,
|
||||
services,
|
||||
marketplace_offers,
|
||||
zk_applications,
|
||||
explorer,
|
||||
payments,
|
||||
web_vitals,
|
||||
edge_gpu,
|
||||
cache_management,
|
||||
agent_identity,
|
||||
agent_router,
|
||||
global_marketplace,
|
||||
client,
|
||||
cross_chain_integration,
|
||||
global_marketplace_integration,
|
||||
developer_platform,
|
||||
edge_gpu,
|
||||
exchange,
|
||||
explorer,
|
||||
global_marketplace,
|
||||
global_marketplace_integration,
|
||||
governance_enhanced,
|
||||
blockchain
|
||||
marketplace,
|
||||
marketplace_gpu,
|
||||
marketplace_offers,
|
||||
miner,
|
||||
payments,
|
||||
services,
|
||||
users,
|
||||
web_vitals,
|
||||
)
|
||||
from .storage import init_db
|
||||
|
||||
# Skip optional routers with missing dependencies
|
||||
try:
|
||||
from .routers.ml_zk_proofs import router as ml_zk_proofs
|
||||
except ImportError:
|
||||
ml_zk_proofs = None
|
||||
print("WARNING: ML ZK proofs router not available (missing tenseal)")
|
||||
from .routers.community import router as community_router
|
||||
from .routers.governance import router as new_governance_router
|
||||
from .routers.partners import router as partners
|
||||
from .routers.marketplace_enhanced_simple import router as marketplace_enhanced
|
||||
from .routers.openclaw_enhanced_simple import router as openclaw_enhanced
|
||||
from .routers.monitoring_dashboard import router as monitoring_dashboard
|
||||
from .routers.openclaw_enhanced_simple import router as openclaw_enhanced
|
||||
|
||||
# Skip optional routers with missing dependencies
|
||||
try:
|
||||
from .routers.multi_modal_rl import router as multi_modal_rl_router
|
||||
@@ -85,35 +80,35 @@ try:
|
||||
except ImportError:
|
||||
ml_zk_proofs = None
|
||||
print("WARNING: ML ZK proofs router not available (missing dependencies)")
|
||||
from .storage.models_governance import GovernanceProposal, ProposalVote, TreasuryTransaction, GovernanceParameter
|
||||
from .exceptions import AITBCError, ErrorResponse
|
||||
import logging
|
||||
|
||||
from .exceptions import AITBCError, ErrorResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from .config import settings
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from .storage.db import init_db
|
||||
|
||||
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Lifecycle events for the Coordinator API."""
|
||||
logger.info("Starting Coordinator API")
|
||||
|
||||
|
||||
try:
|
||||
# Initialize database
|
||||
init_db()
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
|
||||
# Warmup database connections
|
||||
logger.info("Warming up database connections...")
|
||||
try:
|
||||
# Test database connectivity
|
||||
from sqlmodel import select
|
||||
|
||||
from .domain import Job
|
||||
from .storage import get_session
|
||||
|
||||
|
||||
# Simple connectivity test using dependency injection
|
||||
session_gen = get_session()
|
||||
session = next(session_gen)
|
||||
@@ -126,36 +121,37 @@ async def lifespan(app: FastAPI):
|
||||
except Exception as e:
|
||||
logger.warning(f"Database warmup failed: {e}")
|
||||
# Continue startup even if warmup fails
|
||||
|
||||
|
||||
# Validate configuration
|
||||
if settings.app_env == "production":
|
||||
logger.info("Production environment detected, validating configuration")
|
||||
# Configuration validation happens automatically via Pydantic validators
|
||||
logger.info("Configuration validation passed")
|
||||
|
||||
|
||||
# Initialize audit logging directory
|
||||
from pathlib import Path
|
||||
|
||||
audit_dir = Path(settings.audit_log_dir)
|
||||
audit_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Audit logging directory: {audit_dir}")
|
||||
|
||||
|
||||
# Initialize rate limiting configuration
|
||||
logger.info("Rate limiting configuration:")
|
||||
logger.info(f" Jobs submit: {settings.rate_limit_jobs_submit}")
|
||||
logger.info(f" Miner register: {settings.rate_limit_miner_register}")
|
||||
logger.info(f" Miner heartbeat: {settings.rate_limit_miner_heartbeat}")
|
||||
logger.info(f" Admin stats: {settings.rate_limit_admin_stats}")
|
||||
|
||||
|
||||
# Log service startup details
|
||||
logger.info(f"Coordinator API started on {settings.app_host}:{settings.app_port}")
|
||||
logger.info(f"Database adapter: {settings.database.adapter}")
|
||||
logger.info(f"Environment: {settings.app_env}")
|
||||
|
||||
|
||||
# Log complete configuration summary
|
||||
logger.info("=== Coordinator API Configuration Summary ===")
|
||||
logger.info(f"Environment: {settings.app_env}")
|
||||
logger.info(f"Database: {settings.database.adapter}")
|
||||
logger.info(f"Rate Limits:")
|
||||
logger.info("Rate Limits:")
|
||||
logger.info(f" Jobs submit: {settings.rate_limit_jobs_submit}")
|
||||
logger.info(f" Miner register: {settings.rate_limit_miner_register}")
|
||||
logger.info(f" Miner heartbeat: {settings.rate_limit_miner_heartbeat}")
|
||||
@@ -166,32 +162,33 @@ async def lifespan(app: FastAPI):
|
||||
logger.info(f" Exchange payment: {settings.rate_limit_exchange_payment}")
|
||||
logger.info(f"Audit logging: {settings.audit_log_dir}")
|
||||
logger.info("=== Startup Complete ===")
|
||||
|
||||
|
||||
# Initialize health check endpoints
|
||||
logger.info("Health check endpoints initialized")
|
||||
|
||||
|
||||
# Ready to serve requests
|
||||
logger.info("🚀 Coordinator API is ready to serve requests")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start Coordinator API: {e}")
|
||||
raise
|
||||
|
||||
|
||||
yield
|
||||
|
||||
|
||||
logger.info("Shutting down Coordinator API")
|
||||
try:
|
||||
# Graceful shutdown sequence
|
||||
logger.info("Initiating graceful shutdown sequence...")
|
||||
|
||||
|
||||
# Stop accepting new requests
|
||||
logger.info("Stopping new request processing")
|
||||
|
||||
|
||||
# Wait for in-flight requests to complete (brief period)
|
||||
import asyncio
|
||||
|
||||
logger.info("Waiting for in-flight requests to complete...")
|
||||
await asyncio.sleep(1) # Brief grace period
|
||||
|
||||
|
||||
# Cleanup database connections
|
||||
logger.info("Closing database connections...")
|
||||
try:
|
||||
@@ -199,27 +196,28 @@ async def lifespan(app: FastAPI):
|
||||
logger.info("Database connections closed successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing database connections: {e}")
|
||||
|
||||
|
||||
# Cleanup rate limiting state
|
||||
logger.info("Cleaning up rate limiting state...")
|
||||
|
||||
|
||||
# Cleanup audit resources
|
||||
logger.info("Cleaning up audit resources...")
|
||||
|
||||
|
||||
# Log shutdown metrics
|
||||
logger.info("=== Coordinator API Shutdown Summary ===")
|
||||
logger.info("All resources cleaned up successfully")
|
||||
logger.info("Graceful shutdown completed")
|
||||
logger.info("=== Shutdown Complete ===")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during shutdown: {e}")
|
||||
# Continue shutdown even if cleanup fails
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
# Initialize rate limiter
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="AITBC Coordinator API",
|
||||
description="API for coordinating AI training jobs and blockchain operations",
|
||||
@@ -227,15 +225,7 @@ def create_app() -> FastAPI:
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
openapi_components={
|
||||
"securitySchemes": {
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Api-Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
openapi_components={"securitySchemes": {"ApiKeyAuth": {"type": "apiKey", "in": "header", "name": "X-Api-Key"}}},
|
||||
openapi_tags=[
|
||||
{"name": "health", "description": "Health check endpoints"},
|
||||
{"name": "client", "description": "Client operations"},
|
||||
@@ -245,9 +235,9 @@ def create_app() -> FastAPI:
|
||||
{"name": "exchange", "description": "Exchange operations"},
|
||||
{"name": "governance", "description": "Governance operations"},
|
||||
{"name": "zk", "description": "Zero-Knowledge proofs"},
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# API Key middleware (if configured) - DISABLED in favor of dependency injection
|
||||
# required_key = os.getenv("COORDINATOR_API_KEY")
|
||||
# if required_key:
|
||||
@@ -263,10 +253,10 @@ def create_app() -> FastAPI:
|
||||
# content={"detail": "Invalid or missing API key"}
|
||||
# )
|
||||
# return await call_next(request)
|
||||
|
||||
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
|
||||
# Create database tables (now handled in lifespan)
|
||||
# init_db()
|
||||
|
||||
@@ -275,7 +265,7 @@ def create_app() -> FastAPI:
|
||||
allow_origins=settings.allow_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"] # Allow all headers for API keys and content types
|
||||
allow_headers=["*"], # Allow all headers for API keys and content types
|
||||
)
|
||||
|
||||
# Enable all routers with OpenAPI disabled
|
||||
@@ -291,10 +281,10 @@ def create_app() -> FastAPI:
|
||||
app.include_router(payments, prefix="/v1")
|
||||
app.include_router(web_vitals, prefix="/v1")
|
||||
app.include_router(edge_gpu)
|
||||
|
||||
|
||||
# Add standalone routers for tasks and payments
|
||||
app.include_router(marketplace_gpu, prefix="/v1")
|
||||
|
||||
|
||||
if ml_zk_proofs:
|
||||
app.include_router(ml_zk_proofs)
|
||||
app.include_router(marketplace_enhanced, prefix="/v1")
|
||||
@@ -307,10 +297,10 @@ def create_app() -> FastAPI:
|
||||
app.include_router(global_marketplace_integration, prefix="/v1")
|
||||
app.include_router(developer_platform, prefix="/v1")
|
||||
app.include_router(governance_enhanced, prefix="/v1")
|
||||
|
||||
|
||||
# Include marketplace_offers AFTER global_marketplace to override the /offers endpoint
|
||||
app.include_router(marketplace_offers, prefix="/v1")
|
||||
|
||||
|
||||
# Add blockchain router for CLI compatibility
|
||||
# print(f"Adding blockchain router: {blockchain}")
|
||||
# app.include_router(blockchain, prefix="/v1")
|
||||
@@ -325,165 +315,148 @@ def create_app() -> FastAPI:
|
||||
# Add Prometheus metrics for rate limiting
|
||||
rate_limit_registry = CollectorRegistry()
|
||||
rate_limit_hits_total = Counter(
|
||||
'rate_limit_hits_total',
|
||||
'Total number of rate limit violations',
|
||||
['endpoint', 'method', 'limit'],
|
||||
registry=rate_limit_registry
|
||||
"rate_limit_hits_total",
|
||||
"Total number of rate limit violations",
|
||||
["endpoint", "method", "limit"],
|
||||
registry=rate_limit_registry,
|
||||
)
|
||||
rate_limit_response_time = Histogram(
|
||||
'rate_limit_response_time_seconds',
|
||||
'Response time for rate limited requests',
|
||||
['endpoint', 'method'],
|
||||
registry=rate_limit_registry
|
||||
Histogram(
|
||||
"rate_limit_response_time_seconds",
|
||||
"Response time for rate limited requests",
|
||||
["endpoint", "method"],
|
||||
registry=rate_limit_registry,
|
||||
)
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def rate_limit_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
|
||||
"""Handle rate limit exceeded errors with proper 429 status."""
|
||||
request_id = request.headers.get("X-Request-ID")
|
||||
|
||||
|
||||
# Record rate limit hit metrics
|
||||
endpoint = request.url.path
|
||||
method = request.method
|
||||
limit_detail = str(exc.detail) if hasattr(exc, 'detail') else 'unknown'
|
||||
|
||||
rate_limit_hits_total.labels(
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
limit=limit_detail
|
||||
).inc()
|
||||
|
||||
logger.warning(f"Rate limit exceeded: {exc}", extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"rate_limit_detail": limit_detail
|
||||
})
|
||||
|
||||
limit_detail = str(exc.detail) if hasattr(exc, "detail") else "unknown"
|
||||
|
||||
rate_limit_hits_total.labels(endpoint=endpoint, method=method, limit=limit_detail).inc()
|
||||
|
||||
logger.warning(
|
||||
f"Rate limit exceeded: {exc}",
|
||||
extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"rate_limit_detail": limit_detail,
|
||||
},
|
||||
)
|
||||
|
||||
error_response = ErrorResponse(
|
||||
error={
|
||||
"code": "RATE_LIMIT_EXCEEDED",
|
||||
"message": "Too many requests. Please try again later.",
|
||||
"status": 429,
|
||||
"details": [{
|
||||
"field": "rate_limit",
|
||||
"message": str(exc.detail),
|
||||
"code": "too_many_requests",
|
||||
"retry_after": 60 # Default retry after 60 seconds
|
||||
}]
|
||||
"details": [
|
||||
{
|
||||
"field": "rate_limit",
|
||||
"message": str(exc.detail),
|
||||
"code": "too_many_requests",
|
||||
"retry_after": 60, # Default retry after 60 seconds
|
||||
}
|
||||
],
|
||||
},
|
||||
request_id=request_id
|
||||
request_id=request_id,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content=error_response.model_dump(),
|
||||
headers={"Retry-After": "60"}
|
||||
)
|
||||
|
||||
return JSONResponse(status_code=429, content=error_response.model_dump(), headers={"Retry-After": "60"})
|
||||
|
||||
@app.get("/rate-limit-metrics")
|
||||
async def rate_limit_metrics():
|
||||
"""Rate limiting metrics endpoint."""
|
||||
return Response(
|
||||
content=generate_latest(rate_limit_registry),
|
||||
media_type=CONTENT_TYPE_LATEST
|
||||
)
|
||||
return Response(content=generate_latest(rate_limit_registry), media_type=CONTENT_TYPE_LATEST)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""Handle all unhandled exceptions with structured error responses."""
|
||||
request_id = request.headers.get("X-Request-ID")
|
||||
logger.error(f"Unhandled exception: {exc}", extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"error_type": type(exc).__name__
|
||||
})
|
||||
|
||||
logger.error(
|
||||
f"Unhandled exception: {exc}",
|
||||
extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"error_type": type(exc).__name__,
|
||||
},
|
||||
)
|
||||
|
||||
error_response = ErrorResponse(
|
||||
error={
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"message": "An unexpected error occurred",
|
||||
"status": 500,
|
||||
"details": [{
|
||||
"field": "internal",
|
||||
"message": str(exc),
|
||||
"code": type(exc).__name__
|
||||
}]
|
||||
"details": [{"field": "internal", "message": str(exc), "code": type(exc).__name__}],
|
||||
},
|
||||
request_id=request_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=error_response.model_dump()
|
||||
request_id=request_id,
|
||||
)
|
||||
return JSONResponse(status_code=500, content=error_response.model_dump())
|
||||
|
||||
@app.exception_handler(AITBCError)
|
||||
async def aitbc_error_handler(request: Request, exc: AITBCError) -> JSONResponse:
|
||||
"""Handle AITBC exceptions with structured error responses."""
|
||||
request_id = request.headers.get("X-Request-ID")
|
||||
response = exc.to_response(request_id)
|
||||
return JSONResponse(
|
||||
status_code=response.error["status"],
|
||||
content=response.model_dump()
|
||||
)
|
||||
return JSONResponse(status_code=response.error["status"], content=response.model_dump())
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
"""Handle FastAPI validation errors with structured error responses."""
|
||||
request_id = request.headers.get("X-Request-ID")
|
||||
logger.warning(f"Validation error: {exc}", extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"validation_errors": exc.errors()
|
||||
})
|
||||
|
||||
logger.warning(
|
||||
f"Validation error: {exc}",
|
||||
extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"validation_errors": exc.errors(),
|
||||
},
|
||||
)
|
||||
|
||||
details = []
|
||||
for error in exc.errors():
|
||||
details.append({
|
||||
"field": ".".join(str(loc) for loc in error["loc"]),
|
||||
"message": error["msg"],
|
||||
"code": error["type"]
|
||||
})
|
||||
|
||||
details.append(
|
||||
{"field": ".".join(str(loc) for loc in error["loc"]), "message": error["msg"], "code": error["type"]}
|
||||
)
|
||||
|
||||
error_response = ErrorResponse(
|
||||
error={
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Request validation failed",
|
||||
"status": 422,
|
||||
"details": details
|
||||
},
|
||||
request_id=request_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
content=error_response.model_dump()
|
||||
error={"code": "VALIDATION_ERROR", "message": "Request validation failed", "status": 422, "details": details},
|
||||
request_id=request_id,
|
||||
)
|
||||
return JSONResponse(status_code=422, content=error_response.model_dump())
|
||||
|
||||
@app.get("/health", tags=["health"], summary="Root health endpoint for CLI compatibility")
|
||||
async def root_health() -> dict[str, str]:
|
||||
import sys
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"status": "ok",
|
||||
"env": settings.app_env,
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
}
|
||||
|
||||
@app.get("/v1/health", tags=["health"], summary="Service healthcheck")
|
||||
async def health() -> dict[str, str]:
|
||||
import sys
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"status": "ok",
|
||||
"env": settings.app_env,
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
}
|
||||
|
||||
@app.get("/health/live", tags=["health"], summary="Liveness probe")
|
||||
async def liveness() -> dict[str, str]:
|
||||
import sys
|
||||
|
||||
return {
|
||||
"status": "alive",
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
}
|
||||
|
||||
@app.get("/health/ready", tags=["health"], summary="Readiness probe")
|
||||
@@ -491,21 +464,20 @@ def create_app() -> FastAPI:
|
||||
# Check database connectivity
|
||||
try:
|
||||
from .storage import get_engine
|
||||
|
||||
engine = get_engine()
|
||||
with engine.connect() as conn:
|
||||
conn.execute("SELECT 1")
|
||||
import sys
|
||||
|
||||
return {
|
||||
"status": "ready",
|
||||
"status": "ready",
|
||||
"database": "connected",
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Readiness check failed", extra={"error": str(e)})
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"status": "not ready", "error": str(e)}
|
||||
)
|
||||
return JSONResponse(status_code=503, content={"status": "not ready", "error": str(e)})
|
||||
|
||||
return app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user