TreeTrack / config.py
RoyAalekh's picture
Fix map loading issue: Increase max_trees_per_request to 3000
cc1360c
raw
history blame
8.11 kB
"""
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)