From df4119233942202cb10303c6702814833ad30e83 Mon Sep 17 00:00:00 2001 From: Michel Edkrantz Date: Fri, 20 Feb 2026 16:59:51 +0100 Subject: [PATCH 1/5] Rename credentials load to put and add get subcommand - Rename `kognic-auth credentials load` to `put` - Add `kognic-auth credentials get [--env PROFILE]` to read and print stored credentials from the keyring as JSON - Update error message in credentials_parser.py to reference `put` - Update tests accordingly Co-Authored-By: Claude Sonnet 4.6 --- src/kognic/auth/cli/credentials.py | 50 +++++++++++++++++++++---- src/kognic/auth/credentials_parser.py | 2 +- tests/test_cli.py | 53 ++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/src/kognic/auth/cli/credentials.py b/src/kognic/auth/cli/credentials.py index 48455d7..d729413 100644 --- a/src/kognic/auth/cli/credentials.py +++ b/src/kognic/auth/cli/credentials.py @@ -1,10 +1,16 @@ from __future__ import annotations import argparse +import json import sys from kognic.auth.credentials_parser import parse_credentials -from kognic.auth.internal.credentials_store import DEFAULT_PROFILE, clear_credentials, save_credentials +from kognic.auth.internal.credentials_store import ( + DEFAULT_PROFILE, + clear_credentials, + load_credentials, + save_credentials, +) COMMAND = "credentials" @@ -16,9 +22,9 @@ def register_parser(subparsers: argparse._SubParsersAction) -> argparse.Argument ) subs = parser.add_subparsers(dest="credentials_action") - load_p = subs.add_parser("load", help="Load credentials from a JSON file into the system keyring") - load_p.add_argument("file", metavar="FILE", help="Path to credentials JSON file") - load_p.add_argument( + put_p = subs.add_parser("put", help="Store credentials from a JSON file into the system keyring") + put_p.add_argument("file", metavar="FILE", help="Path to credentials JSON file") + put_p.add_argument( "--env", default=DEFAULT_PROFILE, metavar="ENV", @@ -27,6 +33,14 @@ def register_parser(subparsers: argparse._SubParsersAction) -> argparse.Argument "(e.g. --env production → use 'keyring://production' in your config).", ) + get_p = subs.add_parser("get", help="Read stored credentials from the system keyring") + get_p.add_argument( + "--env", + default=DEFAULT_PROFILE, + metavar="ENV", + help=f"Keyring profile name to read credentials from (default: {DEFAULT_PROFILE}).", + ) + clear_p = subs.add_parser("clear", help="Remove stored credentials from the system keyring") clear_p.add_argument( "--env", @@ -39,14 +53,16 @@ def register_parser(subparsers: argparse._SubParsersAction) -> argparse.Argument def run(parsed: argparse.Namespace) -> int: - if parsed.credentials_action == "load": - return _run_load(parsed) + if parsed.credentials_action == "put": + return _run_put(parsed) + if parsed.credentials_action == "get": + return _run_read(parsed) if parsed.credentials_action == "clear": return _run_clear(parsed) return 0 -def _run_load(parsed: argparse.Namespace) -> int: +def _run_put(parsed: argparse.Namespace) -> int: try: creds = parse_credentials(parsed.file) save_credentials(creds, parsed.env) @@ -63,6 +79,26 @@ def _run_load(parsed: argparse.Namespace) -> int: return 1 +def _run_read(parsed: argparse.Namespace) -> int: + try: + creds = load_credentials(parsed.env) + if creds is None: + print(f"No credentials found in keyring (profile={parsed.env!r})", file=sys.stderr) + return 1 + data = { + "clientId": creds.client_id, + "clientSecret": creds.client_secret, + "email": creds.email, + "userId": creds.user_id, + "issuer": creds.issuer, + } + print(json.dumps(data, indent=2)) + return 0 + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + def _run_clear(parsed: argparse.Namespace) -> int: try: clear_credentials(parsed.env) diff --git a/src/kognic/auth/credentials_parser.py b/src/kognic/auth/credentials_parser.py index 05cad84..1afca20 100644 --- a/src/kognic/auth/credentials_parser.py +++ b/src/kognic/auth/credentials_parser.py @@ -100,7 +100,7 @@ def resolve_credentials( else: raise ValueError( f"No credentials found in keyring for profile '{profile}'. " - f"Run 'kognic-auth credentials load --env {profile}' to store them." + f"Run 'kognic-auth credentials put --env {profile}' to store them." ) elif not path.endswith(".json"): raise ValueError(f"Bad auth credentials file, must be json: {path}") diff --git a/tests/test_cli.py b/tests/test_cli.py index 499947f..2a85b62 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1119,7 +1119,7 @@ def test_kog_uses_cached_token( class CredentialsCommandTest(unittest.TestCase): - def test_load_stores_credentials(self): + def test_put_stores_credentials(self): import json import tempfile @@ -1136,7 +1136,7 @@ def test_load_stores_credentials(self): try: with mock.patch("kognic.auth.cli.credentials.save_credentials") as mock_save: - result = main(["credentials", "load", path]) + result = main(["credentials", "put", path]) self.assertEqual(result, 0) args, kwargs = mock_save.call_args self.assertEqual(args[0].client_id, "test-client-id") @@ -1145,7 +1145,7 @@ def test_load_stores_credentials(self): finally: Path(path).unlink() - def test_load_custom_profile(self): + def test_put_custom_profile(self): import json import tempfile @@ -1162,7 +1162,7 @@ def test_load_custom_profile(self): try: with mock.patch("kognic.auth.cli.credentials.save_credentials") as mock_save: - result = main(["credentials", "load", path, "--env", "demo"]) + result = main(["credentials", "put", path, "--env", "demo"]) self.assertEqual(result, 0) args, kwargs = mock_save.call_args self.assertEqual(args[0].client_id, "id") @@ -1171,8 +1171,49 @@ def test_load_custom_profile(self): finally: Path(path).unlink() - def test_load_missing_file_returns_error(self): - result = main(["credentials", "load", "/nonexistent/creds.json"]) + def test_put_missing_file_returns_error(self): + result = main(["credentials", "put", "/nonexistent/creds.json"]) + self.assertEqual(result, 1) + + def test_get_returns_credentials(self): + from kognic.auth.credentials_parser import ApiCredentials + + fake_creds = ApiCredentials( + client_id="my-id", + client_secret="my-secret", + email="user@kognic.com", + user_id=42, + issuer="auth.kognic.com", + ) + with mock.patch("kognic.auth.cli.credentials.load_credentials", return_value=fake_creds): + with mock.patch("builtins.print") as mock_print: + result = main(["credentials", "get"]) + self.assertEqual(result, 0) + output = json.loads(mock_print.call_args[0][0]) + self.assertEqual(output["clientId"], "my-id") + self.assertEqual(output["clientSecret"], "my-secret") + self.assertEqual(output["email"], "user@kognic.com") + self.assertEqual(output["userId"], 42) + self.assertEqual(output["issuer"], "auth.kognic.com") + + def test_get_custom_profile(self): + from kognic.auth.credentials_parser import ApiCredentials + + fake_creds = ApiCredentials( + client_id="demo-id", + client_secret="demo-secret", + email="demo@kognic.com", + user_id=1, + issuer="auth.kognic.com", + ) + with mock.patch("kognic.auth.cli.credentials.load_credentials", return_value=fake_creds) as mock_load: + result = main(["credentials", "get", "--env", "demo"]) + self.assertEqual(result, 0) + mock_load.assert_called_once_with("demo") + + def test_get_not_found_returns_error(self): + with mock.patch("kognic.auth.cli.credentials.load_credentials", return_value=None): + result = main(["credentials", "get"]) self.assertEqual(result, 1) def test_clear_removes_credentials(self): From 6da6d1c25d81310f592858f255c0addf0ed0fe44 Mon Sep 17 00:00:00 2001 From: Michel Edkrantz Date: Fri, 20 Feb 2026 17:00:56 +0100 Subject: [PATCH 2/5] Update README credentials section for put/get commands Co-Authored-By: Claude Sonnet 4.6 --- README.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 622069a..b44524a 100644 --- a/README.md +++ b/README.md @@ -107,25 +107,34 @@ kognic-auth get-access-token --env demo --server https://custom.server Manage credentials stored in the system keyring. This is the recommended way to store credentials on a developer machine — more secure than a credentials file and no environment variables needed. ```bash -kognic-auth credentials load FILE [--env ENV] +kognic-auth credentials put FILE [--env ENV] +kognic-auth credentials get [--env ENV] kognic-auth credentials clear [--env ENV] ``` -**`load`** — reads a Kognic credentials JSON file and stores the `client_id` and `client_secret` in the system keyring. +**`put`** — reads a Kognic credentials JSON file and stores it in the system keyring. - `FILE` - Path to a Kognic credentials JSON file (the same format accepted by `--credentials`) - `--env` - Profile name to store under (default: `default`). Use the environment name from `environments.json` to link the credentials to that environment. +**`get`** — prints credentials stored in the keyring as JSON. + +- `--env` - Profile name to read (default: `default`). + **`clear`** — removes credentials from the keyring for the given profile. **Examples:** ```bash # Store credentials under the default profile (used as fallback when no credentials are configured) -kognic-auth credentials load ~/Downloads/credentials.json +kognic-auth credentials put ~/Downloads/credentials.json # Store per-environment credentials -kognic-auth credentials load ~/Downloads/prod-creds.json --env production -kognic-auth credentials load ~/Downloads/demo-creds.json --env demo +kognic-auth credentials put ~/Downloads/prod-creds.json --env production +kognic-auth credentials put ~/Downloads/demo-creds.json --env demo + +# Read stored credentials +kognic-auth credentials get +kognic-auth credentials get --env production # Remove credentials kognic-auth credentials clear --env demo @@ -137,14 +146,14 @@ The system keyring (macOS Keychain, GNOME Keyring, Windows Credential Manager, e **Single-environment setup** — store once, works everywhere: ```bash -kognic-auth credentials load ~/Downloads/credentials.json +kognic-auth credentials put ~/Downloads/credentials.json # All CLI commands and the SDK will now find credentials automatically ``` **Multi-environment setup** — store per-environment credentials and reference them in `environments.json`: ```bash -kognic-auth credentials load ~/Downloads/prod-creds.json --env production -kognic-auth credentials load ~/Downloads/demo-creds.json --env demo +kognic-auth credentials put ~/Downloads/prod-creds.json --env production +kognic-auth credentials put ~/Downloads/demo-creds.json --env demo ``` Then in `~/.config/kognic/environments.json`, reference the keyring profiles with `keyring://`: From 87b3c478dabf3f022d8e500ad5fb0c029541d0c2 Mon Sep 17 00:00:00 2001 From: Michel Edkrantz Date: Fri, 20 Feb 2026 17:01:35 +0100 Subject: [PATCH 3/5] Replace demo with example in README Co-Authored-By: Claude Sonnet 4.6 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b44524a..1286495 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,10 @@ The CLI can be configured with a JSON file at `~/.config/kognic/environments.jso "auth_server": "https://auth.app.kognic.com", "credentials": "~/.config/kognic/credentials-prod.json" }, - "demo": { - "host": "demo.kognic.com", - "auth_server": "https://auth.demo.kognic.com", - "credentials": "~/.config/kognic/credentials-demo.json" + "example": { + "host": "example.kognic.com", + "auth_server": "https://auth.example.kognic.com", + "credentials": "~/.config/kognic/credentials-example.json" } } } @@ -96,10 +96,10 @@ kognic-auth get-access-token kognic-auth get-access-token --credentials ~/.config/kognic/credentials.json # Using a named environment -kognic-auth get-access-token --env demo +kognic-auth get-access-token --env example # Using an environment but overriding the server -kognic-auth get-access-token --env demo --server https://custom.server +kognic-auth get-access-token --env example --server https://custom.server ``` ### credentials @@ -130,14 +130,14 @@ kognic-auth credentials put ~/Downloads/credentials.json # Store per-environment credentials kognic-auth credentials put ~/Downloads/prod-creds.json --env production -kognic-auth credentials put ~/Downloads/demo-creds.json --env demo +kognic-auth credentials put ~/Downloads/example-creds.json --env example # Read stored credentials kognic-auth credentials get kognic-auth credentials get --env production # Remove credentials -kognic-auth credentials clear --env demo +kognic-auth credentials clear --env example ``` ### Storing credentials in the keyring @@ -153,7 +153,7 @@ kognic-auth credentials put ~/Downloads/credentials.json **Multi-environment setup** — store per-environment credentials and reference them in `environments.json`: ```bash kognic-auth credentials put ~/Downloads/prod-creds.json --env production -kognic-auth credentials put ~/Downloads/demo-creds.json --env demo +kognic-auth credentials put ~/Downloads/example-creds.json --env example ``` Then in `~/.config/kognic/environments.json`, reference the keyring profiles with `keyring://`: @@ -166,16 +166,16 @@ Then in `~/.config/kognic/environments.json`, reference the keyring profiles wit "auth_server": "https://auth.app.kognic.com", "credentials": "keyring://production" }, - "demo": { - "host": "demo.kognic.com", - "auth_server": "https://auth.demo.kognic.com", - "credentials": "keyring://demo" + "example": { + "host": "example.kognic.com", + "auth_server": "https://auth.example.kognic.com", + "credentials": "keyring://example" } } } ``` -Now `kog get https://app.kognic.com/v1/projects` automatically picks up the `production` keyring credentials, and `kog get https://demo.kognic.com/v1/projects` picks up `demo`. +Now `kog get https://app.kognic.com/v1/projects` automatically picks up the `production` keyring credentials, and `kog get https://example.kognic.com/v1/projects` picks up `example`. The `keyring://` URI also works in the `auth` parameter of API clients: ```python @@ -212,7 +212,7 @@ When `--env` is not provided, the environment is automatically resolved by match kog get https://app.kognic.com/v1/projects # Explicit environment -kog get https://demo.kognic.com/v1/projects --env demo +kog get https://example.kognic.com/v1/projects --env example # POST with JSON body kog post https://app.kognic.com/v1/projects -d '{"name": "test"}' From 2a06bdf48d14bacb7c0642dba973031a0f052e88 Mon Sep 17 00:00:00 2001 From: Michel Edkrantz Date: Fri, 20 Feb 2026 17:06:25 +0100 Subject: [PATCH 4/5] final renaming --- src/kognic/auth/cli/credentials.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/kognic/auth/cli/credentials.py b/src/kognic/auth/cli/credentials.py index d729413..2573afd 100644 --- a/src/kognic/auth/cli/credentials.py +++ b/src/kognic/auth/cli/credentials.py @@ -22,7 +22,7 @@ def register_parser(subparsers: argparse._SubParsersAction) -> argparse.Argument ) subs = parser.add_subparsers(dest="credentials_action") - put_p = subs.add_parser("put", help="Store credentials from a JSON file into the system keyring") + put_p = subs.add_parser("put", help="Put credentials from a JSON file into the system keyring") put_p.add_argument("file", metavar="FILE", help="Path to credentials JSON file") put_p.add_argument( "--env", @@ -33,7 +33,7 @@ def register_parser(subparsers: argparse._SubParsersAction) -> argparse.Argument "(e.g. --env production → use 'keyring://production' in your config).", ) - get_p = subs.add_parser("get", help="Read stored credentials from the system keyring") + get_p = subs.add_parser("get", help="Get stored credentials from the system keyring") get_p.add_argument( "--env", default=DEFAULT_PROFILE, @@ -41,7 +41,7 @@ def register_parser(subparsers: argparse._SubParsersAction) -> argparse.Argument help=f"Keyring profile name to read credentials from (default: {DEFAULT_PROFILE}).", ) - clear_p = subs.add_parser("clear", help="Remove stored credentials from the system keyring") + clear_p = subs.add_parser("delete", help="Delete stored credentials from the system keyring") clear_p.add_argument( "--env", default=DEFAULT_PROFILE, @@ -56,9 +56,9 @@ def run(parsed: argparse.Namespace) -> int: if parsed.credentials_action == "put": return _run_put(parsed) if parsed.credentials_action == "get": - return _run_read(parsed) - if parsed.credentials_action == "clear": - return _run_clear(parsed) + return _run_get(parsed) + if parsed.credentials_action == "delete": + return _run_delete(parsed) return 0 @@ -79,7 +79,7 @@ def _run_put(parsed: argparse.Namespace) -> int: return 1 -def _run_read(parsed: argparse.Namespace) -> int: +def _run_get(parsed: argparse.Namespace) -> int: try: creds = load_credentials(parsed.env) if creds is None: @@ -99,7 +99,7 @@ def _run_read(parsed: argparse.Namespace) -> int: return 1 -def _run_clear(parsed: argparse.Namespace) -> int: +def _run_delete(parsed: argparse.Namespace) -> int: try: clear_credentials(parsed.env) print(f"Credentials cleared from keyring (profile={parsed.env!r})") From c5e76dd2bba6b9ef6832ebda521096e5953a7b99 Mon Sep 17 00:00:00 2001 From: Michel Edkrantz Date: Fri, 20 Feb 2026 17:07:52 +0100 Subject: [PATCH 5/5] Rename clear to delete in credentials subcommand, tests, and README Co-Authored-By: Claude Sonnet 4.6 --- README.md | 6 +++--- tests/test_cli.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1286495..d1ff39b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Manage credentials stored in the system keyring. This is the recommended way to ```bash kognic-auth credentials put FILE [--env ENV] kognic-auth credentials get [--env ENV] -kognic-auth credentials clear [--env ENV] +kognic-auth credentials delete [--env ENV] ``` **`put`** — reads a Kognic credentials JSON file and stores it in the system keyring. @@ -121,7 +121,7 @@ kognic-auth credentials clear [--env ENV] - `--env` - Profile name to read (default: `default`). -**`clear`** — removes credentials from the keyring for the given profile. +**`delete`** — removes credentials from the keyring for the given profile. **Examples:** ```bash @@ -137,7 +137,7 @@ kognic-auth credentials get kognic-auth credentials get --env production # Remove credentials -kognic-auth credentials clear --env example +kognic-auth credentials delete --env example ``` ### Storing credentials in the keyring diff --git a/tests/test_cli.py b/tests/test_cli.py index 2a85b62..8a62543 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1216,15 +1216,15 @@ def test_get_not_found_returns_error(self): result = main(["credentials", "get"]) self.assertEqual(result, 1) - def test_clear_removes_credentials(self): + def test_delete_removes_credentials(self): with mock.patch("kognic.auth.cli.credentials.clear_credentials") as mock_clear: - result = main(["credentials", "clear"]) + result = main(["credentials", "delete"]) self.assertEqual(result, 0) mock_clear.assert_called_once_with("default") - def test_clear_custom_profile(self): + def test_delete_custom_profile(self): with mock.patch("kognic.auth.cli.credentials.clear_credentials") as mock_clear: - result = main(["credentials", "clear", "--env", "demo"]) + result = main(["credentials", "delete", "--env", "demo"]) self.assertEqual(result, 0) mock_clear.assert_called_once_with("demo")