Running the application depended on work_item/scripts/python, which is the work-item helper's launcher: it probes for conda with `command -v`, cannot see a lazily-defined shell function, and falls back to a bare system interpreter with none of the dependencies installed. The application now installs into an ordinary virtualenv instead. That exposed two packaging faults. Pillow, numpy, and scipy were declared test-only although the application imports them at runtime — thumbnails decode through Pillow and the perceptual hash is a DCT over decoded pixels — and an editable install failed outright because setuptools could not choose between photo_pipeline, migrations, and work_item. Both are fixed here, and the OpenAI client moves to a `vision` extra so a review-only install does not pull an API client it never calls. Config.from_env now reads `.env`, or the file named by PHOTO_PIPELINE_ENV_FILE, before it looks at the environment. The file is parsed, never executed: KEY=value lines, comments, optional quotes, no interpolation and no export. Anything already exported wins, so the file is standing configuration and the shell stays the override for one run. The archived CLI's names are mapped as aliases (LLM_API_KEY/GEMINI_API_KEY, LLM_BASE_URL, LIBRARY), so an existing photo_analyzer.env configures the application unchanged. .env, *.env, .venv/, and *.egg-info/ are ignored: the real configuration file holds a live provider key and must never be committed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH4M1PHPwJsM9btcg8hzCs
70 lines
2.4 KiB
Python
70 lines
2.4 KiB
Python
"""Configuration from a dotenv file, including the archived CLI's variable names.
|
|
|
|
An operator who already has a ``photo_analyzer.env`` should not have to rewrite it
|
|
to run the application it was replaced by. The file is standing configuration; the
|
|
shell is what you meant this time, so the shell always wins.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from photo_pipeline.config import Config, load_env_file, parse_env_file
|
|
|
|
SAMPLE = """
|
|
# The archived CLI's shape, comments and all.
|
|
LLM_API_KEY=not-real
|
|
LLM_BASE_URL="https://example.invalid/v1beta/openai/"
|
|
LLM_MODEL='gemini-2.5-flash'
|
|
LIBRARY=/tmp/pictures
|
|
|
|
MAX_WORKERS=4
|
|
# commented=ignored
|
|
malformed line without an equals sign
|
|
"""
|
|
|
|
|
|
def test_the_file_is_parsed_and_never_executed():
|
|
values = parse_env_file(SAMPLE)
|
|
|
|
assert values["LLM_BASE_URL"] == "https://example.invalid/v1beta/openai/" # quotes stripped
|
|
assert values["LLM_MODEL"] == "gemini-2.5-flash"
|
|
assert values["MAX_WORKERS"] == "4"
|
|
assert "commented" not in values and "malformed line without an equals sign" not in values
|
|
|
|
|
|
def test_the_archived_cli_names_still_configure_the_application():
|
|
values = parse_env_file(SAMPLE)
|
|
|
|
assert values["OPENAI_API_KEY"] == "not-real"
|
|
assert values["OPENAI_BASE_URL"] == "https://example.invalid/v1beta/openai/"
|
|
assert values["PHOTO_PIPELINE_LIBRARY_ROOTS"] == "/tmp/pictures"
|
|
|
|
|
|
def test_an_explicit_shell_variable_beats_the_file(tmp_path, monkeypatch):
|
|
path = tmp_path / "photo_analyzer.env"
|
|
path.write_text(SAMPLE)
|
|
monkeypatch.setenv("OPENAI_API_KEY", "from-the-shell")
|
|
monkeypatch.delenv("PHOTO_PIPELINE_LIBRARY_ROOTS", raising=False)
|
|
|
|
applied = load_env_file(path)
|
|
|
|
assert "OPENAI_API_KEY" not in applied, "the file overrode an exported value"
|
|
assert os.environ["OPENAI_API_KEY"] == "from-the-shell"
|
|
assert os.environ["PHOTO_PIPELINE_LIBRARY_ROOTS"] == "/tmp/pictures"
|
|
assert Config.from_env().library_roots[0].name == "pictures"
|
|
|
|
|
|
def test_the_file_is_found_through_its_variable(tmp_path, monkeypatch):
|
|
path = tmp_path / "custom.env"
|
|
path.write_text("PHOTO_PIPELINE_PORT=9123\n")
|
|
monkeypatch.delenv("PHOTO_PIPELINE_PORT", raising=False)
|
|
monkeypatch.setenv("PHOTO_PIPELINE_ENV_FILE", str(path))
|
|
monkeypatch.chdir(tmp_path) # no ./.env here, so only the variable can find it
|
|
|
|
assert Config.from_env().port == 9123
|
|
|
|
|
|
def test_a_missing_file_is_not_an_error(tmp_path):
|
|
assert load_env_file(tmp_path / "nothing-here.env") == {}
|