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
nornweave/cli.py ADDED
@@ -0,0 +1,186 @@
1
+ """NornWeave CLI.
2
+
3
+ Provides commands for running the API server and MCP server.
4
+ """
5
+
6
+ import sys
7
+ from typing import Literal
8
+
9
+ import click
10
+
11
+
12
+ @click.group()
13
+ @click.version_option()
14
+ def cli() -> None:
15
+ """NornWeave - Inbox-as-a-Service API for AI Agents.
16
+
17
+ Run the API server or MCP server for AI agent integration.
18
+ """
19
+
20
+
21
+ @cli.command("api")
22
+ @click.option(
23
+ "--host",
24
+ default="0.0.0.0",
25
+ help="Host to bind to",
26
+ show_default=True,
27
+ )
28
+ @click.option(
29
+ "--port",
30
+ default=8000,
31
+ type=int,
32
+ help="Port to listen on",
33
+ show_default=True,
34
+ )
35
+ @click.option(
36
+ "--reload",
37
+ is_flag=True,
38
+ help="Enable auto-reload for development",
39
+ )
40
+ def api_cmd(host: str, port: int, reload: bool) -> None:
41
+ """Run the NornWeave REST API server.
42
+
43
+ This starts the FastAPI server that handles webhooks, REST API requests,
44
+ and serves as the backend for the MCP server.
45
+
46
+ Examples:
47
+
48
+ nornweave api
49
+
50
+ nornweave api --port 9000
51
+
52
+ nornweave api --reload
53
+ """
54
+ import uvicorn
55
+
56
+ uvicorn.run(
57
+ "nornweave.yggdrasil.app:app",
58
+ host=host,
59
+ port=port,
60
+ reload=reload,
61
+ )
62
+
63
+
64
+ @cli.command("mcp")
65
+ @click.option(
66
+ "--transport",
67
+ type=click.Choice(["stdio", "sse", "http"]),
68
+ default="stdio",
69
+ help="MCP transport type",
70
+ show_default=True,
71
+ )
72
+ @click.option(
73
+ "--host",
74
+ default="0.0.0.0",
75
+ help="Host to bind to (SSE/HTTP only)",
76
+ show_default=True,
77
+ )
78
+ @click.option(
79
+ "--port",
80
+ default=3000,
81
+ type=int,
82
+ help="Port to listen on (SSE/HTTP only)",
83
+ show_default=True,
84
+ )
85
+ @click.option(
86
+ "--api-url",
87
+ envvar="NORNWEAVE_API_URL",
88
+ default="http://localhost:8000",
89
+ help="NornWeave API URL",
90
+ show_default=True,
91
+ )
92
+ def mcp_cmd(
93
+ transport: Literal["stdio", "sse", "http"],
94
+ host: str,
95
+ port: int,
96
+ api_url: str,
97
+ ) -> None:
98
+ """Run the NornWeave MCP server.
99
+
100
+ This starts the Model Context Protocol server that allows AI agents
101
+ (Claude, Cursor, LangChain) to interact with email.
102
+
103
+ Transports:
104
+
105
+ stdio - Standard input/output (default). For Claude Desktop, Cursor.
106
+
107
+ sse - Server-Sent Events. For web-based MCP clients.
108
+
109
+ http - Streamable HTTP. For cloud deployments, LangChain.
110
+
111
+ Environment Variables:
112
+
113
+ NORNWEAVE_API_URL - NornWeave API base URL (default: http://localhost:8000)
114
+
115
+ NORNWEAVE_API_KEY - API key for authentication (optional)
116
+
117
+ Examples:
118
+
119
+ nornweave mcp
120
+
121
+ nornweave mcp --transport sse --port 3000
122
+
123
+ nornweave mcp --transport http --host 127.0.0.1 --port 8080
124
+
125
+ nornweave mcp --api-url http://api.example.com:8000
126
+
127
+ Claude Desktop / Cursor Configuration:
128
+
129
+ {
130
+ "mcpServers": {
131
+ "nornweave": {
132
+ "command": "nornweave",
133
+ "args": ["mcp"]
134
+ }
135
+ }
136
+ }
137
+ """
138
+ # Set API URL in environment for the MCP server to pick up
139
+ import os
140
+
141
+ os.environ["NORNWEAVE_API_URL"] = api_url
142
+
143
+ try:
144
+ from nornweave.huginn.server import serve
145
+ except ImportError as e:
146
+ click.echo(
147
+ "Error: MCP dependencies not installed. Install with: pip install nornweave[mcp]",
148
+ err=True,
149
+ )
150
+ click.echo(f"Details: {e}", err=True)
151
+ sys.exit(1)
152
+
153
+ if transport == "stdio":
154
+ click.echo("Starting MCP server (stdio transport)...", err=True)
155
+ click.echo(f"API URL: {api_url}", err=True)
156
+ else:
157
+ click.echo(f"Starting MCP server ({transport} transport)...", err=True)
158
+ click.echo(f"Listening on {host}:{port}", err=True)
159
+ click.echo(f"API URL: {api_url}", err=True)
160
+
161
+ serve(transport=transport, host=host, port=port)
162
+
163
+
164
+ def main() -> None:
165
+ """Main entry point for the CLI."""
166
+ cli()
167
+
168
+
169
+ # Legacy entry point for backwards compatibility
170
+ def run_api() -> None:
171
+ """Run the API server (legacy entry point)."""
172
+ import uvicorn
173
+
174
+ from nornweave.core.config import get_settings
175
+
176
+ settings = get_settings()
177
+ uvicorn.run(
178
+ "nornweave.yggdrasil.app:app",
179
+ host=settings.host,
180
+ port=settings.port,
181
+ reload=settings.environment == "development",
182
+ )
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,26 @@
1
+ """Core interfaces, config, and utilities."""
2
+
3
+ from nornweave.core.config import Settings, get_settings
4
+ from nornweave.core.exceptions import (
5
+ NornWeaveError,
6
+ NotFoundError,
7
+ ProviderError,
8
+ ValidationError,
9
+ )
10
+ from nornweave.core.interfaces import (
11
+ EmailProvider,
12
+ InboundMessage,
13
+ StorageInterface,
14
+ )
15
+
16
+ __all__ = [
17
+ "EmailProvider",
18
+ "InboundMessage",
19
+ "NornWeaveError",
20
+ "NotFoundError",
21
+ "ProviderError",
22
+ "Settings",
23
+ "StorageInterface",
24
+ "ValidationError",
25
+ "get_settings",
26
+ ]
@@ -0,0 +1,172 @@
1
+ """Application configuration (Pydantic settings)."""
2
+
3
+ from functools import lru_cache
4
+ from typing import Literal
5
+
6
+ from pydantic import Field
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+
9
+
10
+ class Settings(BaseSettings):
11
+ """NornWeave configuration from environment."""
12
+
13
+ model_config = SettingsConfigDict(
14
+ env_file=".env",
15
+ env_file_encoding="utf-8",
16
+ case_sensitive=False,
17
+ extra="ignore",
18
+ )
19
+
20
+ # Storage (Urdr) # noqa: ERA001
21
+ db_driver: Literal["postgres", "sqlite"] = Field(default="postgres", alias="DB_DRIVER")
22
+ database_url: str = Field(default="", alias="DATABASE_URL")
23
+
24
+ # Email provider
25
+ email_provider: Literal["mailgun", "ses", "sendgrid", "resend"] = Field(
26
+ default="mailgun", alias="EMAIL_PROVIDER"
27
+ )
28
+ email_domain: str = Field(default="", alias="EMAIL_DOMAIN")
29
+
30
+ # Provider-specific (filled per provider)
31
+ mailgun_api_key: str = Field(default="", alias="MAILGUN_API_KEY")
32
+ mailgun_domain: str = Field(default="", alias="MAILGUN_DOMAIN")
33
+ sendgrid_api_key: str = Field(default="", alias="SENDGRID_API_KEY")
34
+ resend_api_key: str = Field(default="", alias="RESEND_API_KEY")
35
+ resend_webhook_secret: str = Field(default="", alias="RESEND_WEBHOOK_SECRET")
36
+ aws_access_key_id: str = Field(default="", alias="AWS_ACCESS_KEY_ID")
37
+ aws_secret_access_key: str = Field(default="", alias="AWS_SECRET_ACCESS_KEY")
38
+ aws_region: str = Field(default="us-east-1", alias="AWS_REGION")
39
+
40
+ # API security
41
+ api_key: str = Field(default="", alias="API_KEY")
42
+ cors_origins: str = Field(default="*", alias="CORS_ORIGINS")
43
+
44
+ # Server
45
+ host: str = Field(default="0.0.0.0", alias="HOST")
46
+ port: int = Field(default=8000, alias="PORT")
47
+ environment: Literal["development", "staging", "production"] = Field(
48
+ default="development", alias="ENVIRONMENT"
49
+ )
50
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field(
51
+ default="INFO", alias="LOG_LEVEL"
52
+ )
53
+
54
+ # Phase 3
55
+ redis_url: str = Field(default="redis://localhost:6379/0", alias="REDIS_URL")
56
+ openai_api_key: str = Field(default="", alias="OPENAI_API_KEY")
57
+ webhook_secret: str = Field(default="", alias="WEBHOOK_SECRET")
58
+
59
+ # -------------------------------------------------------------------------
60
+ # Attachment Storage Configuration
61
+ # -------------------------------------------------------------------------
62
+ # Backend selection: "local", "s3", "gcs", "database"
63
+ attachment_storage_backend: Literal["local", "s3", "gcs", "database"] = Field(
64
+ default="local",
65
+ alias="ATTACHMENT_STORAGE_BACKEND",
66
+ description="Storage backend for attachments",
67
+ )
68
+
69
+ # Local filesystem settings
70
+ attachment_local_path: str = Field(
71
+ default="./data/attachments",
72
+ alias="ATTACHMENT_LOCAL_PATH",
73
+ description="Base path for local filesystem storage",
74
+ )
75
+ attachment_serve_url_prefix: str = Field(
76
+ default="/v1/attachments",
77
+ alias="ATTACHMENT_SERVE_URL_PREFIX",
78
+ description="URL prefix for attachment downloads",
79
+ )
80
+
81
+ # S3 settings
82
+ attachment_s3_bucket: str | None = Field(
83
+ default=None,
84
+ alias="ATTACHMENT_S3_BUCKET",
85
+ description="S3 bucket for attachment storage",
86
+ )
87
+ attachment_s3_prefix: str = Field(
88
+ default="attachments",
89
+ alias="ATTACHMENT_S3_PREFIX",
90
+ description="S3 key prefix for attachments",
91
+ )
92
+ attachment_s3_region: str = Field(
93
+ default="us-east-1",
94
+ alias="ATTACHMENT_S3_REGION",
95
+ description="AWS region for S3 bucket",
96
+ )
97
+ attachment_s3_access_key: str | None = Field(
98
+ default=None,
99
+ alias="ATTACHMENT_S3_ACCESS_KEY",
100
+ description="AWS access key (uses IAM role if not set)",
101
+ )
102
+ attachment_s3_secret_key: str | None = Field(
103
+ default=None,
104
+ alias="ATTACHMENT_S3_SECRET_KEY",
105
+ description="AWS secret key (uses IAM role if not set)",
106
+ )
107
+
108
+ # GCS settings
109
+ attachment_gcs_bucket: str | None = Field(
110
+ default=None,
111
+ alias="ATTACHMENT_GCS_BUCKET",
112
+ description="GCS bucket for attachment storage",
113
+ )
114
+ attachment_gcs_prefix: str = Field(
115
+ default="attachments",
116
+ alias="ATTACHMENT_GCS_PREFIX",
117
+ description="GCS blob prefix for attachments",
118
+ )
119
+ attachment_gcs_credentials_path: str | None = Field(
120
+ default=None,
121
+ alias="ATTACHMENT_GCS_CREDENTIALS_PATH",
122
+ description="Path to GCS service account JSON (uses ADC if not set)",
123
+ )
124
+
125
+ # Download URL settings
126
+ attachment_url_expiry_seconds: int = Field(
127
+ default=3600,
128
+ alias="ATTACHMENT_URL_EXPIRY_SECONDS",
129
+ description="Default expiry time for download URLs in seconds",
130
+ )
131
+
132
+ # Size limits
133
+ attachment_max_size_mb: int = Field(
134
+ default=25,
135
+ alias="ATTACHMENT_MAX_SIZE_MB",
136
+ description="Maximum size for single attachment in MB",
137
+ )
138
+ attachment_max_total_size_mb: int = Field(
139
+ default=35,
140
+ alias="ATTACHMENT_MAX_TOTAL_SIZE_MB",
141
+ description="Maximum total size for all attachments in MB",
142
+ )
143
+ attachment_max_count: int = Field(
144
+ default=20,
145
+ alias="ATTACHMENT_MAX_COUNT",
146
+ description="Maximum number of attachments per message",
147
+ )
148
+
149
+ # -------------------------------------------------------------------------
150
+ # Content Extraction Configuration (Talon)
151
+ # -------------------------------------------------------------------------
152
+ talon_use_ml_signature: bool = Field(
153
+ default=True,
154
+ alias="TALON_USE_ML_SIGNATURE",
155
+ description="Use ML-based signature extraction (more accurate)",
156
+ )
157
+ message_preview_max_length: int = Field(
158
+ default=100,
159
+ alias="MESSAGE_PREVIEW_MAX_LENGTH",
160
+ description="Maximum length for message preview text",
161
+ )
162
+ extraction_fallback_to_original: bool = Field(
163
+ default=True,
164
+ alias="EXTRACTION_FALLBACK_TO_ORIGINAL",
165
+ description="Return original content if extraction fails",
166
+ )
167
+
168
+
169
+ @lru_cache
170
+ def get_settings() -> Settings:
171
+ """Return cached settings instance."""
172
+ return Settings()
@@ -0,0 +1,25 @@
1
+ """Custom exception hierarchy for NornWeave."""
2
+
3
+
4
+ class NornWeaveError(Exception):
5
+ """Base exception for all NornWeave errors."""
6
+
7
+ pass
8
+
9
+
10
+ class NotFoundError(NornWeaveError):
11
+ """Resource not found (inbox, thread, message)."""
12
+
13
+ pass
14
+
15
+
16
+ class ValidationError(NornWeaveError):
17
+ """Invalid input or state."""
18
+
19
+ pass
20
+
21
+
22
+ class ProviderError(NornWeaveError):
23
+ """Email or storage provider error."""
24
+
25
+ pass