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 d7b4edf Extract or construct release URLs for distributions
d7b4edf is described below
commit d7b4edf02bba004e3365588e60709b655589e7dd
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Aug 8 16:55:09 2025 +0100
Extract or construct release URLs for distributions
---
atr/models/sql.py | 1 +
atr/routes/distribution.py | 95 +++++++++++++++++++++++--
atr/storage/writers/distributions.py | 5 ++
migrations/versions/0021_2025.08.08_3e1625a6.py | 27 +++++++
4 files changed, 123 insertions(+), 5 deletions(-)
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 1cf3820..f6d32d0 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -842,6 +842,7 @@ class Distribution(sqlmodel.SQLModel, table=True):
staging: bool = sqlmodel.Field(default=False)
upload_date: datetime.datetime | None = sqlmodel.Field(default=None)
api_url: str
+ web_url: str | None = sqlmodel.Field(default=None)
# The API response can be huge, e.g. from npm
# So we do not store it in the database
# api_response: Any =
sqlmodel.Field(sa_column=sqlalchemy.Column(sqlalchemy.JSON))
diff --git a/atr/routes/distribution.py b/atr/routes/distribution.py
index e0c9ca2..9e5f7dc 100644
--- a/atr/routes/distribution.py
+++ b/atr/routes/distribution.py
@@ -51,8 +51,22 @@ class ArtifactHubAvailableVersion(schema.Lax):
ts: int
+class ArtifactHubLink(schema.Lax):
+ url: str | None = None
+ name: str | None = None
+
+
+class ArtifactHubRepository(schema.Lax):
+ name: str | None = None
+
+
class ArtifactHubResponse(schema.Lax):
available_versions: list[ArtifactHubAvailableVersion] =
pydantic.Field(default_factory=list)
+ home_url: str | None = None
+ links: list[ArtifactHubLink] = pydantic.Field(default_factory=list)
+ name: str | None = None
+ version: str | None = None
+ repository: ArtifactHubRepository | None = None
class DockerResponse(schema.Lax):
@@ -61,26 +75,41 @@ class DockerResponse(schema.Lax):
class GitHubResponse(schema.Lax):
published_at: str | None = None
+ html_url: str | None = None
class MavenDoc(schema.Lax):
timestamp: int | None = None
+class MavenResponseBody(schema.Lax):
+ start: int | None = None
+ docs: list[MavenDoc] = pydantic.Field(default_factory=list)
+
+
class MavenResponse(schema.Lax):
- response: dict[str, list[MavenDoc]] = pydantic.Field(default_factory=dict)
+ response: MavenResponseBody =
pydantic.Field(default_factory=MavenResponseBody)
class NpmResponse(schema.Lax):
+ name: str | None = None
time: dict[str, str] = pydantic.Field(default_factory=dict)
+ homepage: str | None = None
class PyPIUrl(schema.Lax):
upload_time_iso_8601: str | None = None
+ url: str | None = None
+
+
+class PyPIInfo(schema.Lax):
+ release_url: str | None = None
+ project_url: str | None = None
class PyPIResponse(schema.Lax):
urls: list[PyPIUrl] = pydantic.Field(default_factory=list)
+ info: PyPIInfo = pydantic.Field(default_factory=PyPIInfo)
class DeleteForm(forms.Typed):
@@ -252,7 +281,8 @@ async def list_get(session: routes.CommitterSession,
project: str, version: str)
_html_tr("Version", distribution.version),
_html_tr("Staging", "Yes" if distribution.staging else "No"),
_html_tr("Upload date", str(distribution.upload_date)),
- _html_tr("API URL", distribution.api_url),
+ _html_tr_a("API URL", distribution.api_url),
+ _html_tr_a("Web URL", distribution.web_url),
]
block.table(".table.table-striped.table-bordered")[tbody]
form_action = util.as_url(delete, project=project, version=version)
@@ -333,9 +363,12 @@ def _distribution_upload_date( # noqa: C901
return None
return datetime.datetime.fromisoformat(published_at.rstrip("Z"))
case sql.DistributionPlatform.MAVEN:
- if not (docs :=
MavenResponse.model_validate(data).response.get("docs")):
+ m = MavenResponse.model_validate(data)
+ docs = m.response.docs
+ if not docs:
return None
- if not (timestamp := docs[0].timestamp):
+ timestamp = docs[0].timestamp
+ if not timestamp:
return None
return datetime.datetime.fromtimestamp(timestamp / 1000,
tz=datetime.UTC)
case sql.DistributionPlatform.NPM |
sql.DistributionPlatform.NPM_SCOPED:
@@ -354,6 +387,51 @@ def _distribution_upload_date( # noqa: C901
raise NotImplementedError(f"Platform {platform.name} is not yet supported")
+def _distribution_web_url( # noqa: C901
+ platform: sql.DistributionPlatform,
+ data: basic.JSON,
+ version: str,
+) -> str | None:
+ match platform:
+ case sql.DistributionPlatform.ARTIFACTHUB:
+ ah = ArtifactHubResponse.model_validate(data)
+ repo_name = ah.repository.name if ah.repository else None
+ pkg_name = ah.name
+ ver = ah.version
+ if repo_name and pkg_name:
+ if ver:
+ return
f"https://artifacthub.io/packages/helm/{repo_name}/{pkg_name}/{ver}"
+ return
f"https://artifacthub.io/packages/helm/{repo_name}/{pkg_name}"
+ if ah.home_url:
+ return ah.home_url
+ for link in ah.links:
+ if link.url:
+ return link.url
+ return None
+ case sql.DistributionPlatform.DOCKER:
+ # The best we can do on Docker Hub is:
+ # f"https://hub.docker.com/_/{package}"
+ # TODO: Rename to DOCKER_HUB and "Docker Hub"
+ return None
+ case sql.DistributionPlatform.GITHUB:
+ gh = GitHubResponse.model_validate(data)
+ return gh.html_url
+ case sql.DistributionPlatform.MAVEN:
+ return None
+ case sql.DistributionPlatform.NPM:
+ nr = NpmResponse.model_validate(data)
+ # return nr.homepage
+ return f"https://www.npmjs.com/package/{nr.name}/v/{version}"
+ case sql.DistributionPlatform.NPM_SCOPED:
+ nr = NpmResponse.model_validate(data)
+ # TODO: This is not correct
+ return nr.homepage
+ case sql.DistributionPlatform.PYPI:
+ info = PyPIResponse.model_validate(data).info
+ return info.release_url or info.project_url
+ raise NotImplementedError(f"Platform {platform.name} is not yet supported")
+
+
async def _json_from_distribution_platform(
api_url: str, platform: sql.DistributionPlatform, version: str
) -> outcome.Outcome[basic.JSON]:
@@ -436,6 +514,10 @@ def _html_tr(label: str, value: str) -> htpy.Element:
return htpy.tr[htpy.th[label], htpy.td[value]]
+def _html_tr_a(label: str, value: str | None) -> htpy.Element:
+ return htpy.tr[htpy.th[label], htpy.td[htpy.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(
@@ -510,6 +592,7 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
alert = _alert("upload date", "report this bug to ASF Tooling")
return await _record_form_page(fpv, extra_content=alert,
staging=staging)
+ web_url = _distribution_web_url(dd.platform, result, dd.version)
async with
storage.write_as_committee_member(committee_name=committee.name) as w:
distribution, added = await w.distributions.add_distribution(
release_name=release.name,
@@ -520,6 +603,7 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
staging=staging,
upload_date=upload_date,
api_url=api_url,
+ web_url=web_url,
)
### Record
@@ -537,7 +621,8 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
_html_tr("Version", distribution.version),
_html_tr("Staging", "Yes" if distribution.staging else "No"),
_html_tr("Upload date", str(distribution.upload_date)),
- _html_tr("API URL", distribution.api_url),
+ _html_tr_a("API URL", distribution.api_url),
+ _html_tr_a("Web URL", distribution.web_url),
]
]
block.p[htpy.a(href=util.as_url(list_get, project=fpv.project,
version=fpv.version))["Back to distribution list"],]
diff --git a/atr/storage/writers/distributions.py
b/atr/storage/writers/distributions.py
index 3421222..fa08aae 100644
--- a/atr/storage/writers/distributions.py
+++ b/atr/storage/writers/distributions.py
@@ -100,6 +100,7 @@ class CommitteeMember(CommitteeParticipant):
staging: bool,
upload_date: datetime.datetime | None,
api_url: str,
+ web_url: str | None = None,
) -> tuple[sql.Distribution, bool]:
distribution = sql.Distribution(
platform=platform,
@@ -110,6 +111,7 @@ class CommitteeMember(CommitteeParticipant):
staging=staging,
upload_date=upload_date,
api_url=api_url,
+ web_url=web_url,
)
self.__data.add(distribution)
try:
@@ -130,6 +132,7 @@ class CommitteeMember(CommitteeParticipant):
version,
upload_date,
api_url,
+ web_url,
)
if upgraded is not None:
return upgraded, False
@@ -146,6 +149,7 @@ class CommitteeMember(CommitteeParticipant):
version: str,
upload_date: datetime.datetime | None,
api_url: str,
+ web_url: str | None,
) -> sql.Distribution | None:
tag = f"{release_name} {platform} {owner_namespace or ''} {package}
{version}"
existing = await self.__data.distribution(
@@ -159,6 +163,7 @@ class CommitteeMember(CommitteeParticipant):
existing.staging = False
existing.upload_date = upload_date
existing.api_url = api_url
+ existing.web_url = web_url
await self.__data.commit()
return existing
return None
diff --git a/migrations/versions/0021_2025.08.08_3e1625a6.py
b/migrations/versions/0021_2025.08.08_3e1625a6.py
new file mode 100644
index 0000000..2aef4fd
--- /dev/null
+++ b/migrations/versions/0021_2025.08.08_3e1625a6.py
@@ -0,0 +1,27 @@
+"""Add a web URL field to Distribution
+
+Revision ID: 0021_2025.08.08_3e1625a6
+Revises: 0020_2025.08.07_23999f25
+Create Date: 2025-08-08 15:23:07.713491+00:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0021_2025.08.08_3e1625a6"
+down_revision: str | None = "0020_2025.08.07_23999f25"
+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.add_column(sa.Column("web_url", sa.String(), nullable=True))
+
+
+def downgrade() -> None:
+ with op.batch_alter_table("distribution", schema=None) as batch_op:
+ batch_op.drop_column("web_url")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]