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]