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]

Reply via email to