nornweave 0.1.2__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.
Files changed (80) hide show
  1. nornweave/__init__.py +3 -0
  2. nornweave/adapters/__init__.py +1 -0
  3. nornweave/adapters/base.py +5 -0
  4. nornweave/adapters/mailgun.py +196 -0
  5. nornweave/adapters/resend.py +510 -0
  6. nornweave/adapters/sendgrid.py +492 -0
  7. nornweave/adapters/ses.py +824 -0
  8. nornweave/cli.py +186 -0
  9. nornweave/core/__init__.py +26 -0
  10. nornweave/core/config.py +172 -0
  11. nornweave/core/exceptions.py +25 -0
  12. nornweave/core/interfaces.py +390 -0
  13. nornweave/core/storage.py +192 -0
  14. nornweave/core/utils.py +23 -0
  15. nornweave/huginn/__init__.py +10 -0
  16. nornweave/huginn/client.py +296 -0
  17. nornweave/huginn/config.py +52 -0
  18. nornweave/huginn/resources.py +165 -0
  19. nornweave/huginn/server.py +202 -0
  20. nornweave/models/__init__.py +113 -0
  21. nornweave/models/attachment.py +136 -0
  22. nornweave/models/event.py +275 -0
  23. nornweave/models/inbox.py +33 -0
  24. nornweave/models/message.py +284 -0
  25. nornweave/models/thread.py +172 -0
  26. nornweave/muninn/__init__.py +14 -0
  27. nornweave/muninn/tools.py +207 -0
  28. nornweave/search/__init__.py +1 -0
  29. nornweave/search/embeddings.py +1 -0
  30. nornweave/search/vector_store.py +1 -0
  31. nornweave/skuld/__init__.py +1 -0
  32. nornweave/skuld/rate_limiter.py +1 -0
  33. nornweave/skuld/scheduler.py +1 -0
  34. nornweave/skuld/sender.py +25 -0
  35. nornweave/skuld/webhooks.py +1 -0
  36. nornweave/storage/__init__.py +20 -0
  37. nornweave/storage/database.py +165 -0
  38. nornweave/storage/gcs.py +144 -0
  39. nornweave/storage/local.py +152 -0
  40. nornweave/storage/s3.py +164 -0
  41. nornweave/urdr/__init__.py +14 -0
  42. nornweave/urdr/adapters/__init__.py +16 -0
  43. nornweave/urdr/adapters/base.py +385 -0
  44. nornweave/urdr/adapters/postgres.py +50 -0
  45. nornweave/urdr/adapters/sqlite.py +51 -0
  46. nornweave/urdr/migrations/env.py +94 -0
  47. nornweave/urdr/migrations/script.py.mako +26 -0
  48. nornweave/urdr/migrations/versions/.gitkeep +0 -0
  49. nornweave/urdr/migrations/versions/20260131_0001_initial_schema.py +182 -0
  50. nornweave/urdr/migrations/versions/20260131_0002_extended_schema.py +241 -0
  51. nornweave/urdr/orm.py +641 -0
  52. nornweave/verdandi/__init__.py +45 -0
  53. nornweave/verdandi/attachments.py +471 -0
  54. nornweave/verdandi/content.py +420 -0
  55. nornweave/verdandi/headers.py +404 -0
  56. nornweave/verdandi/parser.py +25 -0
  57. nornweave/verdandi/sanitizer.py +9 -0
  58. nornweave/verdandi/threading.py +359 -0
  59. nornweave/yggdrasil/__init__.py +1 -0
  60. nornweave/yggdrasil/app.py +86 -0
  61. nornweave/yggdrasil/dependencies.py +190 -0
  62. nornweave/yggdrasil/middleware/__init__.py +1 -0
  63. nornweave/yggdrasil/middleware/auth.py +1 -0
  64. nornweave/yggdrasil/middleware/logging.py +1 -0
  65. nornweave/yggdrasil/routes/__init__.py +1 -0
  66. nornweave/yggdrasil/routes/v1/__init__.py +1 -0
  67. nornweave/yggdrasil/routes/v1/inboxes.py +124 -0
  68. nornweave/yggdrasil/routes/v1/messages.py +200 -0
  69. nornweave/yggdrasil/routes/v1/search.py +84 -0
  70. nornweave/yggdrasil/routes/v1/threads.py +142 -0
  71. nornweave/yggdrasil/routes/webhooks/__init__.py +1 -0
  72. nornweave/yggdrasil/routes/webhooks/mailgun.py +136 -0
  73. nornweave/yggdrasil/routes/webhooks/resend.py +344 -0
  74. nornweave/yggdrasil/routes/webhooks/sendgrid.py +15 -0
  75. nornweave/yggdrasil/routes/webhooks/ses.py +15 -0
  76. nornweave-0.1.2.dist-info/METADATA +324 -0
  77. nornweave-0.1.2.dist-info/RECORD +80 -0
  78. nornweave-0.1.2.dist-info/WHEEL +4 -0
  79. nornweave-0.1.2.dist-info/entry_points.txt +5 -0
  80. nornweave-0.1.2.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,182 @@
1
+ """Initial schema: inboxes, threads, messages, events.
2
+
3
+ Revision ID: 0001
4
+ Revises:
5
+ Create Date: 2026-01-31
6
+
7
+ """
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Sequence
16
+
17
+ # revision identifiers, used by Alembic.
18
+ revision: str = "0001"
19
+ down_revision: str | None = None
20
+ branch_labels: str | Sequence[str] | None = None
21
+ depends_on: str | Sequence[str] | None = None
22
+
23
+
24
+ def upgrade() -> None:
25
+ """Create all tables and indexes."""
26
+ # ==========================================================================
27
+ # Inboxes table
28
+ # ==========================================================================
29
+ op.create_table(
30
+ "inboxes",
31
+ sa.Column("id", sa.String(36), primary_key=True),
32
+ sa.Column("email_address", sa.String(255), unique=True, nullable=False),
33
+ sa.Column("name", sa.String(255), nullable=True),
34
+ sa.Column("provider_config", sa.JSON(), nullable=False, server_default="{}"),
35
+ )
36
+ # Index on email_address is created by unique=True
37
+
38
+ # ==========================================================================
39
+ # Threads table
40
+ # ==========================================================================
41
+ op.create_table(
42
+ "threads",
43
+ sa.Column("id", sa.String(36), primary_key=True),
44
+ sa.Column(
45
+ "inbox_id",
46
+ sa.String(36),
47
+ sa.ForeignKey("inboxes.id", ondelete="CASCADE"),
48
+ nullable=False,
49
+ ),
50
+ sa.Column("subject", sa.Text(), nullable=False),
51
+ sa.Column("last_message_at", sa.DateTime(timezone=True), nullable=True),
52
+ sa.Column("participant_hash", sa.String(64), nullable=True),
53
+ )
54
+
55
+ # Index for list_threads_for_inbox: ORDER BY last_message_at DESC
56
+ op.create_index(
57
+ "ix_threads_inbox_last_message",
58
+ "threads",
59
+ ["inbox_id", sa.text("last_message_at DESC")],
60
+ )
61
+
62
+ # Index for get_thread_by_participant_hash
63
+ op.create_index(
64
+ "ix_threads_inbox_participant_hash",
65
+ "threads",
66
+ ["inbox_id", "participant_hash"],
67
+ )
68
+
69
+ # ==========================================================================
70
+ # Messages table
71
+ # ==========================================================================
72
+ op.create_table(
73
+ "messages",
74
+ sa.Column("id", sa.String(36), primary_key=True),
75
+ sa.Column(
76
+ "thread_id",
77
+ sa.String(36),
78
+ sa.ForeignKey("threads.id", ondelete="CASCADE"),
79
+ nullable=False,
80
+ ),
81
+ sa.Column(
82
+ "inbox_id",
83
+ sa.String(36),
84
+ sa.ForeignKey("inboxes.id", ondelete="CASCADE"),
85
+ nullable=False,
86
+ ),
87
+ sa.Column("provider_message_id", sa.String(512), nullable=True),
88
+ sa.Column("direction", sa.String(20), nullable=False),
89
+ sa.Column("content_raw", sa.Text(), nullable=False, server_default=""),
90
+ sa.Column("content_clean", sa.Text(), nullable=False, server_default=""),
91
+ sa.Column("metadata", sa.JSON(), nullable=False, server_default="{}"),
92
+ sa.Column(
93
+ "created_at",
94
+ sa.DateTime(timezone=True),
95
+ nullable=True,
96
+ server_default=sa.func.now(),
97
+ ),
98
+ )
99
+
100
+ # Index for list_messages_for_thread: ORDER BY created_at
101
+ op.create_index(
102
+ "ix_messages_thread_created",
103
+ "messages",
104
+ ["thread_id", "created_at"],
105
+ )
106
+
107
+ # Index for list_messages_for_inbox: ORDER BY created_at
108
+ op.create_index(
109
+ "ix_messages_inbox_created",
110
+ "messages",
111
+ ["inbox_id", "created_at"],
112
+ )
113
+
114
+ # Index for search_messages: filter by inbox
115
+ op.create_index(
116
+ "ix_messages_inbox_id",
117
+ "messages",
118
+ ["inbox_id"],
119
+ )
120
+
121
+ # Partial unique index for deduplication (Postgres only, SQLite ignores where clause)
122
+ # This prevents duplicate provider_message_id per inbox
123
+ op.create_index(
124
+ "ix_messages_inbox_provider_msg",
125
+ "messages",
126
+ ["inbox_id", "provider_message_id"],
127
+ unique=True,
128
+ postgresql_where=sa.text("provider_message_id IS NOT NULL"),
129
+ )
130
+
131
+ # ==========================================================================
132
+ # Events table (Phase 3 webhooks)
133
+ # ==========================================================================
134
+ op.create_table(
135
+ "events",
136
+ sa.Column("id", sa.String(36), primary_key=True),
137
+ sa.Column("type", sa.String(50), nullable=False),
138
+ sa.Column(
139
+ "created_at",
140
+ sa.DateTime(timezone=True),
141
+ nullable=False,
142
+ server_default=sa.func.now(),
143
+ ),
144
+ sa.Column("payload", sa.JSON(), nullable=False, server_default="{}"),
145
+ )
146
+
147
+ # Index for list_events: ORDER BY created_at DESC
148
+ op.create_index(
149
+ "ix_events_created_at",
150
+ "events",
151
+ [sa.text("created_at DESC")],
152
+ )
153
+
154
+ # Index for list_events(type=...): filter by type, ORDER BY created_at DESC
155
+ op.create_index(
156
+ "ix_events_type_created",
157
+ "events",
158
+ ["type", sa.text("created_at DESC")],
159
+ )
160
+
161
+
162
+ def downgrade() -> None:
163
+ """Drop all tables and indexes."""
164
+ # Drop events
165
+ op.drop_index("ix_events_type_created", table_name="events")
166
+ op.drop_index("ix_events_created_at", table_name="events")
167
+ op.drop_table("events")
168
+
169
+ # Drop messages
170
+ op.drop_index("ix_messages_inbox_provider_msg", table_name="messages")
171
+ op.drop_index("ix_messages_inbox_id", table_name="messages")
172
+ op.drop_index("ix_messages_inbox_created", table_name="messages")
173
+ op.drop_index("ix_messages_thread_created", table_name="messages")
174
+ op.drop_table("messages")
175
+
176
+ # Drop threads
177
+ op.drop_index("ix_threads_inbox_participant_hash", table_name="threads")
178
+ op.drop_index("ix_threads_inbox_last_message", table_name="threads")
179
+ op.drop_table("threads")
180
+
181
+ # Drop inboxes
182
+ op.drop_table("inboxes")
@@ -0,0 +1,241 @@
1
+ """Add extended schema fields: attachments table, extended messages/threads/events.
2
+
3
+ Revision ID: 0002
4
+ Revises: 0001
5
+ Create Date: 2026-01-31
6
+
7
+ """
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Sequence
16
+
17
+ # revision identifiers, used by Alembic.
18
+ revision: str = "0002"
19
+ down_revision: str | None = "0001"
20
+ branch_labels: str | Sequence[str] | None = None
21
+ depends_on: str | Sequence[str] | None = None
22
+
23
+
24
+ def upgrade() -> None:
25
+ """Add new tables and columns for extended schema support."""
26
+ # ==========================================================================
27
+ # Attachments table (new)
28
+ # ==========================================================================
29
+ op.create_table(
30
+ "attachments",
31
+ sa.Column("id", sa.String(36), primary_key=True),
32
+ sa.Column(
33
+ "message_id",
34
+ sa.String(36),
35
+ sa.ForeignKey("messages.id", ondelete="CASCADE"),
36
+ nullable=False,
37
+ ),
38
+ sa.Column("filename", sa.String(512), nullable=False),
39
+ sa.Column("content_type", sa.String(255), nullable=False),
40
+ sa.Column("size_bytes", sa.Integer(), nullable=False, server_default="0"),
41
+ sa.Column("disposition", sa.String(20), nullable=False, server_default="attachment"),
42
+ sa.Column("content_id", sa.String(255), nullable=True),
43
+ sa.Column("content", sa.LargeBinary(), nullable=True),
44
+ sa.Column("storage_path", sa.String(1024), nullable=True),
45
+ sa.Column("storage_backend", sa.String(50), nullable=True),
46
+ sa.Column("content_hash", sa.String(64), nullable=True),
47
+ sa.Column(
48
+ "created_at",
49
+ sa.DateTime(timezone=True),
50
+ nullable=False,
51
+ server_default=sa.func.now(),
52
+ ),
53
+ )
54
+ op.create_index("ix_attachments_message_id", "attachments", ["message_id"])
55
+ op.create_index("ix_attachments_content_id", "attachments", ["content_id"])
56
+
57
+ # ==========================================================================
58
+ # Threads table updates
59
+ # ==========================================================================
60
+ # Add new columns
61
+ op.add_column("threads", sa.Column("labels", sa.JSON(), nullable=True))
62
+ op.add_column(
63
+ "threads",
64
+ sa.Column("timestamp", sa.DateTime(timezone=True), nullable=True),
65
+ )
66
+ op.add_column(
67
+ "threads",
68
+ sa.Column("received_timestamp", sa.DateTime(timezone=True), nullable=True),
69
+ )
70
+ op.add_column(
71
+ "threads",
72
+ sa.Column("sent_timestamp", sa.DateTime(timezone=True), nullable=True),
73
+ )
74
+ op.add_column(
75
+ "threads",
76
+ sa.Column(
77
+ "created_at",
78
+ sa.DateTime(timezone=True),
79
+ nullable=True,
80
+ server_default=sa.func.now(),
81
+ ),
82
+ )
83
+ op.add_column(
84
+ "threads",
85
+ sa.Column(
86
+ "updated_at",
87
+ sa.DateTime(timezone=True),
88
+ nullable=True,
89
+ server_default=sa.func.now(),
90
+ ),
91
+ )
92
+ op.add_column("threads", sa.Column("senders", sa.JSON(), nullable=True))
93
+ op.add_column("threads", sa.Column("recipients", sa.JSON(), nullable=True))
94
+ op.add_column("threads", sa.Column("normalized_subject", sa.String(512), nullable=True))
95
+ op.add_column("threads", sa.Column("preview", sa.String(255), nullable=True))
96
+ op.add_column("threads", sa.Column("last_message_id", sa.String(36), nullable=True))
97
+ op.add_column(
98
+ "threads", sa.Column("message_count", sa.Integer(), nullable=True, server_default="0")
99
+ )
100
+ op.add_column("threads", sa.Column("size", sa.Integer(), nullable=True, server_default="0"))
101
+
102
+ # Create index on normalized_subject
103
+ op.create_index(
104
+ "ix_threads_inbox_normalized_subject",
105
+ "threads",
106
+ ["inbox_id", "normalized_subject"],
107
+ )
108
+
109
+ # ==========================================================================
110
+ # Messages table updates
111
+ # ==========================================================================
112
+ # Add new columns
113
+ op.add_column("messages", sa.Column("labels", sa.JSON(), nullable=True))
114
+ op.add_column(
115
+ "messages",
116
+ sa.Column("timestamp", sa.DateTime(timezone=True), nullable=True),
117
+ )
118
+ op.add_column(
119
+ "messages",
120
+ sa.Column(
121
+ "updated_at",
122
+ sa.DateTime(timezone=True),
123
+ nullable=True,
124
+ server_default=sa.func.now(),
125
+ ),
126
+ )
127
+ op.add_column("messages", sa.Column("from_address", sa.String(512), nullable=True))
128
+ op.add_column("messages", sa.Column("reply_to_addresses", sa.JSON(), nullable=True))
129
+ op.add_column("messages", sa.Column("to_addresses", sa.JSON(), nullable=True))
130
+ op.add_column("messages", sa.Column("cc_addresses", sa.JSON(), nullable=True))
131
+ op.add_column("messages", sa.Column("bcc_addresses", sa.JSON(), nullable=True))
132
+ op.add_column("messages", sa.Column("subject", sa.Text(), nullable=True))
133
+ op.add_column("messages", sa.Column("preview", sa.String(255), nullable=True))
134
+ op.add_column("messages", sa.Column("text", sa.Text(), nullable=True))
135
+ op.add_column("messages", sa.Column("html", sa.Text(), nullable=True))
136
+ op.add_column("messages", sa.Column("extracted_text", sa.Text(), nullable=True))
137
+ op.add_column("messages", sa.Column("extracted_html", sa.Text(), nullable=True))
138
+ op.add_column("messages", sa.Column("in_reply_to", sa.String(512), nullable=True))
139
+ op.add_column("messages", sa.Column("references", sa.JSON(), nullable=True))
140
+ op.add_column("messages", sa.Column("headers", sa.JSON(), nullable=True))
141
+ op.add_column("messages", sa.Column("size", sa.Integer(), nullable=True, server_default="0"))
142
+
143
+ # Create index on timestamp
144
+ op.create_index("ix_messages_timestamp", "messages", ["timestamp"])
145
+
146
+ # ==========================================================================
147
+ # Events table updates
148
+ # ==========================================================================
149
+ # Add new columns
150
+ op.add_column("events", sa.Column("event_type", sa.String(50), nullable=True))
151
+ op.add_column(
152
+ "events",
153
+ sa.Column(
154
+ "inbox_id",
155
+ sa.String(36),
156
+ sa.ForeignKey("inboxes.id", ondelete="SET NULL"),
157
+ nullable=True,
158
+ ),
159
+ )
160
+ op.add_column(
161
+ "events",
162
+ sa.Column(
163
+ "thread_id",
164
+ sa.String(36),
165
+ sa.ForeignKey("threads.id", ondelete="SET NULL"),
166
+ nullable=True,
167
+ ),
168
+ )
169
+ op.add_column(
170
+ "events",
171
+ sa.Column(
172
+ "message_id",
173
+ sa.String(36),
174
+ sa.ForeignKey("messages.id", ondelete="SET NULL"),
175
+ nullable=True,
176
+ ),
177
+ )
178
+ op.add_column(
179
+ "events",
180
+ sa.Column("timestamp", sa.DateTime(timezone=True), nullable=True),
181
+ )
182
+
183
+ # Create indexes
184
+ op.create_index("ix_events_event_type", "events", ["event_type"])
185
+ op.create_index("ix_events_inbox_id", "events", ["inbox_id"])
186
+ op.create_index("ix_events_timestamp", "events", [sa.text("timestamp DESC")])
187
+
188
+
189
+ def downgrade() -> None:
190
+ """Remove extended schema updates."""
191
+ # Drop events columns and indexes
192
+ op.drop_index("ix_events_timestamp", table_name="events")
193
+ op.drop_index("ix_events_inbox_id", table_name="events")
194
+ op.drop_index("ix_events_event_type", table_name="events")
195
+ op.drop_column("events", "timestamp")
196
+ op.drop_column("events", "message_id")
197
+ op.drop_column("events", "thread_id")
198
+ op.drop_column("events", "inbox_id")
199
+ op.drop_column("events", "event_type")
200
+
201
+ # Drop messages columns and indexes
202
+ op.drop_index("ix_messages_timestamp", table_name="messages")
203
+ op.drop_column("messages", "size")
204
+ op.drop_column("messages", "headers")
205
+ op.drop_column("messages", "references")
206
+ op.drop_column("messages", "in_reply_to")
207
+ op.drop_column("messages", "extracted_html")
208
+ op.drop_column("messages", "extracted_text")
209
+ op.drop_column("messages", "html")
210
+ op.drop_column("messages", "text")
211
+ op.drop_column("messages", "preview")
212
+ op.drop_column("messages", "subject")
213
+ op.drop_column("messages", "bcc_addresses")
214
+ op.drop_column("messages", "cc_addresses")
215
+ op.drop_column("messages", "to_addresses")
216
+ op.drop_column("messages", "reply_to_addresses")
217
+ op.drop_column("messages", "from_address")
218
+ op.drop_column("messages", "updated_at")
219
+ op.drop_column("messages", "timestamp")
220
+ op.drop_column("messages", "labels")
221
+
222
+ # Drop threads columns and indexes
223
+ op.drop_index("ix_threads_inbox_normalized_subject", table_name="threads")
224
+ op.drop_column("threads", "size")
225
+ op.drop_column("threads", "message_count")
226
+ op.drop_column("threads", "last_message_id")
227
+ op.drop_column("threads", "preview")
228
+ op.drop_column("threads", "normalized_subject")
229
+ op.drop_column("threads", "recipients")
230
+ op.drop_column("threads", "senders")
231
+ op.drop_column("threads", "updated_at")
232
+ op.drop_column("threads", "created_at")
233
+ op.drop_column("threads", "sent_timestamp")
234
+ op.drop_column("threads", "received_timestamp")
235
+ op.drop_column("threads", "timestamp")
236
+ op.drop_column("threads", "labels")
237
+
238
+ # Drop attachments table
239
+ op.drop_index("ix_attachments_content_id", table_name="attachments")
240
+ op.drop_index("ix_attachments_message_id", table_name="attachments")
241
+ op.drop_table("attachments")