Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/medcheck/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,28 @@ def _env_flag(name: str) -> bool:
return os.environ.get(name, "").strip().lower() in _TRUTHY


def _env_int(name: str, default: int) -> int:
"""Parse an integer env var, failing with a clear config error on typos.

A malformed value (e.g. MEDCHECK_PORT=8080a) must not surface as a raw
ValueError traceback from inside dataclass construction — and silently
falling back to the default would hide a misconfigured port or cap.
"""
raw = os.environ.get(name)
if raw is None or not raw.strip():
return default
try:
return int(raw.strip())
except ValueError:
raise SystemExit(f"Invalid {name}={raw!r}: expected an integer") from None


@dataclass
class Settings:
# Bind to localhost by default; operators must opt into 0.0.0.0 explicitly
# via MEDCHECK_HOST for network deployments (this app handles patient PHI).
host: str = field(default_factory=lambda: os.environ.get("MEDCHECK_HOST", "127.0.0.1"))
port: int = field(default_factory=lambda: int(os.environ.get("MEDCHECK_PORT", "8080")))
port: int = field(default_factory=lambda: _env_int("MEDCHECK_PORT", 8080))
api_key: str | None = field(default_factory=lambda: os.environ.get("MEDCHECK_API_KEY"))
# Consent gate: patient-derived data is only sent to external cloud LLM APIs
# when this is explicitly enabled (MEDCHECK_ALLOW_EXTERNAL_LLM=1).
Expand All @@ -26,11 +42,11 @@ class Settings:
default_language: str = field(default_factory=lambda: os.environ.get("MEDCHECK_LANGUAGE", "en"))
# Max slice images sent to the LLM per analysis. Bounds cost/latency and the
# volume of patient-derived data leaving the host (MEDCHECK_MAX_VISION_IMAGES).
max_vision_images: int = field(default_factory=lambda: int(os.environ.get("MEDCHECK_MAX_VISION_IMAGES", "12")))
max_vision_images: int = field(default_factory=lambda: _env_int("MEDCHECK_MAX_VISION_IMAGES", 12))
# Hard cap on a portal exam-ZIP download, in bytes; guards against an oversized
# or hostile response exhausting disk (MEDCHECK_MAX_DOWNLOAD_BYTES, default 2 GiB).
max_download_bytes: int = field(
default_factory=lambda: int(os.environ.get("MEDCHECK_MAX_DOWNLOAD_BYTES", str(2 * 1024 * 1024 * 1024)))
default_factory=lambda: _env_int("MEDCHECK_MAX_DOWNLOAD_BYTES", 2 * 1024 * 1024 * 1024)
)
anthropic_api_key: str | None = field(default_factory=lambda: os.environ.get("ANTHROPIC_API_KEY"))
openai_api_key: str | None = field(default_factory=lambda: os.environ.get("OPENAI_API_KEY"))
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_core/test_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from medcheck.core.config import Settings


Expand Down Expand Up @@ -47,3 +49,29 @@ def test_available_providers(monkeypatch):
assert "gemini" in providers
assert "openai" not in providers
assert "local" in providers


def test_malformed_int_env_raises_clear_error(monkeypatch):
monkeypatch.setenv("MEDCHECK_PORT", "8080a")
with pytest.raises(SystemExit, match="Invalid MEDCHECK_PORT='8080a'"):
Settings()


def test_malformed_download_cap_raises_clear_error(monkeypatch):
monkeypatch.setenv("MEDCHECK_MAX_DOWNLOAD_BYTES", "2GB")
with pytest.raises(SystemExit, match="Invalid MEDCHECK_MAX_DOWNLOAD_BYTES='2GB'"):
Settings()


def test_empty_int_env_uses_default(monkeypatch):
monkeypatch.setenv("MEDCHECK_PORT", "")
monkeypatch.setenv("MEDCHECK_MAX_VISION_IMAGES", " ")
settings = Settings()
assert settings.port == 8080
assert settings.max_vision_images == 12


def test_int_env_tolerates_whitespace(monkeypatch):
monkeypatch.setenv("MEDCHECK_PORT", " 9091 ")
settings = Settings()
assert settings.port == 9091
Loading