musehub_stash_models.py
python
| 1 | """SQLAlchemy ORM models for Muse Hub stash — a temporary shelf for uncommitted changes. |
| 2 | |
| 3 | Analogous to git stash: musicians can save in-progress work, switch context, |
| 4 | and pop the stash later to resume. Each stash record captures the branch it |
| 5 | was created on plus zero or more MIDI file snapshots (entries). |
| 6 | |
| 7 | Tables: |
| 8 | - musehub_stash: one stash record per save point |
| 9 | - musehub_stash_entries: individual MIDI file snapshots within a stash |
| 10 | """ |
| 11 | from __future__ import annotations |
| 12 | |
| 13 | import uuid |
| 14 | from datetime import datetime, timezone |
| 15 | |
| 16 | from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String |
| 17 | from sqlalchemy.orm import Mapped, mapped_column, relationship |
| 18 | |
| 19 | from musehub.db.database import Base |
| 20 | |
| 21 | |
| 22 | def _new_uuid() -> str: |
| 23 | return str(uuid.uuid4()) |
| 24 | |
| 25 | |
| 26 | def _utc_now() -> datetime: |
| 27 | return datetime.now(tz=timezone.utc) |
| 28 | |
| 29 | |
| 30 | class MusehubStash(Base): |
| 31 | """A stash record — a named save point for uncommitted Muse changes. |
| 32 | |
| 33 | ``branch`` records which branch the stash was created on so the user |
| 34 | can be warned if they try to pop it on a different branch. |
| 35 | ``message`` is an optional free-text description (up to 500 chars). |
| 36 | ``is_applied`` flips to True when the stash has been popped back into |
| 37 | the working tree; ``applied_at`` records the exact timestamp. |
| 38 | """ |
| 39 | |
| 40 | __tablename__ = "musehub_stash" |
| 41 | __table_args__ = ( |
| 42 | Index("ix_musehub_stash_repo_id", "repo_id"), |
| 43 | Index("ix_musehub_stash_user_id", "user_id"), |
| 44 | ) |
| 45 | |
| 46 | id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_uuid) |
| 47 | repo_id: Mapped[str] = mapped_column( |
| 48 | String(36), |
| 49 | ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"), |
| 50 | nullable=False, |
| 51 | ) |
| 52 | user_id: Mapped[str] = mapped_column( |
| 53 | String(36), |
| 54 | ForeignKey("maestro_users.id", ondelete="CASCADE"), |
| 55 | nullable=False, |
| 56 | ) |
| 57 | branch: Mapped[str] = mapped_column(String(255), nullable=False) |
| 58 | message: Mapped[str | None] = mapped_column(String(500), nullable=True) |
| 59 | is_applied: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) |
| 60 | created_at: Mapped[datetime] = mapped_column( |
| 61 | DateTime(timezone=True), nullable=False, default=_utc_now |
| 62 | ) |
| 63 | applied_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) |
| 64 | |
| 65 | entries: Mapped[list[MusehubStashEntry]] = relationship( |
| 66 | "MusehubStashEntry", |
| 67 | back_populates="stash", |
| 68 | cascade="all, delete-orphan", |
| 69 | order_by="MusehubStashEntry.position", |
| 70 | ) |
| 71 | |
| 72 | |
| 73 | class MusehubStashEntry(Base): |
| 74 | """A single MIDI file snapshot within a stash. |
| 75 | |
| 76 | ``path`` is the MIDI file's path relative to the repo root. |
| 77 | ``object_id`` is the content-addressed hash of the file at stash time, |
| 78 | matching the format used in ``musehub_objects`` (e.g. ``sha256:<hex>``). |
| 79 | ``position`` preserves the order of entries within the stash so pop |
| 80 | restores files in a deterministic sequence. |
| 81 | """ |
| 82 | |
| 83 | __tablename__ = "musehub_stash_entries" |
| 84 | __table_args__ = (Index("ix_musehub_stash_entries_stash_id", "stash_id"),) |
| 85 | |
| 86 | id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_uuid) |
| 87 | stash_id: Mapped[str] = mapped_column( |
| 88 | String(36), |
| 89 | ForeignKey("musehub_stash.id", ondelete="CASCADE"), |
| 90 | nullable=False, |
| 91 | ) |
| 92 | path: Mapped[str] = mapped_column(String(1024), nullable=False) |
| 93 | object_id: Mapped[str] = mapped_column(String(128), nullable=False) |
| 94 | position: Mapped[int] = mapped_column(Integer, nullable=False) |
| 95 | |
| 96 | stash: Mapped[MusehubStash] = relationship("MusehubStash", back_populates="entries") |