neuroglia-python 0.6.11__tar.gz → 0.6.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/PKG-INFO +1 -1
  2. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/pyproject.toml +1 -1
  3. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/__init__.py +1 -1
  4. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +46 -2
  5. neuroglia_python-0.6.17/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +266 -0
  6. neuroglia_python-0.6.17/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +371 -0
  7. neuroglia_python-0.6.17/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +89 -0
  8. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +12 -4
  9. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/queryable.py +1 -1
  10. neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -89
  11. neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -237
  12. neuroglia_python-0.6.11/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -62
  13. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/LICENSE +0 -0
  14. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/README.md +0 -0
  15. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/application/__init__.py +0 -0
  16. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/application/background_scheduler.py +0 -0
  17. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/coordination/__init__.py +0 -0
  18. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/coordination/abstractions.py +0 -0
  19. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/coordination/leader_election.py +0 -0
  20. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/coordination/redis_backend.py +0 -0
  21. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/core/__init__.py +0 -0
  22. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/core/module_loader.py +0 -0
  23. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/core/operation_result.py +0 -0
  24. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/core/problem_details.py +0 -0
  25. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/core/type_extensions.py +0 -0
  26. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/core/type_finder.py +0 -0
  27. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/core/type_registry.py +0 -0
  28. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/__init__.py +0 -0
  29. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/abstractions.py +0 -0
  30. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/exceptions.py +0 -0
  31. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  32. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  33. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  34. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  35. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  36. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  37. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  38. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  39. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
  40. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  41. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +0 -0
  42. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  43. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  44. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  45. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  46. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  47. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  48. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  49. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  50. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  51. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  52. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  53. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/__init__.py +0 -0
  54. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/abstractions.py +0 -0
  55. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/controller.py +0 -0
  56. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  57. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  58. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  59. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/state_machine.py +0 -0
  60. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/resources/watcher.py +0 -0
  61. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/data/unit_of_work.py +0 -0
  62. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/dependency_injection/__init__.py +0 -0
  63. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/dependency_injection/service_provider.py +0 -0
  64. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  65. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/__init__.py +0 -0
  66. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  67. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  68. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  69. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  70. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  71. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  72. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  73. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  74. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/eventing/tracing.py +0 -0
  75. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/expressions/__init__.py +0 -0
  76. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
  77. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  78. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  79. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  80. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/hosting/__init__.py +0 -0
  81. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/hosting/abstractions.py +0 -0
  82. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/hosting/configuration/data_access_layer.py +0 -0
  83. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/hosting/web.py +0 -0
  84. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/integration/__init__.py +0 -0
  85. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/integration/cache_repository.py +0 -0
  86. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/integration/http_service_client.py +0 -0
  87. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/integration/models.py +0 -0
  88. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/logging/__init__.py +0 -0
  89. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/logging/logger.py +0 -0
  90. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mapping/__init__.py +0 -0
  91. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mapping/mapper.py +0 -0
  92. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/__init__.py +0 -0
  93. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  94. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  95. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/mediator.py +0 -0
  96. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  97. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  98. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/simple.py +0 -0
  99. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  100. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mvc/__init__.py +0 -0
  101. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/mvc/controller_base.py +0 -0
  102. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/observability/__init__.py +0 -0
  103. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/observability/framework.py +0 -0
  104. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/observability/logging.py +0 -0
  105. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/observability/metrics.py +0 -0
  106. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/observability/otel_sdk.py +0 -0
  107. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/observability/settings.py +0 -0
  108. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/observability/tracing.py +0 -0
  109. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/py.typed +0 -0
  110. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/reactive/__init__.py +0 -0
  111. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/reactive/rx_async.py +0 -0
  112. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/serialization/__init__.py +0 -0
  113. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/serialization/abstractions.py +0 -0
  114. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  115. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/serialization/json.py +0 -0
  116. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/utils/__init__.py +0 -0
  117. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/utils/camel_model.py +0 -0
  118. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/utils/case_conversion.py +0 -0
  119. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/validation/__init__.py +0 -0
  120. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/validation/business_rules.py +0 -0
  121. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/src/neuroglia/validation/exceptions.py +0 -0
  122. {neuroglia_python-0.6.11 → neuroglia_python-0.6.17}/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.6.11
3
+ Version: 0.6.17
4
4
  Summary: Port from .NET to Python of the Neuroglia Framework
5
5
  License: Apache
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "neuroglia-python"
3
- version = "0.6.11"
3
+ version = "0.6.17"
4
4
  description = "Port from .NET to Python of the Neuroglia Framework"
5
5
  authors = ["Charles d'Avernas <charles.davernas@neuroglia.io>", "Bruno van de Werve <bvandewe@gmail.com>"]
6
6
  license = "Apache"
@@ -62,7 +62,7 @@ __all__ = [
62
62
  ]
63
63
 
64
64
  # Framework metadata
65
- __version__ = "0.6.11"
65
+ __version__ = "0.6.17"
66
66
  __author__ = "Neuroglia Team"
67
67
  __email__ = "team@neuroglia.io"
68
68
  __license__ = "Apache"
@@ -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,6 +183,22 @@ 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):
@@ -0,0 +1,266 @@
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
+ """Gets the aggregate with the specified id, if any"""
117
+ stream_id = self._build_stream_id_for(id)
118
+ events = await self._eventstore.read_async(stream_id, StreamReadDirection.FORWARDS, 0)
119
+ return self._aggregator.aggregate(events, self.__orig_class__.__args__[0])
120
+
121
+ async def _do_add_async(self, aggregate: TAggregate) -> TAggregate:
122
+ """Adds and persists the specified aggregate"""
123
+ stream_id = self._build_stream_id_for(aggregate.id())
124
+ events = aggregate._pending_events
125
+ if len(events) < 1:
126
+ raise Exception("No pending events to persist")
127
+ encoded_events = [self._encode_event(e) for e in events]
128
+ await self._eventstore.append_async(stream_id, encoded_events)
129
+ aggregate.state.state_version = events[-1].aggregate_version
130
+ aggregate.clear_pending_events()
131
+ return aggregate
132
+
133
+ async def _do_update_async(self, aggregate: TAggregate) -> TAggregate:
134
+ """Persists the changes made to the specified aggregate"""
135
+ stream_id = self._build_stream_id_for(aggregate.id())
136
+ events = aggregate._pending_events
137
+ if len(events) < 1:
138
+ raise Exception("No pending events to persist")
139
+ encoded_events = [self._encode_event(e) for e in events]
140
+ await self._eventstore.append_async(stream_id, encoded_events, aggregate.state.state_version)
141
+ aggregate.state.state_version = events[-1].aggregate_version
142
+ aggregate.clear_pending_events()
143
+ return aggregate
144
+
145
+ async def _do_remove_async(self, id: TKey) -> None:
146
+ """
147
+ Removes the aggregate root with the specified key based on configured delete mode.
148
+
149
+ Behavior depends on delete_mode configuration:
150
+ - DISABLED: Raises NotImplementedError (default)
151
+ - SOFT: Loads aggregate, calls mark_as_deleted(), persists deletion event
152
+ - HARD: Physically deletes the event stream (irreversible)
153
+
154
+ Args:
155
+ id: The identifier of the aggregate to remove
156
+
157
+ Raises:
158
+ NotImplementedError: When delete_mode is DISABLED
159
+ ValueError: When SOFT delete but aggregate lacks deletion method
160
+ Exception: When aggregate not found or deletion fails
161
+ """
162
+ match self._options.delete_mode:
163
+ case DeleteMode.DISABLED:
164
+ 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.")
165
+
166
+ case DeleteMode.SOFT:
167
+ await self._soft_delete_async(id)
168
+
169
+ case DeleteMode.HARD:
170
+ await self._hard_delete_async(id)
171
+
172
+ async def _soft_delete_async(self, id: TKey) -> None:
173
+ """
174
+ Soft delete by calling aggregate's deletion method.
175
+
176
+ The repository will:
177
+ 1. Load the aggregate via get_async()
178
+ 2. Call aggregate's deletion method (mark_as_deleted or mark_deleted)
179
+ 3. Persist the deletion event via _do_update_async()
180
+
181
+ This preserves event history while marking the aggregate as deleted.
182
+ The aggregate controls deletion semantics and domain events.
183
+
184
+ Args:
185
+ id: The identifier of the aggregate to soft delete
186
+
187
+ Raises:
188
+ Exception: If aggregate not found
189
+ ValueError: If aggregate lacks required deletion method
190
+ """
191
+ # Load the aggregate
192
+ aggregate = await self.get_async(id)
193
+ if aggregate is None:
194
+ raise Exception(f"Aggregate with id '{id}' not found")
195
+
196
+ # Call the aggregate's deletion method (convention-based)
197
+ method_name = self._options.soft_delete_method_name
198
+ if hasattr(aggregate, method_name) and callable(getattr(aggregate, method_name)):
199
+ deletion_method = getattr(aggregate, method_name)
200
+ deletion_method()
201
+ elif hasattr(aggregate, "mark_deleted") and callable(getattr(aggregate, "mark_deleted")):
202
+ # Fallback to alternative method name
203
+ aggregate.mark_deleted()
204
+ else:
205
+ aggregate_type = type(aggregate).__name__
206
+ 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: ...")
207
+
208
+ # Persist the deletion event via update
209
+ await self._do_update_async(aggregate)
210
+
211
+ async def _hard_delete_async(self, id: TKey) -> None:
212
+ """
213
+ Physically delete the entire event stream from the event store.
214
+
215
+ WARNING: This is irreversible and removes all history for this aggregate.
216
+ Use for:
217
+ - GDPR compliance (right to be forgotten)
218
+ - Data cleanup after retention period
219
+ - Removing test/invalid data
220
+
221
+ Args:
222
+ id: The identifier of the aggregate to hard delete
223
+
224
+ Raises:
225
+ Exception: If stream deletion fails
226
+ """
227
+ stream_id = self._build_stream_id_for(id)
228
+ await self._eventstore.delete_async(stream_id)
229
+
230
+ async def _publish_domain_events(self, entity: TAggregate) -> None:
231
+ """
232
+ Override base class event publishing for event-sourced aggregates.
233
+
234
+ Event sourcing repositories DO NOT publish events directly because:
235
+ 1. Events are already persisted to the EventStore
236
+ 2. ReadModelReconciliator subscribes to EventStore and publishes ALL events
237
+ 3. Publishing here would cause DOUBLE PUBLISHING (once here, once from ReadModelReconciliator)
238
+
239
+ For event-sourced aggregates:
240
+ - Events are persisted to EventStore by _do_add_async/_do_update_async
241
+ - ReadModelReconciliator.on_event_record_stream_next_async() publishes via mediator
242
+ - This ensures single, reliable event publishing from the source of truth (EventStore)
243
+
244
+ State-based repositories still use the base class _publish_domain_events() correctly.
245
+ """
246
+ # Do nothing - ReadModelReconciliator handles event publishing from EventStore
247
+
248
+ def _build_stream_id_for(self, aggregate_id: TKey):
249
+ """Builds a new stream id for the specified aggregate"""
250
+ aggregate_name = self.__orig_class__.__args__[0].__name__
251
+ return f"{aggregate_name.lower()}-{aggregate_id}"
252
+
253
+ def _encode_event(self, e: DomainEvent):
254
+ """Encodes a domain event into a new event descriptor"""
255
+ event_type = type(e).__name__.lower()
256
+ return EventDescriptor(event_type, e)
257
+
258
+ @staticmethod
259
+ def configure(builder: ApplicationBuilderBase, entity_type: type, key_type: type) -> ApplicationBuilderBase:
260
+ """Configures the specified application to use an event sourcing based repository implementation to manage the specified type of entity"""
261
+ builder.services.try_add_singleton(
262
+ EventSourcingRepositoryOptions[entity_type, key_type],
263
+ singleton=EventSourcingRepositoryOptions[entity_type, key_type](),
264
+ )
265
+ builder.services.try_add_singleton(Repository[entity_type, key_type], EventSourcingRepository[entity_type, key_type])
266
+ return builder