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 bc37a3b  Add cached authorisation checking, and adjust permissions 
levels
bc37a3b is described below

commit bc37a3b9c9ae677d80aaefd0690fbe6560accbcb
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jul 23 14:35:45 2025 +0100

    Add cached authorisation checking, and adjust permissions levels
---
 atr/blueprints/api/api.py   |   8 +-
 atr/db/interaction.py       |  29 -------
 atr/routes/keys.py          |  24 +++---
 atr/storage/__init__.py     | 181 ++++++++++++++++++++++++++++++++++----------
 atr/storage/writers/keys.py |  37 ++++-----
 docs/storage-interface.html |  16 ++--
 docs/storage-interface.md   |  16 ++--
 7 files changed, 194 insertions(+), 117 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 967a14c..420758a 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -304,8 +304,8 @@ async def keys_add(data: models.api.KeysAddArgs) -> 
DictResponse:
     selected_committee_names = data.committees
 
     async with storage.write(asf_uid) as write:
-        wafm = write.as_foundation_member().result_or_raise()
-        ocr: types.Outcome[types.Key] = await 
wafm.keys.ensure_stored_one(data.key)
+        wafc = write.as_foundation_committer().result_or_raise()
+        ocr: types.Outcome[types.Key] = await 
wafc.keys.ensure_stored_one(data.key)
         key = ocr.result_or_raise()
 
         for selected_committee_name in selected_committee_names:
@@ -333,8 +333,8 @@ async def keys_delete(data: models.api.KeysDeleteArgs) -> 
DictResponse:
 
     outcomes = types.Outcomes[str]()
     async with storage.write(asf_uid) as write:
-        wafm = write.as_foundation_member().result_or_raise()
-        outcome: types.Outcome[sql.PublicSigningKey] = await 
wafm.keys.delete_key(fingerprint)
+        wafc = write.as_foundation_committer().result_or_raise()
+        outcome: types.Outcome[sql.PublicSigningKey] = await 
wafc.keys.delete_key(fingerprint)
         key = outcome.result_or_raise()
 
         for committee in key.committees:
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index ab15aeb..4162821 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -396,35 +396,6 @@ async def unfinished_releases(asfuid: str) -> dict[str, 
list[sql.Release]]:
     return releases
 
 
-async def upload_keys(
-    user_committees: list[str],
-    keys_text: str,
-    selected_committees: list[str],
-    ldap_data: dict[str, str] | None = None,
-) -> tuple[list[dict], int, int, list[str]]:
-    key_blocks = util.parse_key_blocks(keys_text)
-    if not key_blocks:
-        raise InteractionError("No valid OpenPGP keys found in the uploaded 
file")
-
-    # Ensure that the selected committees are ones of which the user is 
actually a member
-    invalid_committees = [committee for committee in selected_committees if 
(committee not in user_committees)]
-    if invalid_committees:
-        raise InteractionError(f"Invalid committee selection: {', 
'.join(invalid_committees)}")
-
-    # TODO: Do we modify this? Store a copy just in case, for the template to 
use
-    submitted_committees = selected_committees[:]
-
-    # Process each key block
-    results = await _upload_process_key_blocks(key_blocks, 
selected_committees, ldap_data=ldap_data)
-    # if not results:
-    #     raise InteractionError("No keys were added")
-
-    success_count = sum(1 for result in results if result["status"] == 
"success")
-    error_count = len(results) - success_count
-
-    return results, success_count, error_count, submitted_committees
-
-
 async def upload_keys_bytes(
     user_committees: list[str],
     keys_bytes: bytes,
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 0f45d46..0d8505f 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -131,7 +131,9 @@ async def add(session: routes.CommitterSession) -> str:
         project_list = session.committees + session.projects
         user_committees = await data.committee(name_in=project_list).all()
 
-    committee_choices = [(c.name, c.display_name or c.name) for c in 
user_committees]
+    committee_choices = [
+        (c.name, c.display_name or c.name) for c in user_committees if (not 
util.committee_is_standing(c.name))
+    ]
 
     class AddOpenPGPKeyForm(util.QuartFormTyped):
         public_key = wtforms.TextAreaField(
@@ -163,13 +165,15 @@ async def add(session: routes.CommitterSession) -> str:
             selected_committee_names: list[str] = 
util.unwrap(form.selected_committees.data)
 
             async with storage.write(asf_uid) as write:
-                wafm = write.as_foundation_member().result_or_raise()
-                ocr: types.Outcome[types.Key] = await 
wafm.keys.ensure_stored_one(key_text)
+                wafc = write.as_foundation_committer().result_or_raise()
+                ocr: types.Outcome[types.Key] = await 
wafc.keys.ensure_stored_one(key_text)
                 key = ocr.result_or_raise()
 
                 for selected_committee_name in selected_committee_names:
-                    wacm = 
write.as_committee_member(selected_committee_name).result_or_raise()
-                    outcome: types.Outcome[types.LinkedCommittee] = await 
wacm.keys.associate_fingerprint(
+                    # TODO: Should this be committee member or committee 
participant?
+                    # Also, should we emit warnings and continue here?
+                    wacp = 
write.as_committee_participant(selected_committee_name).result_or_raise()
+                    outcome: types.Outcome[types.LinkedCommittee] = await 
wacp.keys.associate_fingerprint(
                         key.key_model.fingerprint
                     )
                     outcome.result_or_raise()
@@ -218,10 +222,10 @@ async def delete(session: routes.CommitterSession) -> 
response.Response:
 
     # Otherwise, delete an OpenPGP key
     async with storage.write(session.uid) as write:
-        wafm = write.as_foundation_member().result_or_none()
-        if wafm is None:
+        wafc = write.as_foundation_committer().result_or_none()
+        if wafc is None:
             return await session.redirect(keys, error="Key not found or not 
owned by you")
-        outcome: types.Outcome[sql.PublicSigningKey] = await 
wafm.keys.delete_key(fingerprint)
+        outcome: types.Outcome[sql.PublicSigningKey] = await 
wafc.keys.delete_key(fingerprint)
         match outcome:
             case types.OutcomeResult():
                 return await session.redirect(keys, success="Key deleted 
successfully")
@@ -304,8 +308,8 @@ async def details(session: routes.CommitterSession, 
fingerprint: str) -> str | r
 async def export(session: routes.CommitterSession, committee_name: str) -> 
quart.Response:
     """Export a KEYS file for a specific committee."""
     async with storage.write(session.uid) as write:
-        wafm = write.as_foundation_member().result_or_raise()
-        keys_file_text = await wafm.keys.keys_file_text(committee_name)
+        wafc = write.as_foundation_committer().result_or_raise()
+        keys_file_text = await wafc.keys.keys_file_text(committee_name)
 
     return quart.Response(keys_file_text, mimetype="text/plain")
 
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index d22652c..e6517fb 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -18,7 +18,9 @@
 from __future__ import annotations
 
 import contextlib
-from typing import TYPE_CHECKING, Final, TypeVar
+import logging
+import time
+from typing import TYPE_CHECKING, Final
 
 if TYPE_CHECKING:
     from collections.abc import AsyncGenerator
@@ -44,9 +46,9 @@ class AccessCredentialsRead(AccessCredentials): ...
 class AccessCredentialsWrite(AccessCredentials): ...
 
 
-A = TypeVar("A", bound=AccessCredentials)
-R = TypeVar("R", bound=AccessCredentialsRead)
-W = TypeVar("W", bound=AccessCredentialsWrite)
+# A = TypeVar("A", bound=AccessCredentials)
+# R = TypeVar("R", bound=AccessCredentialsRead)
+# W = TypeVar("W", bound=AccessCredentialsWrite)
 
 ## Access error
 
@@ -63,22 +65,24 @@ class ReadAsCommitteeMember(AccessCredentialsRead): ...
 class ReadAsCommitteeParticipant(AccessCredentialsRead): ...
 
 
-class ReadAsFoundationMember(AccessCredentialsRead): ...
+class ReadAsFoundationCommitter(AccessCredentialsRead): ...
 
 
-class ReadAsFoundationParticipant(AccessCredentialsRead): ...
+class ReadAsGeneralPublic(AccessCredentialsRead): ...
 
 
 class Read:
-    def __init__(self, data: db.Session, asf_uid: str | None = None):
+    def __init__(self, data: db.Session, asf_uid: str | None, member_of: 
set[str], participant_of: set[str]):
         self.__data = data
         self.__asf_uid = asf_uid
+        self.__member_of = member_of
+        self.__participant_of = participant_of
 
 
 # Write
 
 
-class WriteAsFoundationParticipant(AccessCredentialsWrite):
+class WriteAsGeneralPublic(AccessCredentialsWrite):
     def __init__(self, write: Write, data: db.Session):
         self.__write = write
         self.__data = data
@@ -94,7 +98,7 @@ class WriteAsFoundationParticipant(AccessCredentialsWrite):
         return VALIDATE_AT_RUNTIME
 
 
-class WriteAsFoundationMember(WriteAsFoundationParticipant):
+class WriteAsFoundationCommitter(WriteAsGeneralPublic):
     def __init__(self, write: Write, data: db.Session, asf_uid: str):
         if self.validate_at_runtime:
             if not isinstance(asf_uid, str):
@@ -104,7 +108,7 @@ class WriteAsFoundationMember(WriteAsFoundationParticipant):
         self.__asf_uid = asf_uid
         self.__authenticated = True
         # TODO: We need a definitive list of ASF UIDs
-        self.keys = writers.keys.FoundationMember(
+        self.keys = writers.keys.FoundationCommitter(
             self,
             self.__write,
             self.__data,
@@ -120,7 +124,7 @@ class WriteAsFoundationMember(WriteAsFoundationParticipant):
         return VALIDATE_AT_RUNTIME
 
 
-class WriteAsCommitteeParticipant(WriteAsFoundationMember):
+class WriteAsCommitteeParticipant(WriteAsFoundationCommitter):
     def __init__(self, write: Write, data: db.Session, asf_uid: str, 
committee_name: str):
         if self.validate_at_runtime:
             if not isinstance(committee_name, str):
@@ -179,7 +183,7 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
         return VALIDATE_AT_RUNTIME
 
 
-# class WriteAsFoundationAdmin(WriteAsFoundationMember):
+# class WriteAsFoundationAdmin(WriteAsFoundationCommitter):
 #     def __init__(self, write: Write, data: db.Session, asf_uid: str):
 #         self.__write = write
 #         self.__data = data
@@ -203,19 +207,52 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
 
 class Write:
     # Read and Write have authenticator methods which return access outcomes
-    def __init__(self, data: db.Session, asf_uid: str | None = None):
+    def __init__(self, data: db.Session, asf_uid: str | None, member_of: 
set[str], participant_of: set[str]):
         self.__data = data
         self.__asf_uid = asf_uid
+        self.__member_of = member_of
+        self.__participant_of = participant_of
+
+    # def as_committee_admin(self, committee_name: str) -> 
types.Outcome[WriteAsCommitteeMember]:
+    #     if self.__asf_uid is None:
+    #         return types.OutcomeException(AccessError("No ASF UID"))
+    #     try:
+    #         wacm = WriteAsCommitteeMember(self, self.__data, self.__asf_uid, 
committee_name)
+    #     except Exception as e:
+    #         return types.OutcomeException(e)
+    #     return types.OutcomeResult(wacm)
 
     def as_committee_member(self, committee_name: str) -> 
types.Outcome[WriteAsCommitteeMember]:
         if self.__asf_uid is None:
             return types.OutcomeException(AccessError("No ASF UID"))
+        if committee_name not in self.__member_of:
+            return types.OutcomeException(AccessError(f"Not a member of 
{committee_name}"))
         try:
             wacm = WriteAsCommitteeMember(self, self.__data, self.__asf_uid, 
committee_name)
         except Exception as e:
             return types.OutcomeException(e)
         return types.OutcomeResult(wacm)
 
+    def as_committee_participant(self, committee_name: str) -> 
types.Outcome[WriteAsCommitteeParticipant]:
+        if self.__asf_uid is None:
+            return types.OutcomeException(AccessError("No ASF UID"))
+        if committee_name not in self.__participant_of:
+            return types.OutcomeException(AccessError(f"Not a participant of 
{committee_name}"))
+        try:
+            wacp = WriteAsCommitteeParticipant(self, self.__data, 
self.__asf_uid, committee_name)
+        except Exception as e:
+            return types.OutcomeException(e)
+        return types.OutcomeResult(wacp)
+
+    def as_foundation_committer(self) -> 
types.Outcome[WriteAsFoundationCommitter]:
+        if self.__asf_uid is None:
+            return types.OutcomeException(AccessError("No ASF UID"))
+        try:
+            wafm = WriteAsFoundationCommitter(self, self.__data, 
self.__asf_uid)
+        except Exception as e:
+            return types.OutcomeException(e)
+        return types.OutcomeResult(wafm)
+
     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}")
@@ -224,41 +261,103 @@ class Write:
             return types.OutcomeException(AccessError("No committee found for 
project"))
         if self.__asf_uid is None:
             return types.OutcomeException(AccessError("No ASF UID"))
+        if project.committee.name not in self.__member_of:
+            return types.OutcomeException(AccessError(f"Not a member of 
{project.committee.name}"))
         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"))
-        try:
-            wafm = WriteAsFoundationMember(self, self.__data, self.__asf_uid)
-        except Exception as e:
-            return types.OutcomeException(e)
-        return types.OutcomeResult(wafm)
-
 
 # Context managers
 
 
[email protected]
-async def read(asf_uid: str | None = None) -> AsyncGenerator[Read]:
-    async with db.session() as data:
-        # TODO: Replace data with a DatabaseReader instance
-        yield Read(data, asf_uid)
-
-
[email protected]
-async def read_and_write(asf_uid: str | None = None) -> 
AsyncGenerator[tuple[Read, Write]]:
-    async with db.session() as data:
-        # TODO: Replace data with DatabaseReader and DatabaseWriter instances
-        yield Read(data, asf_uid), Write(data, asf_uid)
-
-
[email protected]
-async def write(asf_uid: str | None = None) -> AsyncGenerator[Write]:
-    async with db.session() as data:
-        # TODO: Replace data with a DatabaseWriter instance
-        yield Write(data, asf_uid)
+class ContextManagers:
+    def __init__(self, cache_for_at_most_seconds: int = 600):
+        self.__cache_for_at_most_seconds = cache_for_at_most_seconds
+        self.__member_of: dict[str, set[str]] = {}
+        self.__participant_of: dict[str, set[str]] = {}
+        self.__last_refreshed = None
+
+    def __outdated(self) -> bool:
+        if self.__last_refreshed is None:
+            return True
+        now = int(time.time())
+        since_last_refresh = now - self.__last_refreshed
+        return since_last_refresh > self.__cache_for_at_most_seconds
+
+    async def __refresh(self, data: db.Session) -> None:
+        start = time.perf_counter_ns()
+        committees = await data.committee().all()
+        for committee in committees:
+            for member in committee.committee_members:
+                if member not in self.__member_of:
+                    self.__member_of[member] = set()
+                self.__member_of[member].add(committee.name)
+            for participant in committee.committers:
+                if participant not in self.__participant_of:
+                    self.__participant_of[participant] = set()
+                self.__participant_of[participant].add(committee.name)
+        self.__last_refreshed = int(time.time())
+        finish = time.perf_counter_ns()
+        logging.info(f"ContextManagers.__refresh took {finish - start:,} ns")
+
+    async def member_of(self, data: db.Session, asf_uid: str | None = None) -> 
set[str]:
+        start = time.perf_counter_ns()
+        if asf_uid is None:
+            return set()
+        if self.__outdated():
+            # This races, but it doesn't matter
+            await self.__refresh(data)
+        committee_names_set = self.__member_of[asf_uid]
+        finish = time.perf_counter_ns()
+        logging.info(f"ContextManagers.member_of took {finish - start:,} ns")
+        return committee_names_set
+
+    async def participant_of(self, data: db.Session, asf_uid: str | None = 
None) -> set[str]:
+        start = time.perf_counter_ns()
+        if asf_uid is None:
+            return set()
+        if self.__outdated():
+            # This races, but it doesn't matter
+            await self.__refresh(data)
+        committee_names_set = self.__participant_of[asf_uid]
+        finish = time.perf_counter_ns()
+        logging.info(f"ContextManagers.participant_of took {finish - start:,} 
ns")
+        return committee_names_set
+
+    @contextlib.asynccontextmanager
+    async def read(self, asf_uid: str | None = None) -> AsyncGenerator[Read]:
+        async with db.session() as data:
+            # TODO: Replace data with a DatabaseReader instance
+            member_of = await self.member_of(data, asf_uid)
+            participant_of = await self.participant_of(data, asf_uid)
+            r = Read(data, asf_uid, member_of, participant_of)
+            yield r
+
+    @contextlib.asynccontextmanager
+    async def read_and_write(self, asf_uid: str | None = None) -> 
AsyncGenerator[tuple[Read, Write]]:
+        async with db.session() as data:
+            # TODO: Replace data with DatabaseReader and DatabaseWriter 
instances
+            member_of = await self.member_of(data, asf_uid)
+            participant_of = await self.participant_of(data, asf_uid)
+            r = Read(data, asf_uid, member_of, participant_of)
+            w = Write(data, asf_uid, member_of, participant_of)
+            yield r, w
+
+    @contextlib.asynccontextmanager
+    async def write(self, asf_uid: str | None = None) -> AsyncGenerator[Write]:
+        async with db.session() as data:
+            # TODO: Replace data with a DatabaseWriter instance
+            member_of = await self.member_of(data, asf_uid)
+            participant_of = await self.participant_of(data, asf_uid)
+            w = Write(data, asf_uid, member_of, participant_of)
+            yield w
+
+
+_MANAGERS: Final[ContextManagers] = ContextManagers()
+
+read = _MANAGERS.read
+read_and_write = _MANAGERS.read_and_write
+write = _MANAGERS.write
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index a8ae3df..c84d905 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -76,9 +76,9 @@ def performance_async(func: Callable[..., Coroutine[Any, Any, 
Any]]) -> Callable
     return wrapper
 
 
-class FoundationMember:
+class FoundationCommitter:
     def __init__(
-        self, credentials: storage.WriteAsFoundationMember, write: 
storage.Write, data: db.Session, asf_uid: str
+        self, credentials: storage.WriteAsFoundationCommitter, write: 
storage.Write, data: db.Session, asf_uid: str
     ):
         if credentials.validate_at_runtime:
             if credentials.authenticated is not True:
@@ -343,7 +343,7 @@ and was published by the committee.\
         return None
 
 
-class CommitteeParticipant(FoundationMember):
+class CommitteeParticipant(FoundationCommitter):
     def __init__(
         self,
         credentials: storage.WriteAsCommitteeParticipant,
@@ -351,19 +351,6 @@ class CommitteeParticipant(FoundationMember):
         data: db.Session,
         asf_uid: str,
         committee_name: str,
-    ):
-        super().__init__(credentials, write, data, asf_uid)
-        self.__committee_name = committee_name
-
-
-class CommitteeMember(CommitteeParticipant):
-    def __init__(
-        self,
-        credentials: storage.WriteAsCommitteeMember,
-        write: storage.Write,
-        data: db.Session,
-        asf_uid: str,
-        committee_name: str,
     ):
         self.__credentials = credentials
         self.__write = write
@@ -618,7 +605,23 @@ class CommitteeMember(CommitteeParticipant):
         return outcomes
 
 
-# class FoundationAdmin(FoundationMember):
+class CommitteeMember(CommitteeParticipant):
+    def __init__(
+        self,
+        credentials: storage.WriteAsCommitteeMember,
+        write: storage.Write,
+        data: db.Session,
+        asf_uid: str,
+        committee_name: str,
+    ):
+        self.__credentials = credentials
+        self.__write = write
+        self.__data = data
+        self.__asf_uid = asf_uid
+        self.__committee_name = committee_name
+
+
+# class FoundationAdmin(FoundationCommitter):
 #     def __init__(
 #         self, credentials: storage.WriteAsFoundationAdmin, write: 
storage.Write, data: db.Session, asf_uid: str
 #     ):
diff --git a/docs/storage-interface.html b/docs/storage-interface.html
index 02cacc3..727a896 100644
--- a/docs/storage-interface.html
+++ b/docs/storage-interface.html
@@ -9,8 +9,8 @@
 </ol>
 <p>Here is an actual example from our API code:</p>
 <pre><code class="language-python">async with storage.write(asf_uid) as write:
-    wafm = write.as_foundation_member().writer_or_raise()
-    ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+    wafc = write.as_foundation_committer().writer_or_raise()
+    ocr: types.Outcome[types.Key] = await wafc.keys.ensure_stored_one(data.key)
     key = ocr.result_or_raise()
 
     for selected_committee_name in selected_committee_names:
@@ -24,10 +24,10 @@
 <p>In this case we decide to raise as soon as there is any error. We could 
also choose instead to display a warning, ignore the error, etc.</p>
 <p>The first few lines in the context session show the classic three step 
approach. Here they are again with comments:</p>
 <pre><code class="language-python">    # 1. Request permissions
-    wafm = write.as_foundation_member().writer_or_raise()
+    wafc = write.as_foundation_committer().writer_or_raise()
 
     # 2. Use the exposed functionality
-    ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+    ocr: types.Outcome[types.Key] = await wafc.keys.ensure_stored_one(data.key)
 
     # 3. Handle the outcome
     key = ocr.result_or_raise()
@@ -35,19 +35,19 @@
 <h2>How do we add functionality to the storage interface?</h2>
 <p>Add all the functionality to classes in modules in the 
<code>atr/storage/writers</code> directory. Code to write public keys to 
storage, for example, goes in <code>atr/storage/writers/keys.py</code>.</p>
 <p>Classes in modules in the <code>atr/storage/writers</code> directory must 
be named as follows:</p>
-<pre><code class="language-python">class FoundationParticipant:
+<pre><code class="language-python">class GeneralPublic:
     ...
 
-class FoundationMember(FoundationParticipant):
+class FoundationCommitter(GeneralPublic):
     ...
 
-class CommitteeParticipant(FoundationMember):
+class CommitteeParticipant(FoundationCommitter):
     ...
 
 class CommitteeMember(CommitteeParticipant):
     ...
 </code></pre>
-<p>This creates a hierarchy, <code>FoundationParticipant</code> → 
<code>FoundationMember</code> → <code>CommitteeParticipant</code> → 
<code>CommitteeMember</code>. We can add other permissions levels if 
necessary.</p>
+<p>This creates a hierarchy, <code>GeneralPublic</code> → 
<code>FoundationCommitter</code> → <code>CommitteeParticipant</code> → 
<code>CommitteeMember</code>. We can add other permissions levels if 
necessary.</p>
 <p>Use <code>__private_methods</code> for code specific to one permission 
level which is not exposed in the interface, e.g. helpers. Use 
<code>public_methods</code> for code appropriate to expose when users meet the 
appropriate permission level. Consider returning outcomes, as explained in the 
next section.</p>
 <h2>Returning outcomes</h2>
 <p>Consider using the <strong>outcome types</strong> in 
<code>atr.storage.types</code> when returning results from writer module 
methods. The outcome types <em>solve many problems</em>, but here is an 
example:</p>
diff --git a/docs/storage-interface.md b/docs/storage-interface.md
index af6b9e8..8710fb6 100644
--- a/docs/storage-interface.md
+++ b/docs/storage-interface.md
@@ -14,8 +14,8 @@ Here is an actual example from our API code:
 
 ```python
 async with storage.write(asf_uid) as write:
-    wafm = write.as_foundation_member().writer_or_raise()
-    ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+    wafc = write.as_foundation_committer().writer_or_raise()
+    ocr: types.Outcome[types.Key] = await wafc.keys.ensure_stored_one(data.key)
     key = ocr.result_or_raise()
 
     for selected_committee_name in selected_committee_names:
@@ -34,10 +34,10 @@ The first few lines in the context session show the classic 
three step approach.
 
 ```python
     # 1. Request permissions
-    wafm = write.as_foundation_member().writer_or_raise()
+    wafc = write.as_foundation_committer().writer_or_raise()
 
     # 2. Use the exposed functionality
-    ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+    ocr: types.Outcome[types.Key] = await wafc.keys.ensure_stored_one(data.key)
 
     # 3. Handle the outcome
     key = ocr.result_or_raise()
@@ -50,20 +50,20 @@ Add all the functionality to classes in modules in the 
`atr/storage/writers` dir
 Classes in modules in the `atr/storage/writers` directory must be named as 
follows:
 
 ```python
-class FoundationParticipant:
+class GeneralPublic:
     ...
 
-class FoundationMember(FoundationParticipant):
+class FoundationCommitter(GeneralPublic):
     ...
 
-class CommitteeParticipant(FoundationMember):
+class CommitteeParticipant(FoundationCommitter):
     ...
 
 class CommitteeMember(CommitteeParticipant):
     ...
 ```
 
-This creates a hierarchy, `FoundationParticipant` → `FoundationMember` → 
`CommitteeParticipant` → `CommitteeMember`. We can add other permissions levels 
if necessary.
+This creates a hierarchy, `GeneralPublic` → `FoundationCommitter` → 
`CommitteeParticipant` → `CommitteeMember`. We can add other permissions levels 
if necessary.
 
 Use `__private_methods` for code specific to one permission level which is not 
exposed in the interface, e.g. helpers. Use `public_methods` for code 
appropriate to expose when users meet the appropriate permission level. 
Consider returning outcomes, as explained in the next section.
 


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

Reply via email to