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]

Reply via email to