e2a 2.4.0__tar.gz → 3.0.0__tar.gz

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 (150) hide show
  1. {e2a-2.4.0 → e2a-3.0.0}/.gitignore +3 -0
  2. {e2a-2.4.0 → e2a-3.0.0}/CHANGELOG.md +55 -0
  3. e2a-3.0.0/PKG-INFO +178 -0
  4. e2a-3.0.0/README.md +141 -0
  5. {e2a-2.4.0 → e2a-3.0.0}/pyproject.toml +19 -4
  6. e2a-3.0.0/scripts/generate-oag.sh +39 -0
  7. e2a-3.0.0/scripts/strip-enum-validators.py +91 -0
  8. e2a-3.0.0/src/e2a/__init__.py +52 -0
  9. e2a-3.0.0/src/e2a/v1/__init__.py +71 -0
  10. e2a-3.0.0/src/e2a/v1/_retry.py +152 -0
  11. e2a-3.0.0/src/e2a/v1/client.py +644 -0
  12. e2a-3.0.0/src/e2a/v1/errors.py +354 -0
  13. e2a-3.0.0/src/e2a/v1/generated/__init__.py +228 -0
  14. e2a-3.0.0/src/e2a/v1/generated/api/__init__.py +13 -0
  15. e2a-3.0.0/src/e2a/v1/generated/api/account_api.py +2122 -0
  16. e2a-3.0.0/src/e2a/v1/generated/api/agents_api.py +2206 -0
  17. e2a-3.0.0/src/e2a/v1/generated/api/conversations_api.py +645 -0
  18. e2a-3.0.0/src/e2a/v1/generated/api/domains_api.py +1356 -0
  19. e2a-3.0.0/src/e2a/v1/generated/api/events_api.py +971 -0
  20. e2a-3.0.0/src/e2a/v1/generated/api/messages_api.py +2971 -0
  21. e2a-3.0.0/src/e2a/v1/generated/api/meta_api.py +281 -0
  22. e2a-3.0.0/src/e2a/v1/generated/api/reviews_api.py +1144 -0
  23. e2a-3.0.0/src/e2a/v1/generated/api/webhooks_api.py +2226 -0
  24. e2a-3.0.0/src/e2a/v1/generated/api_client.py +807 -0
  25. e2a-3.0.0/src/e2a/v1/generated/api_response.py +21 -0
  26. e2a-3.0.0/src/e2a/v1/generated/configuration.py +577 -0
  27. e2a-3.0.0/src/e2a/v1/generated/exceptions.py +216 -0
  28. e2a-3.0.0/src/e2a/v1/generated/models/__init__.py +98 -0
  29. e2a-3.0.0/src/e2a/v1/generated/models/account_user_view.py +89 -0
  30. e2a-3.0.0/src/e2a/v1/generated/models/account_view.py +111 -0
  31. e2a-3.0.0/src/e2a/v1/generated/models/agent_identity.py +154 -0
  32. e2a-3.0.0/src/e2a/v1/generated/models/agent_view.py +98 -0
  33. e2a-3.0.0/src/e2a/v1/generated/models/api_key_export_entry.py +98 -0
  34. e2a-3.0.0/src/e2a/v1/generated/models/api_key_view.py +102 -0
  35. e2a-3.0.0/src/e2a/v1/generated/models/approve_request.py +107 -0
  36. e2a-3.0.0/src/e2a/v1/generated/models/attachment.py +91 -0
  37. e2a-3.0.0/src/e2a/v1/generated/models/attachment_meta_view.py +93 -0
  38. e2a-3.0.0/src/e2a/v1/generated/models/attachment_view.py +100 -0
  39. e2a-3.0.0/src/e2a/v1/generated/models/auth_verdict.py +101 -0
  40. e2a-3.0.0/src/e2a/v1/generated/models/check_result.py +89 -0
  41. e2a-3.0.0/src/e2a/v1/generated/models/conversation_detail_view.py +118 -0
  42. e2a-3.0.0/src/e2a/v1/generated/models/conversation_summary_view.py +104 -0
  43. e2a-3.0.0/src/e2a/v1/generated/models/create_agent_request.py +89 -0
  44. e2a-3.0.0/src/e2a/v1/generated/models/create_api_key_request.py +94 -0
  45. e2a-3.0.0/src/e2a/v1/generated/models/create_api_key_response.py +104 -0
  46. e2a-3.0.0/src/e2a/v1/generated/models/create_webhook_request.py +97 -0
  47. e2a-3.0.0/src/e2a/v1/generated/models/create_webhook_response.py +110 -0
  48. e2a-3.0.0/src/e2a/v1/generated/models/delete_user_data_result.py +107 -0
  49. e2a-3.0.0/src/e2a/v1/generated/models/delivery_status_json.py +93 -0
  50. e2a-3.0.0/src/e2a/v1/generated/models/deployment_info_view.py +93 -0
  51. e2a-3.0.0/src/e2a/v1/generated/models/dns_record_view.py +91 -0
  52. e2a-3.0.0/src/e2a/v1/generated/models/dns_records_view.py +101 -0
  53. e2a-3.0.0/src/e2a/v1/generated/models/domain.py +114 -0
  54. e2a-3.0.0/src/e2a/v1/generated/models/domain_view.py +122 -0
  55. e2a-3.0.0/src/e2a/v1/generated/models/error_body.py +98 -0
  56. e2a-3.0.0/src/e2a/v1/generated/models/error_envelope.py +91 -0
  57. e2a-3.0.0/src/e2a/v1/generated/models/event_json.py +123 -0
  58. e2a-3.0.0/src/e2a/v1/generated/models/forward_request.py +107 -0
  59. e2a-3.0.0/src/e2a/v1/generated/models/limits_caps_view.py +93 -0
  60. e2a-3.0.0/src/e2a/v1/generated/models/limits_usage_view.py +93 -0
  61. e2a-3.0.0/src/e2a/v1/generated/models/message.py +208 -0
  62. e2a-3.0.0/src/e2a/v1/generated/models/message_body_view.py +89 -0
  63. e2a-3.0.0/src/e2a/v1/generated/models/message_parsed_view.py +89 -0
  64. e2a-3.0.0/src/e2a/v1/generated/models/message_summary_view.py +134 -0
  65. e2a-3.0.0/src/e2a/v1/generated/models/message_view.py +160 -0
  66. e2a-3.0.0/src/e2a/v1/generated/models/o_auth_connection_entry.py +100 -0
  67. e2a-3.0.0/src/e2a/v1/generated/models/page_agent_view.py +102 -0
  68. e2a-3.0.0/src/e2a/v1/generated/models/page_api_key_view.py +102 -0
  69. e2a-3.0.0/src/e2a/v1/generated/models/page_conversation_summary_view.py +102 -0
  70. e2a-3.0.0/src/e2a/v1/generated/models/page_domain_view.py +102 -0
  71. e2a-3.0.0/src/e2a/v1/generated/models/page_event_json.py +102 -0
  72. e2a-3.0.0/src/e2a/v1/generated/models/page_message_summary_view.py +102 -0
  73. e2a-3.0.0/src/e2a/v1/generated/models/page_review_view.py +102 -0
  74. e2a-3.0.0/src/e2a/v1/generated/models/page_suppression.py +102 -0
  75. e2a-3.0.0/src/e2a/v1/generated/models/page_webhook_delivery_view.py +102 -0
  76. e2a-3.0.0/src/e2a/v1/generated/models/page_webhook_view.py +102 -0
  77. e2a-3.0.0/src/e2a/v1/generated/models/protection_config_view.py +102 -0
  78. e2a-3.0.0/src/e2a/v1/generated/models/protection_direction_view.py +97 -0
  79. e2a-3.0.0/src/e2a/v1/generated/models/protection_event_export_entry.py +108 -0
  80. e2a-3.0.0/src/e2a/v1/generated/models/protection_gate_view.py +92 -0
  81. e2a-3.0.0/src/e2a/v1/generated/models/protection_holds_view.py +90 -0
  82. e2a-3.0.0/src/e2a/v1/generated/models/protection_scan_view.py +87 -0
  83. e2a-3.0.0/src/e2a/v1/generated/models/redeliver_delivery.py +93 -0
  84. e2a-3.0.0/src/e2a/v1/generated/models/redeliver_event_request.py +87 -0
  85. e2a-3.0.0/src/e2a/v1/generated/models/redeliver_view.py +103 -0
  86. e2a-3.0.0/src/e2a/v1/generated/models/register_domain_request.py +87 -0
  87. e2a-3.0.0/src/e2a/v1/generated/models/reject_request.py +87 -0
  88. e2a-3.0.0/src/e2a/v1/generated/models/reject_result_view.py +91 -0
  89. e2a-3.0.0/src/e2a/v1/generated/models/reply_request.py +107 -0
  90. e2a-3.0.0/src/e2a/v1/generated/models/result.py +101 -0
  91. e2a-3.0.0/src/e2a/v1/generated/models/review_view.py +108 -0
  92. e2a-3.0.0/src/e2a/v1/generated/models/rotate_secret_response.py +90 -0
  93. e2a-3.0.0/src/e2a/v1/generated/models/send_email_request.py +109 -0
  94. e2a-3.0.0/src/e2a/v1/generated/models/send_result_view.py +100 -0
  95. e2a-3.0.0/src/e2a/v1/generated/models/sending_dns_record_view.py +91 -0
  96. e2a-3.0.0/src/e2a/v1/generated/models/suppression.py +96 -0
  97. e2a-3.0.0/src/e2a/v1/generated/models/suppression_export_entry.py +94 -0
  98. e2a-3.0.0/src/e2a/v1/generated/models/test_webhook_request.py +102 -0
  99. e2a-3.0.0/src/e2a/v1/generated/models/test_webhook_response.py +87 -0
  100. e2a-3.0.0/src/e2a/v1/generated/models/update_agent_request.py +88 -0
  101. e2a-3.0.0/src/e2a/v1/generated/models/update_message_request.py +89 -0
  102. e2a-3.0.0/src/e2a/v1/generated/models/update_message_result_view.py +89 -0
  103. e2a-3.0.0/src/e2a/v1/generated/models/update_webhook_request.py +99 -0
  104. e2a-3.0.0/src/e2a/v1/generated/models/usage_event_entry.py +98 -0
  105. e2a-3.0.0/src/e2a/v1/generated/models/user_export.py +176 -0
  106. e2a-3.0.0/src/e2a/v1/generated/models/user_export_user.py +94 -0
  107. e2a-3.0.0/src/e2a/v1/generated/models/verify_domain_view.py +98 -0
  108. e2a-3.0.0/src/e2a/v1/generated/models/webhook_delivery_view.py +104 -0
  109. e2a-3.0.0/src/e2a/v1/generated/models/webhook_filters_view.py +91 -0
  110. e2a-3.0.0/src/e2a/v1/generated/models/webhook_view.py +108 -0
  111. e2a-3.0.0/src/e2a/v1/generated/py.typed +0 -0
  112. e2a-3.0.0/src/e2a/v1/generated/rest.py +194 -0
  113. e2a-3.0.0/src/e2a/v1/pagination.py +98 -0
  114. e2a-3.0.0/src/e2a/v1/py.typed +0 -0
  115. e2a-3.0.0/src/e2a/v1/webhook_signature.py +141 -0
  116. e2a-3.0.0/src/e2a/v1/websocket.py +215 -0
  117. e2a-3.0.0/tests/__init__.py +0 -0
  118. {e2a-2.4.0 → e2a-3.0.0}/tests/test_contract.py +25 -129
  119. e2a-3.0.0/tests/test_enum_forward_compat.py +64 -0
  120. e2a-3.0.0/tests/test_exports.py +84 -0
  121. e2a-3.0.0/tests/test_v1_client.py +422 -0
  122. e2a-3.0.0/tests/test_v1_errors.py +189 -0
  123. e2a-3.0.0/tests/test_v1_pagination.py +113 -0
  124. e2a-3.0.0/tests/test_v1_retry.py +183 -0
  125. {e2a-2.4.0 → e2a-3.0.0}/tests/test_v1_websocket.py +161 -48
  126. e2a-3.0.0/tests/test_webhook_signature.py +161 -0
  127. {e2a-2.4.0 → e2a-3.0.0}/uv.lock +540 -2
  128. e2a-2.4.0/PKG-INFO +0 -464
  129. e2a-2.4.0/README.md +0 -430
  130. e2a-2.4.0/codegen-requirements.txt +0 -22
  131. e2a-2.4.0/src/e2a/__init__.py +0 -44
  132. e2a-2.4.0/src/e2a/v1/__init__.py +0 -44
  133. e2a-2.4.0/src/e2a/v1/api.py +0 -383
  134. e2a-2.4.0/src/e2a/v1/async_client.py +0 -704
  135. e2a-2.4.0/src/e2a/v1/client.py +0 -458
  136. e2a-2.4.0/src/e2a/v1/generated/__init__.py +0 -526
  137. e2a-2.4.0/src/e2a/v1/generated/_internal.py +0 -401
  138. e2a-2.4.0/src/e2a/v1/generated/github_com_Mnexa_AI_e2a_internal_identity.py +0 -127
  139. e2a-2.4.0/src/e2a/v1/generated/internal_agent.py +0 -27
  140. e2a-2.4.0/src/e2a/v1/handler.py +0 -931
  141. e2a-2.4.0/src/e2a/v1/websocket.py +0 -151
  142. e2a-2.4.0/tests/test_e2e.py +0 -192
  143. e2a-2.4.0/tests/test_exports.py +0 -84
  144. e2a-2.4.0/tests/test_generated_models.py +0 -32
  145. e2a-2.4.0/tests/test_idempotency.py +0 -183
  146. e2a-2.4.0/tests/test_v1_api.py +0 -676
  147. e2a-2.4.0/tests/test_v1_async_client.py +0 -413
  148. e2a-2.4.0/tests/test_v1_client.py +0 -605
  149. e2a-2.4.0/tests/test_v1_handler.py +0 -646
  150. /e2a-2.4.0/tests/__init__.py → /e2a-3.0.0/src/e2a/py.typed +0 -0
@@ -39,3 +39,6 @@ sdks/python/.pytest_cache/
39
39
 
40
40
  # Docs
41
41
  docs/originals/
42
+
43
+ # Go coverage profile (make cover)
44
+ cover.out
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0
4
+
5
+ Breaking redesign. The SDK is now a namespaced, **async-only** `E2AClient`
6
+ wrapping a generated client over the agent-scoped `/v1` API surface, with a
7
+ typed error hierarchy, automatic retries + idempotency, and async
8
+ auto-pagination.
9
+
10
+ ### Changed
11
+ - **Namespaced, async-only surface.** Resources are grouped under the client:
12
+ `client.agents`, `client.messages`, `client.conversations`, `client.domains`,
13
+ `client.events`, `client.webhooks`, `client.account`. Per-agent methods take
14
+ the agent `address` as the first argument
15
+ (`await client.messages.send(address, {...})`,
16
+ `await client.messages.list(address).to_list(limit=...)`,
17
+ `await client.messages.get(address, id)`,
18
+ `await client.messages.reply(address, id, {...})`). Use the client as an async
19
+ context manager (`async with E2AClient() as client:`).
20
+ - **Webhook verification.** Verify and decode a delivery with the standalone
21
+ `construct_event(raw_body, signature_header, secret)`, which checks the
22
+ `X-E2A-Signature` header and returns a typed event (raising
23
+ `E2AWebhookSignatureError` on a bad signature). Per-webhook `whsec_…` secrets,
24
+ Stripe-style.
25
+ - **Typed errors.** Failures raise `E2AError` subclasses (`E2ANotFoundError`,
26
+ `E2AConflictError`, `E2AValidationError`, `E2ARateLimitError`,
27
+ `E2AWebhookSignatureError`, …) carrying `.code`, `.status`, `.request_id`, and
28
+ `.retryable`.
29
+
30
+ ### Removed
31
+ - The flat methods `send` / `reply` / `get_messages` / `get_message` and the
32
+ per-call `agent_email` inference. Pass the agent `address` explicitly.
33
+ - The lower-level `E2AApi` class.
34
+ - The synchronous client — the SDK is async-only.
35
+ - `InboundEmail` / `AsyncInboundEmail` and the `parse_webhook` / `parse` +
36
+ `verify_signature()` flow. Replaced by `construct_event`. There is no
37
+ unverified-email type and no field-access gating.
38
+
39
+ ## 2.5.0
40
+
41
+ ### Added
42
+ - Generated types for the per-user resource-limits primitive that
43
+ shipped with #158: `LimitsInfo`, `LimitsCaps`, `LimitsUsage`. These
44
+ describe the response shape of `GET /api/v1/users/me/limits`, which
45
+ the hosted dashboard uses to render the upgrade affordance and the
46
+ "you've used X of Y" surface. The high-level `E2AClient` doesn't
47
+ yet expose a typed helper for this endpoint — it's surfaced as a
48
+ dashboard-only concern today, and SDK consumers querying their own
49
+ usage should call `/agents` / `/messages` directly. The types are
50
+ emitted so anyone consuming the raw OpenAPI generation has the
51
+ shapes available.
52
+
53
+ ### Notes
54
+ - No runtime client behavior changed in this release. If you're not
55
+ using the limits primitive (self-host deployments without a paid
56
+ tier), 2.5.0 is functionally identical to 2.4.0.
57
+
3
58
  ## 2.4.0
4
59
 
5
60
  ### Added
e2a-3.0.0/PKG-INFO ADDED
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: e2a
3
+ Version: 3.0.0
4
+ Summary: Python SDK for the e2a protocol — email-to-agent authentication
5
+ Project-URL: Homepage, https://e2a.dev
6
+ Project-URL: Repository, https://github.com/Mnexa-AI/e2a
7
+ Project-URL: Documentation, https://e2a.dev
8
+ Author-email: Mnexa AI <josh@mnexa.ai>
9
+ License-Expression: Apache-2.0
10
+ Keywords: agent,authentication,e2a,email,webhook
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications :: Email
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx>=0.24
23
+ Requires-Dist: pydantic<3,>=2.12
24
+ Requires-Dist: python-dateutil>=2.8
25
+ Requires-Dist: typing-extensions>=4.7
26
+ Requires-Dist: urllib3>=1.25
27
+ Provides-Extra: dev
28
+ Requires-Dist: anyio[trio]; extra == 'dev'
29
+ Requires-Dist: build; extra == 'dev'
30
+ Requires-Dist: pytest-httpx; extra == 'dev'
31
+ Requires-Dist: pytest>=7; extra == 'dev'
32
+ Requires-Dist: pyyaml>=6; extra == 'dev'
33
+ Requires-Dist: twine; extra == 'dev'
34
+ Provides-Extra: ws
35
+ Requires-Dist: websockets>=14; extra == 'ws'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # e2a Python SDK
39
+
40
+ Async Python SDK for [e2a](https://e2a.dev) — email for AI agents.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install e2a # add the [ws] extra for client.listen(): pip install "e2a[ws]"
46
+ ```
47
+
48
+ The SDK major version tracks the SDK package's own breaking changes and is
49
+ independent of the API version path (`/v1`): SDK 3.x targets the e2a v1 API.
50
+
51
+ ## Upgrading from 2.x to 3.0
52
+
53
+ 3.0 is a breaking redesign. The SDK now wraps a generated `/v1` client behind a
54
+ namespaced, **async-only** surface, with a typed error hierarchy, automatic
55
+ retries + idempotency, and async auto-pagination.
56
+
57
+ - **Async-only, namespaced.** The sync client and the flat methods are gone.
58
+ `client.get_messages()` → `client.messages.list(address)`,
59
+ `client.get_message(id)` → `client.messages.get(address, id)`,
60
+ `client.send(...)` → `client.messages.send(address, body)`. Per-agent calls
61
+ take an explicit `address`.
62
+ - **Webhook verification.** `client.parse` / `client.parse_webhook` /
63
+ `InboundEmail` are removed. Verify and parse a delivery with the standalone
64
+ `construct_event(raw_body, header, secret)`, which returns a typed
65
+ `WebhookEvent`. Signatures are per-webhook (`whsec_…`), Stripe-style.
66
+ - **Typed errors.** Failures raise `E2AError` subclasses (`E2ANotFoundError`,
67
+ `E2AConflictError`, `E2AValidationError`, `E2ARateLimitError`, …) carrying
68
+ `.code`, `.status`, `.request_id`, and `.retryable`.
69
+
70
+ ## Quick Start
71
+
72
+ ```python
73
+ import asyncio
74
+ from e2a.v1 import E2AClient
75
+
76
+ async def main():
77
+ # reads E2A_API_KEY; base_url defaults to https://api.e2a.dev
78
+ async with E2AClient() as client:
79
+ address = "my-agent@agents.e2a.dev"
80
+
81
+ # List endpoints return an AutoPager: async-iterate, or collect with a limit.
82
+ async for m in client.messages.list(address, status="unread"):
83
+ email = await client.messages.get(address, m.message_id)
84
+ print(email.subject)
85
+ await client.messages.reply(address, m.message_id, {"body": "Got it!"})
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ ### Send mail
91
+
92
+ ```python
93
+ await client.messages.send(address, {
94
+ "to": ["alice@example.com"],
95
+ "subject": "Hello",
96
+ "body": "Hi from my agent!",
97
+ "html_body": "<p>Hi!</p>",
98
+ })
99
+ ```
100
+
101
+ The mail-sending writes (`send` / `reply` / `forward` / `approve`) auto-mint an
102
+ `Idempotency-Key` and reuse it across retries, so a network blip can't
103
+ double-send. Pass a stable key to also survive a process restart:
104
+
105
+ ```python
106
+ await client.messages.send(address, body, idempotency_key=derive_from(event))
107
+ ```
108
+
109
+ Request bodies accept a plain `dict` (shown above) or the generated model
110
+ (`from e2a.v1 import SendEmailRequest`).
111
+
112
+ ### Verify a webhook
113
+
114
+ Each subscription is signed with its own `whsec_…` secret. `construct_event`
115
+ verifies the `X-E2A-Signature` header (replay-protected) and returns a typed
116
+ event. **Pass the raw request body** — re-serialized JSON won't match.
117
+
118
+ ```python
119
+ from e2a.v1 import construct_event, E2AWebhookSignatureError
120
+
121
+ @app.post("/webhook")
122
+ async def webhook(request):
123
+ try:
124
+ event = construct_event(await request.body(), request.headers["X-E2A-Signature"], SECRET)
125
+ except E2AWebhookSignatureError:
126
+ return Response(status_code=400)
127
+ if event.type == "email.received":
128
+ ... # event.data carries the message payload
129
+ return {"ok": True}
130
+ ```
131
+
132
+ During a rotation you can pass a list of secrets — accepted if any matches:
133
+ `construct_event(body, header, [old_secret, new_secret])`.
134
+
135
+ ## Resources
136
+
137
+ `client.agents`, `client.messages`, `client.conversations`, `client.domains`,
138
+ `client.events`, `client.webhooks`, `client.account` (with
139
+ `client.account.suppressions`), plus `await client.info()`. Each method maps to
140
+ a `/v1` operation; per-agent methods take the agent `address` first.
141
+
142
+ ### `E2AClient(api_key=None, *, base_url=None, max_retries=2, max_elapsed_ms=None)`
143
+
144
+ `api_key` falls back to `E2A_API_KEY`; `base_url` to `E2A_BASE_URL` then
145
+ `https://api.e2a.dev`. Use it as an async context manager (or call
146
+ `await client.aclose()`) to close the underlying HTTP connections.
147
+
148
+ ### Errors
149
+
150
+ Every failure raises an `E2AError` (or subclass) with `.code`, `.status`,
151
+ `.request_id`, `.retryable`: `E2AAuthError` (401), `E2APermissionError` (403),
152
+ `E2ANotFoundError` (404), `E2AConflictError` (409), `E2AValidationError` (422),
153
+ `E2AIdempotencyError`, `E2ARateLimitError` (429), `E2AServerError` (5xx),
154
+ `E2AConnectionError` (no response), `E2AWebhookSignatureError`.
155
+
156
+ > e2a hides the existence of agents you don't own — `agents.get` of an unknown
157
+ > address raises `E2APermissionError` (403), not `E2ANotFoundError`.
158
+
159
+ ### Pagination
160
+
161
+ List methods return an `AutoPager` — async-iterate it, or use
162
+ `await pager.to_list(limit=N)` (the limit is required, to bound memory) or
163
+ `await pager.for_each(fn)` (return `False` to stop early).
164
+
165
+ ## WebSocket (real-time delivery for local agents)
166
+
167
+ ```python
168
+ async for notif in client.listen("bot@agents.e2a.dev"): # falls back to E2A_AGENT_EMAIL
169
+ email = await client.messages.get(notif.recipient, notif.message_id)
170
+ ```
171
+
172
+ `client.listen(address)` returns a `WSStream` (async-iterable of
173
+ `WSNotification`) that reconnects with exponential backoff. Requires the `[ws]`
174
+ extra (`pip install "e2a[ws]"`).
175
+
176
+ ## License
177
+
178
+ Apache-2.0 — see [LICENSE](https://github.com/Mnexa-AI/e2a/blob/main/LICENSE) and [NOTICE](https://github.com/Mnexa-AI/e2a/blob/main/NOTICE) in the upstream repo.
e2a-3.0.0/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # e2a Python SDK
2
+
3
+ Async Python SDK for [e2a](https://e2a.dev) — email for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install e2a # add the [ws] extra for client.listen(): pip install "e2a[ws]"
9
+ ```
10
+
11
+ The SDK major version tracks the SDK package's own breaking changes and is
12
+ independent of the API version path (`/v1`): SDK 3.x targets the e2a v1 API.
13
+
14
+ ## Upgrading from 2.x to 3.0
15
+
16
+ 3.0 is a breaking redesign. The SDK now wraps a generated `/v1` client behind a
17
+ namespaced, **async-only** surface, with a typed error hierarchy, automatic
18
+ retries + idempotency, and async auto-pagination.
19
+
20
+ - **Async-only, namespaced.** The sync client and the flat methods are gone.
21
+ `client.get_messages()` → `client.messages.list(address)`,
22
+ `client.get_message(id)` → `client.messages.get(address, id)`,
23
+ `client.send(...)` → `client.messages.send(address, body)`. Per-agent calls
24
+ take an explicit `address`.
25
+ - **Webhook verification.** `client.parse` / `client.parse_webhook` /
26
+ `InboundEmail` are removed. Verify and parse a delivery with the standalone
27
+ `construct_event(raw_body, header, secret)`, which returns a typed
28
+ `WebhookEvent`. Signatures are per-webhook (`whsec_…`), Stripe-style.
29
+ - **Typed errors.** Failures raise `E2AError` subclasses (`E2ANotFoundError`,
30
+ `E2AConflictError`, `E2AValidationError`, `E2ARateLimitError`, …) carrying
31
+ `.code`, `.status`, `.request_id`, and `.retryable`.
32
+
33
+ ## Quick Start
34
+
35
+ ```python
36
+ import asyncio
37
+ from e2a.v1 import E2AClient
38
+
39
+ async def main():
40
+ # reads E2A_API_KEY; base_url defaults to https://api.e2a.dev
41
+ async with E2AClient() as client:
42
+ address = "my-agent@agents.e2a.dev"
43
+
44
+ # List endpoints return an AutoPager: async-iterate, or collect with a limit.
45
+ async for m in client.messages.list(address, status="unread"):
46
+ email = await client.messages.get(address, m.message_id)
47
+ print(email.subject)
48
+ await client.messages.reply(address, m.message_id, {"body": "Got it!"})
49
+
50
+ asyncio.run(main())
51
+ ```
52
+
53
+ ### Send mail
54
+
55
+ ```python
56
+ await client.messages.send(address, {
57
+ "to": ["alice@example.com"],
58
+ "subject": "Hello",
59
+ "body": "Hi from my agent!",
60
+ "html_body": "<p>Hi!</p>",
61
+ })
62
+ ```
63
+
64
+ The mail-sending writes (`send` / `reply` / `forward` / `approve`) auto-mint an
65
+ `Idempotency-Key` and reuse it across retries, so a network blip can't
66
+ double-send. Pass a stable key to also survive a process restart:
67
+
68
+ ```python
69
+ await client.messages.send(address, body, idempotency_key=derive_from(event))
70
+ ```
71
+
72
+ Request bodies accept a plain `dict` (shown above) or the generated model
73
+ (`from e2a.v1 import SendEmailRequest`).
74
+
75
+ ### Verify a webhook
76
+
77
+ Each subscription is signed with its own `whsec_…` secret. `construct_event`
78
+ verifies the `X-E2A-Signature` header (replay-protected) and returns a typed
79
+ event. **Pass the raw request body** — re-serialized JSON won't match.
80
+
81
+ ```python
82
+ from e2a.v1 import construct_event, E2AWebhookSignatureError
83
+
84
+ @app.post("/webhook")
85
+ async def webhook(request):
86
+ try:
87
+ event = construct_event(await request.body(), request.headers["X-E2A-Signature"], SECRET)
88
+ except E2AWebhookSignatureError:
89
+ return Response(status_code=400)
90
+ if event.type == "email.received":
91
+ ... # event.data carries the message payload
92
+ return {"ok": True}
93
+ ```
94
+
95
+ During a rotation you can pass a list of secrets — accepted if any matches:
96
+ `construct_event(body, header, [old_secret, new_secret])`.
97
+
98
+ ## Resources
99
+
100
+ `client.agents`, `client.messages`, `client.conversations`, `client.domains`,
101
+ `client.events`, `client.webhooks`, `client.account` (with
102
+ `client.account.suppressions`), plus `await client.info()`. Each method maps to
103
+ a `/v1` operation; per-agent methods take the agent `address` first.
104
+
105
+ ### `E2AClient(api_key=None, *, base_url=None, max_retries=2, max_elapsed_ms=None)`
106
+
107
+ `api_key` falls back to `E2A_API_KEY`; `base_url` to `E2A_BASE_URL` then
108
+ `https://api.e2a.dev`. Use it as an async context manager (or call
109
+ `await client.aclose()`) to close the underlying HTTP connections.
110
+
111
+ ### Errors
112
+
113
+ Every failure raises an `E2AError` (or subclass) with `.code`, `.status`,
114
+ `.request_id`, `.retryable`: `E2AAuthError` (401), `E2APermissionError` (403),
115
+ `E2ANotFoundError` (404), `E2AConflictError` (409), `E2AValidationError` (422),
116
+ `E2AIdempotencyError`, `E2ARateLimitError` (429), `E2AServerError` (5xx),
117
+ `E2AConnectionError` (no response), `E2AWebhookSignatureError`.
118
+
119
+ > e2a hides the existence of agents you don't own — `agents.get` of an unknown
120
+ > address raises `E2APermissionError` (403), not `E2ANotFoundError`.
121
+
122
+ ### Pagination
123
+
124
+ List methods return an `AutoPager` — async-iterate it, or use
125
+ `await pager.to_list(limit=N)` (the limit is required, to bound memory) or
126
+ `await pager.for_each(fn)` (return `False` to stop early).
127
+
128
+ ## WebSocket (real-time delivery for local agents)
129
+
130
+ ```python
131
+ async for notif in client.listen("bot@agents.e2a.dev"): # falls back to E2A_AGENT_EMAIL
132
+ email = await client.messages.get(notif.recipient, notif.message_id)
133
+ ```
134
+
135
+ `client.listen(address)` returns a `WSStream` (async-iterable of
136
+ `WSNotification`) that reconnects with exponential backoff. Requires the `[ws]`
137
+ extra (`pip install "e2a[ws]"`).
138
+
139
+ ## License
140
+
141
+ Apache-2.0 — see [LICENSE](https://github.com/Mnexa-AI/e2a/blob/main/LICENSE) and [NOTICE](https://github.com/Mnexa-AI/e2a/blob/main/NOTICE) in the upstream repo.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "e2a"
7
- version = "2.4.0"
7
+ version = "3.0.0"
8
8
  description = "Python SDK for the e2a protocol — email-to-agent authentication"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -12,7 +12,7 @@ requires-python = ">=3.9"
12
12
  authors = [{ name = "Mnexa AI", email = "josh@mnexa.ai" }]
13
13
  keywords = ["email", "agent", "authentication", "e2a", "webhook"]
14
14
  classifiers = [
15
- "Development Status :: 4 - Beta",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: Apache Software License",
18
18
  "Programming Language :: Python :: 3",
@@ -23,7 +23,22 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.13",
24
24
  "Topic :: Communications :: Email",
25
25
  ]
26
- dependencies = ["httpx>=0.24", "pydantic>=2.12,<3"]
26
+ dependencies = [
27
+ "httpx>=0.24",
28
+ "pydantic>=2.12,<3",
29
+ # Runtime deps of the OpenAPI-Generator /v1 client base (e2a.v1.generated, httpx
30
+ # library): date parsing, the Retry type in configuration, and Self on the
31
+ # generated pydantic v2 models.
32
+ "python-dateutil>=2.8",
33
+ "urllib3>=1.25",
34
+ "typing-extensions>=4.7",
35
+ ]
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ # src layout: ship the e2a package (incl. PEP 561 py.typed markers at every
39
+ # hand-written level, not just the generated sub-package).
40
+ packages = ["src/e2a"]
41
+ artifacts = ["src/e2a/py.typed", "src/e2a/v1/py.typed", "src/e2a/v1/generated/py.typed"]
27
42
 
28
43
  [project.urls]
29
44
  Homepage = "https://e2a.dev"
@@ -32,4 +47,4 @@ Documentation = "https://e2a.dev"
32
47
 
33
48
  [project.optional-dependencies]
34
49
  dev = ["pytest>=7", "pytest-httpx", "anyio[trio]", "pyyaml>=6", "build", "twine"]
35
- ws = ["websockets>=12"]
50
+ ws = ["websockets>=14"]
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Regenerate the Python /v1 client base from the canonical api/openapi.yaml
3
+ # using OpenAPI Generator's `python` generator with the **httpx** library:
4
+ # async-native (matches the async-only Python decision) and httpx-based (the
5
+ # same HTTP client the hand-written layer uses — no second HTTP dependency).
6
+ # Output lands as the package e2a.v1.generated; the hand-written ergonomic layer wraps it.
7
+ # Pinned image tag → reproducible for the drift gate. Run via Docker (no Java).
8
+ #
9
+ # packageName=e2a.v1.generated so the generator's absolute imports (`from
10
+ # e2a.v1.generated ...`) match the package's final location. We generate to a
11
+ # temp dir and copy only the leaf package, so nothing pollutes the source root
12
+ # and the existing e2a/__init__.py and e2a/v1/__init__.py are never touched.
13
+ set -euo pipefail
14
+ ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
15
+ TMP="$ROOT/sdks/python/.oag-tmp"
16
+ DEST="$ROOT/sdks/python/src/e2a/v1/generated"
17
+ IMG="openapitools/openapi-generator-cli:v7.16.0"
18
+
19
+ rm -rf "$TMP"
20
+ # Run as the invoking host user (not the container's default root) so the
21
+ # generated files + the .oag-tmp scratch dir are host-user-owned and removable
22
+ # on CI's non-root runner. HOME is a writable path for tools that expect it.
23
+ # (Docker Desktop/macOS maps ownership already, so this is a no-op there but
24
+ # required on Linux CI.)
25
+ docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -v "$ROOT:/work" "$IMG" generate \
26
+ -i /work/api/openapi.yaml -g python \
27
+ -o /work/sdks/python/.oag-tmp \
28
+ --additional-properties=packageName=e2a.v1.generated,library=httpx >/dev/null
29
+
30
+ rm -rf "$DEST"
31
+ cp -r "$TMP/e2a/v1/generated" "$DEST"
32
+ rm -rf "$TMP"
33
+
34
+ # Strip the generator's `*_validate_enum` validators so the client tolerates
35
+ # unknown enum values (forward-compat: a new server enum value must not crash a
36
+ # deployed client). Matches the TypeScript SDK's passthrough behavior.
37
+ python3 "$ROOT/sdks/python/scripts/strip-enum-validators.py" "$DEST"
38
+
39
+ echo "Python /v1 client base regenerated at sdks/python/src/e2a/v1/generated"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """Strip OpenAPI-Generator's `*_validate_enum` pydantic validators from the
3
+ generated models so the client tolerates unknown enum values.
4
+
5
+ Why: the generator emits, for every enum-typed field, a `@field_validator` that
6
+ raises `ValueError` when the value isn't in a hard-coded set. On a RESPONSE
7
+ field that means the day the server adds a new enum value (a new event `type`,
8
+ `delivery_status`, `inbound_policy`, …) every deployed client crashes while
9
+ deserializing — turning an additive, non-breaking server change into a
10
+ client-breaking one. The TypeScript SDK passes unknown enum values through
11
+ untouched; this makes Python match, keeping enum fields typed as plain strings.
12
+
13
+ Run as part of generate-oag.sh (so the drift gate's regenerated output matches)
14
+ and idempotently re-runnable on the committed tree. Removes each block of the
15
+ form:
16
+
17
+ @field_validator('field')
18
+ def field_validate_enum(cls, value):
19
+ \"\"\"Validates the enum\"\"\"
20
+ if value not in set([...]):
21
+ raise ValueError(...)
22
+ return value
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import glob
28
+ import os
29
+ import re
30
+ import sys
31
+
32
+
33
+ def strip_file(path: str) -> bool:
34
+ with open(path, "r", encoding="utf-8") as f:
35
+ lines = f.readlines()
36
+
37
+ out: list[str] = []
38
+ i = 0
39
+ n = len(lines)
40
+ changed = False
41
+ decorator = re.compile(r"^\s*@field_validator\(")
42
+ enum_def = re.compile(r"^\s*def\s+\w+_validate_enum\(")
43
+ while i < n:
44
+ line = lines[i]
45
+ # A `@field_validator(...)` decorator whose method is an enum validator:
46
+ # drop the decorator(s) + the entire def body. The body runs until the
47
+ # next non-blank line indented no deeper than the def itself (the next
48
+ # class member). Consuming by indentation — not by a `return value`
49
+ # sentinel — correctly handles optional-field validators that early-
50
+ # return on None before the enum check.
51
+ if decorator.match(line):
52
+ j = i + 1
53
+ while j < n and lines[j].strip().startswith("@"):
54
+ j += 1
55
+ if j < n and enum_def.match(lines[j]):
56
+ def_indent = len(lines[j]) - len(lines[j].lstrip())
57
+ k = j + 1
58
+ while k < n:
59
+ stripped = lines[k].strip()
60
+ if stripped == "":
61
+ k += 1
62
+ continue
63
+ indent = len(lines[k]) - len(lines[k].lstrip())
64
+ if indent > def_indent:
65
+ k += 1
66
+ continue
67
+ break
68
+ i = k
69
+ changed = True
70
+ continue
71
+ out.append(line)
72
+ i += 1
73
+
74
+ if changed:
75
+ with open(path, "w", encoding="utf-8") as f:
76
+ f.writelines(out)
77
+ return changed
78
+
79
+
80
+ def main() -> int:
81
+ root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
82
+ os.path.dirname(__file__), "..", "src", "e2a", "v1", "generated"
83
+ )
84
+ models = glob.glob(os.path.join(root, "models", "*.py"))
85
+ touched = sum(1 for p in sorted(models) if strip_file(p))
86
+ print(f"strip-enum-validators: removed enum validators from {touched} model file(s)")
87
+ return 0
88
+
89
+
90
+ if __name__ == "__main__":
91
+ raise SystemExit(main())
@@ -0,0 +1,52 @@
1
+ # Top-level convenience alias — points to the current stable API version (v1).
2
+ #
3
+ # The pinned contract path is `e2a.v1`:
4
+ # from e2a.v1 import E2AClient
5
+ #
6
+ # The top-level `e2a` package re-exports that surface for convenience.
7
+
8
+ from e2a.v1 import ( # noqa: F401
9
+ AutoPager,
10
+ E2AAuthError,
11
+ E2AClient,
12
+ E2AConflictError,
13
+ E2AConnectionError,
14
+ E2AError,
15
+ E2AIdempotencyError,
16
+ E2ANotFoundError,
17
+ E2APermissionError,
18
+ E2ARateLimitError,
19
+ E2AServerError,
20
+ E2AValidationError,
21
+ E2AWebhookSignatureError,
22
+ Page,
23
+ WebhookEvent,
24
+ WSNotification,
25
+ WSStream,
26
+ construct_event,
27
+ models,
28
+ verify_webhook_signature,
29
+ )
30
+
31
+ __all__ = [
32
+ "E2AClient",
33
+ "E2AError",
34
+ "E2AAuthError",
35
+ "E2APermissionError",
36
+ "E2ANotFoundError",
37
+ "E2AConflictError",
38
+ "E2AValidationError",
39
+ "E2AIdempotencyError",
40
+ "E2ARateLimitError",
41
+ "E2AServerError",
42
+ "E2AConnectionError",
43
+ "E2AWebhookSignatureError",
44
+ "AutoPager",
45
+ "Page",
46
+ "verify_webhook_signature",
47
+ "construct_event",
48
+ "WebhookEvent",
49
+ "WSNotification",
50
+ "WSStream",
51
+ "models",
52
+ ]
@@ -0,0 +1,71 @@
1
+ """Public surface of the e2a v1 SDK (async-only).
2
+
3
+ The canonical request/response types are the OpenAPI-Generator ``generated``
4
+ models; the hand-written ergonomic layer (:class:`E2AClient` + resources, the
5
+ typed error hierarchy, retry/pagination, webhook verification, WS) wraps them.
6
+ The legacy flat/sync ``api`` / ``client`` / ``handler`` surface and the old
7
+ swag-generated Pydantic types have been retired in favour of this.
8
+ """
9
+
10
+ # Generated request/response models (the canonical types).
11
+ from e2a.v1.generated import models # noqa: F401
12
+ from e2a.v1.generated.models import * # noqa: F401,F403
13
+
14
+ # High-level async client.
15
+ from e2a.v1.client import E2AClient # noqa: F401
16
+
17
+ # Typed error hierarchy.
18
+ from e2a.v1.errors import ( # noqa: F401
19
+ E2AAuthError,
20
+ E2AConflictError,
21
+ E2AConnectionError,
22
+ E2AError,
23
+ E2AIdempotencyError,
24
+ E2ANotFoundError,
25
+ E2APermissionError,
26
+ E2ARateLimitError,
27
+ E2AServerError,
28
+ E2AValidationError,
29
+ E2AWebhookSignatureError,
30
+ )
31
+
32
+ # Auto-pagination.
33
+ from e2a.v1.pagination import AutoPager, Page # noqa: F401
34
+
35
+ # Webhook signature verification.
36
+ from e2a.v1.webhook_signature import ( # noqa: F401
37
+ WebhookEvent,
38
+ construct_event,
39
+ verify_webhook_signature,
40
+ )
41
+
42
+ # Real-time WebSocket stream.
43
+ from e2a.v1.websocket import WSNotification, WSStream # noqa: F401
44
+
45
+ __all__ = [
46
+ "E2AClient",
47
+ # Errors
48
+ "E2AError",
49
+ "E2AAuthError",
50
+ "E2APermissionError",
51
+ "E2ANotFoundError",
52
+ "E2AConflictError",
53
+ "E2AValidationError",
54
+ "E2AIdempotencyError",
55
+ "E2ARateLimitError",
56
+ "E2AServerError",
57
+ "E2AConnectionError",
58
+ "E2AWebhookSignatureError",
59
+ # Pagination
60
+ "AutoPager",
61
+ "Page",
62
+ # Webhooks
63
+ "verify_webhook_signature",
64
+ "construct_event",
65
+ "WebhookEvent",
66
+ # WebSocket
67
+ "WSNotification",
68
+ "WSStream",
69
+ # Generated models namespace
70
+ "models",
71
+ ]