neuroglia-python 0.6.14__tar.gz → 0.6.15__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.14 → neuroglia_python-0.6.15}/PKG-INFO +1 -1
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/pyproject.toml +1 -1
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/__init__.py +1 -1
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +44 -0
- neuroglia_python-0.6.15/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +266 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +29 -11
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +10 -2
- neuroglia_python-0.6.14/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -111
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/LICENSE +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/README.md +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/application/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/application/background_scheduler.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/coordination/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/coordination/abstractions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/coordination/leader_election.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/coordination/redis_backend.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/core/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/core/module_loader.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/core/operation_result.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/core/problem_details.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/core/type_extensions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/core/type_finder.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/core/type_registry.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/abstractions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/exceptions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/queries/generic/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/queries/generic/list_query.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/queryable.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/abstractions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/controller.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/state_machine.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/watcher.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/unit_of_work.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/dependency_injection/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/dependency_injection/service_provider.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/tracing.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/expressions/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/extensions/mediator_extensions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/hosting/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/hosting/abstractions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/hosting/configuration/data_access_layer.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/hosting/web.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/integration/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/integration/cache_repository.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/integration/http_service_client.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/integration/models.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/logging/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/logging/logger.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mapping/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mapping/mapper.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/mediator.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/metrics_middleware.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/simple.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/tracing_middleware.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mvc/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mvc/controller_base.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/framework.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/logging.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/metrics.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/otel_sdk.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/settings.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/tracing.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/py.typed +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/reactive/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/reactive/rx_async.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/serialization/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/serialization/abstractions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/serialization/json.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/utils/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/utils/camel_model.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/utils/case_conversion.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/validation/__init__.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/validation/business_rules.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/validation/exceptions.py +0 -0
- {neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/validation/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "neuroglia-python"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.15"
|
|
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"
|
|
@@ -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"""
|
|
@@ -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
|
|
@@ -13,6 +13,7 @@ from rx.subject.subject import Subject
|
|
|
13
13
|
|
|
14
14
|
from neuroglia.data.abstractions import DomainEvent
|
|
15
15
|
from neuroglia.data.infrastructure.event_sourcing.abstractions import (
|
|
16
|
+
AckableEventRecord,
|
|
16
17
|
Aggregator,
|
|
17
18
|
EventDescriptor,
|
|
18
19
|
EventRecord,
|
|
@@ -212,24 +213,41 @@ class ESEventStore(EventStore):
|
|
|
212
213
|
if hasattr(subscription, "nack"):
|
|
213
214
|
subscription.nack(e.id, action="park")
|
|
214
215
|
raise
|
|
215
|
-
try:
|
|
216
|
-
subject.on_next(decoded_event)
|
|
217
216
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
# Convert to AckableEventRecord if subscription supports ack/nack
|
|
218
|
+
if hasattr(subscription, "ack") and hasattr(subscription, "nack"):
|
|
219
|
+
event_id = e.id
|
|
220
|
+
ackable_event = AckableEventRecord(stream_id=decoded_event.stream_id, id=decoded_event.id, offset=decoded_event.offset, position=decoded_event.position, timestamp=decoded_event.timestamp, type=decoded_event.type, data=decoded_event.data, metadata=decoded_event.metadata, replayed=decoded_event.replayed, _ack_delegate=lambda eid=event_id: subscription.ack(eid), _nack_delegate=lambda eid=event_id, action="retry": subscription.nack(eid, action=action))
|
|
221
|
+
subject.on_next(ackable_event)
|
|
222
|
+
else:
|
|
223
|
+
# No ack/nack support, send regular EventRecord
|
|
224
|
+
subject.on_next(decoded_event)
|
|
221
225
|
|
|
222
|
-
except Exception as ex:
|
|
223
|
-
logging.error(f"An exception occurred while handling event with offset '{e.stream_position}' from stream '{e.stream_name}': {ex}")
|
|
224
|
-
# Negative acknowledge (retry) on processing failure
|
|
225
|
-
if hasattr(subscription, "nack"):
|
|
226
|
-
subscription.nack(e.id, action="retry")
|
|
227
|
-
raise
|
|
228
226
|
subject.on_completed()
|
|
229
227
|
except Exception as ex:
|
|
230
228
|
logging.error(f"An exception occurred while consuming events from stream '{stream_id}', consequently to which the related subscription will be stopped: {ex}") # todo: improve feedback
|
|
231
229
|
subscription.stop()
|
|
232
230
|
|
|
231
|
+
async def delete_async(self, stream_id: str) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Delete the stream from EventStoreDB.
|
|
234
|
+
|
|
235
|
+
Uses EventStoreDB's stream deletion which physically removes the stream.
|
|
236
|
+
This operation is irreversible and should be used with caution.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
stream_id: The identifier of the stream to delete
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
Exception: If the stream does not exist or deletion fails
|
|
243
|
+
"""
|
|
244
|
+
stream_name = self._get_stream_name(stream_id)
|
|
245
|
+
try:
|
|
246
|
+
# Delete the stream from EventStoreDB
|
|
247
|
+
self._eventstore_client.delete_stream(stream_name=stream_name, current_version=StreamState.ANY)
|
|
248
|
+
except Exception as ex:
|
|
249
|
+
raise Exception(f"Failed to delete stream '{stream_name}': {ex}") from ex
|
|
250
|
+
|
|
233
251
|
@staticmethod
|
|
234
252
|
def configure(builder: ApplicationBuilderBase, options: EventStoreOptions) -> ApplicationBuilderBase:
|
|
235
253
|
"""Registers and configures an EventStore implementation of the EventStore class.
|
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|
|
5
5
|
from rx.core.typing import Disposable
|
|
6
6
|
|
|
7
7
|
from neuroglia.data.infrastructure.event_sourcing.abstractions import (
|
|
8
|
+
AckableEventRecord,
|
|
8
9
|
EventRecord,
|
|
9
10
|
EventStore,
|
|
10
11
|
EventStoreOptions,
|
|
@@ -72,10 +73,17 @@ class ReadModelReconciliator(HostedService):
|
|
|
72
73
|
try:
|
|
73
74
|
# todo: migrate event
|
|
74
75
|
await self._mediator.publish_async(e.data)
|
|
75
|
-
|
|
76
|
+
|
|
77
|
+
# Acknowledge successful processing
|
|
78
|
+
if isinstance(e, AckableEventRecord):
|
|
79
|
+
await e.ack_async()
|
|
80
|
+
|
|
76
81
|
except Exception as ex:
|
|
77
82
|
logging.error(f"An exception occured while publishing an event of type '{type(e.data).__name__}': {ex}")
|
|
78
|
-
|
|
83
|
+
|
|
84
|
+
# Negative acknowledge on processing failure
|
|
85
|
+
if isinstance(e, AckableEventRecord):
|
|
86
|
+
await e.nack_async()
|
|
79
87
|
|
|
80
88
|
async def on_event_record_stream_error(self, ex: Exception):
|
|
81
89
|
await self.subscribe_async()
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
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
|
-
EventDescriptor,
|
|
9
|
-
EventStore,
|
|
10
|
-
StreamReadDirection,
|
|
11
|
-
)
|
|
12
|
-
from neuroglia.hosting.abstractions import ApplicationBuilderBase
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from neuroglia.mediation.mediator import Mediator
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass
|
|
19
|
-
class EventSourcingRepositoryOptions(Generic[TAggregate, TKey]):
|
|
20
|
-
"""Represents the options used to configure an event sourcing repository"""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class EventSourcingRepository(Generic[TAggregate, TKey], Repository[TAggregate, TKey]):
|
|
24
|
-
"""Represents an event sourcing repository implementation"""
|
|
25
|
-
|
|
26
|
-
def __init__(self, eventstore: EventStore, aggregator: Aggregator, mediator: Optional["Mediator"] = None):
|
|
27
|
-
"""Initialize a new event sourcing repository"""
|
|
28
|
-
super().__init__(mediator) # Pass mediator to base class for event publishing
|
|
29
|
-
self._eventstore = eventstore
|
|
30
|
-
self._aggregator = aggregator
|
|
31
|
-
|
|
32
|
-
_eventstore: EventStore
|
|
33
|
-
""" Gets the underlying event store """
|
|
34
|
-
|
|
35
|
-
_aggregator: Aggregator
|
|
36
|
-
""" Gets the underlying event store """
|
|
37
|
-
|
|
38
|
-
async def contains_async(self, id: TKey) -> bool:
|
|
39
|
-
return self._eventstore.contains_stream(self._build_stream_id_for(id))
|
|
40
|
-
|
|
41
|
-
async def get_async(self, id: TKey) -> Optional[TAggregate]:
|
|
42
|
-
"""Gets the aggregate with the specified id, if any"""
|
|
43
|
-
stream_id = self._build_stream_id_for(id)
|
|
44
|
-
events = await self._eventstore.read_async(stream_id, StreamReadDirection.FORWARDS, 0)
|
|
45
|
-
return self._aggregator.aggregate(events, self.__orig_class__.__args__[0])
|
|
46
|
-
|
|
47
|
-
async def _do_add_async(self, aggregate: TAggregate) -> TAggregate:
|
|
48
|
-
"""Adds and persists the specified aggregate"""
|
|
49
|
-
stream_id = self._build_stream_id_for(aggregate.id())
|
|
50
|
-
events = aggregate._pending_events
|
|
51
|
-
if len(events) < 1:
|
|
52
|
-
raise Exception("No pending events to persist")
|
|
53
|
-
encoded_events = [self._encode_event(e) for e in events]
|
|
54
|
-
await self._eventstore.append_async(stream_id, encoded_events)
|
|
55
|
-
aggregate.state.state_version = events[-1].aggregate_version
|
|
56
|
-
aggregate.clear_pending_events()
|
|
57
|
-
return aggregate
|
|
58
|
-
|
|
59
|
-
async def _do_update_async(self, aggregate: TAggregate) -> TAggregate:
|
|
60
|
-
"""Persists the changes made to the specified aggregate"""
|
|
61
|
-
stream_id = self._build_stream_id_for(aggregate.id())
|
|
62
|
-
events = aggregate._pending_events
|
|
63
|
-
if len(events) < 1:
|
|
64
|
-
raise Exception("No pending events to persist")
|
|
65
|
-
encoded_events = [self._encode_event(e) for e in events]
|
|
66
|
-
await self._eventstore.append_async(stream_id, encoded_events, aggregate.state.state_version)
|
|
67
|
-
aggregate.state.state_version = events[-1].aggregate_version
|
|
68
|
-
aggregate.clear_pending_events()
|
|
69
|
-
return aggregate
|
|
70
|
-
|
|
71
|
-
async def _do_remove_async(self, id: TKey) -> None:
|
|
72
|
-
"""Removes the aggregate root with the specified key, if any"""
|
|
73
|
-
raise NotImplementedError("Event sourcing repositories do not support hard deletes")
|
|
74
|
-
|
|
75
|
-
async def _publish_domain_events(self, entity: TAggregate) -> None:
|
|
76
|
-
"""
|
|
77
|
-
Override base class event publishing for event-sourced aggregates.
|
|
78
|
-
|
|
79
|
-
Event sourcing repositories DO NOT publish events directly because:
|
|
80
|
-
1. Events are already persisted to the EventStore
|
|
81
|
-
2. ReadModelReconciliator subscribes to EventStore and publishes ALL events
|
|
82
|
-
3. Publishing here would cause DOUBLE PUBLISHING (once here, once from ReadModelReconciliator)
|
|
83
|
-
|
|
84
|
-
For event-sourced aggregates:
|
|
85
|
-
- Events are persisted to EventStore by _do_add_async/_do_update_async
|
|
86
|
-
- ReadModelReconciliator.on_event_record_stream_next_async() publishes via mediator
|
|
87
|
-
- This ensures single, reliable event publishing from the source of truth (EventStore)
|
|
88
|
-
|
|
89
|
-
State-based repositories still use the base class _publish_domain_events() correctly.
|
|
90
|
-
"""
|
|
91
|
-
# Do nothing - ReadModelReconciliator handles event publishing from EventStore
|
|
92
|
-
|
|
93
|
-
def _build_stream_id_for(self, aggregate_id: TKey):
|
|
94
|
-
"""Builds a new stream id for the specified aggregate"""
|
|
95
|
-
aggregate_name = self.__orig_class__.__args__[0].__name__
|
|
96
|
-
return f"{aggregate_name.lower()}-{aggregate_id}"
|
|
97
|
-
|
|
98
|
-
def _encode_event(self, e: DomainEvent):
|
|
99
|
-
"""Encodes a domain event into a new event descriptor"""
|
|
100
|
-
event_type = type(e).__name__.lower()
|
|
101
|
-
return EventDescriptor(event_type, e)
|
|
102
|
-
|
|
103
|
-
@staticmethod
|
|
104
|
-
def configure(builder: ApplicationBuilderBase, entity_type: type, key_type: type) -> ApplicationBuilderBase:
|
|
105
|
-
"""Configures the specified application to use an event sourcing based repository implementation to manage the specified type of entity"""
|
|
106
|
-
builder.services.try_add_singleton(
|
|
107
|
-
EventSourcingRepositoryOptions[entity_type, key_type],
|
|
108
|
-
singleton=EventSourcingRepositoryOptions[entity_type, key_type](),
|
|
109
|
-
)
|
|
110
|
-
builder.services.try_add_singleton(Repository[entity_type, key_type], EventSourcingRepository[entity_type, key_type])
|
|
111
|
-
return builder
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/coordination/abstractions.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/coordination/leader_election.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/coordination/redis_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/infrastructure/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/queries/generic/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/queries/generic/list_query.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/__init__.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/abstractions.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/data/resources/state_machine.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/dependency_injection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/eventing/cloud_events/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/extensions/mediator_extensions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/integration/cache_repository.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/integration/http_service_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/metrics_middleware.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/pipeline_behavior.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/mediation/tracing_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/observability/framework.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/serialization/abstractions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.14 → neuroglia_python-0.6.15}/src/neuroglia/validation/business_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|