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]