neuroglia-python 0.7.2__tar.gz → 0.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/PKG-INFO +1 -1
  2. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/pyproject.toml +1 -1
  3. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/__init__.py +1 -1
  4. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/motor_query.py +89 -7
  5. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +1 -1
  6. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/hosting/configuration/data_access_layer.py +267 -125
  7. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/LICENSE +0 -0
  8. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/README.md +0 -0
  9. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/application/__init__.py +0 -0
  10. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/application/background_scheduler.py +0 -0
  11. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/coordination/__init__.py +0 -0
  12. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/coordination/abstractions.py +0 -0
  13. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/coordination/leader_election.py +0 -0
  14. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/coordination/redis_backend.py +0 -0
  15. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/core/__init__.py +0 -0
  16. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/core/module_loader.py +0 -0
  17. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/core/operation_result.py +0 -0
  18. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/core/problem_details.py +0 -0
  19. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/core/type_extensions.py +0 -0
  20. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/core/type_finder.py +0 -0
  21. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/core/type_registry.py +0 -0
  22. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/__init__.py +0 -0
  23. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/abstractions.py +0 -0
  24. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/exceptions.py +0 -0
  25. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  26. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  27. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  28. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
  29. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -0
  30. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  31. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -0
  32. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
  33. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  34. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  35. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  36. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  37. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
  38. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  39. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
  40. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  41. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  42. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  43. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  44. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  45. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  46. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  47. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  48. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  49. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  50. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  51. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/queryable.py +0 -0
  52. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/__init__.py +0 -0
  53. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/abstractions.py +0 -0
  54. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/controller.py +0 -0
  55. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  56. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  57. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  58. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/state_machine.py +0 -0
  59. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/watcher.py +0 -0
  60. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/data/unit_of_work.py +0 -0
  61. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/dependency_injection/__init__.py +0 -0
  62. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/dependency_injection/service_provider.py +0 -0
  63. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  64. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/__init__.py +0 -0
  65. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  66. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  67. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  68. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  69. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  70. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  71. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  72. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  73. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/eventing/tracing.py +0 -0
  74. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/expressions/__init__.py +0 -0
  75. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
  76. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  77. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  78. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  79. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/hosting/__init__.py +0 -0
  80. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/hosting/abstractions.py +0 -0
  81. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/hosting/web.py +0 -0
  82. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/integration/__init__.py +0 -0
  83. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/integration/cache_repository.py +0 -0
  84. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/integration/http_service_client.py +0 -0
  85. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/integration/models.py +0 -0
  86. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/logging/__init__.py +0 -0
  87. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/logging/logger.py +0 -0
  88. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mapping/__init__.py +0 -0
  89. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mapping/mapper.py +0 -0
  90. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/__init__.py +0 -0
  91. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  92. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  93. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/mediator.py +0 -0
  94. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  95. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  96. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/simple.py +0 -0
  97. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  98. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mvc/__init__.py +0 -0
  99. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/mvc/controller_base.py +0 -0
  100. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/observability/__init__.py +0 -0
  101. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/observability/framework.py +0 -0
  102. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/observability/logging.py +0 -0
  103. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/observability/metrics.py +0 -0
  104. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/observability/otel_sdk.py +0 -0
  105. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/observability/settings.py +0 -0
  106. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/observability/tracing.py +0 -0
  107. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/py.typed +0 -0
  108. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/reactive/__init__.py +0 -0
  109. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/reactive/rx_async.py +0 -0
  110. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/serialization/__init__.py +0 -0
  111. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/serialization/abstractions.py +0 -0
  112. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  113. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/serialization/json.py +0 -0
  114. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/utils/__init__.py +0 -0
  115. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/utils/camel_model.py +0 -0
  116. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/utils/case_conversion.py +0 -0
  117. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/validation/__init__.py +0 -0
  118. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/validation/business_rules.py +0 -0
  119. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/validation/exceptions.py +0 -0
  120. {neuroglia_python-0.7.2 → neuroglia_python-0.7.3}/src/neuroglia/validation/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroglia-python
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Port from .NET to Python of the Neuroglia Framework
5
5
  License: Apache
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "neuroglia-python"
3
- version = "0.7.2"
3
+ version = "0.7.3"
4
4
  description = "Port from .NET to Python of the Neuroglia Framework"
5
5
  authors = ["Charles d'Avernas <charles.davernas@neuroglia.io>", "Bruno van de Werve <bvandewe@gmail.com>"]
6
6
  license = "Apache"
@@ -62,7 +62,7 @@ __all__ = [
62
62
  ]
63
63
 
64
64
  # Framework metadata
65
- __version__ = "0.7.2"
65
+ __version__ = "0.7.3"
66
66
  __author__ = "Neuroglia Team"
67
67
  __email__ = "team@neuroglia.io"
68
68
  __license__ = "Apache"
@@ -33,7 +33,7 @@ See Also:
33
33
 
34
34
  import ast
35
35
  from ast import NodeVisitor, expr
36
- from typing import Any, Generic, List, Optional
36
+ from typing import TYPE_CHECKING, Any, Generic, List, Optional
37
37
 
38
38
  import pymongo
39
39
  from motor.motor_asyncio import AsyncIOMotorCollection, AsyncIOMotorCursor
@@ -43,6 +43,9 @@ from neuroglia.expressions.javascript_expression_translator import (
43
43
  JavaScriptExpressionTranslator,
44
44
  )
45
45
 
46
+ if TYPE_CHECKING:
47
+ from neuroglia.serialization import JsonSerializer
48
+
46
49
 
47
50
  class MotorQuery(Generic[T], Queryable[T]):
48
51
  """
@@ -71,6 +74,62 @@ class MotorQuery(Generic[T], Queryable[T]):
71
74
  """
72
75
  super().__init__(query_provider, expression)
73
76
 
77
+ async def to_list_async(self) -> list[T]:
78
+ """
79
+ Asynchronously execute the query and return results as a list.
80
+
81
+ This is the async equivalent of to_list() for Motor queries.
82
+
83
+ Returns:
84
+ List of entities matching the query
85
+
86
+ Example:
87
+ ```python
88
+ products = await repo.query_async() \\
89
+ .where(lambda p: p.price > 10) \\
90
+ .order_by(lambda p: p.name) \\
91
+ .to_list_async()
92
+ ```
93
+ """
94
+ # Get the provider and ensure it's a MotorQueryProvider
95
+ provider = self.provider
96
+ if not isinstance(provider, MotorQueryProvider):
97
+ raise TypeError("MotorQuery requires a MotorQueryProvider")
98
+
99
+ # Execute the query asynchronously
100
+ return await provider.execute_async(self.expression, List)
101
+
102
+ async def first_or_default_async(self, predicate=None) -> Optional[T]:
103
+ """
104
+ Asynchronously get the first element matching the predicate, or None.
105
+
106
+ Args:
107
+ predicate: Optional filter predicate
108
+
109
+ Returns:
110
+ First matching entity or None
111
+
112
+ Example:
113
+ ```python
114
+ product = await repo.query_async() \\
115
+ .where(lambda p: p.id == "prod123") \\
116
+ .first_or_default_async()
117
+ ```
118
+ """
119
+ # Get the provider and ensure it's a MotorQueryProvider
120
+ provider = self.provider
121
+ if not isinstance(provider, MotorQueryProvider):
122
+ raise TypeError("MotorQuery requires a MotorQueryProvider")
123
+
124
+ # If predicate provided, apply it as a where clause
125
+ query = self
126
+ if predicate is not None:
127
+ query = self.where(predicate)
128
+
129
+ # Execute and get list, then return first or None
130
+ results = await provider.execute_async(query.expression, List)
131
+ return results[0] if results else None
132
+
74
133
 
75
134
  class MotorQueryBuilder(NodeVisitor):
76
135
  """
@@ -228,25 +287,28 @@ class MotorQueryProvider(QueryProvider):
228
287
  Attributes:
229
288
  _collection: Motor async collection to query
230
289
  _entity_type: Type of entities in the collection
290
+ _serializer: JSON serializer for entity conversion
231
291
 
232
292
  Example:
233
293
  ```python
234
- provider = MotorQueryProvider(motor_collection, Product)
294
+ provider = MotorQueryProvider(motor_collection, Product, serializer)
235
295
  query = provider.create_query(Product, expression)
236
296
  results = await provider.execute_async(expression, List[Product])
237
297
  ```
238
298
  """
239
299
 
240
- def __init__(self, collection: AsyncIOMotorCollection, entity_type: type):
300
+ def __init__(self, collection: AsyncIOMotorCollection, entity_type: type, serializer: "JsonSerializer"):
241
301
  """
242
302
  Initialize the Motor query provider.
243
303
 
244
304
  Args:
245
305
  collection: Motor async collection
246
306
  entity_type: Type of entities being queried
307
+ serializer: JSON serializer for entity conversion
247
308
  """
248
309
  self._collection = collection
249
310
  self._entity_type = entity_type
311
+ self._serializer = serializer
250
312
 
251
313
  def create_query(self, element_type: type, expression: expr) -> Queryable:
252
314
  """
@@ -308,9 +370,29 @@ class MotorQueryProvider(QueryProvider):
308
370
  is_list_type = query_type.__origin__ in (list, List)
309
371
 
310
372
  if is_list_type:
311
- # Return list of documents
312
- return [doc async for doc in cursor]
373
+ # Return list of deserialized entities
374
+ documents = [doc async for doc in cursor]
375
+ return [self._deserialize_entity(doc) for doc in documents]
313
376
  else:
314
- # Return single document
377
+ # Return single deserialized entity
315
378
  results = await cursor.to_list(length=1)
316
- return results[0] if results else None
379
+ return self._deserialize_entity(results[0]) if results else None
380
+
381
+ def _deserialize_entity(self, doc: dict) -> Any:
382
+ """
383
+ Deserialize a MongoDB document to an entity instance.
384
+
385
+ Args:
386
+ doc: MongoDB document dictionary
387
+
388
+ Returns:
389
+ Deserialized entity instance
390
+ """
391
+ # Remove MongoDB's _id field
392
+ doc.pop("_id", None)
393
+
394
+ # Serialize dict back to JSON for deserialization
395
+ json_str = self._serializer.serialize_to_text(doc)
396
+
397
+ # Deserialize using the entity type
398
+ return self._serializer.deserialize_from_text(json_str, self._entity_type)
@@ -552,7 +552,7 @@ class MotorRepository(Generic[TEntity, TKey], QueryableRepository[TEntity, TKey]
552
552
  if entity_type is None:
553
553
  raise TypeError("Cannot create query: entity type not set. Pass entity_type to constructor.")
554
554
 
555
- return MotorQuery[TEntity](MotorQueryProvider(self.collection, entity_type))
555
+ return MotorQuery[TEntity](MotorQueryProvider(self.collection, entity_type, self._serializer))
556
556
 
557
557
  async def get_all_async(self) -> list[TEntity]:
558
558
  """
@@ -252,24 +252,54 @@ class DataAccessLayer:
252
252
  ValueError: If consumer_group not specified in settings
253
253
  ValueError: If neither repository_setup nor database_name is provided
254
254
  """
255
+ # Configure read model reconciliation
256
+ self._configure_reconciliation(builder)
257
+
258
+ # If custom setup provided, use it (backwards compatible)
259
+ if repository_setup is not None:
260
+ return self._configure_with_custom_setup(builder, modules, repository_setup)
261
+
262
+ # Otherwise use simplified configuration with database_name
263
+ return self._configure_with_database_name(builder, modules)
264
+
265
+ def _configure_reconciliation(self, builder: ApplicationBuilderBase) -> None:
266
+ """Configure read model reconciliation services
267
+
268
+ Args:
269
+ builder: The application builder
270
+
271
+ Raises:
272
+ ValueError: If consumer_group not specified in settings
273
+ """
255
274
  consumer_group = builder.settings.consumer_group
256
275
  if not consumer_group:
257
276
  raise ValueError("Cannot configure Read Model DAL: consumer group not specified in application settings")
258
277
  builder.services.add_singleton(ReadModelConciliationOptions, singleton=ReadModelConciliationOptions(consumer_group))
259
278
  builder.services.add_singleton(HostedService, ReadModelReconciliator)
260
279
 
261
- # If custom setup provided, use it (backwards compatible)
262
- if repository_setup is not None:
263
- for module in [ModuleLoader.load(module_name) for module_name in modules]:
264
- for queryable_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
265
- key_type = str # todo: reflect from DTO base type
266
- repository_setup(builder, queryable_type, key_type)
267
- builder.services.add_transient(RequestHandler, GetByIdQueryHandler[queryable_type, key_type]) # type: ignore
268
- builder.services.add_transient(RequestHandler, ListQueryHandler[queryable_type, key_type]) # type: ignore
269
- return builder
280
+ def _configure_with_custom_setup(
281
+ self,
282
+ builder: ApplicationBuilderBase,
283
+ modules: list[str],
284
+ repository_setup: Callable[[ApplicationBuilderBase, type, type], None],
285
+ ) -> ApplicationBuilderBase:
286
+ """Configure repositories using custom setup function (backwards compatible pattern)
270
287
 
271
- # Otherwise use simplified configuration with database_name
272
- return self._configure_with_database_name(builder, modules)
288
+ Args:
289
+ builder: The application builder
290
+ modules: List of module names to scan for queryable types
291
+ repository_setup: Custom function to setup repositories
292
+
293
+ Returns:
294
+ The configured builder
295
+ """
296
+ for module in [ModuleLoader.load(module_name) for module_name in modules]:
297
+ for queryable_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
298
+ key_type = str # todo: reflect from DTO base type
299
+ repository_setup(builder, queryable_type, key_type)
300
+ builder.services.add_transient(RequestHandler, GetByIdQueryHandler[queryable_type, key_type]) # type: ignore
301
+ builder.services.add_transient(RequestHandler, ListQueryHandler[queryable_type, key_type]) # type: ignore
302
+ return builder
273
303
 
274
304
  def _configure_with_database_name(
275
305
  self,
@@ -291,148 +321,260 @@ class DataAccessLayer:
291
321
  if not self._database_name:
292
322
  raise ValueError("Cannot configure Read Model with simplified API: " "database_name not provided. Either pass database_name to ReadModel() " "or use custom repository_setup function.")
293
323
 
324
+ # Configure based on repository type
325
+ if self._repository_type == "mongo":
326
+ return self._configure_mongo_repositories(builder, modules)
327
+ elif self._repository_type == "motor":
328
+ return self._configure_motor_repositories(builder, modules)
329
+
330
+ return builder
331
+
332
+ def _configure_mongo_repositories(
333
+ self,
334
+ builder: ApplicationBuilderBase,
335
+ modules: list[str],
336
+ ) -> ApplicationBuilderBase:
337
+ """Configure synchronous MongoRepository instances
338
+
339
+ Args:
340
+ builder: The application builder
341
+ modules: List of module names to scan for queryable types
342
+
343
+ Returns:
344
+ The configured builder
345
+
346
+ Raises:
347
+ ValueError: If mongo connection string is missing
348
+ """
349
+ from pymongo import MongoClient
350
+
294
351
  from neuroglia.data.infrastructure.abstractions import (
295
352
  QueryableRepository,
296
353
  Repository,
297
354
  )
355
+ from neuroglia.data.infrastructure.mongo.mongo_repository import (
356
+ MongoRepository,
357
+ MongoRepositoryOptions,
358
+ )
298
359
  from neuroglia.dependency_injection import ServiceProvider
299
360
 
300
- # Configure based on repository type
301
- if self._repository_type == "mongo":
302
- # Synchronous MongoRepository configuration
303
- from pymongo import MongoClient
361
+ # Get MongoDB connection string
362
+ connection_string_name = "mongo"
363
+ connection_string = builder.settings.connection_strings.get(connection_string_name, None)
364
+ if connection_string is None:
365
+ raise ValueError(f"Missing '{connection_string_name}' connection string in application settings")
304
366
 
305
- from neuroglia.data.infrastructure.mongo.mongo_repository import (
306
- MongoRepository,
307
- MongoRepositoryOptions,
308
- )
367
+ # Register MongoClient singleton (shared across all repositories)
368
+ builder.services.try_add_singleton(MongoClient, singleton=MongoClient(connection_string))
309
369
 
310
- # Get MongoDB connection string
311
- connection_string_name = "mongo"
312
- connection_string = builder.settings.connection_strings.get(connection_string_name, None)
313
- if connection_string is None:
314
- raise ValueError(f"Missing '{connection_string_name}' connection string in application settings")
370
+ # Discover and configure each queryable type
371
+ for module in [ModuleLoader.load(module_name) for module_name in modules]:
372
+ for queryable_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
373
+ key_type = str # todo: reflect from DTO base type
315
374
 
316
- # Register MongoClient singleton (shared across all repositories)
317
- builder.services.try_add_singleton(MongoClient, singleton=MongoClient(connection_string))
375
+ # Register options for this entity type
376
+ builder.services.try_add_singleton(
377
+ MongoRepositoryOptions[queryable_type, key_type], # type: ignore
378
+ singleton=MongoRepositoryOptions[queryable_type, key_type](self._database_name), # type: ignore
379
+ )
318
380
 
319
- # Discover and configure each queryable type
320
- for module in [ModuleLoader.load(module_name) for module_name in modules]:
321
- for queryable_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
322
- key_type = str # todo: reflect from DTO base type
381
+ # Register repository
382
+ builder.services.try_add_singleton(
383
+ Repository[queryable_type, key_type], # type: ignore
384
+ MongoRepository[queryable_type, key_type], # type: ignore
385
+ )
323
386
 
324
- # Register options for this entity type
325
- builder.services.try_add_singleton(
326
- MongoRepositoryOptions[queryable_type, key_type], # type: ignore
327
- singleton=MongoRepositoryOptions[queryable_type, key_type](self._database_name), # type: ignore
328
- )
387
+ # Register queryable repository alias
388
+ def make_queryable_factory(qt, kt):
389
+ def queryable_factory(provider: ServiceProvider):
390
+ return provider.get_required_service(Repository[qt, kt]) # type: ignore
329
391
 
330
- # Register repository
331
- builder.services.try_add_singleton(
332
- Repository[queryable_type, key_type], # type: ignore
333
- MongoRepository[queryable_type, key_type], # type: ignore
334
- )
392
+ return queryable_factory
335
393
 
336
- # Register queryable repository alias
337
- def make_queryable_factory(qt, kt):
338
- def queryable_factory(provider: ServiceProvider):
339
- return provider.get_required_service(Repository[qt, kt]) # type: ignore
394
+ builder.services.try_add_singleton(
395
+ QueryableRepository[queryable_type, key_type], # type: ignore
396
+ implementation_factory=make_queryable_factory(queryable_type, key_type),
397
+ )
340
398
 
341
- return queryable_factory
399
+ # Register query handlers
400
+ builder.services.add_transient(RequestHandler, GetByIdQueryHandler[queryable_type, key_type]) # type: ignore
401
+ builder.services.add_transient(RequestHandler, ListQueryHandler[queryable_type, key_type]) # type: ignore
342
402
 
343
- builder.services.try_add_singleton(
344
- QueryableRepository[queryable_type, key_type], # type: ignore
345
- implementation_factory=make_queryable_factory(queryable_type, key_type),
346
- )
403
+ return builder
404
+
405
+ def _configure_motor_repositories(
406
+ self,
407
+ builder: ApplicationBuilderBase,
408
+ modules: list[str],
409
+ ) -> ApplicationBuilderBase:
410
+ """Configure async MotorRepository instances
347
411
 
348
- # Register query handlers
349
- builder.services.add_transient(RequestHandler, GetByIdQueryHandler[queryable_type, key_type]) # type: ignore
350
- builder.services.add_transient(RequestHandler, ListQueryHandler[queryable_type, key_type]) # type: ignore
412
+ Args:
413
+ builder: The application builder
414
+ modules: List of module names to scan for queryable types
351
415
 
352
- elif self._repository_type == "motor":
353
- # Async MotorRepository configuration with queryable support
354
- from motor.motor_asyncio import AsyncIOMotorClient
416
+ Returns:
417
+ The configured builder
355
418
 
356
- from neuroglia.data.infrastructure.mongo.motor_repository import (
357
- MotorRepository,
358
- )
359
- from neuroglia.mediation import Mediator
360
- from neuroglia.serialization.json import JsonSerializer
419
+ Raises:
420
+ ValueError: If mongo connection string is missing
421
+ """
422
+ from motor.motor_asyncio import AsyncIOMotorClient
361
423
 
362
- # Get MongoDB connection string
363
- connection_string_name = "mongo"
364
- connection_string = builder.settings.connection_strings.get(connection_string_name, None)
365
- if connection_string is None:
366
- raise ValueError(f"Missing '{connection_string_name}' connection string in application settings")
424
+ # Get MongoDB connection string
425
+ connection_string_name = "mongo"
426
+ connection_string = builder.settings.connection_strings.get(connection_string_name, None)
427
+ if connection_string is None:
428
+ raise ValueError(f"Missing '{connection_string_name}' connection string in application settings")
367
429
 
368
- # Register AsyncIOMotorClient singleton (shared across all repositories)
369
- builder.services.try_add_singleton(AsyncIOMotorClient, singleton=AsyncIOMotorClient(connection_string))
430
+ # Register AsyncIOMotorClient singleton (shared across all repositories)
431
+ builder.services.try_add_singleton(AsyncIOMotorClient, singleton=AsyncIOMotorClient(connection_string))
370
432
 
371
- # Discover and configure each queryable type (filter by @queryable decorator)
372
- for module in [ModuleLoader.load(module_name) for module_name in modules]:
373
- for entity_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
374
- key_type = str # todo: reflect from DTO base type
433
+ # Discover and configure each queryable type
434
+ self._register_motor_queryable_types(builder, modules)
375
435
 
376
- # Determine collection name (default to lowercase entity name)
377
- collection_name = entity_type.__name__.lower()
378
- if collection_name.endswith("dto"):
379
- collection_name = collection_name[:-3]
380
-
381
- # Factory function to create MotorRepository instance
382
- def make_motor_factory(et, kt, cn):
383
- def motor_factory(provider: ServiceProvider):
384
- # Attempt to resolve Mediator optionally (tests may skip registration)
385
- mediator = provider.get_service(Mediator)
386
- if mediator is None:
387
- mediator = provider.get_required_service(Mediator)
388
-
389
- return MotorRepository[et, kt]( # type: ignore
390
- client=provider.get_required_service(AsyncIOMotorClient),
391
- database_name=self._database_name,
392
- collection_name=cn,
393
- serializer=provider.get_required_service(JsonSerializer),
394
- entity_type=et,
395
- mediator=mediator,
396
- )
397
-
398
- return motor_factory
399
-
400
- # Register MotorRepository (scoped for proper async context per request)
401
- builder.services.try_add_scoped(
402
- MotorRepository[entity_type, key_type], # type: ignore
403
- implementation_factory=make_motor_factory(entity_type, key_type, collection_name),
404
- )
436
+ # Register custom repository mappings
437
+ self._register_custom_repository_mappings(builder)
438
+
439
+ return builder
405
440
 
406
- # Register Repository interface (handlers expect this)
407
- def make_repository_factory(et, kt):
408
- def repository_factory(provider: ServiceProvider):
409
- return provider.get_required_service(MotorRepository[et, kt]) # type: ignore
441
+ def _register_motor_queryable_types(
442
+ self,
443
+ builder: ApplicationBuilderBase,
444
+ modules: list[str],
445
+ ) -> None:
446
+ """Register MotorRepository for each @queryable decorated type
410
447
 
411
- return repository_factory
448
+ Args:
449
+ builder: The application builder
450
+ modules: List of module names to scan for queryable types
451
+ """
452
+ from motor.motor_asyncio import AsyncIOMotorClient
412
453
 
413
- builder.services.try_add_scoped(
414
- Repository[entity_type, key_type], # type: ignore
415
- implementation_factory=make_repository_factory(entity_type, key_type),
416
- )
454
+ from neuroglia.data.infrastructure.abstractions import (
455
+ QueryableRepository,
456
+ Repository,
457
+ )
458
+ from neuroglia.data.infrastructure.mongo.motor_repository import (
459
+ MotorRepository,
460
+ )
461
+ from neuroglia.dependency_injection import ServiceProvider
462
+ from neuroglia.mediation import Mediator
463
+ from neuroglia.serialization.json import JsonSerializer
464
+
465
+ for module in [ModuleLoader.load(module_name) for module_name in modules]:
466
+ for entity_type in TypeFinder.get_types(module, lambda cls: inspect.isclass(cls) and hasattr(cls, "__queryable__")):
467
+ key_type = str # todo: reflect from DTO base type
417
468
 
418
- # Register QueryableRepository interface (for queryable support)
419
- def make_queryable_factory(et, kt):
420
- def queryable_factory(provider: ServiceProvider):
421
- return provider.get_required_service(Repository[et, kt]) # type: ignore
469
+ # Determine collection name (default to lowercase entity name)
470
+ collection_name = entity_type.__name__.lower()
471
+ if collection_name.endswith("dto"):
472
+ collection_name = collection_name[:-3]
473
+
474
+ # Factory function to create MotorRepository instance
475
+ def make_motor_factory(et, kt, cn):
476
+ def motor_factory(provider: ServiceProvider):
477
+ # Attempt to resolve Mediator optionally (tests may skip registration)
478
+ mediator = provider.get_service(Mediator)
479
+ if mediator is None:
480
+ mediator = provider.get_required_service(Mediator)
481
+
482
+ return MotorRepository[et, kt]( # type: ignore
483
+ client=provider.get_required_service(AsyncIOMotorClient),
484
+ database_name=self._database_name,
485
+ collection_name=cn,
486
+ serializer=provider.get_required_service(JsonSerializer),
487
+ entity_type=et,
488
+ mediator=mediator,
489
+ )
422
490
 
423
- return queryable_factory
491
+ return motor_factory
424
492
 
425
- builder.services.try_add_scoped(
426
- QueryableRepository[entity_type, key_type], # type: ignore
427
- implementation_factory=make_queryable_factory(entity_type, key_type),
428
- )
493
+ # Register MotorRepository (scoped for proper async context per request)
494
+ builder.services.try_add_scoped(
495
+ MotorRepository[entity_type, key_type], # type: ignore
496
+ implementation_factory=make_motor_factory(entity_type, key_type, collection_name),
497
+ )
429
498
 
430
- # Register query handlers (consistent with mongo)
431
- builder.services.add_transient(RequestHandler, GetByIdQueryHandler[entity_type, key_type]) # type: ignore
432
- builder.services.add_transient(RequestHandler, ListQueryHandler[entity_type, key_type]) # type: ignore
499
+ # Register Repository interface (handlers expect this)
500
+ def make_repository_factory(et, kt):
501
+ def repository_factory(provider: ServiceProvider):
502
+ return provider.get_required_service(MotorRepository[et, kt]) # type: ignore
433
503
 
434
- # Register custom repository mappings
435
- for abstract_type, implementation_type in self._repository_mappings.items():
436
- builder.services.add_scoped(abstract_type, implementation_type)
504
+ return repository_factory
437
505
 
438
- return builder
506
+ builder.services.try_add_scoped(
507
+ Repository[entity_type, key_type], # type: ignore
508
+ implementation_factory=make_repository_factory(entity_type, key_type),
509
+ )
510
+
511
+ # Register QueryableRepository interface (for queryable support)
512
+ def make_queryable_factory(et, kt):
513
+ def queryable_factory(provider: ServiceProvider):
514
+ return provider.get_required_service(Repository[et, kt]) # type: ignore
515
+
516
+ return queryable_factory
517
+
518
+ builder.services.try_add_scoped(
519
+ QueryableRepository[entity_type, key_type], # type: ignore
520
+ implementation_factory=make_queryable_factory(entity_type, key_type),
521
+ )
522
+
523
+ # Register query handlers (consistent with mongo)
524
+ builder.services.add_transient(RequestHandler, GetByIdQueryHandler[entity_type, key_type]) # type: ignore
525
+ builder.services.add_transient(RequestHandler, ListQueryHandler[entity_type, key_type]) # type: ignore
526
+
527
+ def _register_custom_repository_mappings(self, builder: ApplicationBuilderBase) -> None:
528
+ """Register custom domain repository implementations
529
+
530
+ Uses factory functions to properly instantiate custom repositories,
531
+ avoiding DI auto-construction issues with generic type parameters.
532
+
533
+ Args:
534
+ builder: The application builder
535
+ """
536
+ from motor.motor_asyncio import AsyncIOMotorClient
537
+
538
+ from neuroglia.dependency_injection import ServiceProvider
539
+ from neuroglia.mediation import Mediator
540
+ from neuroglia.serialization.json import JsonSerializer
541
+
542
+ for abstract_type, implementation_type in self._repository_mappings.items():
543
+
544
+ def make_custom_repo_factory(impl_type):
545
+ def custom_repo_factory(provider: ServiceProvider):
546
+ # Get entity type from implementation's base classes
547
+ entity_type = None
548
+ collection_name = None
549
+
550
+ # Extract entity type from MotorRepository[TEntity, TKey] base
551
+ for base in getattr(impl_type, "__orig_bases__", []):
552
+ if hasattr(base, "__origin__") and base.__origin__.__name__ == "MotorRepository":
553
+ if hasattr(base, "__args__") and len(base.__args__) >= 1:
554
+ entity_type = base.__args__[0]
555
+ # Determine collection name
556
+ collection_name = entity_type.__name__.lower()
557
+ if collection_name.endswith("dto"):
558
+ collection_name = collection_name[:-3]
559
+ break
560
+
561
+ if entity_type is None:
562
+ raise ValueError(f"Could not determine entity type for {impl_type.__name__}. " f"Ensure it extends MotorRepository[TEntity, TKey]")
563
+
564
+ # Construct repository with proper dependencies
565
+ mediator = provider.get_service(Mediator)
566
+ if mediator is None:
567
+ mediator = provider.get_required_service(Mediator)
568
+
569
+ return impl_type(
570
+ client=provider.get_required_service(AsyncIOMotorClient),
571
+ database_name=self._database_name,
572
+ collection_name=collection_name,
573
+ serializer=provider.get_required_service(JsonSerializer),
574
+ entity_type=entity_type,
575
+ mediator=mediator,
576
+ )
577
+
578
+ return custom_repo_factory
579
+
580
+ builder.services.add_scoped(abstract_type, implementation_factory=make_custom_repo_factory(implementation_type))