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 f2f9e75  Add project validation
f2f9e75 is described below

commit f2f9e75978ef3c3a96b9fd0810f70f74a7806861
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jun 25 16:30:36 2025 +0100

    Add project validation
---
 atr/blueprints/admin/admin.py |   6 +-
 atr/validate.py               | 139 +++++++++++++++++++++++++++++++++++++++++-
 scripts/integrity_check.py    |  11 ++--
 3 files changed, 146 insertions(+), 10 deletions(-)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index d2b04ef..3322f3a 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -638,13 +638,11 @@ async def admin_validate() -> str:
     """Run validators and display any divergences."""
 
     async with db.session() as data:
-        releases = await data.release().order_by(models.Release.name).all()
-
-    results = list(validate.releases(releases))
+        divergences = [d async for d in validate.everything(data)]
 
     return await template.render(
         "validation.html",
-        divergences=results,
+        divergences=divergences,
     )
 
 
diff --git a/atr/validate.py b/atr/validate.py
index 02b708d..a134c41 100644
--- a/atr/validate.py
+++ b/atr/validate.py
@@ -15,11 +15,13 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import asyncio
 import datetime
 import pathlib
-from collections.abc import Callable, Generator, Iterable, Sequence
+from collections.abc import AsyncGenerator, Callable, Generator, Iterable, 
Sequence
 from typing import NamedTuple, TypeVar
 
+import atr.db as db
 import atr.db.models as models
 import atr.util as util
 
@@ -38,6 +40,9 @@ class AnnotatedDivergence(NamedTuple):
 
 Divergences = Generator[Divergence]
 AnnotatedDivergences = Generator[AnnotatedDivergence]
+AsyncAnnotatedDivergences = AsyncGenerator[AnnotatedDivergence]
+ProjectDivergences = Callable[[models.Project], Divergences]
+ProjectAnnotatedDivergences = Callable[[models.Project], AnnotatedDivergences]
 ReleaseDivergences = Callable[[models.Release], Divergences]
 ReleaseAnnotatedDivergences = Callable[[models.Release], AnnotatedDivergences]
 
@@ -67,6 +72,138 @@ def divergences_with_annotations(
         yield AnnotatedDivergence(list(components), validator, source, d)
 
 
+async def everything(data: db.Session) -> AsyncAnnotatedDivergences:
+    """Yield divergences for all projects and releases in the DB."""
+    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 p in await asyncio.to_thread(projects, projects_sorted):
+        yield p
+
+    for r in await asyncio.to_thread(releases, releases_sorted):
+        yield r
+
+
+def project(p: models.Project) -> AnnotatedDivergences:
+    """Check that a project is valid."""
+
+    yield from project_category(p)
+    yield from project_committee(p)
+    yield from project_created(p)
+    yield from project_distribution_channels(p)
+    yield from project_full_name(p)
+    yield from project_programming_languages(p)
+    yield from project_release_policy(p)
+
+
+def project_components(
+    *components: str,
+) -> Callable[[ProjectDivergences], ProjectAnnotatedDivergences]:
+    """Wrap a Project divergence generator to yield annotated divergences."""
+
+    def wrap(original: ProjectDivergences) -> ProjectAnnotatedDivergences:
+        def replacement(p: models.Project) -> AnnotatedDivergences:
+            yield from divergences_with_annotations(
+                components,
+                original.__name__,
+                p.name,
+                original(p),
+            )
+
+        return replacement
+
+    return wrap
+
+
+@project_components("Project.category")
+def project_category(p: models.Project) -> Divergences:
+    """Check that the category string uses 'label, label' syntax without 
colons."""
+
+    def okay(cat: str | None) -> bool:
+        if not cat:
+            return True
+        tokens = [t.strip() for t in cat.split(",")]
+        if any((not t) or (":" in t) for t in tokens):
+            return False
+        return True
+
+    expected = "comma separated labels without colon"
+    yield from divergences_predicate(okay, expected, p.category)
+
+
+@project_components("Project.committee_name")
+def project_committee(p: models.Project) -> Divergences:
+    """Check that the project is linked to a committee."""
+
+    def okay(cn: str | None) -> bool:
+        return cn is not None
+
+    expected = "committee_name to be set"
+    yield from divergences_predicate(okay, expected, p.committee_name)
+
+
+@project_components("Project.created")
+def project_created(p: models.Project) -> Divergences:
+    """Check that the project created timestamp is in the past."""
+    now = datetime.datetime.now(datetime.UTC)
+
+    def predicate(dt: datetime.datetime) -> bool:
+        return dt < now
+
+    expected = "value to be in the past"
+    yield from divergences_predicate(predicate, expected, p.created)
+
+
+@project_components("Project.distribution_channels")
+def project_distribution_channels(p: models.Project) -> Divergences:
+    """Check that distribution_channels is empty."""
+    expected: list[object] = []
+    actual = p.distribution_channels
+    yield from divergences(expected, actual)
+
+
+@project_components("Project.full_name")
+def project_full_name(p: models.Project) -> Divergences:
+    """Check that the project full_name is present and starts with 'Apache 
'."""
+
+    def okay(fn: str | None) -> bool:
+        return (fn is not None) and fn.startswith("Apache ")
+
+    expected = "full_name to be set and start with 'Apache '"
+    yield from divergences_predicate(okay, expected, p.full_name)
+
+
+@project_components("Project.programming_languages")
+def project_programming_languages(p: models.Project) -> Divergences:
+    """Check that programming_languages uses 'label, label' syntax without 
colons."""
+
+    def okay(pl: str | None) -> bool:
+        if not pl:
+            return True
+        tokens = [t.strip() for t in pl.split(",")]
+        if any((not t) or (":" in t) for t in tokens):
+            return False
+        return True
+
+    expected = "comma separated labels without colon"
+    yield from divergences_predicate(okay, expected, p.programming_languages)
+
+
+@project_components("Project.release_policy")
+def project_release_policy(p: models.Project) -> Divergences:
+    """Ensure that release_policy is None."""
+
+    expected = None
+    actual = p.release_policy_id
+    yield from divergences(expected, actual)
+
+
+def projects(ps: Iterable[models.Project]) -> AnnotatedDivergences:
+    """Validate multiple projects."""
+    for p in ps:
+        yield from project(p)
+
+
 def release(r: models.Release) -> AnnotatedDivergences:
     """Check that a release is valid."""
     yield from release_created(r)
diff --git a/scripts/integrity_check.py b/scripts/integrity_check.py
index 2090dd2..436121e 100644
--- a/scripts/integrity_check.py
+++ b/scripts/integrity_check.py
@@ -14,12 +14,13 @@ import atr.validate as validate
 async def amain() -> None:
     await db.init_database_for_worker()
     async with db.session() as data:
-        releases = await data.release().all()
-        divergences = 0
-        for divergence in validate.releases(releases):
+        divergences = [d async for d in validate.everything(data)]
+        for divergence in divergences:
             print(divergence)
-            divergences += 1
-        print(len(releases), "releases,", divergences, "errors")
+        print(len(divergences), "errors")
+
+    if divergences:
+        sys.exit(1)
 
 
 def main() -> None:


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

Reply via email to