Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| 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") | |
| 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) | |
| 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") | |
| 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") | |
| 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) | |
| 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 | |
| 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) | |