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 f32602f  Move the check matching code to the storage interface
f32602f is described below

commit f32602f42dfddb7a196a3b50d22e477231bbf14c
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jul 30 14:35:03 2025 +0100

    Move the check matching code to the storage interface
---
 atr/db/interaction.py           |  63 +----------------
 atr/routes/report.py            |  36 ++--------
 atr/storage/__init__.py         |  38 +++++++++--
 atr/storage/readers/__init__.py |  20 ++++++
 atr/storage/readers/checks.py   | 147 ++++++++++++++++++++++++++++++++++++++++
 5 files changed, 207 insertions(+), 97 deletions(-)

diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 38f6c38..337b1dc 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -18,7 +18,7 @@
 import contextlib
 import pathlib
 import re
-from collections.abc import AsyncGenerator, Callable, Sequence
+from collections.abc import AsyncGenerator, Sequence
 
 import aiofiles.os
 import aioshutil
@@ -103,25 +103,6 @@ async def latest_revision(release: sql.Release) -> 
sql.Revision | None:
         return await data.revision(release_name=release.name, 
number=release.latest_revision_number).get()
 
 
-async def check_ignores_matcher(
-    committee_name: str,
-    data: db.Session | None = None,
-) -> Callable[[sql.CheckResult], bool]:
-    async with db.ensure_session(data) as data:
-        ignores = await data.check_result_ignore(
-            committee_name=committee_name,
-        ).all()
-
-    def match(cr: sql.CheckResult) -> bool:
-        for ignore in ignores:
-            if _check_ignore_match(cr, ignore):
-                # log.info(f"Ignoring check result {cr} due to ignore 
{ignore}")
-                return True
-        return False
-
-    return match
-
-
 async def path_info(release: sql.Release, paths: list[pathlib.Path]) -> 
PathInfo | None:
     info = PathInfo()
     latest_revision_number = release.latest_revision_number
@@ -315,48 +296,6 @@ async def user_committees_participant(asf_uid: str, 
caller_data: db.Session | No
         return await data.committee(has_participant=asf_uid).all()
 
 
-def _check_ignore_match(cr: sql.CheckResult, cri: sql.CheckResultIgnore) -> 
bool:
-    # Does not check that the committee name matches
-    if cr.status == sql.CheckResultStatus.SUCCESS:
-        # Successes are never ignored
-        return False
-    if cri.release_glob is not None:
-        if not _check_ignore_match_glob(cri.release_glob, cr.release_name):
-            return False
-    if cri.revision_number is not None:
-        if cri.revision_number != cr.revision_number:
-            return False
-    if cri.checker_glob is not None:
-        if not _check_ignore_match_glob(cri.checker_glob, cr.checker):
-            return False
-    return _check_ignore_match_2(cr, cri)
-
-
-def _check_ignore_match_2(cr: sql.CheckResult, cri: sql.CheckResultIgnore) -> 
bool:
-    if cri.primary_rel_path_glob is not None:
-        if not _check_ignore_match_glob(cri.primary_rel_path_glob, 
cr.primary_rel_path):
-            return False
-    if cri.member_rel_path_glob is not None:
-        if not _check_ignore_match_glob(cri.member_rel_path_glob, 
cr.member_rel_path):
-            return False
-    if cri.status is not None:
-        if cr.status != cri.status:
-            return False
-    if cri.message_glob is not None:
-        if not _check_ignore_match_glob(cri.message_glob, cr.message):
-            return False
-    return True
-
-
-def _check_ignore_match_glob(glob: str | None, value: str | None) -> bool:
-    if (glob is None) or (value is None):
-        return False
-    pattern = re.escape(glob).replace(r"\*", ".*")
-    # Should also handle ^ and $
-    # And maybe .replace(r"\?", ".?")
-    return re.match(pattern, value) is not None
-
-
 async def _delete_release_data_downloads(release: sql.Release) -> None:
     # Delete hard links from the downloads directory
     finished_dir = util.release_directory(release)
diff --git a/atr/routes/report.py b/atr/routes/report.py
index 4ba0939..fe16682 100644
--- a/atr/routes/report.py
+++ b/atr/routes/report.py
@@ -21,10 +21,9 @@ import pathlib
 import aiofiles.os
 import asfquart.base as base
 
-import atr.db as db
-import atr.db.interaction as interaction
 import atr.models.sql as sql
 import atr.routes as routes
+import atr.storage as storage
 import atr.template as template
 import atr.util as util
 
@@ -58,36 +57,11 @@ async def selected_path(session: routes.CommitterSession, 
project_name: str, ver
     file_size = await aiofiles.os.path.getsize(abs_path)
 
     # Get all check results for this file
-    async with db.session() as data:
-        query = data.check_result(
-            release_name=release.name,
-            revision_number=release.latest_revision_number,
-            primary_rel_path=str(rel_path),
-        ).order_by(
-            sql.validate_instrumented_attribute(sql.CheckResult.checker).asc(),
-            
sql.validate_instrumented_attribute(sql.CheckResult.created).desc(),
+    async with storage.read() as read:
+        ragp = read.as_general_public()
+        primary_results_list, member_results_list, _ignored_checks = await 
ragp.checks.by_release_path(
+            release, pathlib.Path(rel_path)
         )
-        all_results = await query.all()
-
-        # Filter out any results that are ignored
-        match_ignore = await 
interaction.check_ignores_matcher(release.committee.name, data)
-        all_results = [r for r in all_results if not match_ignore(r)]
-
-    # Filter to separate the primary and member results
-    primary_results_list = []
-    member_results_list: dict[str, list[sql.CheckResult]] = {}
-    for result in all_results:
-        if result.member_rel_path is None:
-            primary_results_list.append(result)
-        else:
-            member_results_list.setdefault(result.member_rel_path, 
[]).append(result)
-
-    # Order primary results by checker name
-    primary_results_list.sort(key=lambda r: r.checker)
-
-    # Order member results by relative path and then by checker name
-    for member_rel_path in sorted(member_results_list.keys()):
-        member_results_list[member_rel_path].sort(key=lambda r: r.checker)
 
     file_data = {
         "filename": pathlib.Path(rel_path).name,
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index b6f5e9f..66f9e93 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -30,6 +30,7 @@ import atr.committer as committer
 import atr.db as db
 import atr.log as log
 import atr.models.sql as sql
+import atr.storage.readers as readers
 import atr.storage.types as types
 import atr.storage.writers as writers
 import atr.user as user
@@ -64,16 +65,35 @@ class AccessError(RuntimeError): ...
 # Read
 
 
-class ReadAsCommitteeMember(AccessCredentialsRead): ...
+class ReadAsGeneralPublic(AccessCredentialsRead):
+    def __init__(self, read: Read, data: db.Session, asf_uid: str | None = 
None):
+        self.__read = read
+        self.__data = data
+        self.__asf_uid = asf_uid
+        self.__authenticated = True
+        self.checks = readers.checks.GeneralPublic(
+            self,
+            self.__read,
+            self.__data,
+            self.__asf_uid,
+        )
+
+    @property
+    def authenticated(self) -> bool:
+        return self.__authenticated
+
+    @property
+    def validate_at_runtime(self) -> bool:
+        return VALIDATE_AT_RUNTIME
 
 
-class ReadAsCommitteeParticipant(AccessCredentialsRead): ...
+class ReadAsFoundationCommitter(ReadAsGeneralPublic): ...
 
 
-class ReadAsFoundationCommitter(AccessCredentialsRead): ...
+class ReadAsCommitteeParticipant(ReadAsFoundationCommitter): ...
 
 
-class ReadAsGeneralPublic(AccessCredentialsRead): ...
+class ReadAsCommitteeMember(ReadAsFoundationCommitter): ...
 
 
 class Read:
@@ -83,6 +103,16 @@ class Read:
         self.__member_of = member_of
         self.__participant_of = participant_of
 
+    def as_general_public(self) -> ReadAsGeneralPublic:
+        return self.as_general_public_outcome().result_or_raise()
+
+    def as_general_public_outcome(self) -> types.Outcome[ReadAsGeneralPublic]:
+        try:
+            ragp = ReadAsGeneralPublic(self, self.__data, self.__asf_uid)
+        except Exception as e:
+            return types.OutcomeException(e)
+        return types.OutcomeResult(ragp)
+
 
 # Write
 
diff --git a/atr/storage/readers/__init__.py b/atr/storage/readers/__init__.py
new file mode 100644
index 0000000..a23781f
--- /dev/null
+++ b/atr/storage/readers/__init__.py
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import atr.storage.readers.checks as checks
+
+__all__ = ["checks"]
diff --git a/atr/storage/readers/checks.py b/atr/storage/readers/checks.py
new file mode 100644
index 0000000..2f517b5
--- /dev/null
+++ b/atr/storage/readers/checks.py
@@ -0,0 +1,147 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Removing this will cause circular imports
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING
+
+import atr.db as db
+import atr.models.sql as sql
+import atr.storage as storage
+
+if TYPE_CHECKING:
+    import pathlib
+    from collections.abc import Callable
+
+
+class GeneralPublic:
+    def __init__(
+        self,
+        credentials: storage.ReadAsGeneralPublic,
+        read: storage.Read,
+        data: db.Session,
+        asf_uid: str | None = None,
+    ):
+        self.__credentials = credentials
+        self.__read = read
+        self.__data = data
+        self.__asf_uid = asf_uid
+
+    async def by_release_path(
+        self, release: sql.Release, rel_path: pathlib.Path
+    ) -> tuple[list[sql.CheckResult], dict[str, list[sql.CheckResult]], 
list[sql.CheckResult]]:
+        if release.committee is None:
+            raise ValueError("Release has no committee")
+        if release.latest_revision_number is None:
+            raise ValueError("Release has no revision")
+
+        query = self.__data.check_result(
+            release_name=release.name,
+            revision_number=release.latest_revision_number,
+            primary_rel_path=str(rel_path),
+        ).order_by(
+            sql.validate_instrumented_attribute(sql.CheckResult.checker).asc(),
+            
sql.validate_instrumented_attribute(sql.CheckResult.created).desc(),
+        )
+        all_check_results = await query.all()
+
+        # Filter out any results that are ignored
+        unignored_checks = []
+        ignored_checks = []
+        match_ignore = await 
self.__check_ignores_matcher(release.committee.name, self.__data)
+        for cr in all_check_results:
+            if not match_ignore(cr):
+                unignored_checks.append(cr)
+            else:
+                ignored_checks.append(cr)
+
+        # Filter to separate the primary and member results
+        primary_results_list = []
+        member_results_list: dict[str, list[sql.CheckResult]] = {}
+        for result in unignored_checks:
+            if result.member_rel_path is None:
+                primary_results_list.append(result)
+            else:
+                member_results_list.setdefault(result.member_rel_path, 
[]).append(result)
+
+        # Order primary results by checker name
+        primary_results_list.sort(key=lambda r: r.checker)
+
+        # Order member results by relative path and then by checker name
+        for member_rel_path in sorted(member_results_list.keys()):
+            member_results_list[member_rel_path].sort(key=lambda r: r.checker)
+        return primary_results_list, member_results_list, ignored_checks
+
+    def __check_ignore_match(self, cr: sql.CheckResult, cri: 
sql.CheckResultIgnore) -> bool:
+        # Does not check that the committee name matches
+        if cr.status == sql.CheckResultStatus.SUCCESS:
+            # Successes are never ignored
+            return False
+        if cri.release_glob is not None:
+            if not self.__check_ignore_match_glob(cri.release_glob, 
cr.release_name):
+                return False
+        if cri.revision_number is not None:
+            if cri.revision_number != cr.revision_number:
+                return False
+        if cri.checker_glob is not None:
+            if not self.__check_ignore_match_glob(cri.checker_glob, 
cr.checker):
+                return False
+        return self.__check_ignore_match_2(cr, cri)
+
+    def __check_ignore_match_2(self, cr: sql.CheckResult, cri: 
sql.CheckResultIgnore) -> bool:
+        if cri.primary_rel_path_glob is not None:
+            if not self.__check_ignore_match_glob(cri.primary_rel_path_glob, 
cr.primary_rel_path):
+                return False
+        if cri.member_rel_path_glob is not None:
+            if not self.__check_ignore_match_glob(cri.member_rel_path_glob, 
cr.member_rel_path):
+                return False
+        if cri.status is not None:
+            if cr.status != cri.status:
+                return False
+        if cri.message_glob is not None:
+            if not self.__check_ignore_match_glob(cri.message_glob, 
cr.message):
+                return False
+        return True
+
+    def __check_ignore_match_glob(self, glob: str | None, value: str | None) 
-> bool:
+        if (glob is None) or (value is None):
+            return False
+        pattern = re.escape(glob).replace(r"\*", ".*")
+        # Should also handle ^ and $
+        # And maybe .replace(r"\?", ".?")
+        return re.match(pattern, value) is not None
+
+    async def __check_ignores_matcher(
+        self,
+        committee_name: str,
+        data: db.Session | None = None,
+    ) -> Callable[[sql.CheckResult], bool]:
+        async with db.ensure_session(data) as data:
+            ignores = await data.check_result_ignore(
+                committee_name=committee_name,
+            ).all()
+
+        def match(cr: sql.CheckResult) -> bool:
+            for ignore in ignores:
+                if self.__check_ignore_match(cr, ignore):
+                    # log.info(f"Ignoring check result {cr} due to ignore 
{ignore}")
+                    return True
+            return False
+
+        return match


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

Reply via email to