From c50106c185a04670fb8d3e28fe338ce9f8c91c72 Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Fri, 3 Apr 2026 13:47:53 +0200 Subject: [PATCH] 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. --- document-parser/infra/settings.py | 5 ++ document-parser/main.py | 8 +-- document-parser/persistence/database.py | 4 +- document-parser/services/document_service.py | 3 +- document-parser/tests/test_settings.py | 75 ++++++++++++++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 document-parser/tests/test_settings.py diff --git a/document-parser/infra/settings.py b/document-parser/infra/settings.py index b08bf2b..2119964 100644 --- a/document-parser/infra/settings.py +++ b/document-parser/infra/settings.py @@ -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() diff --git a/document-parser/main.py b/document-parser/main.py index 5927c46..5a9bc57 100644 --- a/document-parser/main.py +++ b/document-parser/main.py @@ -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.""" diff --git a/document-parser/persistence/database.py b/document-parser/persistence/database.py index 93c7d94..87a0856 100644 --- a/document-parser/persistence/database.py +++ b/document-parser/persistence/database.py @@ -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 ( diff --git a/document-parser/services/document_service.py b/document-parser/services/document_service.py index bee24fb..fbebfa6 100644 --- a/document-parser/services/document_service.py +++ b/document-parser/services/document_service.py @@ -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 diff --git a/document-parser/tests/test_settings.py b/document-parser/tests/test_settings.py new file mode 100644 index 0000000..1326901 --- /dev/null +++ b/document-parser/tests/test_settings.py @@ -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"