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-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new be1e8b7  Simplify the processing of release policy forms
be1e8b7 is described below

commit be1e8b7adea0d1ba0f5afe964d0212f84ef306a0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Dec 5 11:13:54 2025 +0000

    Simplify the processing of release policy forms
---
 atr/models/__init__.py        |   4 +-
 atr/models/policy.py          |  99 ----------------------------
 atr/post/projects.py          |  86 +------------------------
 atr/storage/writers/policy.py | 145 +++++++++++++++++++++++-------------------
 4 files changed, 84 insertions(+), 250 deletions(-)

diff --git a/atr/models/__init__.py b/atr/models/__init__.py
index 4cb70b3..a9cb122 100644
--- a/atr/models/__init__.py
+++ b/atr/models/__init__.py
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from . import api, distribution, helpers, policy, results, schema, sql, 
tabulate
+from . import api, distribution, helpers, results, schema, sql, tabulate
 
 # If we use .__name__, pyright gives a warning
-__all__ = ["api", "distribution", "helpers", "policy", "results", "schema", 
"sql", "tabulate"]
+__all__ = ["api", "distribution", "helpers", "results", "schema", "sql", 
"tabulate"]
diff --git a/atr/models/policy.py b/atr/models/policy.py
deleted file mode 100644
index 6e5e793..0000000
--- a/atr/models/policy.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# 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.
-
-from typing import Any
-
-import pydantic
-
-from . import schema
-
-
-# TODO: Maybe it's easier to use quart_schema for all our forms
-# We can use source=DataSource.FORM
-# But do all form input types have a pydantic counterpart?
-class ReleasePolicyData(schema.Lax):
-    """Pydantic model for release policy form data."""
-
-    project_name: str
-
-    # Compose section
-    source_artifact_paths: list[str] = pydantic.Field(default_factory=list)
-    binary_artifact_paths: list[str] = pydantic.Field(default_factory=list)
-    github_repository_name: str = ""
-    github_compose_workflow_path: list[str] = 
pydantic.Field(default_factory=list)
-    strict_checking: bool = False
-
-    # Vote section
-    mailto_addresses: list[str] = pydantic.Field(default_factory=list)
-    manual_vote: bool = False
-    default_min_hours_value_at_render: int = 72
-    min_hours: int = 72
-    pause_for_rm: bool = False
-    release_checklist: str = ""
-    vote_comment_template: str = ""
-    default_start_vote_template_hash: str = ""
-    start_vote_template: str = ""
-    github_vote_workflow_path: list[str] = pydantic.Field(default_factory=list)
-
-    # Finish section
-    default_announce_release_template_hash: str = ""
-    announce_release_template: str = ""
-    github_finish_workflow_path: list[str] = 
pydantic.Field(default_factory=list)
-    preserve_download_files: bool = False
-
-    @pydantic.field_validator(
-        "source_artifact_paths",
-        "binary_artifact_paths",
-        "github_compose_workflow_path",
-        "github_vote_workflow_path",
-        "github_finish_workflow_path",
-        mode="before",
-    )
-    @classmethod
-    def parse_artifact_paths(cls, v: Any) -> list[str]:
-        if (v is None) or (v == ""):
-            return []
-        if isinstance(v, str):
-            return [path.strip() for path in v.split("\n") if path.strip()]
-        if isinstance(v, list):
-            return v
-        return []
-
-    @pydantic.field_validator("mailto_addresses", mode="before")
-    @classmethod
-    def parse_mailto_addresses(cls, v: Any) -> list[str]:
-        if (v is None) or (v == ""):
-            return []
-        if isinstance(v, str):
-            return [v.strip()] if v.strip() else []
-        if isinstance(v, list):
-            return v
-        return []
-
-    @pydantic.field_validator(
-        "github_repository_name",
-        "release_checklist",
-        "vote_comment_template",
-        "start_vote_template",
-        "announce_release_template",
-        mode="before",
-    )
-    @classmethod
-    def unwrap_values(cls, v: Any) -> Any:
-        if v is None:
-            return ""
-        return v
diff --git a/atr/post/projects.py b/atr/post/projects.py
index 3f86ec6..608b4b0 100644
--- a/atr/post/projects.py
+++ b/atr/post/projects.py
@@ -23,7 +23,6 @@ import quart
 import atr.blueprints.post as post
 import atr.db as db
 import atr.get as get
-import atr.models.policy as policy
 import atr.models.sql as sql
 import atr.shared as shared
 import atr.storage as storage
@@ -197,37 +196,10 @@ async def _process_compose_form(
 ) -> web.WerkzeugResponse:
     project_name = compose_form.project_name
 
-    async with db.session() as data:
-        project = await data.project(name=project_name, _committee=True, 
_release_policy=True).demand(
-            base.ASFQuartException(f"Project {project_name} not found", 
errorcode=404)
-        )
-
-    policy_data = policy.ReleasePolicyData(
-        project_name=project_name,
-        source_artifact_paths=[p.strip() for p in 
compose_form.source_artifact_paths.split("\n") if p.strip()],
-        binary_artifact_paths=[p.strip() for p in 
compose_form.binary_artifact_paths.split("\n") if p.strip()],
-        github_repository_name=compose_form.github_repository_name.strip() or 
"",
-        github_compose_workflow_path=[
-            p.strip() for p in 
compose_form.github_compose_workflow_path.split("\n") if p.strip()
-        ],
-        strict_checking=compose_form.strict_checking,
-        github_vote_workflow_path=project.policy_github_vote_workflow_path,
-        mailto_addresses=project.policy_mailto_addresses,
-        manual_vote=project.policy_manual_vote,
-        min_hours=project.policy_min_hours,
-        pause_for_rm=project.policy_pause_for_rm,
-        release_checklist=project.policy_release_checklist or "",
-        vote_comment_template=project.policy_vote_comment_template or "",
-        start_vote_template=project.policy_start_vote_template or "",
-        github_finish_workflow_path=project.policy_github_finish_workflow_path,
-        announce_release_template=project.policy_announce_release_template or 
"",
-        preserve_download_files=project.policy_preserve_download_files,
-    )
-
     async with storage.write(session) as write:
         wacm = await write.as_project_committee_member(project_name)
         try:
-            await wacm.policy.edit(project_name, policy_data)
+            await wacm.policy.edit_compose(compose_form)
         except storage.AccessError as e:
             return await session.redirect(
                 get.projects.view, name=project_name, error=f"Error editing 
compose policy: {e}"
@@ -256,37 +228,10 @@ async def _process_finish_form(
 ) -> web.WerkzeugResponse:
     project_name = finish_form.project_name
 
-    async with db.session() as data:
-        project = await data.project(name=project_name, _committee=True, 
_release_policy=True).demand(
-            base.ASFQuartException(f"Project {project_name} not found", 
errorcode=404)
-        )
-
-    policy_data = policy.ReleasePolicyData(
-        project_name=project_name,
-        source_artifact_paths=project.policy_source_artifact_paths,
-        binary_artifact_paths=project.policy_binary_artifact_paths,
-        github_repository_name=project.policy_github_repository_name or "",
-        
github_compose_workflow_path=project.policy_github_compose_workflow_path,
-        strict_checking=project.policy_strict_checking,
-        github_vote_workflow_path=project.policy_github_vote_workflow_path,
-        mailto_addresses=project.policy_mailto_addresses,
-        manual_vote=project.policy_manual_vote,
-        min_hours=project.policy_min_hours,
-        pause_for_rm=project.policy_pause_for_rm,
-        release_checklist=project.policy_release_checklist or "",
-        vote_comment_template=project.policy_vote_comment_template or "",
-        start_vote_template=project.policy_start_vote_template or "",
-        github_finish_workflow_path=[
-            p.strip() for p in 
finish_form.github_finish_workflow_path.split("\n") if p.strip()
-        ],
-        announce_release_template=finish_form.announce_release_template or "",
-        preserve_download_files=finish_form.preserve_download_files,
-    )
-
     async with storage.write(session) as write:
         wacm = await write.as_project_committee_member(project_name)
         try:
-            await wacm.policy.edit(project_name, policy_data)
+            await wacm.policy.edit_finish(finish_form)
         except storage.AccessError as e:
             return await session.redirect(
                 get.projects.view, name=project_name, error=f"Error editing 
finish policy: {e}"
@@ -344,35 +289,10 @@ async def _process_remove_language(
 async def _process_vote_form(session: web.Committer, vote_form: 
shared.projects.VotePolicyForm) -> web.WerkzeugResponse:
     project_name = vote_form.project_name
 
-    async with db.session() as data:
-        project = await data.project(name=project_name, _committee=True, 
_release_policy=True).demand(
-            base.ASFQuartException(f"Project {project_name} not found", 
errorcode=404)
-        )
-
-    policy_data = policy.ReleasePolicyData(
-        project_name=project_name,
-        source_artifact_paths=project.policy_source_artifact_paths,
-        binary_artifact_paths=project.policy_binary_artifact_paths,
-        github_repository_name=project.policy_github_repository_name or "",
-        
github_compose_workflow_path=project.policy_github_compose_workflow_path,
-        strict_checking=project.policy_strict_checking,
-        github_vote_workflow_path=[p.strip() for p in 
vote_form.github_vote_workflow_path.split("\n") if p.strip()],
-        mailto_addresses=[vote_form.mailto_addresses],
-        manual_vote=vote_form.manual_vote,
-        min_hours=vote_form.min_hours,
-        pause_for_rm=vote_form.pause_for_rm,
-        release_checklist=vote_form.release_checklist or "",
-        vote_comment_template=vote_form.vote_comment_template or "",
-        start_vote_template=vote_form.start_vote_template or "",
-        github_finish_workflow_path=project.policy_github_finish_workflow_path,
-        announce_release_template=project.policy_announce_release_template or 
"",
-        preserve_download_files=project.policy_preserve_download_files,
-    )
-
     async with storage.write(session) as write:
         wacm = await write.as_project_committee_member(project_name)
         try:
-            await wacm.policy.edit(project_name, policy_data)
+            await wacm.policy.edit_vote(vote_form)
         except storage.AccessError as e:
             return await session.redirect(get.projects.view, 
name=project_name, error=f"Error editing vote policy: {e}")
 
diff --git a/atr/storage/writers/policy.py b/atr/storage/writers/policy.py
index 89f7bf0..e047d6a 100644
--- a/atr/storage/writers/policy.py
+++ b/atr/storage/writers/policy.py
@@ -18,11 +18,16 @@
 # Removing this will cause circular imports
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 import atr.db as db
 import atr.models as models
 import atr.storage as storage
 import atr.util as util
 
+if TYPE_CHECKING:
+    import atr.shared as shared
+
 
 class GeneralPublic:
     def __init__(
@@ -86,104 +91,112 @@ class CommitteeMember(CommitteeParticipant):
         self.__asf_uid = asf_uid
         self.__committee_name = committee_name
 
-    async def edit(self, project_name: str, policy_data: 
models.policy.ReleasePolicyData) -> None:
-        project = await self.__data.project(
-            name=project_name, status=models.sql.ProjectStatus.ACTIVE, 
_release_policy=True
-        ).demand(storage.AccessError(f"Project {project_name} not found"))
+    async def edit_compose(self, form: shared.projects.ComposePolicyForm) -> 
None:
+        project_name = form.project_name
+        _, release_policy = await self.__get_or_create_policy(project_name)
 
-        release_policy = project.release_policy
-        if release_policy is None:
-            release_policy = models.sql.ReleasePolicy(project=project)
-            project.release_policy = release_policy
-            self.__data.add(release_policy)
+        release_policy.source_artifact_paths = 
_split_lines(form.source_artifact_paths)
+        release_policy.binary_artifact_paths = 
_split_lines(form.binary_artifact_paths)
+        release_policy.github_repository_name = 
form.github_repository_name.strip()
+        release_policy.github_compose_workflow_path = 
_split_lines(form.github_compose_workflow_path)
+        release_policy.strict_checking = form.strict_checking
+
+        await self.__commit_and_log(project_name)
+
+    async def edit_finish(self, form: shared.projects.FinishPolicyForm) -> 
None:
+        project_name = form.project_name
+        project, release_policy = await 
self.__get_or_create_policy(project_name)
+
+        release_policy.github_finish_workflow_path = 
_split_lines(form.github_finish_workflow_path)
+        self.__set_announce_release_template(form.announce_release_template or 
"", project, release_policy)
+        release_policy.preserve_download_files = form.preserve_download_files
 
-        # Compose section
-        release_policy.source_artifact_paths = 
policy_data.source_artifact_paths
-        release_policy.binary_artifact_paths = 
policy_data.binary_artifact_paths
-        release_policy.github_repository_name = 
policy_data.github_repository_name
-        # TODO: Change to paths, plural
-        release_policy.github_compose_workflow_path = 
policy_data.github_compose_workflow_path
-        release_policy.strict_checking = policy_data.strict_checking
+        await self.__commit_and_log(project_name)
+
+    async def edit_vote(self, form: shared.projects.VotePolicyForm) -> None:
+        project_name = form.project_name
+        project, release_policy = await 
self.__get_or_create_policy(project_name)
+
+        release_policy.manual_vote = form.manual_vote
 
-        # Vote section
-        release_policy.manual_vote = policy_data.manual_vote
         if not release_policy.manual_vote:
-            release_policy.github_vote_workflow_path = 
policy_data.github_vote_workflow_path
-            release_policy.mailto_addresses = policy_data.mailto_addresses
-            self.__set_default_min_hours(policy_data, project, release_policy)
-            release_policy.pause_for_rm = policy_data.pause_for_rm
-            release_policy.release_checklist = policy_data.release_checklist
-            release_policy.vote_comment_template = 
policy_data.vote_comment_template
-            self.__set_default_start_vote_template(policy_data, project, 
release_policy)
+            release_policy.github_vote_workflow_path = 
_split_lines(form.github_vote_workflow_path)
+            release_policy.mailto_addresses = [form.mailto_addresses]
+            self.__set_min_hours(form.min_hours, project, release_policy)
+            release_policy.pause_for_rm = form.pause_for_rm
+            release_policy.release_checklist = form.release_checklist or ""
+            release_policy.vote_comment_template = form.vote_comment_template 
or ""
+            self.__set_start_vote_template(form.start_vote_template or "", 
project, release_policy)
         elif project.committee and project.committee.is_podling:
-            # The caller ensures that project.committee is not None
             raise storage.AccessError("Manual voting is not allowed for 
podlings.")
 
-        # Finish section
-        release_policy.github_finish_workflow_path = 
policy_data.github_finish_workflow_path
-        self.__set_default_announce_release_template(policy_data, project, 
release_policy)
-        release_policy.preserve_download_files = 
policy_data.preserve_download_files
+        await self.__commit_and_log(project_name)
 
+    async def __commit_and_log(self, project_name: str) -> None:
         await self.__data.commit()
         self.__write_as.append_to_audit_log(
             asf_uid=self.__asf_uid,
             project_name=project_name,
         )
 
-    def __set_default_announce_release_template(
+    async def __get_or_create_policy(self, project_name: str) -> 
tuple[models.sql.Project, models.sql.ReleasePolicy]:
+        project = await self.__data.project(
+            name=project_name, status=models.sql.ProjectStatus.ACTIVE, 
_release_policy=True, _committee=True
+        ).demand(storage.AccessError(f"Project {project_name} not found"))
+
+        release_policy = project.release_policy
+        if release_policy is None:
+            release_policy = models.sql.ReleasePolicy(project=project)
+            project.release_policy = release_policy
+            self.__data.add(release_policy)
+
+        return project, release_policy
+
+    def __set_announce_release_template(
         self,
-        policy_data: models.policy.ReleasePolicyData,
+        submitted_template: str,
         project: models.sql.Project,
         release_policy: models.sql.ReleasePolicy,
     ) -> None:
-        submitted_announce_template = policy_data.announce_release_template
-        submitted_announce_template = 
submitted_announce_template.replace("\r\n", "\n")
-        rendered_default_announce_hash = 
policy_data.default_announce_release_template_hash
-        current_default_announce_text = project.policy_announce_release_default
-        current_default_announce_hash = 
util.compute_sha3_256(current_default_announce_text.encode())
-        submitted_announce_hash = 
util.compute_sha3_256(submitted_announce_template.encode())
-
-        if (submitted_announce_hash == rendered_default_announce_hash) or (
-            submitted_announce_hash == current_default_announce_hash
-        ):
+        submitted_template = submitted_template.replace("\r\n", "\n")
+        current_default_text = project.policy_announce_release_default
+        current_default_hash = 
util.compute_sha3_256(current_default_text.encode())
+        submitted_hash = util.compute_sha3_256(submitted_template.encode())
+
+        if submitted_hash == current_default_hash:
             release_policy.announce_release_template = ""
         else:
-            release_policy.announce_release_template = 
submitted_announce_template
+            release_policy.announce_release_template = submitted_template
 
-    def __set_default_min_hours(
+    def __set_min_hours(
         self,
-        policy_data: models.policy.ReleasePolicyData,
+        submitted_min_hours: int,
         project: models.sql.Project,
         release_policy: models.sql.ReleasePolicy,
     ) -> None:
-        submitted_min_hours = policy_data.min_hours
-        default_value_seen_on_page_min_hours = 
policy_data.default_min_hours_value_at_render
-        current_system_default_min_hours = project.policy_default_min_hours
-
-        if (
-            submitted_min_hours == default_value_seen_on_page_min_hours
-            or submitted_min_hours == current_system_default_min_hours
-        ):
+        current_system_default = project.policy_default_min_hours
+
+        if submitted_min_hours == current_system_default:
             release_policy.min_hours = None
         else:
             release_policy.min_hours = submitted_min_hours
 
-    def __set_default_start_vote_template(
+    def __set_start_vote_template(
         self,
-        policy_data: models.policy.ReleasePolicyData,
+        submitted_template: str,
         project: models.sql.Project,
         release_policy: models.sql.ReleasePolicy,
     ) -> None:
-        submitted_start_template = policy_data.start_vote_template
-        submitted_start_template = submitted_start_template.replace("\r\n", 
"\n")
-        rendered_default_start_hash = 
policy_data.default_start_vote_template_hash
-        current_default_start_text = project.policy_start_vote_default
-        current_default_start_hash = 
util.compute_sha3_256(current_default_start_text.encode())
-        submitted_start_hash = 
util.compute_sha3_256(submitted_start_template.encode())
-
-        if (submitted_start_hash == rendered_default_start_hash) or (
-            submitted_start_hash == current_default_start_hash
-        ):
+        submitted_template = submitted_template.replace("\r\n", "\n")
+        current_default_text = project.policy_start_vote_default
+        current_default_hash = 
util.compute_sha3_256(current_default_text.encode())
+        submitted_hash = util.compute_sha3_256(submitted_template.encode())
+
+        if submitted_hash == current_default_hash:
             release_policy.start_vote_template = ""
         else:
-            release_policy.start_vote_template = submitted_start_template
+            release_policy.start_vote_template = submitted_template
+
+
+def _split_lines(text: str) -> list[str]:
+    return [line.strip() for line in text.split("\n") if line.strip()]


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

Reply via email to