Files
signal-platform/app/main.py
Dennis Thiessen 181cfe6588
Some checks failed
Deploy / lint (push) Failing after 8s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped
major update
2026-02-27 16:08:09 +01:00

159 lines
6.0 KiB
Python

"""FastAPI application entry point with lifespan management."""
# ---------------------------------------------------------------------------
# SSL + proxy injection — MUST happen before any HTTP client imports
# ---------------------------------------------------------------------------
import os as _os
import ssl as _ssl
from pathlib import Path as _Path
_COMBINED_CERT = _Path(__file__).resolve().parent.parent / "combined-ca-bundle.pem"
if _COMBINED_CERT.exists():
_cert_path = str(_COMBINED_CERT)
# Env vars for libraries that respect them (requests, urllib3)
_os.environ["SSL_CERT_FILE"] = _cert_path
_os.environ["REQUESTS_CA_BUNDLE"] = _cert_path
_os.environ["CURL_CA_BUNDLE"] = _cert_path
# Monkey-patch ssl.create_default_context so that ALL libraries
# (aiohttp, httpx, google-genai, alpaca-py, etc.) automatically
# use our combined CA bundle that includes the corporate root cert.
_original_create_default_context = _ssl.create_default_context
def _patched_create_default_context(
purpose=_ssl.Purpose.SERVER_AUTH, *, cafile=None, capath=None, cadata=None
):
ctx = _original_create_default_context(
purpose, cafile=cafile, capath=capath, cadata=cadata
)
# Always load our combined bundle on top of whatever was loaded
ctx.load_verify_locations(cafile=_cert_path)
return ctx
_ssl.create_default_context = _patched_create_default_context
# Also patch aiohttp's cached SSL context objects directly, since
# aiohttp creates them at import time and may have already cached
# a context without our corporate CA bundle.
try:
import aiohttp.connector as _aio_conn
if hasattr(_aio_conn, '_SSL_CONTEXT_VERIFIED') and _aio_conn._SSL_CONTEXT_VERIFIED is not None:
_aio_conn._SSL_CONTEXT_VERIFIED.load_verify_locations(cafile=_cert_path)
if hasattr(_aio_conn, '_SSL_CONTEXT_UNVERIFIED') and _aio_conn._SSL_CONTEXT_UNVERIFIED is not None:
_aio_conn._SSL_CONTEXT_UNVERIFIED.load_verify_locations(cafile=_cert_path)
except ImportError:
pass
# Corporate proxy — needed when Kiro spawns the process (no .zshrc sourced)
_PROXY = "http://aproxy.corproot.net:8080"
_NO_PROXY = "corproot.net,sharedtcs.net,127.0.0.1,localhost,bix.swisscom.com,swisscom.com"
_os.environ.setdefault("HTTP_PROXY", _PROXY)
_os.environ.setdefault("HTTPS_PROXY", _PROXY)
_os.environ.setdefault("NO_PROXY", _NO_PROXY)
import logging
import sys
from contextlib import asynccontextmanager
from collections.abc import AsyncGenerator
from fastapi import FastAPI
from passlib.hash import bcrypt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database import async_session_factory, engine
from app.middleware import register_exception_handlers
from app.models.user import User
from app.scheduler import configure_scheduler, scheduler
from app.routers.admin import router as admin_router
from app.routers.auth import router as auth_router
from app.routers.health import router as health_router
from app.routers.ingestion import router as ingestion_router
from app.routers.ohlcv import router as ohlcv_router
from app.routers.indicators import router as indicators_router
from app.routers.fundamentals import router as fundamentals_router
from app.routers.scores import router as scores_router
from app.routers.trades import router as trades_router
from app.routers.watchlist import router as watchlist_router
from app.routers.sentiment import router as sentiment_router
from app.routers.sr_levels import router as sr_levels_router
from app.routers.tickers import router as tickers_router
def _configure_logging() -> None:
"""Set up structured JSON-style logging."""
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(
logging.Formatter(
'{"time":"%(asctime)s","level":"%(levelname)s",'
'"logger":"%(name)s","message":"%(message)s"}'
)
)
root = logging.getLogger()
root.handlers.clear()
root.addHandler(handler)
root.setLevel(settings.log_level.upper())
async def _create_default_admin(session: AsyncSession) -> None:
"""Create the default admin account if no admin user exists."""
result = await session.execute(
select(User).where(User.role == "admin")
)
if result.scalar_one_or_none() is None:
admin = User(
username="admin",
password_hash=bcrypt.hash("admin"),
role="admin",
has_access=True,
)
session.add(admin)
await session.commit()
logging.getLogger(__name__).info("Default admin account created")
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
"""Manage startup and shutdown lifecycle."""
logger = logging.getLogger(__name__)
_configure_logging()
logger.info("Starting Stock Data Backend")
async with async_session_factory() as session:
await _create_default_admin(session)
configure_scheduler()
scheduler.start()
logger.info("Scheduler started")
yield
scheduler.shutdown(wait=False)
logger.info("Scheduler stopped")
await engine.dispose()
logger.info("Shutting down")
app = FastAPI(
title="Stock Data Backend",
version="0.1.0",
lifespan=lifespan,
)
register_exception_handlers(app)
app.include_router(health_router, prefix="/api/v1")
app.include_router(auth_router, prefix="/api/v1")
app.include_router(admin_router, prefix="/api/v1")
app.include_router(tickers_router, prefix="/api/v1")
app.include_router(ohlcv_router, prefix="/api/v1")
app.include_router(ingestion_router, prefix="/api/v1")
app.include_router(indicators_router, prefix="/api/v1")
app.include_router(sr_levels_router, prefix="/api/v1")
app.include_router(sentiment_router, prefix="/api/v1")
app.include_router(fundamentals_router, prefix="/api/v1")
app.include_router(scores_router, prefix="/api/v1")
app.include_router(trades_router, prefix="/api/v1")
app.include_router(watchlist_router, prefix="/api/v1")