neuroglia-python 0.7.0__tar.gz → 0.7.2__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 (121) hide show
  1. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/PKG-INFO +1 -1
  2. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/pyproject.toml +2 -2
  3. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/__init__.py +1 -1
  4. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +6 -2
  5. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/__init__.py +5 -0
  6. neuroglia_python-0.7.2/src/neuroglia/data/infrastructure/mongo/motor_query.py +316 -0
  7. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +68 -11
  8. neuroglia_python-0.7.2/src/neuroglia/expressions/javascript_expression_translator.py +207 -0
  9. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/configuration/data_access_layer.py +108 -16
  10. neuroglia_python-0.7.0/src/neuroglia/expressions/javascript_expression_translator.py +0 -147
  11. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/LICENSE +0 -0
  12. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/README.md +0 -0
  13. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/application/__init__.py +0 -0
  14. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/application/background_scheduler.py +0 -0
  15. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/__init__.py +0 -0
  16. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/abstractions.py +0 -0
  17. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/leader_election.py +0 -0
  18. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/redis_backend.py +0 -0
  19. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/__init__.py +0 -0
  20. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/module_loader.py +0 -0
  21. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/operation_result.py +0 -0
  22. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/problem_details.py +0 -0
  23. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/type_extensions.py +0 -0
  24. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/type_finder.py +0 -0
  25. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/type_registry.py +0 -0
  26. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/__init__.py +0 -0
  27. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/abstractions.py +0 -0
  28. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/exceptions.py +0 -0
  29. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  30. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  31. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  32. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
  33. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -0
  34. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  35. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
  36. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  37. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  38. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  39. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  40. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  41. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
  42. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  43. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  44. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  45. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  46. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  47. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  48. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  49. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  50. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  51. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  52. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  53. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queryable.py +0 -0
  54. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/__init__.py +0 -0
  55. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/abstractions.py +0 -0
  56. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/controller.py +0 -0
  57. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  58. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  59. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  60. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/state_machine.py +0 -0
  61. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/watcher.py +0 -0
  62. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/unit_of_work.py +0 -0
  63. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/dependency_injection/__init__.py +0 -0
  64. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/dependency_injection/service_provider.py +0 -0
  65. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  66. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/__init__.py +0 -0
  67. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  68. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  69. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  70. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  71. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  72. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  73. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  74. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  75. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/tracing.py +0 -0
  76. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/expressions/__init__.py +0 -0
  77. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  78. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  79. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  80. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/__init__.py +0 -0
  81. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/abstractions.py +0 -0
  82. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/web.py +0 -0
  83. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/__init__.py +0 -0
  84. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/cache_repository.py +0 -0
  85. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/http_service_client.py +0 -0
  86. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/models.py +0 -0
  87. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/logging/__init__.py +0 -0
  88. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/logging/logger.py +0 -0
  89. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mapping/__init__.py +0 -0
  90. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mapping/mapper.py +0 -0
  91. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/__init__.py +0 -0
  92. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  93. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  94. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/mediator.py +0 -0
  95. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  96. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  97. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/simple.py +0 -0
  98. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  99. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mvc/__init__.py +0 -0
  100. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mvc/controller_base.py +0 -0
  101. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/__init__.py +0 -0
  102. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/framework.py +0 -0
  103. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/logging.py +0 -0
  104. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/metrics.py +0 -0
  105. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/otel_sdk.py +0 -0
  106. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/settings.py +0 -0
  107. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/tracing.py +0 -0
  108. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/py.typed +0 -0
  109. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/reactive/__init__.py +0 -0
  110. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/reactive/rx_async.py +0 -0
  111. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/__init__.py +0 -0
  112. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/abstractions.py +0 -0
  113. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  114. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/json.py +0 -0
  115. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/utils/__init__.py +0 -0
  116. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/utils/camel_model.py +0 -0
  117. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/utils/case_conversion.py +0 -0
  118. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/validation/__init__.py +0 -0
  119. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/validation/business_rules.py +0 -0
  120. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/validation/exceptions.py +0 -0
  121. {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/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.0
3
+ Version: 0.7.2
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.0"
3
+ version = "0.7.2"
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"
@@ -43,7 +43,7 @@ jinja2 = "3.1.4"
43
43
  redis = { version = "5.2.0", optional = true }
44
44
  pymongo = { version = "4.15.4", optional = true }
45
45
  motor = { version = "3.7.0", optional = true }
46
- kurrentdbclient = { version = "1.1.2", optional = true } # Bug fix: AsyncPersistentSubscription.init() now propagates subscription_id
46
+ kurrentdbclient = "1.1.2"
47
47
  etcd3-py = { version = "0.1.6", optional = true } # Maintained fork with protobuf 5.x/6.x support
48
48
 
49
49
  # OpenTelemetry dependencies - pinned versions for protobuf 6.x compatibility
@@ -62,7 +62,7 @@ __all__ = [
62
62
  ]
63
63
 
64
64
  # Framework metadata
65
- __version__ = "0.7.0"
65
+ __version__ = "0.7.2"
66
66
  __author__ = "Neuroglia Team"
67
67
  __email__ = "team@neuroglia.io"
68
68
  __license__ = "Apache"
@@ -64,11 +64,15 @@ class ESEventStore(EventStore):
64
64
  self._eventstore_client = connection_string_or_client
65
65
 
66
66
  async def _ensure_client(self) -> Any:
67
- """Lazily initialize the async EventStoreDB client on first use"""
67
+ """Lazily initialize the async KurrentDB client on first use"""
68
68
  if self._eventstore_client is None:
69
69
  if self._connection_string is None:
70
70
  raise RuntimeError("Neither connection string nor client provided")
71
- self._eventstore_client = await AsyncClientFactory(uri=self._connection_string)
71
+ # AsyncKurrentDBClient constructor is NOT awaitable - returns client directly
72
+ client = AsyncClientFactory(uri=self._connection_string)
73
+ # Must call connect() to establish the connection
74
+ await client.connect()
75
+ self._eventstore_client = client
72
76
  return self._eventstore_client
73
77
 
74
78
  async def contains_async(self, stream_id: str) -> bool:
@@ -18,6 +18,7 @@ Note:
18
18
  from typing import TYPE_CHECKING
19
19
 
20
20
  # Eagerly import async/motor-based components (no pymongo dependency)
21
+ from .motor_query import MotorQuery, MotorQueryBuilder, MotorQueryProvider
21
22
  from .motor_repository import MotorRepository
22
23
  from .serialization_helper import MongoSerializationHelper
23
24
  from .typed_mongo_query import TypedMongoQuery, with_typed_mongo_query
@@ -34,6 +35,10 @@ if TYPE_CHECKING:
34
35
  __all__ = [
35
36
  # Async repository (recommended for FastAPI/asyncio)
36
37
  "MotorRepository",
38
+ # Async query support
39
+ "MotorQuery",
40
+ "MotorQueryBuilder",
41
+ "MotorQueryProvider",
37
42
  # Sync repositories (lazy-loaded, require pymongo)
38
43
  "MongoRepository",
39
44
  "MongoQueryProvider",
@@ -0,0 +1,316 @@
1
+ """
2
+ Motor (async) MongoDB Query implementation for Neuroglia.
3
+
4
+ This module provides async LINQ-style queryable support for Motor repositories,
5
+ enabling the same fluent query API available in sync MongoRepository.
6
+
7
+ Classes:
8
+ MotorQuery: Async queryable wrapper for Motor collections
9
+ MotorQueryProvider: Async query provider implementation
10
+ MotorQueryBuilder: AST visitor for building Motor queries from Python expressions
11
+
12
+ Example:
13
+ ```python
14
+ from neuroglia.data.infrastructure.mongo import MotorRepository
15
+
16
+ # Repository with queryable support
17
+ repo = MotorRepository[Product, str](...)
18
+
19
+ # Fluent LINQ-style queries
20
+ products = await repo.query_async() \\
21
+ .where(lambda p: p.price > 10) \\
22
+ .order_by(lambda p: p.name) \\
23
+ .skip(10) \\
24
+ .take(5) \\
25
+ .to_list_async()
26
+ ```
27
+
28
+ See Also:
29
+ - mongo_repository.py: Sync MongoQuery implementation (reference)
30
+ - MotorRepository: Async repository with queryable support
31
+ - Queryable: Base queryable abstraction
32
+ """
33
+
34
+ import ast
35
+ from ast import NodeVisitor, expr
36
+ from typing import Any, Generic, List, Optional
37
+
38
+ import pymongo
39
+ from motor.motor_asyncio import AsyncIOMotorCollection, AsyncIOMotorCursor
40
+
41
+ from neuroglia.data.queryable import Queryable, QueryProvider, T
42
+ from neuroglia.expressions.javascript_expression_translator import (
43
+ JavaScriptExpressionTranslator,
44
+ )
45
+
46
+
47
+ class MotorQuery(Generic[T], Queryable[T]):
48
+ """
49
+ Represents an async Motor MongoDB query.
50
+
51
+ This is the async equivalent of MongoQuery, providing LINQ-style
52
+ queryable support for Motor (async MongoDB driver).
53
+
54
+ Type Parameters:
55
+ T: The entity type being queried
56
+
57
+ Example:
58
+ ```python
59
+ query = await repo.query_async() # Returns MotorQuery[Product]
60
+ results = await query.where(lambda p: p.price > 10).to_list_async()
61
+ ```
62
+ """
63
+
64
+ def __init__(self, query_provider: "MotorQueryProvider", expression: Optional[expr] = None):
65
+ """
66
+ Initialize a Motor query.
67
+
68
+ Args:
69
+ query_provider: The Motor query provider
70
+ expression: Optional AST expression for the query
71
+ """
72
+ super().__init__(query_provider, expression)
73
+
74
+
75
+ class MotorQueryBuilder(NodeVisitor):
76
+ """
77
+ Represents the service used to build Motor (async) MongoDB queries from Python AST expressions.
78
+
79
+ This is the async equivalent of MongoQueryBuilder, translating LINQ-style
80
+ Python expressions into Motor query operations.
81
+
82
+ The builder walks the Python AST tree and translates method calls like
83
+ .where(), .order_by(), .skip(), .take() into corresponding Motor operations.
84
+
85
+ Attributes:
86
+ _collection: Motor collection to query
87
+ _translator: JavaScript expression translator
88
+ _order_by_clauses: Sort specifications
89
+ _select_clause: Field projection
90
+ _skip_clause: Documents to skip
91
+ _take_clause: Maximum documents to return
92
+ _where_clauses: Filter conditions
93
+
94
+ Example:
95
+ ```python
96
+ # This Python expression:
97
+ query.where(lambda x: x.price > 10).order_by(lambda x: x.name).take(5)
98
+
99
+ # Gets translated to Motor operations:
100
+ collection.find({"$where": "this.price > 10"}).sort("name", 1).limit(5)
101
+ ```
102
+ """
103
+
104
+ def __init__(self, collection: AsyncIOMotorCollection, translator: JavaScriptExpressionTranslator):
105
+ """
106
+ Initialize the Motor query builder.
107
+
108
+ Args:
109
+ collection: Motor async collection to query
110
+ translator: JavaScript expression translator for filter expressions
111
+ """
112
+ self._collection = collection
113
+ self._translator = translator
114
+ self._order_by_clauses: dict[str, int] = {}
115
+ self._select_clause: Optional[list[str]] = None
116
+ self._skip_clause: Optional[int] = None
117
+ self._take_clause: Optional[int] = None
118
+ self._where_clauses: list[str] = []
119
+
120
+ def build(self, expression: expr) -> AsyncIOMotorCursor:
121
+ """
122
+ Build a Motor cursor from the Python AST expression.
123
+
124
+ Walks the AST tree, collects query parameters, and returns
125
+ a configured Motor cursor ready for async iteration.
126
+
127
+ Args:
128
+ expression: Python AST expression representing the query
129
+
130
+ Returns:
131
+ Configured Motor cursor with filters, sorts, skip, and limit applied
132
+
133
+ Example:
134
+ ```python
135
+ builder = MotorQueryBuilder(collection, translator)
136
+ cursor = builder.build(query_expression)
137
+ async for doc in cursor:
138
+ print(doc)
139
+ ```
140
+ """
141
+ self.visit(expression)
142
+
143
+ # Build cursor with projection
144
+ cursor = self._collection.find(projection=self._select_clause)
145
+
146
+ # Apply sorting
147
+ if len(self._order_by_clauses) > 0:
148
+ cursor = cursor.sort(list(self._order_by_clauses.items()))
149
+
150
+ # Apply where clauses
151
+ if len(self._where_clauses) > 0:
152
+ # Use $where for JavaScript expressions
153
+ cursor = self._collection.find({"$where": " && ".join(self._where_clauses)}, projection=self._select_clause)
154
+ if len(self._order_by_clauses) > 0:
155
+ cursor = cursor.sort(list(self._order_by_clauses.items()))
156
+
157
+ # Apply skip
158
+ if self._skip_clause is not None:
159
+ cursor = cursor.skip(self._skip_clause)
160
+
161
+ # Apply limit
162
+ if self._take_clause is not None:
163
+ cursor = cursor.limit(self._take_clause)
164
+
165
+ return cursor
166
+
167
+ def visit_Call(self, node: ast.Call):
168
+ """
169
+ Visit a method call node in the AST.
170
+
171
+ Translates LINQ-style method calls (.where, .order_by, etc.)
172
+ into Motor query parameters.
173
+
174
+ Args:
175
+ node: AST Call node representing a method invocation
176
+ """
177
+ if not isinstance(node.func, ast.Attribute):
178
+ return
179
+
180
+ clause = node.func.attr
181
+ expression = node.args[0] if len(node.args) > 0 else None
182
+
183
+ # Recursively visit the receiver (the object being called)
184
+ self.visit(node.func.value)
185
+
186
+ if expression is None:
187
+ return
188
+
189
+ # Translate lambda expressions to JavaScript
190
+ javascript = self._translator.translate(expression)
191
+
192
+ if clause == "distinct_by":
193
+ # Not directly supported in projection, would need aggregation
194
+ pass
195
+ elif clause == "first":
196
+ self._where_clauses.append(javascript)
197
+ self._take_clause = 1
198
+ elif clause == "last":
199
+ self._where_clauses.append(javascript)
200
+ self._take_clause = 1
201
+ # Default sort by created_at descending (could be any field)
202
+ if "created_at" not in self._order_by_clauses:
203
+ self._order_by_clauses["created_at"] = pymongo.DESCENDING
204
+ elif clause == "order_by":
205
+ self._order_by_clauses[javascript.replace("this.", "")] = pymongo.ASCENDING
206
+ elif clause == "order_by_descending":
207
+ self._order_by_clauses[javascript.replace("this.", "")] = pymongo.DESCENDING
208
+ elif clause == "select" and isinstance(expression, ast.Lambda) and isinstance(expression.body, ast.List):
209
+ # Project specific fields
210
+ self._select_clause = [self._translator.translate(elt).replace("this.", "") for elt in expression.body.elts]
211
+ elif clause == "skip" and isinstance(expression, ast.Constant):
212
+ if isinstance(expression.value, int):
213
+ self._skip_clause = expression.value
214
+ elif clause == "take" and isinstance(expression, ast.Constant):
215
+ if isinstance(expression.value, int):
216
+ self._take_clause = expression.value
217
+ elif clause == "where":
218
+ self._where_clauses.append(javascript)
219
+
220
+
221
+ class MotorQueryProvider(QueryProvider):
222
+ """
223
+ Represents the Motor (async) implementation of the QueryProvider.
224
+
225
+ This provider creates MotorQuery instances and executes them asynchronously
226
+ against a Motor collection.
227
+
228
+ Attributes:
229
+ _collection: Motor async collection to query
230
+ _entity_type: Type of entities in the collection
231
+
232
+ Example:
233
+ ```python
234
+ provider = MotorQueryProvider(motor_collection, Product)
235
+ query = provider.create_query(Product, expression)
236
+ results = await provider.execute_async(expression, List[Product])
237
+ ```
238
+ """
239
+
240
+ def __init__(self, collection: AsyncIOMotorCollection, entity_type: type):
241
+ """
242
+ Initialize the Motor query provider.
243
+
244
+ Args:
245
+ collection: Motor async collection
246
+ entity_type: Type of entities being queried
247
+ """
248
+ self._collection = collection
249
+ self._entity_type = entity_type
250
+
251
+ def create_query(self, element_type: type, expression: expr) -> Queryable:
252
+ """
253
+ Create a new queryable instance.
254
+
255
+ Args:
256
+ element_type: Type of elements in the query
257
+ expression: AST expression for the query
258
+
259
+ Returns:
260
+ New MotorQuery instance
261
+ """
262
+ return MotorQuery(self, expression)
263
+
264
+ def execute(self, expression: expr, query_type: type) -> Any:
265
+ """
266
+ Sync execute method (not used, kept for interface compatibility).
267
+
268
+ Motor repositories are async-only. Use execute_async() instead.
269
+
270
+ Raises:
271
+ NotImplementedError: Always, as Motor is async-only
272
+ """
273
+ raise NotImplementedError("Motor repositories are async-only. Use execute_async() instead.")
274
+
275
+ async def execute_async(self, expression: expr, query_type: type) -> Any:
276
+ """
277
+ Execute the query asynchronously using Motor.
278
+
279
+ Builds the Motor cursor from the expression and executes it,
280
+ returning either a list or single result based on query_type.
281
+
282
+ Args:
283
+ expression: AST expression representing the query
284
+ query_type: Expected return type (List[T] or T)
285
+
286
+ Returns:
287
+ Query results (list or single entity)
288
+
289
+ Example:
290
+ ```python
291
+ # Returns list
292
+ results = await provider.execute_async(expr, List[Product])
293
+
294
+ # Returns single result
295
+ result = await provider.execute_async(expr, Product)
296
+ ```
297
+ """
298
+ # Build Motor cursor from expression
299
+ cursor = MotorQueryBuilder(self._collection, JavaScriptExpressionTranslator()).build(expression)
300
+
301
+ # Determine if we're expecting a list or single result
302
+ # Check if query_type is List or list type annotation
303
+ is_list_type = False
304
+ if query_type == List or query_type == list:
305
+ is_list_type = True
306
+ elif hasattr(query_type, "__origin__"):
307
+ # Handle List[T] from typing
308
+ is_list_type = query_type.__origin__ in (list, List)
309
+
310
+ if is_list_type:
311
+ # Return list of documents
312
+ return [doc async for doc in cursor]
313
+ else:
314
+ # Return single document
315
+ results = await cursor.to_list(length=1)
316
+ return results[0] if results else None
@@ -44,7 +44,8 @@ from typing import TYPE_CHECKING, Any, Generic, Optional, cast
44
44
  from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
45
45
 
46
46
  from neuroglia.data.abstractions import AggregateRoot, TEntity, TKey
47
- from neuroglia.data.infrastructure.abstractions import Repository
47
+ from neuroglia.data.infrastructure.abstractions import QueryableRepository, Repository
48
+ from neuroglia.data.queryable import Queryable
48
49
  from neuroglia.hosting.abstractions import ApplicationBuilderBase
49
50
  from neuroglia.mediation.mediator import Mediator
50
51
  from neuroglia.serialization.json import JsonSerializer
@@ -55,15 +56,16 @@ if TYPE_CHECKING:
55
56
  log = logging.getLogger(__name__)
56
57
 
57
58
 
58
- class MotorRepository(Generic[TEntity, TKey], Repository[TEntity, TKey]):
59
+ class MotorRepository(Generic[TEntity, TKey], QueryableRepository[TEntity, TKey]):
59
60
  """
60
- Async MongoDB repository implementation using Motor driver.
61
+ Async MongoDB repository implementation using Motor driver with queryable support.
61
62
 
62
63
  Motor is PyMongo's async driver and the recommended choice for async Python
63
64
  applications using FastAPI, asyncio, or any async framework.
64
65
 
65
- This repository provides full CRUD operations with proper async/await support
66
- and automatic JSON serialization/deserialization of domain entities.
66
+ This repository provides full CRUD operations with proper async/await support,
67
+ automatic JSON serialization/deserialization of domain entities, and LINQ-style
68
+ queryable support for complex queries.
67
69
 
68
70
  Type Parameters:
69
71
  TEntity: The type of entities managed by this repository
@@ -97,6 +99,12 @@ class MotorRepository(Generic[TEntity, TKey], Repository[TEntity, TKey]):
97
99
  await repo.update_async(found)
98
100
 
99
101
  await repo.remove_async("p1")
102
+
103
+ # Queryable support (LINQ-style)
104
+ expensive_products = await repo.query_async() \\
105
+ .where(lambda p: p.price > 100) \\
106
+ .order_by(lambda p: p.name) \\
107
+ .to_list_async()
100
108
  ```
101
109
 
102
110
  See Also:
@@ -133,18 +141,19 @@ class MotorRepository(Generic[TEntity, TKey], Repository[TEntity, TKey]):
133
141
 
134
142
  # Store entity type if provided, otherwise try to infer it
135
143
  if entity_type is not None:
136
- self._entity_type = entity_type
144
+ self._entity_type: Optional[type[TEntity]] = entity_type
137
145
  else:
138
146
  # Try to infer from generic parameters
139
147
  # Search through all base classes to find one with generic type args
140
- self._entity_type = None
148
+ inferred_type: Optional[type[TEntity]] = None
141
149
  try:
142
150
  for base in self.__orig_bases__: # type: ignore[attr-defined]
143
151
  if hasattr(base, "__args__") and len(base.__args__) > 0:
144
- self._entity_type = base.__args__[0]
152
+ inferred_type = base.__args__[0]
145
153
  break
146
154
  except (AttributeError, IndexError):
147
- self._entity_type = None
155
+ pass
156
+ self._entity_type = inferred_type
148
157
 
149
158
  @property
150
159
  def collection(self) -> AsyncIOMotorCollection:
@@ -497,6 +506,54 @@ class MotorRepository(Generic[TEntity, TKey], Repository[TEntity, TKey]):
497
506
  """
498
507
  await self.collection.delete_one({"id": self._normalize_id(id)})
499
508
 
509
+ async def query_async(self) -> Queryable[TEntity]:
510
+ """
511
+ Returns a queryable for fluent LINQ-style queries.
512
+
513
+ This method provides async queryable support, enabling complex queries
514
+ using Python lambda expressions that are translated to MongoDB operations.
515
+
516
+ Returns:
517
+ Queryable instance for building queries fluently
518
+
519
+ Example:
520
+ ```python
521
+ # Complex query with filtering, sorting, pagination
522
+ products = await repository.query_async() \\
523
+ .where(lambda p: p.price > 10 and p.in_stock) \\
524
+ .order_by(lambda p: p.name) \\
525
+ .skip(10) \\
526
+ .take(5) \\
527
+ .to_list_async()
528
+
529
+ # Projection (select specific fields)
530
+ names = await repository.query_async() \\
531
+ .select(lambda p: [p.name, p.price]) \\
532
+ .to_list_async()
533
+
534
+ # Single result
535
+ first_product = await repository.query_async() \\
536
+ .where(lambda p: p.id == "prod123") \\
537
+ .first_or_default_async()
538
+ ```
539
+
540
+ Notes:
541
+ - Lambda expressions are translated to JavaScript for MongoDB $where
542
+ - Supports .where(), .order_by(), .skip(), .take(), .select()
543
+ - Use .to_list_async() or .first_or_default_async() to execute
544
+ """
545
+ from neuroglia.data.infrastructure.mongo.motor_query import (
546
+ MotorQuery,
547
+ MotorQueryProvider,
548
+ )
549
+
550
+ # Determine entity type (raise if not available)
551
+ entity_type = self._entity_type
552
+ if entity_type is None:
553
+ raise TypeError("Cannot create query: entity type not set. Pass entity_type to constructor.")
554
+
555
+ return MotorQuery[TEntity](MotorQueryProvider(self.collection, entity_type))
556
+
500
557
  async def get_all_async(self) -> list[TEntity]:
501
558
  """
502
559
  Retrieve all entities from the repository.
@@ -738,13 +795,13 @@ class MotorRepository(Generic[TEntity, TKey], Repository[TEntity, TKey]):
738
795
  # Register the concrete MotorRepository with SCOPED lifetime
739
796
  # Scoped ensures proper async context per request and integration with UnitOfWork
740
797
  builder.services.add_scoped(
741
- MotorRepository[entity_type, key_type],
798
+ MotorRepository[entity_type, key_type], # type: ignore
742
799
  implementation_factory=create_motor_repository,
743
800
  )
744
801
 
745
802
  # Register the abstract Repository interface that handlers expect (also SCOPED)
746
803
  builder.services.add_scoped(
747
- Repository[entity_type, key_type],
804
+ Repository[entity_type, key_type], # type: ignore
748
805
  implementation_factory=get_repository_interface,
749
806
  )
750
807