Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/cli/references/command_line_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions docs/cli/start_here.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 17 additions & 4 deletions docs/getting_Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,31 @@ The Runpod python library is a powerful library providing SDK functions, API acc

## Credentials File

This python library supports a credentials file saved to `~/.runpod/credentials.toml` that contains the following information:
`rp login` stores your API key in `~/.runpod/config.toml`, one table per profile:

```toml
[profile]
[default]
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.
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.profile = "my_profile"
runpod.api_key = "YOUR_RUNPOD_API_KEY"
```
16 changes: 14 additions & 2 deletions runpod/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -103,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):
Expand All @@ -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
Comment thread
deanq marked this conversation as resolved.
Resolved

api_key = _load_api_key() # pylint: disable=invalid-name

Expand Down
11 changes: 10 additions & 1 deletion runpod/rp_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions tests/test_cli/test_management_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
79 changes: 79 additions & 0 deletions tests/test_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Credential profile selection via RUNPOD_PROFILE."""

import importlib

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"


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
Loading