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 687cc4e Add a command to upload a KEYS file
687cc4e is described below
commit 687cc4ef07f750442eed17e68eba271968424b3f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jul 16 19:21:29 2025 +0100
Add a command to upload a KEYS file
---
pyproject.toml | 4 ++--
src/atrclient/client.py | 39 +++++++++++++++++++++++++++++----------
src/atrclient/models/api.py | 25 ++++++++++++++++++++++++-
src/atrclient/models/schema.py | 4 ++++
tests/cli_keys.t | 9 ++++++---
uv.lock | 4 ++--
6 files changed, 67 insertions(+), 18 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index ccf0ab4..02ee32e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
[project]
name = "apache-trusted-releases"
-version = "0.20250716.1734"
+version = "0.20250716.1818"
description = "ATR CLI and Python API"
readme = "README.md"
requires-python = ">=3.13"
@@ -72,4 +72,4 @@ select = [
]
[tool.uv]
-exclude-newer = "2025-07-16T17:34:00Z"
+exclude-newer = "2025-07-16T18:18:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index ae5a0a7..8979763 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -34,7 +34,6 @@ import re
import signal
import sys
import time
-from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any, Literal, NoReturn,
TypeGuard, TypeVar
import aiohttp
@@ -48,7 +47,7 @@ import strictyaml
import atrclient.models as models
if TYPE_CHECKING:
- from collections.abc import Generator, Sequence
+ from collections.abc import Callable, Generator, Sequence
APP: cyclopts.App = cyclopts.App()
APP_CHECKS: cyclopts.App = cyclopts.App(name="checks", help="Check result
operations.")
@@ -104,12 +103,9 @@ class ApiPost(ApiCore):
A = TypeVar("A", bound=models.schema.Strict)
R = TypeVar("R", bound=models.api.Results)
-ApiGetFunction = Callable[..., R]
-ApiPostFunction = Callable[[ApiPost, A], R]
-
-def api_get(path: str) -> Callable[[ApiGetFunction], Callable[..., R]]:
- def decorator(func: ApiGetFunction) -> Callable[..., R]:
+def api_get(path: str) -> Callable[[Callable[..., R]], Callable[..., R]]:
+ def decorator(func: Callable[..., R]) -> Callable[..., R]:
def wrapper(*args: str, **kwargs: str | None) -> R:
api_instance = ApiGet(path)
try:
@@ -123,8 +119,8 @@ def api_get(path: str) -> Callable[[ApiGetFunction],
Callable[..., R]]:
return decorator
-def api_post(path: str) -> Callable[[ApiPostFunction], Callable[[A], R]]:
- def decorator(func: ApiPostFunction) -> Callable[[A], R]:
+def api_post(path: str) -> Callable[[Callable[[ApiPost, A], R]], Callable[[A],
R]]:
+ def decorator(func: Callable[[ApiPost, A], R]) -> Callable[[A], R]:
def wrapper(args: A) -> R:
api_instance = ApiPost(path)
try:
@@ -182,6 +178,12 @@ def api_keys_get(api: ApiGet, fingerprint: str) ->
models.api.KeysGetResults:
return models.api.validate_keys_get(response)
+@api_post("/keys/upload")
+def api_keys_upload(api: ApiPost, args: models.api.KeysUploadArgs) ->
models.api.KeysUploadResults:
+ response = api.post(args)
+ return models.api.validate_keys_upload(response)
+
+
@api_get("/keys/user")
def api_keys_user(api: ApiGet, asf_uid: str) -> models.api.KeysUserResults:
response = api.get(asf_uid)
@@ -622,12 +624,15 @@ def app_jwt_show() -> None:
@APP_KEYS.command(name="add", help="Add an OpenPGP key.")
def app_keys_add(path: str, committees: str = "", /) -> None:
+ selected_committee_names = []
+ if committees:
+ selected_committee_names[:] = committees.split(",")
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_args = models.api.KeysAddArgs(asfuid=asf_uid, key=key,
committees=selected_committee_names)
keys_add = api_keys_add(keys_add_args)
for fingerprint in keys_add.fingerprints:
print(fingerprint)
@@ -646,6 +651,20 @@ def app_keys_get(fingerprint: str, /) -> None:
print(keys_get.key.model_dump_json(indent=None))
+@APP_KEYS.command(name="upload", help="Upload a KEYS file.")
+def app_keys_upload(path: str, selected_committees: str, /) -> None:
+ selected_committee_names = []
+ if selected_committees:
+ selected_committee_names[:] = selected_committees.split(",")
+ key = pathlib.Path(path).read_text(encoding="utf-8")
+ keys_upload_args = models.api.KeysUploadArgs(filetext=key,
committees=selected_committee_names)
+ keys_upload = api_keys_upload(keys_upload_args)
+ for result in keys_upload.results:
+ print(result.model_dump_json(indent=None))
+ print(f"Successfully uploaded {keys_upload.success_count} keys.")
+ print(f"Failed to upload {keys_upload.error_count} keys.")
+
+
@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 336ab2e..14cc85c 100644
--- a/src/atrclient/models/api.py
+++ b/src/atrclient/models/api.py
@@ -116,7 +116,7 @@ class KeysResults(schema.Strict):
class KeysAddArgs(schema.Strict):
asfuid: str
key: str
- committees: str
+ committees: list[str]
class KeysAddResults(schema.Strict):
@@ -144,6 +144,27 @@ class KeysGetResults(schema.Strict):
key: sql.PublicSigningKey
+class KeysUploadArgs(schema.Strict):
+ filetext: str
+ committees: list[str]
+
+
+class KeysUploadSubset(schema.Lax):
+ status: Literal["success", "error"]
+ key_id: str
+ fingerprint: str
+ user_id: str
+ email: str
+
+
+class KeysUploadResults(schema.Strict):
+ endpoint: Literal["/keys/upload"] = schema.Field(alias="endpoint")
+ results: Sequence[KeysUploadSubset]
+ success_count: int
+ error_count: int
+ submitted_committees: list[str]
+
+
class KeysUserResults(schema.Strict):
endpoint: Literal["/keys/user"] = schema.Field(alias="endpoint")
keys: Sequence[sql.PublicSigningKey]
@@ -329,6 +350,7 @@ Results = Annotated[
| KeysDeleteResults
| KeysGetResults
| KeysCommitteeResults
+ | KeysUploadResults
| KeysUserResults
| ListResults
| ProjectResults
@@ -379,6 +401,7 @@ validate_keys_add = validator(KeysAddResults)
validate_keys_committee = validator(KeysCommitteeResults)
validate_keys_delete = validator(KeysDeleteResults)
validate_keys_get = validator(KeysGetResults)
+validate_keys_upload = validator(KeysUploadResults)
validate_keys_user = validator(KeysUserResults)
validate_list = validator(ListResults)
validate_project = validator(ProjectResults)
diff --git a/src/atrclient/models/schema.py b/src/atrclient/models/schema.py
index 37427f0..e2ecf82 100644
--- a/src/atrclient/models/schema.py
+++ b/src/atrclient/models/schema.py
@@ -24,6 +24,10 @@ import pydantic
Field = pydantic.Field
+class Lax(pydantic.BaseModel):
+ model_config = pydantic.ConfigDict(extra="allow", strict=False,
validate_assignment=True)
+
+
class Strict(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid", strict=True,
validate_assignment=True)
diff --git a/tests/cli_keys.t b/tests/cli_keys.t
index 636e326..e6c8236 100644
--- a/tests/cli_keys.t
+++ b/tests/cli_keys.t
@@ -26,7 +26,10 @@ $ atr keys add tooling-public-test.asc
E35604DD9E2892E5465B3D8A203F105A7B33A64F
$ atr keys get E35604DD9E2892E5465B3D8A203F105A7B33A64F
-<.skip.>e35604dd9e2892e5465b3d8a203f105a7b33a64f<.skip.><!user!><.skip.>
+<.skip.>e35604dd9e2892e5465b3d8a203f105a7b33a64f<.skip.>example.invalid<.skip.>
-$ atr keys delete E35604DD9E2892E5465B3D8A203F105A7B33A64F
-Key deleted
+* atr keys delete E35604DD9E2892E5465B3D8A203F105A7B33A64F
+<.etc.>
+
+$ atr keys upload tooling-public-test.asc tooling
+<.etc.>
diff --git a/uv.lock b/uv.lock
index ff819b7..2501849 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
requires-python = ">=3.13"
[options]
-exclude-newer = "2025-07-16T17:34:00Z"
+exclude-newer = "2025-07-16T18:18:00Z"
[[package]]
name = "aiohappyeyeballs"
@@ -83,7 +83,7 @@ wheels = [
[[package]]
name = "apache-trusted-releases"
-version = "0.20250716.1734"
+version = "0.20250716.1818"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]