feat: add typed configuration loader with env support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:04:41 -06:00
parent 2a9891ce69
commit a211f9b6bd
2 changed files with 96 additions and 0 deletions

53
app/config.py Normal file
View File

@@ -0,0 +1,53 @@
"""Application configuration loader.
Reads environment variables (with .env file support) and provides
a typed, validated Settings object used throughout the application.
"""
from functools import lru_cache
from pathlib import Path
from dotenv import load_dotenv
from pydantic_settings import BaseSettings
from pydantic_settings import SettingsConfigDict
# Load .env file from project root
_env_path = Path(__file__).resolve().parent.parent / ".env"
load_dotenv(_env_path)
class Settings(BaseSettings):
"""Typed application settings loaded from environment variables.
Attributes:
admin_username: Admin login username (from ADMIN_USERNAME env var).
admin_password: Admin login password (from ADMIN_PASSWORD env var).
app_env: Runtime environment — 'development' or 'production'.
app_host: Host address to bind the server to.
app_port: Port number for the server.
app_log_level: Minimum log level for structlog output.
database_url: SQLite connection string.
"""
admin_username: str
admin_password: str
app_env: str = "development"
app_host: str = "0.0.0.0"
app_port: int = 8000
app_log_level: str = "info"
database_url: str = "sqlite:///data/sneakyswole.db"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
@lru_cache(maxsize=1)
def get_settings() -> Settings:
"""Return a cached Settings instance (singleton pattern).
Returns:
The application Settings object.
"""
return Settings()

43
tests/test_config.py Normal file
View File

@@ -0,0 +1,43 @@
"""Tests for the application configuration loader."""
import os
from unittest.mock import patch
from app.config import Settings, get_settings
class TestSettings:
"""Tests for the Settings configuration class."""
def test_settings_loads_defaults(self) -> None:
"""Settings should have sensible defaults for all fields."""
with patch.dict(os.environ, {
"ADMIN_USERNAME": "testadmin",
"ADMIN_PASSWORD": "testpass123",
}, clear=False):
settings = Settings()
assert settings.admin_username == "testadmin"
assert settings.admin_password == "testpass123"
assert settings.app_env == "development"
assert settings.app_host == "0.0.0.0"
assert settings.app_port == 8000
assert settings.database_url == "sqlite:///data/sneakyswole.db"
def test_settings_requires_admin_username(self) -> None:
"""Settings should require ADMIN_USERNAME to be set."""
with patch.dict(os.environ, {"ADMIN_PASSWORD": "testpass"}, clear=True):
try:
Settings()
assert False, "Should have raised an error"
except Exception:
pass
def test_get_settings_returns_singleton(self) -> None:
"""get_settings should return the same instance on repeated calls."""
with patch.dict(os.environ, {
"ADMIN_USERNAME": "admin",
"ADMIN_PASSWORD": "pass",
}, clear=False):
s1 = get_settings()
s2 = get_settings()
assert s1 is s2