gabriel / musehub public
musehub_release_models.py python
131 lines 6.4 KB
Raw
sha256:3c58668648c7323bb9f5c6881cfe6a3f14fc93fcb73b537d253732952a5bf8bf chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 9 days ago
1 """ORM models for releases and release assets.
2
3 Tables:
4 - musehub_releases: Tagged releases
5 - musehub_release_assets: File attachments associated with releases
6 """
7
8 from __future__ import annotations
9
10 from datetime import datetime, timezone
11
12 import sqlalchemy as sa
13 from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
14 from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationship
15 from sqlalchemy.dialects.postgresql import JSONB
16
17 from musehub.db.database import Base
18
19
20 def _utc_now() -> datetime:
21 return datetime.now(tz=timezone.utc)
22
23
24 class MusehubRelease(MappedAsDataclass, Base):
25 """A published version release for a MuseHub repo.
26
27 Releases tie a semver ``tag`` (e.g. "v1.2.3") to a specific commit snapshot
28 and carry structured metadata:
29
30 - Semver components (major/minor/patch/pre/build) for queryable version ordering.
31 - Named distribution channel (stable | beta | alpha | nightly) replacing the
32 boolean ``is_prerelease`` — richer than a flag and machine-filterable.
33 - ``snapshot_id`` pins the release to the content-addressed object store for
34 guaranteed reproducibility forever.
35 - ``agent_id`` / ``model_id`` surface AI provenance from the tip commit.
36 - ``changelog_json`` is a JSON array of ``ChangelogEntry`` objects auto-generated
37 from typed ``sem_ver_bump`` / ``breaking_changes`` fields on commits since the
38 previous release.
39 - ``download_urls`` is a JSON object keyed by package type for generated artifacts.
40
41 ``tag`` is unique per repo — enforced by the DB constraint
42 ``uq_musehub_releases_repo_tag`` and guarded at the service layer to
43 return a clean 409 before the constraint fires.
44 """
45
46 __tablename__ = "musehub_releases"
47 __table_args__ = (UniqueConstraint("repo_id", "tag", name="uq_musehub_releases_repo_tag"),)
48
49 # --- Required fields ---
50 release_id: Mapped[str] = mapped_column(String(128), primary_key=True)
51 repo_id: Mapped[str] = mapped_column(
52 String(128),
53 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
54 nullable=False,
55 index=True,
56 )
57 tag: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
58
59 # --- Optional fields ---
60 title: Mapped[str] = mapped_column(String(500), nullable=False, default="")
61 body: Mapped[str] = mapped_column(Text, nullable=False, default="")
62 commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
63 snapshot_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None)
64 semver_major: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
65 semver_minor: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
66 semver_patch: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
67 semver_pre: Mapped[str] = mapped_column(String(255), nullable=False, default="")
68 semver_build: Mapped[str] = mapped_column(String(255), nullable=False, default="")
69 channel: Mapped[str] = mapped_column(String(20), nullable=False, default="stable", server_default="stable", index=True)
70 download_urls: Mapped[dict[str, str]] = mapped_column(JSONB, nullable=False, default_factory=dict)
71 author: Mapped[str] = mapped_column(String(255), nullable=False, default="")
72 agent_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
73 model_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
74 changelog_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
75 is_draft: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false())
76 gpg_signature: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
77 semantic_report_json: Mapped[str] = mapped_column(Text, nullable=False, default="")
78 created_at: Mapped[datetime] = mapped_column(
79 DateTime(timezone=True), nullable=False, default_factory=_utc_now
80 )
81 updated_at: Mapped[datetime] = mapped_column(
82 DateTime(timezone=True), nullable=False, default_factory=_utc_now, onupdate=_utc_now
83 )
84
85 repo: Mapped["MusehubRepo"] = relationship("MusehubRepo", back_populates="releases", init=False)
86 assets: Mapped[list[MusehubReleaseAsset]] = relationship(
87 "MusehubReleaseAsset", back_populates="release", cascade="all, delete-orphan", init=False, default_factory=list
88 )
89
90
91 class MusehubReleaseAsset(Base):
92 """An asset (file attachment) associated with a MuseHub release.
93
94 Assets represent downloadable artifacts attached to a release — for example
95 a MIDI bundle, a stems archive, or a rendered MP3. ``download_count``
96 tracks how many times the asset has been downloaded so the release page
97 can surface popularity metrics without querying the analytics pipeline.
98
99 ``release_id`` is the FK to the owning release and participates in cascade
100 deletes: removing the release removes all its assets.
101 """
102
103 __tablename__ = "musehub_release_assets"
104
105 # genesis-addressed: sha256(release_id NUL filename NUL created_at_iso)
106 asset_id: Mapped[str] = mapped_column(String(128), primary_key=True)
107 release_id: Mapped[str] = mapped_column(
108 String(128),
109 ForeignKey("musehub_releases.release_id", ondelete="CASCADE"),
110 nullable=False,
111 index=True,
112 )
113 # Denormalised so we can query assets by repo without joining releases.
114 repo_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
115 # Filename shown in the UI, e.g. "summer-sessions-v1.0.mid"
116 name: Mapped[str] = mapped_column(String(500), nullable=False)
117 # Optional human-readable label, e.g. "MIDI Bundle", "Stems Archive"
118 label: Mapped[str] = mapped_column(String(255), nullable=False, default="")
119 # MIME type, e.g. "audio/midi", "application/zip"
120 content_type: Mapped[str] = mapped_column(String(128), nullable=False, default="")
121 # File size in bytes; 0 when unknown
122 size: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
123 # Direct download URL for the artifact (pre-signed or CDN URL)
124 download_url: Mapped[str] = mapped_column(String(2048), nullable=False)
125 # Incrementing counter updated each time the asset is downloaded
126 download_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
127 created_at: Mapped[datetime] = mapped_column(
128 DateTime(timezone=True), nullable=False, default=_utc_now
129 )
130
131 release: Mapped[MusehubRelease] = relationship("MusehubRelease", back_populates="assets")
File History 1 commit
sha256:3c58668648c7323bb9f5c6881cfe6a3f14fc93fcb73b537d253732952a5bf8bf chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 9 days ago