You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

247 lines
8.8 KiB
Python

"""
Main FastAPI application
"""
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, HTMLResponse
from app.core.config import settings
from app.api.v1.api import api_router
from app.core.database import engine, Base
from app.middleware.error_logger import ErrorLoggingMiddleware
from app.models import error_log, request_log, fred_data, filing, finra_short_volume, alpaca_price # Import to register models
from app.models import overlay_registry, overlay_raw_event, overlay_feature # Overlay models
# Create database tables
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup - ensure tables exist
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Start overlay batch scheduler (graceful no-op if apscheduler not installed)
try:
from app.services.overlay.scheduler import start_scheduler
start_scheduler()
except Exception:
pass
yield
# Shutdown
try:
from app.services.overlay.scheduler import stop_scheduler
stop_scheduler()
except Exception:
pass
await engine.dispose()
# Create FastAPI app
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
openapi_url=f"{settings.API_PREFIX}/openapi.json",
docs_url=f"{settings.API_PREFIX}/docs",
redoc_url=f"{settings.API_PREFIX}/redoc",
lifespan=lifespan
)
# Add error logging middleware
app.add_middleware(ErrorLoggingMiddleware)
# Set up CORS - use configured origins if available, otherwise allow all
cors_origins = [str(o) for o in settings.BACKEND_CORS_ORIGINS] if settings.BACKEND_CORS_ORIGINS else ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router, prefix=settings.API_PREFIX)
# Root documentation endpoint — auto-generated from OpenAPI schema
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def root_documentation(request: Request):
try:
schema = request.app.openapi()
paths = schema.get("paths", {})
api_prefix = settings.API_PREFIX
# Collect all tags defined in schema (preserves order)
tag_order = [t["name"] for t in schema.get("tags", [])]
# Group routes by first tag
from collections import defaultdict
tag_routes: dict = defaultdict(list)
untagged: list = []
for path, methods in paths.items():
for method, op in methods.items():
if method.upper() not in ("GET", "POST", "PUT", "DELETE", "PATCH"):
continue
tags = op.get("tags", [])
entry = {
"method": method.upper(),
"path": path,
"summary": op.get("summary", ""),
"deprecated": op.get("deprecated", False),
}
if tags:
tag_routes[tags[0]].append(entry)
else:
untagged.append(entry)
# Build tag sections — respect schema tag order, then alphabetical remainder
all_tags = tag_order + sorted(t for t in tag_routes if t not in tag_order)
if untagged:
all_tags.append("other")
tag_routes["other"] = untagged
method_colors = {
"GET": "#61affe",
"POST": "#49cc90",
"PUT": "#fca130",
"DELETE": "#f93e3e",
"PATCH": "#50e3c2",
}
sections_html = ""
for tag in all_tags:
routes = tag_routes.get(tag, [])
if not routes:
continue
rows = ""
for r in routes:
color = method_colors.get(r["method"], "#999")
deprecated = " style='opacity:0.5;text-decoration:line-through'" if r["deprecated"] else ""
display_path = r["path"].removeprefix(api_prefix)
rows += (
f"<tr{deprecated}>"
f"<td><span class='method' style='background:{color}'>{r['method']}</span></td>"
f"<td><code>{display_path}</code></td>"
f"<td>{r['summary']}</td>"
f"</tr>"
)
sections_html += f"<h3>{tag}</h3><table>{rows}</table>"
title = schema.get("info", {}).get("title", "API")
version = schema.get("info", {}).get("version", "")
total = sum(len(v) for v in tag_routes.values())
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6; max-width: 1100px; margin: 0 auto; padding: 24px; color: #333; }}
h1 {{ color: #1a1a2e; border-bottom: 3px solid #007acc; padding-bottom: 10px; }}
h3 {{ color: #2c3e50; margin-top: 2em; text-transform: capitalize; border-left: 4px solid #007acc; padding-left: 10px; }}
.nav {{ margin: 20px 0; display: flex; gap: 10px; flex-wrap: wrap; }}
.nav a {{ padding: 8px 18px; background: #007acc; color: white; text-decoration: none;
border-radius: 6px; font-size: 0.9em; }}
.nav a:hover {{ background: #005fa3; }}
.meta {{ color: #666; font-size: 0.9em; margin-bottom: 1.5em; }}
table {{ width: 100%; border-collapse: collapse; margin-bottom: 1em; font-size: 0.9em; }}
tr:hover {{ background: #f8f9fa; }}
td {{ padding: 7px 10px; border-bottom: 1px solid #eee; vertical-align: top; }}
td:first-child {{ width: 72px; }}
td:nth-child(2) {{ width: 38%; }}
code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; word-break: break-all; }}
.method {{ display: inline-block; color: white; font-weight: bold; font-size: 0.75em;
padding: 3px 8px; border-radius: 4px; letter-spacing: 0.5px; }}
footer {{ margin-top: 3em; padding-top: 1.5em; border-top: 1px solid #eee;
text-align: center; color: #888; font-size: 0.85em; }}
</style>
</head>
<body>
<h1>🔮 {title}</h1>
<div class="nav">
<a href="{api_prefix}/docs">📋 Swagger UI</a>
<a href="{api_prefix}/redoc">📖 ReDoc</a>
<a href="{api_prefix}/health">💚 Health</a>
<a href="{api_prefix}/openapi.json">⚙️ OpenAPI JSON</a>
</div>
<p class="meta">v{version} &nbsp;·&nbsp; Base URL: <code>{api_prefix}</code> &nbsp;·&nbsp; {total} endpoints</p>
{sections_html}
<footer>Auto-generated from OpenAPI schema · <a href="{api_prefix}/docs">Full interactive docs</a></footer>
</body>
</html>"""
return HTMLResponse(content=html)
except Exception:
return RedirectResponse(url=f"{settings.API_PREFIX}/docs")
# Additional metadata for OpenAPI
app.openapi_tags = [
{
"name": "health",
"description": "Health check endpoints"
},
{
"name": "financial",
"description": "Financial data retrieval endpoints"
},
{
"name": "price",
"description": "Price data endpoints (OHLCV)"
},
{
"name": "news",
"description": "News and social media endpoints"
},
{
"name": "metadata",
"description": "Data catalog and metadata endpoints"
},
{
"name": "filings",
"description": "SEC filings search, document listing, and exhibit extraction (8-K, 6-K, 20-F, 40-F)"
},
{
"name": "etf",
"description": "ETF holdings endpoints"
},
{
"name": "alpaca",
"description": "Alpaca Market Data endpoints (OHLCV bars, connection status)"
},
{
"name": "finra",
"description": "FINRA RegSHO short sale volume data (ingest, query, ratio history)"
},
{
"name": "admin",
"description": "Administrative endpoints (migration, etc.)"
},
{
"name": "overlay",
"description": "Attention Overlay - retail investor interest, media diffusion, crowding signals"
},
{
"name": "overlay-admin",
"description": "Overlay administrative endpoints (pipeline trigger, health, job log)"
},
{
"name": "screener",
"description": "Stock screener — condition-based filtering by market cap, volume, price, P/E, sector, exchange"
},
{
"name": "stocks",
"description": "Stock market data — most active, 52-week gainers, trending, and index constituents (S&P 500 / Nasdaq 100)"
}
]
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=settings.API_PORT,
reload=settings.DEBUG
)