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 51094de Move outcome types to their own module
51094de is described below
commit 51094dee2d57e52e2cde1fab8d4b73712c520046
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Aug 6 20:18:37 2025 +0100
Move outcome types to their own module
---
atr/blueprints/admin/admin.py | 9 +-
atr/blueprints/api/api.py | 28 ++---
atr/routes/distribute.py | 12 +--
atr/routes/keys.py | 29 +++---
atr/storage/__init__.py | 64 ++++++------
atr/storage/outcome.py | 230 ++++++++++++++++++++++++++++++++++++++++++
atr/storage/types.py | 221 +---------------------------------------
atr/storage/writers/keys.py | 69 ++++++-------
docs/storage-interface.html | 4 +-
docs/storage-interface.md | 4 +-
10 files changed, 344 insertions(+), 326 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index be8741d..cda0249 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -46,6 +46,7 @@ import atr.log as log
import atr.models.sql as sql
import atr.routes.mapping as mapping
import atr.storage as storage
+import atr.storage.outcome as outcome
import atr.storage.types as types
import atr.template as template
import atr.util as util
@@ -417,7 +418,7 @@ async def admin_keys_regenerate_all() -> quart.Response:
if asf_uid is None:
raise base.ASFQuartException("Invalid session, uid is None", 500)
- outcomes = types.Outcomes[str]()
+ outcomes = outcome.List[str]()
async with storage.write() as write:
for committee_name in committee_names:
wacm_outcome = write.as_committee_member_outcome(committee_name)
@@ -429,7 +430,7 @@ async def admin_keys_regenerate_all() -> quart.Response:
response_lines = []
for ocr in outcomes.results():
response_lines.append(f"Regenerated: {ocr}")
- for oce in outcomes.exceptions():
+ for oce in outcomes.errors():
response_lines.append(f"Error regenerating: {type(oce).__name__}
{oce}")
return quart.Response("\n".join(response_lines), mimetype="text/plain")
@@ -668,12 +669,12 @@ async def admin_test() ->
quart.wrappers.response.Response:
async with storage.write() as write:
wacm = write.as_committee_member("tooling")
start = time.perf_counter_ns()
- outcomes: types.Outcomes[types.Key] = await
wacm.keys.ensure_stored(keys_file_text)
+ outcomes: outcome.List[types.Key] = await
wacm.keys.ensure_stored(keys_file_text)
end = time.perf_counter_ns()
log.info(f"Upload of {outcomes.result_count} keys took {end - start}
ns")
for ocr in outcomes.results():
log.info(f"Uploaded key: {type(ocr)} {ocr.key_model.fingerprint}")
- for oce in outcomes.exceptions():
+ for oce in outcomes.errors():
log.error(f"Error uploading key: {type(oce)} {oce}")
parsed_count = outcomes.result_predicate_count(lambda k: k.status ==
types.KeyStatus.PARSED)
inserted_count = outcomes.result_predicate_count(lambda k: k.status ==
types.KeyStatus.INSERTED)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index a5eae63..a252770 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -47,6 +47,7 @@ import atr.routes.start as start
import atr.routes.vote as vote
import atr.routes.voting as voting
import atr.storage as storage
+import atr.storage.outcome as outcome
import atr.storage.types as types
import atr.tabulate as tabulate
import atr.tasks.vote as tasks_vote
@@ -365,15 +366,15 @@ async def key_add(data: models.api.KeyAddArgs) ->
DictResponse:
async with storage.write(asf_uid) as write:
wafc = write.as_foundation_committer()
- ocr: types.Outcome[types.Key] = await
wafc.keys.ensure_stored_one(data.key)
+ ocr: outcome.Outcome[types.Key] = await
wafc.keys.ensure_stored_one(data.key)
key = ocr.result_or_raise()
for selected_committee_name in selected_committee_names:
wacm = write.as_committee_member(selected_committee_name)
- outcome: types.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
+ oc: outcome.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
key.key_model.fingerprint
)
- outcome.result_or_raise()
+ oc.result_or_raise()
return models.api.KeyAddResults(
endpoint="/key/add",
@@ -395,11 +396,11 @@ async def key_delete(data: models.api.KeyDeleteArgs) ->
DictResponse:
asf_uid = _jwt_asf_uid()
fingerprint = data.fingerprint.lower()
- outcomes = types.Outcomes[str]()
+ outcomes = outcome.List[str]()
async with storage.write(asf_uid) as write:
wafc = write.as_foundation_committer()
- outcome: types.Outcome[sql.PublicSigningKey] = await
wafc.keys.delete_key(fingerprint)
- key = outcome.result_or_raise()
+ oc: outcome.Outcome[sql.PublicSigningKey] = await
wafc.keys.delete_key(fingerprint)
+ key = oc.result_or_raise()
for committee in key.committees:
wacm =
write.as_committee_member_outcome(committee.name).result_or_none()
@@ -447,25 +448,24 @@ async def keys_upload(data: models.api.KeysUploadArgs) ->
DictResponse:
selected_committee_name = data.committee
async with storage.write(asf_uid) as write:
wacm = write.as_committee_member(selected_committee_name)
- outcomes: types.Outcomes[types.Key] = await
wacm.keys.ensure_associated(filetext)
+ outcomes: outcome.List[types.Key] = await
wacm.keys.ensure_associated(filetext)
# TODO: It would be nice to serialise the actual outcomes
# Or, perhaps better yet, to have a standard datatype mapping
# This would be specified in models.api, then imported into
storage.types
# Or perhaps it should go in models.storage or models.outcomes
api_outcomes = []
- for outcome in outcomes.outcomes():
+ for oc in outcomes.outcomes():
api_outcome: models.api.KeysUploadOutcome | None = None
- match outcome:
- case types.OutcomeResult() as ocr:
- result: types.Key = ocr.result_or_raise()
+ match oc:
+ case outcome.Result(result):
api_outcome = models.api.KeysUploadResult(
status="success",
key=result.key_model,
)
- case types.OutcomeException() as oce:
+ case outcome.Error(error):
# TODO: This branch means we must improve the return type
- match oce.exception_or_none():
+ match error:
case types.PublicKeyError() as pke:
api_outcome = models.api.KeysUploadException(
status="error",
@@ -486,7 +486,7 @@ async def keys_upload(data: models.api.KeysUploadArgs) ->
DictResponse:
endpoint="/keys/upload",
results=api_outcomes,
success_count=outcomes.result_count,
- error_count=outcomes.exception_count,
+ error_count=outcomes.error_count,
submitted_committee=selected_committee_name,
).model_dump(), 200
diff --git a/atr/routes/distribute.py b/atr/routes/distribute.py
index 0f0c6bf..9fb2d1b 100644
--- a/atr/routes/distribute.py
+++ b/atr/routes/distribute.py
@@ -33,7 +33,7 @@ import atr.models.basic as basic
import atr.models.schema as schema
import atr.models.sql as sql
import atr.routes as routes
-import atr.storage.types as types
+import atr.storage.outcome as outcome
import atr.template as template
@@ -177,16 +177,16 @@ async def _distribute_page(*, project: str, version: str,
form: DistributeForm)
return await template.blank("Distribute", content=content)
-async def _distribute_post_api(api_url: str) -> types.Outcome[basic.JSON]:
+async def _distribute_post_api(api_url: str) -> outcome.Outcome[basic.JSON]:
try:
async with aiohttp.ClientSession() as session:
async with session.get(api_url) as response:
response.raise_for_status()
response_json = await response.json()
- return types.OutcomeResult(basic.as_json(response_json))
+ return outcome.Result(basic.as_json(response_json))
except aiohttp.ClientError as e:
# Can be 404
- return types.OutcomeException(e)
+ return outcome.Error(e)
async def _distribute_post_validated(form: DistributeForm) -> str:
@@ -218,9 +218,9 @@ async def _distribute_post_validated(form: DistributeForm)
-> str:
## API response
block.h2["API response"]
match await _distribute_post_api(api_url):
- case types.OutcomeResult(result):
+ case outcome.Result(result):
block.pre[json.dumps(result, indent=2)]
- case types.OutcomeException(exception):
+ case outcome.Error(exception):
block.pre[f"Error: {exception}"]
content = _page("Distribution submitted", block.collect())
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index c815b40..64ff85f 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -40,6 +40,7 @@ import atr.models.sql as sql
import atr.routes as routes
import atr.routes.compose as compose
import atr.storage as storage
+import atr.storage.outcome as outcome
import atr.storage.types as types
import atr.template as template
import atr.user as user
@@ -162,17 +163,17 @@ async def add(session: routes.CommitterSession) -> str:
async with storage.write() as write:
wafc = write.as_foundation_committer()
- ocr: types.Outcome[types.Key] = await
wafc.keys.ensure_stored_one(key_text)
+ ocr: outcome.Outcome[types.Key] = await
wafc.keys.ensure_stored_one(key_text)
key = ocr.result_or_raise()
for selected_committee_name in selected_committee_names:
# 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)
- outcome: types.Outcome[types.LinkedCommittee] = await
wacp.keys.associate_fingerprint(
+ oc: outcome.Outcome[types.LinkedCommittee] = await
wacp.keys.associate_fingerprint(
key.key_model.fingerprint
)
- outcome.result_or_raise()
+ oc.result_or_raise()
await quart.flash(f"OpenPGP key
{key.key_model.fingerprint.upper()} added successfully.", "success")
# Clear form data on success by creating a new empty form instance
@@ -222,12 +223,12 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
wafc = write.as_foundation_committer_outcome().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
wafc.keys.delete_key(fingerprint)
- match outcome:
- case types.OutcomeResult():
+ oc: outcome.Outcome[sql.PublicSigningKey] = await
wafc.keys.delete_key(fingerprint)
+ match oc:
+ case outcome.Result():
return await session.redirect(keys, success="Key deleted
successfully")
- case types.OutcomeException():
- return await session.redirect(keys, error=f"Error deleting
key: {outcome.exception_or_raise()}")
+ case outcome.Error(error):
+ return await session.redirect(keys, error=f"Error deleting
key: {error}")
return await session.redirect(keys, error="Key not found or not owned by
you")
@@ -307,11 +308,11 @@ async def import_selected_revision(
async with storage.write() as write:
wacm = await write.as_project_committee_member(project_name)
- outcomes: types.Outcomes[types.Key] = await
wacm.keys.import_keys_file(project_name, version_name)
+ outcomes: outcome.List[types.Key] = await
wacm.keys.import_keys_file(project_name, version_name)
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}"
+ if outcomes.error_count > 0:
+ message += f" failed to upload {outcomes.error_count} keys for
{wacm.committee_name}"
return await session.redirect(
compose.selected,
success=message,
@@ -438,11 +439,11 @@ async def update_committee_keys(session:
routes.CommitterSession, committee_name
async with storage.write() as write:
wacm = write.as_committee_member(committee_name)
match await wacm.keys.autogenerate_keys_file():
- case types.OutcomeResult():
+ case outcome.Result():
await quart.flash(
f'Successfully regenerated the KEYS file for the
"{committee_name}" committee.', "success"
)
- case types.OutcomeException():
+ case outcome.Error():
await quart.flash(f"Error regenerating the KEYS file for the
{committee_name} committee.", "error")
return await session.redirect(keys)
@@ -474,7 +475,7 @@ async def upload(session: routes.CommitterSession) -> str:
)
form = await UploadKeyForm.create_form()
- results: types.Outcomes[types.Key] | None = None
+ results: outcome.List[types.Key] | None = None
async def render(
error: str | None = None,
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index c4cbcc4..ccc2d14 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -31,8 +31,8 @@ import atr.log as log
import atr.models.basic as basic
import atr.models.sql as sql
import atr.principal as principal
+import atr.storage.outcome as outcome
import atr.storage.readers as readers
-import atr.storage.types as types
import atr.storage.writers as writers
import atr.user as user
@@ -111,22 +111,22 @@ class Read:
def as_foundation_committer(self) -> ReadAsFoundationCommitter:
return self.as_foundation_committer_outcome().result_or_raise()
- def as_foundation_committer_outcome(self) ->
types.Outcome[ReadAsFoundationCommitter]:
+ def as_foundation_committer_outcome(self) ->
outcome.Outcome[ReadAsFoundationCommitter]:
try:
rafc = ReadAsFoundationCommitter(self, self.__data)
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(rafc)
+ return outcome.Error(e)
+ return outcome.Result(rafc)
def as_general_public(self) -> ReadAsGeneralPublic:
return self.as_general_public_outcome().result_or_raise()
- def as_general_public_outcome(self) -> types.Outcome[ReadAsGeneralPublic]:
+ def as_general_public_outcome(self) ->
outcome.Outcome[ReadAsGeneralPublic]:
try:
ragp = ReadAsGeneralPublic(self, self.__data)
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(ragp)
+ return outcome.Error(e)
+ return outcome.Result(ragp)
# Write
@@ -208,58 +208,58 @@ class Write:
def as_committee_member(self, committee_name: str) ->
WriteAsCommitteeMember:
return
self.as_committee_member_outcome(committee_name).result_or_raise()
- def as_committee_member_outcome(self, committee_name: str) ->
types.Outcome[WriteAsCommitteeMember]:
+ def as_committee_member_outcome(self, committee_name: str) ->
outcome.Outcome[WriteAsCommitteeMember]:
if self.__authorisation.asf_uid is None:
- return types.OutcomeException(AccessError("No ASF UID"))
+ return outcome.Error(AccessError("No ASF UID"))
if not self.__authorisation.is_member_of(committee_name):
- return types.OutcomeException(
+ return outcome.Error(
AccessError(f"ASF UID {self.__authorisation.asf_uid} is not a
member of {committee_name}")
)
try:
wacm = WriteAsCommitteeMember(self, self.__data, committee_name)
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(wacm)
+ return outcome.Error(e)
+ return outcome.Result(wacm)
def as_committee_participant(self, committee_name: str) ->
WriteAsCommitteeParticipant:
return
self.as_committee_participant_outcome(committee_name).result_or_raise()
- def as_committee_participant_outcome(self, committee_name: str) ->
types.Outcome[WriteAsCommitteeParticipant]:
+ def as_committee_participant_outcome(self, committee_name: str) ->
outcome.Outcome[WriteAsCommitteeParticipant]:
if self.__authorisation.asf_uid is None:
- return types.OutcomeException(AccessError("No ASF UID"))
+ return outcome.Error(AccessError("No ASF UID"))
if not self.__authorisation.is_participant_of(committee_name):
- return types.OutcomeException(AccessError(f"Not a participant of
{committee_name}"))
+ return outcome.Error(AccessError(f"Not a participant of
{committee_name}"))
try:
wacp = WriteAsCommitteeParticipant(self, self.__data,
committee_name)
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(wacp)
+ return outcome.Error(e)
+ return outcome.Result(wacp)
def as_foundation_committer(self) -> WriteAsFoundationCommitter:
return self.as_foundation_committer_outcome().result_or_raise()
- def as_foundation_committer_outcome(self) ->
types.Outcome[WriteAsFoundationCommitter]:
+ def as_foundation_committer_outcome(self) ->
outcome.Outcome[WriteAsFoundationCommitter]:
if self.__authorisation.asf_uid is None:
- return types.OutcomeException(AccessError("No ASF UID"))
+ return outcome.Error(AccessError("No ASF UID"))
try:
wafm = WriteAsFoundationCommitter(self, self.__data)
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(wafm)
+ return outcome.Error(e)
+ return outcome.Result(wafm)
def as_foundation_admin(self, committee_name: str) ->
WriteAsFoundationAdmin:
return
self.as_foundation_admin_outcome(committee_name).result_or_raise()
- def as_foundation_admin_outcome(self, committee_name: str) ->
types.Outcome[WriteAsFoundationAdmin]:
+ def as_foundation_admin_outcome(self, committee_name: str) ->
outcome.Outcome[WriteAsFoundationAdmin]:
if self.__authorisation.asf_uid is None:
- return types.OutcomeException(AccessError("No ASF UID"))
+ return outcome.Error(AccessError("No ASF UID"))
if not user.is_admin(self.__authorisation.asf_uid):
- return types.OutcomeException(AccessError("Not an admin"))
+ return outcome.Error(AccessError("Not an admin"))
try:
wafa = WriteAsFoundationAdmin(self, self.__data, committee_name)
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(wafa)
+ return outcome.Error(e)
+ return outcome.Result(wafa)
# async def as_key_owner(self) -> types.Outcome[WriteAsKeyOwner]:
# ...
@@ -268,21 +268,21 @@ class Write:
write_as_outcome = await
self.as_project_committee_member_outcome(project_name)
return write_as_outcome.result_or_raise()
- async def as_project_committee_member_outcome(self, project_name: str) ->
types.Outcome[WriteAsCommitteeMember]:
+ async def as_project_committee_member_outcome(self, project_name: str) ->
outcome.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"))
+ return outcome.Error(AccessError("No committee found for project"))
if self.__authorisation.asf_uid is None:
- return types.OutcomeException(AccessError("No ASF UID"))
+ return outcome.Error(AccessError("No ASF UID"))
if not self.__authorisation.is_member_of(project.committee.name):
- return types.OutcomeException(AccessError(f"Not a member of
{project.committee.name}"))
+ return outcome.Error(AccessError(f"Not a member of
{project.committee.name}"))
try:
wacm = WriteAsCommitteeMember(self, self.__data,
project.committee.name)
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(wacm)
+ return outcome.Error(e)
+ return outcome.Result(wacm)
@property
def member_of(self) -> frozenset[str]:
diff --git a/atr/storage/outcome.py b/atr/storage/outcome.py
new file mode 100644
index 0000000..447b91f
--- /dev/null
+++ b/atr/storage/outcome.py
@@ -0,0 +1,230 @@
+# 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 collections.abc import Callable, Sequence
+from typing import NoReturn, TypeVar
+
+R = TypeVar("R", bound=object)
+E = TypeVar("E", bound=Exception)
+
+
+class Result[R]:
+ __match_args__ = ("_result",)
+ __result: R
+
+ def __init__(self, result: R, name: str | None = None):
+ self.__result = result
+ self.__name = name
+
+ @property
+ def name(self) -> str | None:
+ return self.__name
+
+ @property
+ def ok(self) -> bool:
+ return True
+
+ @property
+ def _result(self) -> R:
+ # This is only available on Result
+ # It is intended for pattern matching only
+ return self.__result
+
+ def error_or_none(self) -> Exception | None:
+ return None
+
+ def error_or_raise(self, exception_class: type[Exception] | None = None)
-> NoReturn:
+ if exception_class is not None:
+ raise exception_class(f"Asked for error on a result:
{self.__result}")
+ raise RuntimeError(f"Asked for error on a result: {self.__result}")
+
+ def error_type_or_none(self) -> type[Exception] | None:
+ return None
+
+ def result_or_none(self) -> R | None:
+ return self.__result
+
+ def result_or_raise(self, exception_class: type[Exception] | None = None)
-> R:
+ return self.__result
+
+
+class Error[R, E: Exception = Exception]:
+ __match_args__ = ("_error",)
+ __error: E
+
+ def __init__(self, error: E, name: str | None = None):
+ self.__error = error
+ self.__name = name
+
+ @property
+ def name(self) -> str | None:
+ return self.__name
+
+ @property
+ def ok(self) -> bool:
+ return False
+
+ @property
+ def _error(self) -> E:
+ # This is only available on Error
+ # It is intended for pattern matching only
+ return self.__error
+
+ def error_or_none(self) -> E | None:
+ return self.__error
+
+ def error_or_raise(self, exception_class: type[E] | None = None) ->
NoReturn:
+ if exception_class is not None:
+ raise exception_class(str(self.__error)) from self.__error
+ raise self.__error
+
+ def error_type_or_none(self) -> type[E] | None:
+ return type(self.__error)
+
+ def result_or_none(self) -> R | None:
+ return None
+
+ def result_or_raise(self, exception_class: type[Exception] | None = None)
-> NoReturn:
+ if exception_class is not None:
+ raise exception_class(str(self.__error)) from self.__error
+ raise self.__error
+
+
+type Outcome[R, E: Exception = Exception] = Result[R] | Error[R, E]
+
+
+class List[R, E: Exception = Exception]:
+ __outcomes: list[Outcome[R, E]]
+
+ def __init__(self, *outcomes: Outcome[R, E]):
+ self.__outcomes = list(outcomes)
+
+ def __str__(self) -> str:
+ return f"Outcomes({self.__outcomes})"
+
+ @property
+ def any_error(self) -> bool:
+ return any((not outcome.ok) for outcome in self.__outcomes)
+
+ @property
+ def any_result(self) -> bool:
+ return any(outcome.ok for outcome in self.__outcomes)
+
+ @property
+ def all_errors(self) -> bool:
+ return all((not outcome.ok) for outcome in self.__outcomes)
+
+ @property
+ def all_results(self) -> bool:
+ return all(outcome.ok for outcome in self.__outcomes)
+
+ def append(self, outcome: Outcome[R, E]) -> None:
+ self.__outcomes.append(outcome)
+
+ def append_error(self, error: E, name: str | None = None) -> None:
+ self.__outcomes.append(Error[R, E](error, name))
+
+ def append_result(self, result: R, name: str | None = None) -> None:
+ self.__outcomes.append(Result[R](result, name))
+
+ def append_roe(self, exception_type: type[E], roe: R | E, name: str | None
= None) -> None:
+ if isinstance(roe, exception_type):
+ self.__outcomes.append(Error[R, E](roe, name))
+ elif isinstance(roe, Exception):
+ self.__outcomes.append(Error[R, E](exception_type(str(roe)), name))
+ else:
+ self.__outcomes.append(Result[R](roe, name))
+
+ @property
+ def error_count(self) -> int:
+ return sum(1 for outcome in self.__outcomes if isinstance(outcome,
Error))
+
+ def errors(self) -> list[E]:
+ errors_list = []
+ for outcome in self.__outcomes:
+ if isinstance(outcome, Error):
+ error_or_none = outcome.error_or_none()
+ if error_or_none is not None:
+ errors_list.append(error_or_none)
+ return errors_list
+
+ def errors_print(self) -> None:
+ for error in self.errors():
+ # traceback.print_exception(error)
+ print(error.__class__.__name__ + ":", error)
+
+ def extend_errors(self, errors: Sequence[E]) -> None:
+ for error in errors:
+ self.append_error(error)
+
+ def extend_results(self, results: Sequence[R]) -> None:
+ for result in results:
+ self.append_result(result)
+
+ def extend_roes(self, exception_type: type[E], roes: Sequence[R | E]) ->
None:
+ # The name "roe" is short for "result or error"
+ # It looks opaque and jargonistic, but it has an advantage when
forming plurals
+ # The long form plural is "result or errors", which is ambiguous
+ # I.e. we mean Seq[Result | Error], but it also looks like Result |
Seq[Error]
+ # The short form, however, encapsulates it so that ROE = Result | Error
+ # Then clearly the short form plural, "roes", means Seq[ROE]
+ for roe in roes:
+ self.append_roe(exception_type, roe)
+
+ def named_results(self) -> dict[str, R]:
+ named = {}
+ for outcome in self.__outcomes:
+ if (outcome.name is not None) and outcome.ok:
+ named[outcome.name] = outcome.result_or_raise()
+ return named
+
+ def names(self) -> list[str | None]:
+ return [outcome.name for outcome in self.__outcomes if (outcome.name
is not None)]
+
+ # def replace(self, a: R, b: R) -> None:
+ # for i, outcome in enumerate(self.__outcomes):
+ # if isinstance(outcome, Result):
+ # if outcome.result_or_raise() == a:
+ # self.__outcomes[i] = Result(b, outcome.name)
+
+ def results_or_raise(self, exception_type: type[Exception] | None = None)
-> list[R]:
+ return [outcome.result_or_raise(exception_type) for outcome in
self.__outcomes]
+
+ def results(self) -> list[R]:
+ return [outcome.result_or_raise() for outcome in self.__outcomes if
outcome.ok]
+
+ @property
+ def result_count(self) -> int:
+ return sum(1 for outcome in self.__outcomes if outcome.ok)
+
+ def outcomes(self) -> list[Outcome[R, E]]:
+ return self.__outcomes
+
+ def result_predicate_count(self, predicate: Callable[[R], bool]) -> int:
+ return sum(
+ 1 for outcome in self.__outcomes if isinstance(outcome, Result)
and predicate(outcome.result_or_raise())
+ )
+
+ def update_roes(self, exception_type: type[E], f: Callable[[R], R]) ->
None:
+ for i, outcome in enumerate(self.__outcomes):
+ if isinstance(outcome, Result):
+ try:
+ result = f(outcome.result_or_raise())
+ except exception_type as e:
+ self.__outcomes[i] = Error[R, E](e, outcome.name)
+ else:
+ self.__outcomes[i] = Result[R](result, outcome.name)
diff --git a/atr/storage/types.py b/atr/storage/types.py
index 7f8884e..4087674 100644
--- a/atr/storage/types.py
+++ b/atr/storage/types.py
@@ -18,226 +18,11 @@
import dataclasses
import enum
import pathlib
-from collections.abc import Callable, Sequence
-from typing import NoReturn, TypeVar
+from collections.abc import Callable
import atr.models.schema as schema
import atr.models.sql as sql
-
-# Outcome
-
-E = TypeVar("E", bound=Exception)
-T = TypeVar("T", bound=object)
-
-
-class OutcomeResult[T]:
- __match_args__ = ("_result",)
- __result: T
-
- def __init__(self, result: T, name: str | None = None):
- self.__result = result
- self.__name = name
-
- @property
- def name(self) -> str | None:
- return self.__name
-
- @property
- def ok(self) -> bool:
- return True
-
- @property
- def _result(self) -> T:
- # This is only available on OutcomeResult
- # It is intended for pattern matching only
- return self.__result
-
- def exception_or_none(self) -> Exception | None:
- return None
-
- def exception_or_raise(self, exception_class: type[Exception] | None =
None) -> NoReturn:
- if exception_class is not None:
- raise exception_class(f"Asked for exception on a result:
{self.__result}")
- raise RuntimeError(f"Asked for exception on a result: {self.__result}")
-
- def exception_type_or_none(self) -> type[Exception] | None:
- return None
-
- def result_or_none(self) -> T | None:
- return self.__result
-
- def result_or_raise(self, exception_class: type[Exception] | None = None)
-> T:
- return self.__result
-
-
-class OutcomeException[T, E: Exception = Exception]:
- __match_args__ = ("_exception",)
- __exception: E
-
- def __init__(self, exception: E, name: str | None = None):
- self.__exception = exception
- self.__name = name
-
- @property
- def name(self) -> str | None:
- return self.__name
-
- @property
- def ok(self) -> bool:
- return False
-
- @property
- def _exception(self) -> E:
- # This is only available on OutcomeException
- # It is intended for pattern matching only
- return self.__exception
-
- def exception_or_none(self) -> E | None:
- return self.__exception
-
- def exception_or_raise(self, exception_class: type[E] | None = None) ->
NoReturn:
- if exception_class is not None:
- raise exception_class(str(self.__exception)) from self.__exception
- raise self.__exception
-
- def exception_type_or_none(self) -> type[E] | None:
- return type(self.__exception)
-
- def result_or_none(self) -> T | None:
- return None
-
- def result_or_raise(self, exception_class: type[Exception] | None = None)
-> NoReturn:
- if exception_class is not None:
- raise exception_class(str(self.__exception)) from self.__exception
- raise self.__exception
-
-
-type Outcome[T, E: Exception = Exception] = OutcomeResult[T] |
OutcomeException[T, E]
-
-
-class Outcomes[T, E: Exception = Exception]:
- __outcomes: list[Outcome[T, E]]
-
- def __init__(self, *outcomes: Outcome[T, E]):
- self.__outcomes = list(outcomes)
-
- def __str__(self) -> str:
- return f"Outcomes({self.__outcomes})"
-
- @property
- def any_exception(self) -> bool:
- return any((not outcome.ok) for outcome in self.__outcomes)
-
- @property
- def any_result(self) -> bool:
- return any(outcome.ok for outcome in self.__outcomes)
-
- @property
- def all_exceptions(self) -> bool:
- return all((not outcome.ok) for outcome in self.__outcomes)
-
- @property
- def all_results(self) -> bool:
- return all(outcome.ok for outcome in self.__outcomes)
-
- def append(self, outcome: Outcome[T, E]) -> None:
- self.__outcomes.append(outcome)
-
- def append_exception(self, exception: E, name: str | None = None) -> None:
- self.__outcomes.append(OutcomeException[T, E](exception, name))
-
- def append_result(self, result: T, name: str | None = None) -> None:
- self.__outcomes.append(OutcomeResult[T](result, name))
-
- def append_roe(self, exception_type: type[E], roe: T | E, name: str | None
= None) -> None:
- if isinstance(roe, exception_type):
- self.__outcomes.append(OutcomeException[T, E](roe, name))
- elif isinstance(roe, Exception):
- self.__outcomes.append(OutcomeException[T,
E](exception_type(str(roe)), name))
- else:
- self.__outcomes.append(OutcomeResult[T](roe, name))
-
- @property
- def exception_count(self) -> int:
- return sum(1 for outcome in self.__outcomes if isinstance(outcome,
OutcomeException))
-
- def exceptions(self) -> list[E]:
- exceptions_list = []
- for outcome in self.__outcomes:
- if isinstance(outcome, OutcomeException):
- exception_or_none = outcome.exception_or_none()
- if exception_or_none is not None:
- exceptions_list.append(exception_or_none)
- return exceptions_list
-
- def exceptions_print(self) -> None:
- for exception in self.exceptions():
- # traceback.print_exception(exception)
- print(exception.__class__.__name__ + ":", exception)
-
- def extend_exceptions(self, exceptions: Sequence[E]) -> None:
- for exception in exceptions:
- self.append_exception(exception)
-
- def extend_results(self, results: Sequence[T]) -> None:
- for result in results:
- self.append_result(result)
-
- def extend_roes(self, exception_type: type[E], roes: Sequence[T | E]) ->
None:
- # The name "roe" is short for "result or exception"
- # It looks opaque and jargonistic, but it has an advantage when
forming plurals
- # The long form plural is "result or exceptions", which is ambiguous
- # I.e. we mean Seq[Result | Exception], but it also looks like Result
| Seq[Exception]
- # The short form, however, encapsulates it so that ROE = Result |
Exception
- # Then clearly the short form plural, "roes", means Seq[ROE]
- for roe in roes:
- self.append_roe(exception_type, roe)
-
- def named_results(self) -> dict[str, T]:
- named = {}
- for outcome in self.__outcomes:
- if (outcome.name is not None) and outcome.ok:
- named[outcome.name] = outcome.result_or_raise()
- return named
-
- def names(self) -> list[str | None]:
- return [outcome.name for outcome in self.__outcomes if (outcome.name
is not None)]
-
- # def replace(self, a: T, b: T) -> None:
- # for i, outcome in enumerate(self.__outcomes):
- # if isinstance(outcome, OutcomeResult):
- # if outcome.result_or_raise() == a:
- # self.__outcomes[i] = OutcomeResult(b, outcome.name)
-
- def results_or_raise(self, exception_class: type[Exception] | None = None)
-> list[T]:
- return [outcome.result_or_raise(exception_class) for outcome in
self.__outcomes]
-
- def results(self) -> list[T]:
- return [outcome.result_or_raise() for outcome in self.__outcomes if
outcome.ok]
-
- @property
- def result_count(self) -> int:
- return sum(1 for outcome in self.__outcomes if outcome.ok)
-
- def outcomes(self) -> list[Outcome[T, E]]:
- return self.__outcomes
-
- def result_predicate_count(self, predicate: Callable[[T], bool]) -> int:
- return sum(
- 1
- for outcome in self.__outcomes
- if isinstance(outcome, OutcomeResult) and
predicate(outcome.result_or_raise())
- )
-
- def update_roes(self, exception_type: type[E], f: Callable[[T], T]) ->
None:
- for i, outcome in enumerate(self.__outcomes):
- if isinstance(outcome, OutcomeResult):
- try:
- result = f(outcome.result_or_raise())
- except exception_type as e:
- self.__outcomes[i] = OutcomeException[T, E](e,
outcome.name)
- else:
- self.__outcomes[i] = OutcomeResult[T](result, outcome.name)
+import atr.storage.outcome as outcome
@dataclasses.dataclass
@@ -262,7 +47,7 @@ class Key(schema.Strict):
@dataclasses.dataclass
class LinkedCommittee:
name: str
- autogenerated_keys_file: Outcome[str]
+ autogenerated_keys_file: outcome.Outcome[str]
class PathInfo(schema.Strict):
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index 50f3a76..6b0cdce 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -38,6 +38,7 @@ import atr.db as db
import atr.log as log
import atr.models.sql as sql
import atr.storage as storage
+import atr.storage.outcome as outcome
import atr.storage.types as types
import atr.user as user
import atr.util as util
@@ -101,7 +102,7 @@ class FoundationCommitter(GeneralPublic):
self.__key_block_models_cache = {}
@performance_async
- async def delete_key(self, fingerprint: str) ->
types.Outcome[sql.PublicSigningKey]:
+ async def delete_key(self, fingerprint: str) ->
outcome.Outcome[sql.PublicSigningKey]:
try:
key = await self.__data.public_signing_key(
fingerprint=fingerprint,
@@ -115,12 +116,12 @@ class FoundationCommitter(GeneralPublic):
if wacm is None:
continue
await wacm.keys.autogenerate_keys_file()
- return types.OutcomeResult(key)
+ return outcome.Result(key)
except Exception as e:
- return types.OutcomeException(e)
+ return outcome.Error(e)
@performance_async
- async def ensure_stored_one(self, key_file_text: str) ->
types.Outcome[types.Key]:
+ async def ensure_stored_one(self, key_file_text: str) ->
outcome.Outcome[types.Key]:
return await self.__ensure_one(key_file_text, associate=False)
@performance
@@ -239,7 +240,7 @@ class FoundationCommitter(GeneralPublic):
async def __database_add_model(
self,
key: types.Key,
- ) -> types.Outcome[types.Key]:
+ ) -> outcome.Outcome[types.Key]:
via = sql.validate_instrumented_attribute
await self.__data.begin_immediate()
@@ -258,24 +259,24 @@ class FoundationCommitter(GeneralPublic):
await self.__data.commit()
# TODO: PARSED now acts as "ALREADY_ADDED"
- return types.OutcomeResult(types.Key(status=types.KeyStatus.INSERTED,
key_model=key.key_model))
+ return outcome.Result(types.Key(status=types.KeyStatus.INSERTED,
key_model=key.key_model))
@performance_async
- async def __ensure_one(self, key_file_text: str, associate: bool = True)
-> types.Outcome[types.Key]:
+ async def __ensure_one(self, key_file_text: str, associate: bool = True)
-> outcome.Outcome[types.Key]:
try:
key_blocks = util.parse_key_blocks(key_file_text)
except Exception as e:
- return types.OutcomeException(e)
+ return outcome.Error(e)
if len(key_blocks) != 1:
- return types.OutcomeException(ValueError("Expected one key block,
got none or multiple"))
+ return outcome.Error(ValueError("Expected one key block, got none
or multiple"))
key_block = key_blocks[0]
try:
ldap_data = await util.email_to_uid_map()
key = await asyncio.to_thread(self.__block_model, key_block,
ldap_data)
except Exception as e:
- return types.OutcomeException(e)
- outcome = await self.__database_add_model(key)
- return outcome
+ return outcome.Error(e)
+ oc = await self.__database_add_model(key)
+ return oc
@performance_async
async def __keys_file_format(
@@ -376,7 +377,7 @@ class CommitteeParticipant(FoundationCommitter):
self.__key_block_models_cache = {}
@performance_async
- async def associate_fingerprint(self, fingerprint: str) ->
types.Outcome[types.LinkedCommittee]:
+ async def associate_fingerprint(self, fingerprint: str) ->
outcome.Outcome[types.LinkedCommittee]:
via = sql.validate_instrumented_attribute
link_values = [{"committee_name": self.__committee_name,
"key_fingerprint": fingerprint}]
try:
@@ -392,12 +393,12 @@ class CommitteeParticipant(FoundationCommitter):
pass
await self.__data.commit()
except Exception as e:
- return types.OutcomeException(e)
+ return outcome.Error(e)
try:
autogenerated_outcome = await self.autogenerate_keys_file()
except Exception as e:
- return types.OutcomeException(e)
- return types.OutcomeResult(
+ return outcome.Error(e)
+ return outcome.Result(
types.LinkedCommittee(
name=self.__committee_name,
autogenerated_keys_file=autogenerated_outcome,
@@ -407,7 +408,7 @@ class CommitteeParticipant(FoundationCommitter):
@performance_async
async def autogenerate_keys_file(
self,
- ) -> types.Outcome[str]:
+ ) -> outcome.Outcome[str]:
try:
base_downloads_dir = util.get_downloads_dir()
@@ -421,7 +422,7 @@ class CommitteeParticipant(FoundationCommitter):
committee_keys_dir = base_downloads_dir / self.__committee_name
committee_keys_path = committee_keys_dir / "KEYS"
except Exception as e:
- return types.OutcomeException(e)
+ return outcome.Error(e)
try:
await asyncio.to_thread(committee_keys_dir.mkdir, parents=True,
exist_ok=True)
@@ -429,12 +430,12 @@ class CommitteeParticipant(FoundationCommitter):
await asyncio.to_thread(committee_keys_path.write_text,
full_keys_file_content, encoding="utf-8")
except OSError as e:
error_msg = f"Failed to write KEYS file for committee
{self.__committee_name}: {e}"
- return types.OutcomeException(storage.AccessError(error_msg))
+ return outcome.Error(storage.AccessError(error_msg))
except Exception as e:
error_msg = f"An unexpected error occurred writing KEYS for
committee {self.__committee_name}: {e}"
log.exception(f"An unexpected error occurred writing KEYS for
committee {self.__committee_name}: {e}")
- return types.OutcomeException(storage.AccessError(error_msg))
- return types.OutcomeResult(str(committee_keys_path))
+ return outcome.Error(storage.AccessError(error_msg))
+ return outcome.Result(str(committee_keys_path))
@performance_async
async def committee(self) -> sql.Committee:
@@ -447,21 +448,21 @@ class CommitteeParticipant(FoundationCommitter):
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)
+ async def ensure_associated(self, keys_file_text: str) ->
outcome.List[types.Key]:
+ outcomes: outcome.List[types.Key] = await
self.__ensure(keys_file_text, associate=True)
if outcomes.any_result:
await self.autogenerate_keys_file()
return outcomes
@performance_async
- async def ensure_stored(self, keys_file_text: str) ->
types.Outcomes[types.Key]:
- outcomes: types.Outcomes[types.Key] = await
self.__ensure(keys_file_text, associate=False)
+ async def ensure_stored(self, keys_file_text: str) ->
outcome.List[types.Key]:
+ outcomes: outcome.List[types.Key] = await
self.__ensure(keys_file_text, associate=False)
if outcomes.any_result:
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]:
+ async def import_keys_file(self, project_name: str, version_name: str) ->
outcome.List[types.Key]:
import atr.revision as revision
release = await self.__data.release(
@@ -517,8 +518,8 @@ class CommitteeParticipant(FoundationCommitter):
@performance_async
async def __database_add_models(
- self, outcomes: types.Outcomes[types.Key], associate: bool = True
- ) -> types.Outcomes[types.Key]:
+ self, outcomes: outcome.List[types.Key], associate: bool = True
+ ) -> outcome.List[types.Key]:
# Try to upsert all models and link to the committee in one transaction
try:
outcomes = await self.__database_add_models_core(outcomes,
associate=associate)
@@ -538,9 +539,9 @@ class CommitteeParticipant(FoundationCommitter):
@performance_async
async def __database_add_models_core(
self,
- outcomes: types.Outcomes[types.Key],
+ outcomes: outcome.List[types.Key],
associate: bool = True,
- ) -> types.Outcomes[types.Key]:
+ ) -> outcome.List[types.Key]:
via = sql.validate_instrumented_attribute
key_list = outcomes.results()
@@ -599,13 +600,13 @@ class CommitteeParticipant(FoundationCommitter):
return outcomes
@performance_async
- async def __ensure(self, keys_file_text: str, associate: bool = True) ->
types.Outcomes[types.Key]:
- outcomes = types.Outcomes[types.Key]()
+ async def __ensure(self, keys_file_text: str, associate: bool = True) ->
outcome.List[types.Key]:
+ outcomes = outcome.List[types.Key]()
try:
ldap_data = await util.email_to_uid_map()
key_blocks = util.parse_key_blocks(keys_file_text)
except Exception as e:
- outcomes.append_exception(e)
+ outcomes.append_error(e)
return outcomes
for key_block in key_blocks:
try:
@@ -613,7 +614,7 @@ class CommitteeParticipant(FoundationCommitter):
key_models = await asyncio.to_thread(self.__block_models,
key_block, ldap_data)
outcomes.extend_roes(Exception, key_models)
except Exception as e:
- outcomes.append_exception(e)
+ outcomes.append_error(e)
# Try adding the keys to the database
# If not, all keys will be replaced with a PostParseError
outcomes = await self.__database_add_models(outcomes,
associate=associate)
diff --git a/docs/storage-interface.html b/docs/storage-interface.html
index 549d55f..fdec4dd 100644
--- a/docs/storage-interface.html
+++ b/docs/storage-interface.html
@@ -15,10 +15,10 @@
for selected_committee_name in selected_committee_names:
wacm = write.as_committee_member(selected_committee_name)
- outcome: types.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
+ oc: types.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
key.key_model.fingerprint
)
- outcome.result_or_raise()
+ oc.result_or_raise()
</code></pre>
<p>The <code>wafm</code> (<strong>w</strong>rite <strong>a</strong>s
<strong>f</strong>oundation <strong>m</strong>ember) object exposes
functionality which is only available to foundation members. The
<code>wafm.keys.ensure_stored_one</code> method is an example of such
functionality. The <code>wacm</code> object goes further and exposes
functionality only available to committee members.</p>
<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>
diff --git a/docs/storage-interface.md b/docs/storage-interface.md
index 84f8b1a..5d64e85 100644
--- a/docs/storage-interface.md
+++ b/docs/storage-interface.md
@@ -20,10 +20,10 @@ async with storage.write(asf_uid) as write:
for selected_committee_name in selected_committee_names:
wacm = write.as_committee_member(selected_committee_name)
- outcome: types.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
+ oc: types.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
key.key_model.fingerprint
)
- outcome.result_or_raise()
+ oc.result_or_raise()
```
The `wafm` (**w**rite **a**s **f**oundation **m**ember) object exposes
functionality which is only available to foundation members. The
`wafm.keys.ensure_stored_one` method is an example of such functionality. The
`wacm` object goes further and exposes functionality only available to
committee members.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]