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 70a4ec1  Use consistent API types for JWT requests and more
70a4ec1 is described below

commit 70a4ec1202a10ca698332e860d5a5f22796accd1
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 15 16:39:56 2025 +0100

    Use consistent API types for JWT requests and more
---
 pyproject.toml              |  4 +--
 src/atrclient/client.py     | 58 ++++++++++++++------------------
 src/atrclient/models/api.py | 81 ++++++++++++++++++++++++++++++++++++++++++---
 tests/test_all.py           |  8 -----
 uv.lock                     |  4 +--
 5 files changed, 106 insertions(+), 49 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 78e2ad5..26d3520 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
 
 [project]
 name            = "apache-trusted-releases"
-version         = "0.20250715.1512"
+version         = "0.20250715.1539"
 description     = "ATR CLI and Python API"
 readme          = "README.md"
 requires-python = ">=3.13"
@@ -72,4 +72,4 @@ select = [
 ]
 
 [tool.uv]
-exclude-newer = "2025-07-15T15:12:00Z"
+exclude-newer = "2025-07-15T15:39:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index 693c399..8cbdfc8 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -344,8 +344,12 @@ def app_draft_delete(project: str, version: str, /) -> 
None:
     host, verify_ssl = config_host_get()
     args = models.api.ProjectVersion(project=project, version=version)
     url = f"https://{host}/api/draft/delete";
-    result = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
-    print_json(result)
+    response = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
+    try:
+        draft_delete = models.api.validate_draft_delete(response)
+    except (pydantic.ValidationError, models.api.ResultsTypeError) as e:
+        show_error_and_exit(f"Unexpected API response: {response}\n{e}")
+    print(draft_delete.success)
 
 
 @APP.command(name="docs", help="Show comprehensive CLI documentation in 
Markdown.")
@@ -422,8 +426,13 @@ def app_list(project: str, version: str, revision: str | 
None = None, /) -> None
     url = f"https://{host}/api/list/{project}/{version}";
     if revision:
         url += f"/{revision}"
-    result = asyncio.run(web_get(url, jwt_value, verify_ssl))
-    print(result)
+    response = asyncio.run(web_get(url, jwt_value, verify_ssl))
+    try:
+        list_results = models.api.validate_list(response)
+    except (pydantic.ValidationError, models.api.ResultsTypeError) as e:
+        show_error_and_exit(f"Unexpected API response: {response}\n{e}")
+    for rel_path in list_results.rel_paths:
+        print(rel_path)
 
 
 @APP_RELEASE.command(name="info", help="Show information about a release.")
@@ -698,12 +707,17 @@ def config_jwt_refresh(asf_uid: str | None = None) -> str:
     host, verify_ssl = config_host_get()
     url = f"https://{host}/api/jwt";
 
-    jwt_token = asyncio.run(web_fetch(url, asf_uid, pat_value, verify_ssl))
+    args = models.api.JwtArgs(asfuid=asf_uid, pat=pat_value)
+    response = asyncio.run(web_post(url, args, jwt_token=None, 
verify_ssl=verify_ssl))
+    try:
+        jwt_results = models.api.validate_jwt(response)
+    except (pydantic.ValidationError, models.api.ResultsTypeError) as e:
+        show_error_and_exit(f"Unexpected API response: {response}\n{e}")
 
     with config_lock(write=True) as config:
-        config_set(config, ["tokens", "jwt"], jwt_token)
+        config_set(config, ["tokens", "jwt"], jwt_results.jwt)
 
-    return jwt_token
+    return jwt_results.jwt
 
 
 def config_jwt_usable() -> str:
@@ -983,30 +997,6 @@ def timestamp_format(ts: int | str | None) -> str | None:
         return str(ts)
 
 
-async def web_fetch(url: str, asfuid: str, pat_token: str, verify_ssl: bool = 
True) -> str:
-    # TODO: This is PAT request specific
-    # Should give this a more specific name, e.g. web_post_pat
-    connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
-    async with aiohttp.ClientSession(connector=connector) as session:
-        payload = {"asfuid": asfuid, "pat": pat_token}
-        async with session.post(url, json=payload) as resp:
-            if resp.status != 200:
-                text = await resp.text()
-                show_error_and_exit(f"JWT fetch failed: {payload!r} 
{resp.status} {text!r}")
-
-            data = await resp.json()
-            if not is_json(data):
-                show_error_and_exit(f"Unexpected API response: {data}")
-            if not is_json_dict(data):
-                show_error_and_exit(f"Unexpected API response: {data}")
-            if "jwt" in data:
-                jwt_value = data["jwt"]
-                if not isinstance(jwt_value, str):
-                    show_error_and_exit(f"Unexpected API response: {data}")
-                return jwt_value
-            raise RuntimeError(f"Unexpected response: {data}")
-
-
 async def web_get(url: str, jwt_token: str, verify_ssl: bool = True) -> JSON:
     connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
     headers = {"Authorization": f"Bearer {jwt_token}"}
@@ -1048,9 +1038,11 @@ async def web_get_public(url: str, verify_ssl: bool = 
True) -> JSON:
             return data
 
 
-async def web_post(url: str, args: models.schema.Strict, jwt_token: str, 
verify_ssl: bool = True) -> JSON:
+async def web_post(url: str, args: models.schema.Strict, jwt_token: str | 
None, verify_ssl: bool = True) -> JSON:
     connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
-    headers = {"Authorization": f"Bearer {jwt_token}"}
+    headers = {}
+    if jwt_token is not None:
+        headers["Authorization"] = f"Bearer {jwt_token}"
     async with aiohttp.ClientSession(connector=connector, headers=headers) as 
session:
         async with session.post(url, json=args.model_dump()) as resp:
             if resp.status not in (200, 201):
diff --git a/src/atrclient/models/api.py b/src/atrclient/models/api.py
index e67bcf5..9a0539f 100644
--- a/src/atrclient/models/api.py
+++ b/src/atrclient/models/api.py
@@ -93,16 +93,73 @@ class CommitteesProjectsResults(schema.Strict):
     projects: Sequence[sql.Project]
 
 
-class AsfuidPat(schema.Strict):
+class DraftDeleteArgs(schema.Strict):
+    project: str
+    version: str
+
+
+class DraftDeleteResults(schema.Strict):
+    endpoint: Literal["/draft/delete"] = schema.Field(alias="endpoint")
+    success: str
+
+
+class ListResults(schema.Strict):
+    endpoint: Literal["/list"] = schema.Field(alias="endpoint")
+    rel_paths: Sequence[str]
+
+
+class JwtArgs(schema.Strict):
     asfuid: str
     pat: str
 
 
-class Fingerprint(schema.Strict):
+class JwtResults(schema.Strict):
+    endpoint: Literal["/jwt"] = schema.Field(alias="endpoint")
+    asfuid: str
+    jwt: str
+
+
+class KeyResults(schema.Strict):
+    endpoint: Literal["/key"] = schema.Field(alias="endpoint")
+    key: sql.PublicSigningKey
+
+
[email protected]
+class KeysQuery:
+    offset: int = 0
+    limit: int = 20
+
+
+class KeysResults(schema.Strict):
+    endpoint: Literal["/keys"] = schema.Field(alias="endpoint")
+    data: Sequence[sql.PublicSigningKey]
+    count: int
+
+
+class KeysSshAddArgs(schema.Strict):
+    text: str
+
+
+class KeysSshAddResults(schema.Strict):
     endpoint: Literal["/keys/ssh/add"] = schema.Field(alias="endpoint")
     fingerprint: str
 
 
+class ProjectResults(schema.Strict):
+    endpoint: Literal["/project"] = schema.Field(alias="endpoint")
+    project: sql.Project
+
+
+class ProjectReleasesResults(schema.Strict):
+    endpoint: Literal["/project/releases"] = schema.Field(alias="endpoint")
+    releases: Sequence[sql.Release]
+
+
+class ProjectsResults(schema.Strict):
+    endpoint: Literal["/projects"] = schema.Field(alias="endpoint")
+    projects: Sequence[sql.Project]
+
+
 class ProjectVersion(schema.Strict):
     project: str
     version: str
@@ -143,7 +200,15 @@ Results = Annotated[
     | CommitteesKeysResults
     | CommitteesListResults
     | CommitteesProjectsResults
-    | Fingerprint,
+    | DraftDeleteResults
+    | JwtResults
+    | KeyResults
+    | KeysResults
+    | KeysSshAddResults
+    | ListResults
+    | ProjectResults
+    | ProjectReleasesResults
+    | ProjectsResults,
     schema.Field(discriminator="endpoint"),
 ]
 
@@ -167,4 +232,12 @@ validate_committees = validator(CommitteesResults)
 validate_committees_keys = validator(CommitteesKeysResults)
 validate_committees_list = validator(CommitteesListResults)
 validate_committees_projects = validator(CommitteesProjectsResults)
-validate_fingerprint = validator(Fingerprint)
+validate_draft_delete = validator(DraftDeleteResults)
+validate_jwt = validator(JwtResults)
+validate_key = validator(KeyResults)
+validate_keys = validator(KeysResults)
+validate_keys_ssh_add = validator(KeysSshAddResults)
+validate_list = validator(ListResults)
+validate_project = validator(ProjectResults)
+validate_project_releases = validator(ProjectReleasesResults)
+validate_projects = validator(ProjectsResults)
diff --git a/tests/test_all.py b/tests/test_all.py
index e418886..cab46f6 100755
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -242,14 +242,6 @@ def test_timestamp_format_none_and_bad() -> None:
     assert client.timestamp_format("bad") == "bad"
 
 
[email protected]
-async def test_web_fetch_failure() -> None:
-    with aioresponses.aioresponses() as mock, pytest.raises(SystemExit):
-        mock.post("https://error.invalid";, status=500, body="error")
-        await client.web_fetch("https://error.invalid";, "uid", "pat", 
verify_ssl=False)
-    assert (len(mock.requests)) == 1
-
-
 def transcript_capture(
     transcript_path: pathlib.Path,
     script_runner: pytest_console_scripts.ScriptRunner,
diff --git a/uv.lock b/uv.lock
index 28e0beb..7fdb5c5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
 requires-python = ">=3.13"
 
 [options]
-exclude-newer = "2025-07-15T15:12:00Z"
+exclude-newer = "2025-07-15T15:39:00Z"
 
 [[package]]
 name = "aiohappyeyeballs"
@@ -83,7 +83,7 @@ wheels = [
 
 [[package]]
 name = "apache-trusted-releases"
-version = "0.20250715.1512"
+version = "0.20250715.1539"
 source = { editable = "." }
 dependencies = [
     { name = "aiohttp" },


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

Reply via email to