neuroglia-python 0.6.15__tar.gz → 0.6.16__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.15 → neuroglia_python-0.6.16}/PKG-INFO +1 -1
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/pyproject.toml +1 -1
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/__init__.py +1 -1
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +2 -2
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +144 -39
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/LICENSE +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/README.md +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/application/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/application/background_scheduler.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/coordination/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/coordination/abstractions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/coordination/leader_election.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/coordination/redis_backend.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/core/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/core/module_loader.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/core/operation_result.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/core/problem_details.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/core/type_extensions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/core/type_finder.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/core/type_registry.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/abstractions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/exceptions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/queries/generic/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/queries/generic/list_query.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/queryable.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/abstractions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/controller.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/state_machine.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/watcher.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/unit_of_work.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/dependency_injection/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/dependency_injection/service_provider.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/eventing/tracing.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/expressions/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/extensions/mediator_extensions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/hosting/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/hosting/abstractions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/hosting/configuration/data_access_layer.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/hosting/web.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/integration/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/integration/cache_repository.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/integration/http_service_client.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/integration/models.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/logging/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/logging/logger.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mapping/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mapping/mapper.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/mediator.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/metrics_middleware.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/simple.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/tracing_middleware.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mvc/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mvc/controller_base.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/observability/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/observability/framework.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/observability/logging.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/observability/metrics.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/observability/otel_sdk.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/observability/settings.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/observability/tracing.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/py.typed +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/reactive/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/reactive/rx_async.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/serialization/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/serialization/abstractions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/serialization/json.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/utils/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/utils/camel_model.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/utils/case_conversion.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/validation/__init__.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/validation/business_rules.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/validation/exceptions.py +0 -0
- {neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/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.16"
|
|
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"
|
|
@@ -109,11 +109,11 @@ class AckableEventRecord(EventRecord):
|
|
|
109
109
|
|
|
110
110
|
async def ack_async(self) -> None:
|
|
111
111
|
"""Acks the event record"""
|
|
112
|
-
self._ack_delegate()
|
|
112
|
+
await self._ack_delegate()
|
|
113
113
|
|
|
114
114
|
async def nack_async(self) -> None:
|
|
115
115
|
"""Nacks the event record"""
|
|
116
|
-
self._nack_delegate()
|
|
116
|
+
await self._nack_delegate()
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
class StreamReadDirection(Enum):
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import inspect
|
|
2
3
|
import logging
|
|
3
4
|
import sys
|
|
4
|
-
import threading
|
|
5
5
|
from typing import Any, Dict, Optional
|
|
6
6
|
|
|
7
7
|
import rx
|
|
8
|
-
from esdbclient import
|
|
8
|
+
from esdbclient import AsyncioEventStoreDBClient as AsyncClientFactory
|
|
9
|
+
from esdbclient import NewEvent, RecordedEvent, StreamState
|
|
9
10
|
from esdbclient.exceptions import AlreadyExists
|
|
10
11
|
from rx.core.observable.observable import Observable
|
|
11
12
|
from rx.disposable.disposable import Disposable
|
|
@@ -35,7 +36,10 @@ class ESEventStore(EventStore):
|
|
|
35
36
|
_eventstore_options: EventStoreOptions
|
|
36
37
|
""" Gets the options used to configure the EventStore """
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
_connection_string: str
|
|
40
|
+
""" Gets the connection string for EventStoreDB """
|
|
41
|
+
|
|
42
|
+
_eventstore_client: Optional[Any] # _AsyncioEventStoreDBClient
|
|
39
43
|
""" Gets the service used to interact with the EventStore DB"""
|
|
40
44
|
|
|
41
45
|
_serializer: JsonSerializer
|
|
@@ -44,17 +48,34 @@ class ESEventStore(EventStore):
|
|
|
44
48
|
def __init__(
|
|
45
49
|
self,
|
|
46
50
|
options: EventStoreOptions,
|
|
47
|
-
|
|
51
|
+
connection_string_or_client: str | Any, # Can be connection string or pre-initialized client (for testing)
|
|
48
52
|
serializer: JsonSerializer,
|
|
49
53
|
):
|
|
50
54
|
self._eventstore_options = options
|
|
51
|
-
self._eventstore_client = eventstore_client
|
|
52
55
|
self._serializer = serializer
|
|
53
56
|
|
|
57
|
+
# Check if we got a connection string or an already-initialized client
|
|
58
|
+
if isinstance(connection_string_or_client, str):
|
|
59
|
+
self._connection_string = connection_string_or_client
|
|
60
|
+
self._eventstore_client = None # Will be lazily initialized
|
|
61
|
+
else:
|
|
62
|
+
# Pre-initialized client (typically for testing)
|
|
63
|
+
self._connection_string = None
|
|
64
|
+
self._eventstore_client = connection_string_or_client
|
|
65
|
+
|
|
66
|
+
async def _ensure_client(self) -> Any:
|
|
67
|
+
"""Lazily initialize the async EventStoreDB client on first use"""
|
|
68
|
+
if self._eventstore_client is None:
|
|
69
|
+
if self._connection_string is None:
|
|
70
|
+
raise RuntimeError("Neither connection string nor client provided")
|
|
71
|
+
self._eventstore_client = await AsyncClientFactory(uri=self._connection_string)
|
|
72
|
+
return self._eventstore_client
|
|
73
|
+
|
|
54
74
|
async def contains_async(self, stream_id: str) -> bool:
|
|
55
75
|
return await self.get_async(stream_id) is not None
|
|
56
76
|
|
|
57
77
|
async def append_async(self, stream_id: str, events: list[EventDescriptor], expected_version: Optional[int] = None):
|
|
78
|
+
client = await self._ensure_client()
|
|
58
79
|
if expected_version is not None:
|
|
59
80
|
expected_version = expected_version - 1
|
|
60
81
|
stream_name = self._get_stream_name(stream_id)
|
|
@@ -70,31 +91,34 @@ class ESEventStore(EventStore):
|
|
|
70
91
|
metadata=bytes(self._serializer.serialize(self._build_event_metadata(e.data, e.metadata))),
|
|
71
92
|
)
|
|
72
93
|
)
|
|
73
|
-
|
|
94
|
+
await client.append_to_stream(stream_name=stream_name, current_version=stream_state, events=formatted_events)
|
|
74
95
|
|
|
75
96
|
async def get_async(self, stream_id: str) -> Optional[StreamDescriptor]:
|
|
97
|
+
client = await self._ensure_client()
|
|
76
98
|
stream_name = self._get_stream_name(stream_id)
|
|
77
|
-
metadata, metadata_version =
|
|
99
|
+
metadata, metadata_version = await client.get_stream_metadata(stream_name)
|
|
78
100
|
if metadata_version == StreamState.NO_STREAM:
|
|
79
101
|
return None
|
|
80
102
|
truncate_before = metadata.get("$tb")
|
|
81
103
|
offset = 0 if truncate_before is None else truncate_before
|
|
82
|
-
read_response =
|
|
104
|
+
read_response = client.read_stream(
|
|
83
105
|
stream_name=stream_name,
|
|
84
106
|
stream_position=offset,
|
|
85
107
|
backwards=False,
|
|
86
108
|
resolve_links=True,
|
|
87
109
|
limit=1,
|
|
88
110
|
)
|
|
89
|
-
recorded_events =
|
|
90
|
-
read_response =
|
|
111
|
+
recorded_events = [event async for event in read_response]
|
|
112
|
+
read_response = client.read_stream(
|
|
91
113
|
stream_name=stream_name,
|
|
92
114
|
stream_position=offset,
|
|
93
115
|
backwards=True,
|
|
94
116
|
resolve_links=True,
|
|
95
117
|
limit=1,
|
|
96
118
|
)
|
|
97
|
-
recorded_events =
|
|
119
|
+
recorded_events = [event async for event in read_response]
|
|
120
|
+
if not recorded_events:
|
|
121
|
+
return None
|
|
98
122
|
last_event = recorded_events[0]
|
|
99
123
|
return StreamDescriptor(stream_id, last_event.stream_position, None, None) # todo: esdbclient does not provide timestamps
|
|
100
124
|
|
|
@@ -105,15 +129,16 @@ class ESEventStore(EventStore):
|
|
|
105
129
|
offset: int,
|
|
106
130
|
length: Optional[int] = None,
|
|
107
131
|
) -> list[EventRecord]:
|
|
132
|
+
client = await self._ensure_client()
|
|
108
133
|
stream_name = self._get_stream_name(stream_id)
|
|
109
|
-
read_response =
|
|
134
|
+
read_response = client.read_stream(
|
|
110
135
|
stream_name=stream_name,
|
|
111
136
|
stream_position=offset,
|
|
112
137
|
backwards=True if read_direction == StreamReadDirection.BACKWARDS else False,
|
|
113
138
|
resolve_links=True,
|
|
114
139
|
limit=sys.maxsize if length is None else length,
|
|
115
140
|
)
|
|
116
|
-
recorded_events =
|
|
141
|
+
recorded_events = [event async for event in read_response]
|
|
117
142
|
return [self._decode_recorded_event(stream_id, recorded_event) for recorded_event in recorded_events]
|
|
118
143
|
|
|
119
144
|
async def observe_async(
|
|
@@ -122,36 +147,42 @@ class ESEventStore(EventStore):
|
|
|
122
147
|
consumer_group: Optional[str] = None,
|
|
123
148
|
offset: Optional[int] = None,
|
|
124
149
|
) -> Observable:
|
|
150
|
+
client = await self._ensure_client()
|
|
125
151
|
if stream_id is None:
|
|
126
152
|
raise ValueError("stream_id cannot be None")
|
|
127
153
|
stream_name = self._get_stream_name(stream_id)
|
|
128
154
|
subscription = None
|
|
129
155
|
if consumer_group is None:
|
|
130
156
|
stream_position = offset if offset is not None else 0
|
|
131
|
-
subscription =
|
|
157
|
+
subscription = client.subscribe_to_stream(stream_name=stream_name, resolve_links=True, stream_position=stream_position)
|
|
132
158
|
else:
|
|
133
159
|
try:
|
|
134
|
-
|
|
160
|
+
await client.create_subscription_to_stream(
|
|
135
161
|
group_name=consumer_group,
|
|
136
162
|
stream_name=stream_name,
|
|
137
163
|
resolve_links=True,
|
|
138
164
|
consumer_strategy="RoundRobin",
|
|
165
|
+
# Checkpoint configuration for reliable ACK delivery
|
|
139
166
|
min_checkpoint_count=1,
|
|
140
167
|
max_checkpoint_count=1,
|
|
168
|
+
# Set message timeout to 60s (default is 30s)
|
|
169
|
+
message_timeout=60.0,
|
|
141
170
|
)
|
|
142
171
|
except AlreadyExists:
|
|
143
172
|
pass
|
|
144
|
-
subscription =
|
|
173
|
+
subscription = client.read_subscription_to_stream(
|
|
145
174
|
group_name=consumer_group,
|
|
146
175
|
stream_name=stream_name,
|
|
147
176
|
)
|
|
148
177
|
subject = Subject()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
# Start async task to consume events
|
|
179
|
+
asyncio.create_task(self._consume_events_async(stream_id, subject, subscription))
|
|
180
|
+
|
|
181
|
+
async def stop_subscription():
|
|
182
|
+
if subscription is not None:
|
|
183
|
+
await subscription.stop()
|
|
184
|
+
|
|
185
|
+
return rx.using(lambda: Disposable(lambda: asyncio.create_task(stop_subscription())), lambda s: subject)
|
|
155
186
|
|
|
156
187
|
def _build_event_metadata(self, e: DomainEvent, additional_metadata: Optional[Any]) -> dict[str, Any]:
|
|
157
188
|
module = inspect.getmodule(e)
|
|
@@ -200,33 +231,96 @@ class ESEventStore(EventStore):
|
|
|
200
231
|
"""Converts the specified stream id to a qualified stream id, which is prefixed with the current database name, if any"""
|
|
201
232
|
return stream_id if self._eventstore_options.database_name is None or stream_id.startswith("$ce-") else f"{self._eventstore_options.database_name}-{stream_id}"
|
|
202
233
|
|
|
203
|
-
def _consume_events_async(self, stream_id: str, subject: Subject, subscription):
|
|
204
|
-
"""
|
|
234
|
+
async def _consume_events_async(self, stream_id: str, subject: Subject, subscription):
|
|
235
|
+
"""
|
|
236
|
+
Asynchronously enumerate events returned by a subscription using native async iteration.
|
|
237
|
+
|
|
238
|
+
With AsyncioEventStoreDBClient, subscriptions are async generators that properly handle
|
|
239
|
+
gRPC bidirectional streaming. ACKs are sent immediately without queuing issues.
|
|
240
|
+
|
|
241
|
+
For persistent subscriptions, events are wrapped in AckableEventRecord with async
|
|
242
|
+
ack/nack delegates that directly call the subscription's async methods.
|
|
243
|
+
"""
|
|
244
|
+
# Check if this is a persistent subscription (has ack/nack methods)
|
|
245
|
+
is_persistent = hasattr(subscription, "ack") and hasattr(subscription, "nack")
|
|
246
|
+
|
|
205
247
|
try:
|
|
206
248
|
e: RecordedEvent
|
|
207
|
-
for e in subscription:
|
|
249
|
+
async for e in subscription:
|
|
250
|
+
# Skip tombstone events (streams prefixed with $$)
|
|
251
|
+
# Tombstones are created by EventStoreDB when streams are hard-deleted
|
|
252
|
+
if e.stream_name.startswith("$$"):
|
|
253
|
+
logging.debug(f"Skipping tombstone event from stream: {e.stream_name}")
|
|
254
|
+
# Acknowledge tombstone to continue processing
|
|
255
|
+
if is_persistent:
|
|
256
|
+
await subscription.ack(e.id)
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Skip system event types (prefixed with $)
|
|
260
|
+
# System events are internal EventStoreDB metadata events
|
|
261
|
+
if e.type.startswith("$"):
|
|
262
|
+
logging.debug(f"Skipping system event type '{e.type}' from stream: {e.stream_name}")
|
|
263
|
+
# Acknowledge system event to continue processing
|
|
264
|
+
if is_persistent:
|
|
265
|
+
await subscription.ack(e.id)
|
|
266
|
+
continue
|
|
267
|
+
|
|
208
268
|
try:
|
|
209
269
|
decoded_event = self._decode_recorded_event(stream_id, e)
|
|
210
270
|
except Exception as ex:
|
|
211
|
-
logging.
|
|
212
|
-
#
|
|
213
|
-
if
|
|
214
|
-
subscription.
|
|
215
|
-
|
|
271
|
+
logging.warning(f"Could not decode event with offset '{e.stream_position}' from stream '{e.stream_name}': {ex}")
|
|
272
|
+
# Acknowledge failed decode to continue processing (don't park/retry invalid events)
|
|
273
|
+
if is_persistent:
|
|
274
|
+
await subscription.ack(e.id)
|
|
275
|
+
continue
|
|
216
276
|
|
|
217
277
|
# Convert to AckableEventRecord if subscription supports ack/nack
|
|
218
|
-
if
|
|
278
|
+
if is_persistent:
|
|
219
279
|
event_id = e.id
|
|
220
|
-
|
|
280
|
+
|
|
281
|
+
async def ack_delegate(eid=event_id, sub=subscription):
|
|
282
|
+
"""
|
|
283
|
+
Acknowledge event to EventStoreDB persistent subscription.
|
|
284
|
+
|
|
285
|
+
With async API, ACKs are sent immediately through the gRPC stream.
|
|
286
|
+
"""
|
|
287
|
+
await sub.ack(eid)
|
|
288
|
+
logging.debug(f"ACK sent for event: {eid}")
|
|
289
|
+
|
|
290
|
+
async def nack_delegate(eid=event_id, sub=subscription, action="retry"):
|
|
291
|
+
"""
|
|
292
|
+
Negative acknowledge event with specified action.
|
|
293
|
+
|
|
294
|
+
Actions:
|
|
295
|
+
- "retry": Retry event delivery (default)
|
|
296
|
+
- "park": Move event to parked messages
|
|
297
|
+
- "skip": Skip event without retry
|
|
298
|
+
"""
|
|
299
|
+
await sub.nack(eid, action=action)
|
|
300
|
+
logging.debug(f"NACK sent for event: {eid} with action: {action}")
|
|
301
|
+
|
|
302
|
+
ackable_event = AckableEventRecord(
|
|
303
|
+
stream_id=decoded_event.stream_id,
|
|
304
|
+
id=decoded_event.id,
|
|
305
|
+
offset=decoded_event.offset,
|
|
306
|
+
position=decoded_event.position,
|
|
307
|
+
timestamp=decoded_event.timestamp,
|
|
308
|
+
type=decoded_event.type,
|
|
309
|
+
data=decoded_event.data,
|
|
310
|
+
metadata=decoded_event.metadata,
|
|
311
|
+
replayed=decoded_event.replayed,
|
|
312
|
+
_ack_delegate=ack_delegate,
|
|
313
|
+
_nack_delegate=nack_delegate,
|
|
314
|
+
)
|
|
221
315
|
subject.on_next(ackable_event)
|
|
222
316
|
else:
|
|
223
|
-
# No ack/nack support, send regular EventRecord
|
|
317
|
+
# No ack/nack support (catchup subscription), send regular EventRecord
|
|
224
318
|
subject.on_next(decoded_event)
|
|
225
319
|
|
|
226
320
|
subject.on_completed()
|
|
227
321
|
except Exception as ex:
|
|
228
|
-
logging.error(f"An exception occurred while consuming events from stream '{stream_id}'
|
|
229
|
-
subscription.stop()
|
|
322
|
+
logging.error(f"An exception occurred while consuming events from stream '{stream_id}': {ex}")
|
|
323
|
+
await subscription.stop()
|
|
230
324
|
|
|
231
325
|
async def delete_async(self, stream_id: str) -> None:
|
|
232
326
|
"""
|
|
@@ -241,10 +335,11 @@ class ESEventStore(EventStore):
|
|
|
241
335
|
Raises:
|
|
242
336
|
Exception: If the stream does not exist or deletion fails
|
|
243
337
|
"""
|
|
338
|
+
client = await self._ensure_client()
|
|
244
339
|
stream_name = self._get_stream_name(stream_id)
|
|
245
340
|
try:
|
|
246
341
|
# Delete the stream from EventStoreDB
|
|
247
|
-
|
|
342
|
+
await client.delete_stream(stream_name=stream_name, current_version=StreamState.ANY)
|
|
248
343
|
except Exception as ex:
|
|
249
344
|
raise Exception(f"Failed to delete stream '{stream_name}': {ex}") from ex
|
|
250
345
|
|
|
@@ -253,14 +348,24 @@ class ESEventStore(EventStore):
|
|
|
253
348
|
"""Registers and configures an EventStore implementation of the EventStore class.
|
|
254
349
|
|
|
255
350
|
Args:
|
|
256
|
-
|
|
351
|
+
builder: The application builder to configure
|
|
352
|
+
options: EventStore configuration options
|
|
257
353
|
"""
|
|
258
354
|
connection_string_name = "eventstore"
|
|
259
355
|
connection_string = builder.settings.connection_strings.get(connection_string_name, None)
|
|
260
356
|
if connection_string is None:
|
|
261
357
|
raise Exception(f"Missing '{connection_string_name}' connection string")
|
|
358
|
+
|
|
359
|
+
# Register dependencies
|
|
262
360
|
builder.services.try_add_singleton(Aggregator)
|
|
263
361
|
builder.services.try_add_singleton(EventStoreOptions, singleton=options)
|
|
264
|
-
|
|
265
|
-
|
|
362
|
+
|
|
363
|
+
# Factory function to create ESEventStore with connection string
|
|
364
|
+
def create_event_store(service_provider) -> ESEventStore:
|
|
365
|
+
from neuroglia.serialization.json import JsonSerializer
|
|
366
|
+
|
|
367
|
+
serializer = service_provider.get_service(JsonSerializer)
|
|
368
|
+
return ESEventStore(options, connection_string, serializer)
|
|
369
|
+
|
|
370
|
+
builder.services.try_add_singleton(EventStore, factory=create_event_store)
|
|
266
371
|
return builder
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/coordination/abstractions.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/coordination/leader_election.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/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.15 → neuroglia_python-0.6.16}/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
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/queries/generic/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/queries/generic/list_query.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/__init__.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/abstractions.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/data/resources/state_machine.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/dependency_injection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/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.15 → neuroglia_python-0.6.16}/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.15 → neuroglia_python-0.6.16}/src/neuroglia/integration/cache_repository.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/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.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/metrics_middleware.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/pipeline_behavior.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/src/neuroglia/mediation/tracing_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.15 → neuroglia_python-0.6.16}/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.15 → neuroglia_python-0.6.16}/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.15 → neuroglia_python-0.6.16}/src/neuroglia/validation/business_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|