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:
223
backend/app/core/config.py
Normal file
223
backend/app/core/config.py
Normal 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()
|
||||
Reference in New Issue
Block a user