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 7d7275e Record distributions in the database
7d7275e is described below
commit 7d7275e54b1405250bf28067d7853b493e5c4bee
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Aug 7 16:29:59 2025 +0100
Record distributions in the database
---
atr/models/sql.py | 2 +-
atr/routes/distribute.py | 145 ++++++++++++++++--------
migrations/versions/0019_2025.08.07_279ca4a9.py | 36 ++++++
3 files changed, 135 insertions(+), 48 deletions(-)
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 8431679..ed1750a 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -820,7 +820,7 @@ class CheckResultIgnore(sqlmodel.SQLModel, table=True):
# Distribution: Release
class Distribution(sqlmodel.SQLModel, table=True):
id: int = sqlmodel.Field(default=None, primary_key=True)
- release_name: str = sqlmodel.Field(foreign_key="release.name")
+ release_name: str = sqlmodel.Field(foreign_key="release.name",
ondelete="CASCADE")
release: Release = sqlmodel.Relationship(back_populates="distributions")
platform: DistributionPlatform =
sqlmodel.Field(default=DistributionPlatform.ARTIFACTHUB)
owner_namespace: str | None = sqlmodel.Field(default=None)
diff --git a/atr/routes/distribute.py b/atr/routes/distribute.py
index 00ad36b..01d0680 100644
--- a/atr/routes/distribute.py
+++ b/atr/routes/distribute.py
@@ -17,6 +17,7 @@
from __future__ import annotations
+import dataclasses
import datetime
import json
@@ -32,6 +33,7 @@ import atr.models.basic as basic
import atr.models.schema as schema
import atr.models.sql as sql
import atr.routes as routes
+import atr.storage as storage
import atr.storage.outcome as outcome
import atr.template as template
@@ -107,6 +109,13 @@ class DistributeForm(forms.Typed):
return True
[email protected]
+class FormProjectVersion:
+ form: DistributeForm
+ project: str
+ version: str
+
+
# 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
@@ -127,14 +136,16 @@ class DistributeData(schema.Lax):
@routes.committer("/distribute/<project>/<version>", methods=["GET"])
async def distribute(session: routes.CommitterSession, project: str, version:
str) -> str:
form = await DistributeForm.create_form(data={"package": project,
"version": version})
- return await _distribute_page(project=project, version=version, form=form)
+ fpv = FormProjectVersion(form=form, project=project, version=version)
+ return await _distribute_page(fpv)
@routes.committer("/distribute/<project>/<version>", methods=["POST"])
async def distribute_post(session: routes.CommitterSession, project: str,
version: str) -> str:
form = await DistributeForm.create_form(data=await quart.request.form)
+ fpv = FormProjectVersion(form=form, project=project, version=version)
if await form.validate():
- return await _distribute_post_validated(form, project, version)
+ return await _distribute_post_validated(fpv)
match len(form.errors):
case 0:
# Should not happen
@@ -143,22 +154,13 @@ async def distribute_post(session:
routes.CommitterSession, project: str, versio
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 _distribute_page(project=project, version=version, form=form)
+ return await _distribute_page(fpv)
# This function is used in both GET and POST routes
-async def _distribute_page(
- *, project: str, version: str, form: DistributeForm, extra_content:
htpy.Element | None = None
-) -> str:
+async def _distribute_page(fpv: FormProjectVersion, *, extra_content:
htpy.Element | None = None) -> str:
# Validate the Release
- async with db.session() as data:
- release = await data.release(project_name=project,
version=version).demand(
- RuntimeError(f"Release {project} {version} not found")
- )
- if release.phase != sql.ReleasePhase.RELEASE_PREVIEW:
- raise RuntimeError(f"Release {project} {version} is not a release
preview")
- # if release.project.status != sql.ProjectStatus.ACTIVE:
- # raise RuntimeError(f"Project {project} is not active")
+ await _release_validated(fpv.project, fpv.version)
# Render the explanation and form
block = htm.Block()
@@ -173,7 +175,7 @@ async def _distribute_page(
" phase using the form below.",
]
block.p["Please note that this form is a work in progress and not fully
functional."]
- block.append(forms.render_columns(form, action=quart.request.path,
descriptions=True))
+ block.append(forms.render_columns(fpv.form, action=quart.request.path,
descriptions=True))
# Render the page
return await template.blank("Distribute", content=block.collect())
@@ -198,9 +200,10 @@ async def _distribute_post_api(
return outcome.Result(result)
-async def _distribute_post_validated(form: DistributeForm, project: str,
version: str) -> str:
- dd = DistributeData.model_validate(form.data)
- api_url = form.platform.data.value.template_url.format(
+async def _distribute_post_validated(fpv: FormProjectVersion, /) -> str:
+ dd = DistributeData.model_validate(fpv.form.data)
+ release, committee = await _release_committee_validated(fpv.project,
fpv.version)
+ api_url = fpv.form.platform.data.value.template_url.format(
owner_namespace=dd.owner_namespace,
package=dd.package,
version=dd.version,
@@ -209,34 +212,59 @@ async def _distribute_post_validated(form:
DistributeForm, project: str, version
block = htm.Block()
+ # In case of error, show an alert
+ def _alert(not_found: str, action: str) -> htpy.Element:
+ div = htm.Block(htpy.div(".alert.alert-danger"))
+ div.p[
+ f"The {not_found} was not found in ",
+ htpy.a(href=api_url)["the distribution platform API"],
+ f". Please {action}.",
+ ]
+ return div.collect()
+
# Distribution submitted
- block.h1["Distribution submitted"]
+ block.h1["Distribution recorded"]
match api_oc:
case outcome.Result(result):
- block.p["The distribution was submitted successfully."]
- case outcome.Error(error):
- div = htm.Block(htpy.div(".alert.alert-danger"))
- div.p[
- "This package and version was not found in ",
- htpy.a(href=api_url)["the distribution platform API"],
- ". Please check the package name and version.",
- ]
- div.pre(".atr-pre-wrap")[str(error)]
- return await _distribute_page(
- project=project,
- version=version,
- form=form,
- extra_content=div.collect(),
- )
+ block.p["The distribution was recorded successfully."]
+ case outcome.Error():
+ alert = _alert("package and version", "check the package name and
version")
+ return await _distribute_page(fpv, extra_content=alert)
# We leak result, usefully, from this scope
- ### Upload date
- block.h2["Upload date"]
+ # This must come after the api_oc match, as it uses the result
upload_date = _platform_upload_date(dd.platform, result, dd.version)
- if upload_date is not None:
- block.pre[str(upload_date)]
- else:
- block.p["No upload date found."]
+ if upload_date is None:
+ # TODO: Add a link to an issue tracker
+ alert = _alert("upload date", "report this bug to ASF Tooling")
+ return await _distribute_page(fpv, extra_content=alert)
+
+ async with
storage.write_as_committee_member(committee_name=committee.name) as w:
+ distribution = await w.distributions.add_distribution(
+ release_name=release.name,
+ platform=dd.platform,
+ owner_namespace=dd.owner_namespace,
+ package=dd.package,
+ version=dd.version,
+ staging=False,
+ upload_date=upload_date,
+ api_url=api_url,
+ )
+
+ ### Record
+ block.h2["Record"]
+ block.table(".table.table-striped.table-bordered")[
+ htpy.tbody[
+ _tr("Release name", distribution.release_name),
+ _tr("Platform", distribution.platform.name),
+ _tr("Owner or Namespace", distribution.owner_namespace or "-"),
+ _tr("Package", distribution.package),
+ _tr("Version", distribution.version),
+ _tr("Staging", "No" if distribution.staging else "Yes"),
+ _tr("Upload date", str(distribution.upload_date)),
+ _tr("API URL", distribution.api_url),
+ ]
+ ]
if dd.details:
## Details
@@ -265,14 +293,11 @@ async def _distribute_post_validated(form:
DistributeForm, project: str, version
def _distribute_post_table(block: htm.Block, dd: DistributeData) -> None:
- def row(label: str, value: str) -> htpy.Element:
- return htpy.tr[htpy.th[label], htpy.td[value]]
-
tbody = htpy.tbody[
- row("Platform", dd.platform.name),
- row("Owner or Namespace", dd.owner_namespace or "(blank)"),
- row("Package", dd.package),
- row("Version", dd.version),
+ _tr("Platform", dd.platform.name),
+ _tr("Owner or Namespace", dd.owner_namespace or "-"),
+ _tr("Package", dd.package),
+ _tr("Version", dd.version),
]
block.table(".table.table-striped.table-bordered")[tbody]
@@ -315,3 +340,29 @@ def _platform_upload_date( # noqa: C901
return None
return datetime.datetime.fromisoformat(upload_time.rstrip("Z"))
raise NotImplementedError(f"Platform {platform.name} is not yet supported")
+
+
+async def _release_committee_validated(project: str, version: str) ->
tuple[sql.Release, sql.Committee]:
+ release = await _release_validated(project, version, committee=True)
+ committee = release.committee
+ if committee is None:
+ raise RuntimeError(f"Release {project} {version} has no committee")
+ return release, committee
+
+
+async def _release_validated(project: str, version: str, committee: bool =
False) -> sql.Release:
+ async with db.session() as data:
+ release = await data.release(
+ project_name=project,
+ version=version,
+ _committee=committee,
+ ).demand(RuntimeError(f"Release {project} {version} not found"))
+ if release.phase != sql.ReleasePhase.RELEASE_PREVIEW:
+ raise RuntimeError(f"Release {project} {version} is not a release
preview")
+ # if release.project.status != sql.ProjectStatus.ACTIVE:
+ # raise RuntimeError(f"Project {project} is not active")
+ return release
+
+
+def _tr(label: str, value: str) -> htpy.Element:
+ return htpy.tr[htpy.th[label], htpy.td[value]]
diff --git a/migrations/versions/0019_2025.08.07_279ca4a9.py
b/migrations/versions/0019_2025.08.07_279ca4a9.py
new file mode 100644
index 0000000..0f412b2
--- /dev/null
+++ b/migrations/versions/0019_2025.08.07_279ca4a9.py
@@ -0,0 +1,36 @@
+"""Ensure that Distribution rows are deleted on cascade
+
+Revision ID: 0019_2025.08.07_279ca4a9
+Revises: 0018_2025.08.07_41ccdd9a
+Create Date: 2025-08-07 15:23:18.069506+00:00
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0019_2025.08.07_279ca4a9"
+down_revision: str | None = "0018_2025.08.07_41ccdd9a"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ with op.batch_alter_table("distribution", schema=None) as batch_op:
+
batch_op.drop_constraint(batch_op.f("fk_distribution_release_name_release"),
type_="foreignkey")
+ batch_op.create_foreign_key(
+ batch_op.f("fk_distribution_release_name_release"),
+ "release",
+ ["release_name"],
+ ["name"],
+ ondelete="CASCADE",
+ )
+
+
+def downgrade() -> None:
+ with op.batch_alter_table("distribution", schema=None) as batch_op:
+
batch_op.drop_constraint(batch_op.f("fk_distribution_release_name_release"),
type_="foreignkey")
+ batch_op.create_foreign_key(
+ batch_op.f("fk_distribution_release_name_release"), "release",
["release_name"], ["name"]
+ )
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]