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 7b02fb4  Make some API endpoint types more consistent
7b02fb4 is described below

commit 7b02fb46e774a498ca252114defc0a9aa6d5e427
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 15 16:13:16 2025 +0100

    Make some API endpoint types more consistent
---
 Makefile                  |  2 +-
 atr/blueprints/api/api.py | 95 ++++++++++++++++++++++++++++++-----------------
 atr/models/api.py         | 88 ++++++++++++++++++++++++++++++++-----------
 3 files changed, 129 insertions(+), 56 deletions(-)

diff --git a/Makefile b/Makefile
index da8a784..7da76fa 100644
--- a/Makefile
+++ b/Makefile
@@ -75,4 +75,4 @@ sync:
 
 update-deps:
        uv lock --upgrade
-       uv sync --group test
+       uv sync --all-groups
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 48b3a50..0ad1faf 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -55,13 +55,15 @@ import atr.util as util
 
 # We implicitly have /api/openapi.json
 
+DictResponse = tuple[dict[str, Any], int]
+
 
 @api.BLUEPRINT.route("/announce", methods=["POST"])
 @jwtoken.require
 @quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.Announce)
-@quart_schema.validate_response(sql.Task, 201)
-async def announce_post(data: models.api.Announce) -> tuple[Mapping, int]:
+@quart_schema.validate_request(models.api.AnnounceArgs)
+@quart_schema.validate_response(models.api.AnnounceResults, 201)
+async def announce_post(data: models.api.AnnounceArgs) -> DictResponse:
     asf_uid = _jwt_asf_uid()
 
     try:
@@ -79,24 +81,30 @@ async def announce_post(data: models.api.Announce) -> 
tuple[Mapping, int]:
     except announce.AnnounceError as e:
         raise exceptions.BadRequest(str(e))
 
-    return {"success": "Announcement sent"}, 200
+    return models.api.AnnounceResults(
+        endpoint="/announce",
+        success="Announcement sent",
+    ).model_dump(), 201
 
 
 @api.BLUEPRINT.route("/checks/list/<project>/<version>")
-@quart_schema.validate_response(list[sql.CheckResult], 200)
-async def checks_list_project_version(project: str, version: str) -> 
tuple[list[Mapping], int]:
+@quart_schema.validate_response(models.api.ChecksListResults, 200)
+async def checks_list_project_version(project: str, version: str) -> 
DictResponse:
     """List all check results for a given release."""
     _simple_check(project, version)
     # TODO: Merge with checks_list_project_version_revision
     async with db.session() as data:
         release_name = sql.release_name(project, version)
         check_results = await 
data.check_result(release_name=release_name).all()
-        return [cr.model_dump() for cr in check_results], 200
+        return models.api.ChecksListResults(
+            endpoint="/checks/list",
+            checks=check_results,
+        ).model_dump(), 200
 
 
 @api.BLUEPRINT.route("/checks/list/<project>/<version>/<revision>")
-@quart_schema.validate_response(list[sql.CheckResult], 200)
-async def checks_list_project_version_revision(project: str, version: str, 
revision: str) -> tuple[list[Mapping], int]:
+@quart_schema.validate_response(models.api.ChecksListResults, 200)
+async def checks_list_project_version_revision(project: str, version: str, 
revision: str) -> DictResponse:
     """List all check results for a specific revision of a release."""
     _simple_check(project, version, revision)
     async with db.session() as data:
@@ -114,17 +122,20 @@ async def checks_list_project_version_revision(project: 
str, version: str, revis
             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
+        return models.api.ChecksListResults(
+            endpoint="/checks/list",
+            checks=check_results,
+        ).model_dump(), 200
 
 
 @api.BLUEPRINT.route("/checks/ongoing/<project>/<version>")
 @api.BLUEPRINT.route("/checks/ongoing/<project>/<version>/<revision>")
-@quart_schema.validate_response(models.api.Count, 200)
+@quart_schema.validate_response(models.api.ChecksOngoingResults, 200)
 async def checks_ongoing_project_version(
     project: str,
     version: str,
     revision: str | None = None,
-) -> tuple[Mapping[str, Any], int]:
+) -> DictResponse:
     """Return a count of all unfinished check results for a given release."""
     _simple_check(project, version, revision)
     ongoing_tasks_count, _latest_revision = await 
interaction.tasks_ongoing_revision(project, version, revision)
@@ -141,46 +152,62 @@ async def checks_ongoing_project_version(
     #     Iterator[bytes],
     #     Iterator[str],
     # ]
-    return models.api.Count(kind="count", 
count=ongoing_tasks_count).model_dump(), 200
-
-
[email protected]("/committees")
-@quart_schema.validate_response(list[sql.Committee], 200)
-async def committees() -> tuple[list[Mapping], int]:
-    """List all committees in the database."""
-    async with db.session() as data:
-        committees = await data.committee().all()
-        return [committee.model_dump() for committee in committees], 200
+    return models.api.ChecksOngoingResults(
+        endpoint="/checks/ongoing",
+        ongoing=ongoing_tasks_count,
+    ).model_dump(), 200
 
 
+# TODO: Rename all paths to avoid clashes
 @api.BLUEPRINT.route("/committees/<name>")
-@quart_schema.validate_response(sql.Committee, 200)
-async def committees_name(name: str) -> tuple[Mapping, int]:
+@quart_schema.validate_response(models.api.CommitteesResults, 200)
+async def committees_name(name: str) -> DictResponse:
     """Get a specific committee by name."""
     _simple_check(name)
     async with db.session() as data:
         committee = await 
data.committee(name=name).demand(exceptions.NotFound())
-        return committee.model_dump(), 200
+        return models.api.CommitteesResults(
+            endpoint="/committees",
+            committee=committee,
+        ).model_dump(), 200
 
 
[email protected]("/committees/<name>/keys")
-@quart_schema.validate_response(list[sql.PublicSigningKey], 200)
-async def committees_name_keys(name: str) -> tuple[list[Mapping], int]:
[email protected]("/committees/keys/<name>")
+@quart_schema.validate_response(models.api.CommitteesKeysResults, 200)
+async def committees_keys(name: str) -> DictResponse:
     """List all public signing keys associated with a specific committee."""
     _simple_check(name)
     async with db.session() as data:
         committee = await data.committee(name=name, 
_public_signing_keys=True).demand(exceptions.NotFound())
-        return [key.model_dump() for key in committee.public_signing_keys], 200
+        return models.api.CommitteesKeysResults(
+            endpoint="/committees/keys",
+            keys=committee.public_signing_keys,
+        ).model_dump(), 200
+
+
[email protected]("/committees/list")
+@quart_schema.validate_response(models.api.CommitteesListResults, 200)
+async def committees_list() -> DictResponse:
+    """List all committees in the database."""
+    async with db.session() as data:
+        committees = await data.committee().all()
+        return models.api.CommitteesListResults(
+            endpoint="/committees/list",
+            committees=committees,
+        ).model_dump(), 200
 
 
[email protected]("/committees/<name>/projects")
-@quart_schema.validate_response(list[sql.Project], 200)
-async def committees_name_projects(name: str) -> tuple[list[Mapping], int]:
[email protected]("/committees/projects/<name>")
+@quart_schema.validate_response(models.api.CommitteesProjectsResults, 200)
+async def committees_projects(name: str) -> DictResponse:
     """List all projects for a specific committee."""
     _simple_check(name)
     async with db.session() as data:
         committee = await data.committee(name=name, 
_projects=True).demand(exceptions.NotFound())
-        return [project.model_dump() for project in committee.projects], 200
+        return models.api.CommitteesProjectsResults(
+            endpoint="/committees/projects",
+            projects=committee.projects,
+        ).model_dump(), 200
 
 
 @api.BLUEPRINT.route("/draft/delete", methods=["POST"])
@@ -293,7 +320,7 @@ async def keys_ssh_add(data: models.api.Text) -> 
tuple[Mapping, int]:
     asf_uid = _jwt_asf_uid()
     fingerprint = await keys.ssh_key_add(data.text, asf_uid)
     return models.api.Fingerprint(
-        kind="fingerprint",
+        endpoint="/keys/ssh/add",
         fingerprint=fingerprint,
     ).model_dump(), 201
 
diff --git a/atr/models/api.py b/atr/models/api.py
index 1924eaa..e67bcf5 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -16,11 +16,14 @@
 # under the License.
 
 import dataclasses
-from typing import Annotated, Any, Literal
+from collections.abc import Callable, Sequence
+from typing import Annotated, Any, Literal, TypeVar
 
 import pydantic
 
-from . import schema
+from . import schema, sql
+
+T = TypeVar("T")
 
 
 class ResultsTypeError(TypeError):
@@ -45,7 +48,7 @@ class Task(Pagination):
     status: str | None = None
 
 
-class Announce(schema.Strict):
+class AnnounceArgs(schema.Strict):
     project: str
     version: str
     revision: str
@@ -55,18 +58,48 @@ class Announce(schema.Strict):
     path_suffix: str
 
 
+class AnnounceResults(schema.Strict):
+    endpoint: Literal["/announce"] = schema.Field(alias="endpoint")
+    success: str
+
+
+class ChecksListResults(schema.Strict):
+    endpoint: Literal["/checks/list"] = schema.Field(alias="endpoint")
+    checks: Sequence[sql.CheckResult]
+
+
+class ChecksOngoingResults(schema.Strict):
+    endpoint: Literal["/checks/ongoing"] = schema.Field(alias="endpoint")
+    ongoing: int
+
+
+class CommitteesResults(schema.Strict):
+    endpoint: Literal["/committees"] = schema.Field(alias="endpoint")
+    committee: sql.Committee
+
+
+class CommitteesKeysResults(schema.Strict):
+    endpoint: Literal["/committees/keys"] = schema.Field(alias="endpoint")
+    keys: Sequence[sql.PublicSigningKey]
+
+
+class CommitteesListResults(schema.Strict):
+    endpoint: Literal["/committees/list"] = schema.Field(alias="endpoint")
+    committees: Sequence[sql.Committee]
+
+
+class CommitteesProjectsResults(schema.Strict):
+    endpoint: Literal["/committees/projects"] = schema.Field(alias="endpoint")
+    projects: Sequence[sql.Project]
+
+
 class AsfuidPat(schema.Strict):
     asfuid: str
     pat: str
 
 
-class Count(schema.Strict):
-    kind: Literal["count"] = schema.Field(alias="kind")
-    count: int
-
-
 class Fingerprint(schema.Strict):
-    kind: Literal["fingerprint"] = schema.Field(alias="kind")
+    endpoint: Literal["/keys/ssh/add"] = schema.Field(alias="endpoint")
     fingerprint: str
 
 
@@ -103,22 +136,35 @@ class VoteStart(schema.Strict):
 
 
 Results = Annotated[
-    Count | Fingerprint,
-    schema.Field(discriminator="kind"),
+    AnnounceResults
+    | ChecksListResults
+    | ChecksOngoingResults
+    | CommitteesResults
+    | CommitteesKeysResults
+    | CommitteesListResults
+    | CommitteesProjectsResults
+    | Fingerprint,
+    schema.Field(discriminator="endpoint"),
 ]
 
 ResultsAdapter = pydantic.TypeAdapter(Results)
 
 
-def validate_count(value: Any) -> Count:
-    count = ResultsAdapter.validate_python(value)
-    if not isinstance(count, Count):
-        raise ResultsTypeError(f"Invalid API response: {value}")
-    return count
+def validator[T](t: type[T]) -> Callable[[Any], T]:
+    def validate(value: Any) -> T:
+        obj = ResultsAdapter.validate_python(value)
+        if not isinstance(obj, t):
+            raise ResultsTypeError(f"Invalid API response: {value}")
+        return obj
+
+    return validate
 
 
-def validate_fingerprint(value: Any) -> Fingerprint:
-    fingerprint = ResultsAdapter.validate_python(value)
-    if not isinstance(fingerprint, Fingerprint):
-        raise ResultsTypeError(f"Invalid API response: {value}")
-    return fingerprint
+validate_announce = validator(AnnounceResults)
+validate_checks_list = validator(ChecksListResults)
+validate_checks_ongoing = validator(ChecksOngoingResults)
+validate_committees = validator(CommitteesResults)
+validate_committees_keys = validator(CommitteesKeysResults)
+validate_committees_list = validator(CommitteesListResults)
+validate_committees_projects = validator(CommitteesProjectsResults)
+validate_fingerprint = validator(Fingerprint)


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

Reply via email to