-
-
Notifications
You must be signed in to change notification settings - Fork 9k
fix(proxy): make prisma generate and db push work out-of-the-box after pip install #30051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
steveonjava
wants to merge
7
commits into
BerriAI:litellm_internal_staging
Choose a base branch
from
steveonjava:fix/litellm-prisma-generate-oob
base: litellm_internal_staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+925
−1
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c4a0792
fix(proxy): make prisma generate and db push work out-of-the-box afte…
83e6c28
fix(proxy): resolve db generate schema from litellm package, not prox…
bbfb5ad
fix(proxy): satisfy any-discipline type budget in db generate
00e35d3
style: black-format db generate test files
74bace6
style: use PEP 585 builtin generics in db generate types
0ea1ece
ci: retrigger checks (Vertex AI + documentation cancelled by concurre…
364af60
ci: retrigger checks (prisma client npm fetch flake, 20m timeout)
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
litellm-proxy-extras/tests/test_prisma_env_path_injection.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| """Tests for _get_prisma_env() venv PATH injection (Symptom 3 of issue #26097). | ||
|
|
||
| Symptom 3: after `pip install 'litellm[proxy]'`, running the proxy by absolute | ||
| path in a non-activated venv causes `prisma db push` to exit 0 without creating | ||
| tables because `prisma-client-py` is not on PATH when /bin/sh forks for the | ||
| generator. The fix: _get_prisma_env() prepends the interpreter's scripts dir. | ||
| """ | ||
|
|
||
| import os | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| from litellm_proxy_extras.utils import ProxyExtrasDBManager, _get_prisma_env | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Unit tests for _get_prisma_env() PATH injection | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_get_prisma_env_injects_scripts_dir_at_index_0(monkeypatch): | ||
| """Scripts dir appears at PATH index 0 when venv is not activated.""" | ||
| fake_scripts = "/fake/venv/bin" | ||
| # Remove scripts dir from PATH so it's not already there | ||
| stripped_path = "/usr/local/bin:/usr/bin:/bin" | ||
| monkeypatch.setenv("PATH", stripped_path) | ||
| with (patch("sysconfig.get_path", return_value=fake_scripts),): | ||
| env = _get_prisma_env() | ||
|
|
||
| path_entries = env["PATH"].split(os.pathsep) | ||
| assert ( | ||
| path_entries[0] == fake_scripts | ||
| ), f"Expected scripts dir at index 0, got: {env['PATH']}" | ||
| # Original PATH entries preserved after the injected dir | ||
| assert "/usr/local/bin" in path_entries | ||
| assert "/usr/bin" in path_entries | ||
|
|
||
|
|
||
| def test_get_prisma_env_promotes_scripts_dir_to_index_0(monkeypatch): | ||
| """Scripts dir is promoted to index 0 if already in PATH at a later position.""" | ||
| fake_scripts = "/fake/venv/bin" | ||
| path_with_scripts_at_end = f"/usr/bin:/bin:{fake_scripts}" | ||
| monkeypatch.setenv("PATH", path_with_scripts_at_end) | ||
| with patch("sysconfig.get_path", return_value=fake_scripts): | ||
| env = _get_prisma_env() | ||
|
|
||
| path_entries = env["PATH"].split(os.pathsep) | ||
| assert path_entries[0] == fake_scripts | ||
| # Must appear exactly once | ||
| assert path_entries.count(fake_scripts) == 1 | ||
|
|
||
|
|
||
| def test_get_prisma_env_handles_empty_path(monkeypatch): | ||
| """Empty PATH results in PATH containing only the scripts dir.""" | ||
| fake_scripts = "/fake/venv/bin" | ||
| monkeypatch.setenv("PATH", "") | ||
| with patch("sysconfig.get_path", return_value=fake_scripts): | ||
| env = _get_prisma_env() | ||
|
|
||
| assert env["PATH"] == fake_scripts | ||
| assert ( | ||
| os.pathsep not in env["PATH"] or env["PATH"].rstrip(os.pathsep) == fake_scripts | ||
| ) | ||
|
|
||
|
|
||
| def test_get_prisma_env_falls_back_to_dirname_when_sysconfig_falsy(monkeypatch): | ||
| """Falls back to dirname(sys.executable) when sysconfig returns falsy.""" | ||
| fake_exe = "/fake/venv/bin/python3" | ||
| monkeypatch.setenv("PATH", "/usr/bin") | ||
| with ( | ||
| patch("sysconfig.get_path", return_value=None), | ||
| patch("sys.executable", fake_exe), | ||
| ): | ||
| env = _get_prisma_env() | ||
|
|
||
| expected_scripts = os.path.dirname(os.path.abspath(fake_exe)) | ||
| path_entries = env["PATH"].split(os.pathsep) | ||
| assert path_entries[0] == expected_scripts | ||
|
|
||
|
|
||
| def test_get_prisma_env_sets_offline_vars_when_enabled(monkeypatch): | ||
| """PRISMA_OFFLINE_MODE vars are still set alongside the PATH injection.""" | ||
| monkeypatch.setenv("PRISMA_OFFLINE_MODE", "true") | ||
| monkeypatch.setenv("PATH", "/usr/bin") | ||
| with patch("sysconfig.get_path", return_value="/venv/bin"): | ||
| env = _get_prisma_env() | ||
|
|
||
| assert env.get("NPM_CONFIG_PREFER_OFFLINE") == "true" | ||
| assert "NPM_CONFIG_CACHE" in env | ||
| # PATH injection still happens | ||
| assert env["PATH"].startswith("/venv/bin") | ||
|
|
||
|
|
||
| def test_get_prisma_env_noop_when_scripts_dir_already_at_index_0(monkeypatch): | ||
| """No duplicate entry when scripts dir is already at index 0.""" | ||
| fake_scripts = "/fake/venv/bin" | ||
| monkeypatch.setenv("PATH", f"{fake_scripts}:/usr/bin:/bin") | ||
| with patch("sysconfig.get_path", return_value=fake_scripts): | ||
| env = _get_prisma_env() | ||
|
|
||
| path_entries = env["PATH"].split(os.pathsep) | ||
| assert path_entries[0] == fake_scripts | ||
| assert path_entries.count(fake_scripts) == 1 | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Integration: db push subprocess receives patched env with scripts_dir | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_db_push_subprocess_receives_scripts_dir_on_path(monkeypatch, tmp_path): | ||
| """setup_database(use_migrate=False) passes env with scripts_dir to subprocess. | ||
|
|
||
| This is the Symptom 3 integration guard: the subprocess call must receive | ||
| the patched env dict that has the venv scripts dir at PATH index 0. Without | ||
| it, prisma db push exits 0 but the generator fails silently and tables are | ||
| never created. | ||
| """ | ||
| fake_scripts = "/fake/venv/bin" | ||
| monkeypatch.setenv("PATH", "/usr/bin:/bin") | ||
| monkeypatch.setenv("DATABASE_URL", "postgresql://u:p@localhost:5432/testdb") | ||
|
|
||
| (tmp_path / "schema.prisma").write_text("// stub schema\n") | ||
|
|
||
| captured_calls = [] | ||
|
|
||
| def fake_subprocess_run(args, **kwargs): | ||
| captured_calls.append({"args": list(args), "env": kwargs.get("env", {})}) | ||
| return MagicMock(returncode=0) | ||
|
|
||
| with ( | ||
| patch("sysconfig.get_path", return_value=fake_scripts), | ||
| patch.object( | ||
| ProxyExtrasDBManager, "_get_prisma_dir", return_value=str(tmp_path) | ||
| ), | ||
| patch("litellm_proxy_extras.utils._get_prisma_command", return_value="prisma"), | ||
| patch("subprocess.run", side_effect=fake_subprocess_run), | ||
| ): | ||
| result = ProxyExtrasDBManager.setup_database(use_migrate=False) | ||
|
|
||
| assert result is True, "setup_database should return True on success" | ||
| assert ( | ||
| len(captured_calls) == 1 | ||
| ), f"Expected 1 subprocess call, got {len(captured_calls)}" | ||
|
|
||
| received_env = captured_calls[0]["env"] | ||
| assert received_env is not None, "subprocess.run must be called with env=" | ||
| path_entries = received_env.get("PATH", "").split(os.pathsep) | ||
| assert path_entries[0] == fake_scripts, ( | ||
| f"Expected scripts dir '{fake_scripts}' at PATH index 0 in subprocess env. " | ||
| f"Got PATH: {received_env.get('PATH')}" | ||
| ) | ||
|
|
||
| call_args = captured_calls[0]["args"] | ||
| assert "db" in call_args | ||
| assert "push" in call_args | ||
| assert "--accept-data-loss" in call_args |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| """Database management commands for the LiteLLM proxy CLI.""" | ||
|
|
||
| import os | ||
| import subprocess | ||
| import sys | ||
| import sysconfig | ||
| from typing import Callable, Optional | ||
|
|
||
| import click | ||
|
|
||
| # The Prisma schema ships inside the litellm package itself (litellm/proxy/ | ||
| # schema.prisma); PrismaManager resolves that directory. This is the same | ||
| # schema `db push` operates on, so `db generate` stays consistent with it. | ||
| from litellm.proxy.db.prisma_client import PrismaManager | ||
|
|
||
| # The prisma *binary* discovery and PATH-injection helpers live in | ||
| # litellm-proxy-extras. These are optional dependencies; the ImportError is | ||
| # handled at call time so `litellm` works without the proxy extras installed. | ||
| try: | ||
| from litellm_proxy_extras.utils import ( | ||
| _get_prisma_command as _imported_prisma_command, | ||
| _get_prisma_env as _imported_prisma_env, | ||
| ) | ||
|
|
||
| _PROXY_EXTRAS_AVAILABLE = True | ||
| except ImportError: | ||
| _imported_prisma_command = None | ||
| _imported_prisma_env = None | ||
| _PROXY_EXTRAS_AVAILABLE = False | ||
|
|
||
| # Bind to explicitly typed module-level names. litellm-proxy-extras ships no | ||
| # py.typed marker, so its exports are untyped (Any) at this import boundary; | ||
| # the annotations pin the signatures back down. These are the names the tests | ||
| # patch. The `any-ok` markers acknowledge the unavoidable untyped third-party | ||
| # boundary (the annotation is the concrete type the rest of the file relies on). | ||
| _get_prisma_command: Optional[Callable[[], str]] = ( | ||
| _imported_prisma_command # any-ok: untyped optional import from litellm-proxy-extras | ||
| ) | ||
| _get_prisma_env: Optional[Callable[[], dict[str, str]]] = ( | ||
| _imported_prisma_env # any-ok: untyped optional import from litellm-proxy-extras | ||
| ) | ||
|
|
||
|
|
||
| def _get_venv_scripts_dir() -> str: | ||
| """Return the directory that holds console scripts for the running interpreter. | ||
|
|
||
| The prisma engine shells out to the ``prisma-client-py`` console script via | ||
| ``/bin/sh``, which only finds it when that directory is on PATH. In an | ||
| *activated* venv it is; when ``litellm-proxy`` is invoked by absolute path | ||
| without activation, it is not. We derive the directory from the current | ||
| interpreter so the caller never has to activate the venv. | ||
|
|
||
| ``sysconfig.get_path("scripts")`` is the authoritative answer; we fall back | ||
| to ``dirname(sys.executable)`` if it is empty (defensive: should not happen | ||
| on a normal CPython install). | ||
| """ | ||
| scripts_dir = sysconfig.get_path("scripts") | ||
| if not scripts_dir: | ||
| scripts_dir = os.path.dirname(os.path.abspath(sys.executable)) | ||
| return scripts_dir | ||
|
|
||
|
|
||
| def _get_generate_env() -> dict[str, str]: | ||
| """Build the subprocess env for ``db generate``. | ||
|
|
||
| Delegates to the shared ``_get_prisma_env()`` which now injects the venv | ||
| scripts dir for all Prisma subprocesses (generate, db push, migrate deploy). | ||
| Kept as a named wrapper for backward-compat with existing tests. | ||
| """ | ||
| if _get_prisma_env is not None: | ||
| result = _get_prisma_env() | ||
| if result is not None: | ||
| return result | ||
| return os.environ.copy() | ||
|
|
||
|
|
||
| @click.group() | ||
| def db() -> None: | ||
| """Database management commands.""" | ||
|
|
||
|
|
||
| @db.command(name="generate") | ||
| def db_generate() -> None: | ||
| """Generate the Prisma client using litellm's bundled schema. | ||
|
|
||
| Runs: prisma generate --schema <path_to_schema.prisma> | ||
|
|
||
| The schema is resolved from the litellm package (litellm/proxy/schema.prisma) | ||
| via PrismaManager, which is the same schema `db push` operates on. This keeps | ||
| generate consistent with the existing push path and closes the gap where there | ||
| was no out-of-the-box `generate` step after a plain pip install. | ||
| """ | ||
| if not _PROXY_EXTRAS_AVAILABLE: | ||
| click.echo( | ||
| "Error: litellm-proxy-extras is not installed. " | ||
| "Run: pip install 'litellm[proxy]'", | ||
| err=True, | ||
| ) | ||
| raise SystemExit(1) | ||
|
|
||
| prisma_dir = PrismaManager._get_prisma_dir() | ||
| schema_path = os.path.join(prisma_dir, "schema.prisma") | ||
|
|
||
| if not os.path.exists(schema_path): | ||
| click.echo( | ||
| f"Error: schema.prisma not found at {schema_path}. " | ||
| "Your litellm-proxy-extras installation may be incomplete.", | ||
| err=True, | ||
| ) | ||
| raise SystemExit(1) | ||
|
|
||
| if _get_prisma_command is None: | ||
| click.echo( | ||
| "Error: litellm-proxy-extras is not installed. " | ||
| "Run: pip install 'litellm[proxy]'", | ||
| err=True, | ||
| ) | ||
| raise SystemExit(1) | ||
|
|
||
| click.echo(f"Generating Prisma client from {schema_path} ...") | ||
| command: list[str] = [ | ||
| _get_prisma_command(), | ||
| "generate", | ||
| "--schema", | ||
| schema_path, | ||
| ] | ||
| try: | ||
| subprocess.run( | ||
| command, | ||
| check=True, | ||
| env=_get_generate_env(), | ||
| ) | ||
| click.echo("Prisma client generated successfully.") | ||
| except subprocess.CalledProcessError as e: | ||
| click.echo( | ||
| f"Error: prisma generate failed (exit {e.returncode}).", | ||
| err=True, | ||
| ) | ||
| raise SystemExit(e.returncode) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.