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 69fd3e3 Make the distribution forms more type safe
69fd3e3 is described below
commit 69fd3e3fa9f74dad7ba25e777abec564f04eb88d
Author: Andrew Musselman <[email protected]>
AuthorDate: Tue Nov 11 12:01:29 2025 -0800
Make the distribution forms more type safe
---
atr/form.py | 23 +++-
atr/get/distribution.py | 88 ++++++++----
atr/models/distribution.py | 15 ---
atr/post/distribution.py | 115 ++++++++++------
atr/shared/distribution.py | 252 ++++++++++++-----------------------
atr/storage/writers/distributions.py | 6 +-
6 files changed, 247 insertions(+), 252 deletions(-)
diff --git a/atr/form.py b/atr/form.py
index bff8cd2..8bdca3b 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -20,6 +20,7 @@ from __future__ import annotations
import enum
import json
import pathlib
+import re
import types
from typing import TYPE_CHECKING, Annotated, Any, Final, Literal,
TypeAliasType, get_args, get_origin
@@ -44,6 +45,8 @@ if TYPE_CHECKING:
DISCRIMINATOR_NAME: Final[str] = "variant"
DISCRIMINATOR: Final[Any] = schema.discriminator(DISCRIMINATOR_NAME)
+_CONFIRM_PATTERN = re.compile(r"^[A-Za-z0-9 .,!?-]+$")
+
class Form(schema.Form):
pass
@@ -257,6 +260,7 @@ def render( # noqa: C901
border: bool = False,
wider_widgets: bool = False,
skip: list[str] | None = None,
+ confirm: str | None = None,
) -> htm.Element:
if action is None:
action = quart.request.path
@@ -320,7 +324,17 @@ def render( # noqa: C901
unused = ", ".join(custom.keys())
raise ValueError(f"Custom widgets provided but not used: {unused}")
- return htm.form(form_classes, action=action, method="post",
enctype="multipart/form-data")[form_children]
+ form_attrs: dict[str, str] = {
+ "action": action,
+ "method": "post",
+ "enctype": "multipart/form-data",
+ }
+ if confirm:
+ if not _CONFIRM_PATTERN.match(confirm):
+ raise ValueError(f"Invalid characters in confirm message:
{confirm!r}")
+ form_attrs["onsubmit"] = f"return confirm('{confirm}');"
+
+ return htm.form(form_classes, **form_attrs)[form_children]
def render_block(block: htm.Block, *args, **kwargs) -> None:
@@ -731,7 +745,7 @@ def _render_field_value(
return field_value
-def _render_row(
+def _render_row( # noqa: C901
field_info: pydantic.fields.FieldInfo,
field_name: str,
flash_error_data: dict[str, Any],
@@ -761,7 +775,10 @@ def _render_row(
if widget_type == Widget.HIDDEN:
attrs = {"type": "hidden", "name": field_name, "id": field_name}
if field_value is not None:
- attrs["value"] = str(field_value)
+ if isinstance(field_value, enum.Enum):
+ attrs["value"] = field_value.value
+ else:
+ attrs["value"] = str(field_value)
return htpy.input(**attrs), None
label_text = field_info.description or field_name.replace("_", " ").title()
diff --git a/atr/get/distribution.py b/atr/get/distribution.py
index 971be34..8e12cd4 100644
--- a/atr/get/distribution.py
+++ b/atr/get/distribution.py
@@ -15,9 +15,10 @@
# specific language governing permissions and limitations
# under the License.
+
import atr.blueprints.get as get
import atr.db as db
-import atr.forms as forms
+import atr.form as form
import atr.htm as htm
import atr.models.sql as sql
import atr.post as post
@@ -66,16 +67,6 @@ async def list_get(session: web.Committer, project: str,
version: str) -> str:
## Distributions
block.h2["Distributions"]
for dist in distributions:
- delete_form = await shared.distribution.DeleteForm.create_form(
- data={
- "release_name": dist.release_name,
- "platform": dist.platform.name,
- "owner_namespace": dist.owner_namespace,
- "package": dist.package,
- "version": dist.version,
- }
- )
-
### Platform package version
block.h3(
# Cannot use "#id" here, because the ID contains "."
@@ -94,13 +85,28 @@ async def list_get(session: web.Committer, project: str,
version: str) -> str:
shared.distribution.html_tr_a("Web URL", dist.web_url),
]
block.table(".table.table-striped.table-bordered")[tbody]
- form_action = util.as_url(post.distribution.delete, project=project,
version=version)
- delete_form_element = forms.render_simple(
- delete_form,
- action=form_action,
- submit_classes="btn-danger",
+
+ delete_form = form.render(
+ model_cls=shared.distribution.DeleteForm,
+ action=util.as_url(post.distribution.delete, project=project,
version=version),
+ form_classes=".d-inline-block.m-0",
+ submit_classes="btn-danger btn-sm",
+ submit_label="Delete",
+ empty=True,
+ defaults={
+ "release_name": dist.release_name,
+ "platform":
shared.distribution.DistributionPlatform.from_sql(dist.platform),
+ "owner_namespace": dist.owner_namespace or "",
+ "package": dist.package,
+ "version": dist.version,
+ },
+ confirm=(
+ f"Are you sure you want to delete the distribution "
+ f"{dist.platform.name} {dist.package} {dist.version}? "
+ f"This cannot be undone."
+ ),
)
- block.append(htm.div(".mb-3")[delete_form_element])
+ block.append(htm.div(".mb-3")[delete_form])
title = f"Distribution list for {project} {version}"
return await template.blank(title, content=block.collect())
@@ -108,13 +114,49 @@ async def list_get(session: web.Committer, project: str,
version: str) -> str:
@get.committer("/distribution/record/<project>/<version>")
async def record(session: web.Committer, project: str, version: str) -> str:
- form = await
shared.distribution.DistributeForm.create_form(data={"package": project,
"version": version})
- fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
- return await shared.distribution.record_form_page(fpv)
+ return await _record_form_page(project, version, staging=False)
@get.committer("/distribution/stage/<project>/<version>")
async def stage(session: web.Committer, project: str, version: str) -> str:
- form = await
shared.distribution.DistributeForm.create_form(data={"package": project,
"version": version})
- fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
- return await shared.distribution.record_form_page(fpv, staging=True)
+ return await _record_form_page(project, version, staging=True)
+
+
+async def _record_form_page(project: str, version: str, staging: bool) -> str:
+ """Helper to render the distribution recording form page."""
+ await shared.distribution.release_validated(project, version,
staging=staging)
+
+ block = htm.Block()
+ shared.distribution.html_nav_phase(block, project, version,
staging=staging)
+
+ title = "Record a staging distribution" if staging else "Record a manual
distribution"
+ block.h1[title]
+
+ block.p[
+ "Record a distribution of ",
+ htm.strong[f"{project}-{version}"],
+ " using the form below.",
+ ]
+ block.p[
+ "You can also ",
+ htm.a(href=util.as_url(list_get, project=project,
version=version))["view the distribution list"],
+ ".",
+ ]
+
+ # Determine the action based on staging
+ action = (
+ util.as_url(post.distribution.stage_selected, project=project,
version=version)
+ if staging
+ else util.as_url(post.distribution.record_selected, project=project,
version=version)
+ )
+
+ # Render the distribution form
+ form_html = form.render(
+ model_cls=shared.distribution.DistributeForm,
+ submit_label="Record distribution",
+ action=action,
+ defaults={"package": project, "version": version},
+ )
+ block.append(form_html)
+
+ return await template.blank(title, content=block.collect())
diff --git a/atr/models/distribution.py b/atr/models/distribution.py
index 2c54216..9edf4b3 100644
--- a/atr/models/distribution.py
+++ b/atr/models/distribution.py
@@ -87,21 +87,6 @@ class PyPIResponse(schema.Lax):
info: PyPIInfo = pydantic.Field(default_factory=PyPIInfo)
-class DeleteData(schema.Lax):
- release_name: str
- platform: sql.DistributionPlatform
- owner_namespace: str
- package: str
- version: str
-
- @pydantic.field_validator("platform", mode="before")
- @classmethod
- def coerce_platform(cls, v: object) -> object:
- if isinstance(v, str):
- return sql.DistributionPlatform[v]
- return v
-
-
# Lax to ignore csrf_token and submit
# WTForms types platform as Any, which is insufficient
# And this way we also get nice JSON from the Pydantic model dump
diff --git a/atr/post/distribution.py b/atr/post/distribution.py
index 132cac3..6194d61 100644
--- a/atr/post/distribution.py
+++ b/atr/post/distribution.py
@@ -17,8 +17,6 @@
from __future__ import annotations
-import quart
-
import atr.blueprints.post as post
import atr.db as db
import atr.get as get
@@ -29,25 +27,29 @@ import atr.web as web
@post.committer("/distribution/delete/<project>/<version>")
-async def delete(session: web.Committer, project: str, version: str) ->
web.WerkzeugResponse:
- form = await shared.distribution.DeleteForm.create_form(data=await
quart.request.form)
- dd = distribution.DeleteData.model_validate(form.data)
[email protected](shared.distribution.DeleteForm)
+async def delete(
+ session: web.Committer, delete_form: shared.distribution.DeleteForm,
project: str, version: str
+) -> web.WerkzeugResponse:
+ sql_platform = delete_form.platform.to_sql() # type: ignore[attr-defined]
# Validate the submitted data, and obtain the committee for its name
async with db.session() as data:
- release = await
data.release(name=dd.release_name).demand(RuntimeError(f"Release
{dd.release_name} not found"))
- committee = release.committee
- if committee is None:
- raise RuntimeError(f"Release {dd.release_name} has no committee")
+ release = await data.release(name=delete_form.release_name).demand(
+ RuntimeError(f"Release {delete_form.release_name} not found")
+ )
+ committee = release.committee
+ if committee is None:
+ raise RuntimeError(f"Release {delete_form.release_name} has no
committee")
# Delete the distribution
async with
storage.write_as_committee_member(committee_name=committee.name) as wacm:
await wacm.distributions.delete_distribution(
- release_name=dd.release_name,
- platform=dd.platform,
- owner_namespace=dd.owner_namespace,
- package=dd.package,
- version=dd.version,
+ release_name=delete_form.release_name,
+ platform=sql_platform,
+ owner_namespace=delete_form.owner_namespace,
+ package=delete_form.package,
+ version=delete_form.version,
)
return await session.redirect(
get.distribution.list_get,
@@ -58,33 +60,64 @@ async def delete(session: web.Committer, project: str,
version: str) -> web.Werk
@post.committer("/distribution/record/<project>/<version>")
-async def record_post(session: web.Committer, project: str, version: str) ->
str:
- form = await shared.distribution.DistributeForm.create_form(data=await
quart.request.form)
- fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
- if await form.validate():
- return await shared.distribution.record_form_process_page(fpv)
- match len(form.errors):
- case 0:
- # Should not happen
- await quart.flash("Ambiguous submission errors",
category="warning")
- case 1:
- await quart.flash("There was 1 submission error", category="error")
- case _ as n:
- await quart.flash(f"There were {n} submission errors",
category="error")
- return await shared.distribution.record_form_page(fpv)
[email protected](shared.distribution.DistributeForm)
+async def record_selected(
+ session: web.Committer, distribute_form:
shared.distribution.DistributeForm, project: str, version: str
+) -> web.WerkzeugResponse:
+ return await record_form_process_page(session, distribute_form, project,
version, staging=False)
@post.committer("/distribution/stage/<project>/<version>")
-async def stage_post(session: web.Committer, project: str, version: str) ->
str:
- form = await shared.distribution.DistributeForm.create_form(data=await
quart.request.form)
- fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
- if await form.validate():
- return await shared.distribution.record_form_process_page(fpv,
staging=True)
- match len(form.errors):
- case 0:
- await quart.flash("Ambiguous submission errors",
category="warning")
- case 1:
- await quart.flash("There was 1 submission error", category="error")
- case _ as n:
- await quart.flash(f"There were {n} submission errors",
category="error")
- return await shared.distribution.record_form_page(fpv, staging=True)
[email protected](shared.distribution.DistributeForm)
+async def stage_selected(
+ session: web.Committer, distribute_form:
shared.distribution.DistributeForm, project: str, version: str
+) -> web.WerkzeugResponse:
+ return await record_form_process_page(session, distribute_form, project,
version, staging=True)
+
+
+async def record_form_process_page(
+ session: web.Committer,
+ form_data: shared.distribution.DistributeForm,
+ project: str,
+ version: str,
+ /,
+ staging: bool = False,
+) -> web.WerkzeugResponse:
+ sql_platform = form_data.platform.to_sql() # type: ignore[attr-defined]
+ dd = distribution.Data(
+ platform=sql_platform,
+ owner_namespace=form_data.owner_namespace,
+ package=form_data.package,
+ version=form_data.version,
+ details=form_data.details,
+ )
+ release, committee = await
shared.distribution.release_validated_and_committee(
+ project,
+ version,
+ staging=staging,
+ )
+
+ async with
storage.write_as_committee_member(committee_name=committee.name) as w:
+ try:
+ _dist, added, _metadata = await w.distributions.record_from_data(
+ release=release,
+ staging=staging,
+ dd=dd,
+ )
+ except storage.AccessError as e:
+ # Instead of calling record_form_page_new, redirect with error
message
+ return await session.redirect(
+ get.distribution.stage if staging else get.distribution.record,
+ project=project,
+ version=version,
+ error=str(e),
+ )
+
+ # Success - redirect to distribution list with success message
+ message = "Distribution recorded successfully." if added else
"Distribution was already recorded."
+ return await session.redirect(
+ get.distribution.list_get,
+ project=project,
+ version=version,
+ success=message,
+ )
diff --git a/atr/shared/distribution.py b/atr/shared/distribution.py
index b84ca13..3a12aff 100644
--- a/atr/shared/distribution.py
+++ b/atr/shared/distribution.py
@@ -17,74 +17,105 @@
from __future__ import annotations
-import dataclasses
-import json
+import enum
from typing import Literal
-import quart
+import pydantic
import atr.db as db
-import atr.forms as forms
+import atr.form as form
import atr.get as get
import atr.htm as htm
import atr.models.distribution as distribution
import atr.models.sql as sql
-import atr.storage as storage
-import atr.template as template
import atr.util as util
type Phase = Literal["COMPOSE", "VOTE", "FINISH"]
-class DeleteForm(forms.Typed):
- release_name = forms.hidden()
- platform = forms.hidden()
- owner_namespace = forms.hidden()
- package = forms.hidden()
- version = forms.hidden()
- submit = forms.submit("Delete")
-
-
-class DistributeForm(forms.Typed):
- platform = forms.select("Platform", choices=sql.DistributionPlatform)
- owner_namespace = forms.optional(
+class DistributionPlatform(enum.Enum):
+ """Wrapper enum for distribution platforms."""
+
+ ARTIFACT_HUB = "Artifact Hub"
+ DOCKER_HUB = "Docker Hub"
+ MAVEN = "Maven Central"
+ NPM = "npm"
+ NPM_SCOPED = "npm (scoped)"
+ PYPI = "PyPI"
+
+ def to_sql(self) -> sql.DistributionPlatform:
+ """Convert to SQL enum."""
+ match self:
+ case DistributionPlatform.ARTIFACT_HUB:
+ return sql.DistributionPlatform.ARTIFACT_HUB
+ case DistributionPlatform.DOCKER_HUB:
+ return sql.DistributionPlatform.DOCKER_HUB
+ case DistributionPlatform.MAVEN:
+ return sql.DistributionPlatform.MAVEN
+ case DistributionPlatform.NPM:
+ return sql.DistributionPlatform.NPM
+ case DistributionPlatform.NPM_SCOPED:
+ return sql.DistributionPlatform.NPM_SCOPED
+ case DistributionPlatform.PYPI:
+ return sql.DistributionPlatform.PYPI
+
+ @classmethod
+ def from_sql(cls, platform: sql.DistributionPlatform) ->
DistributionPlatform:
+ """Convert from SQL enum."""
+ match platform:
+ case sql.DistributionPlatform.ARTIFACT_HUB:
+ return cls.ARTIFACT_HUB
+ case sql.DistributionPlatform.DOCKER_HUB:
+ return cls.DOCKER_HUB
+ case sql.DistributionPlatform.MAVEN:
+ return cls.MAVEN
+ case sql.DistributionPlatform.NPM:
+ return cls.NPM
+ case sql.DistributionPlatform.NPM_SCOPED:
+ return cls.NPM_SCOPED
+ case sql.DistributionPlatform.PYPI:
+ return cls.PYPI
+
+
+class DeleteForm(form.Form):
+ release_name: str = form.label("Release name", widget=form.Widget.HIDDEN)
+ platform: form.Enum[DistributionPlatform] = form.label("Platform",
widget=form.Widget.HIDDEN)
+ owner_namespace: str = form.label("Owner namespace",
widget=form.Widget.HIDDEN)
+ package: str = form.label("Package", widget=form.Widget.HIDDEN)
+ version: str = form.label("Version", widget=form.Widget.HIDDEN)
+
+
+class DistributeForm(form.Form):
+ platform: form.Enum[DistributionPlatform] = form.label("Platform",
widget=form.Widget.SELECT)
+ owner_namespace: str = form.label(
"Owner or Namespace",
- placeholder="E.g. com.example or scope or library",
- description="Who owns or names the package (Maven groupId, npm @scope,
"
- "Docker namespace, GitHub owner, ArtifactHub repo). Leave blank if not
used.",
+ "Who owns or names the package (Maven groupId, npm @scope, Docker
namespace, "
+ "GitHub owner, ArtifactHub repo). Leave blank if not used.",
)
- package = forms.string("Package", placeholder="E.g. artifactId or
package-name")
- version = forms.string("Version", placeholder="E.g. 1.2.3, without a
leading v")
- details = forms.checkbox("Include details", description="Include the
details of the distribution in the response")
- submit = forms.submit("Record distribution")
-
- async def validate(self, extra_validators: dict | None = None) -> bool:
- if not await super().validate(extra_validators):
- return False
- if not self.platform.data:
- return False
- default_owner_namespace =
self.platform.data.value.default_owner_namespace
- requires_owner_namespace =
self.platform.data.value.requires_owner_namespace
- owner_namespace = self.owner_namespace.data
- # TODO: We should disable the owner_namespace field if it's not
required
- # But that would be a lot of complexity
- # And this validation, which we need to keep, is complex enough
- if default_owner_namespace and (not owner_namespace):
- self.owner_namespace.data = default_owner_namespace
- if requires_owner_namespace and (not owner_namespace):
- msg = f'Platform "{self.platform.data.name}" requires an owner or
namespace.'
- return forms.error(self.owner_namespace, msg)
- if (not requires_owner_namespace) and (not default_owner_namespace)
and owner_namespace:
- msg = f'Platform "{self.platform.data.name}" does not require an
owner or namespace.'
- return forms.error(self.owner_namespace, msg)
- return True
-
-
[email protected]
-class FormProjectVersion:
- form: DistributeForm
- project: str
- version: str
+ package: str = form.label("Package")
+ version: str = form.label("Version")
+ details: form.Bool = form.label(
+ "Include details",
+ "Include the details of the distribution in the response",
+ )
+
+ @pydantic.model_validator(mode="after")
+ def validate_owner_namespace(self) -> DistributeForm:
+ platform_name: str = self.platform.name # type: ignore[attr-defined]
+ sql_platform = self.platform.to_sql() # type: ignore[attr-defined]
+ default_owner_namespace = sql_platform.value.default_owner_namespace
+ requires_owner_namespace = sql_platform.value.requires_owner_namespace
+
+ if default_owner_namespace and (not self.owner_namespace):
+ self.owner_namespace = default_owner_namespace
+
+ if requires_owner_namespace and (not self.owner_namespace):
+ raise ValueError(f'Platform "{platform_name}" requires an owner or
namespace.')
+
+ if (not requires_owner_namespace) and (not default_owner_namespace)
and self.owner_namespace:
+ raise ValueError(f'Platform "{platform_name}" does not require an
owner or namespace.')
+
+ return self
# TODO: Move this to an appropriate module
@@ -155,121 +186,6 @@ def html_tr_a(label: str, value: str | None) ->
htm.Element:
return htm.tr[htm.th[label], htm.td[htm.a(href=value)[value] if value else
"-"]]
-# This function is used for COMPOSE (stage) and FINISH (record)
-# It's also used whenever there is an error
-async def record_form_page(
- fpv: FormProjectVersion, *, extra_content: htm.Element | None = None,
staging: bool = False
-) -> str:
- await release_validated(fpv.project, fpv.version, staging=staging)
-
- # Render the explanation and form
- block = htm.Block()
- html_nav_phase(block, fpv.project, fpv.version, staging)
-
- # Record a manual distribution
- title_and_heading = f"Record a {'staging' if staging else 'manual'}
distribution"
- block.h1[title_and_heading]
- if extra_content:
- block.append(extra_content)
- block.p[
- "Record a distribution of ",
- htm.strong[f"{fpv.project}-{fpv.version}"],
- " using the form below.",
- ]
- block.p[
- "You can also ",
- htm.a(href=util.as_url(get.distribution.list_get, project=fpv.project,
version=fpv.version))[
- "view the distribution list"
- ],
- ".",
- ]
- block.append(forms.render_columns(fpv.form, action=quart.request.path,
descriptions=True))
-
- # Render the page
- return await template.blank(title_and_heading, content=block.collect())
-
-
-async def record_form_process_page(fpv: FormProjectVersion, /, staging: bool =
False) -> str:
- dd = distribution.Data.model_validate(fpv.form.data)
- release, committee = await release_validated_and_committee(
- fpv.project,
- fpv.version,
- staging=staging,
- )
-
- # In case of error, show an alert
- async def _alert(message: str) -> str:
- div = htm.Block(htm.div(".alert.alert-danger"))
- div.p[message]
- collected = div.collect()
- return await record_form_page(fpv, extra_content=collected,
staging=staging)
-
- async with
storage.write_as_committee_member(committee_name=committee.name) as w:
- try:
- dist, added, metadata = await w.distributions.record_from_data(
- release=release,
- staging=staging,
- dd=dd,
- )
- except storage.AccessError as e:
- return await _alert(str(e))
-
- block = htm.Block()
-
- # Distribution submitted
- block.h1["Distribution recorded"]
-
- ## Record
- block.h2["Record"]
- if added:
- block.p["The distribution was recorded successfully."]
- else:
- block.p["The distribution was already recorded."]
- block.table(".table.table-striped.table-bordered")[
- htm.tbody[
- html_tr("Release name", dist.release_name),
- html_tr("Platform", dist.platform.name),
- html_tr("Owner or Namespace", dist.owner_namespace or "-"),
- html_tr("Package", dist.package),
- html_tr("Version", dist.version),
- html_tr("Staging", "Yes" if dist.staging else "No"),
- html_tr("Upload date", str(dist.upload_date)),
- html_tr_a("API URL", dist.api_url),
- html_tr_a("Web URL", dist.web_url),
- ]
- ]
- block.p[
- htm.a(href=util.as_url(get.distribution.list_get, project=fpv.project,
version=fpv.version))[
- "Back to distribution list"
- ],
- ]
-
- if dd.details:
- ## Details
- block.h2["Details"]
-
- ### Submitted values
- block.h3["Submitted values"]
- html_submitted_values_table(block, dd)
-
- ### As JSON
- block.h3["As JSON"]
- block.pre(".mb-3")[dd.model_dump_json(indent=2)]
-
- ### API URL
- block.h3["API URL"]
- block.pre(".mb-3")[metadata.api_url]
-
- ### API response
- block.h3["API response"]
- block.details[
- htm.summary["Show full API response"],
- htm.pre(".atr-pre-wrap.mb-3")[json.dumps(metadata.result,
indent=2)],
- ]
-
- return await template.blank("Distribution submitted",
content=block.collect())
-
-
async def release_validated_and_committee(
project: str,
version: str,
diff --git a/atr/storage/writers/distributions.py
b/atr/storage/writers/distributions.py
index 7845d94..2db5d85 100644
--- a/atr/storage/writers/distributions.py
+++ b/atr/storage/writers/distributions.py
@@ -25,6 +25,7 @@ import aiohttp
import sqlalchemy.exc as exc
import atr.db as db
+import atr.log as log
import atr.models.basic as basic
import atr.models.distribution as distribution
import atr.models.sql as sql
@@ -161,8 +162,9 @@ class CommitteeMember(CommitteeParticipant):
match api_oc:
case outcome.Result(result):
pass
- case outcome.Error():
- raise storage.AccessError("Failed to get API response from
distribution platform")
+ case outcome.Error(error):
+ log.error(f"Failed to get API response from {api_url}:
{error}")
+ raise storage.AccessError(f"Failed to get API response from
distribution platform: {error}")
upload_date = self.__distribution_upload_date(dd.platform, result,
dd.version)
if upload_date is None:
raise storage.AccessError("Failed to get upload date from
distribution platform")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]