From b71495376af79bb549929a59e0f0f241f715579c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Thu, 5 Nov 2026 15:33:52 -0800 Subject: [PATCH 1/4] feat: select the credential profile with RUNPOD_PROFILE rp login --profile NAME saved a key under [NAME], but runpod.profile was fixed to "default", so the saved key was never used without code changes. RUNPOD_PROFILE now selects the profile for runpod.api_key and is the default for rp login --profile. - unset or blank RUNPOD_PROFILE keeps the "default" profile - an explicit --profile still overrides the env var - RUNPOD_API_KEY still takes precedence over any stored profile --- docs/cli/references/command_line_interface.md | 1 + docs/cli/start_here.md | 2 + docs/getting_Started.md | 6 +++ runpod/__init__.py | 14 +++++- runpod/rp_cli/main.py | 11 +++- tests/test_cli/test_management_commands.py | 29 +++++++++++ tests/test_profile.py | 50 +++++++++++++++++++ 7 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 tests/test_profile.py diff --git a/docs/cli/references/command_line_interface.md b/docs/cli/references/command_line_interface.md index da7e18879..4c0da31c7 100644 --- a/docs/cli/references/command_line_interface.md +++ b/docs/cli/references/command_line_interface.md @@ -48,6 +48,7 @@ rp login --profile staging # save browser credentials in a named profile Credentials are stored in `~/.runpod/config.toml`. Use `--profile NAME` with either login method to update only that profile; the default is `default`. +Set `RUNPOD_PROFILE=NAME` to select the profile for all commands and for `runpod` in Python; `--profile` overrides it, and `RUNPOD_API_KEY` overrides any stored profile. ### Flash apps diff --git a/docs/cli/start_here.md b/docs/cli/start_here.md index 75a97b69a..adaa94aed 100644 --- a/docs/cli/start_here.md +++ b/docs/cli/start_here.md @@ -12,6 +12,8 @@ Store your Runpod API key by running `rp login` (approve in the browser, then pa Pass `--profile staging` with either login method to update a named credential profile without changing the others. The default profile is `default`. +Set `RUNPOD_PROFILE=staging` to use that profile for every command, including `rp login`. `--profile` takes precedence over `RUNPOD_PROFILE`, and `RUNPOD_API_KEY` takes precedence over any stored profile. + ### SSH Add an SSH key to your account by running `rp ssh add`. To use an existing key pass `--key` or `--key-file`. Keys are stored in `~/.runpod/ssh/`. If no key is specified a new one is generated and stored. diff --git a/docs/getting_Started.md b/docs/getting_Started.md index 10e0692f6..96261d403 100644 --- a/docs/getting_Started.md +++ b/docs/getting_Started.md @@ -19,3 +19,9 @@ import runpod runpod.profile = "my_profile" ``` + +To select a profile without changing code, set the `RUNPOD_PROFILE` environment variable before importing `runpod`: + +```bash +export RUNPOD_PROFILE=my_profile +``` diff --git a/runpod/__init__.py b/runpod/__init__.py index 6add070c1..24ad60dcf 100644 --- a/runpod/__init__.py +++ b/runpod/__init__.py @@ -87,6 +87,18 @@ ] +DEFAULT_PROFILE = "default" + + +def _selected_profile() -> str: + """credential profile in ~/.runpod/config.toml to use. + + set RUNPOD_PROFILE to pick a named profile (saved with + `rp login --profile NAME`); unset or blank means "default". + """ + return os.environ.get("RUNPOD_PROFILE", "").strip() or DEFAULT_PROFILE + + def _load_api_key(): """resolve the api key from the environment, then stored credentials. @@ -119,7 +131,7 @@ def __getattr__(name): # ------------------------------- Config Paths ------------------------------- # SSH_KEY_PATH = os.path.expanduser("~/.runpod/ssh") -profile = "default" # pylint: disable=invalid-name +profile = _selected_profile() # pylint: disable=invalid-name api_key = _load_api_key() # pylint: disable=invalid-name diff --git a/runpod/rp_cli/main.py b/runpod/rp_cli/main.py index e878f7bc1..f98650b2c 100644 --- a/runpod/rp_cli/main.py +++ b/runpod/rp_cli/main.py @@ -21,6 +21,12 @@ _FLASH_UPDATE_CHECK_EXCLUDED = frozenset({"dev"}) +def _default_profile() -> str: + from runpod import _selected_profile + + return _selected_profile() + + @click.group() @click.pass_context def cli(ctx): @@ -631,7 +637,10 @@ async def _run(): help="Store this API key directly (skips the browser flow).", ) @click.option( - "--profile", default="default", show_default=True, help="Credential profile." + "--profile", + default=_default_profile, + show_default="RUNPOD_PROFILE, else default", + help="Credential profile.", ) @click.option( "--agent", diff --git a/tests/test_cli/test_management_commands.py b/tests/test_cli/test_management_commands.py index c2101b2e0..67f2e4784 100644 --- a/tests/test_cli/test_management_commands.py +++ b/tests/test_cli/test_management_commands.py @@ -377,6 +377,35 @@ def test_login_updates_only_selected_profile( assert functions.get_credentials(untouched)["api_key"] == f"old-{untouched}" assert functions.get_credentials("production")["api_key"] == "keep-production" + def test_login_saves_to_profile_from_env(self, tmp_path, monkeypatch): + from runpod.cli.groups.config import functions + + credentials = tmp_path / "config.toml" + credentials.write_text('[default]\napi_key = "keep-default"\n') + monkeypatch.setattr(functions, "CREDENTIAL_FILE", str(credentials)) + monkeypatch.setenv("RUNPOD_PROFILE", "team") + + result = _runner().invoke(cli, ["login", "--api-key", "new-key"]) + + assert result.exit_code == 0, result.output + assert functions.get_credentials("team")["api_key"] == "new-key" + assert functions.get_credentials("default")["api_key"] == "keep-default" + + def test_login_profile_flag_overrides_env(self, tmp_path, monkeypatch): + from runpod.cli.groups.config import functions + + credentials = tmp_path / "config.toml" + monkeypatch.setattr(functions, "CREDENTIAL_FILE", str(credentials)) + monkeypatch.setenv("RUNPOD_PROFILE", "team") + + result = _runner().invoke( + cli, ["login", "--profile", "other", "--api-key", "new-key"] + ) + + assert result.exit_code == 0, result.output + assert functions.get_credentials("other")["api_key"] == "new-key" + assert functions.get_credentials("team") is None + def test_browser_flow_error(self): from runpod.apps.auth import LoginError diff --git a/tests/test_profile.py b/tests/test_profile.py new file mode 100644 index 000000000..693bc184a --- /dev/null +++ b/tests/test_profile.py @@ -0,0 +1,50 @@ +"""Credential profile selection via RUNPOD_PROFILE.""" + +import pytest + +import runpod + + +class TestSelectedProfile: + def test_defaults_to_default_profile(self, monkeypatch): + monkeypatch.delenv("RUNPOD_PROFILE", raising=False) + assert runpod._selected_profile() == "default" + + def test_env_selects_profile(self, monkeypatch): + monkeypatch.setenv("RUNPOD_PROFILE", "team") + assert runpod._selected_profile() == "team" + + def test_env_value_is_stripped(self, monkeypatch): + monkeypatch.setenv("RUNPOD_PROFILE", " team ") + assert runpod._selected_profile() == "team" + + @pytest.mark.parametrize("value", ["", " "]) + def test_blank_env_falls_back_to_default(self, monkeypatch, value): + monkeypatch.setenv("RUNPOD_PROFILE", value) + assert runpod._selected_profile() == "default" + + +class TestApiKeyFromSelectedProfile: + def test_loads_key_from_selected_profile(self, tmp_path, monkeypatch): + from runpod.cli.groups.config import functions + + credentials = tmp_path / "config.toml" + credentials.write_text( + '[default]\napi_key = "default-key"\n[team]\napi_key = "team-key"\n' + ) + monkeypatch.setattr(functions, "CREDENTIAL_FILE", str(credentials)) + monkeypatch.delenv("RUNPOD_API_KEY", raising=False) + monkeypatch.setattr(runpod, "profile", "team") + + assert runpod._load_api_key() == "team-key" + + def test_api_key_env_wins_over_profile(self, tmp_path, monkeypatch): + from runpod.cli.groups.config import functions + + credentials = tmp_path / "config.toml" + credentials.write_text('[team]\napi_key = "team-key"\n') + monkeypatch.setattr(functions, "CREDENTIAL_FILE", str(credentials)) + monkeypatch.setenv("RUNPOD_API_KEY", "env-key") + monkeypatch.setattr(runpod, "profile", "team") + + assert runpod._load_api_key() == "env-key" From 531e872d128d8cfa05a8e28e09b1a6fd755d180b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Thu, 5 Nov 2026 17:52:40 -0800 Subject: [PATCH 2/4] fix: tolerate a selected profile without api_key (SLS-971) A RUNPOD_PROFILE pointing at a profile with only S3 credentials made `import runpod` raise KeyError: 'api_key' when RUNPOD_API_KEY was unset, which also blocked `rp login --api-key`. Read the key with .get so such a profile yields api_key = None. --- runpod/__init__.py | 2 +- tests/test_profile.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/runpod/__init__.py b/runpod/__init__.py index 24ad60dcf..36f398c3d 100644 --- a/runpod/__init__.py +++ b/runpod/__init__.py @@ -115,7 +115,7 @@ def _load_api_key(): ).get_credentials(profile) except Exception: # noqa: BLE001 - no stored key is a valid state return None - return creds["api_key"] if creds else None + return creds.get("api_key") if creds else None def __getattr__(name): diff --git a/tests/test_profile.py b/tests/test_profile.py index 693bc184a..7751de035 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,5 +1,7 @@ """Credential profile selection via RUNPOD_PROFILE.""" +import importlib + import pytest import runpod @@ -48,3 +50,30 @@ def test_api_key_env_wins_over_profile(self, tmp_path, monkeypatch): monkeypatch.setattr(runpod, "profile", "team") assert runpod._load_api_key() == "env-key" + + +class TestImportWithSelectedProfile: + @pytest.fixture + def reload_runpod(self, monkeypatch): + """reload runpod under the test env, then again under the restored env.""" + yield lambda: importlib.reload(runpod) + monkeypatch.undo() + importlib.reload(runpod) + + def test_s3_only_profile_imports_without_api_key( + self, tmp_path, monkeypatch, reload_runpod + ): + from runpod.cli.groups.config import functions + + credentials = tmp_path / "config.toml" + credentials.write_text( + '[storage]\ns3_access_key = "id"\ns3_secret_key = "secret"\n' + ) + monkeypatch.setattr(functions, "CREDENTIAL_FILE", str(credentials)) + monkeypatch.delenv("RUNPOD_API_KEY", raising=False) + monkeypatch.setenv("RUNPOD_PROFILE", "storage") + + reload_runpod() + + assert runpod.profile == "storage" + assert runpod.api_key is None From 2cb686561168894396704b910b2823bbd68257e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Sat, 7 Nov 2026 10:25:35 -0800 Subject: [PATCH 3/4] docs: correct the profile and credentials guide - credentials live in ~/.runpod/config.toml, one table per profile, not ~/.runpod/credentials.toml - setting runpod.profile after import does not change the key; the profile and key are read once at import, so document RUNPOD_PROFILE (set before import), rp login --profile, and RUNPOD_API_KEY precedence - rename docs/getting_Started.md to docs/getting_started.md --- docs/getting_Started.md | 27 --------------------------- docs/getting_started.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 27 deletions(-) delete mode 100644 docs/getting_Started.md create mode 100644 docs/getting_started.md diff --git a/docs/getting_Started.md b/docs/getting_Started.md deleted file mode 100644 index 96261d403..000000000 --- a/docs/getting_Started.md +++ /dev/null @@ -1,27 +0,0 @@ -# Getting Started - -The Runpod python library is a powerful library providing SDK functions, API access, and CLI commands for interacting with the Runpod platform. - -## Credentials File - -This python library supports a credentials file saved to `~/.runpod/credentials.toml` that contains the following information: - -```toml -[profile] -api_key = "YOUR_RUNPOD_API_KEY" -``` -### Profile - -By default, all credentials are stored under the `default` profile. To switch profiles you can use the `--profile` argument on CLI commands or change the profile within a script. - -```python -import runpod - -runpod.profile = "my_profile" -``` - -To select a profile without changing code, set the `RUNPOD_PROFILE` environment variable before importing `runpod`: - -```bash -export RUNPOD_PROFILE=my_profile -``` diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 000000000..cf322a812 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,34 @@ +# Getting Started + +The Runpod python library is a powerful library providing SDK functions, API access, and CLI commands for interacting with the Runpod platform. + +## Credentials File + +`rp login` stores your API key in `~/.runpod/config.toml`, one table per profile: + +```toml +[default] +api_key = "YOUR_RUNPOD_API_KEY" +``` + +### Profile + +By default, credentials are stored under and read from the `default` profile. To store a key under another profile, pass `--profile` to `rp login`: + +```bash +rp login --profile my_profile +``` + +To use that profile, set the `RUNPOD_PROFILE` environment variable before importing `runpod`. The library reads the profile and its API key once, at import time. + +```bash +export RUNPOD_PROFILE=my_profile +``` + +`RUNPOD_API_KEY`, when set, takes precedence over any stored profile. Inside a script, you can also set the key directly: + +```python +import runpod + +runpod.api_key = "YOUR_RUNPOD_API_KEY" +``` From 6c96edc6e7db3f5acfadebc4337eb3ee800cc78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Sat, 7 Nov 2026 10:29:10 -0800 Subject: [PATCH 4/4] docs: keep the getting started guide's file name in this change The rename moves to its own change so file history follows it. --- docs/{getting_started.md => getting_Started.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{getting_started.md => getting_Started.md} (100%) diff --git a/docs/getting_started.md b/docs/getting_Started.md similarity index 100% rename from docs/getting_started.md rename to docs/getting_Started.md