From b1656eb66057f51d70b339ff5037bc94234fb7f0 Mon Sep 17 00:00:00 2001 From: ntner Date: Tue, 31 Mar 2026 14:55:10 -0400 Subject: [PATCH] seperate rack --- README.md | 9 ++++++++- src/convox/auth.py | 26 ++++++++++++++++---------- src/convox/client.py | 14 ++++++++++++-- tests/test_client.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a7cf31d..830d4ff 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/convox/auth.py b/src/convox/auth.py index bf33b38..8ac0ba6 100644 --- a/src/convox/auth.py +++ b/src/convox/auth.py @@ -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. @@ -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: @@ -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: diff --git a/src/convox/client.py b/src/convox/client.py index b8f39da..159996d 100644 --- a/src/convox/client.py +++ b/src/convox/client.py @@ -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, @@ -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, diff --git a/tests/test_client.py b/tests/test_client.py index be85c62..202db10 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -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."""