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]