This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-releases-client.git


The following commit(s) were added to refs/heads/main by this push:
     new 2e34718  Add commands to manage SSH keys, and associated tests
2e34718 is described below

commit 2e347185881053593f64b756171c4ec8155a1786
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 15 20:42:00 2025 +0100

    Add commands to manage SSH keys, and associated tests
---
 pyproject.toml              |  4 +--
 src/atrclient/client.py     | 54 +++++++++++++++++++++++++++++++++++++---
 src/atrclient/models/api.py | 60 +++++++++++++++++++++++++++------------------
 tests/cli_ssh.t             | 37 ++++++++++++++++++++++++++++
 uv.lock                     |  4 +--
 5 files changed, 128 insertions(+), 31 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 8dbbede..f7b124b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
 
 [project]
 name            = "apache-trusted-releases"
-version         = "0.20250715.1846"
+version         = "0.20250715.1941"
 description     = "ATR CLI and Python API"
 readme          = "README.md"
 requires-python = ">=3.13"
@@ -72,4 +72,4 @@ select = [
 ]
 
 [tool.uv]
-exclude-newer = "2025-07-15T18:46:00Z"
+exclude-newer = "2025-07-15T19:41:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index 0799eac..591c972 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -56,7 +56,9 @@ APP_CONFIG: cyclopts.App = cyclopts.App(name="config", 
help="Configuration opera
 APP_DEV: cyclopts.App = cyclopts.App(name="dev", help="Developer operations.")
 APP_DRAFT: cyclopts.App = cyclopts.App(name="draft", help="Draft operations.")
 APP_JWT: cyclopts.App = cyclopts.App(name="jwt", help="JWT operations.")
+APP_KEYS: cyclopts.App = cyclopts.App(name="keys", help="Keys operations.")
 APP_RELEASE: cyclopts.App = cyclopts.App(name="release", help="Release 
operations.")
+APP_SSH: cyclopts.App = cyclopts.App(name="ssh", help="SSH operations.")
 APP_VOTE: cyclopts.App = cyclopts.App(name="vote", help="Vote operations.")
 VERSION: str = metadata.version("apache-trusted-releases")
 YAML_DEFAULTS: dict[str, Any] = {"asf": {}, "atr": {}, "tokens": {}}
@@ -198,6 +200,24 @@ def api_revisions(api: ApiGet, project: str, version: str) 
-> models.api.Revisio
     return models.api.validate_revisions(response)
 
 
+@api_post("/ssh/add")
+def api_ssh_add(api: ApiPost, args: models.api.SshAddArgs) -> 
models.api.SshAddResults:
+    response = api.post(args)
+    return models.api.validate_ssh_add(response)
+
+
+@api_post("/ssh/delete")
+def api_ssh_delete(api: ApiPost, args: models.api.SshDeleteArgs) -> 
models.api.SshDeleteResults:
+    response = api.post(args)
+    return models.api.validate_ssh_delete(response)
+
+
+@api_get("/ssh/list")
+def api_ssh_list(api: ApiGet, asf_uid: str) -> models.api.SshListResults:
+    response = api.get(asf_uid)
+    return models.api.validate_ssh_list(response)
+
+
 @api_post("/upload")
 def api_upload(api: ApiPost, args: models.api.UploadArgs) -> 
models.api.UploadResults:
     response = api.post(args)
@@ -561,6 +581,31 @@ def app_show(path: str, /) -> None:
     print(value)
 
 
+@APP_SSH.command(name="add", help="Add an SSH key.")
+def app_ssh_add(text: str, /) -> None:
+    ssh_add_args = models.api.SshAddArgs(text=text)
+    ssh_add = api_ssh_add(ssh_add_args)
+    print(ssh_add.fingerprint)
+
+
+@APP_SSH.command(name="delete", help="Delete an SSH key.")
+def app_ssh_delete(fingerprint: str, /) -> None:
+    ssh_delete_args = models.api.SshDeleteArgs(fingerprint=fingerprint)
+    ssh_delete = api_ssh_delete(ssh_delete_args)
+    print(ssh_delete.success)
+
+
+@APP_SSH.command(name="list", help="List SSH keys.")
+def app_ssh_list(asf_uid: str | None = None) -> None:
+    if asf_uid is None:
+        with config_lock() as config:
+            asf_uid = config_get(config, ["asf", "uid"])
+    if asf_uid is None:
+        show_error_and_exit("No ASF UID provided and asf.uid not configured.")
+    ssh_list = api_ssh_list(asf_uid)
+    print(ssh_list.data)
+
+
 @APP.command(name="upload", help="Upload a file to a release.")
 def app_upload(project: str, version: str, path: str, filepath: str, /) -> 
None:
     with open(filepath, "rb") as fh:
@@ -1009,7 +1054,9 @@ def subcommands_register(app: cyclopts.App) -> None:
     app.command(APP_DEV)
     app.command(APP_DRAFT)
     app.command(APP_JWT)
+    app.command(APP_KEYS)
     app.command(APP_RELEASE)
+    app.command(APP_SSH)
     app.command(APP_VOTE)
 
 
@@ -1036,11 +1083,12 @@ async def web_get(url: str, jwt_token: str | None, 
verify_ssl: bool = True) -> J
                 try:
                     error_data = json.loads(text)
                     if isinstance(error_data, dict) and ("error" in 
error_data):
-                        show_error_and_exit(error_data["error"])
+                        error_message = error_data["error"]
+                        show_error_and_exit(f"{error_message} from {url}")
                     else:
-                        show_error_and_exit(f"Request failed: {resp.status} 
{text}")
+                        show_error_and_exit(f"Request failed: {resp.status} 
{url}\n{text}")
                 except json.JSONDecodeError:
-                    show_error_and_exit(f"Request failed: {resp.status} 
{text}")
+                    show_error_and_exit(f"Request failed: {resp.status} 
{url}\n{text}")
             data = await resp.json()
             if not is_json(data):
                 show_error_and_exit(f"Unexpected API response: {data}")
diff --git a/src/atrclient/models/api.py b/src/atrclient/models/api.py
index 02e63b6..171b3f6 100644
--- a/src/atrclient/models/api.py
+++ b/src/atrclient/models/api.py
@@ -118,26 +118,6 @@ class KeysResults(schema.Strict):
     count: int
 
 
-class KeysSshAddArgs(schema.Strict):
-    text: str
-
-
-class KeysSshAddResults(schema.Strict):
-    endpoint: Literal["/keys/ssh/add"] = schema.Field(alias="endpoint")
-    fingerprint: str
-
-
-class KeysSshListQuery:
-    offset: int = 0
-    limit: int = 20
-
-
-class KeysSshListResults(schema.Strict):
-    endpoint: Literal["/keys/ssh/list"] = schema.Field(alias="endpoint")
-    data: Sequence[sql.SSHKey]
-    count: int
-
-
 class ProjectResults(schema.Strict):
     endpoint: Literal["/project"] = schema.Field(alias="endpoint")
     project: sql.Project
@@ -215,6 +195,36 @@ class RevisionsResults(schema.Strict):
     revisions: Sequence[sql.Revision]
 
 
+class SshAddArgs(schema.Strict):
+    text: str
+
+
+class SshAddResults(schema.Strict):
+    endpoint: Literal["/ssh/add"] = schema.Field(alias="endpoint")
+    fingerprint: str
+
+
+class SshDeleteArgs(schema.Strict):
+    fingerprint: str
+
+
+class SshDeleteResults(schema.Strict):
+    endpoint: Literal["/ssh/delete"] = schema.Field(alias="endpoint")
+    success: str
+
+
[email protected]
+class SshListQuery:
+    offset: int = 0
+    limit: int = 20
+
+
+class SshListResults(schema.Strict):
+    endpoint: Literal["/ssh/list"] = schema.Field(alias="endpoint")
+    data: Sequence[sql.SSHKey]
+    count: int
+
+
 @dataclasses.dataclass
 class TasksQuery:
     limit: int = 20
@@ -280,8 +290,6 @@ Results = Annotated[
     | JwtResults
     | KeyResults
     | KeysResults
-    | KeysSshAddResults
-    | KeysSshListResults
     | ListResults
     | ProjectResults
     | ProjectReleasesResults
@@ -293,6 +301,9 @@ Results = Annotated[
     | ReleasesVersionResults
     | ReleasesRevisionsResults
     | RevisionsResults
+    | SshAddResults
+    | SshDeleteResults
+    | SshListResults
     | TasksResults
     | VoteResolveResults
     | VoteStartResults
@@ -324,8 +335,6 @@ validate_draft_delete = validator(DraftDeleteResults)
 validate_jwt = validator(JwtResults)
 validate_key = validator(KeyResults)
 validate_keys = validator(KeysResults)
-validate_keys_ssh_add = validator(KeysSshAddResults)
-validate_keys_ssh_list = validator(KeysSshListResults)
 validate_list = validator(ListResults)
 validate_project = validator(ProjectResults)
 validate_project_releases = validator(ProjectReleasesResults)
@@ -337,6 +346,9 @@ validate_releases_project = 
validator(ReleasesProjectResults)
 validate_releases_version = validator(ReleasesVersionResults)
 validate_releases_revisions = validator(ReleasesRevisionsResults)
 validate_revisions = validator(RevisionsResults)
+validate_ssh_add = validator(SshAddResults)
+validate_ssh_delete = validator(SshDeleteResults)
+validate_ssh_list = validator(SshListResults)
 validate_tasks = validator(TasksResults)
 validate_vote_resolve = validator(VoteResolveResults)
 validate_vote_start = validator(VoteStartResults)
diff --git a/tests/cli_ssh.t b/tests/cli_ssh.t
new file mode 100644
index 0000000..7f87b4f
--- /dev/null
+++ b/tests/cli_ssh.t
@@ -0,0 +1,37 @@
+$ atr set atr.host 127.0.0.1:8080
+Set atr.host to "127.0.0.1:8080".
+
+$ atr dev user
+<?user?>
+
+$ atr set asf.uid <!user!>
+Set asf.uid to "<!user!>".
+
+$ atr dev pat
+<?pat?>
+
+$ atr set tokens.pat <!pat!>
+Set tokens.pat to "<!pat!>".
+
+$ atr ssh list
+<.etc.>
+
+! atr ssh add invalid-key
+<.stderr.>
+atr: error: Error message from the API:
+500 https://127.0.0.1:8080/api/ssh/add
+{
+  "error": "Invalid SSH key format"
+}
+
+* atr ssh delete SHA256:p/i72djQU2/tmcCOtW6YzLoSVhmaaaQd+2/uaTkbp4M
+<.etc.>
+
+$ atr ssh add "ssh-ed25519 
AAAAC3NzaC1lZDI1NTE5AAAAIL63zVyeqQ0jF33V9Uq+R0cmsgZC8RoG9yZoe3Zap0Xl 
testing-key"
+SHA256:p/i72djQU2/tmcCOtW6YzLoSVhmaaaQd+2/uaTkbp4M
+
+$ atr ssh list
+<.skip.>SHA256:p/i72djQU2/tmcCOtW6YzLoSVhmaaaQd+2/uaTkbp4M<.skip.>
+
+* atr ssh delete SHA256:p/i72djQU2/tmcCOtW6YzLoSVhmaaaQd+2/uaTkbp4M
+<.etc.>
diff --git a/uv.lock b/uv.lock
index 2cd71f9..f7e7686 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
 requires-python = ">=3.13"
 
 [options]
-exclude-newer = "2025-07-15T18:46:00Z"
+exclude-newer = "2025-07-15T19:41:00Z"
 
 [[package]]
 name = "aiohappyeyeballs"
@@ -83,7 +83,7 @@ wheels = [
 
 [[package]]
 name = "apache-trusted-releases"
-version = "0.20250715.1846"
+version = "0.20250715.1941"
 source = { editable = "." }
 dependencies = [
     { name = "aiohttp" },


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to