# app/config.py import os from pydantic_settings import BaseSettings from dotenv import load_dotenv import logging import secrets from typing import List load_dotenv() logger = logging.getLogger(__name__) class Settings(BaseSettings): DATABASE_URL: str | None = None GEMINI_API_KEY: str | None = None SENTRY_DSN: str | None = None # Sentry DSN for error tracking # --- Environment Settings --- ENVIRONMENT: str = "development" # development, staging, production # --- JWT Settings --- (SECRET_KEY is used by FastAPI-Users) SECRET_KEY: str # Must be set via environment variable TOKEN_TYPE: str = "bearer" # Default token type for JWT authentication # FastAPI-Users handles JWT algorithm internally # --- 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 """ # --- OCR Error Messages --- OCR_SERVICE_UNAVAILABLE: str = "OCR service is currently unavailable. Please try again later." OCR_SERVICE_CONFIG_ERROR: str = "OCR service configuration error. Please contact support." OCR_UNEXPECTED_ERROR: str = "An unexpected error occurred during OCR processing." OCR_QUOTA_EXCEEDED: str = "OCR service quota exceeded. Please try again later." OCR_INVALID_FILE_TYPE: str = "Invalid file type. Supported types: {types}" OCR_FILE_TOO_LARGE: str = "File too large. Maximum size: {size}MB" OCR_PROCESSING_ERROR: str = "Error processing image: {detail}" # --- 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 - environment dependent CORS_ORIGINS: str = "http://localhost:5173,http://localhost:5174,http://localhost:8000,http://127.0.0.1:5173,http://127.0.0.1:5174,http://127.0.0.1:8000" FRONTEND_URL: str = "http://localhost:5173" # URL for the frontend application # --- 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 = "WARNING" LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # --- Health Check Settings --- HEALTH_STATUS_OK: str = "ok" HEALTH_STATUS_ERROR: str = "error" # --- 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" # --- Database Error Messages --- DB_CONNECTION_ERROR: str = "Database connection error" DB_INTEGRITY_ERROR: str = "Database integrity error" DB_TRANSACTION_ERROR: str = "Database transaction error" DB_QUERY_ERROR: str = "Database query error" # --- Auth Error Messages --- AUTH_INVALID_CREDENTIALS: str = "Invalid username or password" AUTH_NOT_AUTHENTICATED: str = "Not authenticated" AUTH_JWT_ERROR: str = "JWT token error: {error}" AUTH_JWT_UNEXPECTED_ERROR: str = "Unexpected JWT error: {error}" AUTH_HEADER_NAME: str = "WWW-Authenticate" AUTH_HEADER_PREFIX: str = "Bearer" # OAuth Settings GOOGLE_CLIENT_ID: str = "" GOOGLE_CLIENT_SECRET: str = "" GOOGLE_REDIRECT_URI: str = "http://localhost:8000/api/v1/auth/google/callback" APPLE_CLIENT_ID: str = "" APPLE_TEAM_ID: str = "" APPLE_KEY_ID: str = "" APPLE_PRIVATE_KEY: str = "" APPLE_REDIRECT_URI: str = "http://localhost:8000/api/v1/auth/apple/callback" # Session Settings SESSION_SECRET_KEY: str = "your-session-secret-key" # Change this in production ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 # 8 hours instead of 30 minutes # Redis Settings REDIS_URL: str = "redis://localhost:6379" REDIS_PASSWORD: str = "" class Config: env_file = ".env" env_file_encoding = 'utf-8' extra = "ignore" @property def cors_origins_list(self) -> List[str]: """Convert CORS_ORIGINS string to list""" return [origin.strip() for origin in self.CORS_ORIGINS.split(",")] @property def is_production(self) -> bool: """Check if running in production environment""" return self.ENVIRONMENT.lower() == "production" @property def is_development(self) -> bool: """Check if running in development environment""" return self.ENVIRONMENT.lower() == "development" @property def docs_url(self) -> str | None: """Return docs URL only in development""" return self.API_DOCS_URL if self.is_development else None @property def redoc_url(self) -> str | None: """Return redoc URL only in development""" return self.API_REDOC_URL if self.is_development else None @property def openapi_url(self) -> str | None: """Return OpenAPI URL only in development""" return self.API_OPENAPI_URL if self.is_development else None 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") # Production-specific validations if settings.is_production: if settings.SESSION_SECRET_KEY == "your-session-secret-key": raise ValueError("SESSION_SECRET_KEY must be changed from default value in production") if not settings.SENTRY_DSN: logger.warning("SENTRY_DSN not set in production environment. Error tracking will be unavailable.") 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]}...).") # Log environment information logger.info(f"Application starting in {settings.ENVIRONMENT} environment") if settings.is_production: logger.info("Production mode: API documentation disabled") else: logger.info(f"Development mode: API documentation available at {settings.API_DOCS_URL}")