neuroglia-python 0.7.3__tar.gz → 0.7.5__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.3 → neuroglia_python-0.7.5}/PKG-INFO +1 -1
  2. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/pyproject.toml +1 -1
  3. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/__init__.py +1 -1
  4. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +13 -3
  5. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +3 -3
  6. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/motor_query.py +5 -4
  7. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +98 -9
  8. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/queryable.py +68 -13
  9. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/hosting/configuration/data_access_layer.py +79 -17
  10. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/LICENSE +0 -0
  11. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/README.md +0 -0
  12. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/application/__init__.py +0 -0
  13. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/application/background_scheduler.py +0 -0
  14. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/coordination/__init__.py +0 -0
  15. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/coordination/abstractions.py +0 -0
  16. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/coordination/leader_election.py +0 -0
  17. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/coordination/redis_backend.py +0 -0
  18. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/core/__init__.py +0 -0
  19. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/core/module_loader.py +0 -0
  20. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/core/operation_result.py +0 -0
  21. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/core/problem_details.py +0 -0
  22. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/core/type_extensions.py +0 -0
  23. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/core/type_finder.py +0 -0
  24. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/core/type_registry.py +0 -0
  25. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/__init__.py +0 -0
  26. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/abstractions.py +0 -0
  27. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/exceptions.py +0 -0
  28. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  29. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  30. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  31. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
  32. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  33. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -0
  34. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
  35. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  36. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  37. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  38. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  39. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
  40. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  41. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  42. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  43. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  44. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  45. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  46. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  47. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  48. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  49. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  50. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  51. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  52. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/__init__.py +0 -0
  53. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/abstractions.py +0 -0
  54. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/controller.py +0 -0
  55. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  56. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  57. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  58. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/state_machine.py +0 -0
  59. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/watcher.py +0 -0
  60. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/data/unit_of_work.py +0 -0
  61. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/dependency_injection/__init__.py +0 -0
  62. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/dependency_injection/service_provider.py +0 -0
  63. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  64. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/__init__.py +0 -0
  65. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  66. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  67. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  68. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  69. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  70. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  71. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  72. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  73. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/eventing/tracing.py +0 -0
  74. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/expressions/__init__.py +0 -0
  75. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
  76. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  77. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  78. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  79. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/hosting/__init__.py +0 -0
  80. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/hosting/abstractions.py +0 -0
  81. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/hosting/web.py +0 -0
  82. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/integration/__init__.py +0 -0
  83. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/integration/cache_repository.py +0 -0
  84. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/integration/http_service_client.py +0 -0
  85. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/integration/models.py +0 -0
  86. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/logging/__init__.py +0 -0
  87. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/logging/logger.py +0 -0
  88. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mapping/__init__.py +0 -0
  89. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mapping/mapper.py +0 -0
  90. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/__init__.py +0 -0
  91. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  92. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  93. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/mediator.py +0 -0
  94. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  95. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  96. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/simple.py +0 -0
  97. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  98. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mvc/__init__.py +0 -0
  99. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/mvc/controller_base.py +0 -0
  100. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/observability/__init__.py +0 -0
  101. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/observability/framework.py +0 -0
  102. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/observability/logging.py +0 -0
  103. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/observability/metrics.py +0 -0
  104. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/observability/otel_sdk.py +0 -0
  105. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/observability/settings.py +0 -0
  106. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/observability/tracing.py +0 -0
  107. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/py.typed +0 -0
  108. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/reactive/__init__.py +0 -0
  109. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/reactive/rx_async.py +0 -0
  110. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/serialization/__init__.py +0 -0
  111. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/serialization/abstractions.py +0 -0
  112. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  113. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/serialization/json.py +0 -0
  114. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/utils/__init__.py +0 -0
  115. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/utils/camel_model.py +0 -0
  116. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/utils/case_conversion.py +0 -0
  117. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/validation/__init__.py +0 -0
  118. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/validation/business_rules.py +0 -0
  119. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/src/neuroglia/validation/exceptions.py +0 -0
  120. {neuroglia_python-0.7.3 → neuroglia_python-0.7.5}/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.3
3
+ Version: 0.7.5
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.3"
3
+ version = "0.7.5"
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.3"
65
+ __version__ = "0.7.5"
66
66
  __author__ = "Neuroglia Team"
67
67
  __email__ = "team@neuroglia.io"
68
68
  __license__ = "Apache"
@@ -113,10 +113,20 @@ class EventSourcingRepository(Generic[TAggregate, TKey], Repository[TAggregate,
113
113
  return self._eventstore.contains_stream(self._build_stream_id_for(id))
114
114
 
115
115
  async def get_async(self, id: TKey) -> Optional[TAggregate]:
116
- """Gets the aggregate with the specified id, if any"""
116
+ """
117
+ Gets the aggregate with the specified id, if any.
118
+
119
+ Returns None if the stream does not exist.
120
+ """
117
121
  stream_id = self._build_stream_id_for(id)
118
- events = await self._eventstore.read_async(stream_id, StreamReadDirection.FORWARDS, 0)
119
- return self._aggregator.aggregate(events, self.__orig_class__.__args__[0])
122
+ try:
123
+ events = await self._eventstore.read_async(stream_id, StreamReadDirection.FORWARDS, 0)
124
+ if not events:
125
+ return None
126
+ return self._aggregator.aggregate(events, self.__orig_class__.__args__[0])
127
+ except Exception:
128
+ # If stream doesn't exist or any other error occurs, return None
129
+ return None
120
130
 
121
131
  async def _do_add_async(self, aggregate: TAggregate) -> TAggregate:
122
132
  """Adds and persists the specified aggregate"""
@@ -38,8 +38,8 @@ class MongoRepositoryOptions(Generic[TEntity, TKey]):
38
38
  class MongoQuery(Generic[T], Queryable[T]):
39
39
  """Represents a Mongo query"""
40
40
 
41
- def __init__(self, query_provider: "MongoQueryProvider", expression: Optional[expr] = None):
42
- super().__init__(query_provider, expression)
41
+ def __init__(self, query_provider: "MongoQueryProvider", expression: Optional[expr] = None, element_type: Optional[type] = None):
42
+ super().__init__(query_provider, expression, element_type)
43
43
 
44
44
 
45
45
  class MongoQueryBuilder(NodeVisitor):
@@ -114,7 +114,7 @@ class MongoQueryProvider(QueryProvider):
114
114
  _collection: Collection
115
115
 
116
116
  def create_query(self, element_type: type, expression: expr) -> Queryable:
117
- return MongoQuery[element_type](self, expression)
117
+ return MongoQuery(self, expression, element_type)
118
118
 
119
119
  def execute(self, expression: expr, query_type: type) -> Any:
120
120
  query = MongoQueryBuilder(self._collection, JavaScriptExpressionTranslator()).build(expression)
@@ -64,15 +64,16 @@ class MotorQuery(Generic[T], Queryable[T]):
64
64
  ```
65
65
  """
66
66
 
67
- def __init__(self, query_provider: "MotorQueryProvider", expression: Optional[expr] = None):
67
+ def __init__(self, query_provider: "MotorQueryProvider", expression: Optional[expr] = None, element_type: Optional[type] = None):
68
68
  """
69
69
  Initialize a Motor query.
70
70
 
71
71
  Args:
72
72
  query_provider: The Motor query provider
73
73
  expression: Optional AST expression for the query
74
+ element_type: Optional element type (used for chaining to preserve type info)
74
75
  """
75
- super().__init__(query_provider, expression)
76
+ super().__init__(query_provider, expression, element_type)
76
77
 
77
78
  async def to_list_async(self) -> list[T]:
78
79
  """
@@ -319,9 +320,9 @@ class MotorQueryProvider(QueryProvider):
319
320
  expression: AST expression for the query
320
321
 
321
322
  Returns:
322
- New MotorQuery instance
323
+ New MotorQuery instance with element_type propagated
323
324
  """
324
- return MotorQuery(self, expression)
325
+ return MotorQuery(self, expression, element_type)
325
326
 
326
327
  def execute(self, expression: expr, query_type: type) -> Any:
327
328
  """
@@ -578,31 +578,68 @@ class MotorRepository(Generic[TEntity, TKey], QueryableRepository[TEntity, TKey]
578
578
 
579
579
  return entities
580
580
 
581
- async def find_async(self, filter_dict: dict) -> list[TEntity]:
581
+ async def find_async(
582
+ self,
583
+ filter_dict: dict,
584
+ sort: Optional[list[tuple[str, int]]] = None,
585
+ limit: Optional[int] = None,
586
+ skip: Optional[int] = None,
587
+ projection: Optional[dict] = None,
588
+ ) -> list[TEntity]:
582
589
  """
583
- Find entities matching a MongoDB filter query.
590
+ Find entities matching a MongoDB filter query with optional sorting, pagination, and projection.
584
591
 
585
- This provides direct access to MongoDB's query language for complex queries.
592
+ This provides direct access to MongoDB's query language for complex queries
593
+ with full support for cursor operations.
586
594
 
587
595
  Args:
588
596
  filter_dict: MongoDB query filter (e.g., {"state.email": "user@example.com"})
597
+ sort: List of (field, direction) tuples. Direction: 1 for ascending, -1 for descending.
598
+ Example: [("name", 1), ("created_at", -1)]
599
+ limit: Maximum number of documents to return
600
+ skip: Number of documents to skip (for pagination)
601
+ projection: Fields to include/exclude. Example: {"name": 1, "email": 1}
589
602
 
590
603
  Returns:
591
604
  List of entities matching the filter
592
605
 
593
- Example:
606
+ Examples:
594
607
  ```python
595
608
  # Find all active users
596
609
  active_users = await repository.find_async({"state.is_active": True})
597
610
 
598
- # Find users by email domain
599
- gmail_users = await repository.find_async({
600
- "state.email": {"$regex": "@gmail.com$"}
601
- })
611
+ # Find users by email domain with sorting
612
+ gmail_users = await repository.find_async(
613
+ {"state.email": {"$regex": "@gmail.com$"}},
614
+ sort=[("state.name", 1)]
615
+ )
616
+
617
+ # Paginated query with sorting
618
+ page_2_users = await repository.find_async(
619
+ {"state.is_active": True},
620
+ sort=[("state.created_at", -1)],
621
+ skip=20,
622
+ limit=10
623
+ )
624
+
625
+ # Query with field projection (only return specific fields)
626
+ users = await repository.find_async(
627
+ {"state.is_active": True},
628
+ projection={"state.name": 1, "state.email": 1}
629
+ )
602
630
  ```
603
631
  """
632
+ cursor = self.collection.find(filter_dict, projection)
633
+
634
+ if sort:
635
+ cursor = cursor.sort(sort)
636
+ if skip:
637
+ cursor = cursor.skip(skip)
638
+ if limit:
639
+ cursor = cursor.limit(limit)
640
+
604
641
  entities = []
605
- async for doc in self.collection.find(filter_dict):
642
+ async for doc in cursor:
606
643
  entity = self._deserialize_entity(doc)
607
644
  entities.append(entity)
608
645
 
@@ -631,6 +668,58 @@ class MotorRepository(Generic[TEntity, TKey], QueryableRepository[TEntity, TKey]
631
668
 
632
669
  return self._deserialize_entity(doc)
633
670
 
671
+ async def count_async(self, filter_dict: Optional[dict] = None) -> int:
672
+ """
673
+ Count documents matching a MongoDB filter query.
674
+
675
+ Args:
676
+ filter_dict: MongoDB query filter. If None or empty dict, counts all documents.
677
+
678
+ Returns:
679
+ Count of documents matching the filter
680
+
681
+ Examples:
682
+ ```python
683
+ # Count all documents
684
+ total_count = await repository.count_async()
685
+
686
+ # Count with filter
687
+ active_count = await repository.count_async({"state.is_active": True})
688
+
689
+ # Useful for pagination
690
+ filter_query = {"state.status": "pending"}
691
+ total_pages = (await repository.count_async(filter_query) + page_size - 1) // page_size
692
+ ```
693
+ """
694
+ filter_dict = filter_dict or {}
695
+ return await self.collection.count_documents(filter_dict)
696
+
697
+ async def exists_async(self, filter_dict: dict) -> bool:
698
+ """
699
+ Check if any document matches the MongoDB filter query.
700
+
701
+ This is more efficient than count_async for existence checks as it
702
+ stops after finding the first match.
703
+
704
+ Args:
705
+ filter_dict: MongoDB query filter
706
+
707
+ Returns:
708
+ True if at least one document matches, False otherwise
709
+
710
+ Examples:
711
+ ```python
712
+ # Check if email already exists
713
+ email_exists = await repository.exists_async({"state.email": "user@example.com"})
714
+ if email_exists:
715
+ raise ValueError("Email already registered")
716
+
717
+ # Check if any active users exist
718
+ has_active = await repository.exists_async({"state.is_active": True})
719
+ ```
720
+ """
721
+ return await self.collection.count_documents(filter_dict, limit=1) > 0
722
+
634
723
  @staticmethod
635
724
  def configure(
636
725
  builder: ApplicationBuilderBase,
@@ -1,6 +1,5 @@
1
1
  import ast
2
2
  import inspect
3
- import os
4
3
  from abc import ABC, abstractclassmethod
5
4
  from ast import Attribute, Name, NodeTransformer, expr
6
5
  from collections.abc import Callable
@@ -80,9 +79,10 @@ class Queryable(Generic[T]):
80
79
  - Repository Pattern Guide: https://bvandewe.github.io/pyneuro/patterns/
81
80
  """
82
81
 
83
- def __init__(self, provider: QueryProvider, expression: Optional[expr] = None):
82
+ def __init__(self, provider: QueryProvider, expression: Optional[expr] = None, element_type: Optional[type] = None):
84
83
  self.provider = provider
85
84
  self.expression = ast.Name(id="__query") if expression is None else expression
85
+ self._element_type = element_type
86
86
 
87
87
  expression: expr
88
88
  """ Gets the expression that is associated with the queryable """
@@ -92,6 +92,10 @@ class Queryable(Generic[T]):
92
92
 
93
93
  def get_element_type(self) -> type:
94
94
  """Gets the type of elements to query against"""
95
+ # Try explicit element type first (set during chaining)
96
+ if hasattr(self, "_element_type") and self._element_type is not None:
97
+ return self._element_type
98
+ # Fall back to __orig_class__ for initial query creation
95
99
  return self.__orig_class__.__args__[0]
96
100
 
97
101
  def first_or_default(self, predicate: Callable[[T], bool] = None) -> T:
@@ -146,7 +150,9 @@ class Queryable(Generic[T]):
146
150
 
147
151
  def order_by(self, selector: Callable[[T], Any]):
148
152
  """Orders the sequence using the specified attribute"""
149
- selector_source = self._get_lambda_source_code(selector)
153
+ frame = inspect.currentframe().f_back
154
+ frame_info = inspect.getframeinfo(frame)
155
+ selector_source = self._get_lambda_source_code(selector, frame_info.positions.end_col_offset)
150
156
  selector_tree = ast.parse(selector_source)
151
157
  selector_lambda_expression = selector_tree.body[0].value
152
158
  if not isinstance(selector_lambda_expression.body, Attribute):
@@ -160,7 +166,9 @@ class Queryable(Generic[T]):
160
166
 
161
167
  def order_by_descending(self, selector: Callable[[T], Any]):
162
168
  """Orders the sequence in a descending fashion using the specified attribute"""
163
- selector_source = self._get_lambda_source_code(selector)
169
+ frame = inspect.currentframe().f_back
170
+ frame_info = inspect.getframeinfo(frame)
171
+ selector_source = self._get_lambda_source_code(selector, frame_info.positions.end_col_offset)
164
172
  selector_tree = ast.parse(selector_source)
165
173
  selector_lambda_expression = selector_tree.body[0].value
166
174
  if not isinstance(selector_lambda_expression.body, Attribute):
@@ -175,8 +183,9 @@ class Queryable(Generic[T]):
175
183
  def select(self, selector: Callable[[T], Any]):
176
184
  """Projects each element of a sequence into a new form"""
177
185
  frame = inspect.currentframe().f_back
186
+ frame_info = inspect.getframeinfo(frame)
178
187
  variables = {**frame.f_locals}
179
- selector_source = self._get_lambda_source_code(selector)
188
+ selector_source = self._get_lambda_source_code(selector, frame_info.positions.end_col_offset)
180
189
  selector_tree = ast.parse(selector_source)
181
190
  selector_lambda_expression = selector_tree.body[0].value
182
191
  if not isinstance(selector_lambda_expression.body, Attribute) and not isinstance(selector_lambda_expression.body, ast.List):
@@ -239,18 +248,64 @@ class Queryable(Generic[T]):
239
248
  function (Callable): The lambda to get the source code of
240
249
  max_col_offset (int): The maximum column offset to walk the AST tree for the target lamba
241
250
 
251
+ Returns:
252
+ Optional[str]: The lambda source code, or None if extraction fails
253
+
242
254
  Notes:
243
255
  Credits to https://gist.github.com/Xion/617c1496ff45f3673a5692c3b0e3f75a
256
+
257
+ This method handles the case where the lambda is on a continuation line
258
+ that starts with '.' (common in method chaining). In such cases, the raw
259
+ source line is invalid Python syntax, so we prepend a dummy identifier
260
+ to make it parseable.
261
+
262
+ For multi-line continuations (backslash or implicit), we only process
263
+ the first line containing the lambda, as that's sufficient for extraction.
244
264
  """
245
- source_lines, _ = inspect.getsourcelines(function)
246
- if len(source_lines) != 1:
265
+ try:
266
+ source_lines, _ = inspect.getsourcelines(function)
267
+ except (OSError, TypeError) as e:
268
+ # Cannot extract source (e.g., built-in functions, C extensions)
247
269
  return None
248
- source_text = os.linesep.join(source_lines).strip()
249
- source_ast = ast.parse(source_text)
250
- lambda_node = next(
251
- (node for node in ast.walk(source_ast) if isinstance(node, ast.Lambda) and node.col_offset <= max_col_offset),
252
- None,
253
- )
270
+
271
+ # For multi-line sources (continuation), use only the first line
272
+ # The lambda itself is typically on the first line
273
+ if len(source_lines) == 0:
274
+ return None
275
+
276
+ # Strip the line and remove trailing backslash if present (line continuation)
277
+ source_text = source_lines[0].strip().rstrip("\\").strip()
278
+
279
+ # Handle continuation lines that start with '.' (method chaining)
280
+ # These are invalid Python syntax on their own
281
+ offset_adjustment = 0
282
+ if source_text.startswith("."):
283
+ source_text = "_" + source_text # Prepend dummy identifier
284
+ offset_adjustment = 1
285
+
286
+ try:
287
+ source_ast = ast.parse(source_text)
288
+ except SyntaxError:
289
+ return None
290
+
291
+ # Adjust max_col_offset if we prepended a dummy identifier
292
+ # Set to None if not provided to match all lambdas
293
+ adjusted_max_col_offset = None
294
+ if max_col_offset is not None:
295
+ adjusted_max_col_offset = max_col_offset + offset_adjustment
296
+
297
+ # Find lambda node within the adjusted column offset
298
+ if adjusted_max_col_offset is not None:
299
+ lambda_node = next(
300
+ (node for node in ast.walk(source_ast) if isinstance(node, ast.Lambda) and node.col_offset <= adjusted_max_col_offset),
301
+ None,
302
+ )
303
+ else:
304
+ lambda_node = next(
305
+ (node for node in ast.walk(source_ast) if isinstance(node, ast.Lambda)),
306
+ None,
307
+ )
308
+
254
309
  if lambda_node is None:
255
310
  return None
256
311
  lambda_text = source_text[lambda_node.col_offset : lambda_node.end_col_offset]
@@ -23,15 +23,26 @@ class DataAccessLayer:
23
23
  """Represents a helper class used to configure an application's Write Model DAL
24
24
 
25
25
  Supports two configuration patterns:
26
- 1. Simplified: Pass options directly to constructor
26
+ 1. Simplified: Pass database_name, consumer_group, and delete_mode to constructor
27
27
  2. Custom: Pass custom repository_setup function to configure()
28
28
 
29
29
  Examples:
30
- # Simple configuration with default options
31
- DataAccessLayer.WriteModel().configure(builder, ["domain.entities"])
32
-
33
- # With custom delete mode
30
+ # Simple configuration with all required parameters
34
31
  from neuroglia.data.infrastructure.event_sourcing.abstractions import DeleteMode
32
+
33
+ DataAccessLayer.WriteModel(
34
+ database_name="myapp",
35
+ consumer_group="myapp_group",
36
+ delete_mode=DeleteMode.HARD
37
+ ).configure(builder, ["domain.entities"])
38
+
39
+ # Minimal configuration (uses default delete mode)
40
+ DataAccessLayer.WriteModel(
41
+ database_name="myapp",
42
+ consumer_group="myapp_group"
43
+ ).configure(builder, ["domain.entities"])
44
+
45
+ # Legacy: With EventSourcingRepositoryOptions (backwards compatible)
35
46
  from neuroglia.data.infrastructure.event_sourcing.event_sourcing_repository import (
36
47
  EventSourcingRepositoryOptions
37
48
  )
@@ -49,13 +60,42 @@ class DataAccessLayer:
49
60
  )
50
61
  """
51
62
 
52
- def __init__(self, options: Optional["EventSourcingRepositoryOptions"] = None):
63
+ def __init__(
64
+ self,
65
+ database_name: Optional[str] = None,
66
+ consumer_group: Optional[str] = None,
67
+ delete_mode: Optional["DeleteMode"] = None,
68
+ options: Optional["EventSourcingRepositoryOptions"] = None,
69
+ ):
53
70
  """Initialize WriteModel configuration
54
71
 
55
72
  Args:
56
- options: Optional repository options (e.g., delete_mode).
57
- If not provided, default options will be used.
73
+ database_name: Database/stream prefix for event store (e.g., "myapp").
74
+ Internally creates EventStoreOptions and calls ESEventStore.configure().
75
+ consumer_group: Consumer group name for event store subscriptions.
76
+ delete_mode: Optional deletion strategy (DISABLED, SOFT, HARD).
77
+ If not provided, defaults to DISABLED.
78
+ options: Legacy parameter for EventSourcingRepositoryOptions (backwards compatible).
79
+ If database_name is provided, this parameter is ignored.
80
+
81
+ Example:
82
+ ```python
83
+ # Simplified configuration (recommended)
84
+ DataAccessLayer.WriteModel(
85
+ database_name="myapp",
86
+ consumer_group="myapp_group",
87
+ delete_mode=DeleteMode.HARD
88
+ ).configure(builder, ["domain.entities"])
89
+
90
+ # Legacy (still supported)
91
+ DataAccessLayer.WriteModel(
92
+ options=EventSourcingRepositoryOptions(delete_mode=DeleteMode.HARD)
93
+ ).configure(builder, ["domain.entities"])
94
+ ```
58
95
  """
96
+ self._database_name = database_name
97
+ self._consumer_group = consumer_group
98
+ self._delete_mode = delete_mode
59
99
  self._options = options
60
100
 
61
101
  def configure(self, builder: ApplicationBuilderBase, modules: list[str], repository_setup: Optional[Callable[[ApplicationBuilderBase, type, type], None]] = None) -> ApplicationBuilderBase:
@@ -66,13 +106,14 @@ class DataAccessLayer:
66
106
  modules (List[str]): a list containing the names of the modules to scan for aggregate root types
67
107
  repository_setup (Optional[Callable[[ApplicationBuilderBase, Type, Type], None]]):
68
108
  Optional custom function to setup repositories. If provided, takes precedence over options.
69
- If not provided, uses simplified configuration with options (if any).
109
+ If not provided, uses simplified configuration with database_name/consumer_group or options.
70
110
 
71
111
  Returns:
72
112
  ApplicationBuilderBase: The configured builder
73
113
 
74
114
  Raises:
75
115
  ImportError: If EventSourcingRepository cannot be imported
116
+ ValueError: If database_name is provided without consumer_group
76
117
  """
77
118
  # If custom setup provided, use it (backwards compatible)
78
119
  if repository_setup is not None:
@@ -82,7 +123,26 @@ class DataAccessLayer:
82
123
  repository_setup(builder, aggregate_type, key_type)
83
124
  return builder
84
125
 
85
- # Otherwise use simplified configuration with options
126
+ # If database_name provided, configure event store and use simplified configuration
127
+ if self._database_name is not None:
128
+ if self._consumer_group is None:
129
+ raise ValueError("consumer_group is required when database_name is provided")
130
+
131
+ # Configure ESEventStore
132
+ from neuroglia.data.infrastructure.event_sourcing.abstractions import (
133
+ EventStoreOptions,
134
+ )
135
+ from neuroglia.data.infrastructure.event_sourcing.event_store.event_store import (
136
+ ESEventStore,
137
+ )
138
+
139
+ event_store_options = EventStoreOptions(
140
+ database_name=self._database_name,
141
+ consumer_group=self._consumer_group,
142
+ )
143
+ ESEventStore.configure(builder, event_store_options)
144
+
145
+ # Otherwise use simplified configuration with options or defaults
86
146
  return self._configure_with_options(builder, modules)
87
147
 
88
148
  def _configure_with_options(self, builder: ApplicationBuilderBase, modules: list[str]) -> ApplicationBuilderBase:
@@ -98,6 +158,7 @@ class DataAccessLayer:
98
158
  from neuroglia.data.infrastructure.abstractions import Repository
99
159
  from neuroglia.data.infrastructure.event_sourcing.abstractions import (
100
160
  Aggregator,
161
+ DeleteMode,
101
162
  EventStore,
102
163
  )
103
164
  from neuroglia.data.infrastructure.event_sourcing.event_sourcing_repository import (
@@ -107,6 +168,9 @@ class DataAccessLayer:
107
168
  from neuroglia.dependency_injection import ServiceProvider
108
169
  from neuroglia.mediation import Mediator
109
170
 
171
+ # Determine delete mode to use
172
+ delete_mode = self._delete_mode if self._delete_mode is not None else (self._options.delete_mode if self._options else DeleteMode.DISABLED)
173
+
110
174
  # Discover and configure each aggregate type
111
175
  for module in [ModuleLoader.load(module_name) for module_name in modules]:
112
176
  for aggregate_type in TypeFinder.get_types(
@@ -115,13 +179,11 @@ class DataAccessLayer:
115
179
  ):
116
180
  key_type = str # todo: reflect from DTO base type
117
181
 
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
- )
182
+ # Create type-specific options
183
+ typed_options = EventSourcingRepositoryOptions[aggregate_type, key_type]( # type: ignore
184
+ delete_mode=delete_mode,
185
+ soft_delete_method_name=self._options.soft_delete_method_name if self._options else "mark_as_deleted",
186
+ )
125
187
 
126
188
  # Create factory function with proper closure
127
189
  def make_factory(et, kt, opts):