Skip to content
Merged
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ credential is stored for the given host, whether that is a CLI session token
or a deploy key you placed there manually.

```python
client = ConvoxClient.from_cli_config(rack="console.convox.com")
# Console-managed rack — specify both host and rack
client = ConvoxClient.from_cli_config(
host="console.convox.com", # auth file lookup key
rack="org/rack-name", # rack to target
)

# Direct rack connection (host and rack are the same)
client = ConvoxClient.from_cli_config(rack="rack.example.com")
```

## API Reference
Expand Down
26 changes: 16 additions & 10 deletions src/convox/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def credentials_from_env() -> tuple[str, str]:
def credentials_from_cli_config(
rack: str | None = None,
config_path: str | None = None,
*,
host: str | None = None,
) -> tuple[str, str]:
"""Read host and API key from the CLI config file.

Expand All @@ -54,13 +56,16 @@ def credentials_from_cli_config(
``~/.convox/auth`` (v2 CLI).

Parameters:
rack: Rack host to look up. If None, reads from CONVOX_HOST
environment variable.
rack: Deprecated for host lookup — use *host* instead. Still
accepted as a fallback when *host* is not provided.
config_path: Path to the auth config file. Defaults to
``~/.config/convox/auth`` with ``~/.convox/auth`` fallback.
host: Console or rack host to look up in the auth file
(e.g. ``console.convox.com``). Falls back to *rack*, then
the ``CONVOX_HOST`` environment variable.

Raises:
ValueError: If the rack host is not found in the config.
ValueError: If the host is not found in the config.
FileNotFoundError: If the config file does not exist.
"""
if config_path:
Expand All @@ -76,19 +81,20 @@ def credentials_from_cli_config(
with open(path) as f:
auth_data: dict[str, str] = json.load(f)

host = rack or os.environ.get("CONVOX_HOST")
if not host:
lookup = host or rack or os.environ.get("CONVOX_HOST")
if not lookup:
# If only one entry, use it
if len(auth_data) == 1:
host = next(iter(auth_data))
lookup = next(iter(auth_data))
else:
raise ValueError("Multiple racks in CLI config; specify rack name or set CONVOX_HOST")
raise ValueError("Multiple hosts in CLI config; specify host= or set CONVOX_HOST")

password = auth_data.get(host)
password = auth_data.get(lookup)
if password is None:
raise ValueError(f"No credentials found for rack: {host}")
available = ", ".join(sorted(auth_data.keys()))
raise ValueError(f"No credentials found for host: {lookup} (available: {available})")

return host, password
return lookup, password


def normalize_host(host: str) -> str:
Expand Down
14 changes: 12 additions & 2 deletions src/convox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def from_cli_config(
cls,
rack: str | None = None,
*,
host: str | None = None,
config_path: str | None = None,
retry: RetryConfig | None = None,
timeout: float = 30.0,
Expand All @@ -126,9 +127,18 @@ def from_cli_config(

Checks ``~/.config/convox/auth`` (v3 CLI) first, then falls back
to ``~/.convox/auth`` (v2 CLI).

Parameters:
rack: Rack to target (e.g. ``org/rack-name``). Required for
console-managed racks.
host: Console or rack host to look up in the auth file. If
not provided, falls back to *rack*, then ``CONVOX_HOST``.
"""
host, api_key = credentials_from_cli_config(rack=rack, config_path=config_path)
return cls(host, api_key, rack=rack, retry=retry, timeout=timeout)
auth_host, api_key = credentials_from_cli_config(
config_path=config_path,
host=host or rack,
)
return cls(auth_host, api_key, rack=rack, retry=retry, timeout=timeout)

def stream(
self,
Expand Down
33 changes: 33 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,39 @@ def test_rack_not_in_config(self, tmp_path: Path) -> None:
config_path=str(auth_file),
)

def test_console_with_separate_host_and_rack(
self, tmp_path: Path, httpx_mock: HTTPXMock
) -> None:
"""Console connections: host= looks up the auth file, rack= targets
the specific rack. These are different values."""
auth_file = tmp_path / "auth"
auth_file.write_text(json.dumps({"console.test.com": "my-key"}))
httpx_mock.add_response(json=[])
client = ConvoxClient.from_cli_config(
host="console.test.com",
rack="myorg/myrack",
config_path=str(auth_file),
)
client.apps.list()
req = httpx_mock.get_requests()[0]
assert client._http.base_url == "https://console.test.com"
assert req.headers.get("rack") == "myorg/myrack"
assert req.headers.get("user-agent").startswith("convox.go/")
client.close()

def test_host_overrides_rack_for_lookup(self, tmp_path: Path) -> None:
"""When both host= and rack= are given, host= is used for the
auth file lookup, not rack=."""
auth_file = tmp_path / "auth"
auth_file.write_text(json.dumps({"console.test.com": "my-key"}))
client = ConvoxClient.from_cli_config(
host="console.test.com",
rack="org/rack-name",
config_path=str(auth_file),
)
assert client._http.base_url == "https://console.test.com"
client.close()


class TestCliConfigPathFallback:
"""Verify credentials_from_cli_config checks v3 path first, then v2."""
Expand Down
Loading