Centralize config through Settings singleton

UPLOAD_DIR and DB_PATH were read directly from os.environ, bypassing
the Settings dataclass. This caused an inconsistency where overriding
Settings had no effect on these values. Now all modules import from
infra.settings.settings.
This commit is contained in:
Pier-Jean Malandrino 2026-04-03 13:47:53 +02:00
parent f4e11645c7
commit c50106c185
5 changed files with 86 additions and 9 deletions

View file

@ -21,6 +21,7 @@ class Settings:
@classmethod
def from_env(cls) -> Settings:
"""Build a Settings instance from environment variables."""
cors_raw = os.environ.get("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173")
return cls(
app_version=os.environ.get("APP_VERSION", "dev"),
@ -32,3 +33,7 @@ class Settings:
db_path=os.environ.get("DB_PATH", "./data/docling_studio.db"),
cors_origins=[o.strip() for o in cors_raw.split(",")],
)
# Module-level singleton — import this from other modules.
settings = Settings.from_env()

View file

@ -19,7 +19,7 @@ from fastapi.middleware.cors import CORSMiddleware
from api.analyses import router as analyses_router
from api.documents import router as documents_router
from infra.settings import Settings
from infra.settings import settings
from persistence.database import init_db
from services.analysis_service import AnalysisService
@ -29,12 +29,6 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Settings & dependency wiring
# ---------------------------------------------------------------------------
settings = Settings.from_env()
def _build_converter():
"""Build the converter adapter based on configuration."""

View file

@ -8,9 +8,11 @@ from contextlib import asynccontextmanager
import aiosqlite
from infra.settings import settings
logger = logging.getLogger(__name__)
DB_PATH = os.environ.get("DB_PATH", "./data/docling_studio.db")
DB_PATH = settings.db_path
_SCHEMA = """
CREATE TABLE IF NOT EXISTS documents (

View file

@ -10,11 +10,12 @@ import uuid
from pdf2image import convert_from_bytes, pdfinfo_from_bytes
from domain.models import Document
from infra.settings import settings
from persistence import analysis_repo, document_repo
logger = logging.getLogger(__name__)
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "./uploads")
UPLOAD_DIR = settings.upload_dir
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50 MB

View file

@ -0,0 +1,75 @@
"""Tests for Settings — environment variable parsing and defaults."""
from __future__ import annotations
from infra.settings import Settings
class TestSettingsDefaults:
def test_default_values(self):
s = Settings()
assert s.app_version == "dev"
assert s.conversion_engine == "local"
assert s.docling_serve_url == "http://localhost:5001"
assert s.docling_serve_api_key is None
assert s.conversion_timeout == 600
assert s.upload_dir == "./uploads"
assert s.db_path == "./data/docling_studio.db"
assert "http://localhost:3000" in s.cors_origins
def test_frozen(self):
"""Settings should be immutable."""
import pytest
s = Settings()
with pytest.raises(AttributeError):
s.upload_dir = "/other" # type: ignore[misc]
class TestSettingsFromEnv:
def test_reads_env_vars(self, monkeypatch):
monkeypatch.setenv("APP_VERSION", "1.2.3")
monkeypatch.setenv("CONVERSION_ENGINE", "remote")
monkeypatch.setenv("DOCLING_SERVE_URL", "http://serve:9000")
monkeypatch.setenv("DOCLING_SERVE_API_KEY", "secret-key")
monkeypatch.setenv("CONVERSION_TIMEOUT", "120")
monkeypatch.setenv("UPLOAD_DIR", "/data/uploads")
monkeypatch.setenv("DB_PATH", "/data/test.db")
monkeypatch.setenv("CORS_ORIGINS", "http://a.com, http://b.com")
s = Settings.from_env()
assert s.app_version == "1.2.3"
assert s.conversion_engine == "remote"
assert s.docling_serve_url == "http://serve:9000"
assert s.docling_serve_api_key == "secret-key"
assert s.conversion_timeout == 120
assert s.upload_dir == "/data/uploads"
assert s.db_path == "/data/test.db"
assert s.cors_origins == ["http://a.com", "http://b.com"]
def test_defaults_when_env_empty(self, monkeypatch):
"""When no env vars set, from_env returns sensible defaults."""
for key in (
"APP_VERSION",
"CONVERSION_ENGINE",
"DOCLING_SERVE_URL",
"DOCLING_SERVE_API_KEY",
"CONVERSION_TIMEOUT",
"UPLOAD_DIR",
"DB_PATH",
"CORS_ORIGINS",
):
monkeypatch.delenv(key, raising=False)
s = Settings.from_env()
assert s.app_version == "dev"
assert s.conversion_engine == "local"
assert s.conversion_timeout == 600
def test_cors_origins_split(self, monkeypatch):
monkeypatch.setenv("CORS_ORIGINS", "http://a.com,http://b.com,http://c.com")
s = Settings.from_env()
assert len(s.cors_origins) == 3
assert s.cors_origins[2] == "http://c.com"