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 4b3d51e  Add a command to associate an OpenPGP key with a user account
4b3d51e is described below

commit 4b3d51eba704814ac2ec99d15a22bcb1a0b05eed
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jul 16 17:16:49 2025 +0100

    Add a command to associate an OpenPGP key with a user account
---
 pyproject.toml              |  4 +-
 src/atrclient/client.py     | 95 +++++++++++++++++++++++++++++++++++++++++++++
 src/atrclient/models/api.py | 14 +++++++
 tests/cli_keys.t            |  9 +++++
 tests/test_all.py           |  5 ++-
 uv.lock                     |  4 +-
 6 files changed, 126 insertions(+), 5 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 3158a05..5aa836e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
 
 [project]
 name            = "apache-trusted-releases"
-version         = "0.20250716.1524"
+version         = "0.20250716.1616"
 description     = "ATR CLI and Python API"
 readme          = "README.md"
 requires-python = ">=3.13"
@@ -72,4 +72,4 @@ select = [
 ]
 
 [tool.uv]
-exclude-newer = "2025-07-16T15:24:00Z"
+exclude-newer = "2025-07-16T16:16:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index fb54b07..feaa676 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -164,6 +164,12 @@ def api_draft_delete(api: ApiPost, args: 
models.api.DraftDeleteArgs) -> models.a
     return models.api.validate_draft_delete(response)
 
 
+@api_post("/keys/add")
+def api_keys_add(api: ApiPost, args: models.api.KeysAddArgs) -> 
models.api.KeysAddResults:
+    response = api.post(args)
+    return models.api.validate_keys_add(response)
+
+
 @api_get("/keys/user")
 def api_keys_user(api: ApiGet, asf_uid: str) -> models.api.KeysUserResults:
     response = api.get(asf_uid)
@@ -390,6 +396,77 @@ def app_dev_env() -> None:
     print(f"There are {total} ATR_* environment variables.")
 
 
+@APP_DEV.command(name="key", help="Return a test OpenPGP key.")
+def app_dev_key() -> None:
+    with open("tooling-public-test.asc", "w") as w:
+        w.write("""\
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+Qx20dp/Ekji4w6nAtopc4CTjL7YeRFdBKQ==
+=CTh1
+-----END PGP PUBLIC KEY BLOCK-----
+""")
+
+
 @APP_DEV.command(name="pat", help="Read a PAT from development configuration.")
 def app_dev_pat() -> None:
     atr_pat_path = pathlib.Path.home() / ".atr-pat"
@@ -399,6 +476,11 @@ def app_dev_pat() -> None:
     print(text)
 
 
+@APP_DEV.command(name="pwd", help="Show the current working directory.")
+def app_dev_pwd() -> None:
+    print(os.getcwd())
+
+
 @APP_DEV.command(name="stamp", help="Update version and exclude-newer in 
pyproject.toml.")
 def app_dev_stamp() -> None:
     path = pathlib.Path("pyproject.toml")
@@ -526,6 +608,19 @@ def app_jwt_show() -> None:
     return app_show("tokens.jwt")
 
 
+@APP_KEYS.command(name="add", help="Add an OpenPGP key.")
+def app_keys_add(path: str, committees: str = "", /) -> None:
+    key = pathlib.Path(path).read_text(encoding="utf-8")
+    with config_lock() as config:
+        asf_uid = config_get(config, ["asf", "uid"])
+    if asf_uid is None:
+        show_error_and_exit("Please configure asf.uid before adding a key.")
+    keys_add_args = models.api.KeysAddArgs(asfuid=asf_uid, key=key, 
committees=committees)
+    keys_add = api_keys_add(keys_add_args)
+    for fingerprint in keys_add.fingerprints:
+        print(fingerprint)
+
+
 @APP_KEYS.command(name="user", help="List OpenPGP keys for a user.")
 def app_keys_user(asf_uid: str | None = None) -> None:
     if asf_uid is None:
diff --git a/src/atrclient/models/api.py b/src/atrclient/models/api.py
index 4d51105..be659ef 100644
--- a/src/atrclient/models/api.py
+++ b/src/atrclient/models/api.py
@@ -101,6 +101,18 @@ class JwtResults(schema.Strict):
     jwt: str
 
 
+class KeysAddArgs(schema.Strict):
+    asfuid: str
+    key: str
+    committees: str
+
+
+class KeysAddResults(schema.Strict):
+    endpoint: Literal["/keys/add"] = schema.Field(alias="endpoint")
+    success: str
+    fingerprints: list[str]
+
+
 @dataclasses.dataclass
 class KeysQuery:
     offset: int = 0
@@ -304,6 +316,7 @@ Results = Annotated[
     | DraftDeleteResults
     | JwtResults
     | KeysResults
+    | KeysAddResults
     | KeysGetResults
     | KeysCommitteeResults
     | KeysUserResults
@@ -352,6 +365,7 @@ validate_committees_projects = 
validator(CommitteesProjectsResults)
 validate_draft_delete = validator(DraftDeleteResults)
 validate_jwt = validator(JwtResults)
 validate_keys = validator(KeysResults)
+validate_keys_add = validator(KeysAddResults)
 validate_keys_committee = validator(KeysCommitteeResults)
 validate_keys_get = validator(KeysGetResults)
 validate_keys_user = validator(KeysUserResults)
diff --git a/tests/cli_keys.t b/tests/cli_keys.t
index 4b1b953..c424e54 100644
--- a/tests/cli_keys.t
+++ b/tests/cli_keys.t
@@ -15,3 +15,12 @@ Set tokens.pat to "<!pat!>".
 
 $ atr keys user
 <.etc.>
+
+$ atr dev pwd
+<.skip.>tmp<.skip.>
+
+<# write a test key to tooling-public-test.asc #>
+$ atr dev key
+
+$ atr keys add tooling-public-test.asc
+E35604DD9E2892E5465B3D8A203F105A7B33A64F
diff --git a/tests/test_all.py b/tests/test_all.py
index fe79150..2268b06 100755
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -23,6 +23,7 @@ import os
 import pathlib
 import re
 import shlex
+import tempfile
 from typing import TYPE_CHECKING, Any, Final
 
 import aioresponses
@@ -233,7 +234,9 @@ def test_cli_transcripts(
     script_runner: pytest_console_scripts.ScriptRunner,
     fixture_config_env: pathlib.Path,
 ) -> None:
-    return transcript_capture(transcript_path, script_runner, 
fixture_config_env)
+    with tempfile.TemporaryDirectory() as tmpdir:
+        os.chdir(tmpdir)
+        return transcript_capture(transcript_path, script_runner, 
fixture_config_env)
 
 
 def test_config_set_get_roundtrip() -> None:
diff --git a/uv.lock b/uv.lock
index 86207c3..86a01e3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
 requires-python = ">=3.13"
 
 [options]
-exclude-newer = "2025-07-16T15:24:00Z"
+exclude-newer = "2025-07-16T16:16:00Z"
 
 [[package]]
 name = "aiohappyeyeballs"
@@ -83,7 +83,7 @@ wheels = [
 
 [[package]]
 name = "apache-trusted-releases"
-version = "0.20250716.1524"
+version = "0.20250716.1616"
 source = { editable = "." }
 dependencies = [
     { name = "aiohttp" },


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

Reply via email to