Files
library-desk/src/routers/volatile.py
T
jpmschweitzerandClaude Opus 4.5 7297e6b9f1 feat: add volatile cache system for ephemeral data
Phase 2 of Memory Management System - volatile memory tier:

- VolatileCacheService: Redis-backed TTL storage
- Volatile router with full CRUD operations
- Predefined namespaces: weather, news, financial, transit, traffic,
  air_quality, sports, social, system, context, custom
- Each namespace has appropriate default TTL (1min to 1hr)
- Refresh schedule support via cron expressions
- Scheduler integration endpoint: GET /volatile/scheduled

Endpoints:
- GET/POST/DELETE /volatile/{namespace}/{key}
- GET/DELETE /volatile/{namespace}
- GET /volatile/stats
- GET /volatile/scheduled
- GET /volatile/namespaces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 17:31:45 +01:00

272 lines
7.8 KiB
Python

"""
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,
)