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 ea8f674  Move more KEYS functionality to the storage interface
ea8f674 is described below

commit ea8f674859dd40caa232a461182a67aee90ab475
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 22 19:09:50 2025 +0100

    Move more KEYS functionality to the storage interface
---
 atr/routes/keys.py          | 31 ++++++-------------------------
 atr/storage/__init__.py     | 22 ++++++++++++++++++++++
 atr/storage/writers/keys.py | 36 ++++++++++++++++++++++++++++++++++++
 3 files changed, 64 insertions(+), 25 deletions(-)

diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index e361989..0f45d46 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -26,7 +26,6 @@ import logging
 import logging.handlers
 from collections.abc import Awaitable, Callable, Sequence
 
-import aiofiles.os
 import aiohttp
 import asfquart as asfquart
 import asfquart.base as base
@@ -38,7 +37,6 @@ import wtforms
 
 import atr.db as db
 import atr.models.sql as sql
-import atr.revision as revision
 import atr.routes as routes
 import atr.routes.compose as compose
 import atr.storage as storage
@@ -316,33 +314,16 @@ async def export(session: routes.CommitterSession, 
committee_name: str) -> quart
 async def import_selected_revision(
     session: routes.CommitterSession, project_name: str, version_name: str
 ) -> response.Response:
-    await session.check_access(project_name)
-
     await util.validate_empty_form()
-    release = await session.release(project_name, version_name, 
with_committee=True)
-    keys_path = util.release_directory(release) / "KEYS"
-    async with aiofiles.open(keys_path, encoding="utf-8") as f:
-        keys_text = await f.read()
-    if release.committee is None:
-        raise routes.FlashError("No committee found for release")
 
     async with storage.write(session.uid) as write:
-        wacm = 
write.as_committee_member(release.committee.name).result_or_raise()
-        outcomes: types.Outcomes[types.Key] = await 
wacm.keys.ensure_associated(keys_text)
-        success_count = outcomes.result_count
-        error_count = outcomes.exception_count
+        access_outcome = await write.as_project_committee_member(project_name)
+        wacm = access_outcome.result_or_raise()
+        outcomes: types.Outcomes[types.Key] = await 
wacm.keys.import_keys_file(project_name, version_name)
 
-    message = f"Uploaded {success_count} keys,"
-    if error_count > 0:
-        message += f" failed to upload {error_count} keys for 
{release.committee.name}"
-    # Remove the KEYS file if 100% imported
-    if (success_count > 0) and (error_count == 0):
-        description = "Removed KEYS file after successful import through web 
interface"
-        async with revision.create_and_manage(
-            project_name, version_name, session.uid, description=description
-        ) as creating:
-            path_in_new_revision = creating.interim_path / "KEYS"
-            await aiofiles.os.remove(path_in_new_revision)
+    message = f"Uploaded {outcomes.result_count} keys,"
+    if outcomes.exception_count > 0:
+        message += f" failed to upload {outcomes.exception_count} keys for 
{wacm.committee_name}"
     return await session.redirect(
         compose.selected,
         success=message,
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index 05c1c18..d22652c 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -142,6 +142,10 @@ class WriteAsCommitteeParticipant(WriteAsFoundationMember):
     def authenticated(self) -> bool:
         return self.__authenticated
 
+    @property
+    def committee_name(self) -> str:
+        return self.__committee_name
+
     @property
     def validate_at_runtime(self) -> bool:
         return VALIDATE_AT_RUNTIME
@@ -166,6 +170,10 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
     def authenticated(self) -> bool:
         return self.__authenticated
 
+    @property
+    def committee_name(self) -> str:
+        return self.__committee_name
+
     @property
     def validate_at_runtime(self) -> bool:
         return VALIDATE_AT_RUNTIME
@@ -208,6 +216,20 @@ class Write:
             return types.OutcomeException(e)
         return types.OutcomeResult(wacm)
 
+    async def as_project_committee_member(self, project_name: str) -> 
types.Outcome[WriteAsCommitteeMember]:
+        project = await self.__data.project(project_name, 
_committee=True).demand(
+            AccessError(f"Project not found: {project_name}")
+        )
+        if project.committee is None:
+            return types.OutcomeException(AccessError("No committee found for 
project"))
+        if self.__asf_uid is None:
+            return types.OutcomeException(AccessError("No ASF UID"))
+        try:
+            wacm = WriteAsCommitteeMember(self, self.__data, self.__asf_uid, 
project.committee.name)
+        except Exception as e:
+            return types.OutcomeException(e)
+        return types.OutcomeResult(wacm)
+
     def as_foundation_member(self) -> types.Outcome[WriteAsFoundationMember]:
         if self.__asf_uid is None:
             return types.OutcomeException(AccessError("No ASF UID"))
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index 57eeb0f..7cd088e 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -29,6 +29,8 @@ import textwrap
 import time
 from typing import TYPE_CHECKING, Any, Final, NoReturn
 
+import aiofiles
+import aiofiles.os
 import pgpy
 import pgpy.constants as constants
 import sqlalchemy.dialects.sqlite as sqlite
@@ -436,6 +438,10 @@ class CommitteeMember(CommitteeParticipant):
             storage.AccessError(f"Committee not found: 
{self.__committee_name}")
         )
 
+    @property
+    def committee_name(self) -> str:
+        return self.__committee_name
+
     @performance_async
     async def ensure_associated(self, keys_file_text: str) -> 
types.Outcomes[types.Key]:
         outcomes: types.Outcomes[types.Key] = await 
self.__ensure(keys_file_text, associate=True)
@@ -450,6 +456,36 @@ class CommitteeMember(CommitteeParticipant):
             await self.autogenerate_keys_file()
         return outcomes
 
+    @performance_async
+    async def import_keys_file(self, project_name: str, version_name: str) -> 
types.Outcomes[types.Key]:
+        import atr.revision as revision
+
+        release = await self.__data.release(
+            project_name=project_name,
+            version=version_name,
+            _committee=True,
+        ).demand(storage.AccessError(f"Release not found: {project_name} 
{version_name}"))
+        keys_path = util.release_directory(release) / "KEYS"
+        async with aiofiles.open(keys_path, encoding="utf-8") as f:
+            keys_file_text = await f.read()
+        if release.committee is None:
+            raise storage.AccessError("No committee found for release")
+        if release.committee.name != self.__committee_name:
+            raise storage.AccessError(
+                f"Release {project_name} {version_name} is not associated with 
committee {self.__committee_name}"
+            )
+
+        outcomes = await self.ensure_associated(keys_file_text)
+        # Remove the KEYS file if 100% imported
+        if (outcomes.result_count > 0) and (outcomes.exception_count == 0):
+            description = "Removed KEYS file after successful import through 
web interface"
+            async with revision.create_and_manage(
+                project_name, version_name, self.__asf_uid, 
description=description
+            ) as creating:
+                path_in_new_revision = creating.interim_path / "KEYS"
+                await aiofiles.os.remove(path_in_new_revision)
+        return outcomes
+
     @performance
     def __block_models(self, key_block: str, ldap_data: dict[str, str]) -> 
list[types.Key | Exception]:
         # This cache is only held for the session


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

Reply via email to