You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/04/15 10:21:53 UTC

(tooling-trusted-releases) 01/01: #1053 - Move tasks/list endpoint to /admin/tasks/list, shares permissions with UI view of tasks.

This is an automated email from the ASF dual-hosted git repository.

arm pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit dfd1d72288abb6336a7ac750e7c90831ddf0feb8
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Wed Apr 15 11:21:32 2026 +0100

    #1053 - Move tasks/list endpoint to /admin/tasks/list, shares permissions with UI view of tasks.
---
 atr/admin/__init__.py    | 38 ++++++++++++++++++++++++++++++
 atr/api/__init__.py      | 61 +++++++-----------------------------------------
 atr/models/api.py        |  2 +-
 atr/models/validation.py | 21 ++++++++++++++++-
 4 files changed, 68 insertions(+), 54 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 9064e30c..5b33e6b7 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -36,6 +36,7 @@ import pydantic
 import quart
 import sqlalchemy
 import sqlmodel
+import werkzeug.exceptions as exceptions
 
 import atr.blueprints.admin as admin
 import atr.config as config
@@ -48,9 +49,11 @@ import atr.jwtoken as jwtoken
 import atr.ldap as ldap
 import atr.log as log
 import atr.mapping as mapping
+import atr.models.api as api
 import atr.models.safe as safe
 import atr.models.sql as sql
 import atr.models.unsafe as unsafe
+import atr.models.validation as validation
 import atr.paths as paths
 import atr.principal as principal
 import atr.storage as storage
@@ -896,6 +899,41 @@ async def rotate_jwt_key_post(
     return await session.redirect(rotate_jwt_key_get)
 
 
+@admin.typed
+async def tasks_list(
+    _session: web.Committer,
+    _tasks_list: Literal["tasks/list"],
+    query_args: api.TasksListQuery,
+) -> tuple[dict[str, Any], int]:
+    """
+    URL: GET /tasks/list
+
+    List tasks.
+    """
+    try:
+        validation.pagination_args_validate(query_args)
+    except ValueError as e:
+        raise exceptions.BadRequest(str(e))
+    via = sql.validate_instrumented_attribute
+    async with db.session() as data:
+        statement = sqlmodel.select(sql.Task).limit(query_args.limit).offset(query_args.offset)
+        if query_args.status:
+            if query_args.status not in sql.TaskStatus:
+                raise exceptions.BadRequest(f"Invalid status: {query_args.status}")
+            statement = statement.where(sql.Task.status == query_args.status)
+        statement = statement.order_by(via(sql.Task.id).desc())
+        paged_tasks = (await data.execute(statement)).scalars().all()
+        count_statement = sqlalchemy.select(sqlalchemy.func.count(via(sql.Task.id)))
+        if query_args.status:
+            count_statement = count_statement.where(via(sql.Task.status) == query_args.status)
+        count = (await data.execute(count_statement)).scalar_one()
+    return api.TasksListResults(
+        endpoint="/admin/tasks/list",
+        data=paged_tasks,
+        count=count,
+    ).model_dump(mode="json"), 200
+
+
 @admin.typed
 async def task_times(
     _session: web.Committer,
diff --git a/atr/api/__init__.py b/atr/api/__init__.py
index c83b445f..afcfa182 100644
--- a/atr/api/__init__.py
+++ b/atr/api/__init__.py
@@ -39,6 +39,7 @@ import atr.models as models
 import atr.models.safe as safe
 import atr.models.sql as sql
 import atr.models.unsafe as unsafe
+import atr.models.validation as validation
 import atr.paths as paths
 import atr.principal as principal
 import atr.storage as storage
@@ -1218,7 +1219,10 @@ async def releases_list(
 
     The list of releases is paged and can be filtered by phase.
     """
-    _pagination_args_validate(query_args)
+    try:
+        validation.pagination_args_validate(query_args)
+    except ValueError as e:
+        raise exceptions.BadRequest(str(e))
     via = sql.validate_instrumented_attribute
     async with db.session() as data:
         statement = sqlmodel.select(sql.Release)
@@ -1394,7 +1398,10 @@ async def ssh_keys_list(
 
     List SSH keys by ASF UID.
     """
-    _pagination_args_validate(query_args)
+    try:
+        validation.pagination_args_validate(query_args)
+    except ValueError as e:
+        raise exceptions.BadRequest(str(e))
     via = sql.validate_instrumented_attribute
     async with db.session() as data:
         statement = (
@@ -1416,37 +1423,6 @@ async def ssh_keys_list(
     ).model_dump(mode="json"), 200
 
 
-@api.typed
-async def tasks_list(
-    _tasks_list: Literal["tasks/list"],
-    query_args: models.api.TasksListQuery,
-) -> DictResponse:
-    """
-    URL: GET /tasks/list
-
-    List tasks.
-    """
-    _pagination_args_validate(query_args)
-    via = sql.validate_instrumented_attribute
-    async with db.session() as data:
-        statement = sqlmodel.select(sql.Task).limit(query_args.limit).offset(query_args.offset)
-        if query_args.status:
-            if query_args.status not in sql.TaskStatus:
-                raise exceptions.BadRequest(f"Invalid status: {query_args.status}")
-            statement = statement.where(sql.Task.status == query_args.status)
-        statement = statement.order_by(via(sql.Task.id).desc())
-        paged_tasks = (await data.execute(statement)).scalars().all()
-        count_statement = sqlalchemy.select(sqlalchemy.func.count(via(sql.Task.id)))
-        if query_args.status:
-            count_statement = count_statement.where(via(sql.Task.status) == query_args.status)
-        count = (await data.execute(count_statement)).scalar_one()
-    return models.api.TasksListResults(
-        endpoint="/tasks/list",
-        data=paged_tasks,
-        count=count,
-    ).model_dump(mode="json"), 200
-
-
 @api.typed
 async def update_distribution_task_status(
     _distribute_task_status: Literal["distribute/task/status"],
@@ -1783,22 +1759,3 @@ async def _match_release(release_directory: safe.StatePath, data: models.api.Sig
             if rel_path_sha3_256 == data.signature_sha3_256:
                 return True
     return False
-
-
-def _pagination_args_validate(query_args: Any) -> None:
-    # Users could request any amount using limit=N with arbitrarily high N
-    # We therefore limit the maximum limit to 1000
-    if hasattr(query_args, "limit"):
-        limit = query_args.limit
-        if limit > 1000:
-            raise exceptions.BadRequest("Maximum limit of 1000 exceeded")
-        elif limit < 1:
-            raise exceptions.BadRequest("Minimum limit less than 1 is nonsense")
-    # Users could request any amount using offset=N with arbitrarily high N
-    # We therefore limit the maximum offset to 1000000
-    if hasattr(query_args, "offset"):
-        offset = query_args.offset
-        if offset > 1000000:
-            raise exceptions.BadRequest("Maximum offset of 1000000 exceeded")
-        elif offset < 0:
-            raise exceptions.BadRequest("Minimum offset less than 0 is nonsense")
diff --git a/atr/models/api.py b/atr/models/api.py
index a623b2ca..0752c958 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -581,7 +581,7 @@ class TasksListQuery:
 
 
 class TasksListResults(schema.Strict):
-    endpoint: Literal["/tasks/list"] = schema.alias("endpoint")
+    endpoint: Literal["/admin/tasks/list"] = schema.alias("endpoint")
     data: Sequence[sql.Task]
     count: int = schema.example(10)
 
diff --git a/atr/models/validation.py b/atr/models/validation.py
index 00ff352c..6c6c511f 100644
--- a/atr/models/validation.py
+++ b/atr/models/validation.py
@@ -16,7 +16,7 @@
 # under the License.
 
 import re
-from typing import Final
+from typing import Any, Final
 
 import hyperscan
 
@@ -67,6 +67,25 @@ def compile_ignore_pattern(pattern: str):
     return HyperscanPattern(db)
 
 
+def pagination_args_validate(query_args: Any) -> None:
+    # Users could request any amount using limit=N with arbitrarily high N
+    # We therefore limit the maximum limit to 1000
+    if hasattr(query_args, "limit"):
+        limit = query_args.limit
+        if limit > 1000:
+            raise ValueError("Maximum limit of 1000 exceeded")
+        elif limit < 1:
+            raise ValueError("Minimum limit less than 1 is nonsense")
+    # Users could request any amount using offset=N with arbitrarily high N
+    # We therefore limit the maximum offset to 1000000
+    if hasattr(query_args, "offset"):
+        offset = query_args.offset
+        if offset > 1000000:
+            raise ValueError("Maximum offset of 1000000 exceeded")
+        elif offset < 0:
+            raise ValueError("Minimum offset less than 0 is nonsense")
+
+
 def validate_ignore_pattern(pattern: str) -> None:
     """Raise an exception if the pattern is invalid."""
     if pattern == "!":


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org