e2a 5.2.0__tar.gz → 5.3.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 (199) hide show
  1. {e2a-5.2.0 → e2a-5.3.0}/CHANGELOG.md +32 -12
  2. {e2a-5.2.0 → e2a-5.3.0}/PKG-INFO +34 -7
  3. {e2a-5.2.0 → e2a-5.3.0}/README.md +33 -6
  4. {e2a-5.2.0 → e2a-5.3.0}/pyproject.toml +2 -2
  5. {e2a-5.2.0 → e2a-5.3.0}/scripts/generate-oag.sh +4 -3
  6. {e2a-5.2.0 → e2a-5.3.0}/scripts/strip-enum-validators.py +16 -5
  7. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/__init__.py +2 -0
  8. e2a-5.3.0/src/e2a/v1/_event_lifecycle.py +44 -0
  9. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/client.py +51 -0
  10. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/__init__.py +4 -0
  11. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/messages_api.py +311 -0
  12. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/__init__.py +2 -0
  13. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/domain_suppression_added_data.py +11 -1
  14. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/email_bounced_data.py +11 -1
  15. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/email_complained_data.py +11 -1
  16. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/email_delivered_data.py +11 -1
  17. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/email_failed_data.py +11 -1
  18. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/email_received_data.py +11 -1
  19. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/email_sent_data.py +11 -1
  20. e2a-5.3.0/src/e2a/v1/generated/models/message_lifecycle_transition.py +156 -0
  21. e2a-5.3.0/src/e2a/v1/generated/models/page_message_lifecycle_transition.py +115 -0
  22. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/webhook_signature.py +43 -14
  23. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/websocket.py +12 -13
  24. {e2a-5.2.0 → e2a-5.3.0}/tests/test_contract.py +283 -11
  25. e2a-5.3.0/tests/test_enum_forward_compat.py +167 -0
  26. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_client.py +95 -0
  27. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_sync_client.py +57 -2
  28. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_websocket.py +81 -1
  29. {e2a-5.2.0 → e2a-5.3.0}/tests/test_webhook_payloads.py +120 -1
  30. e2a-5.3.0/tests/typecheck_event_envelope.py +84 -0
  31. e2a-5.3.0/tests/typecheck_lifecycle_payloads.py +47 -0
  32. e2a-5.2.0/tests/test_enum_forward_compat.py +0 -64
  33. e2a-5.2.0/tests/typecheck_event_envelope.py +0 -36
  34. {e2a-5.2.0 → e2a-5.3.0}/.gitignore +0 -0
  35. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/__init__.py +0 -0
  36. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/py.typed +0 -0
  37. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/_retry.py +0 -0
  38. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/errors.py +0 -0
  39. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/__init__.py +0 -0
  40. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/account_api.py +0 -0
  41. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/agents_api.py +0 -0
  42. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/conversations_api.py +0 -0
  43. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/domains_api.py +0 -0
  44. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/events_api.py +0 -0
  45. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/meta_api.py +0 -0
  46. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/reviews_api.py +0 -0
  47. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/templates_api.py +0 -0
  48. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api/webhooks_api.py +0 -0
  49. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api_client.py +0 -0
  50. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/api_response.py +0 -0
  51. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/configuration.py +0 -0
  52. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/exceptions.py +0 -0
  53. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/account_user_view.py +0 -0
  54. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/account_view.py +0 -0
  55. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/agent_identity.py +0 -0
  56. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/agent_suppression_added_data.py +0 -0
  57. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/agent_suppression_view.py +0 -0
  58. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/agent_view.py +0 -0
  59. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/api_key_export_entry.py +0 -0
  60. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/api_key_view.py +0 -0
  61. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/approve_request.py +0 -0
  62. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/attachment.py +0 -0
  63. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/attachment_meta_view.py +0 -0
  64. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/attachment_view.py +0 -0
  65. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/authentication.py +0 -0
  66. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/conversation_detail_view.py +0 -0
  67. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/conversation_summary_view.py +0 -0
  68. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/create_agent_request.py +0 -0
  69. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/create_agent_suppression_request.py +0 -0
  70. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/create_api_key_request.py +0 -0
  71. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/create_api_key_response.py +0 -0
  72. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/create_template_request.py +0 -0
  73. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/create_webhook_request.py +0 -0
  74. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/create_webhook_response.py +0 -0
  75. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_agent_result.py +0 -0
  76. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_api_key_result.py +0 -0
  77. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_domain_result.py +0 -0
  78. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_message_result.py +0 -0
  79. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_suppression_result.py +0 -0
  80. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_template_result.py +0 -0
  81. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_user_data_result.py +0 -0
  82. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delete_webhook_result.py +0 -0
  83. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/delivery_status_json.py +0 -0
  84. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/deployment_info_view.py +0 -0
  85. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/dkim_result.py +0 -0
  86. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/dmarc_result.py +0 -0
  87. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/dns_record.py +0 -0
  88. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/domain.py +0 -0
  89. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/domain_sending_failed_data.py +0 -0
  90. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/domain_sending_verified_data.py +0 -0
  91. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/domain_view.py +0 -0
  92. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/error_body.py +0 -0
  93. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/error_envelope.py +0 -0
  94. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/event_envelope.py +0 -0
  95. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/event_view.py +0 -0
  96. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/field_error.py +0 -0
  97. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/forward_request.py +0 -0
  98. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/hold_reason_view.py +0 -0
  99. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/limit_exceeded_details.py +0 -0
  100. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/limit_exceeded_envelope.py +0 -0
  101. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/limit_exceeded_error_body.py +0 -0
  102. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/limits_caps_view.py +0 -0
  103. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/limits_usage_view.py +0 -0
  104. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/message.py +0 -0
  105. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/message_body_view.py +0 -0
  106. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/message_parsed_view.py +0 -0
  107. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/message_summary_view.py +0 -0
  108. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/message_view.py +0 -0
  109. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/o_auth_connection_entry.py +0 -0
  110. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_agent_suppression_view.py +0 -0
  111. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_agent_view.py +0 -0
  112. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_api_key_view.py +0 -0
  113. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_conversation_summary_view.py +0 -0
  114. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_domain_view.py +0 -0
  115. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_event_view.py +0 -0
  116. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_message_summary_view.py +0 -0
  117. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_review_view.py +0 -0
  118. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_starter_template_view.py +0 -0
  119. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_suppression_view.py +0 -0
  120. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_template_summary_view.py +0 -0
  121. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_webhook_delivery_view.py +0 -0
  122. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/page_webhook_view.py +0 -0
  123. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/payload_too_large_details.py +0 -0
  124. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_config_request.py +0 -0
  125. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_config_view.py +0 -0
  126. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_direction_request.py +0 -0
  127. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_direction_view.py +0 -0
  128. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_event_export_entry.py +0 -0
  129. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_finding_view.py +0 -0
  130. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_gate_request.py +0 -0
  131. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_gate_view.py +0 -0
  132. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_holds_request.py +0 -0
  133. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_holds_view.py +0 -0
  134. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_scan_request.py +0 -0
  135. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/protection_scan_view.py +0 -0
  136. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/rate_limited_details.py +0 -0
  137. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/rate_limited_envelope.py +0 -0
  138. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/rate_limited_error_body.py +0 -0
  139. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/redeliver_delivery.py +0 -0
  140. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/redeliver_event_request.py +0 -0
  141. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/redeliver_view.py +0 -0
  142. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/register_domain_request.py +0 -0
  143. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/reject_request.py +0 -0
  144. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/reject_result_view.py +0 -0
  145. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/rendered_template_view.py +0 -0
  146. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/reply_request.py +0 -0
  147. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/retry_after_details.py +0 -0
  148. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/review_view.py +0 -0
  149. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/rotate_secret_response.py +0 -0
  150. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/send_email_request.py +0 -0
  151. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/send_result_view.py +0 -0
  152. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/sending_ramp_view.py +0 -0
  153. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/spf_result.py +0 -0
  154. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/starter_template_detail_view.py +0 -0
  155. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/starter_template_variable_view.py +0 -0
  156. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/starter_template_view.py +0 -0
  157. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/suppression_export_entry.py +0 -0
  158. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/suppression_view.py +0 -0
  159. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/template_part_error.py +0 -0
  160. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/template_summary_view.py +0 -0
  161. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/template_view.py +0 -0
  162. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/test_webhook_request.py +0 -0
  163. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/test_webhook_response.py +0 -0
  164. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/threat_category_view.py +0 -0
  165. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/too_many_recipients_details.py +0 -0
  166. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/unsubscribe_options.py +0 -0
  167. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/update_agent_request.py +0 -0
  168. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/update_message_request.py +0 -0
  169. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/update_message_result_view.py +0 -0
  170. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/update_template_request.py +0 -0
  171. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/update_webhook_request.py +0 -0
  172. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/usage_event_entry.py +0 -0
  173. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/user_export.py +0 -0
  174. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/user_export_user.py +0 -0
  175. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/validate_template_request.py +0 -0
  176. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/validate_template_response.py +0 -0
  177. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/validation_error_details.py +0 -0
  178. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/verify_domain_view.py +0 -0
  179. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/webhook_delivery_view.py +0 -0
  180. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/webhook_filters_request.py +0 -0
  181. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/webhook_filters_view.py +0 -0
  182. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/models/webhook_view.py +0 -0
  183. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/py.typed +0 -0
  184. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/generated/rest.py +0 -0
  185. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/inbound.py +0 -0
  186. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/pagination.py +0 -0
  187. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/py.typed +0 -0
  188. {e2a-5.2.0 → e2a-5.3.0}/src/e2a/v1/sync_client.py +0 -0
  189. {e2a-5.2.0 → e2a-5.3.0}/tests/__init__.py +0 -0
  190. {e2a-5.2.0 → e2a-5.3.0}/tests/test_e2e.py +0 -0
  191. {e2a-5.2.0 → e2a-5.3.0}/tests/test_exports.py +0 -0
  192. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_client_side_validation.py +0 -0
  193. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_errors.py +0 -0
  194. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_inbound.py +0 -0
  195. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_pagination.py +0 -0
  196. {e2a-5.2.0 → e2a-5.3.0}/tests/test_v1_retry.py +0 -0
  197. {e2a-5.2.0 → e2a-5.3.0}/tests/test_webhook_signature.py +0 -0
  198. {e2a-5.2.0 → e2a-5.3.0}/tests/typecheck_inbound.py +0 -0
  199. {e2a-5.2.0 → e2a-5.3.0}/uv.lock +0 -0
@@ -1,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 5.3.0
4
+
5
+ ### Added
6
+ - **`client.messages.delete(email, message_id, *, permanent=False)`** — move a
7
+ message to the trash, reversible via `client.messages.restore(...)` until the
8
+ trash retention window expires (30 days by default), so the soft delete needs
9
+ no confirmation. Pass `permanent=True` to delete forever a message that is
10
+ already in the trash (irreversible, account scope only; the SDK supplies the
11
+ `?confirm=DELETE` guard the raw API requires on that path). Available on both
12
+ `AsyncE2AClient` and the synchronous `E2AClient`.
13
+ - **`client.messages.get_lifecycle(email, message_id, *, cursor=None, limit=None)`**
14
+ — page through a message's canonical lifecycle transitions (send, delivery,
15
+ bounce, review, deletion, …) as `PageMessageLifecycleTransition`. Beta.
16
+
17
+ ## 5.2.0
18
+
19
+ The first publish to PyPI since 4.0.1: 5.0.0, 5.1.0, and 5.2.0 all reach users
20
+ in this release, so read those three sections together when upgrading from 4.x.
4
21
 
5
22
  ### Added
6
23
  - `client.inbound.from_event(event)` returns `AsyncInboundEmail` on
@@ -41,20 +58,23 @@
41
58
  ``EventJSON`` → ``EventView``, ``Suppression`` → ``SuppressionView``,
42
59
  ``Result`` → ``AuthVerdict``, ``AttachmentMeta`` → ``AttachmentMetaView``.
43
60
 
44
- ## 5.2.0
45
-
46
- ### Breaking (pre-GA)
47
- - **The reserved-word wire field `from` is now uniformly exposed as `from_`
48
- (PEP 8 trailing underscore) on generated models** (was the generator-mangled
49
- `var_from`). Affected models: `Message`, `MessageView`, `MessageSummaryView`,
50
- `ReviewView`, `EmailReceivedData`, `EmailSentData`, `EmailFailedData`
51
- (generated), plus the generated `list_messages` sender filter parameter. The
61
+ - **The reserved-word wire field `from` is exposed as `from_` (PEP 8 trailing
62
+ underscore) where a sender is still projected** (was the generator-mangled
63
+ `var_from`). This is the generated `list_messages` sender filter parameter
64
+ and the outbound-only `EmailSentData` / `EmailFailedData` models; the
52
65
  hand-written layer's `messages.list(from_=...)` already used `from_`, so the
53
- SDK now teaches exactly one spelling; the TypeScript SDK exposes the same
66
+ SDK teaches exactly one spelling, and the TypeScript SDK exposes the same
54
67
  `from_`. The wire JSON is unchanged — requests and responses still carry
55
68
  `from` (pydantic alias). The webhook/WS `data` payload TypedDicts are
56
- wire-true dicts and keep the literal `"from"` key (access as
57
- `data["from"]`). Migrate: `message.var_from` → `message.from_`.
69
+ wire-true dicts and keep the literal `"from"` key (access as `data["from"]`).
70
+
71
+ An intermediate step on `main` also renamed the *inbound* sender projection
72
+ on `Message`, `MessageView`, `MessageSummaryView`, `ReviewView`, and
73
+ `EmailReceivedData` from `var_from` to `from_`. The DMARC-aligned contract
74
+ above replaced that projection with `header_from` before this release, so
75
+ `from_` never reached PyPI on those models and there is nothing to migrate
76
+ off — 4.x callers reading `message.var_from` go straight to
77
+ `message.header_from`.
58
78
 
59
79
  ## 5.1.0
60
80
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e2a
3
- Version: 5.2.0
3
+ Version: 5.3.0
4
4
  Summary: Python SDK for the e2a protocol — email-to-agent authentication
5
5
  Project-URL: Homepage, https://e2a.dev
6
6
  Project-URL: Repository, https://github.com/tokencanopy/e2a
@@ -141,6 +141,10 @@ retries + idempotency, and async auto-pagination.
141
141
 
142
142
  ## Quick Start
143
143
 
144
+ For signed-webhook examples that fetch and reply through the ergonomic inbound
145
+ facade, see the [minimal Python and TypeScript OpenAI examples with provider
146
+ snippets](../../examples/agent-framework-webhooks/README.md).
147
+
144
148
  ### Synchronous
145
149
 
146
150
  ```python
@@ -264,7 +268,11 @@ async def webhook(request):
264
268
  # From, Reply-To, bodies, and attachment names/types are untrusted input.
265
269
  print(email.envelope_from, email.verified, email.subject, email.text)
266
270
  print("reply will target", email.reply_targets)
267
- result = await email.reply({"text": "Got it"}, idempotency_key=f"reply:{event.id}")
271
+ agent_thread_id = await get_or_create_agent_thread(email.conversation_id)
272
+ result = await email.reply(
273
+ {"text": "Got it", "conversation_id": agent_thread_id},
274
+ idempotency_key=f"reply:{event.id}",
275
+ )
268
276
  if result.status == "pending_review":
269
277
  print("reply is awaiting approval")
270
278
  return {"ok": True}
@@ -375,18 +383,29 @@ page = client.messages.list("bot@agents.e2a.dev", limit=100).page()
375
383
 
376
384
  ### Trash and restore
377
385
 
378
- Soft-deleted agents and messages remain restorable for about 30 days. List the
379
- trash with `deleted=True`, then restore an item through the same resource. The
380
- sync client exposes the same methods without `await`.
386
+ `delete()` is a soft delete: agents and messages move to the trash and stay
387
+ restorable for about 30 days. List the trash with `deleted=True`, then restore
388
+ an item through the same resource. The sync client exposes the same methods
389
+ without `await`.
381
390
 
382
391
  ```python
392
+ await client.agents.delete("bot@agents.e2a.dev")
383
393
  trashed_agents = client.agents.list(deleted=True)
384
394
  await client.agents.restore("bot@agents.e2a.dev")
385
395
 
396
+ await client.messages.delete("bot@agents.e2a.dev", "msg_abc123")
386
397
  trashed_messages = client.messages.list("bot@agents.e2a.dev", deleted=True)
387
398
  await client.messages.restore("bot@agents.e2a.dev", "msg_abc123")
388
399
  ```
389
400
 
401
+ A message already in the trash can be purged early and irreversibly. That path
402
+ needs an account-scoped credential; the SDK supplies the API's `?confirm=DELETE`
403
+ guard for you:
404
+
405
+ ```python
406
+ await client.messages.delete("bot@agents.e2a.dev", "msg_abc123", permanent=True)
407
+ ```
408
+
390
409
  ## WebSocket (real-time delivery for local agents)
391
410
 
392
411
  ```python
@@ -427,8 +446,16 @@ e2a surfaces it on the recipient's inbound — via `In-Reply-To` for humans, or
427
446
  forge-resistant `X-E2A-Conversation-Id` header for same-platform agent-to-agent
428
447
  mail. It is not a security boundary; for sender-domain authentication require
429
448
  `message.authentication is not None and message.authentication.dmarc.status == "pass"`
430
- and compare the literal `message.header_from` address separately. On first contact from a human the
431
- conversation ID arrives `None` assign one yourself if you want to thread.
449
+ and compare the literal `message.header_from` address separately. On first
450
+ contact from a human the conversation ID arrives `None`. Create the agent
451
+ runtime's internal thread before replying, then pass its stable, non-sensitive
452
+ thread/session ID (or an opaque stored alias) as `conversation_id`; reuse it on
453
+ every later send or reply. If a later inbound ID matches a binding you
454
+ previously stored, resume that internal thread. Keep replying by the original
455
+ message ID as well — the conversation ID aligns e2a grouping with agent memory,
456
+ while the reply endpoint sets the email headers Gmail/Outlook use. Scope
457
+ bindings to the inbox and sender, and never use the conversation ID as
458
+ authorization.
432
459
 
433
460
  ```python
434
461
  await client.messages.send(address, {
@@ -103,6 +103,10 @@ retries + idempotency, and async auto-pagination.
103
103
 
104
104
  ## Quick Start
105
105
 
106
+ For signed-webhook examples that fetch and reply through the ergonomic inbound
107
+ facade, see the [minimal Python and TypeScript OpenAI examples with provider
108
+ snippets](../../examples/agent-framework-webhooks/README.md).
109
+
106
110
  ### Synchronous
107
111
 
108
112
  ```python
@@ -226,7 +230,11 @@ async def webhook(request):
226
230
  # From, Reply-To, bodies, and attachment names/types are untrusted input.
227
231
  print(email.envelope_from, email.verified, email.subject, email.text)
228
232
  print("reply will target", email.reply_targets)
229
- result = await email.reply({"text": "Got it"}, idempotency_key=f"reply:{event.id}")
233
+ agent_thread_id = await get_or_create_agent_thread(email.conversation_id)
234
+ result = await email.reply(
235
+ {"text": "Got it", "conversation_id": agent_thread_id},
236
+ idempotency_key=f"reply:{event.id}",
237
+ )
230
238
  if result.status == "pending_review":
231
239
  print("reply is awaiting approval")
232
240
  return {"ok": True}
@@ -337,18 +345,29 @@ page = client.messages.list("bot@agents.e2a.dev", limit=100).page()
337
345
 
338
346
  ### Trash and restore
339
347
 
340
- Soft-deleted agents and messages remain restorable for about 30 days. List the
341
- trash with `deleted=True`, then restore an item through the same resource. The
342
- sync client exposes the same methods without `await`.
348
+ `delete()` is a soft delete: agents and messages move to the trash and stay
349
+ restorable for about 30 days. List the trash with `deleted=True`, then restore
350
+ an item through the same resource. The sync client exposes the same methods
351
+ without `await`.
343
352
 
344
353
  ```python
354
+ await client.agents.delete("bot@agents.e2a.dev")
345
355
  trashed_agents = client.agents.list(deleted=True)
346
356
  await client.agents.restore("bot@agents.e2a.dev")
347
357
 
358
+ await client.messages.delete("bot@agents.e2a.dev", "msg_abc123")
348
359
  trashed_messages = client.messages.list("bot@agents.e2a.dev", deleted=True)
349
360
  await client.messages.restore("bot@agents.e2a.dev", "msg_abc123")
350
361
  ```
351
362
 
363
+ A message already in the trash can be purged early and irreversibly. That path
364
+ needs an account-scoped credential; the SDK supplies the API's `?confirm=DELETE`
365
+ guard for you:
366
+
367
+ ```python
368
+ await client.messages.delete("bot@agents.e2a.dev", "msg_abc123", permanent=True)
369
+ ```
370
+
352
371
  ## WebSocket (real-time delivery for local agents)
353
372
 
354
373
  ```python
@@ -389,8 +408,16 @@ e2a surfaces it on the recipient's inbound — via `In-Reply-To` for humans, or
389
408
  forge-resistant `X-E2A-Conversation-Id` header for same-platform agent-to-agent
390
409
  mail. It is not a security boundary; for sender-domain authentication require
391
410
  `message.authentication is not None and message.authentication.dmarc.status == "pass"`
392
- and compare the literal `message.header_from` address separately. On first contact from a human the
393
- conversation ID arrives `None` assign one yourself if you want to thread.
411
+ and compare the literal `message.header_from` address separately. On first
412
+ contact from a human the conversation ID arrives `None`. Create the agent
413
+ runtime's internal thread before replying, then pass its stable, non-sensitive
414
+ thread/session ID (or an opaque stored alias) as `conversation_id`; reuse it on
415
+ every later send or reply. If a later inbound ID matches a binding you
416
+ previously stored, resume that internal thread. Keep replying by the original
417
+ message ID as well — the conversation ID aligns e2a grouping with agent memory,
418
+ while the reply endpoint sets the email headers Gmail/Outlook use. Scope
419
+ bindings to the inbox and sender, and never use the conversation ID as
420
+ authorization.
394
421
 
395
422
  ```python
396
423
  await client.messages.send(address, {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "e2a"
7
- version = "5.2.0"
7
+ version = "5.3.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"
@@ -51,7 +51,7 @@ ws = ["websockets>=14"]
51
51
 
52
52
  [tool.mypy]
53
53
  python_version = "3.9"
54
- files = ["tests/test_e2e.py", "tests/typecheck_event_envelope.py", "tests/typecheck_inbound.py"]
54
+ files = ["tests/test_e2e.py", "tests/typecheck_event_envelope.py", "tests/typecheck_inbound.py", "tests/typecheck_lifecycle_payloads.py"]
55
55
  follow_imports = "silent"
56
56
  show_error_codes = true
57
57
  warn_unused_ignores = true
@@ -46,9 +46,10 @@ rm -rf "$DEST"
46
46
  cp -r "$TMP/e2a/v1/generated" "$DEST"
47
47
  rm -rf "$TMP"
48
48
 
49
- # Strip the generator's `*_validate_enum` validators so the client tolerates
50
- # unknown enum values (forward-compat: a new server enum value must not crash a
51
- # deployed client). Matches the TypeScript SDK's passthrough behavior.
49
+ # Strip the generator's `*_validate_enum` validators from open response
50
+ # vocabularies so the client tolerates additive values. The post-processor
51
+ # preserves validators for the canonical, deliberately closed message
52
+ # lifecycle vocabulary.
52
53
  python3 "$ROOT/sdks/python/scripts/strip-enum-validators.py" "$DEST"
53
54
 
54
55
  # Normalize selected generator-known unused imports without making hand edits
@@ -1,14 +1,18 @@
1
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.
2
+ """Strip OpenAPI-Generator's open-set enum validators from generated models.
4
3
 
5
4
  Why: the generator emits, for every enum-typed field, a `@field_validator` that
6
5
  raises `ValueError` when the value isn't in a hard-coded set. On a RESPONSE
7
6
  field that means the day the server adds a new enum value (a new event `type`,
8
7
  `delivery_status`, `inbound_policy`, …) every deployed client crashes while
9
8
  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.
9
+ client-breaking one. The TypeScript SDK passes unknown open-set enum values
10
+ through untouched; this makes Python match, keeping those fields typed as
11
+ plain strings.
12
+
13
+ The canonical ``MessageLifecycleTransition`` vocabulary is deliberately
14
+ closed and versioned. Its generated validators are retained so Python enforces
15
+ the same direction, stage, outcome, and reason-code contract as the API.
12
16
 
13
17
  Run as part of generate-oag.sh (so the drift gate's regenerated output matches)
14
18
  and idempotently re-runnable on the committed tree. Removes each block of the
@@ -30,6 +34,9 @@ import re
30
34
  import sys
31
35
 
32
36
 
37
+ CLOSED_ENUM_MODELS = {"message_lifecycle_transition.py"}
38
+
39
+
33
40
  def strip_file(path: str) -> bool:
34
41
  with open(path, "r", encoding="utf-8") as f:
35
42
  lines = f.readlines()
@@ -81,7 +88,11 @@ def main() -> int:
81
88
  root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
82
89
  os.path.dirname(__file__), "..", "src", "e2a", "v1", "generated"
83
90
  )
84
- models = glob.glob(os.path.join(root, "models", "*.py"))
91
+ models = [
92
+ path
93
+ for path in glob.glob(os.path.join(root, "models", "*.py"))
94
+ if os.path.basename(path) not in CLOSED_ENUM_MODELS
95
+ ]
85
96
  touched = sum(1 for p in sorted(models) if strip_file(p))
86
97
  print(f"strip-enum-validators: removed enum validators from {touched} model file(s)")
87
98
  return 0
@@ -102,6 +102,8 @@ __all__ = [
102
102
  "E2AClient",
103
103
  "SyncAutoPager",
104
104
  "SyncStream",
105
+ "MessageLifecycleTransition",
106
+ "PageMessageLifecycleTransition",
105
107
  # Errors
106
108
  "E2AError",
107
109
  "E2AAuthError",
@@ -0,0 +1,44 @@
1
+ """Shared lifecycle coercion for v1 stable event envelopes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict
6
+
7
+ from .generated.models import MessageLifecycleTransition
8
+
9
+
10
+ _V1_STABLE_LIFECYCLE_EVENTS = {
11
+ "email.received",
12
+ "email.sent",
13
+ "email.failed",
14
+ "email.delivered",
15
+ "email.bounced",
16
+ "email.complained",
17
+ "domain.suppression_added",
18
+ }
19
+
20
+
21
+ def coerce_v1_stable_lifecycle(
22
+ schema_version: str,
23
+ event_type: str,
24
+ data: Dict[str, Any],
25
+ ) -> Dict[str, Any]:
26
+ """Return typed lifecycle rows only for the closed v1 stable contract.
27
+
28
+ When the field is absent—or the envelope version/type is outside that
29
+ contract—the original data object is returned unchanged. When present, a
30
+ shallow copy keeps ``event.raw`` as the original wire representation.
31
+ """
32
+ if (
33
+ schema_version != "1"
34
+ or event_type not in _V1_STABLE_LIFECYCLE_EVENTS
35
+ or "lifecycle_transitions" not in data
36
+ ):
37
+ return data
38
+
39
+ coerced = dict(data)
40
+ coerced["lifecycle_transitions"] = [
41
+ MessageLifecycleTransition.model_validate(row)
42
+ for row in data["lifecycle_transitions"]
43
+ ]
44
+ return coerced
@@ -45,6 +45,7 @@ from .generated.models import (
45
45
  DeleteAgentResult,
46
46
  DeleteApiKeyResult,
47
47
  DeleteDomainResult,
48
+ DeleteMessageResult,
48
49
  DeleteSuppressionResult,
49
50
  DeleteTemplateResult,
50
51
  DeleteUserDataResult,
@@ -56,6 +57,7 @@ from .generated.models import (
56
57
  AccountView,
57
58
  AttachmentView,
58
59
  MessageSummaryView,
60
+ PageMessageLifecycleTransition,
59
61
  MessageView,
60
62
  RedeliverEventRequest,
61
63
  RedeliverView,
@@ -477,6 +479,55 @@ class MessagesResource:
477
479
  async def get(self, email: str, message_id: str) -> MessageView:
478
480
  return await self._c._read(lambda h: self._api.get_message(email, message_id, _headers=h))
479
481
 
482
+ async def get_lifecycle(
483
+ self,
484
+ email: str,
485
+ message_id: str,
486
+ *,
487
+ cursor: Optional[str] = None,
488
+ limit: Optional[int] = None,
489
+ ) -> PageMessageLifecycleTransition:
490
+ """Beta: return the ordered observations e2a recorded for one message.
491
+
492
+ The lifecycle contract may change before it is declared stable.
493
+ """
494
+ return await self._c._read(
495
+ lambda h: self._api.get_message_lifecycle(
496
+ email,
497
+ message_id,
498
+ cursor=cursor,
499
+ limit=limit,
500
+ _headers=h,
501
+ )
502
+ )
503
+
504
+ async def delete(
505
+ self, email: str, message_id: str, *, permanent: bool = False
506
+ ) -> DeleteMessageResult:
507
+ """Move a message to the trash.
508
+
509
+ Reversible via ``restore()`` until the trash retention window expires
510
+ (30 days by default), so the default soft delete needs no confirmation.
511
+
512
+ Pass ``permanent=True`` to permanently delete a message that is ALREADY
513
+ in the trash ("delete forever") — irreversible, account scope only. The
514
+ typed .delete() call is the confirmation; the SDK supplies the
515
+ ?confirm=DELETE guard the raw API requires on that path (it is ignored
516
+ when permanent is unset).
517
+
518
+ A message held for review cannot be deleted (409 message_held) — resolve
519
+ it on the review queue first. Returns the deletion receipt
520
+ ({deleted, id}).
521
+ """
522
+ return await self._c._write_idempotent(
523
+ lambda h: self._api.delete_message(
524
+ # `permanent or None` omits the param entirely on the soft path,
525
+ # matching the TS SDK's wire shape (the server treats an absent
526
+ # and a false `permanent` identically).
527
+ email, message_id, permanent=permanent or None, confirm="DELETE", _headers=h
528
+ )
529
+ )
530
+
480
531
  async def restore(self, email: str, message_id: str) -> MessageView:
481
532
  """Restore a soft-deleted message and resume its retention clock."""
482
533
  return await self._c._write_unsafe(
@@ -97,6 +97,7 @@ __all__ = [
97
97
  "LimitsUsageView",
98
98
  "Message",
99
99
  "MessageBodyView",
100
+ "MessageLifecycleTransition",
100
101
  "MessageParsedView",
101
102
  "MessageSummaryView",
102
103
  "MessageView",
@@ -107,6 +108,7 @@ __all__ = [
107
108
  "PageConversationSummaryView",
108
109
  "PageDomainView",
109
110
  "PageEventView",
111
+ "PageMessageLifecycleTransition",
110
112
  "PageMessageSummaryView",
111
113
  "PageReviewView",
112
114
  "PageStarterTemplateView",
@@ -260,6 +262,7 @@ from e2a.v1.generated.models.limits_caps_view import LimitsCapsView as LimitsCap
260
262
  from e2a.v1.generated.models.limits_usage_view import LimitsUsageView as LimitsUsageView
261
263
  from e2a.v1.generated.models.message import Message as Message
262
264
  from e2a.v1.generated.models.message_body_view import MessageBodyView as MessageBodyView
265
+ from e2a.v1.generated.models.message_lifecycle_transition import MessageLifecycleTransition as MessageLifecycleTransition
263
266
  from e2a.v1.generated.models.message_parsed_view import MessageParsedView as MessageParsedView
264
267
  from e2a.v1.generated.models.message_summary_view import MessageSummaryView as MessageSummaryView
265
268
  from e2a.v1.generated.models.message_view import MessageView as MessageView
@@ -270,6 +273,7 @@ from e2a.v1.generated.models.page_agent_view import PageAgentView as PageAgentVi
270
273
  from e2a.v1.generated.models.page_conversation_summary_view import PageConversationSummaryView as PageConversationSummaryView
271
274
  from e2a.v1.generated.models.page_domain_view import PageDomainView as PageDomainView
272
275
  from e2a.v1.generated.models.page_event_view import PageEventView as PageEventView
276
+ from e2a.v1.generated.models.page_message_lifecycle_transition import PageMessageLifecycleTransition as PageMessageLifecycleTransition
273
277
  from e2a.v1.generated.models.page_message_summary_view import PageMessageSummaryView as PageMessageSummaryView
274
278
  from e2a.v1.generated.models.page_review_view import PageReviewView as PageReviewView
275
279
  from e2a.v1.generated.models.page_starter_template_view import PageStarterTemplateView as PageStarterTemplateView