feat: add config module with environment variable handling
Implements Step 1.1 config.py with Pydantic Settings: - PORT (default 3366), DB_PATH, BASE_PATH - AUTH_HEADER_NAME, TRUSTED_PROXY_IPS (comma-separated) - CSRF_SECRET (required), CSRF_COOKIE_NAME - SEED_ON_START, LOG_LEVEL (validated), METRICS_ENABLED Includes 28 TDD tests covering defaults, overrides, and validation. Also fixes pytest pythonpath configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
65
src/animaltrack/config.py
Normal file
65
src/animaltrack/config.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# ABOUTME: Application configuration loaded from environment variables.
|
||||
# ABOUTME: Uses Pydantic Settings for validation and type coercion.
|
||||
|
||||
import logging
|
||||
from functools import cached_property
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
|
||||
|
||||
def _parse_comma_separated(value: str) -> list[str]:
|
||||
"""Parse a comma-separated string into a list of trimmed strings."""
|
||||
if not value:
|
||||
return []
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings loaded from environment variables."""
|
||||
|
||||
# Server configuration
|
||||
port: int = 3366 # EGG in leet: 3=E, 6=G, 6=G
|
||||
db_path: str = "animaltrack.db"
|
||||
base_path: str = "/"
|
||||
|
||||
# Authentication
|
||||
auth_header_name: str = "X-Oidc-Username"
|
||||
trusted_proxy_ips_raw: str = Field(default="", validation_alias="trusted_proxy_ips")
|
||||
|
||||
# CSRF protection
|
||||
csrf_secret: str # Required, no default
|
||||
csrf_cookie_name: str = "csrf_token"
|
||||
|
||||
# Application behavior
|
||||
seed_on_start: bool = False
|
||||
log_level: str = "INFO"
|
||||
metrics_enabled: bool = False
|
||||
|
||||
@cached_property
|
||||
def trusted_proxy_ips(self) -> list[str]:
|
||||
"""Parse trusted proxy IPs from comma-separated raw string."""
|
||||
return _parse_comma_separated(self.trusted_proxy_ips_raw)
|
||||
|
||||
@field_validator("log_level", mode="before")
|
||||
@classmethod
|
||||
def normalize_and_validate_log_level(cls, v: str) -> str:
|
||||
"""Normalize log level to uppercase and validate it's a valid Python log level."""
|
||||
if v is None:
|
||||
return "INFO"
|
||||
normalized = v.upper()
|
||||
if normalized not in VALID_LOG_LEVELS:
|
||||
msg = f"Invalid log level: {v}. Must be one of: {', '.join(sorted(VALID_LOG_LEVELS))}"
|
||||
raise ValueError(msg)
|
||||
return normalized
|
||||
|
||||
def get_numeric_log_level(self) -> int:
|
||||
"""Get the numeric log level for use with logging module."""
|
||||
return getattr(logging, self.log_level)
|
||||
|
||||
model_config = {
|
||||
"env_prefix": "",
|
||||
"case_sensitive": False,
|
||||
}
|
||||
Reference in New Issue
Block a user