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-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 6681862  Add an endpoint for getting checks for a specific revision
6681862 is described below

commit 66818624e697ce2db41cfc9904dcb1c68d0b8721
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 7 14:46:56 2025 +0100

    Add an endpoint for getting checks for a specific revision
---
 atr/blueprints/api/api.py | 26 ++++++++++++++++++--
 client/atr                | 62 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 89efb0e..72fb713 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -256,9 +256,9 @@ async def releases_project_version(project: str, version: 
str) -> tuple[Mapping,
         return release.model_dump(), 200
 
 
[email protected]("/releases/<project>/<version>/check-results")
[email protected]("/checks/<project>/<version>")
 @quart_schema.validate_response(list[models.CheckResult], 200)
-async def releases_project_version_check_results(project: str, version: str) 
-> tuple[list[Mapping], int]:
+async def checks_project_version(project: str, version: str) -> 
tuple[list[Mapping], int]:
     """List all check results for a given release."""
     async with db.session() as data:
         release_name = models.release_name(project, version)
@@ -276,6 +276,28 @@ async def releases_project_version_revisions(project: str, 
version: str) -> tupl
         return [rev.model_dump() for rev in revisions], 200
 
 
[email protected]("/checks/<project>/<version>/<revision>")
+@quart_schema.validate_response(list[models.CheckResult], 200)
+async def checks_project_version_revision(project: str, version: str, 
revision: str) -> tuple[list[Mapping], int]:
+    """List all check results for a specific revision of a release."""
+    async with db.session() as data:
+        project_result = await data.project(name=project).get()
+        if project_result is None:
+            raise exceptions.NotFound(f"Project '{project}' does not exist")
+
+        release_name = models.release_name(project, version)
+        release_result = await data.release(name=release_name).get()
+        if release_result is None:
+            raise exceptions.NotFound(f"Release '{project}-{version}' does not 
exist")
+
+        revision_result = await data.revision(release_name=release_name, 
number=revision).get()
+        if revision_result is None:
+            raise exceptions.NotFound(f"Revision '{revision}' does not exist 
for release '{project}-{version}'")
+
+        check_results = await data.check_result(release_name=release_name, 
revision_number=revision).all()
+        return [cr.model_dump() for cr in check_results], 200
+
+
 @api.BLUEPRINT.route("/secret")
 @jwtoken.require
 @quart_schema.security_scheme([{"BearerAuth": []}])
diff --git a/client/atr b/client/atr
index 8f80250..1c22188 100755
--- a/client/atr
+++ b/client/atr
@@ -73,6 +73,7 @@ if TYPE_CHECKING:
     from collections.abc import Generator
 
 APP: cyclopts.App = cyclopts.App()
+CHECKS: cyclopts.App = cyclopts.App(name="checks", help="Check result 
operations.")
 CONFIG: cyclopts.App = cyclopts.App(name="config", help="Configuration 
operations.")
 DEV: cyclopts.App = cyclopts.App(name="dev", help="Developer operations.")
 JWT: cyclopts.App = cyclopts.App(name="jwt", help="JWT operations.")
@@ -92,12 +93,31 @@ YAML_SCHEMA: strictyaml.Map = strictyaml.Map(
     }
 )
 
+APP.command(CHECKS)
 APP.command(CONFIG)
 APP.command(DEV)
 APP.command(JWT)
 APP.command(RELEASE)
 
 
[email protected](name="status", help="Get check status for a release revision.")
+def app_checks_status(project: str, version: str, revision: str) -> None:
+    with config_lock() as config:
+        jwt_value = config_get(config, ["tokens", "jwt"])
+
+    if jwt_value is None:
+        LOGGER.error("No JWT stored in configuration.")
+        sys.exit(1)
+
+    host = config.get("atr", {}).get("host", "release-test.apache.org")
+    url = f"https://{host}/api/checks/{project}/{version}/{revision}";
+
+    verify_ssl = not host.startswith("127.0.0.1")
+    results = asyncio.run(web_get(url, jwt_value, verify_ssl))
+
+    check_results_display(results)
+
+
 @CONFIG.command(name="file", help="Display the configuration file contents.")
 def app_config_file() -> None:
     path = config_path()
@@ -383,6 +403,27 @@ def config_write(data: dict[str, Any]) -> None:
     os.replace(tmp, path)
 
 
+def check_results_display(results: list[dict[str, Any]]) -> None:
+    if not results:
+        print("No check results found for this revision.")
+        return
+
+    by_status = {}
+    for result in results:
+        status = result["status"]
+        by_status.setdefault(status, []).append(result)
+
+    print(f"Total checks: {len(results)}")
+    for status, checks in by_status.items():
+        print(f"  {status}: {len(checks)}")
+
+    for status in ["FAILURE", "EXCEPTION", "WARNING"]:
+        if status in by_status:
+            print(f"\n{status}:")
+            for check in by_status[status]:
+                print(f"  - {check['checker']}: {check['message']}")
+
+
 def main() -> None:
     logging.basicConfig(level=logging.INFO, format="%(message)s")
     APP()
@@ -476,6 +517,8 @@ def timestamp_format(ts: int | str | None) -> str | None:
 
 
 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
     connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
     async with aiohttp.ClientSession(connector=connector) as session:
         payload = {"asfuid": asfuid, "pat": pat_token}
@@ -491,6 +534,25 @@ async def web_fetch(url: str, asfuid: str, pat_token: str, 
verify_ssl: bool = Tr
             raise RuntimeError(f"Unexpected response: {data}")
 
 
+async def web_get(url: str, jwt_token: str, verify_ssl: bool = True) -> Any:
+    connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
+    headers = {"Authorization": f"Bearer {jwt_token}"}
+    async with aiohttp.ClientSession(connector=connector, headers=headers) as 
session:
+        async with session.get(url) as resp:
+            if resp.status != 200:
+                text = await resp.text()
+                try:
+                    error_data = json.loads(text)
+                    if isinstance(error_data, dict) and "message" in 
error_data:
+                        LOGGER.error(error_data["message"])
+                    else:
+                        LOGGER.error(f"Request failed: {resp.status} {text}")
+                except json.JSONDecodeError:
+                    LOGGER.error(f"Request failed: {resp.status} {text}")
+                sys.exit(1)
+            return await resp.json()
+
+
 async def web_post(url: str, payload: dict[str, Any], jwt_token: str, 
verify_ssl: bool = True) -> Any:
     connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
     headers = {"Authorization": f"Bearer {jwt_token}"}


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

Reply via email to