Files
unitforge/backend/app/core/config.py

241 lines
9.3 KiB
Python

"""
Configuration module for UnitForge application.
This module handles environment variable loading and provides
default values for configuration settings.
"""
import ast
import json
import os
from pathlib import Path
from typing import List, Optional
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) -> None:
"""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") # nosec B104
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 (ValueError, SyntaxError, json.JSONDecodeError, TypeError):
# Fall back to comma-separated parsing if JSON/literal parsing fails
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()