Skip to content

Commit e6bd18d

Browse files
olivermeyerclaude
andcommitted
fix(foundry): detect db url in .env files in from_package
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6e5bc92 commit e6bd18d

4 files changed

Lines changed: 98 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dependencies = [
5858
"psutil>=6",
5959
"pydantic>=2,<3",
6060
"pydantic-settings>=2,<3",
61+
"python-dotenv>=1,<2",
6162
"rich>=14,<15",
6263
"sentry-sdk>=2,<3",
6364
"sqlalchemy[asyncio]>=2,<3",

src/aignostics_foundry_core/foundry.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from importlib import metadata
2828
from pathlib import Path
2929

30+
from dotenv import dotenv_values
3031
from pydantic import BaseModel, Field
3132

3233
from aignostics_foundry_core.database import DatabaseSettings
@@ -122,14 +123,20 @@ def from_package(cls, package_name: str) -> FoundryContext:
122123
project_path = _find_project_path(package_name)
123124
vcs_ref = os.environ.get("VCS_REF") or (project_path and _get_vcs_ref_from_git(project_path)) or "unknown"
124125
env_prefix = f"{name_upper}_"
125-
database = DatabaseSettings(_env_prefix=f"{env_prefix}DB_") if os.environ.get(f"{env_prefix}DB_URL") else None
126+
env_files = _build_env_file_list(name, name_upper, environment)
127+
db_url_key = f"{env_prefix}DB_URL"
128+
database = (
129+
DatabaseSettings(_env_prefix=f"{env_prefix}DB_", _env_file=env_files)
130+
if os.environ.get(db_url_key) or _any_env_file_has(db_url_key, env_files)
131+
else None
132+
)
126133
return cls(
127134
name=name,
128135
version=version,
129136
version_full=_build_version_full(version, vcs_ref),
130137
version_with_vcs_ref=_build_version_with_vcs_ref(version, vcs_ref),
131138
environment=environment,
132-
env_file=_build_env_file_list(name, name_upper, environment),
139+
env_file=env_files,
133140
env_prefix=env_prefix,
134141
repository_url=repository_url,
135142
documentation_url=documentation_url,
@@ -264,6 +271,19 @@ def _build_env_file_list(name: str, name_upper: str, environment: str) -> list[P
264271
return paths
265272

266273

274+
def _any_env_file_has(key: str, env_files: list[Path]) -> bool:
275+
"""Return True if *key* appears in any of the given env files.
276+
277+
Args:
278+
key: The environment variable key to look for.
279+
env_files: Ordered list of env file paths to search.
280+
281+
Returns:
282+
True if *key* is found in any readable env file, False otherwise.
283+
"""
284+
return any(key in dotenv_values(f) for f in env_files if f.is_file())
285+
286+
267287
def _extract_urls(package_name: str) -> tuple[str, str]:
268288
"""Return ``(repository_url, documentation_url)`` from package metadata."""
269289
pkg_metadata = metadata.metadata(package_name)

tests/aignostics_foundry_core/foundry_test.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
SQLITE_URL = "sqlite+aiosqlite:///test.db"
2424
DB_URL_ENV_KEY = f"{PACKAGE_NAME.upper()}_DB_URL"
2525
DB_POOL_SIZE_ENV_KEY = f"{PACKAGE_NAME.upper()}_DB_POOL_SIZE"
26+
ENV_FILE_ENV_KEY = f"{PACKAGE_NAME.upper()}_ENV_FILE"
2627
ERROR_MSG_FRAGMENT = "set_context"
2728
VCS_REF_VALUE = "abc123"
2829
VCS_REF_OVERRIDE = "ci-override-ref"
@@ -675,6 +676,78 @@ def test_from_package_called_twice_is_safe() -> None:
675676
assert result.returncode == 0, result.stderr
676677

677678

679+
@pytest.mark.integration
680+
def test_from_package_sets_database_when_db_url_only_in_env_file(tmp_path: Path) -> None:
681+
"""from_package() populates database when DB_URL is only in a .env file, not in OS env."""
682+
env_file = tmp_path / ".env"
683+
env_file.write_text(f"{DB_URL_ENV_KEY}={SQLITE_URL}\n")
684+
685+
script = textwrap.dedent(f"""
686+
from aignostics_foundry_core.foundry import FoundryContext
687+
ctx = FoundryContext.from_package("{PACKAGE_NAME}")
688+
assert ctx.database is not None, "database should not be None"
689+
assert ctx.database.get_url() == "{SQLITE_URL}", f"expected {SQLITE_URL!r}, got {{ctx.database.get_url()!r}}"
690+
""")
691+
env = os.environ.copy()
692+
env.pop(DB_URL_ENV_KEY, None)
693+
env[ENV_FILE_ENV_KEY] = str(env_file)
694+
result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, env=env, check=False)
695+
assert result.returncode == 0, result.stderr
696+
697+
698+
@pytest.mark.integration
699+
def test_from_package_database_is_none_when_db_url_absent_from_env_file(tmp_path: Path) -> None:
700+
"""from_package() sets database=None when DB_URL is absent from both OS env and .env file."""
701+
env_file = tmp_path / ".env"
702+
env_file.write_text("SOME_OTHER_KEY=value\n")
703+
704+
script = textwrap.dedent(f"""
705+
from aignostics_foundry_core.foundry import FoundryContext
706+
ctx = FoundryContext.from_package("{PACKAGE_NAME}")
707+
assert ctx.database is None, "database should be None when DB_URL is absent"
708+
""")
709+
env = os.environ.copy()
710+
env.pop(DB_URL_ENV_KEY, None)
711+
env[ENV_FILE_ENV_KEY] = str(env_file)
712+
result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, env=env, check=False)
713+
assert result.returncode == 0, result.stderr
714+
715+
716+
@pytest.mark.integration
717+
def test_from_package_reads_pool_settings_from_env_file(tmp_path: Path) -> None:
718+
"""from_package() reads pool_size from .env file when DB_URL is also in the .env file."""
719+
env_file = tmp_path / ".env"
720+
env_file.write_text(f"{DB_URL_ENV_KEY}={SQLITE_URL}\n{DB_POOL_SIZE_ENV_KEY}=3\n")
721+
722+
script = textwrap.dedent(f"""
723+
from aignostics_foundry_core.foundry import FoundryContext
724+
ctx = FoundryContext.from_package("{PACKAGE_NAME}")
725+
assert ctx.database is not None, "database should not be None"
726+
assert ctx.database.pool_size == 3, f"expected pool_size=3, got {{ctx.database.pool_size}}"
727+
""")
728+
env = os.environ.copy()
729+
env.pop(DB_URL_ENV_KEY, None)
730+
env.pop(DB_POOL_SIZE_ENV_KEY, None)
731+
env[ENV_FILE_ENV_KEY] = str(env_file)
732+
result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, env=env, check=False)
733+
assert result.returncode == 0, result.stderr
734+
735+
736+
@pytest.mark.integration
737+
def test_from_package_sets_database_when_db_url_in_os_env_and_env_file_absent(tmp_path: Path) -> None:
738+
"""from_package() populates database when DB_URL is in OS env and no .env file is set — regression."""
739+
script = textwrap.dedent(f"""
740+
from aignostics_foundry_core.foundry import FoundryContext
741+
ctx = FoundryContext.from_package("{PACKAGE_NAME}")
742+
assert ctx.database is not None, "database should not be None when DB_URL is in OS env"
743+
""")
744+
env = os.environ.copy()
745+
env[DB_URL_ENV_KEY] = SQLITE_URL
746+
env.pop(ENV_FILE_ENV_KEY, None)
747+
result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, env=env, check=False)
748+
assert result.returncode == 0, result.stderr
749+
750+
678751
@pytest.mark.integration
679752
def test_make_context_without_prior_from_package() -> None:
680753
"""Constructing FoundryContext directly (no from_package()) has database=None."""

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)