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 8403836  Add committee validation
8403836 is described below

commit 8403836911212fe712d825992ba139fd57e58ff1
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jun 25 16:45:25 2025 +0100

    Add committee validation
---
 atr/db/__init__.py |  3 ++
 atr/validate.py    | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+)

diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index cf5d44d..ad4c8e2 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -204,6 +204,7 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         committers: Opt[list[str]] = NOT_SET,
         release_managers: Opt[list[str]] = NOT_SET,
         name_in: Opt[list[str]] = NOT_SET,
+        _child_committees: bool = False,
         _projects: bool = False,
         _public_signing_keys: bool = False,
     ) -> Query[models.Committee]:
@@ -228,6 +229,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
             models_committee_name = 
models.validate_instrumented_attribute(models.Committee.name)
             query = query.where(models_committee_name.in_(name_in))
 
+        if _child_committees:
+            query = 
query.options(select_in_load(models.Committee.child_committees))
         if _projects:
             query = query.options(select_in_load(models.Committee.projects))
         if _public_signing_keys:
diff --git a/atr/validate.py b/atr/validate.py
index a134c41..1b1e492 100644
--- a/atr/validate.py
+++ b/atr/validate.py
@@ -41,6 +41,8 @@ class AnnotatedDivergence(NamedTuple):
 Divergences = Generator[Divergence]
 AnnotatedDivergences = Generator[AnnotatedDivergence]
 AsyncAnnotatedDivergences = AsyncGenerator[AnnotatedDivergence]
+CommitteeDivergences = Callable[[models.Committee], Divergences]
+CommitteeAnnotatedDivergences = Callable[[models.Committee], 
AnnotatedDivergences]
 ProjectDivergences = Callable[[models.Project], Divergences]
 ProjectAnnotatedDivergences = Callable[[models.Project], AnnotatedDivergences]
 ReleaseDivergences = Callable[[models.Release], Divergences]
@@ -49,6 +51,81 @@ ReleaseAnnotatedDivergences = Callable[[models.Release], 
AnnotatedDivergences]
 T = TypeVar("T")
 
 
+def committee(c: models.Committee) -> AnnotatedDivergences:
+    """Check that a committee is valid."""
+
+    yield from committee_child_committees(c)
+    yield from committee_full_name(c)
+
+
+def committee_components(
+    *components: str,
+) -> Callable[[CommitteeDivergences], CommitteeAnnotatedDivergences]:
+    """Wrap a Committee divergence generator to yield annotated divergences."""
+
+    def wrap(original: CommitteeDivergences) -> CommitteeAnnotatedDivergences:
+        def replacement(c: models.Committee) -> AnnotatedDivergences:
+            yield from divergences_with_annotations(
+                components,
+                original.__name__,
+                c.name,
+                original(c),
+            )
+
+        return replacement
+
+    return wrap
+
+
+@committee_components("Committee.full_name")
+def committee_full_name(c: models.Committee) -> Divergences:
+    """Validate the Committee.full_name value."""
+
+    full_name = c.full_name
+
+    def present(fn: str | None) -> bool:
+        return bool(fn)
+
+    yield from divergences_predicate(
+        present,
+        "value to be set",
+        full_name,
+    )
+
+    def trimmed(fn: str | None) -> bool:
+        return False if fn is None else (fn == fn.strip())
+
+    yield from divergences_predicate(
+        trimmed,
+        "value not to have surrounding whitespace",
+        full_name,
+    )
+
+    def not_prefixed(fn: str | None) -> bool:
+        return False if fn is None else (not fn.startswith("Apache "))
+
+    yield from divergences_predicate(
+        not_prefixed,
+        "value not to start with 'Apache '",
+        full_name,
+    )
+
+
+@committee_components("Committee.child_committees")
+def committee_child_committees(c: models.Committee) -> Divergences:
+    """Check that a committee has no child_committees."""
+
+    expected: list[object] = []
+    actual = c.child_committees
+    yield from divergences(expected, actual)
+
+
+def committees(cs: Iterable[models.Committee]) -> AnnotatedDivergences:
+    """Validate multiple committees."""
+    for c in cs:
+        yield from committee(c)
+
+
 def divergences[T](expected: T, actual: T) -> Divergences:
     """Compare two values and yield the divergence if they differ."""
     if expected != actual:
@@ -74,9 +151,13 @@ def divergences_with_annotations(
 
 async def everything(data: db.Session) -> AsyncAnnotatedDivergences:
     """Yield divergences for all projects and releases in the DB."""
+    committees_sorted = await 
data.committee(_child_committees=True).order_by(models.Committee.name).all()
     projects_sorted = await 
data.project(_distribution_channels=True).order_by(models.Project.name).all()
     releases_sorted = await data.release().order_by(models.Release.name).all()
 
+    for c in await asyncio.to_thread(committees, committees_sorted):
+        yield c
+
     for p in await asyncio.to_thread(projects, projects_sorted):
         yield p
 


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

Reply via email to