neuroglia-python 0.6.18__tar.gz → 0.6.21__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.18 → neuroglia_python-0.6.21}/PKG-INFO +1 -1
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/pyproject.toml +1 -1
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +8 -6
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +4 -2
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +12 -4
- neuroglia_python-0.6.21/src/neuroglia/data/infrastructure/event_sourcing/patches.py +74 -0
- neuroglia_python-0.6.21/src/neuroglia/hosting/configuration/data_access_layer.py +169 -0
- neuroglia_python-0.6.18/src/neuroglia/hosting/configuration/data_access_layer.py +0 -57
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/LICENSE +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/README.md +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/application/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/application/background_scheduler.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/coordination/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/coordination/abstractions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/coordination/leader_election.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/coordination/redis_backend.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/core/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/core/module_loader.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/core/operation_result.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/core/problem_details.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/core/type_extensions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/core/type_finder.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/core/type_registry.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/abstractions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/exceptions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/queries/generic/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/queries/generic/list_query.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/queryable.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/abstractions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/controller.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/state_machine.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/watcher.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/unit_of_work.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/dependency_injection/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/dependency_injection/service_provider.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/eventing/tracing.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/expressions/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/extensions/mediator_extensions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/hosting/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/hosting/abstractions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/hosting/web.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/integration/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/integration/cache_repository.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/integration/http_service_client.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/integration/models.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/logging/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/logging/logger.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mapping/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mapping/mapper.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/mediator.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/metrics_middleware.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/simple.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/tracing_middleware.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mvc/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mvc/controller_base.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/observability/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/observability/framework.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/observability/logging.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/observability/metrics.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/observability/otel_sdk.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/observability/settings.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/observability/tracing.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/py.typed +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/reactive/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/reactive/rx_async.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/serialization/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/serialization/abstractions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/serialization/json.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/utils/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/utils/camel_model.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/utils/case_conversion.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/validation/__init__.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/validation/business_rules.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/validation/exceptions.py +0 -0
- {neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/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.21"
|
|
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"
|
|
@@ -4,14 +4,16 @@ Event sourcing infrastructure for Neuroglia.
|
|
|
4
4
|
Provides event store implementations and aggregate root support.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
# Apply runtime patches for third-party library bugs
|
|
8
|
+
from . import patches # noqa: F401
|
|
7
9
|
from .abstractions import (
|
|
8
|
-
|
|
9
|
-
EventRecord,
|
|
10
|
+
Aggregator,
|
|
10
11
|
EventDescriptor,
|
|
12
|
+
EventRecord,
|
|
13
|
+
EventStore,
|
|
14
|
+
EventStoreOptions,
|
|
11
15
|
StreamDescriptor,
|
|
12
16
|
StreamReadDirection,
|
|
13
|
-
EventStoreOptions,
|
|
14
|
-
Aggregator
|
|
15
17
|
)
|
|
16
18
|
from .event_sourcing_repository import EventSourcingRepository
|
|
17
19
|
|
|
@@ -19,9 +21,9 @@ __all__ = [
|
|
|
19
21
|
"EventStore",
|
|
20
22
|
"EventSourcingRepository",
|
|
21
23
|
"EventRecord",
|
|
22
|
-
"EventDescriptor",
|
|
24
|
+
"EventDescriptor",
|
|
23
25
|
"StreamDescriptor",
|
|
24
26
|
"StreamReadDirection",
|
|
25
27
|
"EventStoreOptions",
|
|
26
28
|
"Aggregator",
|
|
27
|
-
]
|
|
29
|
+
]
|
|
@@ -127,7 +127,8 @@ class EventSourcingRepository(Generic[TAggregate, TKey], Repository[TAggregate,
|
|
|
127
127
|
encoded_events = [self._encode_event(e) for e in events]
|
|
128
128
|
await self._eventstore.append_async(stream_id, encoded_events)
|
|
129
129
|
aggregate.state.state_version = events[-1].aggregate_version
|
|
130
|
-
|
|
130
|
+
# DON'T clear pending events here - let base class do it after publishing!
|
|
131
|
+
# aggregate.clear_pending_events()
|
|
131
132
|
return aggregate
|
|
132
133
|
|
|
133
134
|
async def _do_update_async(self, aggregate: TAggregate) -> TAggregate:
|
|
@@ -139,7 +140,8 @@ class EventSourcingRepository(Generic[TAggregate, TKey], Repository[TAggregate,
|
|
|
139
140
|
encoded_events = [self._encode_event(e) for e in events]
|
|
140
141
|
await self._eventstore.append_async(stream_id, encoded_events, aggregate.state.state_version)
|
|
141
142
|
aggregate.state.state_version = events[-1].aggregate_version
|
|
142
|
-
|
|
143
|
+
# DON'T clear pending events here - let base class do it after publishing!
|
|
144
|
+
# aggregate.clear_pending_events()
|
|
143
145
|
return aggregate
|
|
144
146
|
|
|
145
147
|
async def _do_remove_async(self, id: TKey) -> None:
|
|
@@ -253,7 +253,9 @@ class ESEventStore(EventStore):
|
|
|
253
253
|
logging.debug(f"Skipping tombstone event from stream: {e.stream_name}")
|
|
254
254
|
# Acknowledge tombstone to continue processing
|
|
255
255
|
if is_persistent:
|
|
256
|
-
|
|
256
|
+
# Use ack_id for resolved link events, fall back to id
|
|
257
|
+
ack_id = getattr(e, "ack_id", e.id)
|
|
258
|
+
await subscription.ack(ack_id)
|
|
257
259
|
continue
|
|
258
260
|
|
|
259
261
|
# Skip system event types (prefixed with $)
|
|
@@ -262,7 +264,9 @@ class ESEventStore(EventStore):
|
|
|
262
264
|
logging.debug(f"Skipping system event type '{e.type}' from stream: {e.stream_name}")
|
|
263
265
|
# Acknowledge system event to continue processing
|
|
264
266
|
if is_persistent:
|
|
265
|
-
|
|
267
|
+
# Use ack_id for resolved link events, fall back to id
|
|
268
|
+
ack_id = getattr(e, "ack_id", e.id)
|
|
269
|
+
await subscription.ack(ack_id)
|
|
266
270
|
continue
|
|
267
271
|
|
|
268
272
|
try:
|
|
@@ -271,12 +275,16 @@ class ESEventStore(EventStore):
|
|
|
271
275
|
logging.warning(f"Could not decode event with offset '{e.stream_position}' from stream '{e.stream_name}': {ex}")
|
|
272
276
|
# Acknowledge failed decode to continue processing (don't park/retry invalid events)
|
|
273
277
|
if is_persistent:
|
|
274
|
-
|
|
278
|
+
# Use ack_id for resolved link events, fall back to id
|
|
279
|
+
ack_id = getattr(e, "ack_id", e.id)
|
|
280
|
+
await subscription.ack(ack_id)
|
|
275
281
|
continue
|
|
276
282
|
|
|
277
283
|
# Convert to AckableEventRecord if subscription supports ack/nack
|
|
278
284
|
if is_persistent:
|
|
279
|
-
|
|
285
|
+
# Use ack_id for resolved link events (resolveLinktos=true), fall back to id
|
|
286
|
+
# This is critical for persistent subscriptions to category streams ($ce-*)
|
|
287
|
+
event_id = getattr(e, "ack_id", e.id)
|
|
280
288
|
|
|
281
289
|
async def ack_delegate(eid=event_id, sub=subscription):
|
|
282
290
|
"""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime patches for third-party library bugs affecting Neuroglia event sourcing.
|
|
3
|
+
|
|
4
|
+
This module contains monkey-patches that fix critical bugs in dependencies.
|
|
5
|
+
Patches are applied automatically when this module is imported.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def patch_esdbclient_async_subscription_id():
|
|
14
|
+
"""
|
|
15
|
+
Fix esdbclient bug: AsyncPersistentSubscription.init() doesn't propagate
|
|
16
|
+
subscription_id to _read_reqs, causing ACKs to be sent with empty subscription ID.
|
|
17
|
+
|
|
18
|
+
**Bug Description**:
|
|
19
|
+
The sync version (PersistentSubscription.__init__) has this line:
|
|
20
|
+
self._read_reqs.subscription_id = subscription_id.encode()
|
|
21
|
+
|
|
22
|
+
But the async version (AsyncPersistentSubscription.init) is missing it!
|
|
23
|
+
This causes ACKs to fail silently because EventStoreDB doesn't know which
|
|
24
|
+
subscription the ACK is for.
|
|
25
|
+
|
|
26
|
+
**Symptoms**:
|
|
27
|
+
- Events are redelivered every message_timeout seconds despite being processed
|
|
28
|
+
- Checkpoint never advances in EventStoreDB
|
|
29
|
+
- Events eventually get parked after maxRetryCount attempts
|
|
30
|
+
- Read models may process the same event multiple times
|
|
31
|
+
|
|
32
|
+
**Root Cause**:
|
|
33
|
+
BaseSubscriptionReadReqs.__init__() initializes subscription_id = b"" (empty bytes).
|
|
34
|
+
The sync version overwrites this with the correct value in __init__, but the async
|
|
35
|
+
version only sets self._subscription_id without propagating it to _read_reqs.
|
|
36
|
+
|
|
37
|
+
**Affected Versions**:
|
|
38
|
+
- esdbclient: 1.1.7 (and likely all earlier versions with async support)
|
|
39
|
+
- kurrentdbclient: Likely affected (same codebase, rebranded)
|
|
40
|
+
|
|
41
|
+
**Upstream Issue**:
|
|
42
|
+
Bug report to be filed with esdbclient/kurrentdbclient maintainers.
|
|
43
|
+
|
|
44
|
+
**Verification**:
|
|
45
|
+
After applying this patch, check EventStoreDB admin UI:
|
|
46
|
+
- Navigate to Persistent Subscriptions → your subscription
|
|
47
|
+
- lastCheckpointedEventPosition should advance after processing events
|
|
48
|
+
- totalInFlightMessages should decrease after successful ACKs
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
from esdbclient.persistent import (
|
|
52
|
+
AsyncPersistentSubscription, # type: ignore[import-not-found]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
original_init = AsyncPersistentSubscription.init
|
|
56
|
+
|
|
57
|
+
async def patched_init(self) -> None:
|
|
58
|
+
await original_init(self)
|
|
59
|
+
# Propagate subscription_id to _read_reqs (missing in esdbclient async version)
|
|
60
|
+
if hasattr(self, "_subscription_id") and hasattr(self, "_read_reqs"):
|
|
61
|
+
self._read_reqs.subscription_id = self._subscription_id.encode()
|
|
62
|
+
log.debug(f"🔧 Patched: propagated subscription_id to _read_reqs: {self._subscription_id}")
|
|
63
|
+
|
|
64
|
+
AsyncPersistentSubscription.init = patched_init
|
|
65
|
+
log.info("🔧 Patched esdbclient AsyncPersistentSubscription.init() to propagate subscription_id")
|
|
66
|
+
|
|
67
|
+
except ImportError:
|
|
68
|
+
log.debug("esdbclient not installed, skipping AsyncPersistentSubscription patch")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
log.warning(f"Failed to apply esdbclient AsyncPersistentSubscription patch: {e}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Auto-apply patches when this module is imported
|
|
74
|
+
patch_esdbclient_async_subscription_id()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
from neuroglia.core import ModuleLoader, TypeFinder
|
|
6
|
+
from neuroglia.data.abstractions import AggregateRoot
|
|
7
|
+
from neuroglia.data.infrastructure.event_sourcing.read_model_reconciliator import (
|
|
8
|
+
ReadModelConciliationOptions,
|
|
9
|
+
ReadModelReconciliator,
|
|
10
|
+
)
|
|
11
|
+
from neuroglia.data.queries.generic import GetByIdQueryHandler, ListQueryHandler
|
|
12
|
+
from neuroglia.hosting.abstractions import ApplicationBuilderBase, HostedService
|
|
13
|
+
from neuroglia.mediation.mediator import RequestHandler
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from neuroglia.data.infrastructure.event_sourcing.event_sourcing_repository import (
|
|
17
|
+
EventSourcingRepositoryOptions,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DataAccessLayer:
|
|
22
|
+
class WriteModel:
|
|
23
|
+
"""Represents a helper class used to configure an application's Write Model DAL
|
|
24
|
+
|
|
25
|
+
Supports two configuration patterns:
|
|
26
|
+
1. Simplified: Pass options directly to constructor
|
|
27
|
+
2. Custom: Pass custom repository_setup function to configure()
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
# Simple configuration with default options
|
|
31
|
+
DataAccessLayer.WriteModel().configure(builder, ["domain.entities"])
|
|
32
|
+
|
|
33
|
+
# With custom delete mode
|
|
34
|
+
from neuroglia.data.infrastructure.event_sourcing.abstractions import DeleteMode
|
|
35
|
+
from neuroglia.data.infrastructure.event_sourcing.event_sourcing_repository import (
|
|
36
|
+
EventSourcingRepositoryOptions
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
DataAccessLayer.WriteModel(
|
|
40
|
+
options=EventSourcingRepositoryOptions(delete_mode=DeleteMode.HARD)
|
|
41
|
+
).configure(builder, ["domain.entities"])
|
|
42
|
+
|
|
43
|
+
# Custom factory (advanced, backwards compatible)
|
|
44
|
+
def custom_setup(builder_, entity_type, key_type):
|
|
45
|
+
# Custom configuration logic
|
|
46
|
+
pass
|
|
47
|
+
DataAccessLayer.WriteModel().configure(
|
|
48
|
+
builder, ["domain.entities"], custom_setup
|
|
49
|
+
)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, options: Optional["EventSourcingRepositoryOptions"] = None):
|
|
53
|
+
"""Initialize WriteModel configuration
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
options: Optional repository options (e.g., delete_mode).
|
|
57
|
+
If not provided, default options will be used.
|
|
58
|
+
"""
|
|
59
|
+
self._options = options
|
|
60
|
+
|
|
61
|
+
def configure(self, builder: ApplicationBuilderBase, modules: list[str], repository_setup: Optional[Callable[[ApplicationBuilderBase, type, type], None]] = None) -> ApplicationBuilderBase:
|
|
62
|
+
"""Configures the application's Write Model DAL, scanning for aggregate root types within the specified modules
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
builder (ApplicationBuilderBase): the application builder to configure
|
|
66
|
+
modules (List[str]): a list containing the names of the modules to scan for aggregate root types
|
|
67
|
+
repository_setup (Optional[Callable[[ApplicationBuilderBase, Type, Type], None]]):
|
|
68
|
+
Optional custom function to setup repositories. If provided, takes precedence over options.
|
|
69
|
+
If not provided, uses simplified configuration with options (if any).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ApplicationBuilderBase: The configured builder
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ImportError: If EventSourcingRepository cannot be imported
|
|
76
|
+
"""
|
|
77
|
+
# If custom setup provided, use it (backwards compatible)
|
|
78
|
+
if repository_setup is not None:
|
|
79
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
80
|
+
for aggregate_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and issubclass(cls, AggregateRoot) and not cls == AggregateRoot):
|
|
81
|
+
key_type = str # todo: reflect from DTO base type
|
|
82
|
+
repository_setup(builder, aggregate_type, key_type)
|
|
83
|
+
return builder
|
|
84
|
+
|
|
85
|
+
# Otherwise use simplified configuration with options
|
|
86
|
+
return self._configure_with_options(builder, modules)
|
|
87
|
+
|
|
88
|
+
def _configure_with_options(self, builder: ApplicationBuilderBase, modules: list[str]) -> ApplicationBuilderBase:
|
|
89
|
+
"""Configure repositories using simplified options pattern
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
builder: The application builder
|
|
93
|
+
modules: List of module names to scan for aggregates
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The configured builder
|
|
97
|
+
"""
|
|
98
|
+
from neuroglia.data.infrastructure.abstractions import Repository
|
|
99
|
+
from neuroglia.data.infrastructure.event_sourcing.abstractions import (
|
|
100
|
+
Aggregator,
|
|
101
|
+
EventStore,
|
|
102
|
+
)
|
|
103
|
+
from neuroglia.data.infrastructure.event_sourcing.event_sourcing_repository import (
|
|
104
|
+
EventSourcingRepository,
|
|
105
|
+
EventSourcingRepositoryOptions,
|
|
106
|
+
)
|
|
107
|
+
from neuroglia.dependency_injection import ServiceProvider
|
|
108
|
+
from neuroglia.mediation import Mediator
|
|
109
|
+
|
|
110
|
+
# Discover and configure each aggregate type
|
|
111
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
112
|
+
for aggregate_type in TypeFinder.get_types(
|
|
113
|
+
module,
|
|
114
|
+
lambda cls: inspect.isclass(cls) and issubclass(cls, AggregateRoot) and not cls == AggregateRoot,
|
|
115
|
+
):
|
|
116
|
+
key_type = str # todo: reflect from DTO base type
|
|
117
|
+
|
|
118
|
+
# Create type-specific options if global options provided
|
|
119
|
+
typed_options = None
|
|
120
|
+
if self._options:
|
|
121
|
+
typed_options = EventSourcingRepositoryOptions[aggregate_type, key_type]( # type: ignore
|
|
122
|
+
delete_mode=self._options.delete_mode,
|
|
123
|
+
soft_delete_method_name=self._options.soft_delete_method_name,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Create factory function with proper closure
|
|
127
|
+
def make_factory(et, kt, opts):
|
|
128
|
+
def repository_factory(sp: ServiceProvider):
|
|
129
|
+
return EventSourcingRepository[et, kt]( # type: ignore
|
|
130
|
+
eventstore=sp.get_required_service(EventStore),
|
|
131
|
+
aggregator=sp.get_required_service(Aggregator),
|
|
132
|
+
mediator=sp.get_service(Mediator),
|
|
133
|
+
options=opts,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return repository_factory
|
|
137
|
+
|
|
138
|
+
# Register repository with factory
|
|
139
|
+
builder.services.add_singleton(
|
|
140
|
+
Repository[aggregate_type, key_type], # type: ignore
|
|
141
|
+
implementation_factory=make_factory(aggregate_type, key_type, typed_options),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return builder
|
|
145
|
+
|
|
146
|
+
class ReadModel:
|
|
147
|
+
"""Represents an helper class used to configure an application's Read Model DAL"""
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def configure(builder: ApplicationBuilderBase, modules: list[str], repository_setup: Callable[[ApplicationBuilderBase, type, type], None]) -> ApplicationBuilderBase:
|
|
151
|
+
"""Configures the application's Read Model DAL, scanning for types marked with the 'queryable' decorator within the specified modules
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
builder (ApplicationBuilderBase): the application builder to configure
|
|
155
|
+
modules (List[str]): a list containing the names of the modules to scan for types decorated with 'queryable'
|
|
156
|
+
repository_setup (Callable[[ApplicationBuilderBase, Type, Type], None]): a function used to setup the repository for the specified entity and key types
|
|
157
|
+
"""
|
|
158
|
+
consumer_group = builder.settings.consumer_group
|
|
159
|
+
if not consumer_group:
|
|
160
|
+
raise ValueError("Cannot configure Read Model DAL: consumer group not specified in application settings")
|
|
161
|
+
builder.services.add_singleton(ReadModelConciliationOptions, singleton=ReadModelConciliationOptions(consumer_group))
|
|
162
|
+
builder.services.add_singleton(HostedService, ReadModelReconciliator)
|
|
163
|
+
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
164
|
+
for queryable_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
|
|
165
|
+
key_type = str # todo: reflect from DTO base type
|
|
166
|
+
repository_setup(builder, queryable_type, key_type)
|
|
167
|
+
builder.services.add_transient(RequestHandler, GetByIdQueryHandler[queryable_type, key_type]) # type: ignore
|
|
168
|
+
builder.services.add_transient(RequestHandler, ListQueryHandler[queryable_type, key_type]) # type: ignore
|
|
169
|
+
return builder
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
from collections.abc import Callable
|
|
3
|
-
|
|
4
|
-
from neuroglia.core import ModuleLoader, TypeFinder
|
|
5
|
-
from neuroglia.data.abstractions import AggregateRoot
|
|
6
|
-
from neuroglia.data.infrastructure.event_sourcing.read_model_reconciliator import (
|
|
7
|
-
ReadModelConciliationOptions,
|
|
8
|
-
ReadModelReconciliator,
|
|
9
|
-
)
|
|
10
|
-
from neuroglia.data.queries.generic import GetByIdQueryHandler, ListQueryHandler
|
|
11
|
-
from neuroglia.hosting.abstractions import ApplicationBuilderBase, HostedService
|
|
12
|
-
from neuroglia.mediation.mediator import RequestHandler
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class DataAccessLayer:
|
|
16
|
-
class WriteModel:
|
|
17
|
-
"""Represents an helper class used to configure an application's Write Model DAL"""
|
|
18
|
-
|
|
19
|
-
@staticmethod
|
|
20
|
-
def configure(builder: ApplicationBuilderBase, modules: list[str], repository_setup: Callable[[ApplicationBuilderBase, type, type], None]) -> ApplicationBuilderBase:
|
|
21
|
-
"""Configures the application's Write Model DAL, scanning for aggregate root types within the specified modules
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
builder (ApplicationBuilderBase): the application builder to configure
|
|
25
|
-
modules (List[str]): a list containing the names of the modules to scan for aggregate root types
|
|
26
|
-
repository_setup (Callable[[ApplicationBuilderBase, Type, Type], None]): a function used to setup the repository for the specified entity and key types
|
|
27
|
-
"""
|
|
28
|
-
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
29
|
-
for aggregate_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and issubclass(cls, AggregateRoot) and not cls == AggregateRoot):
|
|
30
|
-
key_type = str # todo: reflect from DTO base type
|
|
31
|
-
repository_setup(builder, aggregate_type, key_type)
|
|
32
|
-
return builder
|
|
33
|
-
|
|
34
|
-
class ReadModel:
|
|
35
|
-
"""Represents an helper class used to configure an application's Read Model DAL"""
|
|
36
|
-
|
|
37
|
-
@staticmethod
|
|
38
|
-
def configure(builder: ApplicationBuilderBase, modules: list[str], repository_setup: Callable[[ApplicationBuilderBase, type, type], None]) -> ApplicationBuilderBase:
|
|
39
|
-
"""Configures the application's Read Model DAL, scanning for types marked with the 'queryable' decorator within the specified modules
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
builder (ApplicationBuilderBase): the application builder to configure
|
|
43
|
-
modules (List[str]): a list containing the names of the modules to scan for types decorated with 'queryable'
|
|
44
|
-
repository_setup (Callable[[ApplicationBuilderBase, Type, Type], None]): a function used to setup the repository for the specified entity and key types
|
|
45
|
-
"""
|
|
46
|
-
consumer_group = builder.settings.consumer_group
|
|
47
|
-
if not consumer_group:
|
|
48
|
-
raise ValueError("Cannot configure Read Model DAL: consumer group not specified in application settings")
|
|
49
|
-
builder.services.add_singleton(ReadModelConciliationOptions, singleton=ReadModelConciliationOptions(consumer_group))
|
|
50
|
-
builder.services.add_singleton(HostedService, ReadModelReconciliator)
|
|
51
|
-
for module in [ModuleLoader.load(module_name) for module_name in modules]:
|
|
52
|
-
for queryable_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
|
|
53
|
-
key_type = str # todo: reflect from DTO base type
|
|
54
|
-
repository_setup(builder, queryable_type, key_type)
|
|
55
|
-
builder.services.add_transient(RequestHandler, GetByIdQueryHandler[queryable_type, key_type])
|
|
56
|
-
builder.services.add_transient(RequestHandler, ListQueryHandler[queryable_type, key_type])
|
|
57
|
-
return builder
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/coordination/abstractions.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/coordination/leader_election.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/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.18 → neuroglia_python-0.6.21}/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
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/queries/generic/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/queries/generic/list_query.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/__init__.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/abstractions.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/data/resources/state_machine.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/dependency_injection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/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.18 → neuroglia_python-0.6.21}/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
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/integration/cache_repository.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/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.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/metrics_middleware.py
RENAMED
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/pipeline_behavior.py
RENAMED
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/src/neuroglia/mediation/tracing_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{neuroglia_python-0.6.18 → neuroglia_python-0.6.21}/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.18 → neuroglia_python-0.6.21}/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.18 → neuroglia_python-0.6.21}/src/neuroglia/validation/business_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|