| """ |
| Application configuration using pydantic-settings. |
| |
| Loads environment variables from .env file and provides type-safe access. |
| """ |
| from functools import lru_cache |
| from typing import Optional |
|
|
| from pydantic import Field, field_validator |
| from pydantic_settings import BaseSettings, SettingsConfigDict |
|
|
|
|
| class Settings(BaseSettings): |
| """Application settings loaded from environment variables.""" |
|
|
| model_config = SettingsConfigDict( |
| env_file='.env', |
| env_file_encoding='utf-8', |
| case_sensitive=False, |
| extra='ignore', |
| ) |
|
|
| |
| |
| |
| database_url: str = Field( |
| default='postgresql+psycopg://todoapp:todoapp_password@localhost:5432/todoapp', |
| description='PostgreSQL connection string', |
| ) |
|
|
| |
| |
| |
| jwt_secret: str = Field( |
| default='development-secret-key-change-in-production-min-32-chars', |
| min_length=32, |
| description='Secret key for JWT token signing (min 32 characters)', |
| ) |
|
|
| jwt_algorithm: str = Field(default='HS256', description='JWT algorithm') |
| jwt_expiration_days: int = Field(default=7, description='JWT token expiration in days') |
|
|
| |
| |
| |
| cloudinary_cloud_name: Optional[str] = Field( |
| default=None, description='Cloudinary cloud name' |
| ) |
| cloudinary_api_key: Optional[str] = Field(default=None, description='Cloudinary API key') |
| cloudinary_api_secret: Optional[str] = Field( |
| default=None, description='Cloudinary API secret' |
| ) |
|
|
| |
| |
| |
| huggingface_api_key: Optional[str] = Field( |
| default=None, description='Hugging Face API key' |
| ) |
|
|
| |
| |
| |
| gmail_email: Optional[str] = Field( |
| default=None, description='Gmail address for sending reminders' |
| ) |
| gmail_app_password: Optional[str] = Field( |
| default=None, description='Gmail app-specific password for SMTP' |
| ) |
|
|
| |
| |
| |
| frontend_url: str = Field( |
| default='http://localhost:3000', |
| description='Allowed CORS origin for frontend', |
| ) |
|
|
| |
| |
| |
| env: str = Field(default='development', description='Environment: development, staging, production') |
| port: int = Field(default=8801, description='API port') |
| log_level: str = Field(default='info', description='Log level: debug, info, warning, error, critical') |
|
|
| |
| |
| |
| bcrypt_rounds: int = Field(default=12, description='Bcrypt password hashing rounds') |
| cors_origins: str | list[str] = Field( |
| default=[ |
| 'http://localhost:3000', 'http://localhost:3001', 'http://localhost:3002', |
| 'http://127.0.0.1:3000', 'http://127.0.0.1:3001', 'http://127.0.0.1:3002', |
| 'https://todo-frontend-alpha-five.vercel.app', |
| 'https://todo-frontend.vercel.app', |
| 'https://*.vercel.app' |
| ], |
| description='CORS allowed origins' |
| ) |
|
|
| @field_validator('cors_origins', mode='before') |
| @classmethod |
| def parse_cors_origins(cls, v): |
| """Parse CORS origins from string or list.""" |
| if isinstance(v, str): |
| return [origin.strip() for origin in v.split(',')] |
| return v |
|
|
| @field_validator('env') |
| @classmethod |
| def validate_environment(cls, v: str) -> str: |
| """Validate environment value.""" |
| allowed = ['development', 'staging', 'production'] |
| if v not in allowed: |
| raise ValueError(f'env must be one of {allowed}') |
| return v |
|
|
| @field_validator('log_level') |
| @classmethod |
| def validate_log_level(cls, v: str) -> str: |
| """Validate log level value.""" |
| allowed = ['debug', 'info', 'warning', 'error', 'critical'] |
| if v not in allowed: |
| raise ValueError(f'log_level must be one of {allowed}') |
| return v |
|
|
| @property |
| def is_development(self) -> bool: |
| """Check if running in development mode.""" |
| return self.env == 'development' |
|
|
| @property |
| def is_production(self) -> bool: |
| """Check if running in production mode.""" |
| return self.env == 'production' |
|
|
| @property |
| def database_url_sync(self) -> str: |
| """ |
| Get synchronous database URL for Alembic migrations. |
| Replaces postgresql+psycopg with postgresql+psycopg2. |
| """ |
| return self.database_url.replace('+psycopg', '+psycopg2') |
|
|
|
|
| @lru_cache() |
| def get_settings() -> Settings: |
| """ |
| Get cached settings instance. |
| |
| Uses lru_cache to ensure settings are loaded only once. |
| """ |
| return Settings() |
|
|
|
|
| |
| settings = get_settings() |
|
|