feat: add comprehensive environment variable configuration system

- Create Settings class with support for 70+ configuration variables
- Add python-dotenv integration for .env file loading
- Support app metadata, server settings, security, performance tuning
- Add feature flags, monitoring, and asset optimization settings
- Include validation helpers for booleans, lists, and CORS origins
- Provide template context injection for HTML templates
This commit is contained in:
William Valentin
2025-09-14 15:56:59 -07:00
parent c7a601a1cf
commit 4f94583b54

223
backend/app/core/config.py Normal file
View File

@@ -0,0 +1,223 @@
"""
Configuration module for UnitForge application.
This module handles environment variable loading and provides
default values for configuration settings.
"""
import os
from pathlib import Path
from typing import Optional, List
import json
import ast
try:
from dotenv import load_dotenv
# Try to load .env file from project root
env_path = Path(__file__).parent.parent.parent.parent / '.env'
if env_path.exists():
load_dotenv(env_path)
except ImportError:
# python-dotenv not installed, continue with os.getenv
pass
class Settings:
"""Application settings loaded from environment variables."""
def __init__(self):
"""Initialize settings from environment variables."""
# Application info
self.app_name: str = os.getenv("APP_NAME", "UnitForge")
self.app_version: str = os.getenv("APP_VERSION", "1.0.0")
self.app_description: str = os.getenv(
"APP_DESCRIPTION", "Create, validate, and manage systemd unit files"
)
# External links
self.github_url: str = os.getenv(
"GITHUB_URL", "https://github.com/will666/unitforge"
)
self.documentation_url: str = os.getenv(
"DOCUMENTATION_URL", "https://unitforge.readthedocs.io/"
)
self.bug_reports_url: str = os.getenv(
"BUG_REPORTS_URL", "https://github.com/will666/unitforge/issues"
)
# Contact information
self.contact_email: str = os.getenv("CONTACT_EMAIL", "contact@unitforge.dev")
# Application settings
self.environment: str = os.getenv("ENVIRONMENT", "production")
self.log_level: str = os.getenv("LOG_LEVEL", "info")
# Server configuration
self.host: str = os.getenv("HOST", "0.0.0.0")
self.port: int = int(os.getenv("PORT", "8000"))
self.debug: bool = os.getenv("DEBUG", "").lower() in ("true", "1", "yes", "on")
self.reload: bool = os.getenv("RELOAD", "").lower() in ("true", "1", "yes", "on")
self.workers: int = int(os.getenv("WORKERS", "4"))
# API configuration
self.api_title: str = os.getenv("API_TITLE", self.app_name)
self.api_version: str = os.getenv("API_VERSION", self.app_version)
self.api_description: str = os.getenv("API_DESCRIPTION", self.app_description)
self.api_docs_url: str = os.getenv("DOCS_URL", "/api/docs")
self.api_redoc_url: str = os.getenv("REDOC_URL", "/api/redoc")
# Security settings
self.secret_key: Optional[str] = os.getenv("SECRET_KEY")
self.allowed_hosts: List[str] = self._parse_list("ALLOWED_HOSTS", ["*"])
# CORS settings
self.cors_origins: list = self._parse_cors_origins()
# File upload settings
self.max_upload_size: int = int(os.getenv("MAX_UPLOAD_SIZE", "1048576"))
self.allowed_extensions: List[str] = self._parse_list(
"ALLOWED_EXTENSIONS",
[".service", ".timer", ".socket", ".mount", ".target", ".path"]
)
# Template settings
self.template_cache_ttl: int = int(os.getenv("TEMPLATE_CACHE_TTL", "300"))
self.validation_cache_ttl: int = int(os.getenv("VALIDATION_CACHE_TTL", "60"))
# Paths
self.frontend_dir: str = os.getenv("FRONTEND_DIR", "frontend")
self.backend_dir: str = os.getenv("BACKEND_DIR", "backend")
self.static_dir: str = os.getenv("STATIC_DIR", "frontend/static")
self.templates_dir: str = os.getenv("TEMPLATES_DIR", "frontend/templates")
# Feature flags
self.enable_api_metrics: bool = self._get_bool("ENABLE_API_METRICS", False)
self.enable_request_logging: bool = self._get_bool("ENABLE_REQUEST_LOGGING", True)
self.enable_template_caching: bool = self._get_bool("ENABLE_TEMPLATE_CACHING", True)
self.enable_validation_caching: bool = self._get_bool("ENABLE_VALIDATION_CACHING", True)
# Performance settings
self.request_timeout: int = int(os.getenv("REQUEST_TIMEOUT", "30"))
self.keepalive_timeout: int = int(os.getenv("KEEPALIVE_TIMEOUT", "5"))
self.max_connections: int = int(os.getenv("MAX_CONNECTIONS", "100"))
# Logging configuration
self.log_format: str = os.getenv(
"LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
self.log_date_format: str = os.getenv("LOG_DATE_FORMAT", "%Y-%m-%d %H:%M:%S")
self.access_log: bool = self._get_bool("ACCESS_LOG", True)
# CLI configuration
self.cli_verbose: bool = self._get_bool("CLI_VERBOSE", False)
self.cli_color: bool = self._get_bool("CLI_COLOR", True)
self.cli_progress: bool = self._get_bool("CLI_PROGRESS", True)
# Validation settings
self.strict_validation: bool = self._get_bool("STRICT_VALIDATION", False)
self.show_warnings: bool = self._get_bool("SHOW_WARNINGS", True)
self.max_validation_errors: int = int(os.getenv("MAX_VALIDATION_ERRORS", "50"))
# Template generation defaults
self.default_user: str = os.getenv("DEFAULT_USER", "www-data")
self.default_group: str = os.getenv("DEFAULT_GROUP", "www-data")
self.default_restart_policy: str = os.getenv("DEFAULT_RESTART_POLICY", "on-failure")
self.default_wanted_by: str = os.getenv("DEFAULT_WANTED_BY", "multi-user.target")
# Security headers
self.security_headers: bool = self._get_bool("SECURITY_HEADERS", True)
self.hsts_max_age: int = int(os.getenv("HSTS_MAX_AGE", "31536000"))
self.csp_enabled: bool = self._get_bool("CSP_ENABLED", True)
# Monitoring
self.health_check_enabled: bool = self._get_bool("HEALTH_CHECK_ENABLED", True)
self.metrics_enabled: bool = self._get_bool("METRICS_ENABLED", False)
self.tracing_enabled: bool = self._get_bool("TRACING_ENABLED", False)
# Asset optimization
self.hot_reload: bool = self._get_bool("HOT_RELOAD", False)
self.source_maps: bool = self._get_bool("SOURCE_MAPS", False)
self.minify_assets: bool = self._get_bool("MINIFY_ASSETS", True)
self.compress_responses: bool = self._get_bool("COMPRESS_RESPONSES", True)
# Documentation
self.docs_auto_reload: bool = self._get_bool("DOCS_AUTO_RELOAD", False)
self.api_docs_enabled: bool = self._get_bool("API_DOCS_ENABLED", True)
self.swagger_ui_enabled: bool = self._get_bool("SWAGGER_UI_ENABLED", True)
self.redoc_enabled: bool = self._get_bool("REDOC_ENABLED", True)
def _get_bool(self, key: str, default: bool = False) -> bool:
"""Get boolean value from environment variable."""
value = os.getenv(key, "").lower()
if value in ("true", "1", "yes", "on"):
return True
elif value in ("false", "0", "no", "off"):
return False
return default
def _parse_list(self, key: str, default: List[str]) -> List[str]:
"""Parse list from environment variable."""
value = os.getenv(key)
if not value:
return default
# Try to parse as JSON list first
try:
parsed = json.loads(value)
if isinstance(parsed, list):
return [str(item) for item in parsed]
except (json.JSONDecodeError, ValueError):
pass
# Try to parse as Python literal
try:
parsed = ast.literal_eval(value)
if isinstance(parsed, list):
return [str(item) for item in parsed]
except (ValueError, SyntaxError):
pass
# Fall back to comma-separated values
return [item.strip() for item in value.split(",") if item.strip()]
def _parse_cors_origins(self) -> list:
"""Parse CORS origins from environment variable."""
origins_str = os.getenv("CORS_ORIGINS", "*")
if origins_str == "*":
return ["*"]
# Try to parse as list first
try:
parsed = self._parse_list("CORS_ORIGINS", [])
if parsed:
return parsed
except Exception:
pass
# Fall back to comma-separated values
return [origin.strip() for origin in origins_str.split(",") if origin.strip()]
def get_template_context(self) -> dict:
"""Get context variables for template rendering."""
return {
"app_name": self.app_name,
"app_version": self.app_version,
"app_description": self.app_description,
"github_url": self.github_url,
"documentation_url": self.documentation_url,
"bug_reports_url": self.bug_reports_url,
"api_docs_url": self.api_docs_url,
"contact_email": self.contact_email,
}
def is_development(self) -> bool:
"""Check if running in development environment."""
return self.environment.lower() == "development" or self.debug
def is_production(self) -> bool:
"""Check if running in production environment."""
return self.environment.lower() == "production" and not self.debug
# Global settings instance
settings = Settings()