python-event-sourcery 0.5.3a2__tar.gz → 0.5.3a4__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.3a4/.adr-dir +1 -0
- python_event_sourcery-0.5.3a4/.github/workflows/bump-version-and-publish.yml +35 -0
- python_event_sourcery-0.5.3a4/.github/workflows/deploy-docs.yml +32 -0
- python_event_sourcery-0.5.3a4/.github/workflows/main.yml +58 -0
- python_event_sourcery-0.5.3a4/.github/workflows/publish-release.yml +24 -0
- python_event_sourcery-0.5.3a4/.gitignore +129 -0
- python_event_sourcery-0.5.3a4/AGENTS.md +45 -0
- python_event_sourcery-0.5.3a4/CHANGELOG.md +25 -0
- python_event_sourcery-0.5.3a4/CONTRIBUTING.md +40 -0
- python_event_sourcery-0.5.3a4/Makefile +20 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/PKG-INFO +11 -18
- python_event_sourcery-0.5.3a4/django_manage.py +23 -0
- python_event_sourcery-0.5.3a4/docker-compose.yml +26 -0
- python_event_sourcery-0.5.3a4/docs/adr/20220730-record-architecture-decisions.md +19 -0
- python_event_sourcery-0.5.3a4/docs/adr/20230903-explicit-init-arguments-for-sqlalchemy-s-models.md +129 -0
- python_event_sourcery-0.5.3a4/docs/adr/20231027_1-packaging-of-the-project.md +45 -0
- python_event_sourcery-0.5.3a4/docs/adr/20231027_2-leave-implementation-of-synchronous-dispatcher-to-users-of-the-library.md +34 -0
- python_event_sourcery-0.5.3a4/docs/adr/20231029-in-memory-event-store.md +23 -0
- python_event_sourcery-0.5.3a4/docs/adr/20231230-in_transaction_subscription.md +26 -0
- python_event_sourcery-0.5.3a4/docs/adr/20240726-dependencies-and-distribution.md +60 -0
- python_event_sourcery-0.5.3a4/docs/adr/20241007-multitenancy.md +86 -0
- python_event_sourcery-0.5.3a4/docs/adr/20250411-packaging-of-the-project-revised.md +21 -0
- python_event_sourcery-0.5.3a4/docs/adr/20250729-privacy-data-encryption-and-shredding.md +103 -0
- python_event_sourcery-0.5.3a4/docs/adr/20251004-packaging-of-the-event_store-package.md +17 -0
- python_event_sourcery-0.5.3a4/docs/adr/20260314-aggregate-metadata.md +143 -0
- python_event_sourcery-0.5.3a4/docs/code/recipes/__init__.py +1 -0
- python_event_sourcery-0.5.3a4/docs/code/recipes/integrate/kurrent_01.py +8 -0
- python_event_sourcery-0.5.3a4/docs/code/test_get_started.py +25 -0
- python_event_sourcery-0.5.3a4/docs/code/test_recipes.py +440 -0
- python_event_sourcery-0.5.3a4/docs/concepts/basics.md +278 -0
- python_event_sourcery-0.5.3a4/docs/get_started.md +41 -0
- python_event_sourcery-0.5.3a4/docs/index.md +3 -0
- python_event_sourcery-0.5.3a4/docs/recipes/data_privacy.md +149 -0
- python_event_sourcery-0.5.3a4/docs/recipes/defining_events.md +11 -0
- python_event_sourcery-0.5.3a4/docs/recipes/event_sourcing.md +49 -0
- python_event_sourcery-0.5.3a4/docs/recipes/integrate.md +69 -0
- python_event_sourcery-0.5.3a4/docs/recipes/multitenancy.md +75 -0
- python_event_sourcery-0.5.3a4/docs/recipes/outbox.md +112 -0
- python_event_sourcery-0.5.3a4/docs/recipes/saving_events.md +77 -0
- python_event_sourcery-0.5.3a4/docs/recipes/snapshots.md +43 -0
- python_event_sourcery-0.5.3a4/docs/recipes/subscriptions.md +59 -0
- python_event_sourcery-0.5.3a4/docs/reference/backends/django.md +5 -0
- python_event_sourcery-0.5.3a4/docs/reference/backends/in_memory.md +8 -0
- python_event_sourcery-0.5.3a4/docs/reference/backends/kurrentdb.md +5 -0
- python_event_sourcery-0.5.3a4/docs/reference/backends/sqlalchemy.md +26 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_sourcing/Aggregate.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_sourcing/Repository.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_sourcing/WrappedAggregate.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/DEFAULT_TENANT.md +3 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/EventStore.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/NO_VERSIONING.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/StreamCategory.md +7 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/StreamId.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/StreamUUID.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/TenantId.md +4 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/backend/Backend.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/backend/TransactionalBackend.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/backend/not_configured.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/backend/singleton.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/encryption/DataSubject.md +7 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/encryption/Encrypted.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/encryption/Encryption.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/encryption/NoEncryptionStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/encryption/NoKeyStorageStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/Context.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/Entry.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/Event.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/Position.md +5 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/RawEvent.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/Recorded.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/RecordedRaw.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/event/WrappedEvent.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/exceptions.md +36 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/in_transaction/Dispatcher.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/in_transaction/Listener.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/in_transaction/Listeners.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/interfaces/EncryptionKeyStorageStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/interfaces/EncryptionStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/interfaces/OutboxFiltererStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/interfaces/OutboxStorageStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/interfaces/StorageStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/interfaces/SubscriptionStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/outbox/NoOutboxStorageStrategy.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/outbox/Outbox.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/outbox/no_filter.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/subscription/BuildPhase.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/subscription/FilterPhase.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/subscription/PositionPhase.md +1 -0
- python_event_sourcery-0.5.3a4/docs/reference/event_store/subscription/SubscriptionBuilder.md +1 -0
- python_event_sourcery-0.5.3a4/docs/res/eda_example.plantuml +13 -0
- python_event_sourcery-0.5.3a4/docs/res/eda_example.png +0 -0
- python_event_sourcery-0.5.3a4/event_sourcery_kurrentdb/py.typed +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/cursors_dao.py +3 -2
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/event_store.py +3 -2
- python_event_sourcery-0.5.3a4/event_sourcery_sqlalchemy/py.typed +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/pyproject.toml +44 -41
- python_event_sourcery-0.5.3a4/tests/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/backend/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/backend/django.py +34 -0
- python_event_sourcery-0.5.3a4/tests/backend/in_memory.py +8 -0
- python_event_sourcery-0.5.3a4/tests/backend/kurrentdb.py +28 -0
- python_event_sourcery-0.5.3a4/tests/backend/sqlalchemy.py +94 -0
- python_event_sourcery-0.5.3a4/tests/backend/test_sqlalchemy.py +88 -0
- python_event_sourcery-0.5.3a4/tests/bdd.py +309 -0
- python_event_sourcery-0.5.3a4/tests/conftest.py +47 -0
- python_event_sourcery-0.5.3a4/tests/event_sourcing/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/event_sourcing/conftest.py +10 -0
- python_event_sourcery-0.5.3a4/tests/event_sourcing/light_switch.py +45 -0
- python_event_sourcery-0.5.3a4/tests/event_sourcing/test_aggregate.py +101 -0
- python_event_sourcery-0.5.3a4/tests/event_sourcing/test_wrapped_aggregate.py +153 -0
- python_event_sourcery-0.5.3a4/tests/event_store/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/event_store/conftest.py +63 -0
- python_event_sourcery-0.5.3a4/tests/event_store/event/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/event_store/event/test_dto.py +30 -0
- python_event_sourcery-0.5.3a4/tests/event_store/event/test_privacy.py +312 -0
- python_event_sourcery-0.5.3a4/tests/event_store/event/test_registry.py +87 -0
- python_event_sourcery-0.5.3a4/tests/event_store/outbox/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/event_store/outbox/conftest.py +83 -0
- python_event_sourcery-0.5.3a4/tests/event_store/outbox/test_filters.py +31 -0
- python_event_sourcery-0.5.3a4/tests/event_store/outbox/test_multitenancy.py +32 -0
- python_event_sourcery-0.5.3a4/tests/event_store/outbox/test_outbox.py +75 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/other_client.py +128 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/test_batch.py +68 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/test_from_position.py +77 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/test_handling_uncommitted_messages_from_other_clients.py +240 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/test_in_transaction_subscription.py +185 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/test_subscriptions.py +74 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/test_to_category.py +106 -0
- python_event_sourcery-0.5.3a4/tests/event_store/subscriptions/test_to_event_types.py +107 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_category.py +65 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_concurrency_control.py +42 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_multitenancy.py +44 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_named_streams.py +66 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_no_outbox.py +11 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_no_versioning.py +62 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_removing_stream.py +15 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_save_retrieval.py +85 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_snapshots.py +76 -0
- python_event_sourcery-0.5.3a4/tests/event_store/test_stream_id.py +105 -0
- python_event_sourcery-0.5.3a4/tests/factories.py +45 -0
- python_event_sourcery-0.5.3a4/tests/mark.py +33 -0
- python_event_sourcery-0.5.3a4/tests/matchers.py +42 -0
- python_event_sourcery-0.5.3a4/tests/read_model/__init__.py +0 -0
- python_event_sourcery-0.5.3a4/tests/read_model/test_projector.py +252 -0
- python_event_sourcery-0.5.3a4/uv.lock +1675 -0
- python_event_sourcery-0.5.3a4/zensical.toml +126 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/LICENSE +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/README.md +0 -0
- {python_event_sourcery-0.5.3a2/event_sourcery/_event_store → python_event_sourcery-0.5.3a4/docs/code}/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2/event_sourcery/_event_store/event → python_event_sourcery-0.5.3a4/docs/code/recipes/integrate}/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2/event_sourcery/_event_store/subscription → python_event_sourcery-0.5.3a4/event_sourcery/_event_store}/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/backend.py +0 -0
- {python_event_sourcery-0.5.3a2/event_sourcery_django/migrations → python_event_sourcery-0.5.3a4/event_sourcery/_event_store/event}/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/event/dto.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/event/encryption.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/event/registry.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/event/serde.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/event_store.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/in_memory.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/outbox.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/stream_id.py +0 -0
- /python_event_sourcery-0.5.3a2/event_sourcery/py.typed → /python_event_sourcery-0.5.3a4/event_sourcery/_event_store/subscription/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/subscription/builder.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/subscription/in_transaction.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/subscription/interfaces.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/tenant_id.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/_event_store/versioning.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/backend.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/encryption.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/event.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/event_sourcing/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/event_sourcing/aggregate.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/event_sourcing/repository.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/exceptions.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/in_transaction.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/interfaces.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/outbox.py +0 -0
- {python_event_sourcery-0.5.3a2/event_sourcery_django → python_event_sourcery-0.5.3a4/event_sourcery}/py.typed +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/read_model/cursors_dao.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/read_model/projector.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery/subscription.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_django/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_django/dto.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_django/event_store.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_django/migrations/0001_initial.py +0 -0
- /python_event_sourcery-0.5.3a2/event_sourcery_kurrentdb/py.typed → /python_event_sourcery-0.5.3a4/event_sourcery_django/migrations/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_django/models.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_django/outbox.py +0 -0
- {python_event_sourcery-0.5.3a2/event_sourcery_sqlalchemy → python_event_sourcery-0.5.3a4/event_sourcery_django}/py.typed +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_django/subscription.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_kurrentdb/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_kurrentdb/dto.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_kurrentdb/event_store.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_kurrentdb/outbox.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_kurrentdb/stream.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_kurrentdb/subscription.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/dto.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/models/__init__.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/models/base.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/models/default.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/outbox.py +0 -0
- {python_event_sourcery-0.5.3a2 → python_event_sourcery-0.5.3a4}/event_sourcery_sqlalchemy/subscription.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
docs/adr
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Bump version and publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
PYTHON_VERSION: "3.14"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
bump-and-publish:
|
|
12
|
+
if: github.event.head_commit.message != 'Autobump version'
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v2
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v7
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
22
|
+
enable-cache: true
|
|
23
|
+
- name: Bump version
|
|
24
|
+
run: uv version --bump alpha
|
|
25
|
+
- uses: EndBug/add-and-commit@v9
|
|
26
|
+
with:
|
|
27
|
+
add: "pyproject.toml uv.lock"
|
|
28
|
+
author_name: Python Event Sourcery Bot
|
|
29
|
+
message: 'Autobump version'
|
|
30
|
+
- name: Publish prerelease
|
|
31
|
+
env:
|
|
32
|
+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
33
|
+
run: |
|
|
34
|
+
uv build
|
|
35
|
+
uv publish --token $PYPI_TOKEN
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Deploy docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
PYTHON_VERSION: "3.14"
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
deploy:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v2
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v7
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
23
|
+
enable-cache: true
|
|
24
|
+
- name: Install deps
|
|
25
|
+
run: uv sync --all-extras
|
|
26
|
+
- name: Build docs
|
|
27
|
+
run: uv run --all-extras zensical build --clean
|
|
28
|
+
- name: Deploy to GitHub Pages
|
|
29
|
+
uses: peaceiris/actions-gh-pages@v4
|
|
30
|
+
with:
|
|
31
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
publish_dir: ./site
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Lint & tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
|
|
16
|
+
|
|
17
|
+
services:
|
|
18
|
+
postgres:
|
|
19
|
+
image: postgres:14
|
|
20
|
+
env:
|
|
21
|
+
POSTGRES_USER: es
|
|
22
|
+
POSTGRES_PASSWORD: es
|
|
23
|
+
options: >-
|
|
24
|
+
--health-cmd pg_isready
|
|
25
|
+
--health-interval 10s
|
|
26
|
+
--health-timeout 5s
|
|
27
|
+
--health-retries 5
|
|
28
|
+
ports:
|
|
29
|
+
- 5432:5432
|
|
30
|
+
|
|
31
|
+
eventstoredb:
|
|
32
|
+
image: eventstore/eventstore:latest
|
|
33
|
+
env:
|
|
34
|
+
EVENTSTORE_MEM_DB: True
|
|
35
|
+
EVENTSTORE_CLUSTER_SIZE: 1
|
|
36
|
+
EVENTSTORE_RUN_PROJECTIONS: All
|
|
37
|
+
EVENTSTORE_START_STANDARD_PROJECTIONS: true
|
|
38
|
+
EVENTSTORE_INSECURE: true
|
|
39
|
+
EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP: true
|
|
40
|
+
ports:
|
|
41
|
+
- 1113:1113
|
|
42
|
+
- 2113:2113
|
|
43
|
+
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v2
|
|
46
|
+
- name: Install uv
|
|
47
|
+
uses: astral-sh/setup-uv@v7
|
|
48
|
+
with:
|
|
49
|
+
python-version: ${{ matrix.python-version }}
|
|
50
|
+
enable-cache: true
|
|
51
|
+
- name: Install deps
|
|
52
|
+
run: uv sync --all-extras
|
|
53
|
+
- name: Run linters
|
|
54
|
+
run: make lint
|
|
55
|
+
- name: Build docs
|
|
56
|
+
run: uv run --all-extras zensical build --clean
|
|
57
|
+
- name: Test with pytest
|
|
58
|
+
run: make test
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish on Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
PYTHON_VERSION: "3.14"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v2
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v7
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
19
|
+
- name: Publish to PyPI
|
|
20
|
+
env:
|
|
21
|
+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
22
|
+
run: |
|
|
23
|
+
uv build
|
|
24
|
+
uv publish --token $PYPI_TOKEN
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
target/
|
|
76
|
+
|
|
77
|
+
# Jupyter Notebook
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
|
|
80
|
+
# IPython
|
|
81
|
+
profile_default/
|
|
82
|
+
ipython_config.py
|
|
83
|
+
|
|
84
|
+
# pyenv
|
|
85
|
+
.python-version
|
|
86
|
+
|
|
87
|
+
# pipenv
|
|
88
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
89
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
90
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
91
|
+
# install all needed dependencies.
|
|
92
|
+
#Pipfile.lock
|
|
93
|
+
|
|
94
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
95
|
+
__pypackages__/
|
|
96
|
+
|
|
97
|
+
# Celery stuff
|
|
98
|
+
celerybeat-schedule
|
|
99
|
+
celerybeat.pid
|
|
100
|
+
|
|
101
|
+
# SageMath parsed files
|
|
102
|
+
*.sage.py
|
|
103
|
+
|
|
104
|
+
# Environments
|
|
105
|
+
.env
|
|
106
|
+
.venv
|
|
107
|
+
env/
|
|
108
|
+
venv/
|
|
109
|
+
ENV/
|
|
110
|
+
env.bak/
|
|
111
|
+
venv.bak/
|
|
112
|
+
|
|
113
|
+
# Spyder project settings
|
|
114
|
+
.spyderproject
|
|
115
|
+
.spyproject
|
|
116
|
+
|
|
117
|
+
# Rope project settings
|
|
118
|
+
.ropeproject
|
|
119
|
+
|
|
120
|
+
# mkdocs documentation
|
|
121
|
+
/site
|
|
122
|
+
|
|
123
|
+
# mypy
|
|
124
|
+
.mypy_cache/
|
|
125
|
+
.dmypy.json
|
|
126
|
+
dmypy.json
|
|
127
|
+
|
|
128
|
+
# Pyre type checker
|
|
129
|
+
.pyre/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Event sourcing library for Python with pluggable backends.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
- `event_sourcery/` — core library
|
|
8
|
+
- `event_sourcery_sqlalchemy/`, `event_sourcery_django/`, `event_sourcery_kurrentdb/` — backends (optional extras: `sqlalchemy`, `django`, `kurrentdb`)
|
|
9
|
+
- `tests/` — shared test suite run against every backend
|
|
10
|
+
- `docs/` — documentation (zensical)
|
|
11
|
+
|
|
12
|
+
## Tooling
|
|
13
|
+
|
|
14
|
+
Package manager: **uv** (PEP 621 `pyproject.toml`, `uv.lock` committed).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync --all-extras # install all deps (dev group is included by default)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
docker compose up -d # required before tests (postgres, kurrentdb, rabbitmq)
|
|
24
|
+
make test # pytest with coverage (fail_under = 100)
|
|
25
|
+
make lint # ruff format + ruff check + mypy (must pass clean)
|
|
26
|
+
make docs-serve # docs live preview
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run single tools via `uv run --all-extras <tool>`, e.g. `uv run --all-extras pytest tests/event_store -k django`.
|
|
30
|
+
|
|
31
|
+
## Conventions
|
|
32
|
+
|
|
33
|
+
- Python >= 3.10, strict mypy (`disallow_untyped_defs`), ruff line-length 88.
|
|
34
|
+
- New backend functionality goes through the shared test suite in `tests/`; use `not_implemented` / `skip_backend` markers for unsupported features.
|
|
35
|
+
- Version bumps: `uv version --bump alpha` (CI autopublishes prereleases on push to main).
|
|
36
|
+
|
|
37
|
+
## Docs
|
|
38
|
+
|
|
39
|
+
Changing public API or behavior may require updating `docs/` — pages embed runnable snippets from `docs/code/` (via `--8<--` includes). After touching docs code:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run --all-extras pytest docs/code # snippets are real tests, must pass
|
|
43
|
+
make lint-fix-docs # lint docs/code
|
|
44
|
+
uv run --all-extras zensical build --clean # verify docs build
|
|
45
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.2
|
|
4
|
+
### Changed
|
|
5
|
+
- **Breaking:** `Repository.aggregate()` yields `WrappedAggregate` instead of `Aggregate` directly
|
|
6
|
+
|
|
7
|
+
## 0.5.1
|
|
8
|
+
### Added
|
|
9
|
+
- In transaction subscription to category
|
|
10
|
+
- Custom context support in `Repository.aggregate()` via optional `context` parameter
|
|
11
|
+
- `WrappedEvent.get_context()` method for typed context retrieval
|
|
12
|
+
### Fixes
|
|
13
|
+
- event_sourcery_sqlalchemy.configure_models annotation for sqlalchemy.orm.DeclarativeBase
|
|
14
|
+
|
|
15
|
+
## 0.5.0
|
|
16
|
+
### Added
|
|
17
|
+
- Event definitions via the `Event` base class (Pydantic) with built-in metadata and tracing IDs.
|
|
18
|
+
- Event Store API for appending/loading streams with optimistic concurrency and optional `NO_VERSIONING`.
|
|
19
|
+
- Stream snapshots to optimize long event streams.
|
|
20
|
+
- Outbox pattern with retries and optional filters, plus synchronous in-process subscriptions.
|
|
21
|
+
- Subscription builder with single-event and batch iterators for asynchronous processing.
|
|
22
|
+
- Multi-tenancy with tenant-scoped stores and explicit visibility rules.
|
|
23
|
+
- Crypto-shredding and field-level encryption via `Encrypted` and `DataSubject` markers.
|
|
24
|
+
- Event sourcing primitives: `Aggregate` and `Repository` base classes.
|
|
25
|
+
- Backend integrations for SQLAlchemy, Django, KurrentDB (EventStoreDB), and in-memory storage.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Basics
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- docker + docker compose / any compatible replacement
|
|
6
|
+
- uv
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv sync --all-extras
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Running tests
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
docker compose up
|
|
18
|
+
make test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Running linters
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
make lint
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
# Documentation
|
|
28
|
+
|
|
29
|
+
First, `cd docs/documentation`
|
|
30
|
+
|
|
31
|
+
## Serve docs with live preview
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
zensical serve
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Deploy docs to github pages
|
|
38
|
+
|
|
39
|
+
Docs get auto deployed from main after each change.
|
|
40
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
SRC_DIRS ?= ${wildcard event_sourcery*}
|
|
2
|
+
|
|
3
|
+
.PHONY: lint
|
|
4
|
+
lint:
|
|
5
|
+
uv run --all-extras ruff format $(SRC_DIRS) tests/
|
|
6
|
+
uv run --all-extras ruff check $(SRC_DIRS) tests/ --fix
|
|
7
|
+
uv run --all-extras mypy $(SRC_DIRS) tests/
|
|
8
|
+
|
|
9
|
+
.PHONY: test
|
|
10
|
+
test:
|
|
11
|
+
uv run --all-extras pytest $(addprefix --cov ,$(SRC_DIRS)) tests/
|
|
12
|
+
|
|
13
|
+
.PHONY: docs-serve
|
|
14
|
+
docs-serve:
|
|
15
|
+
uv run --all-extras zensical serve
|
|
16
|
+
|
|
17
|
+
.PHONY: lint-fix-docs
|
|
18
|
+
lint-fix-docs:
|
|
19
|
+
uv run --all-extras ruff format docs/code tests/
|
|
20
|
+
uv run --all-extras ruff check docs/code --fix
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-event-sourcery
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3a4
|
|
4
4
|
Summary: A library for event-based systems in Python.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Author-email: Sebastian Buczyński <nnplaya@gmail.com>, Łukasz Żukowski <lukasz.zukowski@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Dist: more-itertools
|
|
10
|
+
Requires-Dist: psycopg2-binary<3,>2
|
|
11
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
15
12
|
Provides-Extra: django
|
|
13
|
+
Requires-Dist: django<6,>=4; extra == 'django'
|
|
16
14
|
Provides-Extra: kurrentdb
|
|
15
|
+
Requires-Dist: kurrentdbclient>=1.0.6; extra == 'kurrentdb'
|
|
17
16
|
Provides-Extra: sqlalchemy
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist: django (>=4,<6) ; extra == "django"
|
|
20
|
-
Requires-Dist: kurrentdbclient (>=1.0.6) ; extra == "kurrentdb"
|
|
21
|
-
Requires-Dist: more-itertools
|
|
22
|
-
Requires-Dist: psycopg2-binary (>2,<3)
|
|
23
|
-
Requires-Dist: pydantic (>=2.0,<3)
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
|
|
24
18
|
Description-Content-Type: text/markdown
|
|
25
19
|
|
|
26
20
|
|
|
@@ -149,4 +143,3 @@ Anything missing? Create an issue in this repository. Let others upvote
|
|
|
149
143
|
## Inspirations
|
|
150
144
|
- [Rails Event Store](https://railseventstore.org/)
|
|
151
145
|
- [Marten](https://martendb.io/)
|
|
152
|
-
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Django's command-line utility for administrative tasks."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
"""Run administrative tasks."""
|
|
9
|
+
from tests.backend import django as django_backend
|
|
10
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", django_backend.__name__)
|
|
11
|
+
try:
|
|
12
|
+
from django.core.management import execute_from_command_line
|
|
13
|
+
except ImportError as exc:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"Couldn't import Django. Are you sure it's installed and "
|
|
16
|
+
"available on your PYTHONPATH environment variable? Did you "
|
|
17
|
+
"forget to activate a virtual environment?"
|
|
18
|
+
) from exc
|
|
19
|
+
execute_from_command_line(sys.argv)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:14
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_USER: es
|
|
6
|
+
POSTGRES_PASSWORD: es
|
|
7
|
+
ports:
|
|
8
|
+
- "5432:5432"
|
|
9
|
+
|
|
10
|
+
rabbitmq:
|
|
11
|
+
image: rabbitmq:3.11.2-management
|
|
12
|
+
ports:
|
|
13
|
+
- '5672:5672'
|
|
14
|
+
- '15672:15672'
|
|
15
|
+
|
|
16
|
+
kurrentdb:
|
|
17
|
+
image: kurrentplatform/kurrentdb
|
|
18
|
+
environment:
|
|
19
|
+
- KURRENTDB_CLUSTER_SIZE=1
|
|
20
|
+
- KURRENTDB_RUN_PROJECTIONS=All
|
|
21
|
+
- KURRENTDB_START_STANDARD_PROJECTIONS=true
|
|
22
|
+
- KURRENTDB_INSECURE=true
|
|
23
|
+
- KURRENTDB_ENABLE_ATOM_PUB_OVER_HTTP=true
|
|
24
|
+
- KURRENTDB_NODE_PORT=2113
|
|
25
|
+
ports:
|
|
26
|
+
- "2113:2113"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Record architecture decisions
|
|
2
|
+
|
|
3
|
+
Date: 2022-07-30
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
We need to record the architectural decisions made on this project.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
|
|
19
|
+
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
|
python_event_sourcery-0.5.3a4/docs/adr/20230903-explicit-init-arguments-for-sqlalchemy-s-models.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Explicit __init__ arguments for SQLAlchemy's models
|
|
2
|
+
|
|
3
|
+
Date: 2023-09-03
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Draft
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
We aim for having full support regarding
|
|
12
|
+
|
|
13
|
+
## Possible solutions
|
|
14
|
+
|
|
15
|
+
### Use native support for dataclasses in SQLAlchemy
|
|
16
|
+
|
|
17
|
+
Starting from SQLAlchemy 2.0, there's a [native support for dataclasses](https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#native-support-for-dataclasses-mapped-as-orm-models).
|
|
18
|
+
|
|
19
|
+
However, it's less seamless than one might initially imagine. It comes down to inheriting explicitly from another base class, namely `sqlalchemy.orm.MappedAsDataclass` and using it as a base for models:
|
|
20
|
+
```python
|
|
21
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
22
|
+
from sqlalchemy.orm import MappedAsDataclass
|
|
23
|
+
|
|
24
|
+
class Base(MappedAsDataclass, DeclarativeBase):
|
|
25
|
+
"""subclasses will be converted to dataclasses"""
|
|
26
|
+
|
|
27
|
+
class User(Base):
|
|
28
|
+
__tablename__ = "user_account"
|
|
29
|
+
|
|
30
|
+
id: Mapped[intpk] = mapped_column(init=False)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
As a result, `User` becomes a dataclass with attached SQLAlchemy's behaviour.
|
|
34
|
+
|
|
35
|
+
In terms of risk, this may cause compatibility issues. As a reminder, Event Sourcery's models are declared as bare classes (notice lack of inheritance from `Base`):
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
class Stream:
|
|
39
|
+
__tablename__ = "event_sourcery_streams"
|
|
40
|
+
__table_args__ = (
|
|
41
|
+
UniqueConstraint("uuid", "category"),
|
|
42
|
+
UniqueConstraint("name", "category"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
id = mapped_column(BigInteger().with_variant(Integer(), "sqlite"), primary_key=True)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The assumption is that someone setting up a project with SQLAlchemy will have their own `Base` class and models. The library will let them attach our models to their declarative base later using `event_sourcery_sqlalchemy.models.configure_models`, like:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
@as_declarative()
|
|
52
|
+
class Base:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# initialize Event Sourcery models, so they can be handled by SQLAlchemy and e.g. alembic
|
|
57
|
+
configure_models(Base)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The exception is raised in case `MappedAsDataclass` appears multiple time in model's MRO, for example:
|
|
61
|
+
- let's say we have a base class for our models that inherits from `MappedAsDataclass`
|
|
62
|
+
- someone has their own `Base` that also inherits from `MappedAsDataclass`
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
sqlalchemy.exc.InvalidRequestError: Class <class 'event_sourcery_sqlalchemy.models.Stream'> is already a dataclass; ensure that base classes / decorator styles of establishing dataclasses are not being mixed. This can happen if a class that inherits from 'MappedAsDataclass', even indirectly, is been mapped with '@registry.mapped_as_dataclass'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To reproduce, add `MappedAsDataclass` as a base to any model and use this snippet:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from sqlalchemy import create_engine
|
|
72
|
+
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
|
|
73
|
+
|
|
74
|
+
from event_sourcery_sqlalchemy.models import configure_models
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
engine = create_engine(
|
|
78
|
+
"sqlite+pysqlite:///:memory:", echo=True, future=True
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Base(MappedAsDataclass, DeclarativeBase):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
Base.metadata.create_all(bind=engine)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# initialize Event Sourcery models, so they can be handled by SQLAlchemy and e.g. alembic
|
|
90
|
+
configure_models(Base)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
### Create base class for models with overridden __init__
|
|
95
|
+
|
|
96
|
+
There is a recipe for providing base class model with overriding `__init__`:
|
|
97
|
+
```python
|
|
98
|
+
class ModelBase:
|
|
99
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
100
|
+
return super().__init__(*args, **kwargs)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This basically makes mypy unable to verify anything extra because such a signature allows everything to be passed in. Conversely, all operations are considered to be correct from type checker's perspective.
|
|
104
|
+
|
|
105
|
+
This solution is a bit tricky and just gets rid of warnings, but doesn't give us any additional benefits.
|
|
106
|
+
|
|
107
|
+
### Define our own, explicit `__init__` definition
|
|
108
|
+
|
|
109
|
+
We can simply provide tailored `__init__` for each model to get type safety out of the box.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
class Stream:
|
|
113
|
+
def __init__(self) -> None:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This is safe & compatible with other ORM's features because SQLAlchemy uses other means to build an object when e.g. fetching them from the database.
|
|
120
|
+
In other words, SQLAlchemy internally is not invoking `__init__`.
|
|
121
|
+
|
|
122
|
+
## Decision
|
|
123
|
+
|
|
124
|
+
Writing custom `__init__` makes the most sense.
|
|
125
|
+
|
|
126
|
+
## Consequences
|
|
127
|
+
|
|
128
|
+
Whenever someone changes fields of a model (this should be rare after release), `__init__` needs to follow.
|
|
129
|
+
However, this inconvenience can be potentially automated away in the future. At least, we could also have a custom static code check that would guard that.
|