This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new b17cc7f  Allow configurations without LDAP credentials to do more
b17cc7f is described below

commit b17cc7fe6b499b8fe1ba145e9840988540428e58
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Oct 24 14:50:12 2025 +0100

    Allow configurations without LDAP credentials to do more
---
 atr/blueprints/admin/admin.py   |  8 +++++---
 atr/blueprints/api/api.py       | 22 +++++++++++++++++++++-
 atr/docs/storage-interface.html |  4 ++--
 atr/docs/storage-interface.md   |  4 ++--
 atr/models/api.py               |  8 ++++++++
 atr/principal.py                | 26 ++++++++++++++++++--------
 atr/routes/announce.py          |  2 +-
 atr/routes/compose.py           |  3 +--
 atr/routes/draft.py             | 12 ++++++------
 atr/routes/finish.py            |  6 +++---
 atr/routes/keys.py              |  2 +-
 atr/routes/preview.py           |  2 +-
 atr/routes/projects.py          | 17 ++++++++---------
 atr/routes/revisions.py         |  2 +-
 atr/routes/start.py             |  2 +-
 atr/routes/upload.py            |  2 +-
 atr/routes/vote.py              |  3 +--
 atr/routes/voting.py            |  2 +-
 atr/storage/__init__.py         | 29 +++++++++++------------------
 19 files changed, 93 insertions(+), 63 deletions(-)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 46b2fcb..e1ffa68 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -44,6 +44,7 @@ import atr.ldap as ldap
 import atr.log as log
 import atr.models.sql as sql
 import atr.principal as principal
+import atr.route as route
 import atr.routes.mapping as mapping
 import atr.storage as storage
 import atr.storage.outcome as outcome
@@ -370,7 +371,8 @@ async def admin_delete_release() -> str | response.Response:
                 await quart.flash("No releases selected for deletion.", 
"warning")
                 return 
quart.redirect(quart.url_for("admin.admin_delete_release"))
 
-            await _delete_releases(releases_to_delete, asf_uid)
+            committer_session = route.CommitterSession(web_session)
+            await _delete_releases(committer_session, releases_to_delete)
 
             # Redirecting back to the deletion page will refresh the list of 
releases too
             return quart.redirect(quart.url_for("admin.admin_delete_release"))
@@ -787,7 +789,7 @@ async def _check_keys(fix: bool = False) -> str:
     return message
 
 
-async def _delete_releases(releases_to_delete: list[str], asf_uid: str) -> 
None:
+async def _delete_releases(session: route.CommitterSession, 
releases_to_delete: list[str]) -> None:
     success_count = 0
     fail_count = 0
     error_messages = []
@@ -800,7 +802,7 @@ async def _delete_releases(releases_to_delete: list[str], 
asf_uid: str) -> None:
                 )
                 if release.committee is None:
                     raise RuntimeError(f"Release {release_name} has no 
committee")
-            async with storage.write(asf_uid) as write:
+            async with storage.write(session) as write:
                 wafa = write.as_foundation_admin(release.committee.name)
                 await wafa.release.delete(release.project.name, 
release.version)
             success_count += 1
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 83f3689..ca371f6 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -36,6 +36,7 @@ import atr.db.interaction as interaction
 import atr.jwtoken as jwtoken
 import atr.models as models
 import atr.models.sql as sql
+import atr.principal as principal
 import atr.storage as storage
 import atr.storage.outcome as outcome
 import atr.storage.types as types
@@ -1094,6 +1095,25 @@ async def tasks_list(query_args: 
models.api.TasksListQuery) -> DictResponse:
     ).model_dump(), 200
 
 
[email protected]("/user/info")
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_response(models.api.UserInfoResults, 200)
+async def user_info() -> DictResponse:
+    """
+    Get information about a user.
+    """
+    asf_uid = _jwt_asf_uid()
+    authorisation = await principal.Authorisation(asf_uid)
+    participant_of = authorisation.participant_of()
+    member_of = authorisation.member_of()
+    return models.api.UserInfoResults(
+        endpoint="/user/info",
+        participant_of=list(participant_of),
+        member_of=list(member_of),
+    ).model_dump(), 200
+
+
 @api.BLUEPRINT.route("/users/list")
 @quart_schema.validate_response(models.api.UsersListResults, 200)
 async def users_list() -> DictResponse:
@@ -1265,7 +1285,7 @@ def _jwt_asf_uid() -> str:
     claims = getattr(quart.g, "jwt_claims", {})
     asf_uid = claims.get("sub")
     if not isinstance(asf_uid, str):
-        raise base.ASFQuartException("Invalid token subject", errorcode=401)
+        raise base.ASFQuartException(f"Invalid token subject: {asf_uid!r}, 
type: {type(asf_uid)}", errorcode=401)
     return asf_uid
 
 
diff --git a/atr/docs/storage-interface.html b/atr/docs/storage-interface.html
index 98818c2..26792a5 100644
--- a/atr/docs/storage-interface.html
+++ b/atr/docs/storage-interface.html
@@ -19,12 +19,12 @@
 <p>Reading from storage is a work in progress. There are some existing 
methods, but most of the functionality is currently in <code>db</code> or 
<code>db.interaction</code>, and much work is required to migrate this to the 
storage interface. We have given this less priority because reads are generally 
safe, with the exception of a few components such as user tokens, which should 
be given greater migration priority.</p>
 <h2 id="how-do-we-write-to-storage">How do we write to storage?</h2>
 <p>To write to storage we open a write session, request specific permissions, 
use the exposed functionality, and then handle the outcome. Here is an actual 
example from <a 
href="/ref/atr/routes/start.py"><code>routes/start.py</code></a>:</p>
-<pre><code class="language-python">async with storage.write(session.uid) as 
write:
+<pre><code class="language-python">async with storage.write(session) as write:
     wacp = await write.as_project_committee_participant(project_name)
     new_release, _project = await wacp.release.start(project_name, version)
 </code></pre>
 <p>The <code>wacp</code> object, short for <code>w</code>rite <code>a</code>s 
<code>c</code>ommittee <code>p</code>articipant, provides access to 
domain-specific writers: <code>announce</code>, <code>checks</code>, 
<code>distributions</code>, <code>keys</code>, <code>policy</code>, 
<code>project</code>, <code>release</code>, <code>sbom</code>, 
<code>ssh</code>, <code>tokens</code>, and <code>vote</code>.</p>
-<p>The write session takes an optional ASF UID, typically 
<code>session.uid</code> from the logged-in user. If you omit the UID, the 
session determines it automatically from the current request context. The write 
object checks LDAP memberships and raises <a 
href="/ref/atr/storage/__init__.py:AccessError"><code>storage.AccessError</code></a>
 if the user is not authorized for the requested permission level.</p>
+<p>The write session takes an optional <a 
href="/ref/atr/route.py:CommitterSession"><code>CommitterSession</code></a> or 
ASF UID, typically <code>session.uid</code> from the logged-in user. If you 
omit the UID, the session determines it automatically from the current request 
context. The write object checks LDAP memberships and raises <a 
href="/ref/atr/storage/__init__.py:AccessError"><code>storage.AccessError</code></a>
 if the user is not authorized for the requested permission level.</p>
 <p>Because projects belong to committees, we provide <a 
href="/ref/atr/storage/__init__.py:as_project_committee_member"><code>write.as_project_committee_member(project_name)</code></a>
 and <a 
href="/ref/atr/storage/__init__.py:as_project_committee_participant"><code>write.as_project_committee_participant(project_name)</code></a>,
 which look up the project's committee and authenticate the user as a member or 
participant of that committee. This is convenient when, for example, the URL 
prov [...]
 <p>Here is a more complete example from <a 
href="/ref/atr/blueprints/api/api.py"><code>blueprints/api/api.py</code></a> 
that shows the classic three step pattern:</p>
 <pre><code class="language-python">async with storage.write(asf_uid) as write:
diff --git a/atr/docs/storage-interface.md b/atr/docs/storage-interface.md
index fe7fad4..d2def41 100644
--- a/atr/docs/storage-interface.md
+++ b/atr/docs/storage-interface.md
@@ -32,14 +32,14 @@ Reading from storage is a work in progress. There are some 
existing methods, but
 To write to storage we open a write session, request specific permissions, use 
the exposed functionality, and then handle the outcome. Here is an actual 
example from [`routes/start.py`](/ref/atr/routes/start.py):
 
 ```python
-async with storage.write(session.uid) as write:
+async with storage.write(session) as write:
     wacp = await write.as_project_committee_participant(project_name)
     new_release, _project = await wacp.release.start(project_name, version)
 ```
 
 The `wacp` object, short for `w`rite `a`s `c`ommittee `p`articipant, provides 
access to domain-specific writers: `announce`, `checks`, `distributions`, 
`keys`, `policy`, `project`, `release`, `sbom`, `ssh`, `tokens`, and `vote`.
 
-The write session takes an optional ASF UID, typically `session.uid` from the 
logged-in user. If you omit the UID, the session determines it automatically 
from the current request context. The write object checks LDAP memberships and 
raises [`storage.AccessError`](/ref/atr/storage/__init__.py:AccessError) if the 
user is not authorized for the requested permission level.
+The write session takes an optional 
[`CommitterSession`](/ref/atr/route.py:CommitterSession) or ASF UID, typically 
`session.uid` from the logged-in user. If you omit the UID, the session 
determines it automatically from the current request context. The write object 
checks LDAP memberships and raises 
[`storage.AccessError`](/ref/atr/storage/__init__.py:AccessError) if the user 
is not authorized for the requested permission level.
 
 Because projects belong to committees, we provide 
[`write.as_project_committee_member(project_name)`](/ref/atr/storage/__init__.py:as_project_committee_member)
 and 
[`write.as_project_committee_participant(project_name)`](/ref/atr/storage/__init__.py:as_project_committee_participant),
 which look up the project's committee and authenticate the user as a member or 
participant of that committee. This is convenient when, for example, the URL 
provides a project name.
 
diff --git a/atr/models/api.py b/atr/models/api.py
index 23d4dab..716fbce 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -453,6 +453,12 @@ class TasksListResults(schema.Strict):
     count: int = schema.example(10)
 
 
+class UserInfoResults(schema.Strict):
+    endpoint: Literal["/user/info"] = schema.alias("endpoint")
+    participant_of: list[str] = schema.example(["committee_name_a", 
"committee_name_b"])
+    member_of: list[str] = schema.example(["committee_name_a"])
+
+
 class UsersListResults(schema.Strict):
     endpoint: Literal["/users/list"] = schema.alias("endpoint")
     users: Sequence[str] = schema.example(["user1", "user2"])
@@ -534,6 +540,7 @@ type Results = Annotated[
     | SshKeyDeleteResults
     | SshKeysListResults
     | TasksListResults
+    | UserInfoResults
     | UsersListResults
     | VoteResolveResults
     | VoteStartResults
@@ -591,6 +598,7 @@ validate_ssh_key_add = validator(SshKeyAddResults)
 validate_ssh_key_delete = validator(SshKeyDeleteResults)
 validate_ssh_keys_list = validator(SshKeysListResults)
 validate_tasks_list = validator(TasksListResults)
+validate_user_info = validator(UserInfoResults)
 validate_users_list = validator(UsersListResults)
 validate_vote_resolve = validator(VoteResolveResults)
 validate_vote_start = validator(VoteStartResults)
diff --git a/atr/principal.py b/atr/principal.py
index ec3f75a..dd70885 100644
--- a/atr/principal.py
+++ b/atr/principal.py
@@ -27,6 +27,7 @@ import asfquart.session as session
 import atr.config as config
 import atr.ldap as ldap
 import atr.log as log
+import atr.route as route
 
 LDAP_CHAIRS_BASE = "cn=pmc-chairs,ou=groups,ou=services,dc=apache,dc=org"
 LDAP_DN = "uid=%s,ou=people,dc=apache,dc=org"
@@ -51,6 +52,15 @@ class CommitterError(Exception):
         self.origin = origin
 
 
+class ArgumentNoneType:
+    pass
+
+
+ArgumentNone = ArgumentNoneType()
+
+type UID = route.CommitterSession | str | None | ArgumentNoneType
+
+
 def attr_to_list(attr):
     """Converts a list of bytestring attribute values to a unique list of 
strings."""
     return list(set([value for value in attr or []]))
@@ -325,15 +335,15 @@ class AsyncObject:
 
 
 class Authorisation(AsyncObject):
-    class __ArgumentNoneType:
-        pass
-
-    __ArgumentNone = __ArgumentNoneType()
-
-    async def __init__(self, asf_uid: str | None | __ArgumentNoneType = 
__ArgumentNone):
+    async def __init__(self, asf_uid: UID = ArgumentNone):
         match asf_uid:
-            case Authorisation.__ArgumentNone:
-                asfquart_session = await session.read()
+            case ArgumentNoneType() | route.CommitterSession():
+                match asf_uid:
+                    case route.CommitterSession():
+                        asfquart_session = asf_uid._session
+                    case _:
+                        asfquart_session = await session.read()
+                # asfquart_session = await session.read()
                 if asfquart_session is None:
                     raise AuthenticationError("No ASFQuart session found")
                 self.__authoriser = authoriser_asfquart
diff --git a/atr/routes/announce.py b/atr/routes/announce.py
index d15909a..0fa8f8d 100644
--- a/atr/routes/announce.py
+++ b/atr/routes/announce.py
@@ -141,7 +141,7 @@ async def selected_post(
     download_path_suffix = _download_path_suffix_validated(announce_form)
 
     try:
-        async with storage.write_as_project_committee_member(project_name, 
session.uid) as wacm:
+        async with storage.write_as_project_committee_member(project_name, 
session) as wacm:
             await wacm.announce.release(
                 project_name,
                 version_name,
diff --git a/atr/routes/compose.py b/atr/routes/compose.py
index 88803b3..3fc4d77 100644
--- a/atr/routes/compose.py
+++ b/atr/routes/compose.py
@@ -55,8 +55,7 @@ async def check(
     paths = [path async for path in util.paths_recursive(base_path)]
     paths.sort()
 
-    asf_uid = session.uid if (session is not None) else None
-    async with storage.read(asf_uid) as read:
+    async with storage.read(session) as read:
         ragp = read.as_general_public()
         info = await ragp.releases.path_info(release, paths)
 
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index dcac593..265156d 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -97,7 +97,7 @@ async def delete(session: route.CommitterSession) -> 
response.Response:
     await session.check_access(project_name)
 
     # Delete the metadata from the database
-    async with storage.write(session.uid) as write:
+    async with storage.write(session) as write:
         wacp = await write.as_project_committee_participant(project_name)
         await wacp.release.delete(
             project_name, version_name, 
phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT, include_downloads=False
@@ -132,7 +132,7 @@ async def delete_file(session: route.CommitterSession, 
project_name: str, versio
     rel_path_to_delete = pathlib.Path(str(form.file_path.data))
 
     try:
-        async with storage.write(session.uid) as write:
+        async with storage.write(session) as write:
             wacp = await write.as_project_committee_participant(project_name)
             metadata_files_deleted = await 
wacp.release.delete_file(project_name, version_name, rel_path_to_delete)
     except Exception as e:
@@ -162,7 +162,7 @@ async def fresh(session: route.CommitterSession, 
project_name: str, version_name
     # This doesn't make sense unless the checks themselves have been updated
     # Therefore we only show the button for this to admins
     description = "Empty revision to restart all checks for the whole release 
candidate draft"
-    async with storage.write(session.uid) as write:
+    async with storage.write(session) as write:
         wacp = await write.as_project_committee_participant(project_name)
         async with wacp.revision.create_and_manage(
             project_name, version_name, session.uid, description=description
@@ -195,7 +195,7 @@ async def hashgen(
     rel_path = pathlib.Path(file_path)
 
     try:
-        async with storage.write(session.uid) as write:
+        async with storage.write(session) as write:
             wacp = await write.as_project_committee_participant(project_name)
             await wacp.release.generate_hash_file(project_name, version_name, 
rel_path, hash_type)
 
@@ -230,7 +230,7 @@ async def sbomgen(
 
     try:
         description = "SBOM generation through web interface"
-        async with storage.write(session.uid) as write:
+        async with storage.write(session) as write:
             wacp = await write.as_project_committee_participant(project_name)
             async with wacp.revision.create_and_manage(
                 project_name, version_name, session.uid, 
description=description
@@ -288,7 +288,7 @@ async def svnload(session: route.CommitterSession, 
project_name: str, version_na
         )
 
     try:
-        async with storage.write(session.uid) as write:
+        async with storage.write(session) as write:
             wacp = await write.as_project_committee_participant(project_name)
             await wacp.release.import_from_svn(
                 project_name,
diff --git a/atr/routes/finish.py b/atr/routes/finish.py
index 295ad9a..2388044 100644
--- a/atr/routes/finish.py
+++ b/atr/routes/finish.py
@@ -251,7 +251,7 @@ async def _delete_empty_directory(
     respond: Respond,
 ) -> tuple[quart_response.Response, int] | response.Response:
     try:
-        async with storage.write(session.uid) as write:
+        async with storage.write(session) as write:
             wacp = await write.as_project_committee_member(project_name)
             creation_error = await 
wacp.release.delete_empty_directory(project_name, version_name, 
dir_to_delete_rel)
     except Exception:
@@ -272,7 +272,7 @@ async def _move_file_to_revision(
     respond: Respond,
 ) -> tuple[quart_response.Response, int] | response.Response:
     try:
-        async with storage.write(session.uid) as write:
+        async with storage.write(session) as write:
             wacp = await write.as_project_committee_member(project_name)
             creation_error, moved_files_names, skipped_files_names = await 
wacp.release.move_file(
                 project_name, version_name, source_files_rel, target_dir_rel
@@ -313,7 +313,7 @@ async def _remove_rc_tags(
     respond: Respond,
 ) -> tuple[quart_response.Response, int] | response.Response:
     try:
-        async with storage.write(session.uid) as write:
+        async with storage.write(session) as write:
             wacp = await write.as_project_committee_member(project_name)
             creation_error, renamed_count, error_messages = await 
wacp.release.remove_rc_tags(
                 project_name, version_name
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 31770c4..9bafb90 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -359,7 +359,7 @@ async def ssh_add(session: route.CommitterSession) -> 
response.Response | str:
     if await form.validate_on_submit():
         key: str = util.unwrap(form.key.data)
         try:
-            async with storage.write(session.uid) as write:
+            async with storage.write(session) as write:
                 wafc = write.as_foundation_committer()
                 fingerprint = await wafc.ssh.add_key(key, session.uid)
         except util.SshFingerprintError as e:
diff --git a/atr/routes/preview.py b/atr/routes/preview.py
index 0d9c156..87889e7 100644
--- a/atr/routes/preview.py
+++ b/atr/routes/preview.py
@@ -101,7 +101,7 @@ async def delete(session: route.CommitterSession) -> 
response.Response:
         return await session.redirect(root.index, error="Missing required 
parameters")
 
     # Check that the user has access to the project
-    async with storage.write(session.uid) as write:
+    async with storage.write(session) as write:
         wacp = await write.as_project_committee_participant(project_name)
         await wacp.release.delete(
             project_name, version_name, 
phase=sql.ReleasePhase.RELEASE_PREVIEW, include_downloads=False
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 165e418..e3e662c 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -250,7 +250,7 @@ You must start with your committee label, and you must use 
lower case.
 """
 
     if await form.validate_on_submit():
-        return await _project_add(form, session.uid)
+        return await _project_add(form, session)
 
     return await template.render("project-add-project.html", form=form, 
committee_name=committee.display_name)
 
@@ -265,7 +265,7 @@ async def delete(session: route.CommitterSession) -> 
response.Response:
     if not project_name:
         return await session.redirect(projects, error="Missing project name 
for deletion.")
 
-    async with storage.write(session.uid) as write:
+    async with storage.write(session) as write:
         wacm = await write.as_project_committee_member(project_name)
         try:
             await wacm.project.delete(project_name)
@@ -328,16 +328,15 @@ async def view(session: route.CommitterSession, name: 
str) -> response.Response
 
     if can_edit and (quart.request.method == "POST"):
         form_data = await quart.request.form
-        asf_uid = util.unwrap(session.uid)
         if "submit_metadata" in form_data:
-            edited_metadata, metadata_form = await _metadata_edit(asf_uid, 
project, form_data)
+            edited_metadata, metadata_form = await _metadata_edit(session, 
project, form_data)
             if edited_metadata is True:
                 return quart.redirect(util.as_url(view, name=project.name))
         elif "submit_policy" in form_data:
             policy_form = await ReleasePolicyForm.create_form(data=form_data)
             if await policy_form.validate_on_submit():
                 policy_data = 
policy.ReleasePolicyData.model_validate(policy_form.data)
-                async with storage.write(asf_uid) as write:
+                async with storage.write(session) as write:
                     wacm = await 
write.as_project_committee_member(project.name)
                     try:
                         await wacm.policy.edit(project, policy_data)
@@ -407,7 +406,7 @@ async def _metadata_category_remove(
 
 
 async def _metadata_edit(
-    asf_uid: str, project: sql.Project, form_data: dict[str, str]
+    session: route.CommitterSession, project: sql.Project, form_data: 
dict[str, str]
 ) -> tuple[bool, ProjectMetadataForm]:
     metadata_form = await ProjectMetadataForm.create_form(data=form_data)
 
@@ -429,7 +428,7 @@ async def _metadata_edit(
     category_to_add = metadata_form.category_to_add.data
     language_to_add = metadata_form.language_to_add.data
 
-    async with storage.write(asf_uid) as write:
+    async with storage.write(session) as write:
         wacm = await write.as_project_committee_member(project.name)
 
         if (action_type == "add_category") and category_to_add:
@@ -508,13 +507,13 @@ async def _policy_form_create(project: sql.Project) -> 
ReleasePolicyForm:
     return policy_form
 
 
-async def _project_add(form: AddForm, asf_id: str) -> response.Response:
+async def _project_add(form: AddForm, session: route.CommitterSession) -> 
response.Response:
     form_values = await _project_add_validate(form)
     if form_values is None:
         return quart.redirect(util.as_url(add_project, 
committee_name=form.committee_name.data))
     committee_name, display_name, label = form_values
 
-    async with storage.write(asf_id) as write:
+    async with storage.write(session) as write:
         wacm = await write.as_project_committee_member(committee_name)
         try:
             await wacm.project.create(committee_name, display_name, label)
diff --git a/atr/routes/revisions.py b/atr/routes/revisions.py
index 0a23405..872026c 100644
--- a/atr/routes/revisions.py
+++ b/atr/routes/revisions.py
@@ -121,7 +121,7 @@ async def selected_post(session: route.CommitterSession, 
project_name: str, vers
             )
 
     description = f"Copy of revision {selected_revision_number} through web 
interface"
-    async with storage.write(session.uid) as write:
+    async with storage.write(session) as write:
         wacp = await write.as_project_committee_participant(project_name)
         async with wacp.revision.create_and_manage(
             project_name, version_name, session.uid, description=description
diff --git a/atr/routes/start.py b/atr/routes/start.py
index 8c9b080..99ff31a 100644
--- a/atr/routes/start.py
+++ b/atr/routes/start.py
@@ -61,7 +61,7 @@ async def selected(session: route.CommitterSession, 
project_name: str) -> respon
             project_name = str(form.project_name.data)
             version = str(form.version_name.data)
             # We already have the project, so we only need to get the new 
release
-            async with storage.write(session.uid) as write:
+            async with storage.write(session) as write:
                 wacp = await 
write.as_project_committee_participant(project_name)
                 new_release, _project = await wacp.release.start(project_name, 
version)
             # Redirect to the new draft's overview page on success
diff --git a/atr/routes/upload.py b/atr/routes/upload.py
index d2010bf..65785e5 100644
--- a/atr/routes/upload.py
+++ b/atr/routes/upload.py
@@ -74,7 +74,7 @@ async def selected(session: route.CommitterSession, 
project_name: str, version_n
                 file_name = pathlib.Path(form.file_name.data)
             file_data = form.file_data.data
 
-            async with storage.write(session.uid) as write:
+            async with storage.write(session) as write:
                 wacp = await 
write.as_project_committee_participant(project_name)
                 number_of_files = await 
wacp.release.upload_files(project_name, version_name, file_name, file_data)
             return await session.redirect(
diff --git a/atr/routes/vote.py b/atr/routes/vote.py
index d4999b2..29b9fd1 100644
--- a/atr/routes/vote.py
+++ b/atr/routes/vote.py
@@ -89,8 +89,7 @@ async def selected(
 
         # Move task_mid_get here?
         task_mid = interaction.task_mid_get(latest_vote_task)
-        asf_uid = session.uid if (session is not None) else None
-        async with storage.write(asf_uid) as write:
+        async with storage.write(session) as write:
             wagp = write.as_general_public()
             archive_url = await wagp.cache.get_message_archive_url(task_mid)
 
diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index b5c94c1..5a30456 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -117,7 +117,7 @@ async def start_vote_manual(
     session: route.CommitterSession,
     data: db.Session,
 ) -> response.Response | str:
-    async with storage.write(session.uid) as write:
+    async with storage.write(session) as write:
         wacp = await 
write.as_project_committee_participant(release.project_name)
         # This verifies the state and sets the phase to RELEASE_CANDIDATE
         error = await wacp.release.promote_to_candidate(release.name, 
selected_revision_number, vote_manual=True)
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index 39e6f49..cd77e89 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -397,16 +397,9 @@ class Write:
 # Context managers
 
 
-class ArgumentNoneType:
-    pass
-
-
-ArgumentNone = ArgumentNoneType()
-
-
 @contextlib.asynccontextmanager
-async def read(asf_uid: str | None | ArgumentNoneType = ArgumentNone) -> 
AsyncGenerator[Read]:
-    if asf_uid is ArgumentNone:
+async def read(asf_uid: principal.UID = principal.ArgumentNone) -> 
AsyncGenerator[Read]:
+    if asf_uid is principal.ArgumentNone:
         authorisation = await principal.Authorisation()
     else:
         authorisation = await principal.Authorisation(asf_uid)
@@ -417,7 +410,7 @@ async def read(asf_uid: str | None | ArgumentNoneType = 
ArgumentNone) -> AsyncGe
 
 @contextlib.asynccontextmanager
 async def read_as_foundation_committer(
-    asf_uid: str | None | ArgumentNoneType = ArgumentNone,
+    asf_uid: principal.UID = principal.ArgumentNone,
 ) -> AsyncGenerator[ReadAsFoundationCommitter]:
     async with read(asf_uid) as r:
         yield r.as_foundation_committer()
@@ -425,15 +418,15 @@ async def read_as_foundation_committer(
 
 @contextlib.asynccontextmanager
 async def read_as_general_public(
-    asf_uid: str | None | ArgumentNoneType = ArgumentNone,
+    asf_uid: principal.UID = principal.ArgumentNone,
 ) -> AsyncGenerator[ReadAsGeneralPublic]:
     async with read(asf_uid) as r:
         yield r.as_general_public()
 
 
 @contextlib.asynccontextmanager
-async def read_and_write(asf_uid: str | None | ArgumentNoneType = 
ArgumentNone) -> AsyncGenerator[tuple[Read, Write]]:
-    if asf_uid is ArgumentNone:
+async def read_and_write(asf_uid: principal.UID = principal.ArgumentNone) -> 
AsyncGenerator[tuple[Read, Write]]:
+    if asf_uid is principal.ArgumentNone:
         authorisation = await principal.Authorisation()
     else:
         authorisation = await principal.Authorisation(asf_uid)
@@ -445,8 +438,8 @@ async def read_and_write(asf_uid: str | None | 
ArgumentNoneType = ArgumentNone)
 
 
 @contextlib.asynccontextmanager
-async def write(asf_uid: str | None | ArgumentNoneType = ArgumentNone) -> 
AsyncGenerator[Write]:
-    if asf_uid is ArgumentNone:
+async def write(asf_uid: principal.UID = principal.ArgumentNone) -> 
AsyncGenerator[Write]:
+    if asf_uid is principal.ArgumentNone:
         authorisation = await principal.Authorisation()
     else:
         authorisation = await principal.Authorisation(asf_uid)
@@ -458,7 +451,7 @@ async def write(asf_uid: str | None | ArgumentNoneType = 
ArgumentNone) -> AsyncG
 @contextlib.asynccontextmanager
 async def write_as_committee_member(
     committee_name: str,
-    asf_uid: str | None | ArgumentNoneType = ArgumentNone,
+    asf_uid: principal.UID = principal.ArgumentNone,
 ) -> AsyncGenerator[WriteAsCommitteeMember]:
     async with write(asf_uid) as w:
         yield w.as_committee_member(committee_name)
@@ -467,7 +460,7 @@ async def write_as_committee_member(
 @contextlib.asynccontextmanager
 async def write_as_committee_participant(
     committee_name: str,
-    asf_uid: str | None | ArgumentNoneType = ArgumentNone,
+    asf_uid: principal.UID = principal.ArgumentNone,
 ) -> AsyncGenerator[WriteAsCommitteeParticipant]:
     async with write(asf_uid) as w:
         yield w.as_committee_participant(committee_name)
@@ -476,7 +469,7 @@ async def write_as_committee_participant(
 @contextlib.asynccontextmanager
 async def write_as_project_committee_member(
     project_name: str,
-    asf_uid: str | None | ArgumentNoneType = ArgumentNone,
+    asf_uid: principal.UID = principal.ArgumentNone,
 ) -> AsyncGenerator[WriteAsCommitteeMember]:
     async with write(asf_uid) as w:
         yield await w.as_project_committee_member(project_name)


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

Reply via email to