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.
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/AGENTS.md +4 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/CHANGELOG.md +9 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/PKG-INFO +3 -1
- python_event_sourcery-0.6.0/docs/adr/20260926-typed-event-context-components-and-enrichers.md +204 -0
- python_event_sourcery-0.6.0/docs/code/test_context_enrichers.py +40 -0
- python_event_sourcery-0.6.0/docs/code/test_otel.py +38 -0
- python_event_sourcery-0.6.0/docs/recipes/context_enrichment.md +17 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/integrate.md +1 -0
- python_event_sourcery-0.6.0/docs/recipes/opentelemetry.md +25 -0
- python_event_sourcery-0.6.0/docs/reference/event_store/event/ContextComponent.md +1 -0
- python_event_sourcery-0.6.0/docs/reference/event_store/event/EventContextEnricher.md +1 -0
- python_event_sourcery-0.6.0/docs/reference/event_store/event/EventCorrelation.md +1 -0
- python_event_sourcery-0.6.0/docs/reference/otel/OpenTelemetryCarrier.md +1 -0
- python_event_sourcery-0.6.0/docs/reference/otel/OpenTelemetryContextEnricher.md +1 -0
- python_event_sourcery-0.6.0/docs/reference/otel/get_parent_context.md +1 -0
- python_event_sourcery-0.6.0/docs/reference/otel/use_parent_context.md +1 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/backend.py +18 -1
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/dto.py +59 -13
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/serde.py +22 -12
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event_store.py +44 -4
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event.py +6 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/exceptions.py +5 -0
- python_event_sourcery-0.6.0/event_sourcery_otel/__init__.py +12 -0
- python_event_sourcery-0.6.0/event_sourcery_otel/_carrier.py +9 -0
- python_event_sourcery-0.6.0/event_sourcery_otel/_context.py +38 -0
- python_event_sourcery-0.6.0/event_sourcery_otel/_enricher.py +27 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/pyproject.toml +8 -4
- python_event_sourcery-0.6.0/tests/AGENTS.md +95 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/bdd.py +9 -1
- python_event_sourcery-0.6.0/tests/event_store/event/test_dto.py +100 -0
- python_event_sourcery-0.6.0/tests/event_store/test_context_enrichment.py +174 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_save_retrieval.py +28 -14
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/matchers.py +28 -3
- python_event_sourcery-0.6.0/tests/otel/AGENTS.md +23 -0
- python_event_sourcery-0.6.0/tests/otel/bdd.py +67 -0
- python_event_sourcery-0.6.0/tests/otel/conftest.py +15 -0
- python_event_sourcery-0.6.0/tests/otel/test_context_propagation.py +64 -0
- python_event_sourcery-0.6.0/tests/otel/test_parent_context.py +73 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/uv.lock +18 -2
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/zensical.toml +12 -0
- python_event_sourcery-0.5.3a5/tests/event_store/event/test_dto.py +0 -30
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.adr-dir +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/bump-version-and-publish.yml +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/deploy-docs.yml +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/main.yml +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.github/workflows/publish-release.yml +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/.gitignore +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/CONTRIBUTING.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/LICENSE +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/README.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/compose.yml +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/django_manage.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20220730-record-architecture-decisions.md +0 -0
- {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
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20231027_1-packaging-of-the-project.md +0 -0
- {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
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20231029-in-memory-event-store.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20231230-in_transaction_subscription.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20240726-dependencies-and-distribution.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20241007-multitenancy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20250411-packaging-of-the-project-revised.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20250729-privacy-data-encryption-and-shredding.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20251004-packaging-of-the-event_store-package.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/adr/20260314-aggregate-metadata.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/recipes/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/recipes/integrate/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/recipes/integrate/kurrent_01.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/test_get_started.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/code/test_recipes.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/concepts.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/get_started.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/index.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/data_privacy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/defining_events.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/event_sourcing.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/multitenancy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/outbox.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/saving_events.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/snapshots.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/recipes/subscriptions.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/django.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/in_memory.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/kurrentdb.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/backends/sqlalchemy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_sourcing/Aggregate.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_sourcing/Repository.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_sourcing/WrappedAggregate.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/DEFAULT_TENANT.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/EventStore.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/NO_VERSIONING.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/StreamCategory.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/StreamId.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/StreamUUID.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/TenantId.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/Backend.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/TransactionalBackend.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/not_configured.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/backend/singleton.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/DataSubject.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/Encrypted.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/Encryption.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/NoEncryptionStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/encryption/NoKeyStorageStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Context.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Entry.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Event.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Position.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/RawEvent.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/Recorded.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/RecordedRaw.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/event/WrappedEvent.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/exceptions.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/in_transaction/Dispatcher.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/in_transaction/Listener.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/in_transaction/Listeners.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/EncryptionKeyStorageStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/EncryptionStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/OutboxFiltererStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/OutboxStorageStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/StorageStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/interfaces/SubscriptionStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/outbox/NoOutboxStorageStrategy.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/outbox/Outbox.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/outbox/no_filter.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/BuildPhase.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/FilterPhase.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/PositionPhase.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/reference/event_store/subscription/SubscriptionBuilder.md +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/res/eda_example.plantuml +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/docs/res/eda_example.png +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/encryption.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/event/registry.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/in_memory.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/outbox.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/stream_id.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/builder.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/in_transaction.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/subscription/interfaces.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/tenant_id.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/versioning.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/backend.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/encryption.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event_sourcing/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event_sourcing/aggregate.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/event_sourcing/repository.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/in_transaction.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/interfaces.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/outbox.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/py.typed +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/read_model/cursors_dao.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/read_model/projector.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/subscription.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/dto.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/event_store.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/migrations/0001_initial.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/migrations/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/models.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/outbox.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/py.typed +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_django/subscription.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/dto.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/event_store.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/outbox.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/py.typed +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/stream.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_kurrentdb/subscription.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/cursors_dao.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/dto.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/event_store.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/models/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/models/base.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/models/default.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/outbox.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/py.typed +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery_sqlalchemy/subscription.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/django.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/in_memory.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/kurrentdb.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/sqlalchemy.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/backend/test_sqlalchemy.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/conftest.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/conftest.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/light_switch.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/test_aggregate.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_sourcing/test_wrapped_aggregate.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/conftest.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/event/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/event/test_privacy.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/event/test_registry.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/conftest.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/test_filters.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/test_multitenancy.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/outbox/test_outbox.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/__init__.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/other_client.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_batch.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_from_position.py +0 -0
- {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
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_in_transaction_subscription.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_subscriptions.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_to_category.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/subscriptions/test_to_event_types.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_category.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_concurrency_control.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_multitenancy.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_named_streams.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_no_outbox.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_no_versioning.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_removing_stream.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_snapshots.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/event_store/test_stream_id.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/factories.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/mark.py +0 -0
- {python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/tests/read_model/__init__.py +0 -0
- {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.
|
|
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
|
{python_event_sourcery-0.5.3a5 → python_event_sourcery-0.6.0}/event_sourcery/_event_store/backend.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"] =
|
|
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
|
|
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]
|