python-event-sourcery 0.5.3a5__tar.gz → 0.6.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 (231) hide show
  1. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/AGENTS.md +4 -0
  2. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/CHANGELOG.md +9 -0
  3. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/PKG-INFO +3 -1
  4. python_event_sourcery-0.6.0/docs/adr/20260926-typed-event-context-components-and-enrichers.md +204 -0
  5. python_event_sourcery-0.6.0/docs/code/test_context_enrichers.py +40 -0
  6. python_event_sourcery-0.6.0/docs/code/test_otel.py +38 -0
  7. python_event_sourcery-0.6.0/docs/recipes/context_enrichment.md +17 -0
  8. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/integrate.md +1 -0
  9. python_event_sourcery-0.6.0/docs/recipes/opentelemetry.md +25 -0
  10. python_event_sourcery-0.6.0/docs/reference/event_store/event/ContextComponent.md +1 -0
  11. python_event_sourcery-0.6.0/docs/reference/event_store/event/EventContextEnricher.md +1 -0
  12. python_event_sourcery-0.6.0/docs/reference/event_store/event/EventCorrelation.md +1 -0
  13. python_event_sourcery-0.6.0/docs/reference/otel/OpenTelemetryCarrier.md +1 -0
  14. python_event_sourcery-0.6.0/docs/reference/otel/OpenTelemetryContextEnricher.md +1 -0
  15. python_event_sourcery-0.6.0/docs/reference/otel/get_parent_context.md +1 -0
  16. python_event_sourcery-0.6.0/docs/reference/otel/use_parent_context.md +1 -0
  17. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/backend.py +18 -1
  18. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/dto.py +59 -13
  19. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/serde.py +22 -12
  20. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event_store.py +44 -4
  21. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event.py +6 -0
  22. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/exceptions.py +5 -0
  23. python_event_sourcery-0.6.0/event_sourcery_otel/__init__.py +12 -0
  24. python_event_sourcery-0.6.0/event_sourcery_otel/_carrier.py +9 -0
  25. python_event_sourcery-0.6.0/event_sourcery_otel/_context.py +38 -0
  26. python_event_sourcery-0.6.0/event_sourcery_otel/_enricher.py +27 -0
  27. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/pyproject.toml +8 -4
  28. python_event_sourcery-0.6.0/tests/AGENTS.md +95 -0
  29. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/bdd.py +9 -1
  30. python_event_sourcery-0.6.0/tests/event_store/event/test_dto.py +100 -0
  31. python_event_sourcery-0.6.0/tests/event_store/test_context_enrichment.py +174 -0
  32. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_save_retrieval.py +28 -14
  33. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/matchers.py +28 -3
  34. python_event_sourcery-0.6.0/tests/otel/AGENTS.md +23 -0
  35. python_event_sourcery-0.6.0/tests/otel/bdd.py +67 -0
  36. python_event_sourcery-0.6.0/tests/otel/conftest.py +15 -0
  37. python_event_sourcery-0.6.0/tests/otel/test_context_propagation.py +64 -0
  38. python_event_sourcery-0.6.0/tests/otel/test_parent_context.py +73 -0
  39. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/uv.lock +18 -2
  40. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/zensical.toml +12 -0
  41. python_event_sourcery-0.5.3a5/tests/event_store/event/test_dto.py +0 -30
  42. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.adr-dir +0 -0
  43. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/bump-version-and-publish.yml +0 -0
  44. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/deploy-docs.yml +0 -0
  45. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/main.yml +0 -0
  46. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/publish-release.yml +0 -0
  47. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.gitignore +0 -0
  48. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/CONTRIBUTING.md +0 -0
  49. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/LICENSE +0 -0
  50. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/README.md +0 -0
  51. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/compose.yml +0 -0
  52. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/django_manage.py +0 -0
  53. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20220730-record-architecture-decisions.md +0 -0
  54. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20230903-explicit-init-arguments-for-sqlalchemy-s-models.md +0 -0
  55. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20231027_1-packaging-of-the-project.md +0 -0
  56. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20231027_2-leave-implementation-of-synchronous-dispatcher-to-users-of-the-library.md +0 -0
  57. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20231029-in-memory-event-store.md +0 -0
  58. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20231230-in_transaction_subscription.md +0 -0
  59. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20240726-dependencies-and-distribution.md +0 -0
  60. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20241007-multitenancy.md +0 -0
  61. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20250411-packaging-of-the-project-revised.md +0 -0
  62. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20250729-privacy-data-encryption-and-shredding.md +0 -0
  63. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20251004-packaging-of-the-event_store-package.md +0 -0
  64. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20260314-aggregate-metadata.md +0 -0
  65. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/__init__.py +0 -0
  66. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/recipes/__init__.py +0 -0
  67. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/recipes/integrate/__init__.py +0 -0
  68. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/recipes/integrate/kurrent_01.py +0 -0
  69. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/test_get_started.py +0 -0
  70. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/test_recipes.py +0 -0
  71. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/concepts.md +0 -0
  72. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/get_started.md +0 -0
  73. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/index.md +0 -0
  74. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/data_privacy.md +0 -0
  75. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/defining_events.md +0 -0
  76. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/event_sourcing.md +0 -0
  77. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/multitenancy.md +0 -0
  78. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/outbox.md +0 -0
  79. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/saving_events.md +0 -0
  80. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/snapshots.md +0 -0
  81. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/subscriptions.md +0 -0
  82. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/django.md +0 -0
  83. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/in_memory.md +0 -0
  84. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/kurrentdb.md +0 -0
  85. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/sqlalchemy.md +0 -0
  86. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_sourcing/Aggregate.md +0 -0
  87. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_sourcing/Repository.md +0 -0
  88. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_sourcing/WrappedAggregate.md +0 -0
  89. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/DEFAULT_TENANT.md +0 -0
  90. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/EventStore.md +0 -0
  91. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/NO_VERSIONING.md +0 -0
  92. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/StreamCategory.md +0 -0
  93. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/StreamId.md +0 -0
  94. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/StreamUUID.md +0 -0
  95. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/TenantId.md +0 -0
  96. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/Backend.md +0 -0
  97. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/TransactionalBackend.md +0 -0
  98. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/not_configured.md +0 -0
  99. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/singleton.md +0 -0
  100. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/DataSubject.md +0 -0
  101. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/Encrypted.md +0 -0
  102. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/Encryption.md +0 -0
  103. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/NoEncryptionStrategy.md +0 -0
  104. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/NoKeyStorageStrategy.md +0 -0
  105. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Context.md +0 -0
  106. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Entry.md +0 -0
  107. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Event.md +0 -0
  108. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Position.md +0 -0
  109. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/RawEvent.md +0 -0
  110. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Recorded.md +0 -0
  111. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/RecordedRaw.md +0 -0
  112. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/WrappedEvent.md +0 -0
  113. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/exceptions.md +0 -0
  114. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/in_transaction/Dispatcher.md +0 -0
  115. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/in_transaction/Listener.md +0 -0
  116. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/in_transaction/Listeners.md +0 -0
  117. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/EncryptionKeyStorageStrategy.md +0 -0
  118. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/EncryptionStrategy.md +0 -0
  119. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/OutboxFiltererStrategy.md +0 -0
  120. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/OutboxStorageStrategy.md +0 -0
  121. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/StorageStrategy.md +0 -0
  122. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/SubscriptionStrategy.md +0 -0
  123. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/outbox/NoOutboxStorageStrategy.md +0 -0
  124. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/outbox/Outbox.md +0 -0
  125. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/outbox/no_filter.md +0 -0
  126. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/BuildPhase.md +0 -0
  127. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/FilterPhase.md +0 -0
  128. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/PositionPhase.md +0 -0
  129. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/SubscriptionBuilder.md +0 -0
  130. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/res/eda_example.plantuml +0 -0
  131. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/res/eda_example.png +0 -0
  132. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/__init__.py +0 -0
  133. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/__init__.py +0 -0
  134. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/__init__.py +0 -0
  135. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/encryption.py +0 -0
  136. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/registry.py +0 -0
  137. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/in_memory.py +0 -0
  138. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/outbox.py +0 -0
  139. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/stream_id.py +0 -0
  140. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/__init__.py +0 -0
  141. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/builder.py +0 -0
  142. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/in_transaction.py +0 -0
  143. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/interfaces.py +0 -0
  144. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/tenant_id.py +0 -0
  145. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/versioning.py +0 -0
  146. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/backend.py +0 -0
  147. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/encryption.py +0 -0
  148. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event_sourcing/__init__.py +0 -0
  149. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event_sourcing/aggregate.py +0 -0
  150. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event_sourcing/repository.py +0 -0
  151. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/in_transaction.py +0 -0
  152. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/interfaces.py +0 -0
  153. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/outbox.py +0 -0
  154. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/py.typed +0 -0
  155. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/read_model/__init__.py +0 -0
  156. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/read_model/cursors_dao.py +0 -0
  157. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/read_model/projector.py +0 -0
  158. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/subscription.py +0 -0
  159. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/__init__.py +0 -0
  160. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/dto.py +0 -0
  161. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/event_store.py +0 -0
  162. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/migrations/0001_initial.py +0 -0
  163. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/migrations/__init__.py +0 -0
  164. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/models.py +0 -0
  165. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/outbox.py +0 -0
  166. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/py.typed +0 -0
  167. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/subscription.py +0 -0
  168. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/__init__.py +0 -0
  169. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/dto.py +0 -0
  170. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/event_store.py +0 -0
  171. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/outbox.py +0 -0
  172. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/py.typed +0 -0
  173. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/stream.py +0 -0
  174. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/subscription.py +0 -0
  175. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/__init__.py +0 -0
  176. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/cursors_dao.py +0 -0
  177. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/dto.py +0 -0
  178. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/event_store.py +0 -0
  179. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/guid.py +0 -0
  180. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/jsonb.py +0 -0
  181. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/models/__init__.py +0 -0
  182. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/models/base.py +0 -0
  183. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/models/default.py +0 -0
  184. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/outbox.py +0 -0
  185. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/py.typed +0 -0
  186. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/subscription.py +0 -0
  187. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/__init__.py +0 -0
  188. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/__init__.py +0 -0
  189. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/django.py +0 -0
  190. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/in_memory.py +0 -0
  191. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/kurrentdb.py +0 -0
  192. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/sqlalchemy.py +0 -0
  193. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/test_sqlalchemy.py +0 -0
  194. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/conftest.py +0 -0
  195. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/__init__.py +0 -0
  196. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/conftest.py +0 -0
  197. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/light_switch.py +0 -0
  198. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/test_aggregate.py +0 -0
  199. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/test_wrapped_aggregate.py +0 -0
  200. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/__init__.py +0 -0
  201. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/conftest.py +0 -0
  202. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/event/__init__.py +0 -0
  203. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/event/test_privacy.py +0 -0
  204. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/event/test_registry.py +0 -0
  205. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/__init__.py +0 -0
  206. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/conftest.py +0 -0
  207. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/test_filters.py +0 -0
  208. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/test_multitenancy.py +0 -0
  209. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/test_outbox.py +0 -0
  210. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/__init__.py +0 -0
  211. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/other_client.py +0 -0
  212. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_batch.py +0 -0
  213. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_from_position.py +0 -0
  214. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_handling_uncommitted_messages_from_other_clients.py +0 -0
  215. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_in_transaction_subscription.py +0 -0
  216. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_subscriptions.py +0 -0
  217. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_to_category.py +0 -0
  218. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_to_event_types.py +0 -0
  219. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_category.py +0 -0
  220. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_concurrency_control.py +0 -0
  221. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_multitenancy.py +0 -0
  222. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_named_streams.py +0 -0
  223. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_no_outbox.py +0 -0
  224. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_no_versioning.py +0 -0
  225. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_removing_stream.py +0 -0
  226. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_snapshots.py +0 -0
  227. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_stream_id.py +0 -0
  228. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/factories.py +0 -0
  229. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/mark.py +0 -0
  230. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/read_model/__init__.py +0 -0
  231. {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/read_model/test_projector.py +0 -0
@@ -35,6 +35,10 @@ Run single tools via `uv run --all-extras <tool>`, e.g. `uv run --all-extras pyt
35
35
  - New backend functionality goes through the shared test suite in `tests/`; use `not_implemented` / `skip_backend` markers for unsupported features.
36
36
  - Version bumps: `uv version --bump alpha` (CI autopublishes prereleases on push to main).
37
37
 
38
+ ## Tests
39
+
40
+ See [`tests/AGENTS.md`](tests/AGENTS.md) for required TDD workflow and test style.
41
+
38
42
  ## Docs
39
43
 
40
44
  Changing public API or behavior **requires** updating `docs/` and running docs tests —
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+ ### Added
5
+ - Typed, independently owned event context components with lazy access through `Context.get()`.
6
+ - Context enrichers configured through `Backend.with_context_enrichers()`.
7
+ - Optional `event_sourcery_otel` package for OpenTelemetry context propagation and parent-context activation.
8
+
9
+ ### Changed
10
+ - **Breaking:** New `Context` values reject arbitrary root fields; add new context data with typed components.
11
+
3
12
  ## 0.5.2
4
13
  ### Changed
5
14
  - **Breaking:** `Repository.aggregate()` yields `WrappedAggregate` instead of `Aggregate` directly
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-event-sourcery
3
- Version: 0.5.3a5
3
+ Version: 0.6.0
4
4
  Summary: A library for event-based systems in Python.
5
5
  Author-email: Sebastian Buczyński <nnplaya@gmail.com>, Łukasz Żukowski <lukasz.zukowski@gmail.com>
6
6
  License-Expression: MIT
@@ -13,6 +13,8 @@ Provides-Extra: django
13
13
  Requires-Dist: django<6,>=4; extra == 'django'
14
14
  Provides-Extra: kurrentdb
15
15
  Requires-Dist: kurrentdbclient>=1.0.6; extra == 'kurrentdb'
16
+ Provides-Extra: otel
17
+ Requires-Dist: opentelemetry-api<2,>=1; extra == 'otel'
16
18
  Provides-Extra: sqlalchemy
17
19
  Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
18
20
  Description-Content-Type: text/markdown
@@ -0,0 +1,204 @@
1
+ # Typed Event Context Components and Enrichers
2
+
3
+ Date: 2026-09-26
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ Discussion: [GitHub issue #115](https://github.com/python-event-sourcery/python-event-sourcery/issues/115)
10
+
11
+ ## Problem Statement
12
+
13
+ `Context` currently stores `correlation_id`, `causation_id`, and arbitrary extra
14
+ fields. Applications can validate the complete context as their own `Context`
15
+ subclass, but optional integrations cannot add strongly typed data without knowing
16
+ that complete application-specific type.
17
+
18
+ OpenTelemetry is the initial use case. Its integration needs to persist a typed
19
+ propagation carrier alongside existing application context. The same mechanism may
20
+ support request, tenant, authentication, and other integration-owned context data.
21
+
22
+ Python cannot express an accumulated type such as `ApplicationContext &
23
+ OpenTelemetryContext & RequestContext`. Each integration therefore needs to own and
24
+ type only its part of the event context.
25
+
26
+ ## Decision
27
+
28
+ ### Context components
29
+
30
+ Introduce a common Pydantic base for independently typed context components. Every
31
+ component declares an explicit, stable wire name:
32
+
33
+ ```python
34
+ class ContextComponent(BaseModel):
35
+ __context_name__: ClassVar[str]
36
+
37
+
38
+ class OpenTelemetryCarrier(ContextComponent):
39
+ __context_name__ = "opentelemetry"
40
+
41
+ values: dict[str, str]
42
+ ```
43
+
44
+ `Context` stores extension payloads in a dedicated `components` mapping. Its keys
45
+ are component wire names and its values are JSON-compatible component payloads:
46
+
47
+ ```json
48
+ {
49
+ "correlation_id": "...",
50
+ "causation_id": "...",
51
+ "components": {
52
+ "opentelemetry": {
53
+ "values": {
54
+ "traceparent": "00-...",
55
+ "tracestate": "..."
56
+ }
57
+ },
58
+ "application.request": {
59
+ "user_id": "123",
60
+ "request_id": "..."
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ The mapping prevents duplicate components by construction and keeps new
67
+ integration-owned data separate from root context fields.
68
+
69
+ ### Typed access and serialization
70
+
71
+ `Context` exposes lazy typed access:
72
+
73
+ ```python
74
+ otel = context.get(OpenTelemetryCarrier)
75
+ # OpenTelemetryCarrier | None
76
+ ```
77
+
78
+ `get()` derives the wire name from the requested component type, finds that
79
+ payload in `components`, and validates it with `component_type.model_validate()`.
80
+ `Context.with_component(component)` returns a new context containing the component
81
+ payload; it does not mutate the existing context.
82
+
83
+ `Serde` persists and restores `components` as raw JSON-compatible data. It does
84
+ not eagerly materialize component classes. Consequently, this change does not add
85
+ `ContextRegistry` or a generic registry abstraction: the caller supplies the
86
+ component class when it requests a typed value. An event written with an optional
87
+ integration can therefore be read and round-tripped by a process that does not
88
+ install that integration.
89
+
90
+ ### Context enrichers
91
+
92
+ An enricher declares the component type it owns and produces that component for an
93
+ event when applicable:
94
+
95
+ ```python
96
+ class EventContextEnricher(Protocol):
97
+ component_type: type[ContextComponent]
98
+
99
+ def enrich(
100
+ self,
101
+ context: Context,
102
+ *,
103
+ event: WrappedEvent,
104
+ stream_id: StreamId,
105
+ ) -> ContextComponent | None:
106
+ ...
107
+ ```
108
+
109
+ Enrichers are configured through `Backend.with_context_enrichers()` and run in
110
+ `EventStore._append()` after a `WrappedEvent` and its `Context` already exist, but
111
+ before serialization:
112
+
113
+ ```text
114
+ existing Context
115
+ -> context enrichers
116
+ -> Serde
117
+ -> StorageStrategy
118
+ ```
119
+
120
+ An enricher returns only the component it owns. It does not replace or mutate the
121
+ complete context. Enrichment happens once when a new event is written; it does not
122
+ run during loading, replay, subscription processing, or outbox retries.
123
+
124
+ A component already present on the incoming context is explicit and takes
125
+ precedence over enricher output. Two enrichers declaring the same component are
126
+ rejected by `with_context_enrichers()` before events can be appended.
127
+
128
+ ## Backward compatibility
129
+
130
+ ### Root correlation fields
131
+
132
+ The existing `correlation_id` and `causation_id` fields remain at the context root.
133
+ They are exposed through the component API as a special built-in
134
+ `EventCorrelation` component:
135
+
136
+ ```python
137
+ correlation = context.get(EventCorrelation)
138
+ ```
139
+
140
+ `EventCorrelation` is a typed view over the existing root fields.
141
+ `Context.with_component(EventCorrelation(...))` updates those root fields rather
142
+ than adding a nested component key.
143
+
144
+ ### Existing API and stored events
145
+
146
+ New `Context` values accept the root correlation fields and typed components. New
147
+ integration data is added with `Context.with_component()` rather than arbitrary
148
+ root fields:
149
+
150
+ ```python
151
+ context = Context(correlation_id=...)
152
+ context = context.with_component(RequestContext(user_id="123"))
153
+ ```
154
+
155
+ Existing stored events remain readable without data migration. Historical root
156
+ fields are retained when an event is deserialized, and existing whole-context
157
+ validation remains available:
158
+
159
+ ```python
160
+ wrapped_event.get_context(ApplicationContext)
161
+ ```
162
+
163
+ If `ApplicationContext` declares `components`, it receives their raw payloads;
164
+ otherwise `components` are excluded for compatibility with models using
165
+ `extra="forbid"`. Unknown keys within persisted `components` remain opaque raw
166
+ payloads, allowing processes without an optional integration to read and
167
+ round-trip their events.
168
+
169
+ This gives two compatible views:
170
+
171
+ ```text
172
+ Legacy: validate the complete historical context as an application model
173
+ Typed: retrieve an independent component by its class
174
+ ```
175
+
176
+ ## OpenTelemetry application
177
+
178
+ The optional `event_sourcery_otel` package defines an `OpenTelemetryCarrier` and
179
+ an enricher without adding an OpenTelemetry dependency to the core package:
180
+
181
+ ```text
182
+ current OpenTelemetry Context
183
+ -> propagator.inject()
184
+ -> OpenTelemetryCarrier
185
+ -> persisted event Context
186
+ ```
187
+
188
+ The enricher only snapshots the active propagation context. Consumers can
189
+ explicitly obtain or activate a persisted parent context through
190
+ `get_parent_context()` and `use_parent_context()`. Creating spans and deciding
191
+ their lifecycle remain application concerns.
192
+
193
+
194
+ ## Consequences
195
+
196
+ - Integrations can own strongly typed context data independently.
197
+ - Application-defined context and integration-defined components can coexist.
198
+ - Existing root fields, custom context models, and stored events remain compatible.
199
+ - Component validation happens lazily when code requests a component class.
200
+ - `EventStore` gains a context enrichment step before serialization.
201
+ - Stable component names become part of the persisted event format and must not be
202
+ changed without an explicit migration strategy.
203
+ - New integration data cannot collide with legacy root fields because it is stored
204
+ under `components`.
@@ -0,0 +1,40 @@
1
+ from typing import ClassVar
2
+
3
+ from event_sourcery import StreamId
4
+ from event_sourcery.backend import InMemoryBackend
5
+ from event_sourcery.event import Context, ContextComponent, Event, WrappedEvent
6
+
7
+
8
+ def test_context_enrichers() -> None:
9
+ # --8<-- [start:context_enrichers_01]
10
+ class RequestContext(ContextComponent):
11
+ __context_name__: ClassVar[str] = "application.request"
12
+
13
+ request_id: str
14
+
15
+ class RequestContextEnricher:
16
+ component_type: type[ContextComponent] = RequestContext
17
+
18
+ def enrich(
19
+ self,
20
+ context: Context,
21
+ *,
22
+ event: WrappedEvent,
23
+ stream_id: StreamId,
24
+ ) -> RequestContext:
25
+ return RequestContext(request_id="request-123")
26
+
27
+ class InvoicePaid(Event):
28
+ invoice_number: str
29
+
30
+ backend = InMemoryBackend().with_context_enrichers(RequestContextEnricher())
31
+ event_store = backend.event_store
32
+ stream_id = StreamId(name="invoices/1003")
33
+
34
+ event_store.append(InvoicePaid(invoice_number="1003"), stream_id=stream_id)
35
+
36
+ recorded = event_store.load_stream(stream_id)[0]
37
+ assert recorded.context.get(RequestContext) == RequestContext(
38
+ request_id="request-123"
39
+ )
40
+ # --8<-- [end:context_enrichers_01]
@@ -0,0 +1,38 @@
1
+ from opentelemetry import trace
2
+
3
+ from event_sourcery import StreamId
4
+ from event_sourcery.backend import InMemoryBackend
5
+ from event_sourcery.event import Event
6
+ from event_sourcery_otel import OpenTelemetryContextEnricher, use_parent_context
7
+
8
+
9
+ def test_otel_propagation() -> None:
10
+ # --8<-- [start:otel_01]
11
+ class InvoicePaid(Event):
12
+ invoice_number: str
13
+
14
+ backend = InMemoryBackend().with_context_enrichers(OpenTelemetryContextEnricher())
15
+ event_store = backend.event_store
16
+ stream_id = StreamId(name="invoices/1003")
17
+
18
+ span_context = trace.SpanContext(
19
+ trace_id=0x1234567890ABCDEF1234567890ABCDEF,
20
+ span_id=0x1234567890ABCDEF,
21
+ is_remote=False,
22
+ trace_flags=trace.TraceFlags(trace.TraceFlags.SAMPLED),
23
+ trace_state=trace.TraceState(),
24
+ )
25
+ with trace.use_span(trace.NonRecordingSpan(span_context), end_on_exit=False):
26
+ event_store.append(InvoicePaid(invoice_number="1003"), stream_id=stream_id)
27
+
28
+ recorded = event_store.load_stream(stream_id)[0]
29
+ # --8<-- [end:otel_01]
30
+
31
+ # --8<-- [start:otel_02]
32
+ with use_parent_context(recorded):
33
+ consume(recorded.event)
34
+ # --8<-- [end:otel_02]
35
+
36
+
37
+ def consume(event: Event) -> None:
38
+ assert isinstance(event, Event)
@@ -0,0 +1,17 @@
1
+ # Enrich event context
2
+
3
+ A context enricher adds a typed component immediately before an event is
4
+ serialized. Configure enrichers on the backend before accessing its event store.
5
+ The original event context is not mutated.
6
+
7
+ ```python
8
+ --8<--
9
+ docs/code/test_context_enrichers.py:context_enrichers_01
10
+ --8<--
11
+ ```
12
+
13
+ Pass multiple enrichers to `with_context_enrichers()` in their execution order.
14
+ Each enricher may return `None` when it has no component to add for an event.
15
+ If two enrichers declare the same component, `with_context_enrichers()` raises
16
+ `ConflictingContextComponent`. A component explicitly supplied with the event
17
+ takes precedence over enricher output.
@@ -61,6 +61,7 @@ docs/code/test_recipes.py:integrate_sql_02
61
61
 
62
62
  You can now use [EventStore] to load events from a stream or append new events.
63
63
 
64
+
64
65
  [Backend]: ../reference/event_store/backend/Backend.md
65
66
  [EventStore]: ../reference/event_store/EventStore.md
66
67
  [SQLAlchemy]: ../reference/backends/sqlalchemy.md
@@ -0,0 +1,25 @@
1
+ # OpenTelemetry context propagation
2
+
3
+ Install the optional integration:
4
+
5
+ ```bash
6
+ pip install "python-event-sourcery[otel]"
7
+ ```
8
+
9
+ Configure its enricher before accessing the event store. It persists the active
10
+ OpenTelemetry propagation context; it does not create or end spans.
11
+
12
+ ```python
13
+ --8<--
14
+ docs/code/test_otel.py:otel_01
15
+ --8<--
16
+ ```
17
+
18
+ When consuming a recorded event, explicitly activate its propagated parent
19
+ context before starting application work or a consumer span:
20
+
21
+ ```python
22
+ --8<--
23
+ docs/code/test_otel.py:otel_02
24
+ --8<--
25
+ ```
@@ -0,0 +1 @@
1
+ ::: event_sourcery.event.ContextComponent
@@ -0,0 +1 @@
1
+ ::: event_sourcery.event.EventContextEnricher
@@ -0,0 +1 @@
1
+ ::: event_sourcery.event.EventCorrelation
@@ -0,0 +1 @@
1
+ ::: event_sourcery_otel.OpenTelemetryCarrier
@@ -0,0 +1 @@
1
+ ::: event_sourcery_otel.OpenTelemetryContextEnricher
@@ -0,0 +1 @@
1
+ ::: event_sourcery_otel.get_parent_context
@@ -0,0 +1 @@
1
+ ::: event_sourcery_otel.use_parent_context
@@ -14,6 +14,7 @@ from event_sourcery._event_store.event.encryption import (
14
14
  from event_sourcery._event_store.event.registry import EventRegistry
15
15
  from event_sourcery._event_store.event.serde import Serde
16
16
  from event_sourcery._event_store.event_store import (
17
+ EventContextEnricher,
17
18
  EventStore,
18
19
  StorageStrategy,
19
20
  )
@@ -36,7 +37,7 @@ from event_sourcery._event_store.subscription.interfaces import (
36
37
  SubscriptionStrategy,
37
38
  )
38
39
  from event_sourcery._event_store.tenant_id import DEFAULT_TENANT, TenantId
39
- from event_sourcery.exceptions import NoProviderConfigured
40
+ from event_sourcery.exceptions import ConflictingContextComponent, NoProviderConfigured
40
41
 
41
42
  T = TypeVar("T")
42
43
  T_co = TypeVar("T_co", covariant=True)
@@ -107,6 +108,10 @@ class _Container:
107
108
  return new
108
109
 
109
110
 
111
+ class ContextEnrichers(tuple[EventContextEnricher, ...]):
112
+ pass
113
+
114
+
110
115
  class Backend(_Container):
111
116
  """
112
117
  Dependency Injection container for Event Sourcery components.
@@ -140,9 +145,11 @@ class Backend(_Container):
140
145
  self[StorageStrategy] = not_configured(
141
146
  "Use one of pyES backends: SQLAlchemy, Django or KurrentDB",
142
147
  )
148
+ self[ContextEnrichers] = ContextEnrichers()
143
149
  self[EventStore] = lambda c: EventStore(
144
150
  storage_strategy=c[StorageStrategy],
145
151
  serde=c[Serde],
152
+ context_enrichers=c[ContextEnrichers],
146
153
  )
147
154
  self[Outbox] = lambda c: Outbox(
148
155
  strategy=c[OutboxStorageStrategy],
@@ -206,6 +213,16 @@ class Backend(_Container):
206
213
  )
207
214
  return self
208
215
 
216
+ def with_context_enrichers(self, *enrichers: EventContextEnricher) -> Self:
217
+ names = set[str]()
218
+ for enricher in enrichers:
219
+ name = enricher.component_type.__context_name__
220
+ if name in names:
221
+ raise ConflictingContextComponent(name)
222
+ names.add(name)
223
+ self[ContextEnrichers] = ContextEnrichers(enrichers)
224
+ return self
225
+
209
226
 
210
227
  class TransactionalBackend(Backend):
211
228
  """
@@ -1,10 +1,11 @@
1
1
  import dataclasses
2
2
  from dataclasses import dataclass
3
3
  from datetime import datetime, timezone
4
- from typing import Any, Generic, TypeAlias, TypeVar
4
+ from typing import Any, ClassVar, Generic, TypeAlias, TypeVar, cast
5
5
  from uuid import UUID, uuid4
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, Field, PrivateAttr
8
+ from typing_extensions import Self
8
9
 
9
10
  from event_sourcery._event_store.stream_id import StreamId
10
11
  from event_sourcery._event_store.tenant_id import DEFAULT_TENANT, TenantId
@@ -76,23 +77,66 @@ TEvent = TypeVar("TEvent", bound=Event)
76
77
  TContext = TypeVar("TContext", bound=BaseModel)
77
78
 
78
79
 
79
- class Context(BaseModel, extra="allow"):
80
- """
81
- Extensible context object for event metadata.
80
+ class ContextComponent(BaseModel):
81
+ """A typed, independently owned part of an event context."""
82
82
 
83
- Designed to carry correlation and causation identifiers, and can be extended
84
- with additional metadata fields as needed. This allows for flexible propagation
85
- of context information throughout event processing and handling pipelines.
83
+ __context_name__: ClassVar[str]
84
+
85
+
86
+ TContextComponent = TypeVar("TContextComponent", bound=ContextComponent)
87
+
88
+
89
+ class EventCorrelation(ContextComponent):
90
+ """Typed view of the root correlation and causation fields."""
91
+
92
+ correlation_id: UUID | None = None
93
+ causation_id: UUID | None = None
86
94
 
87
- Attributes:
88
- correlation_id (UUID | None): Identifier for correlating related events.
89
- causation_id (UUID | None): Identifier for the cause of the event.
90
95
 
91
- Additional fields can be added dynamically due to `extra="allow"`.
96
+ class Context(BaseModel, extra="forbid"):
97
+ """Event metadata with fixed fields and independently typed components.
98
+
99
+ New writes may contain root correlation fields and registered component payloads.
100
+ Extra root fields in historical events are retained by the deserialization path
101
+ for backwards-compatible reads.
92
102
  """
93
103
 
94
104
  correlation_id: UUID | None = None
95
105
  causation_id: UUID | None = None
106
+ components: dict[str, dict[str, Any]] = Field(default_factory=dict)
107
+ _legacy_fields: dict[str, Any] = PrivateAttr(default_factory=dict)
108
+
109
+ def get(self, component_type: type[TContextComponent]) -> TContextComponent | None:
110
+ """Return a component validated as ``component_type`` when present."""
111
+ if component_type is EventCorrelation:
112
+ return cast(
113
+ TContextComponent,
114
+ EventCorrelation(
115
+ correlation_id=self.correlation_id,
116
+ causation_id=self.causation_id,
117
+ ),
118
+ )
119
+ payload = self.components.get(component_type.__context_name__)
120
+ return component_type.model_validate(payload) if payload is not None else None
121
+
122
+ def with_component(self, component: ContextComponent) -> Self:
123
+ """Return a copy with ``component`` added without mutating this context."""
124
+ if isinstance(component, EventCorrelation):
125
+ return self.model_copy(
126
+ update={
127
+ "correlation_id": component.correlation_id,
128
+ "causation_id": component.causation_id,
129
+ }
130
+ )
131
+
132
+ return self.model_copy(
133
+ update={
134
+ "components": {
135
+ **self.components,
136
+ component.__context_name__: component.model_dump(mode="json"),
137
+ }
138
+ }
139
+ )
96
140
 
97
141
 
98
142
  @dataclasses.dataclass()
@@ -139,7 +183,9 @@ class WrappedEvent(Generic[TEvent]):
139
183
  )
140
184
 
141
185
  def get_context(self, context_type: type[TContext]) -> TContext:
142
- return context_type.model_validate(self.context.model_dump())
186
+ exclude = set() if "components" in context_type.model_fields else {"components"}
187
+ context = self.context.model_dump(exclude=exclude)
188
+ return context_type.model_validate({**context, **self.context._legacy_fields})
143
189
 
144
190
 
145
191
  @dataclasses.dataclass(frozen=True)
@@ -1,6 +1,6 @@
1
1
  import dataclasses
2
- from collections.abc import Mapping, Sequence
3
- from typing import cast
2
+ from collections.abc import Mapping
3
+ from typing import Any, cast
4
4
 
5
5
  from event_sourcery._event_store.event.dto import (
6
6
  Context,
@@ -23,13 +23,31 @@ class Serde:
23
23
  self.registry = registry
24
24
  self.encryption = encryption
25
25
 
26
+ def _deserialize_context(self, context: Mapping[str, Any]) -> Context:
27
+ field_names = {"correlation_id", "causation_id", "components"}
28
+ restored = Context(
29
+ correlation_id=context.get("correlation_id"),
30
+ causation_id=context.get("causation_id"),
31
+ components=context.get("components", {}),
32
+ )
33
+ restored._legacy_fields = {
34
+ name: value for name, value in context.items() if name not in field_names
35
+ }
36
+ return restored
37
+
38
+ def _serialize_context(self, context: Context) -> dict[str, Any]:
39
+ stored = context.model_dump(mode="json")
40
+ if not context.components:
41
+ del stored["components"]
42
+ return {**stored, **context._legacy_fields}
43
+
26
44
  def deserialize(self, event: RawEvent) -> WrappedEvent:
27
45
  event_as_dict = dataclasses.asdict(event)
28
46
  del event_as_dict["stream_id"]
29
47
  del event_as_dict["name"]
30
48
  data = cast(Mapping, event_as_dict.pop("data"))
31
49
  context = event_as_dict.pop("context", {})
32
- event_as_dict["context"] = Context(**context)
50
+ event_as_dict["context"] = self._deserialize_context(context)
33
51
  event_type = self.registry.type_for_name(event.name)
34
52
 
35
53
  processed_data = self.encryption.decrypt(
@@ -43,9 +61,6 @@ class Serde:
43
61
  event=event_type(**processed_data),
44
62
  )
45
63
 
46
- def deserialize_many(self, events: Sequence[RawEvent]) -> list[WrappedEvent]:
47
- return [self.deserialize(event) for event in events]
48
-
49
64
  def deserialize_record(self, record: RecordedRaw) -> Recorded:
50
65
  return Recorded(
51
66
  wrapped_event=self.deserialize(record.entry),
@@ -66,10 +81,5 @@ class Serde:
66
81
  version=event.version,
67
82
  name=self.registry.name_for_type(type(event.event)),
68
83
  data=self.encryption.encrypt(event.event, stream_id),
69
- context=event.context.model_dump(mode="json"),
84
+ context=self._serialize_context(event.context),
70
85
  )
71
-
72
- def serialize_many(
73
- self, events: Sequence[WrappedEvent], stream_id: StreamId
74
- ) -> list[RawEvent]:
75
- return [self.serialize(event, stream_id) for event in events]