RL-Project / api /main.py
Shubham Sattigeri
fix: SSO dotenv path and auth routes
c5f36f3
Raw
History Blame Contribute Delete
8.58 kB
"""
FastAPI application entry point.
"""
import os
import json
import time
from dotenv import load_dotenv
from fastapi import FastAPI
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '..', '.env'))
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from authlib.integrations.starlette_client import OAuth
from .routes import router
from artwork_bandit.db import database
from artwork_bandit.features.nlp_encoder import NLPEncoder
from artwork_bandit.features.vision_encoder import VisionEncoder
from artwork_bandit.features.feature_store import FeatureStore
from artwork_bandit.bandit.linucb import LinUCBBandit
from artwork_bandit.bandit.thompson import ThompsonBandit
APP = FastAPI()
FRONTEND_URLS = os.getenv('FRONTEND_URLS', 'http://localhost:5173,http://127.0.0.1:5173').split(',')
FRONTEND_URL = FRONTEND_URLS[0].strip()
APP.add_middleware(
SessionMiddleware,
secret_key=os.getenv('SESSION_SECRET', 'replace-with-a-secure-secret'),
same_site='lax',
)
APP.add_middleware(
CORSMiddleware,
allow_origins=[url.strip() for url in FRONTEND_URLS if url.strip()],
allow_methods=['*'],
allow_headers=['*'],
allow_credentials=True,
)
SSO_DISCOVERY_URL = os.getenv('SSO_DISCOVERY_URL')
SSO_CLIENT_ID = os.getenv('SSO_CLIENT_ID')
SSO_CLIENT_SECRET = os.getenv('SSO_CLIENT_SECRET')
SSO_SCOPES = os.getenv('SSO_SCOPES', 'openid profile email')
APP.state.frontend_url = FRONTEND_URL
APP.state.sso_enabled = bool(SSO_DISCOVERY_URL and SSO_CLIENT_ID and SSO_CLIENT_SECRET)
print(f"[STARTUP] SSO enabled: {APP.state.sso_enabled}")
APP.state.oauth = None
if APP.state.sso_enabled:
oauth = OAuth()
oauth.register(
name='sso',
client_id=SSO_CLIENT_ID,
client_secret=SSO_CLIENT_SECRET,
server_metadata_url=SSO_DISCOVERY_URL,
client_kwargs={'scope': SSO_SCOPES},
)
APP.state.oauth = oauth
APP.include_router(router)
DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
USERS_F = os.path.join(DATA_DIR, 'users.json')
CONTENTS_F = os.path.join(DATA_DIR, 'content.json')
ARTWORK_F = os.path.join(DATA_DIR, 'artwork.json')
LINUCB_ALPHA = float(os.getenv('LINUCB_ALPHA', '1.0'))
@APP.on_event('startup')
def startup():
try:
print("[STARTUP] Loading contents...")
# load data
with open(CONTENTS_F, 'r', encoding='utf-8') as f:
contents = json.load(f)
print(f"[STARTUP] Loaded {len(contents)} contents")
print("[STARTUP] Checking users...")
# generate users/artworks if missing
if not os.path.exists(USERS_F) or os.path.getsize(USERS_F) == 0:
try:
from artwork_bandit.data.generate_synthetic import main as gen_main
gen_main()
except Exception as e:
print(f"[STARTUP] Warning: Failed to generate users: {e}")
with open(USERS_F, 'w', encoding='utf-8') as f:
json.dump([], f)
with open(USERS_F, 'r', encoding='utf-8') as f:
users = json.load(f)
print(f"[STARTUP] Loaded {len(users)} users")
print("[STARTUP] Checking artworks...")
if not os.path.exists(ARTWORK_F) or os.path.getsize(ARTWORK_F) == 0:
try:
from artwork_bandit.data.generate_synthetic import main as gen_main
gen_main()
except Exception as e:
print(f"[STARTUP] Warning: Failed to generate artworks: {e}")
with open(ARTWORK_F, 'w', encoding='utf-8') as f:
json.dump([], f)
with open(ARTWORK_F, 'r', encoding='utf-8') as f:
artworks = json.load(f)
print(f"[STARTUP] Loaded {len(artworks)} artworks")
print("[STARTUP] Initializing database...")
# init db
database.init_db()
print("[STARTUP] Database initialized")
print("[STARTUP] Creating encoders...")
# encoders - these are now lazy-loaded
nlp = NLPEncoder()
vis = VisionEncoder()
print("[STARTUP] Encoders created (lazy-loaded)")
print("[STARTUP] Creating feature store...")
fs = FeatureStore(database, nlp, vis)
print("[STARTUP] Feature store created")
print("[STARTUP] Precomputing embeddings...")
# precompute
fs.precompute_all(users, contents, artworks)
print("[STARTUP] Embeddings precomputed")
print("[STARTUP] Creating bandits...")
# bandits
# collect all artwork ids
all_arts = [a['artwork_id'] for a in artworks]
d = 384 + 384 + 512
linucb = LinUCBBandit(all_arts, d=d, alpha=LINUCB_ALPHA)
try:
linucb.load(database)
except Exception as e:
print(f"[STARTUP] Warning: Failed to load linucb state: {e}")
thompson = ThompsonBandit(all_arts)
print(f"[STARTUP] Bandits created (LinUCB with {len(all_arts)} arms, Thompson with {len(all_arts)} arms)")
print("[STARTUP] Attaching state...")
# attach
APP.state.feature_store = fs
APP.state.linucb = linucb
APP.state.thompson = thompson
APP.state.users = users
APP.state.contents = contents
APP.state.artworks = artworks
print("[STARTUP] State attached - startup complete!")
except Exception as e:
print(f"[STARTUP ERROR] {e}")
import traceback
traceback.print_exc()
raise
def init_app():
"""Idempotent initialisation run at import time to help tests and CLI use.
Runs a safe subset of the startup routine and ensures DB tables exist.
"""
try:
# create DB tables early so tests don't fail if startup() isn't invoked
database.init_db()
except Exception:
pass
try:
# load minimal data and set placeholder state if startup hasn't run
if not hasattr(APP.state, 'feature_store'):
with open(CONTENTS_F, 'r', encoding='utf-8') as f:
contents = json.load(f)
# ensure users/artworks exist
if not os.path.exists(USERS_F) or os.path.getsize(USERS_F) == 0:
try:
from artwork_bandit.data.generate_synthetic import main as gen_main
gen_main()
except Exception:
with open(USERS_F, 'w', encoding='utf-8') as f:
json.dump([], f)
with open(USERS_F, 'r', encoding='utf-8') as f:
users = json.load(f)
if not os.path.exists(ARTWORK_F) or os.path.getsize(ARTWORK_F) == 0:
try:
from data.generate_synthetic import main as gen_main
gen_main()
except Exception:
with open(ARTWORK_F, 'w', encoding='utf-8') as f:
json.dump([], f)
with open(ARTWORK_F, 'r', encoding='utf-8') as f:
artworks = json.load(f)
nlp = NLPEncoder()
vis = VisionEncoder()
fs = FeatureStore(database, nlp, vis)
fs.precompute_all(users, contents, artworks)
all_arts = [a['artwork_id'] for a in artworks]
d = 384 + 384 + 512
linucb = LinUCBBandit(all_arts, d=d, alpha=LINUCB_ALPHA)
thompson = ThompsonBandit(all_arts)
APP.state.feature_store = fs
APP.state.linucb = linucb
APP.state.thompson = thompson
APP.state.users = users
APP.state.contents = contents
APP.state.artworks = artworks
except Exception as e:
print(f"ERROR in init_app: {e}")
import traceback
traceback.print_exc()
# NOTE: Do NOT call init_app() at import time - let the startup event handle initialization
# This prevents encoder loading and model downloads from blocking the import
@APP.get('/health')
def health():
# impressions count
from artwork_bandit.db.database import SessionLocal, Impression
db = SessionLocal()
try:
cnt = db.query(Impression).count()
finally:
db.close()
return {"status": "ok", "impressions": cnt, "features_loaded": True}
@APP.on_event('shutdown')
def shutdown():
try:
APP.state.linucb.save(database)
except Exception:
pass
app = APP
if __name__ == '__main__':
import uvicorn
uvicorn.run('api.main:app', host='0.0.0.0', port=8000, reload=True)