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 4af230c Copy some distribution construction code into the storage
interface
4af230c is described below
commit 4af230c6e151e2fa91424a472d0cd07307605634
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Sep 8 15:38:33 2025 +0100
Copy some distribution construction code into the storage interface
---
atr/models/distribution.py | 118 ++++++++++++++++++++++
atr/routes/distribution.py | 186 +++++++++--------------------------
atr/storage/writers/distributions.py | 140 +++++++++++++++++++++++++-
3 files changed, 298 insertions(+), 146 deletions(-)
diff --git a/atr/models/distribution.py b/atr/models/distribution.py
new file mode 100644
index 0000000..1a5e6e4
--- /dev/null
+++ b/atr/models/distribution.py
@@ -0,0 +1,118 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pydantic
+
+import atr.models.schema as schema
+import atr.models.sql as sql
+
+
+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):
+ tag_last_pushed: str | None = None
+
+
+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: 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 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
+# Including all of the enum properties
+class Data(schema.Lax):
+ platform: sql.DistributionPlatform
+ owner_namespace: str | None = None
+ package: str
+ version: str
+ details: bool
+
+ @pydantic.field_validator("owner_namespace", mode="before")
+ @classmethod
+ def empty_to_none(cls, v):
+ return None if v is None or (isinstance(v, str) and v.strip() == "")
else v
diff --git a/atr/routes/distribution.py b/atr/routes/distribution.py
index 5ccbf03..201f76a 100644
--- a/atr/routes/distribution.py
+++ b/atr/routes/distribution.py
@@ -24,14 +24,13 @@ from typing import TYPE_CHECKING, Literal
import aiohttp
import htpy
-import pydantic
import quart
import atr.db as db
import atr.forms as forms
import atr.htm as htm
import atr.models.basic as basic
-import atr.models.schema as schema
+import atr.models.distribution as distribution
import atr.models.sql as sql
import atr.routes as routes
import atr.routes.compose as compose
@@ -47,71 +46,6 @@ if TYPE_CHECKING:
type Phase = Literal["COMPOSE", "VOTE", "FINISH"]
-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):
- tag_last_pushed: str | None = None
-
-
-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: 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):
release_name = forms.hidden()
platform = forms.hidden()
@@ -121,21 +55,6 @@ class DeleteForm(forms.Typed):
submit = forms.submit("Delete")
-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
-
-
class DistributeForm(forms.Typed):
platform = forms.select("Platform", choices=sql.DistributionPlatform)
owner_namespace = forms.optional(
@@ -178,27 +97,10 @@ class FormProjectVersion:
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
-# Including all of the enum properties
-class DistributeData(schema.Lax):
- platform: sql.DistributionPlatform
- owner_namespace: str | None = None
- package: str
- version: str
- details: bool
-
- @pydantic.field_validator("owner_namespace", mode="before")
- @classmethod
- def empty_to_none(cls, v):
- return None if v is None or (isinstance(v, str) and v.strip() == "")
else v
-
-
@routes.committer("/distribution/delete/<project>/<version>", methods=["POST"])
async def delete(session: routes.CommitterSession, project: str, version: str)
-> response.Response:
form = await DeleteForm.create_form(data=await quart.request.form)
- dd = DeleteData.model_validate(form.data)
+ dd = distribution.DeleteData.model_validate(form.data)
# Validate the submitted data, and obtain the committee for its name
async with db.session() as data:
@@ -259,21 +161,21 @@ async def list_get(session: routes.CommitterSession,
project: str, version: str)
block.p[record_a_distribution]
# Table of contents
ul_toc = htm.Block(htpy.ul)
- for distribution in distributions:
- a =
htpy.a(href=f"#distribution-{distribution.identifier}")[distribution.title]
+ for dist in distributions:
+ a = htpy.a(href=f"#distribution-{dist.identifier}")[dist.title]
ul_toc.li[a]
block.append(ul_toc)
## Distributions
block.h2["Distributions"]
- for distribution in distributions:
+ for dist in distributions:
delete_form = await DeleteForm.create_form(
data={
- "release_name": distribution.release_name,
- "platform": distribution.platform.name,
- "owner_namespace": distribution.owner_namespace,
- "package": distribution.package,
- "version": distribution.version,
+ "release_name": dist.release_name,
+ "platform": dist.platform.name,
+ "owner_namespace": dist.owner_namespace,
+ "package": dist.package,
+ "version": dist.version,
}
)
@@ -281,18 +183,18 @@ async def list_get(session: routes.CommitterSession,
project: str, version: str)
block.h3(
# Cannot use "#id" here, because the ID contains "."
# If an ID contains ".", htpy parses that as a class
- id=f"distribution-{distribution.identifier}"
- )[distribution.title]
+ id=f"distribution-{dist.identifier}"
+ )[dist.title]
tbody = htpy.tbody[
- _html_tr("Release name", distribution.release_name),
- _html_tr("Platform", distribution.platform.value.name),
- _html_tr("Owner or Namespace", distribution.owner_namespace or
"-"),
- _html_tr("Package", distribution.package),
- _html_tr("Version", distribution.version),
- _html_tr("Staging", "Yes" if distribution.staging else "No"),
- _html_tr("Upload date", str(distribution.upload_date)),
- _html_tr_a("API URL", distribution.api_url),
- _html_tr_a("Web URL", distribution.web_url),
+ _html_tr("Release name", dist.release_name),
+ _html_tr("Platform", dist.platform.value.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.table(".table.table-striped.table-bordered")[tbody]
form_action = util.as_url(delete, project=project, version=version)
@@ -361,11 +263,11 @@ def _distribution_upload_date( # noqa: C901
) -> datetime.datetime | None:
match platform:
case sql.DistributionPlatform.ARTIFACT_HUB:
- if not (versions :=
ArtifactHubResponse.model_validate(data).available_versions):
+ if not (versions :=
distribution.ArtifactHubResponse.model_validate(data).available_versions):
return None
return datetime.datetime.fromtimestamp(versions[0].ts,
tz=datetime.UTC)
case sql.DistributionPlatform.DOCKER_HUB:
- if not (pushed_at :=
DockerResponse.model_validate(data).tag_last_pushed):
+ if not (pushed_at :=
distribution.DockerResponse.model_validate(data).tag_last_pushed):
return None
return datetime.datetime.fromisoformat(pushed_at.rstrip("Z"))
# case sql.DistributionPlatform.GITHUB:
@@ -373,7 +275,7 @@ def _distribution_upload_date( # noqa: C901
# return None
# return datetime.datetime.fromisoformat(published_at.rstrip("Z"))
case sql.DistributionPlatform.MAVEN:
- m = MavenResponse.model_validate(data)
+ m = distribution.MavenResponse.model_validate(data)
docs = m.response.docs
if not docs:
return None
@@ -382,14 +284,14 @@ def _distribution_upload_date( # noqa: C901
return None
return datetime.datetime.fromtimestamp(timestamp / 1000,
tz=datetime.UTC)
case sql.DistributionPlatform.NPM |
sql.DistributionPlatform.NPM_SCOPED:
- if not (times := NpmResponse.model_validate(data).time):
+ if not (times :=
distribution.NpmResponse.model_validate(data).time):
return None
# Versions can be in the form "1.2.3" or "v1.2.3", so we check for
both
if not (upload_time := times.get(version) or
times.get(f"v{version}")):
return None
return datetime.datetime.fromisoformat(upload_time.rstrip("Z"))
case sql.DistributionPlatform.PYPI:
- if not (urls := PyPIResponse.model_validate(data).urls):
+ if not (urls :=
distribution.PyPIResponse.model_validate(data).urls):
return None
if not (upload_time := urls[0].upload_time_iso_8601):
return None
@@ -404,7 +306,7 @@ def _distribution_web_url( # noqa: C901
) -> str | None:
match platform:
case sql.DistributionPlatform.ARTIFACT_HUB:
- ah = ArtifactHubResponse.model_validate(data)
+ ah = distribution.ArtifactHubResponse.model_validate(data)
repo_name = ah.repository.name if ah.repository else None
pkg_name = ah.name
ver = ah.version
@@ -428,15 +330,15 @@ def _distribution_web_url( # noqa: C901
case sql.DistributionPlatform.MAVEN:
return None
case sql.DistributionPlatform.NPM:
- nr = NpmResponse.model_validate(data)
+ nr = distribution.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)
+ nr = distribution.NpmResponse.model_validate(data)
# TODO: This is not correct
return nr.homepage
case sql.DistributionPlatform.PYPI:
- info = PyPIResponse.model_validate(data).info
+ info = distribution.PyPIResponse.model_validate(data).info
return info.release_url or info.project_url
raise NotImplementedError(f"Platform {platform.name} is not yet supported")
@@ -454,7 +356,7 @@ async def _json_from_distribution_platform(
return outcome.Error(e)
match platform:
case sql.DistributionPlatform.NPM |
sql.DistributionPlatform.NPM_SCOPED:
- if version not in NpmResponse.model_validate(result).time:
+ if version not in
distribution.NpmResponse.model_validate(result).time:
e = RuntimeError(f"Version '{version}' not found")
return outcome.Error(e)
return outcome.Result(result)
@@ -509,7 +411,7 @@ def _html_nav_phase(block: htm.Block, project: str,
version: str, staging: bool)
)
-def _html_submitted_values_table(block: htm.Block, dd: DistributeData) -> None:
+def _html_submitted_values_table(block: htm.Block, dd: distribution.Data) ->
None:
tbody = htpy.tbody[
_html_tr("Platform", dd.platform.name),
_html_tr("Owner or Namespace", dd.owner_namespace or "-"),
@@ -560,7 +462,7 @@ async def _record_form_page(
async def _record_form_process_page(fpv: FormProjectVersion, /, staging: bool
= False) -> str:
- dd = DistributeData.model_validate(fpv.form.data)
+ dd = distribution.Data.model_validate(fpv.form.data)
resolved = await _release_validated_and_committee_and_template(fpv, dd,
staging)
if isinstance(resolved, htpy.Element):
return await _record_form_page(fpv, extra_content=resolved,
staging=staging)
@@ -603,7 +505,7 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
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(
+ dist, added = await w.distributions.add_distribution(
release_name=release.name,
platform=dd.platform,
owner_namespace=dd.owner_namespace,
@@ -623,15 +525,15 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
block.p["The distribution was already recorded."]
block.table(".table.table-striped.table-bordered")[
htpy.tbody[
- _html_tr("Release name", distribution.release_name),
- _html_tr("Platform", distribution.platform.name),
- _html_tr("Owner or Namespace", distribution.owner_namespace or
"-"),
- _html_tr("Package", distribution.package),
- _html_tr("Version", distribution.version),
- _html_tr("Staging", "Yes" if distribution.staging else "No"),
- _html_tr("Upload date", str(distribution.upload_date)),
- _html_tr_a("API URL", distribution.api_url),
- _html_tr_a("Web URL", distribution.web_url),
+ _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[htpy.a(href=util.as_url(list_get, project=fpv.project,
version=fpv.version))["Back to distribution list"],]
@@ -703,7 +605,7 @@ async def _release_validated(
async def _release_validated_and_committee_and_template(
fpv: FormProjectVersion,
- dd: DistributeData,
+ dd: distribution.Data,
staging: bool | None = None,
) -> tuple[sql.Release, sql.Committee, str] | htpy.Element:
release, committee = await _release_validated_and_committee(
diff --git a/atr/storage/writers/distributions.py
b/atr/storage/writers/distributions.py
index fa08aae..7e1b1e4 100644
--- a/atr/storage/writers/distributions.py
+++ b/atr/storage/writers/distributions.py
@@ -18,17 +18,18 @@
# Removing this will cause circular imports
from __future__ import annotations
+import datetime
import sqlite3
-from typing import TYPE_CHECKING
+import aiohttp
import sqlalchemy.exc as exc
import atr.db as db
+import atr.models.basic as basic
+import atr.models.distribution as distribution
import atr.models.sql as sql
import atr.storage as storage
-
-if TYPE_CHECKING:
- import datetime
+import atr.storage.outcome as outcome
class GeneralPublic:
@@ -122,6 +123,7 @@ class CommitteeMember(CommitteeParticipant):
# e.orig.sqlite_errorcode == 1555
# e.orig.sqlite_errorname == "SQLITE_CONSTRAINT_PRIMARYKEY"
match e.orig:
+ # TODO: Document this
case
sqlite3.IntegrityError(sqlite_errorcode=sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY):
if not staging:
upgraded = await self.__upgrade_staging_to_final(
@@ -140,6 +142,136 @@ class CommitteeMember(CommitteeParticipant):
raise e
return distribution, True
+ async def add_distribution_from_url(
+ self,
+ api_url: str,
+ platform: sql.DistributionPlatform,
+ version: str,
+ ) -> tuple[sql.Distribution, bool]: ...
+
+ def __distribution_upload_date( # noqa: C901
+ self,
+ platform: sql.DistributionPlatform,
+ data: basic.JSON,
+ version: str,
+ ) -> datetime.datetime | None:
+ match platform:
+ case sql.DistributionPlatform.ARTIFACT_HUB:
+ if not (versions :=
distribution.ArtifactHubResponse.model_validate(data).available_versions):
+ return None
+ return datetime.datetime.fromtimestamp(versions[0].ts,
tz=datetime.UTC)
+ case sql.DistributionPlatform.DOCKER_HUB:
+ if not (pushed_at :=
distribution.DockerResponse.model_validate(data).tag_last_pushed):
+ return None
+ return datetime.datetime.fromisoformat(pushed_at.rstrip("Z"))
+ # case sql.DistributionPlatform.GITHUB:
+ # if not (published_at :=
GitHubResponse.model_validate(data).published_at):
+ # return None
+ # return
datetime.datetime.fromisoformat(published_at.rstrip("Z"))
+ case sql.DistributionPlatform.MAVEN:
+ m = distribution.MavenResponse.model_validate(data)
+ docs = m.response.docs
+ if not docs:
+ return None
+ 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:
+ if not (times :=
distribution.NpmResponse.model_validate(data).time):
+ return None
+ # Versions can be in the form "1.2.3" or "v1.2.3", so we check
for both
+ if not (upload_time := times.get(version) or
times.get(f"v{version}")):
+ return None
+ return datetime.datetime.fromisoformat(upload_time.rstrip("Z"))
+ case sql.DistributionPlatform.PYPI:
+ if not (urls :=
distribution.PyPIResponse.model_validate(data).urls):
+ return None
+ if not (upload_time := urls[0].upload_time_iso_8601):
+ return None
+ return datetime.datetime.fromisoformat(upload_time.rstrip("Z"))
+ raise NotImplementedError(f"Platform {platform.name} is not yet
supported")
+
+ def __distribution_web_url( # noqa: C901
+ self,
+ platform: sql.DistributionPlatform,
+ data: basic.JSON,
+ version: str,
+ ) -> str | None:
+ match platform:
+ case sql.DistributionPlatform.ARTIFACT_HUB:
+ ah = distribution.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}/{version}"
+ 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_HUB:
+ # The best we can do on Docker Hub is:
+ # f"https://hub.docker.com/_/{package}"
+ 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 = distribution.NpmResponse.model_validate(data)
+ # return nr.homepage
+ return f"https://www.npmjs.com/package/{nr.name}/v/{version}"
+ case sql.DistributionPlatform.NPM_SCOPED:
+ nr = distribution.NpmResponse.model_validate(data)
+ # TODO: This is not correct
+ return nr.homepage
+ case sql.DistributionPlatform.PYPI:
+ info = distribution.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(
+ self, api_url: str, platform: sql.DistributionPlatform, version: str
+ ) -> outcome.Outcome[basic.JSON]:
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(api_url) as response:
+ response.raise_for_status()
+ response_json = await response.json()
+ result = basic.as_json(response_json)
+ except aiohttp.ClientError as e:
+ return outcome.Error(e)
+ match platform:
+ case sql.DistributionPlatform.NPM |
sql.DistributionPlatform.NPM_SCOPED:
+ if version not in
distribution.NpmResponse.model_validate(result).time:
+ e = RuntimeError(f"Version '{version}' not found")
+ return outcome.Error(e)
+ return outcome.Result(result)
+
+ async def __template_url(
+ self,
+ dd: distribution.Data,
+ staging: bool | None = None,
+ ) -> str:
+ if staging is False:
+ return dd.platform.value.template_url
+
+ supported = {sql.DistributionPlatform.ARTIFACT_HUB,
sql.DistributionPlatform.PYPI}
+ if dd.platform not in supported:
+ raise storage.AccessError("Staging is currently supported only for
ArtifactHub and PyPI.")
+
+ template_url = dd.platform.value.template_staging_url
+ if template_url is None:
+ raise storage.AccessError("This platform does not provide a
staging API endpoint.")
+
+ return template_url
+
async def __upgrade_staging_to_final(
self,
release_name: str,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]