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]