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 819651d  Migrate to the storage interface for handling KEYS upload 
submissions
819651d is described below

commit 819651d1c96fbabdfa42c74e8ad0c30c5b1f83d1
Author: Sean B. Palmer <[email protected]>
AuthorDate: Sun Jul 20 10:37:14 2025 +0100

    Migrate to the storage interface for handling KEYS upload submissions
---
 atr/blueprints/api/api.py      | 15 ++++++------
 atr/routes/keys.py             | 53 ++++++++++++++++++++++++------------------
 atr/storage/types.py           |  8 +++----
 atr/templates/keys-upload.html | 52 ++++++++++++++++++++++++-----------------
 4 files changed, 71 insertions(+), 57 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 21acfa8..291aaf1 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -387,16 +387,14 @@ async def keys_upload(data: models.api.KeysUploadArgs) -> 
DictResponse:
     asf_uid = _jwt_asf_uid()
     filetext = data.filetext
     selected_committee_name = data.committee
-    outcomes_list = []
     async with storage.write(asf_uid) as write:
         wacm = 
write.as_committee_member(selected_committee_name).writer_or_raise()
-        associated: types.KeyOutcomes = await 
wacm.keys.ensure_associated(filetext)
-        outcomes_list.append(associated)
+        outcomes: types.KeyOutcomes = await 
wacm.keys.ensure_associated(filetext)
 
         # TODO: It would be nice to serialise the actual outcomes
         api_outcomes = []
-        merged_outcomes = storage.outcomes_merge(*outcomes_list)
-        for outcome in merged_outcomes.outcomes():
+        for outcome in outcomes.outcomes():
+            api_outcome: models.api.KeysUploadOutcome | None = None
             match outcome:
                 case storage.OutcomeResult() as ocr:
                     result: types.Key = ocr.result_or_raise()
@@ -421,12 +419,13 @@ async def keys_upload(data: models.api.KeysUploadArgs) -> 
DictResponse:
                                 error=str(e),
                                 error_type=type(e).__name__,
                             )
-            api_outcomes.append(api_outcome)
+            if api_outcome is not None:
+                api_outcomes.append(api_outcome)
     return models.api.KeysUploadResults(
         endpoint="/keys/upload",
         results=api_outcomes,
-        success_count=merged_outcomes.result_count,
-        error_count=merged_outcomes.exception_count,
+        success_count=outcomes.result_count,
+        error_count=outcomes.exception_count,
         submitted_committee=selected_committee_name,
     ).model_dump(), 200
 
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index ebf869e..ba859e8 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -45,6 +45,8 @@ import atr.models.sql as sql
 import atr.revision as revision
 import atr.routes as routes
 import atr.routes.compose as compose
+import atr.storage as storage
+import atr.storage.types as types
 import atr.template as template
 import atr.user as user
 import atr.util as util
@@ -92,15 +94,15 @@ class UploadKeyFormBase(util.QuartFormTyped):
         description="Enter a URL to a KEYS file. This will be fetched by the 
server.",
     )
     submit = wtforms.SubmitField("Upload KEYS file")
-    selected_committees = wtforms.SelectMultipleField(
+    selected_committee = wtforms.SelectField(
         "Associate keys with committees",
         choices=[(c.name, c.display_name) for c in [] if (not 
util.committee_is_standing(c.name))],
         coerce=str,
-        option_widget=wtforms.widgets.CheckboxInput(),
+        option_widget=wtforms.widgets.RadioInput(),
         widget=wtforms.widgets.ListWidget(prefix_label=False),
         validators=[wtforms.validators.InputRequired("You must select at least 
one committee")],
         description=(
-            "Select the committees with which to associate these keys. You 
must be a member of the selected committees."
+            "Select the committee with which to associate these keys. You must 
be a member of the selected committee."
         ),
     )
 
@@ -524,7 +526,7 @@ async def upload(session: routes.CommitterSession) -> str:
         user_committees = await data.committee(name_in=project_list).all()
 
     class UploadKeyForm(UploadKeyFormBase):
-        selected_committees = wtforms.SelectMultipleField(
+        selected_committee = wtforms.SelectField(
             "Associate keys with committee",
             choices=[(c.name, c.display_name) for c in user_committees if (not 
util.committee_is_standing(c.name))],
             coerce=str,
@@ -538,12 +540,11 @@ async def upload(session: routes.CommitterSession) -> str:
         )
 
     form = await UploadKeyForm.create_form()
-    results: list[dict] = []
-    submitted_committees: list[str] | None = None
+    results: types.KeyOutcomes | None = None
 
     async def render(
         error: str | None = None,
-        submitted_committees_list: list[str] | None = None,
+        submitted_committees: list[str] | None = None,
         all_user_committees: Sequence[sql.Committee] | None = None,
     ) -> str:
         # For easier happy pathing
@@ -562,7 +563,7 @@ async def upload(session: routes.CommitterSession) -> str:
             form=form,
             results=results,
             algorithms=routes.algorithms,
-            submitted_committees=submitted_committees_list,
+            submitted_committees=submitted_committees,
         )
 
     if await form.validate_on_submit():
@@ -580,27 +581,22 @@ async def upload(session: routes.CommitterSession) -> str:
             return await render(error="No KEYS data found.")
 
         # Get selected committee list from the form
-        selected_committees = form.selected_committees.data
-        if not selected_committees:
+        selected_committee = form.selected_committee.data
+        if not selected_committee:
             return await render(error="You must select at least one committee")
-        # This is a KEYS file of multiple OpenPGP keys
-        # We need to parse it and add each key to the user's account
-        try:
-            upload_results, success_count, error_count, submitted_committees = 
await interaction.upload_keys(
-                project_list, keys_text, selected_committees
-            )
-        except interaction.InteractionError as e:
-            return await render(error=str(e))
-        # We use results in a closure
-        # So we have to mutate it, not replace it
-        results[:] = upload_results
+
+        outcomes = await _upload_keys(session.uid, keys_text, 
selected_committee)
+        results = outcomes
+        success_count = outcomes.result_count
+        error_count = outcomes.exception_count
+        total_count = success_count + error_count
 
         await quart.flash(
-            f"Processed {len(results)} keys: {success_count} successful, 
{error_count} failed",
+            f"Processed {total_count} keys: {success_count} successful, 
{error_count} failed",
             "success" if success_count > 0 else "error",
         )
         return await render(
-            submitted_committees_list=submitted_committees,
+            submitted_committees=[selected_committee],
             all_user_committees=user_committees,
         )
 
@@ -748,6 +744,17 @@ async def _keys_formatter(committee_name: str, data: 
db.Session) -> str:
     )
 
 
+async def _upload_keys(
+    asf_uid: str,
+    filetext: str,
+    selected_committee: str,
+) -> types.KeyOutcomes:
+    async with storage.write(asf_uid) as write:
+        wacm = write.as_committee_member(selected_committee).writer_or_raise()
+        outcomes: types.KeyOutcomes = await 
wacm.keys.ensure_associated(filetext)
+    return outcomes
+
+
 async def _write_keys_file(
     committee_keys_dir: pathlib.Path,
     full_keys_file_content: str,
diff --git a/atr/storage/types.py b/atr/storage/types.py
index 53a4423..09bf7b3 100644
--- a/atr/storage/types.py
+++ b/atr/storage/types.py
@@ -16,7 +16,6 @@
 # under the License.
 
 import enum
-from typing import TYPE_CHECKING
 
 import atr.models.schema as schema
 import atr.models.sql as sql
@@ -52,7 +51,6 @@ class PublicKeyError(Exception):
         return self.__original_error
 
 
-if TYPE_CHECKING:
-    KeyOutcomes = storage.Outcomes[Key]
-    # KeyOutcomeResult = storage.OutcomeResult[Key]
-    # KeyOutcomeError = storage.OutcomeError[Key, Exception]
+type KeyOutcomes = storage.Outcomes[Key]
+# type KeyOutcomeResult = storage.OutcomeResult[Key]
+# type KeyOutcomeError = storage.OutcomeError[Key, Exception]
diff --git a/atr/templates/keys-upload.html b/atr/templates/keys-upload.html
index d12d5d8..6dd8d7f 100644
--- a/atr/templates/keys-upload.html
+++ b/atr/templates/keys-upload.html
@@ -105,22 +105,33 @@
           </tr>
         </thead>
         <tbody>
-          {% for key_info in results %}
+          {% for outcome in results.outcomes() %}
+            {% if outcome.ok %}
+              {% set key_obj = outcome.result_or_none() %}
+              {% set fingerprint = key_obj.key_model.fingerprint %}
+              {% set email_addr = key_obj.key_model.primary_declared_uid or "" 
%}
+              {% set added_flag = key_obj.status.value > 0 %}
+              {% set error_flag = False %}
+            {% else %}
+              {% set err = outcome.exception_or_none() %}
+              {% set key_obj = err.key if (err is not none and err.key is 
defined) else None %}
+              {% set fingerprint = key_obj.key_model.fingerprint if key_obj is 
not none else "UNKNOWN" %}
+              {% set email_addr = key_obj.key_model.primary_declared_uid if 
key_obj is not none else "" %}
+              {% set added_flag = False %}
+              {% set error_flag = True %}
+            {% endif %}
             <tr>
               <td class="page-key-details px-2">
-                <code>{{ key_info.fingerprint[-16:]|upper }}</code>
+                <code>{{ fingerprint[-16:]|upper }}</code>
               </td>
-              <td class="page-key-details px-2">{{ key_info.email }}</td>
+              <td class="page-key-details px-2">{{ email_addr }}</td>
               {% for committee_name in submitted_committees %}
-                {% set status = 
key_info.committee_statuses.get(committee_name) %}
-                {% set cell_class = 'page-status-cell-error' if 
key_info.status == 'error'
-                                  else 'page-status-cell-new' if status == 
'newly_linked'
-                                  else 'page-status-cell-existing' if status 
== 'already_linked'
-                                else 'page-status-cell-unknown' %}
-                {% set title_text = 'Error processing key' if key_info.status 
== 'error'
-                                  else 'Newly linked' if status == 
'newly_linked'
-                                  else 'Already linked' if status == 
'already_linked'
-                                else 'Unknown status' %}
+                {% set cell_class = 'page-status-cell-error' if error_flag
+                                  else 'page-status-cell-new' if added_flag
+                                else 'page-status-cell-existing' %}
+                {% set title_text = 'Error processing key' if error_flag
+                                  else 'Newly linked' if added_flag
+                                else 'Already linked' %}
                 <td class="text-center align-middle 
page-status-cell-container">
                   <span class="page-status-square {{ cell_class }}" title="{{ 
title_text }}"></span>
                 </td>
@@ -131,13 +142,12 @@
       </table>
     </div>
 
-    {% set processing_errors = results | selectattr('status', 'equalto', 
'error') | list %}
+    {% set processing_errors = results.outcomes() | selectattr('ok', 
'equalto', False) | list %}
     {% if processing_errors %}
       <h3 class="text-danger mt-4">Processing errors</h3>
-      {% for error_info in processing_errors %}
-        <div class="alert alert-danger p-2 mb-3">
-          <strong>{{ error_info.key_id }} / {{ error_info.fingerprint 
}}</strong>: {{ error_info.message }}
-        </div>
+      {% for outcome in processing_errors %}
+        {% set err = outcome.exception_or_none() %}
+        <div class="alert alert-danger p-2 mb-3">{{ err }}</div>
       {% endfor %}
     {% endif %}
 
@@ -202,10 +212,10 @@
 
       {% if user_committees %}
         <div class="row mb-3 pb-3 border-bottom">
-          {{ forms.label(form.selected_committees, col="md2") }}
+          {{ forms.label(form.selected_committee, col="md2") }}
           <div class="col-md-9">
             <div class="row">
-              {% for subfield in form.selected_committees %}
+              {% for subfield in form.selected_committee %}
                 <div class="col-sm-12 col-md-6 col-lg-4">
                   <div class="form-check mb-2">
                     {{ forms.widget(subfield, classes="form-check-input") }}
@@ -216,8 +226,8 @@
                 <p class="text-muted fst-italic">No committees available for 
association.</p>
               {% endfor %}
             </div>
-            {{ forms.errors(form.selected_committees, 
classes="invalid-feedback d-block") }}
-            {{ forms.description(form.selected_committees, classes="form-text 
text-muted mt-2") }}
+            {{ forms.errors(form.selected_committee, classes="invalid-feedback 
d-block") }}
+            {{ forms.description(form.selected_committee, classes="form-text 
text-muted mt-2") }}
           </div>
         </div>
       {% else %}


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

Reply via email to