Files
motovaultpro/mvp-platform-services/vehicles/api/main.py
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

203 lines
6.4 KiB
Python

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import asyncpg
import redis.asyncio as redis
import time
from .config import get_settings
from .dependencies import get_db_pool, get_redis_client, get_cache, verify_bearer_token
from .routes import vehicles, vin
from .models.responses import HealthResponse
from .services.cache_service import CacheService
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
settings = get_settings()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager"""
# Startup
logger.info("Starting MVP Platform Vehicles API...")
# Initialize database pool
try:
app.state.db_pool = await asyncpg.create_pool(
host=settings.POSTGRES_HOST,
port=settings.POSTGRES_PORT,
user=settings.POSTGRES_USER,
password=settings.POSTGRES_PASSWORD,
database=settings.POSTGRES_DATABASE,
min_size=settings.DATABASE_MIN_CONNECTIONS,
max_size=settings.DATABASE_MAX_CONNECTIONS,
command_timeout=30
)
logger.info("Database pool initialized")
except Exception as e:
logger.error(f"Failed to initialize database pool: {e}")
raise
# Initialize Redis client
try:
app.state.redis_client = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB,
decode_responses=False,
socket_connect_timeout=5,
socket_timeout=5
)
# Test connection
await app.state.redis_client.ping()
logger.info("Redis client initialized")
except Exception as e:
logger.warning(f"Failed to initialize Redis client: {e}")
app.state.redis_client = None
# Initialize cache service
app.state.cache_service = CacheService(
app.state.redis_client,
enabled=bool(app.state.redis_client),
default_ttl=settings.CACHE_TTL
)
yield
# Shutdown
logger.info("Shutting down MVP Platform Vehicles API...")
if hasattr(app.state, 'db_pool') and app.state.db_pool:
await app.state.db_pool.close()
logger.info("Database pool closed")
if hasattr(app.state, 'redis_client') and app.state.redis_client:
await app.state.redis_client.aclose()
logger.info("Redis client closed")
# Create FastAPI app
app = FastAPI(
title="MVP Platform Vehicles API",
description="Hierarchical Vehicle API with VIN decoding for MotoVaultPro platform services",
version="1.0.0",
lifespan=lifespan,
docs_url="/docs" if settings.DEBUG else None,
redoc_url="/redoc" if settings.DEBUG else None
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Request timing middleware
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
# Global exception handler
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"Unhandled exception in {request.method} {request.url.path}: {exc}")
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"}
)
# Include routers
app.include_router(vehicles.router, prefix="/api/v1", dependencies=[Depends(verify_bearer_token)])
app.include_router(vin.router, prefix="/api/v1", dependencies=[Depends(verify_bearer_token)])
# Health check endpoint
@app.api_route("/health", methods=["GET", "HEAD"], response_model=HealthResponse)
async def health_check(request: Request):
"""Health check endpoint"""
db_status = "ok"
cache_status = "ok"
# Check database
try:
db_pool = request.app.state.db_pool
async with db_pool.acquire() as conn:
await conn.fetchval("SELECT 1")
except Exception as e:
logger.error(f"Database health check failed: {e}")
db_status = "error"
# Check cache
try:
cache = request.app.state.cache_service
if cache and cache.enabled:
await cache.redis.ping()
else:
cache_status = "disabled"
except Exception as e:
logger.error(f"Cache health check failed: {e}")
cache_status = "error"
overall_status = "ok" if db_status == "ok" else "degraded"
return HealthResponse(
status=overall_status,
database=db_status,
cache=cache_status,
version="1.0.0"
)
# Root endpoint
@app.get("/")
async def root():
"""Root endpoint with API information"""
return {
"name": "MVP Platform Vehicles API",
"version": "1.0.0",
"description": "Hierarchical Vehicle API with VIN decoding",
"docs_url": "/docs" if settings.DEBUG else "Contact administrator for documentation",
"endpoints": {
"health": "/health",
"makes": "/api/v1/vehicles/makes?year=2024",
"models": "/api/v1/vehicles/models?year=2024&make_id=1",
"trims": "/api/v1/vehicles/trims?year=2024&make_id=1&model_id=1",
"engines": "/api/v1/vehicles/engines?year=2024&make_id=1&model_id=1",
"transmissions": "/api/v1/vehicles/transmissions?year=2024&make_id=1&model_id=1",
"vin_decode": "/api/v1/vehicles/vindecode"
}
}
# Cache stats endpoint
@app.get("/api/v1/cache/stats")
async def cache_stats(request: Request, token: str = Depends(verify_bearer_token)):
"""Get cache statistics"""
try:
cache = request.app.state.cache_service
stats = await cache.get_stats()
return stats
except Exception as e:
logger.error(f"Failed to get cache stats: {e}")
raise HTTPException(status_code=500, detail="Failed to retrieve cache statistics")
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"api.main:app",
host="0.0.0.0",
port=8000,
reload=settings.DEBUG,
log_level="info"
)