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]