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]