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

- 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:
aitbc
2026-03-31 21:45:43 +02:00
parent 26592ddf55
commit 9db720add8
308 changed files with 34194 additions and 34575 deletions

View File

@@ -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