feat: add volatile fetch endpoints for scheduler-driven prefetch

- Add VolatileFetchService to orchestrate API fetch and cache storage
- Add POST /volatile/fetch/weather/{city} endpoint
- Add POST /volatile/fetch/news/{category} endpoint
- Add POST /volatile/fetch/stock/{symbol} endpoint
- Add POST /volatile/fetch/crypto/{symbol} endpoint

Endpoints integrate with external API providers (OpenMeteo, NOS/BBC,
AlphaVantage) and store results in volatile cache with configurable TTL.
Designed for scheduler cron jobs to prefetch user-relevant data.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-26 13:40:40 +01:00
co-authored by Claude Opus 4.5
parent 2b8c229f53
commit ab892745fa
2 changed files with 550 additions and 1 deletions
+191 -1
View File
@@ -19,7 +19,15 @@ from src.models.volatile import (
NAMESPACE_DEFAULT_TTL,
)
from src.services.volatile_service import VolatileCacheService
from src.core.dependencies import verify_api_key, QdrantDep, OllamaDep
from src.services.volatile_fetch_service import VolatileFetchService
from src.core.dependencies import (
verify_api_key,
QdrantDep,
OllamaDep,
get_weather_provider,
get_news_provider,
get_alphavantage_provider,
)
from src.core.multi_tenancy import DEFAULT_USER
from src.config import get_settings
@@ -221,6 +229,188 @@ async def store_volatile(
raise HTTPException(status_code=500, detail=f"Failed to store record: {str(e)}")
@router.post("/fetch/weather/{city}")
async def fetch_weather(
city: str,
user: str = Query(default=DEFAULT_USER, description="User identifier"),
ttl: int = Query(default=86400, ge=60, le=604800, description="TTL in seconds"),
qdrant: QdrantDep = None,
ollama: OllamaDep = None,
api_key: str = Depends(verify_api_key)
):
"""
Fetch current weather for a city and store in volatile cache.
Called by scheduler for prefetch or on-demand. Geocodes city name
and fetches weather from Open-Meteo API.
**Example:**
```
POST /volatile/fetch/weather/amsterdam?user=jpmschweitzer
```
"""
volatile_service = get_volatile_service(qdrant, ollama)
weather_provider = get_weather_provider()
fetch_service = VolatileFetchService(
volatile_service=volatile_service,
weather_provider=weather_provider,
)
result = await fetch_service.fetch_weather(user, city, ttl=ttl)
if not result.success:
raise HTTPException(status_code=500, detail=result.error)
return {
"success": True,
"namespace": result.namespace,
"key": result.key,
"record": result.record,
}
@router.post("/fetch/news/{category}")
async def fetch_news(
category: str = "general",
user: str = Query(default=DEFAULT_USER, description="User identifier"),
limit: int = Query(default=10, ge=1, le=50, description="Max headlines"),
ttl: int = Query(default=7200, ge=60, le=86400, description="TTL in seconds"),
qdrant: QdrantDep = None,
ollama: OllamaDep = None,
api_key: str = Depends(verify_api_key)
):
"""
Fetch news headlines and store in volatile cache.
Fetches from configured news sources (NOS, BBC) based on user settings.
Categories: general, world, tech, business, politics, etc.
**Example:**
```
POST /volatile/fetch/news/tech?user=jpmschweitzer&limit=15
```
"""
volatile_service = get_volatile_service(qdrant, ollama)
weather_provider = get_weather_provider()
news_provider = await get_news_provider()
fetch_service = VolatileFetchService(
volatile_service=volatile_service,
weather_provider=weather_provider,
news_provider=news_provider,
)
result = await fetch_service.fetch_news(user, category, limit=limit, ttl=ttl)
if not result.success:
raise HTTPException(status_code=500, detail=result.error)
return {
"success": True,
"namespace": result.namespace,
"key": result.key,
"record": result.record,
}
@router.post("/fetch/stock/{symbol}")
async def fetch_stock(
symbol: str,
user: str = Query(default=DEFAULT_USER, description="User identifier"),
ttl: int = Query(default=300, ge=60, le=3600, description="TTL in seconds"),
qdrant: QdrantDep = None,
ollama: OllamaDep = None,
api_key: str = Depends(verify_api_key)
):
"""
Fetch stock quote and store in volatile cache.
Fetches from Alpha Vantage API. Requires API key configured in settings.
**Example:**
```
POST /volatile/fetch/stock/AAPL?user=jpmschweitzer
```
"""
volatile_service = get_volatile_service(qdrant, ollama)
weather_provider = get_weather_provider()
financial_provider = await get_alphavantage_provider()
if not financial_provider:
raise HTTPException(
status_code=503,
detail="Financial provider not configured (Alpha Vantage API key missing)"
)
fetch_service = VolatileFetchService(
volatile_service=volatile_service,
weather_provider=weather_provider,
financial_provider=financial_provider,
)
result = await fetch_service.fetch_stock(user, symbol, ttl=ttl)
if not result.success:
raise HTTPException(status_code=500, detail=result.error)
return {
"success": True,
"namespace": result.namespace,
"key": result.key,
"record": result.record,
}
@router.post("/fetch/crypto/{symbol}")
async def fetch_crypto(
symbol: str,
market: str = Query(default="USD", description="Market currency"),
user: str = Query(default=DEFAULT_USER, description="User identifier"),
ttl: int = Query(default=300, ge=60, le=3600, description="TTL in seconds"),
qdrant: QdrantDep = None,
ollama: OllamaDep = None,
api_key: str = Depends(verify_api_key)
):
"""
Fetch cryptocurrency quote and store in volatile cache.
Fetches from Alpha Vantage API. Requires API key configured in settings.
**Example:**
```
POST /volatile/fetch/crypto/BTC?market=EUR&user=jpmschweitzer
```
"""
volatile_service = get_volatile_service(qdrant, ollama)
weather_provider = get_weather_provider()
financial_provider = await get_alphavantage_provider()
if not financial_provider:
raise HTTPException(
status_code=503,
detail="Financial provider not configured (Alpha Vantage API key missing)"
)
fetch_service = VolatileFetchService(
volatile_service=volatile_service,
weather_provider=weather_provider,
financial_provider=financial_provider,
)
result = await fetch_service.fetch_crypto(user, symbol, market=market, ttl=ttl)
if not result.success:
raise HTTPException(status_code=500, detail=result.error)
return {
"success": True,
"namespace": result.namespace,
"key": result.key,
"record": result.record,
}
@router.get("/{namespace}/{key}", response_model=VolatileRecordResponse)
async def get_record(
namespace: str,