""" Configuration management for TreeTrack - Supabase Edition Environment-based configuration for cloud deployment """ import os from functools import lru_cache from pathlib import Path from typing import List, Optional from pydantic import Field, field_validator from pydantic_settings import BaseSettings class SecurityConfig(BaseSettings): """Security configuration settings""" # CORS settings for web deployment cors_origins: List[str] = Field( default=["*"], env="CORS_ORIGINS" # Allow all origins for HuggingFace Spaces ) # Request limits max_request_size: int = Field( default=10485760, env="MAX_REQUEST_SIZE", ge=1024 ) # 10MB default for file uploads @field_validator("cors_origins") def validate_cors_origins(cls, v): """Validate CORS origins""" if isinstance(v, str): # Handle comma-separated string return [origin.strip() for origin in v.split(",") if origin.strip()] return v class ServerConfig(BaseSettings): """Server configuration settings""" # Server settings - optimized for HuggingFace Spaces host: str = Field(default="0.0.0.0", env="HOST") port: int = Field(default=7860, env="PORT", ge=1, le=65535) # HF Spaces default workers: int = Field(default=1, env="WORKERS", ge=1, le=4) reload: bool = Field(default=False, env="RELOAD") debug: bool = Field(default=False, env="DEBUG") @field_validator("reload", "debug", mode='before') @classmethod def validate_server_boolean_fields(cls, v): """Convert string boolean values to actual booleans""" if isinstance(v, str): v = v.strip().lower() if v in ('true', '1', 'yes', 'on'): return True elif v in ('false', '0', 'no', 'off', ''): return False else: raise ValueError(f"Invalid boolean value: {v}") return v # Request handling request_timeout: int = Field(default=30, env="REQUEST_TIMEOUT", ge=1, le=300) max_trees_per_request: int = Field( default=3000, env="MAX_TREES_PER_REQUEST", ge=1, le=10000 ) class SupabaseConfig(BaseSettings): """Supabase configuration settings""" # Supabase credentials - optional for development supabase_url: Optional[str] = Field(default=None, env="SUPABASE_URL") supabase_anon_key: Optional[str] = Field(default=None, env="SUPABASE_ANON_KEY") supabase_service_role_key: Optional[str] = Field(default=None, env="SUPABASE_SERVICE_ROLE_KEY") # Storage bucket names image_bucket: str = Field(default="tree-images", env="IMAGE_BUCKET") audio_bucket: str = Field(default="tree-audios", env="AUDIO_BUCKET") # File URL expiry (in seconds) signed_url_expiry: int = Field(default=3600, env="SIGNED_URL_EXPIRY", ge=300) @field_validator("supabase_url") def validate_supabase_url(cls, v): """Validate Supabase URL format""" if v is not None and not v.startswith("https://"): raise ValueError("SUPABASE_URL must be a valid HTTPS URL") return v # Remove key validation - accept any provided keys for deployment flexibility class ApplicationConfig(BaseSettings): """Main application configuration""" # Application metadata app_name: str = Field(default="TreeTrack", env="APP_NAME") app_version: str = Field(default="3.0.0", env="APP_VERSION") app_description: str = Field( default="Tree mapping and tracking with cloud storage", env="APP_DESCRIPTION" ) environment: str = Field(default="production", env="ENVIRONMENT") # Conference/Demo mode conference_mode: bool = Field(default=False, env="CONFERENCE_MODE") demo_mode: bool = Field(default=False, env="DEMO_MODE") @field_validator("conference_mode", "demo_mode", mode='before') @classmethod def validate_boolean_fields(cls, v): """Convert string boolean values to actual booleans""" if isinstance(v, str): v = v.strip().lower() if v in ('true', '1', 'yes', 'on'): return True elif v in ('false', '0', 'no', 'off', ''): return False else: raise ValueError(f"Invalid boolean value: {v}") return v # Feature flags enable_api_docs: bool = Field(default=True, env="ENABLE_API_DOCS") enable_frontend: bool = Field(default=True, env="ENABLE_FRONTEND") enable_statistics: bool = Field(default=True, env="ENABLE_STATISTICS") enable_master_db: bool = Field(default=True, env="ENABLE_MASTER_DB") @field_validator("enable_api_docs", "enable_frontend", "enable_statistics", "enable_master_db", mode='before') @classmethod def validate_feature_flags(cls, v): """Convert string boolean values to actual booleans""" if isinstance(v, str): v = v.strip().lower() if v in ('true', '1', 'yes', 'on'): return True elif v in ('false', '0', 'no', 'off', ''): return False else: raise ValueError(f"Invalid boolean value: {v}") return v # Data validation limits max_species_length: int = Field(default=200, env="MAX_SPECIES_LENGTH", ge=1, le=500) max_notes_length: int = Field(default=2000, env="MAX_NOTES_LENGTH", ge=1, le=10000) @field_validator("environment") def validate_environment(cls, v): """Validate environment""" valid_envs = ["development", "testing", "staging", "production"] if v.lower() not in valid_envs: raise ValueError(f"Environment must be one of: {valid_envs}") return v.lower() class Settings(BaseSettings): """Combined application settings""" # Sub-configurations security: SecurityConfig = SecurityConfig() server: ServerConfig = ServerConfig() supabase: SupabaseConfig = SupabaseConfig() app: ApplicationConfig = ApplicationConfig() def is_development(self) -> bool: """Check if running in development mode""" return self.app.environment == "development" def is_production(self) -> bool: """Check if running in production mode""" return self.app.environment == "production" def is_conference_mode(self) -> bool: """Check if running in conference demo mode""" return self.app.conference_mode def is_demo_mode(self) -> bool: """Check if running in demo mode (no database writes)""" return self.app.demo_mode or self.app.conference_mode def get_cors_config(self) -> dict: """Get CORS configuration""" return { "allow_origins": self.security.cors_origins, "allow_credentials": True, "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], "allow_headers": ["*"], } class Config: env_file = ".env" env_file_encoding = "utf-8" case_sensitive = False extra = "allow" # Allow extra fields during development @lru_cache def get_settings() -> Settings: """Get cached application settings""" return Settings() # For backward compatibility - expose individual configs def get_security_config(): return get_settings().security def get_server_config(): return get_settings().server def get_supabase_config(): return get_settings().supabase if __name__ == "__main__": # Test configuration loading try: settings = get_settings() print("Configuration loaded successfully") print(f"App: {settings.app.app_name} v{settings.app.app_version}") print(f"Environment: {settings.app.environment}") print(f"Server: {settings.server.host}:{settings.server.port}") print(f"Supabase URL: {settings.supabase.supabase_url}") print(f"CORS Origins: {settings.security.cors_origins}") except Exception as e: print(f"Configuration error: {e}") exit(1)