neuroglia-python 0.6.11__tar.gz → 0.7.8__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.
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/PKG-INFO +39 -38
- neuroglia_python-0.7.8/pyproject.toml +97 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/__init__.py +1 -1
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +12 -6
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +61 -2
- neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +278 -0
- neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +383 -0
- neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +328 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/__init__.py +5 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +15 -7
- neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/mongo/motor_query.py +399 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +166 -20
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queryable.py +69 -14
- neuroglia_python-0.7.8/src/neuroglia/expressions/javascript_expression_translator.py +207 -0
- neuroglia_python-0.7.8/src/neuroglia/hosting/configuration/data_access_layer.py +642 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/json.py +114 -8
- neuroglia_python-0.6.11/pyproject.toml +0 -97
- neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -89
- neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -237
- neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -62
- neuroglia_python-0.6.11/src/neuroglia/expressions/javascript_expression_translator.py +0 -147
- neuroglia_python-0.6.11/src/neuroglia/hosting/configuration/data_access_layer.py +0 -57
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/LICENSE +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/README.md +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/application/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/application/background_scheduler.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/abstractions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/leader_election.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/coordination/redis_backend.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/module_loader.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/operation_result.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/problem_details.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/type_extensions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/type_finder.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/core/type_registry.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/abstractions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/exceptions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queries/generic/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/queries/generic/list_query.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/abstractions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/controller.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/state_machine.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/resources/watcher.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/data/unit_of_work.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/dependency_injection/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/dependency_injection/service_provider.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/eventing/tracing.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/expressions/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/extensions/mediator_extensions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/hosting/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/hosting/abstractions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/hosting/web.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/cache_repository.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/http_service_client.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/integration/models.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/logging/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/logging/logger.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mapping/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mapping/mapper.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/mediator.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/metrics_middleware.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/simple.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mediation/tracing_middleware.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mvc/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/mvc/controller_base.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/framework.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/logging.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/metrics.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/otel_sdk.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/settings.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/observability/tracing.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/py.typed +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/reactive/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/reactive/rx_async.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/abstractions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/utils/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/utils/camel_model.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/utils/case_conversion.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/__init__.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/business_rules.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/exceptions.py +0 -0
- {neuroglia_python-0.6.11 → neuroglia_python-0.7.8}/src/neuroglia/validation/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: neuroglia-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.8
|
|
4
4
|
Summary: Port from .NET to Python of the Neuroglia Framework
|
|
5
5
|
License: Apache
|
|
6
6
|
License-File: LICENSE
|
|
@@ -21,43 +21,44 @@ Provides-Extra: etcd
|
|
|
21
21
|
Provides-Extra: eventstore
|
|
22
22
|
Provides-Extra: mongodb
|
|
23
23
|
Provides-Extra: redis
|
|
24
|
-
Requires-Dist: annotated-types (
|
|
25
|
-
Requires-Dist: apscheduler (
|
|
26
|
-
Requires-Dist: boto3 (
|
|
27
|
-
Requires-Dist: classy-fastapi (
|
|
28
|
-
Requires-Dist: email-validator (
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist: httpx (
|
|
34
|
-
Requires-Dist: itsdangerous (
|
|
35
|
-
Requires-Dist: jinja2 (
|
|
36
|
-
Requires-Dist:
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist:
|
|
39
|
-
Requires-Dist: opentelemetry-
|
|
40
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-
|
|
41
|
-
Requires-Dist: opentelemetry-exporter-
|
|
42
|
-
Requires-Dist: opentelemetry-
|
|
43
|
-
Requires-Dist: opentelemetry-instrumentation
|
|
44
|
-
Requires-Dist: opentelemetry-instrumentation-
|
|
45
|
-
Requires-Dist: opentelemetry-instrumentation-
|
|
46
|
-
Requires-Dist: opentelemetry-instrumentation-
|
|
47
|
-
Requires-Dist: opentelemetry-
|
|
48
|
-
Requires-Dist:
|
|
49
|
-
Requires-Dist:
|
|
50
|
-
Requires-Dist:
|
|
51
|
-
Requires-Dist:
|
|
52
|
-
Requires-Dist: pydantic
|
|
53
|
-
Requires-Dist:
|
|
54
|
-
Requires-Dist:
|
|
55
|
-
Requires-Dist:
|
|
56
|
-
Requires-Dist: python-
|
|
57
|
-
Requires-Dist:
|
|
58
|
-
Requires-Dist:
|
|
59
|
-
Requires-Dist:
|
|
60
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: annotated-types (==0.7.0)
|
|
25
|
+
Requires-Dist: apscheduler (==3.11.0)
|
|
26
|
+
Requires-Dist: boto3 (==1.40.64) ; extra == "aws" or extra == "all"
|
|
27
|
+
Requires-Dist: classy-fastapi (==0.6.1)
|
|
28
|
+
Requires-Dist: email-validator (==2.2.0)
|
|
29
|
+
Requires-Dist: etcd3-py (==0.1.6) ; extra == "etcd" or extra == "all"
|
|
30
|
+
Requires-Dist: fastapi (==0.115.5)
|
|
31
|
+
Requires-Dist: grpcio (==1.76.0)
|
|
32
|
+
Requires-Dist: h11 (==0.16.0)
|
|
33
|
+
Requires-Dist: httpx (==0.27.2)
|
|
34
|
+
Requires-Dist: itsdangerous (==2.2.0)
|
|
35
|
+
Requires-Dist: jinja2 (==3.1.4)
|
|
36
|
+
Requires-Dist: kurrentdbclient (==1.1.2) ; extra == "eventstore" or extra == "all"
|
|
37
|
+
Requires-Dist: motor (==3.7.0) ; extra == "mongodb" or extra == "all"
|
|
38
|
+
Requires-Dist: multipledispatch (==1.0.0)
|
|
39
|
+
Requires-Dist: opentelemetry-api (==1.38.0)
|
|
40
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (==1.38.0)
|
|
41
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.38.0)
|
|
42
|
+
Requires-Dist: opentelemetry-exporter-prometheus (==0.59b0)
|
|
43
|
+
Requires-Dist: opentelemetry-instrumentation (==0.59b0)
|
|
44
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi (==0.59b0)
|
|
45
|
+
Requires-Dist: opentelemetry-instrumentation-httpx (==0.59b0)
|
|
46
|
+
Requires-Dist: opentelemetry-instrumentation-logging (==0.59b0)
|
|
47
|
+
Requires-Dist: opentelemetry-instrumentation-system-metrics (==0.59b0)
|
|
48
|
+
Requires-Dist: opentelemetry-sdk (==1.38.0)
|
|
49
|
+
Requires-Dist: passlib[bcrypt] (==1.7.4)
|
|
50
|
+
Requires-Dist: prometheus-client (==0.21.0)
|
|
51
|
+
Requires-Dist: protobuf (==6.33.1) ; extra == "etcd" or extra == "all"
|
|
52
|
+
Requires-Dist: pydantic-settings (==2.6.1)
|
|
53
|
+
Requires-Dist: pydantic[email] (==2.10.3)
|
|
54
|
+
Requires-Dist: pyjwt (==2.10.1)
|
|
55
|
+
Requires-Dist: pymongo (==4.15.4) ; extra == "mongodb" or extra == "all"
|
|
56
|
+
Requires-Dist: python-dotenv (==1.0.1)
|
|
57
|
+
Requires-Dist: python-multipart (==0.0.17)
|
|
58
|
+
Requires-Dist: redis (==5.2.0) ; extra == "redis" or extra == "all"
|
|
59
|
+
Requires-Dist: rx (==3.2.0)
|
|
60
|
+
Requires-Dist: typing-extensions (==4.12.2)
|
|
61
|
+
Requires-Dist: uvicorn (==0.32.1)
|
|
61
62
|
Description-Content-Type: text/markdown
|
|
62
63
|
|
|
63
64
|
# Neuroglia Python Framework
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "neuroglia-python"
|
|
3
|
+
version = "0.7.8"
|
|
4
|
+
description = "Port from .NET to Python of the Neuroglia Framework"
|
|
5
|
+
authors = ["Charles d'Avernas <charles.davernas@neuroglia.io>", "Bruno van de Werve <bvandewe@gmail.com>"]
|
|
6
|
+
license = "Apache"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [
|
|
9
|
+
{ include = "neuroglia", from = "src" },
|
|
10
|
+
]
|
|
11
|
+
package-mode = true
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = "^3.9"
|
|
15
|
+
# Core framework dependencies - always required
|
|
16
|
+
annotated-types = "0.7.0"
|
|
17
|
+
classy-fastapi = "0.6.1"
|
|
18
|
+
fastapi = "0.115.5"
|
|
19
|
+
multipledispatch = "1.0.0"
|
|
20
|
+
pydantic-settings = "2.6.1"
|
|
21
|
+
python-dotenv = "1.0.1"
|
|
22
|
+
typing-extensions = "4.12.2"
|
|
23
|
+
rx = { version = "3.2.0" }
|
|
24
|
+
uvicorn = { version = "0.32.1" }
|
|
25
|
+
httpx = { version = "0.27.2" }
|
|
26
|
+
grpcio = { version = "1.76.0" }
|
|
27
|
+
h11 = { version = "0.16.0" } # Security fix for CVE-2025-43859
|
|
28
|
+
|
|
29
|
+
# Authentication dependencies
|
|
30
|
+
pyjwt = "2.10.1"
|
|
31
|
+
passlib = { extras = ["bcrypt"], version = "1.7.4" }
|
|
32
|
+
python-multipart = "0.0.17"
|
|
33
|
+
itsdangerous = "2.2.0"
|
|
34
|
+
|
|
35
|
+
# Core dependencies
|
|
36
|
+
protobuf = "6.33.1" # Pinned for compatibility with kurrentdbclient, OpenTelemetry, and etcd3-py
|
|
37
|
+
apscheduler = "3.11.0"
|
|
38
|
+
pydantic = {extras = ["email"], version = "2.10.3"}
|
|
39
|
+
email-validator = "2.2.0"
|
|
40
|
+
jinja2 = "3.1.4"
|
|
41
|
+
|
|
42
|
+
# Optional dependencies - install with extras
|
|
43
|
+
redis = { version = "5.2.0", optional = true }
|
|
44
|
+
pymongo = { version = "4.15.4", optional = true }
|
|
45
|
+
motor = { version = "3.7.0", optional = true }
|
|
46
|
+
kurrentdbclient = "1.1.2"
|
|
47
|
+
etcd3-py = { version = "0.1.6", optional = true } # Maintained fork with protobuf 5.x/6.x support
|
|
48
|
+
|
|
49
|
+
# OpenTelemetry dependencies - pinned versions for protobuf 6.x compatibility
|
|
50
|
+
opentelemetry-api = "1.38.0"
|
|
51
|
+
opentelemetry-sdk = "1.38.0"
|
|
52
|
+
opentelemetry-exporter-otlp-proto-grpc = "1.38.0"
|
|
53
|
+
opentelemetry-exporter-otlp-proto-http = "1.38.0"
|
|
54
|
+
opentelemetry-instrumentation = "0.59b0"
|
|
55
|
+
opentelemetry-instrumentation-fastapi = "0.59b0"
|
|
56
|
+
opentelemetry-instrumentation-httpx = "0.59b0"
|
|
57
|
+
opentelemetry-instrumentation-logging = "0.59b0"
|
|
58
|
+
opentelemetry-instrumentation-system-metrics = "0.59b0"
|
|
59
|
+
opentelemetry-exporter-prometheus = "0.59b0" # Prometheus /metrics endpoint support
|
|
60
|
+
prometheus-client = "0.21.0" # Required by opentelemetry-exporter-prometheus
|
|
61
|
+
boto3 = { version = "1.40.64", optional = true }
|
|
62
|
+
|
|
63
|
+
[tool.poetry.extras]
|
|
64
|
+
mongodb = ["pymongo", "motor"]
|
|
65
|
+
eventstore = ["kurrentdbclient"] # Formerly esdbclient
|
|
66
|
+
redis = ["redis"]
|
|
67
|
+
etcd = ["etcd3-py", "protobuf"]
|
|
68
|
+
aws = ["boto3"]
|
|
69
|
+
all = ["pymongo", "motor", "kurrentdbclient", "redis", "etcd3-py", "protobuf", "boto3"]
|
|
70
|
+
|
|
71
|
+
[tool.poetry.group.dev.dependencies]
|
|
72
|
+
# Development and testing dependencies
|
|
73
|
+
pre-commit = "^4.0.1"
|
|
74
|
+
pytest = "^8.3.3"
|
|
75
|
+
pytest-asyncio = "^0.24.0"
|
|
76
|
+
mypy = "^1.13.0"
|
|
77
|
+
autopep8 = "^2.3.1"
|
|
78
|
+
coverage = "^7.6.9"
|
|
79
|
+
mypy-extensions = "^1.0.0"
|
|
80
|
+
flake8 = "^7.1.1"
|
|
81
|
+
isort = "^5.13.2"
|
|
82
|
+
|
|
83
|
+
[tool.poetry.group.docs]
|
|
84
|
+
optional = true
|
|
85
|
+
|
|
86
|
+
[tool.poetry.group.docs.dependencies]
|
|
87
|
+
# Documentation dependencies
|
|
88
|
+
mkdocs = "^1.6.1"
|
|
89
|
+
mkdocs-material = "^9.5.48"
|
|
90
|
+
mkdocs-mermaid2-plugin = "^1.2.2"
|
|
91
|
+
|
|
92
|
+
[build-system]
|
|
93
|
+
requires = ["poetry-core"]
|
|
94
|
+
build-backend = "poetry.core.masonry.api"
|
|
95
|
+
|
|
96
|
+
[tool.black]
|
|
97
|
+
line-length = 500
|
|
@@ -5,23 +5,29 @@ Provides event store implementations and aggregate root support.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .abstractions import (
|
|
8
|
-
|
|
9
|
-
EventRecord,
|
|
8
|
+
Aggregator,
|
|
10
9
|
EventDescriptor,
|
|
10
|
+
EventRecord,
|
|
11
|
+
EventStore,
|
|
12
|
+
EventStoreOptions,
|
|
11
13
|
StreamDescriptor,
|
|
12
14
|
StreamReadDirection,
|
|
13
|
-
EventStoreOptions,
|
|
14
|
-
Aggregator
|
|
15
15
|
)
|
|
16
16
|
from .event_sourcing_repository import EventSourcingRepository
|
|
17
|
+
from .read_model_reconciliator import (
|
|
18
|
+
ReadModelConciliationOptions,
|
|
19
|
+
ReadModelReconciliator,
|
|
20
|
+
)
|
|
17
21
|
|
|
18
22
|
__all__ = [
|
|
19
23
|
"EventStore",
|
|
20
24
|
"EventSourcingRepository",
|
|
21
25
|
"EventRecord",
|
|
22
|
-
"EventDescriptor",
|
|
26
|
+
"EventDescriptor",
|
|
23
27
|
"StreamDescriptor",
|
|
24
28
|
"StreamReadDirection",
|
|
25
29
|
"EventStoreOptions",
|
|
26
30
|
"Aggregator",
|
|
27
|
-
|
|
31
|
+
"ReadModelConciliationOptions",
|
|
32
|
+
"ReadModelReconciliator",
|
|
33
|
+
]
|
|
@@ -8,6 +8,34 @@ from typing import Any, Optional
|
|
|
8
8
|
from neuroglia.data.abstractions import AggregateRoot
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class DeleteMode(Enum):
|
|
12
|
+
"""Specifies how deletion should be handled in event-sourced repositories."""
|
|
13
|
+
|
|
14
|
+
DISABLED = "disabled"
|
|
15
|
+
"""Deletion is not allowed (default behavior). Raises NotImplementedError."""
|
|
16
|
+
|
|
17
|
+
SOFT = "soft"
|
|
18
|
+
"""
|
|
19
|
+
Soft delete by calling the aggregate's deletion method.
|
|
20
|
+
|
|
21
|
+
The repository will:
|
|
22
|
+
1. Load the aggregate
|
|
23
|
+
2. Call aggregate.mark_as_deleted() or aggregate.mark_deleted()
|
|
24
|
+
3. Persist the deletion event via update_async()
|
|
25
|
+
|
|
26
|
+
The aggregate controls deletion semantics and domain events.
|
|
27
|
+
Event stream is preserved for audit and potential restoration.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
HARD = "hard"
|
|
31
|
+
"""
|
|
32
|
+
Physically delete the entire event stream from the event store.
|
|
33
|
+
|
|
34
|
+
WARNING: This is irreversible and removes all history for this aggregate.
|
|
35
|
+
Use for GDPR compliance (right to be forgotten) or data cleanup.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
11
39
|
@dataclass
|
|
12
40
|
class StreamDescriptor:
|
|
13
41
|
"""Represents a class used to describe a stream of recorded events"""
|
|
@@ -81,11 +109,11 @@ class AckableEventRecord(EventRecord):
|
|
|
81
109
|
|
|
82
110
|
async def ack_async(self) -> None:
|
|
83
111
|
"""Acks the event record"""
|
|
84
|
-
self._ack_delegate()
|
|
112
|
+
await self._ack_delegate()
|
|
85
113
|
|
|
86
114
|
async def nack_async(self) -> None:
|
|
87
115
|
"""Nacks the event record"""
|
|
88
|
-
self._nack_delegate()
|
|
116
|
+
await self._nack_delegate()
|
|
89
117
|
|
|
90
118
|
|
|
91
119
|
class StreamReadDirection(Enum):
|
|
@@ -155,11 +183,42 @@ class EventStore(ABC):
|
|
|
155
183
|
"""
|
|
156
184
|
raise NotImplementedError()
|
|
157
185
|
|
|
186
|
+
@abstractmethod
|
|
187
|
+
async def delete_async(self, stream_id: str) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Deletes the entire event stream.
|
|
190
|
+
|
|
191
|
+
This operation is irreversible and should be used with caution.
|
|
192
|
+
Useful for GDPR compliance (right to be forgotten) or data cleanup.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
stream_id: The identifier of the stream to delete
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
Exception: If the stream does not exist or deletion fails
|
|
199
|
+
"""
|
|
200
|
+
raise NotImplementedError()
|
|
201
|
+
|
|
158
202
|
|
|
159
203
|
class Aggregator:
|
|
160
204
|
def aggregate(self, events: list, aggregate_type: type):
|
|
205
|
+
"""
|
|
206
|
+
Reconstitutes an aggregate from a list of domain events.
|
|
207
|
+
|
|
208
|
+
Creates a new aggregate instance using __new__() to bypass __init__,
|
|
209
|
+
then manually initializes the state and _pending_events list before
|
|
210
|
+
replaying events to rebuild the aggregate's state.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
events: List of event records to replay
|
|
214
|
+
aggregate_type: The aggregate root class to instantiate
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Reconstituted aggregate with replayed state
|
|
218
|
+
"""
|
|
161
219
|
aggregate: AggregateRoot = object.__new__(aggregate_type)
|
|
162
220
|
aggregate.state = aggregate.__orig_bases__[0].__args__[0]()
|
|
221
|
+
aggregate._pending_events = list() # Initialize _pending_events to prevent AttributeError
|
|
163
222
|
for e in events:
|
|
164
223
|
aggregate.state.on(e.data)
|
|
165
224
|
aggregate.state.state_version = e.data.aggregate_version
|
neuroglia_python-0.7.8/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import TYPE_CHECKING, Generic, Optional
|
|
3
|
+
|
|
4
|
+
from neuroglia.data.abstractions import DomainEvent, TAggregate, TKey
|
|
5
|
+
from neuroglia.data.infrastructure.abstractions import Repository
|
|
6
|
+
from neuroglia.data.infrastructure.event_sourcing.abstractions import (
|
|
7
|
+
Aggregator,
|
|
8
|
+
DeleteMode,
|
|
9
|
+
EventDescriptor,
|
|
10
|
+
EventStore,
|
|
11
|
+
StreamReadDirection,
|
|
12
|
+
)
|
|
13
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from neuroglia.mediation.mediator import Mediator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class EventSourcingRepositoryOptions(Generic[TAggregate, TKey]):
|
|
21
|
+
"""
|
|
22
|
+
Configuration options for EventSourcingRepository.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
delete_mode: Specifies how deletion should be handled (DISABLED, SOFT, HARD)
|
|
26
|
+
soft_delete_method_name: Method name to call on aggregate for soft delete
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
```python
|
|
30
|
+
# Disable deletion (default)
|
|
31
|
+
options = EventSourcingRepositoryOptions[Task, str]()
|
|
32
|
+
|
|
33
|
+
# Enable soft delete
|
|
34
|
+
options = EventSourcingRepositoryOptions[Task, str](
|
|
35
|
+
delete_mode=DeleteMode.SOFT
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Enable hard delete (GDPR compliance)
|
|
39
|
+
options = EventSourcingRepositoryOptions[Task, str](
|
|
40
|
+
delete_mode=DeleteMode.HARD
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
delete_mode: DeleteMode = field(default=DeleteMode.DISABLED)
|
|
46
|
+
"""
|
|
47
|
+
Specifies how deletion should be handled:
|
|
48
|
+
- DISABLED: Raises NotImplementedError (default, event sourcing best practice)
|
|
49
|
+
- SOFT: Calls aggregate.mark_as_deleted() or aggregate.mark_deleted() then persists
|
|
50
|
+
- HARD: Physically deletes the event stream (use with caution)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
soft_delete_method_name: str = field(default="mark_as_deleted")
|
|
54
|
+
"""
|
|
55
|
+
Method name to call on aggregate for soft delete.
|
|
56
|
+
Defaults to 'mark_as_deleted'. Alternative: 'mark_deleted'.
|
|
57
|
+
Only used when delete_mode is SOFT.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EventSourcingRepository(Generic[TAggregate, TKey], Repository[TAggregate, TKey]):
|
|
62
|
+
"""
|
|
63
|
+
Event sourcing repository implementation with configurable deletion strategies.
|
|
64
|
+
|
|
65
|
+
Supports three deletion modes:
|
|
66
|
+
- DISABLED: No deletion allowed (default, follows event sourcing best practices)
|
|
67
|
+
- SOFT: Delegates to aggregate's deletion method, preserving event history
|
|
68
|
+
- HARD: Physical stream deletion for GDPR compliance or data cleanup
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
```python
|
|
72
|
+
# Default configuration (deletion disabled)
|
|
73
|
+
repo = EventSourcingRepository[Task, str](eventstore, aggregator)
|
|
74
|
+
|
|
75
|
+
# With soft delete enabled
|
|
76
|
+
options = EventSourcingRepositoryOptions[Task, str](
|
|
77
|
+
delete_mode=DeleteMode.SOFT
|
|
78
|
+
)
|
|
79
|
+
repo = EventSourcingRepository[Task, str](
|
|
80
|
+
eventstore, aggregator, options=options
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# With hard delete for GDPR compliance
|
|
84
|
+
options = EventSourcingRepositoryOptions[Task, str](
|
|
85
|
+
delete_mode=DeleteMode.HARD
|
|
86
|
+
)
|
|
87
|
+
repo = EventSourcingRepository[Task, str](
|
|
88
|
+
eventstore, aggregator, options=options
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
eventstore: EventStore,
|
|
96
|
+
aggregator: Aggregator,
|
|
97
|
+
mediator: Optional["Mediator"] = None,
|
|
98
|
+
options: Optional[EventSourcingRepositoryOptions[TAggregate, TKey]] = None,
|
|
99
|
+
):
|
|
100
|
+
"""Initialize a new event sourcing repository"""
|
|
101
|
+
super().__init__(mediator) # Pass mediator to base class for event publishing
|
|
102
|
+
self._eventstore = eventstore
|
|
103
|
+
self._aggregator = aggregator
|
|
104
|
+
self._options = options or EventSourcingRepositoryOptions[TAggregate, TKey]()
|
|
105
|
+
|
|
106
|
+
_eventstore: EventStore
|
|
107
|
+
""" Gets the underlying event store """
|
|
108
|
+
|
|
109
|
+
_aggregator: Aggregator
|
|
110
|
+
""" Gets the underlying event store """
|
|
111
|
+
|
|
112
|
+
async def contains_async(self, id: TKey) -> bool:
|
|
113
|
+
return self._eventstore.contains_stream(self._build_stream_id_for(id))
|
|
114
|
+
|
|
115
|
+
async def get_async(self, id: TKey) -> Optional[TAggregate]:
|
|
116
|
+
"""
|
|
117
|
+
Gets the aggregate with the specified id, if any.
|
|
118
|
+
|
|
119
|
+
Returns None if the stream does not exist.
|
|
120
|
+
"""
|
|
121
|
+
stream_id = self._build_stream_id_for(id)
|
|
122
|
+
try:
|
|
123
|
+
events = await self._eventstore.read_async(stream_id, StreamReadDirection.FORWARDS, 0)
|
|
124
|
+
if not events:
|
|
125
|
+
return None
|
|
126
|
+
return self._aggregator.aggregate(events, self.__orig_class__.__args__[0])
|
|
127
|
+
except Exception:
|
|
128
|
+
# If stream doesn't exist or any other error occurs, return None
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
async def _do_add_async(self, aggregate: TAggregate) -> TAggregate:
|
|
132
|
+
"""Adds and persists the specified aggregate"""
|
|
133
|
+
stream_id = self._build_stream_id_for(aggregate.id())
|
|
134
|
+
events = aggregate._pending_events
|
|
135
|
+
if len(events) < 1:
|
|
136
|
+
raise Exception("No pending events to persist")
|
|
137
|
+
encoded_events = [self._encode_event(e) for e in events]
|
|
138
|
+
await self._eventstore.append_async(stream_id, encoded_events)
|
|
139
|
+
aggregate.state.state_version = events[-1].aggregate_version
|
|
140
|
+
# DON'T clear pending events here - let base class do it after publishing!
|
|
141
|
+
# aggregate.clear_pending_events()
|
|
142
|
+
return aggregate
|
|
143
|
+
|
|
144
|
+
async def _do_update_async(self, aggregate: TAggregate) -> TAggregate:
|
|
145
|
+
"""Persists the changes made to the specified aggregate"""
|
|
146
|
+
stream_id = self._build_stream_id_for(aggregate.id())
|
|
147
|
+
events = aggregate._pending_events
|
|
148
|
+
if len(events) < 1:
|
|
149
|
+
raise Exception("No pending events to persist")
|
|
150
|
+
encoded_events = [self._encode_event(e) for e in events]
|
|
151
|
+
await self._eventstore.append_async(stream_id, encoded_events, aggregate.state.state_version)
|
|
152
|
+
aggregate.state.state_version = events[-1].aggregate_version
|
|
153
|
+
# DON'T clear pending events here - let base class do it after publishing!
|
|
154
|
+
# aggregate.clear_pending_events()
|
|
155
|
+
return aggregate
|
|
156
|
+
|
|
157
|
+
async def _do_remove_async(self, id: TKey) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Removes the aggregate root with the specified key based on configured delete mode.
|
|
160
|
+
|
|
161
|
+
Behavior depends on delete_mode configuration:
|
|
162
|
+
- DISABLED: Raises NotImplementedError (default)
|
|
163
|
+
- SOFT: Loads aggregate, calls mark_as_deleted(), persists deletion event
|
|
164
|
+
- HARD: Physically deletes the event stream (irreversible)
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
id: The identifier of the aggregate to remove
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
NotImplementedError: When delete_mode is DISABLED
|
|
171
|
+
ValueError: When SOFT delete but aggregate lacks deletion method
|
|
172
|
+
Exception: When aggregate not found or deletion fails
|
|
173
|
+
"""
|
|
174
|
+
match self._options.delete_mode:
|
|
175
|
+
case DeleteMode.DISABLED:
|
|
176
|
+
raise NotImplementedError("Deletion is disabled for this repository. " "Event sourcing repositories preserve immutable history by default. " "Configure delete_mode=DeleteMode.SOFT for soft delete " "or delete_mode=DeleteMode.HARD for physical stream deletion.")
|
|
177
|
+
|
|
178
|
+
case DeleteMode.SOFT:
|
|
179
|
+
await self._soft_delete_async(id)
|
|
180
|
+
|
|
181
|
+
case DeleteMode.HARD:
|
|
182
|
+
await self._hard_delete_async(id)
|
|
183
|
+
|
|
184
|
+
async def _soft_delete_async(self, id: TKey) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Soft delete by calling aggregate's deletion method.
|
|
187
|
+
|
|
188
|
+
The repository will:
|
|
189
|
+
1. Load the aggregate via get_async()
|
|
190
|
+
2. Call aggregate's deletion method (mark_as_deleted or mark_deleted)
|
|
191
|
+
3. Persist the deletion event via _do_update_async()
|
|
192
|
+
|
|
193
|
+
This preserves event history while marking the aggregate as deleted.
|
|
194
|
+
The aggregate controls deletion semantics and domain events.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
id: The identifier of the aggregate to soft delete
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
Exception: If aggregate not found
|
|
201
|
+
ValueError: If aggregate lacks required deletion method
|
|
202
|
+
"""
|
|
203
|
+
# Load the aggregate
|
|
204
|
+
aggregate = await self.get_async(id)
|
|
205
|
+
if aggregate is None:
|
|
206
|
+
raise Exception(f"Aggregate with id '{id}' not found")
|
|
207
|
+
|
|
208
|
+
# Call the aggregate's deletion method (convention-based)
|
|
209
|
+
method_name = self._options.soft_delete_method_name
|
|
210
|
+
if hasattr(aggregate, method_name) and callable(getattr(aggregate, method_name)):
|
|
211
|
+
deletion_method = getattr(aggregate, method_name)
|
|
212
|
+
deletion_method()
|
|
213
|
+
elif hasattr(aggregate, "mark_deleted") and callable(getattr(aggregate, "mark_deleted")):
|
|
214
|
+
# Fallback to alternative method name
|
|
215
|
+
aggregate.mark_deleted()
|
|
216
|
+
else:
|
|
217
|
+
aggregate_type = type(aggregate).__name__
|
|
218
|
+
raise ValueError(f"Aggregate {aggregate_type} does not have a '{method_name}()' or 'mark_deleted()' method. " f"Soft delete requires the aggregate to implement deletion logic. " f"Add a method like: def {method_name}(self) -> None: ...")
|
|
219
|
+
|
|
220
|
+
# Persist the deletion event via update
|
|
221
|
+
await self._do_update_async(aggregate)
|
|
222
|
+
|
|
223
|
+
async def _hard_delete_async(self, id: TKey) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Physically delete the entire event stream from the event store.
|
|
226
|
+
|
|
227
|
+
WARNING: This is irreversible and removes all history for this aggregate.
|
|
228
|
+
Use for:
|
|
229
|
+
- GDPR compliance (right to be forgotten)
|
|
230
|
+
- Data cleanup after retention period
|
|
231
|
+
- Removing test/invalid data
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
id: The identifier of the aggregate to hard delete
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
Exception: If stream deletion fails
|
|
238
|
+
"""
|
|
239
|
+
stream_id = self._build_stream_id_for(id)
|
|
240
|
+
await self._eventstore.delete_async(stream_id)
|
|
241
|
+
|
|
242
|
+
async def _publish_domain_events(self, entity: TAggregate) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Override base class event publishing for event-sourced aggregates.
|
|
245
|
+
|
|
246
|
+
Event sourcing repositories DO NOT publish events directly because:
|
|
247
|
+
1. Events are already persisted to the EventStore
|
|
248
|
+
2. ReadModelReconciliator subscribes to EventStore and publishes ALL events
|
|
249
|
+
3. Publishing here would cause DOUBLE PUBLISHING (once here, once from ReadModelReconciliator)
|
|
250
|
+
|
|
251
|
+
For event-sourced aggregates:
|
|
252
|
+
- Events are persisted to EventStore by _do_add_async/_do_update_async
|
|
253
|
+
- ReadModelReconciliator.on_event_record_stream_next_async() publishes via mediator
|
|
254
|
+
- This ensures single, reliable event publishing from the source of truth (EventStore)
|
|
255
|
+
|
|
256
|
+
State-based repositories still use the base class _publish_domain_events() correctly.
|
|
257
|
+
"""
|
|
258
|
+
# Do nothing - ReadModelReconciliator handles event publishing from EventStore
|
|
259
|
+
|
|
260
|
+
def _build_stream_id_for(self, aggregate_id: TKey):
|
|
261
|
+
"""Builds a new stream id for the specified aggregate"""
|
|
262
|
+
aggregate_name = self.__orig_class__.__args__[0].__name__
|
|
263
|
+
return f"{aggregate_name.lower()}-{aggregate_id}"
|
|
264
|
+
|
|
265
|
+
def _encode_event(self, e: DomainEvent):
|
|
266
|
+
"""Encodes a domain event into a new event descriptor"""
|
|
267
|
+
event_type = type(e).__name__.lower()
|
|
268
|
+
return EventDescriptor(event_type, e)
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def configure(builder: ApplicationBuilderBase, entity_type: type, key_type: type) -> ApplicationBuilderBase:
|
|
272
|
+
"""Configures the specified application to use an event sourcing based repository implementation to manage the specified type of entity"""
|
|
273
|
+
builder.services.try_add_singleton(
|
|
274
|
+
EventSourcingRepositoryOptions[entity_type, key_type],
|
|
275
|
+
singleton=EventSourcingRepositoryOptions[entity_type, key_type](),
|
|
276
|
+
)
|
|
277
|
+
builder.services.try_add_singleton(Repository[entity_type, key_type], EventSourcingRepository[entity_type, key_type])
|
|
278
|
+
return builder
|