feat: shared library for stock analysis microservices
- Common models (Stock, DailyPrice, Valuation, ScreeningResult) - Database connectors (PostgreSQL via asyncpg, MongoDB via motor, Redis) - Redis Streams pub/sub utilities - Base collector class with common patterns - Logging configuration with structlog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
98
src/stock_common/api_base.py
Normal file
98
src/stock_common/api_base.py
Normal file
@ -0,0 +1,98 @@
|
||||
"""Shared FastAPI app factory and common endpoints for all microservices.
|
||||
|
||||
Each service calls `create_app()` to get a FastAPI instance with:
|
||||
- /health - liveness/readiness check
|
||||
- /streams - Redis stream queue lengths
|
||||
"""
|
||||
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator, Callable, Awaitable
|
||||
|
||||
import structlog
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from stock_common.queue.redis_client import get_redis, close_redis
|
||||
|
||||
logger = structlog.get_logger(module="api_base")
|
||||
|
||||
_start_time: float = 0.0
|
||||
|
||||
|
||||
def create_app(
|
||||
title: str,
|
||||
version: str = "0.1.0",
|
||||
streams: list[str] | None = None,
|
||||
on_startup: Callable[[], Awaitable[None]] | None = None,
|
||||
on_shutdown: Callable[[], Awaitable[None]] | None = None,
|
||||
) -> FastAPI:
|
||||
"""Create a FastAPI app with common health/stream endpoints.
|
||||
|
||||
Args:
|
||||
title: Service name (e.g., "stock-dart-collector").
|
||||
version: API version.
|
||||
streams: Redis stream names this service reads/writes.
|
||||
on_startup: Optional async callback run on startup.
|
||||
on_shutdown: Optional async callback run on shutdown.
|
||||
"""
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
global _start_time
|
||||
_start_time = time.time()
|
||||
if on_startup:
|
||||
await on_startup()
|
||||
logger.info("api_started", service=title)
|
||||
yield
|
||||
if on_shutdown:
|
||||
await on_shutdown()
|
||||
await close_redis()
|
||||
logger.info("api_stopped", service=title)
|
||||
|
||||
app = FastAPI(title=title, version=version, lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
uptime = time.time() - _start_time
|
||||
try:
|
||||
r = await get_redis()
|
||||
await r.ping()
|
||||
redis_ok = True
|
||||
except Exception:
|
||||
redis_ok = False
|
||||
return {
|
||||
"service": title,
|
||||
"status": "ok" if redis_ok else "degraded",
|
||||
"redis": redis_ok,
|
||||
"uptime_seconds": round(uptime, 1),
|
||||
}
|
||||
|
||||
@app.get("/streams")
|
||||
async def stream_info():
|
||||
if not streams:
|
||||
return {"streams": {}}
|
||||
r = await get_redis()
|
||||
info = {}
|
||||
for stream_name in streams:
|
||||
try:
|
||||
length = await r.xlen(stream_name)
|
||||
info[stream_name] = {"length": length}
|
||||
except Exception:
|
||||
info[stream_name] = {"length": -1, "error": "stream not found"}
|
||||
return {"streams": info}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def run_api(app: FastAPI, host: str = "0.0.0.0", port: int = 8000) -> None:
|
||||
"""Run the FastAPI app with uvicorn (blocking)."""
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
Reference in New Issue
Block a user