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]

Reply via email to