gabriel / musehub public
musehub_label_models.py python
86 lines 3.1 KB
Raw
sha256:969ddc5e88776e70af33c016b8777bb721356508ae5f304b3fb46c451494ece7 test(mwp3): Phase 4 — fix adjacent suite regressions, lock … Sonnet 4.6 22 days ago
1 """SQLAlchemy ORM models for MuseHub label tables.
2
3 Tables:
4 - musehub_labels: Coloured label definitions scoped to a repo
5 - musehub_proposal_labels: Many-to-many join between proposals and labels
6
7 Note: issues store labels as a denormalized ARRAY(Text) on musehub_issues.labels —
8 there is no issue-label junction table. Proposals use the junction table because
9 the assignment flow goes through label_id FKs; issues use the ARRAY because
10 the assignment flow stores names directly.
11 """
12
13 from datetime import datetime, timezone
14
15 from sqlalchemy import DateTime, ForeignKey, Index, String, UniqueConstraint
16 from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationship
17
18 from musehub.db.database import Base
19
20 def _utc_now() -> datetime:
21 return datetime.now(tz=timezone.utc)
22
23 class MusehubLabel(MappedAsDataclass, Base):
24 """A coloured label tag that can be applied to issues and proposals.
25
26 Labels are scoped to a repo — the same name may exist across repos with
27 different colours. The UNIQUE(repo_id, name) constraint enforces uniqueness
28 within a repo. ``color`` stores a hex string like ``#d73a4a``.
29
30 ``label_id`` is genesis-addressed: sha256(repo_id NUL name NUL created_at_iso).
31 """
32
33 __tablename__ = "musehub_labels"
34 __table_args__ = (
35 UniqueConstraint("repo_id", "name", name="uq_musehub_labels_repo_name"),
36 Index("ix_musehub_labels_repo_id", "repo_id"),
37 )
38
39 # --- Required fields ---
40 id: Mapped[str] = mapped_column(String(128), primary_key=True)
41 repo_id: Mapped[str] = mapped_column(
42 String(128),
43 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
44 nullable=False,
45 )
46 name: Mapped[str] = mapped_column(String(50), nullable=False)
47 # Hex colour string, e.g. "#d73a4a"
48 color: Mapped[str] = mapped_column(String(7), nullable=False)
49
50 # --- Optional fields with Python-side defaults ---
51 description: Mapped[str | None] = mapped_column(String(200), nullable=True, default=None)
52 created_at: Mapped[datetime] = mapped_column(
53 DateTime(timezone=True), nullable=False, default_factory=_utc_now
54 )
55
56 proposal_labels: Mapped[list[MusehubProposalLabel]] = relationship(
57 "MusehubProposalLabel", back_populates="label", cascade="all, delete-orphan",
58 init=False, default_factory=list,
59 )
60
61 class MusehubProposalLabel(Base):
62 """Join table linking proposals to labels.
63
64 Composite primary key on (proposal_id, label_id). Both sides cascade-delete
65 so removing a proposal or a label automatically cleans up the association.
66 """
67
68 __tablename__ = "musehub_proposal_labels"
69 __table_args__ = (
70 Index("ix_musehub_proposal_labels_label_id", "label_id"),
71 )
72
73 proposal_id: Mapped[str] = mapped_column(
74 String(128),
75 ForeignKey("musehub_proposals.proposal_id", ondelete="CASCADE"),
76 primary_key=True,
77 )
78 label_id: Mapped[str] = mapped_column(
79 String(128),
80 ForeignKey("musehub_labels.id", ondelete="CASCADE"),
81 primary_key=True,
82 )
83
84 label: Mapped[MusehubLabel] = relationship(
85 "MusehubLabel", back_populates="proposal_labels"
86 )
File History 2 commits
sha256:969ddc5e88776e70af33c016b8777bb721356508ae5f304b3fb46c451494ece7 test(mwp3): Phase 4 — fix adjacent suite regressions, lock … Sonnet 4.6 22 days ago
sha256:1749b9cc5cd2583c56d3261c4c00a342c27435f3946d4bc3e70f76aa2795458a docs(mwp-3): TDD implementation plan for job-enqueue idempo… Opus 4.8 22 days ago