neuroglia-python 0.7.4__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.4 → neuroglia_python-0.7.5}/PKG-INFO +1 -1
  2. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/pyproject.toml +1 -1
  3. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/__init__.py +1 -1
  4. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +13 -3
  5. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +3 -3
  6. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/motor_query.py +5 -4
  7. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +98 -9
  8. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/queryable.py +6 -1
  9. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/LICENSE +0 -0
  10. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/README.md +0 -0
  11. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/application/__init__.py +0 -0
  12. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/application/background_scheduler.py +0 -0
  13. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/coordination/__init__.py +0 -0
  14. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/coordination/abstractions.py +0 -0
  15. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/coordination/leader_election.py +0 -0
  16. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/coordination/redis_backend.py +0 -0
  17. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/core/__init__.py +0 -0
  18. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/core/module_loader.py +0 -0
  19. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/core/operation_result.py +0 -0
  20. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/core/problem_details.py +0 -0
  21. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/core/type_extensions.py +0 -0
  22. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/core/type_finder.py +0 -0
  23. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/core/type_registry.py +0 -0
  24. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/__init__.py +0 -0
  25. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/abstractions.py +0 -0
  26. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/exceptions.py +0 -0
  27. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  28. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  29. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  30. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
  31. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  32. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -0
  33. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
  34. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  35. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  36. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  37. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  38. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
  39. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  40. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  41. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  42. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  43. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  44. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  45. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  46. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  47. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  48. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  49. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  50. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  51. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/__init__.py +0 -0
  52. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/abstractions.py +0 -0
  53. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/controller.py +0 -0
  54. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  55. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  56. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  57. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/state_machine.py +0 -0
  58. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/resources/watcher.py +0 -0
  59. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/data/unit_of_work.py +0 -0
  60. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/dependency_injection/__init__.py +0 -0
  61. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/dependency_injection/service_provider.py +0 -0
  62. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  63. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/__init__.py +0 -0
  64. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  65. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  66. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  67. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  68. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  69. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  70. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  71. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  72. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/eventing/tracing.py +0 -0
  73. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/expressions/__init__.py +0 -0
  74. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
  75. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  76. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  77. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  78. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/hosting/__init__.py +0 -0
  79. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/hosting/abstractions.py +0 -0
  80. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/hosting/configuration/data_access_layer.py +0 -0
  81. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/hosting/web.py +0 -0
  82. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/integration/__init__.py +0 -0
  83. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/integration/cache_repository.py +0 -0
  84. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/integration/http_service_client.py +0 -0
  85. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/integration/models.py +0 -0
  86. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/logging/__init__.py +0 -0
  87. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/logging/logger.py +0 -0
  88. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mapping/__init__.py +0 -0
  89. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mapping/mapper.py +0 -0
  90. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/__init__.py +0 -0
  91. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  92. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  93. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/mediator.py +0 -0
  94. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  95. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  96. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/simple.py +0 -0
  97. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  98. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mvc/__init__.py +0 -0
  99. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/mvc/controller_base.py +0 -0
  100. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/observability/__init__.py +0 -0
  101. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/observability/framework.py +0 -0
  102. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/observability/logging.py +0 -0
  103. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/observability/metrics.py +0 -0
  104. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/observability/otel_sdk.py +0 -0
  105. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/observability/settings.py +0 -0
  106. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/observability/tracing.py +0 -0
  107. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/py.typed +0 -0
  108. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/reactive/__init__.py +0 -0
  109. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/reactive/rx_async.py +0 -0
  110. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/serialization/__init__.py +0 -0
  111. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/serialization/abstractions.py +0 -0
  112. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  113. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/serialization/json.py +0 -0
  114. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/utils/__init__.py +0 -0
  115. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/utils/camel_model.py +0 -0
  116. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/utils/case_conversion.py +0 -0
  117. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/validation/__init__.py +0 -0
  118. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/validation/business_rules.py +0 -0
  119. {neuroglia_python-0.7.4 → neuroglia_python-0.7.5}/src/neuroglia/validation/exceptions.py +0 -0
  120. {neuroglia_python-0.7.4 → 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.4
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.4"
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.4"
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,
@@ -79,9 +79,10 @@ class Queryable(Generic[T]):
79
79
  - Repository Pattern Guide: https://bvandewe.github.io/pyneuro/patterns/
80
80
  """
81
81
 
82
- def __init__(self, provider: QueryProvider, expression: Optional[expr] = None):
82
+ def __init__(self, provider: QueryProvider, expression: Optional[expr] = None, element_type: Optional[type] = None):
83
83
  self.provider = provider
84
84
  self.expression = ast.Name(id="__query") if expression is None else expression
85
+ self._element_type = element_type
85
86
 
86
87
  expression: expr
87
88
  """ Gets the expression that is associated with the queryable """
@@ -91,6 +92,10 @@ class Queryable(Generic[T]):
91
92
 
92
93
  def get_element_type(self) -> type:
93
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
94
99
  return self.__orig_class__.__args__[0]
95
100
 
96
101
  def first_or_default(self, predicate: Callable[[T], bool] = None) -> T: