Files
bakery-ia/services/recipes/app/main.py
2026-01-02 12:18:46 +01:00

136 lines
4.8 KiB
Python

# services/recipes/app/main.py
"""
Recipe Service - FastAPI application
Handles recipe management, production planning, and inventory consumption tracking
"""
import time
from fastapi import FastAPI, Request
from sqlalchemy import text
from fastapi.middleware.gzip import GZipMiddleware
from .core.config import settings
from .core.database import db_manager
from shared.service_base import StandardFastAPIService
# Import API routers
from .api import recipes, recipe_quality_configs, recipe_operations, audit, internal_demo, internal
# Import models to register them with SQLAlchemy metadata
from .models import recipes as recipe_models
class RecipesService(StandardFastAPIService):
"""Recipes Service with standardized setup"""
expected_migration_version = "00001"
async def on_startup(self, app):
"""Custom startup logic including migration verification"""
await self.verify_migrations()
await super().on_startup(app)
async def verify_migrations(self):
"""Verify database schema matches the latest migrations."""
try:
async with self.database_manager.get_session() as session:
result = await session.execute(text("SELECT version_num FROM alembic_version"))
version = result.scalar()
if version != self.expected_migration_version:
self.logger.error(f"Migration version mismatch: expected {self.expected_migration_version}, got {version}")
raise RuntimeError(f"Migration version mismatch: expected {self.expected_migration_version}, got {version}")
self.logger.info(f"Migration verification successful: {version}")
except Exception as e:
self.logger.error(f"Migration verification failed: {e}")
raise
def __init__(self):
# Define expected database tables for health checks
recipes_expected_tables = [
'recipes', 'recipe_ingredients', 'production_batches',
'production_ingredient_consumption', 'production_schedules'
]
super().__init__(
service_name="recipes-service",
app_name="Recipe Management Service",
description="Comprehensive recipe management, production planning, and inventory consumption tracking for bakery operations",
version=settings.VERSION,
log_level=settings.LOG_LEVEL,
cors_origins=settings.CORS_ORIGINS,
api_prefix="", # Empty because RouteBuilder already includes /api/v1
database_manager=db_manager,
expected_tables=recipes_expected_tables
)
async def on_startup(self, app: FastAPI):
"""Custom startup logic for recipes service"""
# Custom startup completed
pass
async def on_shutdown(self, app: FastAPI):
"""Custom shutdown logic for recipes service"""
# Database cleanup is handled by the base class
pass
def get_service_features(self):
"""Return recipes-specific features"""
return [
"recipe_management",
"production_planning",
"inventory_consumption_tracking",
"batch_production",
"tenant_scoped_operations"
]
def setup_custom_middleware(self):
"""Setup custom middleware for recipes service"""
# Add GZip middleware
self.app.add_middleware(GZipMiddleware, minimum_size=1000)
# Request timing middleware
@self.app.middleware("http")
async def add_process_time_header(request: Request, call_next):
"""Add processing time header to responses"""
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
# Create service instance
service = RecipesService()
# Create FastAPI app with standardized setup
app = service.create_app(
docs_url="/docs" if settings.DEBUG else None,
redoc_url="/redoc" if settings.DEBUG else None
)
# Setup standard endpoints
service.setup_standard_endpoints()
# Setup custom middleware
service.setup_custom_middleware()
# Include routers
# IMPORTANT: Register audit router FIRST to avoid route matching conflicts
# where {recipe_id} would match literal paths like "audit-logs"
service.add_router(audit.router)
service.add_router(recipes.router)
service.add_router(recipe_quality_configs.router)
service.add_router(recipe_operations.router)
service.add_router(internal_demo.router, tags=["internal-demo"])
service.add_router(internal.router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=settings.DEBUG,
log_level=settings.LOG_LEVEL.lower()
)