203 lines
6.4 KiB
Python
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"
|
|
)
|