""" Volatile cache router for Library Desk API. Endpoints for ephemeral cached data with TTL - weather, news, financial, etc. """ from fastapi import APIRouter, HTTPException, Depends, Query import logging from src.models.volatile import ( VolatileRecordCreate, VolatileRecordResponse, VolatileListResponse, VolatileScheduledResponse, VolatileStatsResponse, VolatileDeleteResponse, VolatileBulkDeleteResponse, VolatileNamespace, ) from src.services.volatile_service import VolatileCacheService from src.core.dependencies import verify_api_key, RedisDep from src.core.multi_tenancy import DEFAULT_USER from src.config import get_settings logger = logging.getLogger(__name__) router = APIRouter(prefix="/volatile", tags=["Volatile Cache"]) def get_volatile_service(redis: RedisDep) -> VolatileCacheService: """Get volatile cache service instance.""" settings = get_settings() return VolatileCacheService(redis_client=redis, settings=settings) @router.get("/stats", response_model=VolatileStatsResponse) async def get_stats( user: str = Query(default=DEFAULT_USER, description="User identifier"), redis: RedisDep = None, api_key: str = Depends(verify_api_key) ): """ Get volatile cache statistics. Returns counts of records by namespace and scheduled refresh info. """ service = get_volatile_service(redis) stats = await service.get_stats(user) return VolatileStatsResponse( total_records=stats["total_records"], by_namespace=stats["by_namespace"], scheduled_count=stats["scheduled_count"], total_memory_bytes=stats.get("total_memory_bytes"), user=user, ) @router.get("/scheduled", response_model=VolatileScheduledResponse) async def get_scheduled( user: str = Query(default=DEFAULT_USER, description="User identifier"), redis: RedisDep = None, api_key: str = Depends(verify_api_key) ): """ Get records with refresh schedules. Used by scheduler to determine what volatile data needs refreshing. Returns all records that have a refresh_schedule cron expression set. """ service = get_volatile_service(redis) records = await service.get_scheduled(user) return VolatileScheduledResponse( records=records, count=len(records), user=user, ) @router.get("/namespaces") async def list_namespaces( api_key: str = Depends(verify_api_key) ): """ List available namespaces and their default TTLs. Returns predefined namespaces with their default TTL values. Custom namespaces can also be used with the default TTL. """ from src.models.volatile import NAMESPACE_DEFAULT_TTL return { "namespaces": [ { "name": ns.value, "default_ttl": NAMESPACE_DEFAULT_TTL.get(ns, 3600), "description": _get_namespace_description(ns), } for ns in VolatileNamespace ] } def _get_namespace_description(ns: VolatileNamespace) -> str: """Get human-readable description for namespace.""" descriptions = { VolatileNamespace.WEATHER: "Weather conditions and forecasts", VolatileNamespace.NEWS: "Headlines and breaking news", VolatileNamespace.FINANCIAL: "Stock prices, exchange rates, crypto", VolatileNamespace.TRANSIT: "Train/bus schedules, delays", VolatileNamespace.TRAFFIC: "Commute times, road conditions", VolatileNamespace.AIR_QUALITY: "Pollution levels, pollen counts", VolatileNamespace.SPORTS: "Live scores, upcoming matches", VolatileNamespace.SOCIAL: "Social media mentions, notifications", VolatileNamespace.SYSTEM: "Service health, infrastructure status", VolatileNamespace.CONTEXT: "Conversation context, session state", VolatileNamespace.CUSTOM: "User-defined volatile data", } return descriptions.get(ns, "Custom namespace") @router.get("/{namespace}", response_model=VolatileListResponse) async def list_keys( namespace: str, user: str = Query(default=DEFAULT_USER, description="User identifier"), redis: RedisDep = None, api_key: str = Depends(verify_api_key) ): """ List all keys in a namespace. Returns the list of keys stored in the specified namespace. """ service = get_volatile_service(redis) keys = await service.list_namespace(user, namespace) return VolatileListResponse( namespace=namespace, keys=keys, count=len(keys), user=user, ) @router.delete("/{namespace}", response_model=VolatileBulkDeleteResponse) async def delete_namespace( namespace: str, user: str = Query(default=DEFAULT_USER, description="User identifier"), redis: RedisDep = None, api_key: str = Depends(verify_api_key) ): """ Delete all records in a namespace. Removes all volatile data for the specified namespace. """ service = get_volatile_service(redis) deleted = await service.delete_namespace(user, namespace) return VolatileBulkDeleteResponse( namespace=namespace, deleted_count=deleted, user=user, ) @router.get("/{namespace}/{key}", response_model=VolatileRecordResponse) async def get_record( namespace: str, key: str, user: str = Query(default=DEFAULT_USER, description="User identifier"), redis: RedisDep = None, api_key: str = Depends(verify_api_key) ): """ Get a volatile record. Returns the record if it exists and has not expired. **Example:** ``` GET /volatile/weather/rotterdam?user=jpmschweitzer ``` """ service = get_volatile_service(redis) record = await service.get(user, namespace, key) if not record: raise HTTPException( status_code=404, detail=f"Record '{key}' not found in namespace '{namespace}'" ) return record @router.post("/{namespace}/{key}", response_model=VolatileRecordResponse) async def set_record( namespace: str, key: str, request: VolatileRecordCreate, user: str = Query(default=DEFAULT_USER, description="User identifier"), redis: RedisDep = None, api_key: str = Depends(verify_api_key) ): """ Store or update a volatile record. Creates or updates a record with the specified TTL. If TTL is not provided, the namespace default is used. **Example Request:** ```json { "data": { "temperature": 18, "conditions": "Partly cloudy", "humidity": 65 }, "source": "openweathermap", "ttl": 1800, "refresh_schedule": "0 * * * *" } ``` **Refresh Schedule:** Optional cron expression for automatic refresh. The scheduler will query `/volatile/scheduled` and trigger refreshes. """ service = get_volatile_service(redis) try: record = await service.set( user=user, namespace=namespace, key=key, data=request.data, source=request.source, ttl=request.ttl, refresh_schedule=request.refresh_schedule, ) return record except Exception as e: logger.error(f"Failed to store volatile record: {e}") raise HTTPException(status_code=500, detail="Failed to store record") @router.delete("/{namespace}/{key}", response_model=VolatileDeleteResponse) async def delete_record( namespace: str, key: str, user: str = Query(default=DEFAULT_USER, description="User identifier"), redis: RedisDep = None, api_key: str = Depends(verify_api_key) ): """ Delete a volatile record. Removes the record from the cache. """ service = get_volatile_service(redis) deleted = await service.delete(user, namespace, key) return VolatileDeleteResponse( key=key, namespace=namespace, deleted=deleted, user=user, )