import os
import sys
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from loguru import logger
from dotenv import load_dotenv
import time
import api
from app.database import engine
from app.models import Base
# Load environment variables early
load_dotenv() # Removed override=True - don't override existing env vars
# Configuration from environment
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "7860"))
RELOAD = os.getenv("RELOAD", "true").lower() == "true"
LOG = os.getenv("LOG_LEVEL", "DEBUG")
ALLOWED_ORIGINS_ENV = os.getenv("ALLOWED_ORIGINS", "")
# Logging setup
logger.remove()
# Simplified plain-text format
LOG_FORMAT = (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <7} | "
"{message}"
)
# Configure with defaults for extra fields to prevent KeyError
logger.configure(
extra={"context": "SYS", "url": "GLOBAL"},
patcher=lambda record: (
record["extra"].setdefault("context", "SYS")
or record["extra"].setdefault("url", "GLOBAL")
),
)
logger.add(sys.stderr, format=LOG_FORMAT, colorize=True, level=LOG)
log = logger.bind(context="STARTUP")
# Lifespan context manager for startup/shutdown events
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Handle startup and shutdown events"""
# Startup logging (Moved from module level to avoid duplication)
if not ALLOWED_ORIGINS_ENV:
log.warning(
"CORS: Using default origins (localhost/127.0.0.1) - set ALLOWED_ORIGINS for production"
)
else:
log.info(
f"CORS: Configured with {len(ALLOWED_ORIGINS_ENV.split(','))} custom origins"
)
log.info("Starting up Web Scraper API...")
try:
Base.metadata.create_all(bind=engine)
log.info("Database: Tables verified/created")
except Exception as e:
log.error(f"Database: Initialization failed: {e}")
raise
yield
# Shutdown
log.info("Shutting down Web Scraper API...")
engine.dispose()
log.info("Database connections closed")
# FastAPI app initialization
app = FastAPI(
title="Web Scraper API",
description="API for triggering and polling asynchronous web scraping jobs.",
version="1.0.0",
lifespan=lifespan,
)
# Middleware for logging requests
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
# cleaner, standardized visit log
log.info(
f"VISIT: {getattr(request.client, 'host', 'unknown')} - "
f"{request.method} {request.url.path} - "
f"Status: {response.status_code} - "
f"Time: {duration:.4f}s"
)
return response
# CORS configuration
if not ALLOWED_ORIGINS_ENV:
origins = [
"http://localhost",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:8000",
]
else:
origins = [origin.strip() for origin in ALLOWED_ORIGINS_ENV.split(",")]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API routes
app.include_router(api.router)
# Serve static files from the Frontend/dist directory
# Ensure 'dist' folder is copied to the backend during build
frontend_path = os.path.join(os.path.dirname(__file__), "dist")
if os.path.exists(frontend_path):
app.mount("/", StaticFiles(directory=frontend_path, html=True), name="static")
else:
log.warning(f"Frontend: 'dist' folder not found at {frontend_path}. Static file serving disabled.")
# Run server
if __name__ == "__main__":
log.info(f"Starting server on {HOST}:{PORT}")
uvicorn.run(
"main:app",
host=HOST,
port=PORT,
reload=RELOAD,
log_config=None, # Disabled uvicorn's default logging since we use loguru
)