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 51197b1  Use consistent types for API endpoints from revisions to 
uploads
51197b1 is described below

commit 51197b13fc96b34a6b5816c7d5588d29334b2fb7
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 15 18:54:50 2025 +0100

    Use consistent types for API endpoints from revisions to uploads
---
 atr/blueprints/api/api.py | 65 +++++++++++++++++----------------
 atr/models/api.py         | 92 +++++++++++++++++++++++++++++------------------
 atr/models/sql.py         | 23 ++++++++++++
 3 files changed, 115 insertions(+), 65 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index c4a1f9c..88a8ddb 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -20,7 +20,6 @@ import base64
 import datetime
 import hashlib
 import pathlib
-from collections.abc import Mapping
 from typing import Any
 
 import aiofiles.os
@@ -552,8 +551,8 @@ async def releases_project_version_revisions(project: str, 
version: str) -> Dict
 
 
 @api.BLUEPRINT.route("/revisions/<project>/<version>")
-@quart_schema.validate_response(dict[str, list[sql.Revision]], 200)
-async def revisions_project_version(project: str, version: str) -> 
tuple[dict[str, list[sql.Revision]], int]:
+@quart_schema.validate_response(models.api.RevisionsResults, 200)
+async def revisions_project_version(project: str, version: str) -> 
DictResponse:
     _simple_check(project, version)
     async with db.session() as data:
         release_name = sql.release_name(project, version)
@@ -562,21 +561,15 @@ async def revisions_project_version(project: str, 
version: str) -> tuple[dict[st
     if not isinstance(revisions, list):
         revisions = list(revisions)
     revisions.sort(key=lambda rev: rev.number)
-    return {"revisions": revisions}, 200
-
-
-# @api.BLUEPRINT.route("/secret")
-# @jwtoken.require
-# @quart_schema.security_scheme([{"BearerAuth": []}])
-# @quart_schema.validate_response(dict[str, str], 200)
-# async def secret() -> tuple[Mapping, int]:
-#     """Return a secret."""
-#     return {"secret": "*******"}, 200
+    return models.api.RevisionsResults(
+        endpoint="/revisions",
+        revisions=revisions,
+    ).model_dump(), 200
 
 
 @api.BLUEPRINT.route("/tasks")
-@quart_schema.validate_querystring(models.api.Task)
-async def tasks(query_args: models.api.Task) -> quart.Response:
+@quart_schema.validate_querystring(models.api.TasksQuery)
+async def tasks(query_args: models.api.TasksQuery) -> DictResponse:
     _pagination_args_validate(query_args)
     via = sql.validate_instrumented_attribute
     async with db.session() as data:
@@ -591,16 +584,19 @@ async def tasks(query_args: models.api.Task) -> 
quart.Response:
         if query_args.status:
             count_statement = count_statement.where(via(sql.Task.status) == 
query_args.status)
         count = (await data.execute(count_statement)).scalar_one()
-        result = {"data": [paged_task.model_dump(exclude={"result"}) for 
paged_task in paged_tasks], "count": count}
-        return quart.jsonify(result)
+        return models.api.TasksResults(
+            endpoint="/tasks",
+            data=paged_tasks,
+            count=count,
+        ).model_dump(), 200
 
 
 @api.BLUEPRINT.route("/vote/resolve", methods=["POST"])
 @jwtoken.require
 @quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.ProjectVersionResolution)
-@quart_schema.validate_response(dict[str, str], 200)
-async def vote_resolve(data: models.api.ProjectVersionResolution) -> 
tuple[Mapping, int]:
+@quart_schema.validate_request(models.api.VoteResolveArgs)
+@quart_schema.validate_response(models.api.VoteResolveResults, 200)
+async def vote_resolve(data: models.api.VoteResolveArgs) -> DictResponse:
     asf_uid = _jwt_asf_uid()
 
     async with db.session() as db_data:
@@ -624,15 +620,18 @@ async def vote_resolve(data: 
models.api.ProjectVersionResolution) -> tuple[Mappi
                 release.phase = sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
                 success_message = "Vote marked as failed"
         await db_data.commit()
-    return {"success": success_message}, 200
+    return models.api.VoteResolveResults(
+        endpoint="/vote/resolve",
+        success=success_message,
+    ).model_dump(), 200
 
 
 @api.BLUEPRINT.route("/vote/start", methods=["POST"])
 @jwtoken.require
 @quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.VoteStart)
-@quart_schema.validate_response(sql.Task, 201)
-async def vote_start(data: models.api.VoteStart) -> tuple[Mapping, int]:
+@quart_schema.validate_request(models.api.VoteStartArgs)
+@quart_schema.validate_response(models.api.VoteStartResults, 201)
+async def vote_start(data: models.api.VoteStartArgs) -> DictResponse:
     asf_uid = _jwt_asf_uid()
 
     permitted_recipients = util.permitted_recipients(asf_uid)
@@ -672,15 +671,18 @@ async def vote_start(data: models.api.VoteStart) -> 
tuple[Mapping, int]:
         )
         db_data.add(task)
         await db_data.commit()
-        return task.model_dump(exclude={"result"}), 201
+        return models.api.VoteStartResults(
+            endpoint="/vote/start",
+            task=task,
+        ).model_dump(), 201
 
 
 @api.BLUEPRINT.route("/upload", methods=["POST"])
 @jwtoken.require
 @quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.ProjectVersionRelpathContent)
-@quart_schema.validate_response(sql.Revision, 201)
-async def upload(data: models.api.ProjectVersionRelpathContent) -> 
tuple[Mapping, int]:
+@quart_schema.validate_request(models.api.UploadArgs)
+@quart_schema.validate_response(models.api.UploadResults, 201)
+async def upload(data: models.api.UploadArgs) -> DictResponse:
     asf_uid = _jwt_asf_uid()
 
     async with db.session() as db_data:
@@ -690,7 +692,10 @@ async def upload(data: 
models.api.ProjectVersionRelpathContent) -> tuple[Mapping
             raise exceptions.Forbidden("You do not have permission to upload 
to this project")
 
     revision = await _upload_process_file(data, asf_uid)
-    return revision.model_dump(), 201
+    return models.api.UploadResults(
+        endpoint="/upload",
+        revision=revision,
+    ).model_dump(), 201
 
 
 def _committee_member_or_admin(committee: sql.Committee, asf_uid: str) -> None:
@@ -730,7 +735,7 @@ def _simple_check(*args: str | None) -> None:
             raise exceptions.BadRequest("Argument cannot be the string 'None'")
 
 
-async def _upload_process_file(args: models.api.ProjectVersionRelpathContent, 
asf_uid: str) -> sql.Revision:
+async def _upload_process_file(args: models.api.UploadArgs, asf_uid: str) -> 
sql.Revision:
     file_bytes = base64.b64decode(args.content, validate=True)
     file_path = args.relpath.lstrip("/")
     description = f"Upload via API: {file_path}"
diff --git a/atr/models/api.py b/atr/models/api.py
index 70c3036..02e63b6 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -30,18 +30,6 @@ class ResultsTypeError(TypeError):
     pass
 
 
[email protected]
-class Pagination:
-    offset: int = 0
-    limit: int = 20
-
-
-# TODO: TaskPagination?
[email protected]
-class Task(Pagination):
-    status: str | None = None
-
-
 class AnnounceArgs(schema.Strict):
     project: str
     version: str
@@ -139,7 +127,7 @@ class KeysSshAddResults(schema.Strict):
     fingerprint: str
 
 
-class KeysSshListQuery(Pagination):
+class KeysSshListQuery:
     offset: int = 0
     limit: int = 20
 
@@ -165,24 +153,6 @@ class ProjectsResults(schema.Strict):
     projects: Sequence[sql.Project]
 
 
-class ProjectVersion(schema.Strict):
-    project: str
-    version: str
-
-
-class ProjectVersionRelpathContent(schema.Strict):
-    project: str
-    version: str
-    relpath: str
-    content: str
-
-
-class ProjectVersionResolution(schema.Strict):
-    project: str
-    version: str
-    resolution: Literal["passed", "failed"]
-
-
 @dataclasses.dataclass
 class ReleasesQuery:
     offset: int = 0
@@ -240,11 +210,36 @@ class ReleasesRevisionsResults(schema.Strict):
     revisions: Sequence[sql.Revision]
 
 
-class Text(schema.Strict):
-    text: str
+class RevisionsResults(schema.Strict):
+    endpoint: Literal["/revisions"] = schema.Field(alias="endpoint")
+    revisions: Sequence[sql.Revision]
 
 
-class VoteStart(schema.Strict):
[email protected]
+class TasksQuery:
+    limit: int = 20
+    offset: int = 0
+    status: str | None = None
+
+
+class TasksResults(schema.Strict):
+    endpoint: Literal["/tasks"] = schema.Field(alias="endpoint")
+    data: Sequence[sql.Task]
+    count: int
+
+
+class VoteResolveArgs(schema.Strict):
+    project: str
+    version: str
+    resolution: Literal["passed", "failed"]
+
+
+class VoteResolveResults(schema.Strict):
+    endpoint: Literal["/vote/resolve"] = schema.Field(alias="endpoint")
+    success: str
+
+
+class VoteStartArgs(schema.Strict):
     project: str
     version: str
     revision: str
@@ -254,6 +249,23 @@ class VoteStart(schema.Strict):
     body: str
 
 
+class VoteStartResults(schema.Strict):
+    endpoint: Literal["/vote/start"] = schema.Field(alias="endpoint")
+    task: sql.Task
+
+
+class UploadArgs(schema.Strict):
+    project: str
+    version: str
+    relpath: str
+    content: str
+
+
+class UploadResults(schema.Strict):
+    endpoint: Literal["/upload"] = schema.Field(alias="endpoint")
+    revision: sql.Revision
+
+
 # This is for *Results classes only
 # We do NOT put *Args classes here
 Results = Annotated[
@@ -279,7 +291,12 @@ Results = Annotated[
     | ReleasesDeleteResults
     | ReleasesProjectResults
     | ReleasesVersionResults
-    | ReleasesRevisionsResults,
+    | ReleasesRevisionsResults
+    | RevisionsResults
+    | TasksResults
+    | VoteResolveResults
+    | VoteStartResults
+    | UploadResults,
     schema.Field(discriminator="endpoint"),
 ]
 
@@ -319,3 +336,8 @@ validate_releases_delete = validator(ReleasesDeleteResults)
 validate_releases_project = validator(ReleasesProjectResults)
 validate_releases_version = validator(ReleasesVersionResults)
 validate_releases_revisions = validator(ReleasesRevisionsResults)
+validate_revisions = validator(RevisionsResults)
+validate_tasks = validator(TasksResults)
+validate_vote_resolve = validator(VoteResolveResults)
+validate_vote_start = validator(VoteStartResults)
+validate_upload = validator(UploadResults)
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 802e349..90b6584 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -244,6 +244,22 @@ class Task(sqlmodel.SQLModel, table=True):
     revision_number: str | None = sqlmodel.Field(default=None, index=True)
     primary_rel_path: str | None = sqlmodel.Field(default=None, index=True)
 
+    def model_post_init(self, _context):
+        if isinstance(self.task_type, str):
+            self.task_type = TaskType(self.task_type)
+
+        if isinstance(self.status, str):
+            self.status = TaskStatus(self.status)
+
+        if isinstance(self.added, str):
+            self.added = 
datetime.datetime.fromisoformat(self.added.rstrip("Z"))
+
+        if isinstance(self.started, str):
+            self.started = 
datetime.datetime.fromisoformat(self.started.rstrip("Z"))
+
+        if isinstance(self.completed, str):
+            self.completed = 
datetime.datetime.fromisoformat(self.completed.rstrip("Z"))
+
     # Create an index on status and added for efficient task claiming
     __table_args__ = (
         sqlalchemy.Index("ix_task_status_added", "status", "added"),
@@ -755,6 +771,13 @@ class Revision(sqlmodel.SQLModel, table=True):
 
     description: str | None = sqlmodel.Field(default=None)
 
+    def model_post_init(self, _context):
+        if isinstance(self.created, str):
+            self.created = 
datetime.datetime.fromisoformat(self.created.rstrip("Z"))
+
+        if isinstance(self.phase, str):
+            self.phase = ReleasePhase(self.phase)
+
     __table_args__ = (
         sqlmodel.UniqueConstraint("release_name", "seq", 
name="uq_revision_release_seq"),
         sqlmodel.UniqueConstraint("release_name", "number", 
name="uq_revision_release_number"),


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

Reply via email to