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 8ed9369  Add API endpoints to delete and list ignores
8ed9369 is described below

commit 8ed93691d03d67e32c8cdfc0fb1546c1b6729fa3
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jul 30 15:47:08 2025 +0100

    Add API endpoints to delete and list ignores
---
 atr/blueprints/api/api.py     | 96 ++++++++++++++++++++++++++++++-------------
 atr/models/api.py             | 61 +++++++++++++++++----------
 atr/models/sql.py             |  4 ++
 atr/storage/__init__.py       | 10 +++++
 atr/storage/readers/checks.py |  7 ++++
 atr/storage/writers/checks.py |  9 ++--
 6 files changed, 134 insertions(+), 53 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index ac252d1..2d91e51 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -64,35 +64,6 @@ import atr.util as util
 DictResponse = tuple[dict[str, Any], int]
 
 
[email protected]("/checks/ignore/add", methods=["POST"])
[email protected]
-@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.ChecksIgnoreAddArgs)
-@quart_schema.validate_response(models.api.ChecksIgnoreAddResults, 200)
-async def checks_ignore_add(data: models.api.ChecksIgnoreAddArgs) -> 
DictResponse:
-    """
-    Add a check ignore.
-    """
-    asf_uid = _jwt_asf_uid()
-    if not any(data.model_dump().values()):
-        raise exceptions.BadRequest("At least one field must be provided")
-    async with storage.write(asf_uid) as write:
-        wacm = write.as_committee_member(data.committee_name)
-        await wacm.checks.ignore_add(
-            data.release_glob,
-            data.revision_number,
-            data.checker_glob,
-            data.primary_rel_path_glob,
-            data.member_rel_path_glob,
-            data.status,
-            data.message_glob,
-        )
-    return models.api.ChecksIgnoreAddResults(
-        endpoint="/checks/ignore/add",
-        success=True,
-    ).model_dump(), 200
-
-
 @api.BLUEPRINT.route("/checks/list/<project>/<version>")
 @quart_schema.validate_response(models.api.ChecksListResults, 200)
 async def checks_list(project: str, version: str) -> DictResponse:
@@ -288,6 +259,73 @@ async def committees_list() -> DictResponse:
     ).model_dump(), 200
 
 
[email protected]("/ignore/add", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.IgnoreAddArgs)
+@quart_schema.validate_response(models.api.IgnoreAddResults, 200)
+async def ignore_add(data: models.api.IgnoreAddArgs) -> DictResponse:
+    """
+    Add a check ignore.
+    """
+    asf_uid = _jwt_asf_uid()
+    if not any(data.model_dump().values()):
+        raise exceptions.BadRequest("At least one field must be provided")
+    async with storage.write(asf_uid) as write:
+        wacm = write.as_committee_member(data.committee_name)
+        await wacm.checks.ignore_add(
+            data.release_glob,
+            data.revision_number,
+            data.checker_glob,
+            data.primary_rel_path_glob,
+            data.member_rel_path_glob,
+            data.status,
+            data.message_glob,
+        )
+    return models.api.IgnoreAddResults(
+        endpoint="/ignore/add",
+        success=True,
+    ).model_dump(), 200
+
+
[email protected]("/ignore/delete", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.IgnoreDeleteArgs)
+@quart_schema.validate_response(models.api.IgnoreDeleteResults, 200)
+async def ignore_delete(data: models.api.IgnoreDeleteArgs) -> DictResponse:
+    """
+    Delete a check ignore.
+    """
+    asf_uid = _jwt_asf_uid()
+    if not any(data.model_dump().values()):
+        raise exceptions.BadRequest("At least one field must be provided")
+    async with storage.write(asf_uid) as write:
+        wacm = write.as_committee_member(data.committee)
+        # TODO: This is more like discard
+        # Should potentially check for rowcount, and raise an error if it's 0
+        await wacm.checks.ignore_delete(data.id)
+    return models.api.IgnoreDeleteResults(
+        endpoint="/ignore/delete",
+        success=True,
+    ).model_dump(), 200
+
+
[email protected]("/ignore/list/<committee_name>")
+@quart_schema.validate_response(models.api.IgnoreListResults, 200)
+async def ignore_list(committee_name: str) -> DictResponse:
+    """
+    List ignores by committee name.
+    """
+    _simple_check(committee_name)
+    async with db.session() as data:
+        ignores = await 
data.check_result_ignore(committee_name=committee_name).all()
+    return models.api.IgnoreListResults(
+        endpoint="/ignore/list",
+        ignores=ignores,
+    ).model_dump(), 200
+
+
 # This is the only POST endpoint that does not require a JWT
 @api.BLUEPRINT.route("/jwt/create", methods=["POST"])
 @quart_schema.validate_request(models.api.JwtCreateArgs)
diff --git a/atr/models/api.py b/atr/models/api.py
index 009ae07..a0294cd 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -34,24 +34,6 @@ class ResultsTypeError(TypeError):
     pass
 
 
-class ChecksIgnoreAddArgs(schema.Strict):
-    committee_name: str = schema.Field(..., **example("example"))
-    release_glob: str | None = schema.Field(default=None, 
**example("example-0.0.*"))
-    revision_number: str | None = schema.Field(default=None, 
**example("00001"))
-    checker_glob: str | None = schema.Field(default=None, 
**example("atr.tasks.checks.license.files"))
-    primary_rel_path_glob: str | None = schema.Field(default=None, 
**example("apache-example-0.0.1-*.tar.gz"))
-    member_rel_path_glob: str | None = schema.Field(default=None, 
**example("apache-example-0.0.1/*.xml"))
-    status: sql.CheckResultStatusIgnore | None = schema.Field(
-        default=None, **example(sql.CheckResultStatusIgnore.FAILURE)
-    )
-    message_glob: str | None = schema.Field(default=None, **example("sha512 
matches for apache-example-0.0.1/*.xml"))
-
-
-class ChecksIgnoreAddResults(schema.Strict):
-    endpoint: Literal["/checks/ignore/add"] = schema.Field(alias="endpoint")
-    success: Literal[True] = schema.Field(..., **example(True))
-
-
 class ChecksListResults(schema.Strict):
     endpoint: Literal["/checks/list"] = schema.Field(alias="endpoint")
     checks: Sequence[sql.CheckResult]
@@ -89,6 +71,39 @@ class CommitteesListResults(schema.Strict):
     committees: Sequence[sql.Committee]
 
 
+class IgnoreAddArgs(schema.Strict):
+    committee_name: str = schema.Field(..., **example("example"))
+    release_glob: str | None = schema.Field(default=None, 
**example("example-0.0.*"))
+    revision_number: str | None = schema.Field(default=None, 
**example("00001"))
+    checker_glob: str | None = schema.Field(default=None, 
**example("atr.tasks.checks.license.files"))
+    primary_rel_path_glob: str | None = schema.Field(default=None, 
**example("apache-example-0.0.1-*.tar.gz"))
+    member_rel_path_glob: str | None = schema.Field(default=None, 
**example("apache-example-0.0.1/*.xml"))
+    status: sql.CheckResultStatusIgnore | None = schema.Field(
+        default=None, **example(sql.CheckResultStatusIgnore.FAILURE)
+    )
+    message_glob: str | None = schema.Field(default=None, **example("sha512 
matches for apache-example-0.0.1/*.xml"))
+
+
+class IgnoreAddResults(schema.Strict):
+    endpoint: Literal["/ignore/add"] = schema.Field(alias="endpoint")
+    success: Literal[True] = schema.Field(..., **example(True))
+
+
+class IgnoreDeleteArgs(schema.Strict):
+    committee: str = schema.Field(..., **example("example"))
+    id: int = schema.Field(..., **example(1))
+
+
+class IgnoreDeleteResults(schema.Strict):
+    endpoint: Literal["/ignore/delete"] = schema.Field(alias="endpoint")
+    success: Literal[True] = schema.Field(..., **example(True))
+
+
+class IgnoreListResults(schema.Strict):
+    endpoint: Literal["/ignore/list"] = schema.Field(alias="endpoint")
+    ignores: Sequence[sql.CheckResultIgnore]
+
+
 class JwtCreateArgs(schema.Strict):
     asfuid: str = schema.Field(..., **example("user"))
     pat: str = schema.Field(..., 
**example("8M5t4GCU63EdOy4NNXgXn7o-bc-muK8TRg5W-DeBaWY"))
@@ -399,13 +414,15 @@ class VoteTabulateResults(schema.Strict):
 # This is for *Results classes only
 # We do NOT put *Args classes here
 Results = Annotated[
-    ChecksIgnoreAddResults
-    | ChecksListResults
+    ChecksListResults
     | ChecksOngoingResults
     | CommitteeGetResults
     | CommitteeKeysResults
     | CommitteeProjectsResults
     | CommitteesListResults
+    | IgnoreAddResults
+    | IgnoreDeleteResults
+    | IgnoreListResults
     | JwtCreateResults
     | KeyAddResults
     | KeyDeleteResults
@@ -449,13 +466,15 @@ def validator[T](t: type[T]) -> Callable[[Any], T]:
     return validate
 
 
-validate_checks_ignore_add = validator(ChecksIgnoreAddResults)
 validate_checks_list = validator(ChecksListResults)
 validate_checks_ongoing = validator(ChecksOngoingResults)
 validate_committee_get = validator(CommitteeGetResults)
 validate_committee_keys = validator(CommitteeKeysResults)
 validate_committee_projects = validator(CommitteeProjectsResults)
 validate_committees_list = validator(CommitteesListResults)
+validate_ignore_add = validator(IgnoreAddResults)
+validate_ignore_delete = validator(IgnoreDeleteResults)
+validate_ignore_list = validator(IgnoreListResults)
 validate_jwt_create = validator(JwtCreateResults)
 validate_key_add = validator(KeyAddResults)
 validate_key_delete = validator(KeyDeleteResults)
diff --git a/atr/models/sql.py b/atr/models/sql.py
index d58121c..e77d213 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -737,6 +737,10 @@ class CheckResultIgnore(sqlmodel.SQLModel, table=True):
     )
     message_glob: str | None = sqlmodel.Field(**example("sha512 matches for 
apache-example-0.0.1/*.xml"))
 
+    def model_post_init(self, _context):
+        if isinstance(self.created, str):
+            self.created = 
datetime.datetime.fromisoformat(self.created.rstrip("Z"))
+
 
 # DistributionChannel: Project
 class DistributionChannel(sqlmodel.SQLModel, table=True):
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index 1f44d19..edb2176 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -483,6 +483,15 @@ class ContextManagers:
             member_of, participant_of = await 
self.__member_and_participant(data, asf_uid)
             yield Read(data, asf_uid, member_of, participant_of)
 
+    @contextlib.asynccontextmanager
+    async def read_and_write(self, asf_uid: str | None = None) -> 
AsyncGenerator[tuple[Read, Write]]:
+        async with db.session() as data:
+            # TODO: Replace data with a DatabaseWriter instance
+            member_of, participant_of = await 
self.__member_and_participant(data, asf_uid)
+            r = Read(data, asf_uid, member_of, participant_of)
+            w = Write(data, asf_uid, member_of, participant_of)
+            yield r, w
+
     @contextlib.asynccontextmanager
     async def write(self, asf_uid: str | None = None) -> AsyncGenerator[Write]:
         async with db.session() as data:
@@ -494,4 +503,5 @@ class ContextManagers:
 _MANAGERS: Final[ContextManagers] = ContextManagers()
 
 read = _MANAGERS.read
+read_and_write = _MANAGERS.read_and_write
 write = _MANAGERS.write
diff --git a/atr/storage/readers/checks.py b/atr/storage/readers/checks.py
index fbe8ed3..fb56817 100644
--- a/atr/storage/readers/checks.py
+++ b/atr/storage/readers/checks.py
@@ -87,6 +87,12 @@ class GeneralPublic:
             member_results_list[member_rel_path].sort(key=lambda r: r.checker)
         return types.CheckResults(primary_results_list, member_results_list, 
ignored_checks)
 
+    async def ignores(self, committee_name: str) -> 
list[sql.CheckResultIgnore]:
+        results = await self.__data.check_result_ignore(
+            committee_name=committee_name,
+        ).all()
+        return list(results)
+
     async def ignores_matcher(
         self,
         committee_name: str,
@@ -141,4 +147,5 @@ class GeneralPublic:
         pattern = re.escape(glob).replace(r"\*", ".*")
         # Should also handle ^ and $
         # And maybe .replace(r"\?", ".?")
+        # Could also use "!" for negation
         return re.match(pattern, value) is not None
diff --git a/atr/storage/writers/checks.py b/atr/storage/writers/checks.py
index 2fcb8fb..c6566a4 100644
--- a/atr/storage/writers/checks.py
+++ b/atr/storage/writers/checks.py
@@ -20,6 +20,8 @@ from __future__ import annotations
 
 import datetime
 
+import sqlmodel
+
 import atr.db as db
 import atr.log as log
 import atr.models.sql as sql
@@ -114,6 +116,7 @@ class CommitteeMember(CommitteeParticipant):
         self.__data.add(cri)
         await self.__data.commit()
 
-    # def ignore_delete(self, id: int):
-    #     self.__data.delete(sql.CheckResultIgnore, id=id)
-    #     self.__data.commit()
+    async def ignore_delete(self, id: int) -> None:
+        via = sql.validate_instrumented_attribute
+        await 
self.__data.execute(sqlmodel.delete(sql.CheckResultIgnore).where(via(sql.CheckResultIgnore.id)
 == id))
+        await self.__data.commit()


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

Reply via email to