Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

View File

@@ -25,7 +25,7 @@ from app.middleware.rate_limiting import APIRateLimitMiddleware
from app.middleware.subscription import SubscriptionMiddleware
from app.middleware.demo_middleware import DemoMiddleware
from app.middleware.read_only_mode import ReadOnlyModeMiddleware
from app.routes import auth, tenant, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks
from app.routes import auth, tenant, registration, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks
# Initialize logger
logger = structlog.get_logger()
@@ -40,36 +40,61 @@ try:
except Exception as e:
logger.debug(f"Could not check file descriptor limits: {e}")
# Redis client for SSE streaming
# Global Redis client for SSE streaming
redis_client = None
class GatewayService(StandardFastAPIService):
"""Gateway Service with standardized monitoring setup"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Initialize HeaderManager early
header_manager.initialize()
logger.info("HeaderManager initialized")
# Initialize Redis during service creation so it's available when needed
try:
# We need to run the async initialization in a sync context
import asyncio
try:
# Check if there's already a running event loop
loop = asyncio.get_running_loop()
# If there is, we'll initialize Redis later in on_startup
self.redis_initialized = False
self.redis_client = None
except RuntimeError:
# No event loop running, safe to run the async function
import asyncio
import nest_asyncio
nest_asyncio.apply() # Allow nested event loops
async def init_redis():
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
return await get_redis_client()
self.redis_client = asyncio.run(init_redis())
self.redis_initialized = True
logger.info("Connected to Redis for SSE streaming")
except Exception as e:
logger.error(f"Failed to initialize Redis during service creation: {e}")
self.redis_initialized = False
self.redis_client = None
async def on_startup(self, app):
"""Custom startup logic for Gateway"""
global redis_client
# Initialize HeaderManager
header_manager.initialize()
logger.info("HeaderManager initialized")
# Initialize Redis
try:
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
redis_client = await get_redis_client()
logger.info("Connected to Redis for SSE streaming")
# Add API rate limiting middleware with Redis client
app.add_middleware(APIRateLimitMiddleware, redis_client=redis_client)
logger.info("API rate limiting middleware enabled")
# NOTE: SubscriptionMiddleware and AuthMiddleware are instantiated without redis_client
# They will gracefully degrade (skip Redis-dependent features) when redis_client is None
# For future enhancement: consider using lifespan context to inject redis_client into middleware
except Exception as e:
logger.error(f"Failed to connect to Redis: {e}")
# Initialize Redis if not already done during service creation
if not self.redis_initialized:
try:
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
self.redis_client = await get_redis_client()
redis_client = self.redis_client # Update global variable
self.redis_initialized = True
logger.info("Connected to Redis for SSE streaming")
except Exception as e:
logger.error(f"Failed to connect to Redis during startup: {e}")
# Register custom metrics for gateway-specific operations
if self.telemetry_providers and self.telemetry_providers.app_metrics:
@@ -103,6 +128,23 @@ service = GatewayService(
# Create FastAPI app
app = service.create_app()
# Add API rate limiting middleware with Redis client - this needs to be done after app creation
# but before other middleware that might depend on it
# Wait for Redis to be initialized if not already done
if not hasattr(service, 'redis_client') or not service.redis_client:
# Wait briefly for Redis initialization to complete
import time
time.sleep(1)
# Check again after allowing time for initialization
if hasattr(service, 'redis_client') and service.redis_client:
app.add_middleware(APIRateLimitMiddleware, redis_client=service.redis_client)
logger.info("API rate limiting middleware enabled")
else:
logger.warning("Redis client not available for API rate limiting middleware")
else:
app.add_middleware(APIRateLimitMiddleware, redis_client=service.redis_client)
logger.info("API rate limiting middleware enabled")
# Add gateway-specific middleware (in REVERSE order of execution)
# Execution order: RequestIDMiddleware -> DemoMiddleware -> AuthMiddleware -> ReadOnlyModeMiddleware -> SubscriptionMiddleware -> APIRateLimitMiddleware -> RateLimitMiddleware -> LoggingMiddleware
app.add_middleware(LoggingMiddleware)
@@ -115,6 +157,7 @@ app.add_middleware(RequestIDMiddleware)
# Include routers
app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
app.include_router(registration.router, prefix="/api/v1", tags=["registration"])
app.include_router(tenant.router, prefix="/api/v1/tenants", tags=["tenants"])
app.include_router(subscription.router, prefix="/api/v1", tags=["subscriptions"])
# Notification routes are now handled by tenant router at /api/v1/tenants/{tenant_id}/notifications/*
@@ -122,9 +165,9 @@ app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location
app.include_router(geocoding.router, prefix="/api/v1/geocoding", tags=["geocoding"])
app.include_router(pos.router, prefix="/api/v1/pos", tags=["pos"])
app.include_router(demo.router, prefix="/api/v1", tags=["demo"])
app.include_router(webhooks.router, prefix="/api/v1", tags=["webhooks"])
# Also include webhooks at /webhooks prefix to support direct webhook URLs like /webhooks/stripe
app.include_router(webhooks.router, prefix="/webhooks", tags=["webhooks-external"])
# Include webhooks at the root level to handle /api/v1/webhooks/*
# Webhook routes are defined with full /api/v1/webhooks/* paths for consistency
app.include_router(webhooks.router, prefix="", tags=["webhooks"])
# ================================================================
@@ -257,7 +300,7 @@ def _determine_event_type(event_data: dict) -> str:
# SERVER-SENT EVENTS (SSE) ENDPOINT
# ================================================================
@app.get("/api/events")
@app.get("/api/v1/events")
async def events_stream(
request: Request,
tenant_id: str,