# app/config.py import os from pydantic_settings import BaseSettings from dotenv import load_dotenv import logging import secrets load_dotenv() logger = logging.getLogger(__name__) class Settings(BaseSettings): DATABASE_URL: str | None = None GEMINI_API_KEY: str | None = None # --- JWT Settings --- SECRET_KEY: str # Must be set via environment variable ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # Default token lifetime: 30 minutes REFRESH_TOKEN_EXPIRE_MINUTES: int = 10080 # Default refresh token lifetime: 7 days # --- OCR Settings --- MAX_FILE_SIZE_MB: int = 10 # Maximum allowed file size for OCR processing ALLOWED_IMAGE_TYPES: list[str] = ["image/jpeg", "image/png", "image/webp"] # Supported image formats OCR_ITEM_EXTRACTION_PROMPT: str = """ Extract the shopping list items from this image. List each distinct item on a new line. Ignore prices, quantities, store names, discounts, taxes, totals, and other non-item text. Focus only on the names of the products or items to be purchased. Add 2 underscores before and after the item name, if it is struck through. If the image does not appear to be a shopping list or receipt, state that clearly. Example output for a grocery list: Milk Eggs Bread __Apples__ Organic Bananas """ # --- Gemini AI Settings --- GEMINI_MODEL_NAME: str = "gemini-2.0-flash" # The model to use for OCR GEMINI_SAFETY_SETTINGS: dict = { "HARM_CATEGORY_HATE_SPEECH": "BLOCK_MEDIUM_AND_ABOVE", "HARM_CATEGORY_DANGEROUS_CONTENT": "BLOCK_MEDIUM_AND_ABOVE", "HARM_CATEGORY_HARASSMENT": "BLOCK_MEDIUM_AND_ABOVE", "HARM_CATEGORY_SEXUALLY_EXPLICIT": "BLOCK_MEDIUM_AND_ABOVE", } GEMINI_GENERATION_CONFIG: dict = { "candidate_count": 1, "max_output_tokens": 2048, "temperature": 0.9, "top_p": 1, "top_k": 1 } # --- API Settings --- API_PREFIX: str = "/api" # Base path for all API endpoints API_OPENAPI_URL: str = "/api/openapi.json" API_DOCS_URL: str = "/api/docs" API_REDOC_URL: str = "/api/redoc" CORS_ORIGINS: list[str] = [ "http://localhost:5174", "http://localhost:8000", "http://localhost:9000", # Add your deployed frontend URL here later # "https://your-frontend-domain.com", ] # --- API Metadata --- API_TITLE: str = "Shared Lists API" API_DESCRIPTION: str = "API for managing shared shopping lists, OCR, and cost splitting." API_VERSION: str = "0.1.0" ROOT_MESSAGE: str = "Welcome to the Shared Lists API! Docs available at /api/docs" # --- Logging Settings --- LOG_LEVEL: str = "INFO" LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # --- Health Check Settings --- HEALTH_STATUS_OK: str = "ok" HEALTH_STATUS_ERROR: str = "error" # --- Auth Settings --- OAUTH2_TOKEN_URL: str = "/api/v1/auth/login" # Path to login endpoint TOKEN_TYPE: str = "bearer" # Default token type for OAuth2 AUTH_HEADER_PREFIX: str = "Bearer" # Prefix for Authorization header AUTH_HEADER_NAME: str = "WWW-Authenticate" # Name of auth header AUTH_CREDENTIALS_ERROR: str = "Could not validate credentials" AUTH_INVALID_CREDENTIALS: str = "Incorrect email or password" AUTH_NOT_AUTHENTICATED: str = "Not authenticated" # --- HTTP Status Messages --- HTTP_400_DETAIL: str = "Bad Request" HTTP_401_DETAIL: str = "Unauthorized" HTTP_403_DETAIL: str = "Forbidden" HTTP_404_DETAIL: str = "Not Found" HTTP_422_DETAIL: str = "Unprocessable Entity" HTTP_429_DETAIL: str = "Too Many Requests" HTTP_500_DETAIL: str = "Internal Server Error" HTTP_503_DETAIL: str = "Service Unavailable" class Config: env_file = ".env" env_file_encoding = 'utf-8' extra = "ignore" settings = Settings() # Validation for critical settings if settings.DATABASE_URL is None: raise ValueError("DATABASE_URL environment variable must be set.") # Enforce secure secret key if not settings.SECRET_KEY: raise ValueError("SECRET_KEY environment variable must be set. Generate a secure key using: openssl rand -hex 32") # Validate secret key strength if len(settings.SECRET_KEY) < 32: raise ValueError("SECRET_KEY must be at least 32 characters long for security") if settings.GEMINI_API_KEY is None: logger.error("CRITICAL: GEMINI_API_KEY environment variable not set. Gemini features will be unavailable.") else: # Optional: Log partial key for confirmation (avoid logging full key) logger.info(f"GEMINI_API_KEY loaded (starts with: {settings.GEMINI_API_KEY[:4]}...).")