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 279ca4a  Add a distribution table to the database
279ca4a is described below

commit 279ca4a9dd6cb8b901a990e14e1214b948b4944c
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Aug 7 15:45:11 2025 +0100

    Add a distribution table to the database
---
 atr/db/__init__.py                              |   2 -
 atr/models/sql.py                               | 104 ++++++++++++++++++----
 atr/routes/distribute.py                        |  74 ++++------------
 atr/routes/projects.py                          |   5 --
 atr/storage/__init__.py                         |  20 ++---
 atr/storage/writers/__init__.py                 |   3 +-
 atr/storage/writers/distributions.py            | 113 ++++++++++++++++++++++++
 atr/validate.py                                 |   9 --
 migrations/versions/0018_2025.08.07_41ccdd9a.py |  65 ++++++++++++++
 pyproject.toml                                  |   1 -
 10 files changed, 294 insertions(+), 102 deletions(-)

diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 39ba3e1..530a7ce 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -370,8 +370,6 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
 
         if _releases:
             query = query.options(select_in_load(sql.Project.releases))
-        if _distribution_channels:
-            query = 
query.options(select_in_load(sql.Project.distribution_channels))
         if _super_project:
             query = query.options(joined_load(sql.Project.super_project))
         if _release_policy:
diff --git a/atr/models/sql.py b/atr/models/sql.py
index ebe226e..8431679 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -22,6 +22,7 @@
 # https://github.com/fastapi/sqlmodel/pull/778/files
 # from __future__ import annotations
 
+import dataclasses
 import datetime
 import enum
 from typing import Any, Final, Literal, Optional, TypeVar
@@ -47,6 +48,16 @@ sqlmodel.SQLModel.metadata = sqlalchemy.MetaData(
     }
 )
 
+# Data classes
+
+
[email protected](frozen=True)
+class DistributionPlatformValue:
+    name: str
+    template_url: str
+    requires_owner_namespace: bool = False
+    default_owner_namespace: str | None = None
+
 
 # Enumerations
 
@@ -81,6 +92,42 @@ class CheckResultStatusIgnore(str, enum.Enum):
         return f"CheckResultStatusIgnore.{self.value.upper()}"
 
 
+class DistributionPlatform(enum.Enum):
+    ARTIFACTHUB = DistributionPlatformValue(
+        name="ArtifactHub (Helm)",
+        
template_url="https://artifacthub.io/api/v1/packages/helm/{owner_namespace}/{package}/{version}";,
+        requires_owner_namespace=True,
+    )
+    DOCKER = DistributionPlatformValue(
+        name="Docker",
+        
template_url="https://hub.docker.com/v2/namespaces/{owner_namespace}/repositories/{package}/tags/{version}";,
+        default_owner_namespace="library",
+    )
+    GITHUB = DistributionPlatformValue(
+        name="GitHub",
+        
template_url="https://api.github.com/repos/{owner_namespace}/{package}/releases/tags/v{version}";,
+        requires_owner_namespace=True,
+    )
+    MAVEN = DistributionPlatformValue(
+        name="Maven Central",
+        
template_url="https://search.maven.org/solrsearch/select?q=g:{owner_namespace}+AND+a:{package}+AND+v:{version}&core=gav&rows=20&wt=json";,
+        requires_owner_namespace=True,
+    )
+    NPM = DistributionPlatformValue(
+        name="npm",
+        template_url="https://registry.npmjs.org/{package}";,
+    )
+    NPM_SCOPED = DistributionPlatformValue(
+        name="npm (scoped)",
+        template_url="https://registry.npmjs.org/@{owner_namespace}/{package}";,
+        requires_owner_namespace=True,
+    )
+    PYPI = DistributionPlatformValue(
+        name="PyPI",
+        template_url="https://pypi.org/pypi/{package}/{version}/json";,
+    )
+
+
 class ProjectStatus(str, enum.Enum):
     ACTIVE = "active"
     DORMANT = "dormant"
@@ -426,9 +473,9 @@ class Project(sqlmodel.SQLModel, table=True):
     # see_also(Release.project)
     releases: list["Release"] = sqlmodel.Relationship(back_populates="project")
 
-    # 1-M: Project -> [DistributionChannel]
-    # M-1: DistributionChannel -> Project
-    distribution_channels: list["DistributionChannel"] = 
sqlmodel.Relationship(back_populates="project")
+    # # 1-M: Project -> [DistributionChannel]
+    # # M-1: DistributionChannel -> Project
+    # distribution_channels: list["DistributionChannel"] = 
sqlmodel.Relationship(back_populates="project")
 
     # 1-1: Project -C-> ReleasePolicy
     # 1-1: ReleasePolicy -> Project
@@ -652,6 +699,12 @@ class Release(sqlmodel.SQLModel, table=True):
         back_populates="release", sa_relationship_kwargs={"cascade": "all, 
delete-orphan"}
     )
 
+    # 1-M: Release -> [Distribution]
+    # M-1: Distribution -> Release
+    distributions: list["Distribution"] = sqlmodel.Relationship(
+        back_populates="release", sa_relationship_kwargs={"cascade": "all, 
delete-orphan"}
+    )
+
     # The combination of project_name and version must be unique
     __table_args__ = (sqlmodel.UniqueConstraint("project_name", "version", 
name="unique_project_version"),)
 
@@ -764,21 +817,38 @@ class CheckResultIgnore(sqlmodel.SQLModel, table=True):
             self.created = 
datetime.datetime.fromisoformat(self.created.rstrip("Z"))
 
 
-# DistributionChannel: Project
-class DistributionChannel(sqlmodel.SQLModel, table=True):
+# Distribution: Release
+class Distribution(sqlmodel.SQLModel, table=True):
     id: int = sqlmodel.Field(default=None, primary_key=True)
-    name: str = sqlmodel.Field(index=True, unique=True)
-    url: str
-    credentials: str
-    is_test: bool = sqlmodel.Field(default=False)
-    automation_endpoint: str
-
-    project_name: str = sqlmodel.Field(foreign_key="project.name")
-
-    # M-1: DistributionChannel -> Project
-    # 1-M: Project -> [DistributionChannel]
-    project: Project = 
sqlmodel.Relationship(back_populates="distribution_channels")
-    see_also(Project.distribution_channels)
+    release_name: str = sqlmodel.Field(foreign_key="release.name")
+    release: Release = sqlmodel.Relationship(back_populates="distributions")
+    platform: DistributionPlatform = 
sqlmodel.Field(default=DistributionPlatform.ARTIFACTHUB)
+    owner_namespace: str | None = sqlmodel.Field(default=None)
+    package: str
+    version: str
+    staging: bool = sqlmodel.Field(default=False)
+    upload_date: datetime.datetime | None = sqlmodel.Field(default=None)
+    api_url: str
+    # 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))
+
+
+# # DistributionChannel: Project
+# class DistributionChannel(sqlmodel.SQLModel, table=True):
+#     id: int = sqlmodel.Field(default=None, primary_key=True)
+#     name: str = sqlmodel.Field(index=True, unique=True)
+#     url: str
+#     credentials: str
+#     is_test: bool = sqlmodel.Field(default=False)
+#     automation_endpoint: str
+#
+#     project_name: str = sqlmodel.Field(foreign_key="project.name")
+#
+#     # M-1: DistributionChannel -> Project
+#     # 1-M: Project -> [DistributionChannel]
+#     project: Project = 
sqlmodel.Relationship(back_populates="distribution_channels")
+#     see_also(Project.distribution_channels)
 
 
 # PublicSigningKey: Committee
diff --git a/atr/routes/distribute.py b/atr/routes/distribute.py
index 1463cd8..00ad36b 100644
--- a/atr/routes/distribute.py
+++ b/atr/routes/distribute.py
@@ -17,9 +17,7 @@
 
 from __future__ import annotations
 
-import dataclasses
 import datetime
-import enum
 import json
 
 import aiohttp
@@ -74,52 +72,8 @@ class PyPIResponse(schema.Lax):
     urls: list[PyPIUrl] = pydantic.Field(default_factory=list)
 
 
[email protected](frozen=True)
-class PlatformValue:
-    name: str
-    template_url: str
-    requires_owner_namespace: bool = False
-    default_owner_namespace: str | None = None
-
-
-class Platform(enum.Enum):
-    ARTIFACTHUB = PlatformValue(
-        name="ArtifactHub (Helm)",
-        
template_url="https://artifacthub.io/api/v1/packages/helm/{owner_namespace}/{package}/{version}";,
-        requires_owner_namespace=True,
-    )
-    DOCKER = PlatformValue(
-        name="Docker",
-        
template_url="https://hub.docker.com/v2/namespaces/{owner_namespace}/repositories/{package}/tags/{version}";,
-        default_owner_namespace="library",
-    )
-    GITHUB = PlatformValue(
-        name="GitHub",
-        
template_url="https://api.github.com/repos/{owner_namespace}/{package}/releases/tags/v{version}";,
-        requires_owner_namespace=True,
-    )
-    MAVEN = PlatformValue(
-        name="Maven Central",
-        
template_url="https://search.maven.org/solrsearch/select?q=g:{owner_namespace}+AND+a:{package}+AND+v:{version}&core=gav&rows=20&wt=json";,
-        requires_owner_namespace=True,
-    )
-    NPM = PlatformValue(
-        name="npm",
-        template_url="https://registry.npmjs.org/{package}";,
-    )
-    NPM_SCOPED = PlatformValue(
-        name="npm (scoped)",
-        template_url="https://registry.npmjs.org/@{owner_namespace}/{package}";,
-        requires_owner_namespace=True,
-    )
-    PYPI = PlatformValue(
-        name="PyPI",
-        template_url="https://pypi.org/pypi/{package}/{version}/json";,
-    )
-
-
 class DistributeForm(forms.Typed):
-    platform = forms.select("Platform", choices=Platform)
+    platform = forms.select("Platform", choices=sql.DistributionPlatform)
     owner_namespace = forms.optional(
         "Owner or Namespace",
         placeholder="E.g. com.example or scope or library",
@@ -158,7 +112,7 @@ class DistributeForm(forms.Typed):
 # And this way we also get nice JSON from the Pydantic model dump
 # Including all of the enum properties
 class DistributeData(schema.Lax):
-    platform: Platform
+    platform: sql.DistributionPlatform
     owner_namespace: str | None = None
     package: str
     version: str
@@ -225,7 +179,9 @@ async def _distribute_page(
     return await template.blank("Distribute", content=block.collect())
 
 
-async def _distribute_post_api(api_url: str, platform: Platform, version: str) 
-> outcome.Outcome[basic.JSON]:
+async def _distribute_post_api(
+    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:
@@ -235,7 +191,7 @@ async def _distribute_post_api(api_url: str, platform: 
Platform, version: str) -
     except aiohttp.ClientError as e:
         return outcome.Error(e)
     match platform:
-        case Platform.NPM | Platform.NPM_SCOPED:
+        case sql.DistributionPlatform.NPM | 
sql.DistributionPlatform.NPM_SCOPED:
             if version not in NpmResponse.model_validate(result).time:
                 e = RuntimeError(f"Version '{version}' not found")
                 return outcome.Error(e)
@@ -321,34 +277,38 @@ def _distribute_post_table(block: htm.Block, dd: 
DistributeData) -> None:
     block.table(".table.table-striped.table-bordered")[tbody]
 
 
-def _platform_upload_date(platform: Platform, data: basic.JSON, version: str) 
-> datetime.datetime | None:  # noqa: C901
+def _platform_upload_date(  # noqa: C901
+    platform: sql.DistributionPlatform,
+    data: basic.JSON,
+    version: str,
+) -> datetime.datetime | None:
     match platform:
-        case Platform.ARTIFACTHUB:
+        case sql.DistributionPlatform.ARTIFACTHUB:
             if not (versions := 
ArtifactHubResponse.model_validate(data).available_versions):
                 return None
             return datetime.datetime.fromtimestamp(versions[0].ts, 
tz=datetime.UTC)
-        case Platform.DOCKER:
+        case sql.DistributionPlatform.DOCKER:
             if not (pushed_at := 
DockerResponse.model_validate(data).tag_last_pushed):
                 return None
             return datetime.datetime.fromisoformat(pushed_at.rstrip("Z"))
-        case Platform.GITHUB:
+        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 Platform.MAVEN:
+        case sql.DistributionPlatform.MAVEN:
             if not (docs := 
MavenResponse.model_validate(data).response.get("docs")):
                 return None
             if not (timestamp := docs[0].timestamp):
                 return None
             return datetime.datetime.fromtimestamp(timestamp / 1000, 
tz=datetime.UTC)
-        case Platform.NPM | Platform.NPM_SCOPED:
+        case sql.DistributionPlatform.NPM | 
sql.DistributionPlatform.NPM_SCOPED:
             if not (times := 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 Platform.PYPI:
+        case sql.DistributionPlatform.PYPI:
             if not (urls := PyPIResponse.model_validate(data).urls):
                 return None
             if not (upload_time := urls[0].upload_time_iso_8601):
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index c3255ab..1428c60 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -206,11 +206,6 @@ async def delete(session: routes.CommitterSession) -> 
response.Response:
             return await session.redirect(
                 projects, error=f"Cannot delete project '{project_name}' 
because it has associated releases."
             )
-        if project.distribution_channels:
-            return await session.redirect(
-                projects,
-                error=f"Cannot delete project '{project_name}' because it has 
associated distribution channels.",
-            )
 
         await data.delete(project)
         await data.commit()
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index ccc2d14..4f27492 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -163,6 +163,7 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
     def __init__(self, write: Write, data: db.Session, committee_name: str):
         self.__committee_name = committee_name
         self.checks = writers.checks.CommitteeMember(write, self, data, 
committee_name)
+        self.distributions = writers.distributions.CommitteeMember(write, 
self, data, committee_name)
         self.keys = writers.keys.CommitteeMember(write, self, data, 
committee_name)
         self.tokens = writers.tokens.CommitteeMember(write, self, data, 
committee_name)
 
@@ -171,7 +172,6 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
         return self.__committee_name
 
 
-# TODO: Or WriteAsCommitteeAdmin
 class WriteAsFoundationAdmin(WriteAsCommitteeMember):
     def __init__(self, write: Write, data: db.Session, committee_name: str):
         self.__committee_name = committee_name
@@ -196,15 +196,6 @@ class Write:
     def authorisation(self) -> principal.Authorisation:
         return self.__authorisation
 
-    # def as_committee_admin(self, committee_name: str) -> 
types.Outcome[WriteAsCommitteeMember]:
-    #     if self.__asf_uid is None:
-    #         return types.OutcomeException(AccessError("No ASF UID"))
-    #     try:
-    #         wacm = WriteAsCommitteeMember(self, self.__data, self.__asf_uid, 
committee_name)
-    #     except Exception as e:
-    #         return types.OutcomeException(e)
-    #     return types.OutcomeResult(wacm)
-
     def as_committee_member(self, committee_name: str) -> 
WriteAsCommitteeMember:
         return 
self.as_committee_member_outcome(committee_name).result_or_raise()
 
@@ -366,3 +357,12 @@ async def write(asf_uid: str | None | ArgumentNoneType = 
ArgumentNone) -> AsyncG
     async with db.session() as data:
         # TODO: Replace data with a DatabaseWriter instance
         yield Write(authorisation, data)
+
+
[email protected]
+async def write_as_committee_member(
+    committee_name: str,
+    asf_uid: str | None | ArgumentNoneType = ArgumentNone,
+) -> AsyncGenerator[WriteAsCommitteeMember]:
+    async with write(asf_uid) as w:
+        yield w.as_committee_member(committee_name)
diff --git a/atr/storage/writers/__init__.py b/atr/storage/writers/__init__.py
index ddb82d7..f3613b0 100644
--- a/atr/storage/writers/__init__.py
+++ b/atr/storage/writers/__init__.py
@@ -16,7 +16,8 @@
 # under the License.
 
 import atr.storage.writers.checks as checks
+import atr.storage.writers.distributions as distributions
 import atr.storage.writers.keys as keys
 import atr.storage.writers.tokens as tokens
 
-__all__ = ["checks", "keys", "tokens"]
+__all__ = ["checks", "distributions", "keys", "tokens"]
diff --git a/atr/storage/writers/distributions.py 
b/atr/storage/writers/distributions.py
new file mode 100644
index 0000000..8786f6f
--- /dev/null
+++ b/atr/storage/writers/distributions.py
@@ -0,0 +1,113 @@
+# 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.
+
+# Removing this will cause circular imports
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import atr.db as db
+import atr.models.sql as sql
+import atr.storage as storage
+
+if TYPE_CHECKING:
+    import datetime
+
+
+class GeneralPublic:
+    def __init__(
+        self,
+        write: storage.Write,
+        write_as: storage.WriteAsGeneralPublic,
+        data: db.Session,
+    ):
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        self.__asf_uid = write.authorisation.asf_uid
+
+
+class FoundationCommitter(GeneralPublic):
+    def __init__(self, write: storage.Write, write_as: 
storage.WriteAsFoundationCommitter, data: db.Session):
+        super().__init__(write, write_as, data)
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        asf_uid = write.authorisation.asf_uid
+        if asf_uid is None:
+            raise storage.AccessError("No ASF UID")
+        self.__asf_uid = asf_uid
+
+
+class CommitteeParticipant(FoundationCommitter):
+    def __init__(
+        self,
+        write: storage.Write,
+        write_as: storage.WriteAsCommitteeParticipant,
+        data: db.Session,
+        committee_name: str,
+    ):
+        super().__init__(write, write_as, data)
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        self.__asf_uid = write.authorisation.asf_uid
+        self.__committee_name = committee_name
+
+
+class CommitteeMember(CommitteeParticipant):
+    def __init__(
+        self,
+        write: storage.Write,
+        write_as: storage.WriteAsCommitteeMember,
+        data: db.Session,
+        committee_name: str,
+    ):
+        super().__init__(write, write_as, data, committee_name)
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        asf_uid = write.authorisation.asf_uid
+        if asf_uid is None:
+            raise storage.AccessError("No ASF UID")
+        self.__asf_uid = asf_uid
+        self.__committee_name = committee_name
+
+    async def add_distribution(
+        self,
+        release_name: str,
+        platform: sql.DistributionPlatform,
+        owner_namespace: str | None,
+        package: str,
+        version: str,
+        staging: bool,
+        upload_date: datetime.datetime | None,
+        api_url: str,
+    ) -> sql.Distribution:
+        distribution = sql.Distribution(
+            platform=platform,
+            release_name=release_name,
+            owner_namespace=owner_namespace,
+            package=package,
+            version=version,
+            staging=staging,
+            upload_date=upload_date,
+            api_url=api_url,
+        )
+        self.__data.add(distribution)
+        await self.__data.commit()
+        return distribution
diff --git a/atr/validate.py b/atr/validate.py
index ad55c23..0d29952 100644
--- a/atr/validate.py
+++ b/atr/validate.py
@@ -171,7 +171,6 @@ def project(p: sql.Project) -> AnnotatedDivergences:
     yield from project_category(p)
     yield from project_committee(p)
     yield from project_created(p)
-    yield from project_distribution_channels(p)
     yield from project_full_name(p)
     yield from project_programming_languages(p)
     yield from project_release_policy(p)
@@ -235,14 +234,6 @@ def project_created(p: sql.Project) -> Divergences:
     yield from divergences_predicate(predicate, expected, p.created)
 
 
-@project_components("Project.distribution_channels")
-def project_distribution_channels(p: sql.Project) -> Divergences:
-    """Check that distribution_channels is empty."""
-    expected: list[object] = []
-    actual = p.distribution_channels
-    yield from divergences(expected, actual)
-
-
 @project_components("Project.full_name")
 def project_full_name(p: sql.Project) -> Divergences:
     """Check that the project full_name is present and starts with 'Apache 
'."""
diff --git a/migrations/versions/0018_2025.08.07_41ccdd9a.py 
b/migrations/versions/0018_2025.08.07_41ccdd9a.py
new file mode 100644
index 0000000..4094fc5
--- /dev/null
+++ b/migrations/versions/0018_2025.08.07_41ccdd9a.py
@@ -0,0 +1,65 @@
+"""Add Distribution, and remove DistributionChannel
+
+Revision ID: 0018_2025.08.07_41ccdd9a
+Revises: 0017_2025.07.29_a880eb40
+Create Date: 2025-08-07 14:32:07.523311+00:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0018_2025.08.07_41ccdd9a"
+down_revision: str | None = "0017_2025.07.29_a880eb40"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "distribution",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("release_name", sa.String(), nullable=False),
+        sa.Column(
+            "platform",
+            sa.Enum(
+                "ARTIFACTHUB", "DOCKER", "GITHUB", "MAVEN", "NPM", 
"NPM_SCOPED", "PYPI", name="distributionplatform"
+            ),
+            nullable=False,
+        ),
+        sa.Column("owner_namespace", sa.String(), nullable=True),
+        sa.Column("package", sa.String(), nullable=False),
+        sa.Column("version", sa.String(), nullable=False),
+        sa.Column("staging", sa.Boolean(), nullable=False, 
server_default=sa.false()),
+        sa.Column("upload_date", sa.DateTime(), nullable=True),
+        sa.Column("api_url", sa.String(), nullable=False),
+        sa.ForeignKeyConstraint(["release_name"], ["release.name"], 
name=op.f("fk_distribution_release_name_release")),
+        sa.PrimaryKeyConstraint("id", name=op.f("pk_distribution")),
+    )
+    with op.batch_alter_table("distributionchannel", schema=None) as batch_op:
+        batch_op.drop_index(batch_op.f("ix_distributionchannel_name"))
+
+    op.drop_table("distributionchannel")
+
+
+def downgrade() -> None:
+    op.create_table(
+        "distributionchannel",
+        sa.Column("id", sa.INTEGER(), nullable=False),
+        sa.Column("name", sa.VARCHAR(), nullable=False),
+        sa.Column("url", sa.VARCHAR(), nullable=False),
+        sa.Column("credentials", sa.VARCHAR(), nullable=False),
+        sa.Column("is_test", sa.BOOLEAN(), nullable=False),
+        sa.Column("automation_endpoint", sa.VARCHAR(), nullable=False),
+        sa.Column("project_name", sa.VARCHAR(), nullable=False),
+        sa.ForeignKeyConstraint(
+            ["project_name"], ["project.name"], 
name=op.f("fk_distributionchannel_project_name_project")
+        ),
+        sa.PrimaryKeyConstraint("id", name=op.f("pk_distributionchannel")),
+    )
+    with op.batch_alter_table("distributionchannel", schema=None) as batch_op:
+        batch_op.create_index(batch_op.f("ix_distributionchannel_name"), 
["name"], unique=1)
+
+    op.drop_table("distribution")
diff --git a/pyproject.toml b/pyproject.toml
index db3a264..8dfd794 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -129,4 +129,3 @@ select = [
 "atr/db/__init__.py" = ["C901"]
 "atr/routes/modules.py" = ["F401"]
 "migrations/env.py" = ["E402"]
-"scripts/release_path_parse.py" = ["C901", "RUF001"]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to