gabriel / musehub public
musehub_webhook_models.py python
101 lines 4.2 KB
Raw
sha256:5667a3e21bf16fd2e6d6bd4a769bd1c0cf7634afa12cef6450cc77573196b7f9 asyncpg caps query parameters Human patch 9 days ago
1 """ORM models for webhooks and delivery logs.
2
3 Tables:
4 - musehub_webhooks: Registered webhook subscriptions per repo
5 - musehub_webhook_deliveries: Delivery log for each webhook dispatch attempt
6 """
7
8 from __future__ import annotations
9
10 from datetime import datetime, timezone
11
12 import sqlalchemy as sa
13 from sqlalchemy import ARRAY, Boolean, DateTime, ForeignKey, Integer, String, Text
14 from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationship
15
16 from musehub.db.database import Base
17
18
19 def _utc_now() -> datetime:
20 return datetime.now(tz=timezone.utc)
21
22
23 class MusehubWebhook(MappedAsDataclass, Base):
24 """A registered webhook subscription for a MuseHub repo.
25
26 When an event matching one of the subscribed ``events`` types fires, the
27 dispatcher POSTs a signed JSON payload to ``url``. The ``secret`` is used
28 to compute an HMAC-SHA256 signature sent in the ``X-MuseHub-Signature``
29 header so receivers can verify authenticity without trusting the network.
30
31 ``events`` is a JSON list of event-type strings (e.g. ``["push", "issue"]``).
32 An empty list means the webhook receives no events and is effectively paused.
33 """
34
35 __tablename__ = "musehub_webhooks"
36
37 # --- Required fields ---
38 # genesis-addressed: sha256(repo_id NUL url NUL created_at_iso)
39 webhook_id: Mapped[str] = mapped_column(String(128), primary_key=True)
40 repo_id: Mapped[str] = mapped_column(
41 String(128),
42 ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"),
43 nullable=False,
44 index=True,
45 )
46 url: Mapped[str] = mapped_column(String(2048), nullable=False)
47
48 # --- Optional fields ---
49 events: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
50 secret: Mapped[str] = mapped_column(Text, nullable=False, default="")
51 active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=sa.true())
52 created_at: Mapped[datetime] = mapped_column(
53 DateTime(timezone=True), nullable=False, default_factory=_utc_now
54 )
55 updated_at: Mapped[datetime] = mapped_column(
56 DateTime(timezone=True), nullable=False, default_factory=_utc_now, onupdate=_utc_now
57 )
58
59 repo: Mapped["MusehubRepo"] = relationship("MusehubRepo", back_populates="webhooks", init=False)
60 deliveries: Mapped[list[MusehubWebhookDelivery]] = relationship(
61 "MusehubWebhookDelivery", back_populates="webhook", cascade="all, delete-orphan", init=False, default_factory=list
62 )
63
64
65 class MusehubWebhookDelivery(Base):
66 """One delivery attempt for a webhook event.
67
68 Each row records the outcome of a single HTTP POST to a ``MusehubWebhook``
69 URL. The dispatcher creates one row per attempt (including retries), so a
70 delivery that required 3 attempts produces 3 rows with the same
71 ``event_type`` and incrementing ``attempt`` counters.
72
73 ``success`` is True only when the receiver responded with a 2xx status.
74 ``response_status`` is 0 when the request did not reach the server
75 (network error, DNS failure, timeout).
76 """
77
78 __tablename__ = "musehub_webhook_deliveries"
79
80 delivery_id: Mapped[str] = mapped_column(String(128), primary_key=True)
81 webhook_id: Mapped[str] = mapped_column(
82 String(128),
83 ForeignKey("musehub_webhooks.webhook_id", ondelete="CASCADE"),
84 nullable=False,
85 index=True,
86 )
87 event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
88 # JSON-encoded payload bytes that were (or will be) sent to the subscriber URL.
89 # Stored so that failed deliveries can be retried with the original payload.
90 payload: Mapped[str] = mapped_column(Text, nullable=False, default="")
91 attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
92 success: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false())
93 response_status: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
94 response_body: Mapped[str] = mapped_column(Text, nullable=False, default="")
95 delivered_at: Mapped[datetime] = mapped_column(
96 DateTime(timezone=True), nullable=False, default=_utc_now
97 )
98
99 webhook: Mapped[MusehubWebhook] = relationship(
100 "MusehubWebhook", back_populates="deliveries"
101 )
File History 1 commit
sha256:5667a3e21bf16fd2e6d6bd4a769bd1c0cf7634afa12cef6450cc77573196b7f9 asyncpg caps query parameters Human patch 9 days ago