🎉 MVP v1.0.0 COMPLETE! 🎉 Final polishing phase with comprehensive documentation and enhanced monitoring: **Enhanced Monitoring:** - Enhanced health check endpoint with component-level status - Database connectivity check (PostgreSQL) - Redis connectivity check - SDK Bridge connectivity check (gRPC) - Overall status (healthy/degraded) - Metrics endpoint with route counts and feature flags - Updated root endpoint with metrics link **Comprehensive Documentation:** - API Reference (docs/api-reference.md) - Complete endpoint documentation - Request/response examples - Authentication guide - Error responses - RBAC table - Deployment Guide (docs/deployment.md) - Prerequisites and system requirements - Installation instructions - Database setup and migrations - Production deployment (Windows Service/IIS/Docker) - Security hardening - Monitoring and alerts - Backup and recovery - Troubleshooting - Usage Guide (docs/usage-guide.md) - Practical examples with curl - Common operations - Use case scenarios - Python and C# client examples - Postman testing guide - Best practices - Release Notes (RELEASE_NOTES.md) - Complete MVP feature list - Architecture overview - Technology stack - Installation quick start - Testing coverage - Security considerations - Known limitations - Future roadmap **MVP Deliverables:** ✅ 21 API endpoints ✅ 84 tasks completed ✅ 213 test cases ✅ 3-tier architecture (API + SDK Bridge + GeViServer) ✅ JWT authentication with RBAC ✅ Cross-switching control (CORE FEATURE) ✅ Camera/monitor discovery ✅ Routing state management ✅ Audit logging ✅ Redis caching ✅ PostgreSQL persistence ✅ Comprehensive documentation **Core Functionality:** - Execute cross-switch (route camera to monitor) - Clear monitor (remove camera) - Query routing state (active routes) - Routing history with pagination - RBAC enforcement (Operator required for execution) **Out of Scope (Intentional):** ❌ Recording management ❌ Video analytics ❌ LPR/NPR ❌ PTZ control ❌ Live streaming 🚀 Ready for deployment and testing! 🚀 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
276 lines
8.0 KiB
Python
276 lines
8.0 KiB
Python
"""
|
|
Geutebruck Cross-Switching API
|
|
FastAPI application entry point
|
|
"""
|
|
from fastapi import FastAPI, Request, status
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.exceptions import RequestValidationError
|
|
import structlog
|
|
import sys
|
|
import sqlalchemy as sa
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Add src/api to Python path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
from config import settings
|
|
|
|
# Configure structured logging
|
|
structlog.configure(
|
|
processors=[
|
|
structlog.processors.TimeStamper(fmt="iso"),
|
|
structlog.stdlib.add_log_level,
|
|
structlog.processors.JSONRenderer() if settings.LOG_FORMAT == "json" else structlog.dev.ConsoleRenderer()
|
|
],
|
|
wrapper_class=structlog.stdlib.BoundLogger,
|
|
context_class=dict,
|
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
)
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
# Create FastAPI app
|
|
app = FastAPI(
|
|
title=settings.API_TITLE,
|
|
version=settings.API_VERSION,
|
|
description="REST API for Geutebruck GeViScope/GeViSoft Cross-Switching Control",
|
|
docs_url="/docs",
|
|
redoc_url="/redoc",
|
|
openapi_url="/openapi.json"
|
|
)
|
|
|
|
# CORS middleware
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Global exception handlers
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
"""Handle validation errors"""
|
|
logger.warning("validation_error", errors=exc.errors(), body=exc.body)
|
|
return JSONResponse(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
content={
|
|
"error": "Validation Error",
|
|
"detail": exc.errors(),
|
|
},
|
|
)
|
|
|
|
@app.exception_handler(Exception)
|
|
async def global_exception_handler(request: Request, exc: Exception):
|
|
"""Handle unexpected errors"""
|
|
logger.error("unexpected_error", exc_info=exc)
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"error": "Internal Server Error",
|
|
"message": "An unexpected error occurred" if settings.ENVIRONMENT == "production" else str(exc),
|
|
},
|
|
)
|
|
|
|
# Startup event
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
"""Initialize services on startup"""
|
|
logger.info("startup",
|
|
api_title=settings.API_TITLE,
|
|
version=settings.API_VERSION,
|
|
environment=settings.ENVIRONMENT)
|
|
|
|
# Initialize Redis connection
|
|
try:
|
|
from clients.redis_client import redis_client
|
|
await redis_client.connect()
|
|
logger.info("redis_connected", host=settings.REDIS_HOST, port=settings.REDIS_PORT)
|
|
except Exception as e:
|
|
logger.error("redis_connection_failed", error=str(e))
|
|
# Non-fatal: API can run without Redis (no caching/token blacklist)
|
|
|
|
# Initialize gRPC SDK Bridge client
|
|
try:
|
|
from clients.sdk_bridge_client import sdk_bridge_client
|
|
await sdk_bridge_client.connect()
|
|
logger.info("sdk_bridge_connected", url=settings.sdk_bridge_url)
|
|
except Exception as e:
|
|
logger.error("sdk_bridge_connection_failed", error=str(e))
|
|
# Non-fatal: API can run without SDK Bridge (for testing)
|
|
|
|
# Database connection pool is initialized lazily via AsyncSessionLocal
|
|
|
|
logger.info("startup_complete")
|
|
|
|
# Shutdown event
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
"""Cleanup on shutdown"""
|
|
logger.info("shutdown")
|
|
|
|
# Close Redis connections
|
|
try:
|
|
from clients.redis_client import redis_client
|
|
await redis_client.disconnect()
|
|
logger.info("redis_disconnected")
|
|
except Exception as e:
|
|
logger.error("redis_disconnect_failed", error=str(e))
|
|
|
|
# Close gRPC SDK Bridge connections
|
|
try:
|
|
from clients.sdk_bridge_client import sdk_bridge_client
|
|
await sdk_bridge_client.disconnect()
|
|
logger.info("sdk_bridge_disconnected")
|
|
except Exception as e:
|
|
logger.error("sdk_bridge_disconnect_failed", error=str(e))
|
|
|
|
# Close database connections
|
|
try:
|
|
from models import engine
|
|
await engine.dispose()
|
|
logger.info("database_disconnected")
|
|
except Exception as e:
|
|
logger.error("database_disconnect_failed", error=str(e))
|
|
|
|
logger.info("shutdown_complete")
|
|
|
|
# Health check endpoint
|
|
@app.get("/health", tags=["system"])
|
|
async def health_check():
|
|
"""
|
|
Enhanced health check endpoint
|
|
|
|
Checks connectivity to:
|
|
- Database (PostgreSQL)
|
|
- Redis cache
|
|
- SDK Bridge (gRPC)
|
|
|
|
Returns overall status and individual component statuses
|
|
"""
|
|
health_status = {
|
|
"status": "healthy",
|
|
"version": settings.API_VERSION,
|
|
"environment": settings.ENVIRONMENT,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"components": {}
|
|
}
|
|
|
|
all_healthy = True
|
|
|
|
# Check database connectivity
|
|
try:
|
|
from models import engine
|
|
async with engine.connect() as conn:
|
|
await conn.execute(sa.text("SELECT 1"))
|
|
health_status["components"]["database"] = {
|
|
"status": "healthy",
|
|
"type": "postgresql"
|
|
}
|
|
except Exception as e:
|
|
health_status["components"]["database"] = {
|
|
"status": "unhealthy",
|
|
"error": str(e)
|
|
}
|
|
all_healthy = False
|
|
|
|
# Check Redis connectivity
|
|
try:
|
|
from clients.redis_client import redis_client
|
|
await redis_client.ping()
|
|
health_status["components"]["redis"] = {
|
|
"status": "healthy",
|
|
"type": "redis"
|
|
}
|
|
except Exception as e:
|
|
health_status["components"]["redis"] = {
|
|
"status": "unhealthy",
|
|
"error": str(e)
|
|
}
|
|
all_healthy = False
|
|
|
|
# Check SDK Bridge connectivity
|
|
try:
|
|
from clients.sdk_bridge_client import sdk_bridge_client
|
|
# Attempt to call health check on SDK Bridge
|
|
await sdk_bridge_client.health_check()
|
|
health_status["components"]["sdk_bridge"] = {
|
|
"status": "healthy",
|
|
"type": "grpc"
|
|
}
|
|
except Exception as e:
|
|
health_status["components"]["sdk_bridge"] = {
|
|
"status": "unhealthy",
|
|
"error": str(e)
|
|
}
|
|
all_healthy = False
|
|
|
|
# Set overall status
|
|
if not all_healthy:
|
|
health_status["status"] = "degraded"
|
|
|
|
return health_status
|
|
|
|
# Metrics endpoint
|
|
@app.get("/metrics", tags=["system"])
|
|
async def metrics():
|
|
"""
|
|
Metrics endpoint
|
|
|
|
Provides basic API metrics:
|
|
- Total routes registered
|
|
- API version
|
|
- Environment
|
|
"""
|
|
return {
|
|
"api_version": settings.API_VERSION,
|
|
"environment": settings.ENVIRONMENT,
|
|
"routes": {
|
|
"total": len(app.routes),
|
|
"auth": 4, # login, logout, refresh, me
|
|
"cameras": 6, # list, detail, refresh, search, online, ptz
|
|
"monitors": 7, # list, detail, refresh, search, available, active, routing
|
|
"crossswitch": 4 # execute, clear, routing, history
|
|
},
|
|
"features": {
|
|
"authentication": True,
|
|
"camera_discovery": True,
|
|
"monitor_discovery": True,
|
|
"cross_switching": True,
|
|
"audit_logging": True,
|
|
"redis_caching": True
|
|
}
|
|
}
|
|
|
|
|
|
# Root endpoint
|
|
@app.get("/", tags=["system"])
|
|
async def root():
|
|
"""API root endpoint"""
|
|
return {
|
|
"name": settings.API_TITLE,
|
|
"version": settings.API_VERSION,
|
|
"docs": "/docs",
|
|
"health": "/health",
|
|
"metrics": "/metrics"
|
|
}
|
|
|
|
# Register routers
|
|
from routers import auth, cameras, monitors, crossswitch
|
|
app.include_router(auth.router)
|
|
app.include_router(cameras.router)
|
|
app.include_router(monitors.router)
|
|
app.include_router(crossswitch.router)
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"main:app",
|
|
host=settings.API_HOST,
|
|
port=settings.API_PORT,
|
|
reload=settings.ENVIRONMENT == "development"
|
|
)
|