prism-models 0.7.6__py3-none-any.whl → 0.7.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prism_models/__init__.py CHANGED
@@ -17,6 +17,7 @@ from prism_models.chat import Contact, Conversation, ConversationMessage, Conver
17
17
  from prism_models.content import Chunk, ChunkConfig, Collection, CollectionDocument, Document, IntegrationConfig, Source, Vector
18
18
  from prism_models.feedback import Augmentation, Feedback, FeedbackAnalysis, FeedbackStatus, FeedbackType
19
19
  from prism_models.qdrant import QdrantVectorPayload, DestinationVectorPayload, PydanticType
20
+ from prism_models.notification import NotificationTarget, NotificationOutbox
20
21
 
21
22
  __all__ = [
22
23
  "POSTGRES_NAMING_CONVENTION",
@@ -51,4 +52,6 @@ __all__ = [
51
52
  "Source",
52
53
  "TimestampMixin",
53
54
  "Vector",
55
+ "NotificationTarget",
56
+ "NotificationOutbox",
54
57
  ]
@@ -36,6 +36,8 @@ class Profile(BaseModel):
36
36
  String(255), nullable=True, default="openai:gpt-4.1"
37
37
  )
38
38
  is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
39
+
40
+ is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
39
41
 
40
42
  __table_args__ = (UniqueConstraint("name", name="uq_profile_name"),)
41
43
 
@@ -0,0 +1,87 @@
1
+ """add notification tables
2
+
3
+ Revision ID: ed197421f25a
4
+ Revises: 53d6d0b55f11
5
+ Create Date: 2026-01-29 13:12:33.275063
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ from sqlalchemy.dialects import postgresql
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = 'ed197421f25a'
16
+ down_revision: Union[str, Sequence[str], None] = '53d6d0b55f11'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('notification_target',
25
+ sa.Column('provider', sa.String(length=50), nullable=False),
26
+ sa.Column('target_key', sa.String(length=100), nullable=False),
27
+ sa.Column('display_name', sa.String(length=255), nullable=True),
28
+ sa.Column('webhook_url', sa.Text(), nullable=False),
29
+ sa.Column('is_enabled', sa.Boolean(), nullable=False),
30
+ sa.Column('id', sa.Integer(), nullable=False),
31
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
32
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
33
+ sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
34
+ sa.PrimaryKeyConstraint('id', name=op.f('notification_target_pkey')),
35
+ sa.UniqueConstraint('provider', 'target_key', name='uq_notification_target_provider_key')
36
+ )
37
+ op.create_index(op.f('notification_target_id_idx'), 'notification_target', ['id'], unique=False)
38
+ op.create_table('notification_outbox',
39
+ sa.Column('provider', sa.String(length=50), nullable=False),
40
+ sa.Column('target_key', sa.String(length=100), nullable=False),
41
+ sa.Column('event_type', sa.String(length=100), nullable=False),
42
+ sa.Column('severity', sa.String(length=20), nullable=True),
43
+ sa.Column('dedupe_key', sa.String(length=255), nullable=False),
44
+ sa.Column('correlation_id', sa.String(length=100), nullable=True),
45
+ sa.Column('entity_type', sa.String(length=50), nullable=True),
46
+ sa.Column('entity_id', sa.String(length=100), nullable=True),
47
+ sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
48
+ sa.Column('status', sa.String(length=20), nullable=False),
49
+ sa.Column('priority', sa.Integer(), nullable=False),
50
+ sa.Column('attempts', sa.Integer(), nullable=False),
51
+ sa.Column('max_attempts', sa.Integer(), nullable=False),
52
+ sa.Column('next_attempt_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
53
+ sa.Column('last_error', sa.Text(), nullable=True),
54
+ sa.Column('sent_at', sa.DateTime(timezone=True), nullable=True),
55
+ sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
56
+ sa.Column('locked_by', sa.String(length=100), nullable=True),
57
+ sa.Column('locked_at', sa.DateTime(timezone=True), nullable=True),
58
+ sa.Column('id', sa.Integer(), nullable=False),
59
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
60
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
61
+ sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
62
+ sa.ForeignKeyConstraint(['provider', 'target_key'], ['notification_target.provider', 'notification_target.target_key'], name='fk_notification_outbox_target'),
63
+ sa.PrimaryKeyConstraint('id', name=op.f('notification_outbox_pkey')),
64
+ sa.UniqueConstraint('dedupe_key', name=op.f('notification_outbox_dedupe_key_key'))
65
+ )
66
+ op.create_index('ix_notification_outbox_entity', 'notification_outbox', ['entity_type', 'entity_id'], unique=False)
67
+ op.create_index('ix_notification_outbox_ready', 'notification_outbox', ['status', 'priority', 'next_attempt_at'], unique=False)
68
+ op.create_index(op.f('notification_outbox_correlation_id_idx'), 'notification_outbox', ['correlation_id'], unique=False)
69
+ op.create_index(op.f('notification_outbox_expires_at_idx'), 'notification_outbox', ['expires_at'], unique=False)
70
+ op.create_index(op.f('notification_outbox_id_idx'), 'notification_outbox', ['id'], unique=False)
71
+ op.create_index(op.f('notification_outbox_status_idx'), 'notification_outbox', ['status'], unique=False)
72
+ # ### end Alembic commands ###
73
+
74
+
75
+ def downgrade() -> None:
76
+ """Downgrade schema."""
77
+ # ### commands auto generated by Alembic - please adjust! ###
78
+ op.drop_index(op.f('notification_outbox_status_idx'), table_name='notification_outbox')
79
+ op.drop_index(op.f('notification_outbox_id_idx'), table_name='notification_outbox')
80
+ op.drop_index(op.f('notification_outbox_expires_at_idx'), table_name='notification_outbox')
81
+ op.drop_index(op.f('notification_outbox_correlation_id_idx'), table_name='notification_outbox')
82
+ op.drop_index('ix_notification_outbox_ready', table_name='notification_outbox')
83
+ op.drop_index('ix_notification_outbox_entity', table_name='notification_outbox')
84
+ op.drop_table('notification_outbox')
85
+ op.drop_index(op.f('notification_target_id_idx'), table_name='notification_target')
86
+ op.drop_table('notification_target')
87
+ # ### end Alembic commands ###
@@ -0,0 +1,32 @@
1
+ """Default column added to profile
2
+
3
+ Revision ID: 04c9051208cd
4
+ Revises: ed197421f25a
5
+ Create Date: 2026-02-03 13:08:18.193148
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '04c9051208cd'
16
+ down_revision: Union[str, Sequence[str], None] = 'ed197421f25a'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.add_column('profile', sa.Column('is_default', sa.Boolean(), nullable=False))
25
+ # ### end Alembic commands ###
26
+
27
+
28
+ def downgrade() -> None:
29
+ """Downgrade schema."""
30
+ # ### commands auto generated by Alembic - please adjust! ###
31
+ op.drop_column('profile', 'is_default')
32
+ # ### end Alembic commands ###
@@ -0,0 +1,142 @@
1
+ """Notification models for outbox-based delivery to Teams channels."""
2
+
3
+ import enum
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+ from sqlalchemy import (
8
+ Boolean,
9
+ DateTime,
10
+ ForeignKeyConstraint,
11
+ Index,
12
+ Integer,
13
+ String,
14
+ Text,
15
+ UniqueConstraint,
16
+ func,
17
+ )
18
+ from sqlalchemy.dialects.postgresql import JSONB
19
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
20
+
21
+ from prism_models.base import Base, BaseModel, TimestampMixin
22
+
23
+
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ # Enums
26
+ # ─────────────────────────────────────────────────────────────────────────────
27
+
28
+
29
+ class NotificationStatus(str, enum.Enum):
30
+ """Lifecycle states for outbox notifications."""
31
+
32
+ PENDING = "pending"
33
+ SENDING = "sending"
34
+ SENT = "sent"
35
+ FAILED = "failed"
36
+
37
+
38
+ class NotificationSeverity(str, enum.Enum):
39
+ """Severity levels for notification prioritization and formatting."""
40
+
41
+ INFO = "info"
42
+ WARN = "warn"
43
+ ERROR = "error"
44
+ CRITICAL = "critical"
45
+
46
+
47
+ class NotificationProvider(str, enum.Enum):
48
+ """Supported notification providers (only Teams for now)."""
49
+
50
+ TEAMS = "teams"
51
+
52
+ class NotificationTarget(BaseModel):
53
+ """
54
+ Webhook destinations for notifications.
55
+
56
+ Each target represents a Teams channel webhook.
57
+ Multiple event types can route to the same target.
58
+ """
59
+
60
+ __tablename__ = "notification_target"
61
+
62
+ provider: Mapped[str] = mapped_column(String(50), nullable=False)
63
+ target_key: Mapped[str] = mapped_column(String(100), nullable=False)
64
+ display_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
65
+ webhook_url: Mapped[str] = mapped_column(Text, nullable=False)
66
+ is_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
67
+
68
+ outbox_entries: Mapped[list["NotificationOutbox"]] = relationship(
69
+ back_populates="target",
70
+ foreign_keys="[NotificationOutbox.provider, NotificationOutbox.target_key]",
71
+ )
72
+
73
+ __table_args__ = (
74
+ UniqueConstraint("provider", "target_key", name="uq_notification_target_provider_key"),
75
+ )
76
+
77
+ def __repr__(self):
78
+ return f"<NotificationTarget(id={self.id}, provider='{self.provider}', key='{self.target_key}')>"
79
+
80
+
81
+ class NotificationOutbox(BaseModel):
82
+ """
83
+ Transactional outbox for reliable notification delivery.
84
+
85
+ Design notes:
86
+ - dedupe_key: Prevents duplicate sends at insert time
87
+ - entity_type + entity_id: Polymorphic reference to source entity
88
+ - locked_by + locked_at: Enables safe multi-worker polling
89
+ - priority: Critical alerts jump ahead of digests
90
+ - max_attempts: Per-notification retry limit
91
+ - expires_at: Auto-expire stale notifications
92
+ """
93
+
94
+ __tablename__ = "notification_outbox"
95
+
96
+ provider: Mapped[str] = mapped_column(String(50), nullable=False)
97
+ target_key: Mapped[str] = mapped_column(String(100), nullable=False)
98
+ event_type: Mapped[str] = mapped_column(String(100), nullable=False)
99
+ severity: Mapped[str | None] = mapped_column(String(20), nullable=True)
100
+
101
+ dedupe_key: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
102
+ correlation_id: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
103
+
104
+ entity_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
105
+ entity_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
106
+
107
+ payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
108
+
109
+ status: Mapped[str] = mapped_column(
110
+ String(20), default=NotificationStatus.PENDING.value, nullable=False, index=True
111
+ )
112
+ priority: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
113
+ attempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
114
+ max_attempts: Mapped[int] = mapped_column(Integer, default=5, nullable=False)
115
+ next_attempt_at: Mapped[datetime] = mapped_column(
116
+ DateTime(timezone=True), server_default=func.now(), nullable=False
117
+ )
118
+ last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
119
+ sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
120
+
121
+ expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
122
+
123
+ locked_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
124
+ locked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
125
+
126
+ target: Mapped[Optional["NotificationTarget"]] = relationship(
127
+ back_populates="outbox_entries",
128
+ foreign_keys=[provider, target_key],
129
+ )
130
+
131
+ __table_args__ = (
132
+ ForeignKeyConstraint(
133
+ ["provider", "target_key"],
134
+ ["notification_target.provider", "notification_target.target_key"],
135
+ name="fk_notification_outbox_target",
136
+ ),
137
+ Index("ix_notification_outbox_ready", "status", "priority", "next_attempt_at"),
138
+ Index("ix_notification_outbox_entity", "entity_type", "entity_id"),
139
+ )
140
+
141
+ def __repr__(self):
142
+ return f"<NotificationOutbox(id={self.id}, event='{self.event_type}', status='{self.status}')>"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prism-models
3
- Version: 0.7.6
3
+ Version: 0.7.8
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: sqlalchemy[asyncio]>=2.0.0
6
6
  Requires-Dist: alembic>=1.16.0
@@ -1,10 +1,11 @@
1
- prism_models/__init__.py,sha256=jD-ASYrK4-FW1txMuUnUCwvZCHN7d9ko8b_lCLuZS9s,1491
2
- prism_models/agent_profile.py,sha256=sNO9NfOpOFXgpkvxMry-5rcHNfFdWXtMV6b45JA0Wr8,8159
1
+ prism_models/__init__.py,sha256=NBynAYHnMVNrnkDdO_PZ-LlL9vnqAF1DlrTotZY-ZKM,1620
2
+ prism_models/agent_profile.py,sha256=g5gvkeYUO-W-eociuUWIE3X0Osu3BtOrjZKLjzet2Zs,8249
3
3
  prism_models/base.py,sha256=Ka8zNToTiTG0jNgzqqj2goGmPXt8zxuuSMYvotnb8wY,2281
4
4
  prism_models/chat.py,sha256=0nsp1u-zzibIYHEAvuz_lu8qzulzzVD7vOjSrwcHMjI,15205
5
5
  prism_models/config.py,sha256=gy_6HknnG17Clv8X_EBhaXEBFBLGR1PLjvN5-SFTo5E,962
6
6
  prism_models/content.py,sha256=mzVZw54UiLIdWB9-A_HdjigJNNWBevmTMbGW6copXBM,10361
7
7
  prism_models/feedback.py,sha256=k4EvZg6bJXhCuPCwYizUCIP1TYpSjBDGPdtvpKj_U4k,6761
8
+ prism_models/notification.py,sha256=ho9oIfv9f09YpQ7zsdz6MYke0mMleKEs1wP3DRPNBSY,5427
8
9
  prism_models/qdrant.py,sha256=Ta0fsLsxbT3Evdm8KEo-zw6IzoNckuWDmDZNfBtqjfA,3950
9
10
  prism_models/migration/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
10
11
  prism_models/migration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -67,7 +68,9 @@ prism_models/migration/versions/2025_12_17_1220_4d9291a3acb1_removed_qa_pair_chu
67
68
  prism_models/migration/versions/2026_01_14_1742_147509a82af8_changed_user_role_enums.py,sha256=S4ZSqgbCe0Xrp3Z1E10J1vg3Arh_bmCxk7YP1g67eDQ,2000
68
69
  prism_models/migration/versions/2026_01_15_1010_3b7d0b2e9f5a_add_contact_profile_table.py,sha256=5G-ZmYZGXJ4TNdZms3WDwhHKbUMVp2M94f5uBSVW-j4,2790
69
70
  prism_models/migration/versions/2026_01_26_1648_53d6d0b55f11_feedback_changes.py,sha256=KyAE4YxXE1FY8yC5aLoA6TXI9TlI1Bk4kNsT5RCSfBw,878
70
- prism_models-0.7.6.dist-info/METADATA,sha256=0MDnTMQHDJ2uKaQyBguMFoYZ2jx6TKj0izdDw1isFy4,283
71
- prism_models-0.7.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
72
- prism_models-0.7.6.dist-info/top_level.txt,sha256=UNzqwpLgFYU0EoyB9LiB1jTtc89A1sQ24fSEyNVvgJI,13
73
- prism_models-0.7.6.dist-info/RECORD,,
71
+ prism_models/migration/versions/2026_01_29_1312_ed197421f25a_add_notification_tables.py,sha256=KpP6B5vc55EJedHe04grvgQbyVzMSTh9t8d9qem6wVE,5197
72
+ prism_models/migration/versions/2026_02_03_1308_04c9051208cd_default_column_added_to_profile.py,sha256=n2rkjSd8unV6ngqxA1tNCZBCCSF0KOzJzjfHiR4BVnc,891
73
+ prism_models-0.7.8.dist-info/METADATA,sha256=uUHkcy_EOXosG60td54paJBmLnQb79arYAFFRG0Qexs,283
74
+ prism_models-0.7.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
75
+ prism_models-0.7.8.dist-info/top_level.txt,sha256=UNzqwpLgFYU0EoyB9LiB1jTtc89A1sQ24fSEyNVvgJI,13
76
+ prism_models-0.7.8.dist-info/RECORD,,