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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/PKG-INFO +1 -1
  2. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/pyproject.toml +2 -2
  3. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/__init__.py +1 -1
  4. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/__init__.py +5 -0
  5. neuroglia_python-0.7.3/src/neuroglia/data/infrastructure/mongo/motor_query.py +398 -0
  6. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +68 -11
  7. neuroglia_python-0.7.3/src/neuroglia/expressions/javascript_expression_translator.py +207 -0
  8. neuroglia_python-0.7.3/src/neuroglia/hosting/configuration/data_access_layer.py +580 -0
  9. neuroglia_python-0.7.1/src/neuroglia/expressions/javascript_expression_translator.py +0 -147
  10. neuroglia_python-0.7.1/src/neuroglia/hosting/configuration/data_access_layer.py +0 -346
  11. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/LICENSE +0 -0
  12. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/README.md +0 -0
  13. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/application/__init__.py +0 -0
  14. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/application/background_scheduler.py +0 -0
  15. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/coordination/__init__.py +0 -0
  16. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/coordination/abstractions.py +0 -0
  17. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/coordination/leader_election.py +0 -0
  18. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/coordination/redis_backend.py +0 -0
  19. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/core/__init__.py +0 -0
  20. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/core/module_loader.py +0 -0
  21. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/core/operation_result.py +0 -0
  22. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/core/problem_details.py +0 -0
  23. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/core/type_extensions.py +0 -0
  24. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/core/type_finder.py +0 -0
  25. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/core/type_registry.py +0 -0
  26. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/__init__.py +0 -0
  27. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/abstractions.py +0 -0
  28. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/exceptions.py +0 -0
  29. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  30. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  31. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  32. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
  33. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -0
  34. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  35. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -0
  36. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
  37. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  38. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  39. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  40. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  41. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  42. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
  43. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  44. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  45. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  46. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  47. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  48. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  49. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  50. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  51. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  52. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  53. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  54. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/queryable.py +0 -0
  55. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/__init__.py +0 -0
  56. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/abstractions.py +0 -0
  57. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/controller.py +0 -0
  58. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  59. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  60. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  61. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/state_machine.py +0 -0
  62. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/resources/watcher.py +0 -0
  63. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/data/unit_of_work.py +0 -0
  64. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/dependency_injection/__init__.py +0 -0
  65. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/dependency_injection/service_provider.py +0 -0
  66. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  67. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/__init__.py +0 -0
  68. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  69. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  70. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  71. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  72. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  73. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  74. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  75. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  76. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/eventing/tracing.py +0 -0
  77. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/expressions/__init__.py +0 -0
  78. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  79. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  80. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  81. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/hosting/__init__.py +0 -0
  82. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/hosting/abstractions.py +0 -0
  83. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/hosting/web.py +0 -0
  84. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/integration/__init__.py +0 -0
  85. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/integration/cache_repository.py +0 -0
  86. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/integration/http_service_client.py +0 -0
  87. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/integration/models.py +0 -0
  88. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/logging/__init__.py +0 -0
  89. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/logging/logger.py +0 -0
  90. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mapping/__init__.py +0 -0
  91. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mapping/mapper.py +0 -0
  92. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/__init__.py +0 -0
  93. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  94. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  95. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/mediator.py +0 -0
  96. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  97. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  98. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/simple.py +0 -0
  99. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  100. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mvc/__init__.py +0 -0
  101. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/mvc/controller_base.py +0 -0
  102. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/observability/__init__.py +0 -0
  103. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/observability/framework.py +0 -0
  104. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/observability/logging.py +0 -0
  105. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/observability/metrics.py +0 -0
  106. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/observability/otel_sdk.py +0 -0
  107. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/observability/settings.py +0 -0
  108. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/observability/tracing.py +0 -0
  109. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/py.typed +0 -0
  110. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/reactive/__init__.py +0 -0
  111. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/reactive/rx_async.py +0 -0
  112. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/serialization/__init__.py +0 -0
  113. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/serialization/abstractions.py +0 -0
  114. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  115. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/serialization/json.py +0 -0
  116. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/utils/__init__.py +0 -0
  117. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/utils/camel_model.py +0 -0
  118. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/utils/case_conversion.py +0 -0
  119. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/validation/__init__.py +0 -0
  120. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/validation/business_rules.py +0 -0
  121. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/validation/exceptions.py +0 -0
  122. {neuroglia_python-0.7.1 → neuroglia_python-0.7.3}/src/neuroglia/validation/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroglia-python
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Port from .NET to Python of the Neuroglia Framework
5
5
  License: Apache
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "neuroglia-python"
3
- version = "0.7.1"
3
+ version = "0.7.3"
4
4
  description = "Port from .NET to Python of the Neuroglia Framework"
5
5
  authors = ["Charles d'Avernas <charles.davernas@neuroglia.io>", "Bruno van de Werve <bvandewe@gmail.com>"]
6
6
  license = "Apache"
@@ -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.3"
66
66
  __author__ = "Neuroglia Team"
67
67
  __email__ = "team@neuroglia.io"
68
68
  __license__ = "Apache"
@@ -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,398 @@
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 TYPE_CHECKING, 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
+ if TYPE_CHECKING:
47
+ from neuroglia.serialization import JsonSerializer
48
+
49
+
50
+ class MotorQuery(Generic[T], Queryable[T]):
51
+ """
52
+ Represents an async Motor MongoDB query.
53
+
54
+ This is the async equivalent of MongoQuery, providing LINQ-style
55
+ queryable support for Motor (async MongoDB driver).
56
+
57
+ Type Parameters:
58
+ T: The entity type being queried
59
+
60
+ Example:
61
+ ```python
62
+ query = await repo.query_async() # Returns MotorQuery[Product]
63
+ results = await query.where(lambda p: p.price > 10).to_list_async()
64
+ ```
65
+ """
66
+
67
+ def __init__(self, query_provider: "MotorQueryProvider", expression: Optional[expr] = None):
68
+ """
69
+ Initialize a Motor query.
70
+
71
+ Args:
72
+ query_provider: The Motor query provider
73
+ expression: Optional AST expression for the query
74
+ """
75
+ super().__init__(query_provider, expression)
76
+
77
+ async def to_list_async(self) -> list[T]:
78
+ """
79
+ Asynchronously execute the query and return results as a list.
80
+
81
+ This is the async equivalent of to_list() for Motor queries.
82
+
83
+ Returns:
84
+ List of entities matching the query
85
+
86
+ Example:
87
+ ```python
88
+ products = await repo.query_async() \\
89
+ .where(lambda p: p.price > 10) \\
90
+ .order_by(lambda p: p.name) \\
91
+ .to_list_async()
92
+ ```
93
+ """
94
+ # Get the provider and ensure it's a MotorQueryProvider
95
+ provider = self.provider
96
+ if not isinstance(provider, MotorQueryProvider):
97
+ raise TypeError("MotorQuery requires a MotorQueryProvider")
98
+
99
+ # Execute the query asynchronously
100
+ return await provider.execute_async(self.expression, List)
101
+
102
+ async def first_or_default_async(self, predicate=None) -> Optional[T]:
103
+ """
104
+ Asynchronously get the first element matching the predicate, or None.
105
+
106
+ Args:
107
+ predicate: Optional filter predicate
108
+
109
+ Returns:
110
+ First matching entity or None
111
+
112
+ Example:
113
+ ```python
114
+ product = await repo.query_async() \\
115
+ .where(lambda p: p.id == "prod123") \\
116
+ .first_or_default_async()
117
+ ```
118
+ """
119
+ # Get the provider and ensure it's a MotorQueryProvider
120
+ provider = self.provider
121
+ if not isinstance(provider, MotorQueryProvider):
122
+ raise TypeError("MotorQuery requires a MotorQueryProvider")
123
+
124
+ # If predicate provided, apply it as a where clause
125
+ query = self
126
+ if predicate is not None:
127
+ query = self.where(predicate)
128
+
129
+ # Execute and get list, then return first or None
130
+ results = await provider.execute_async(query.expression, List)
131
+ return results[0] if results else None
132
+
133
+
134
+ class MotorQueryBuilder(NodeVisitor):
135
+ """
136
+ Represents the service used to build Motor (async) MongoDB queries from Python AST expressions.
137
+
138
+ This is the async equivalent of MongoQueryBuilder, translating LINQ-style
139
+ Python expressions into Motor query operations.
140
+
141
+ The builder walks the Python AST tree and translates method calls like
142
+ .where(), .order_by(), .skip(), .take() into corresponding Motor operations.
143
+
144
+ Attributes:
145
+ _collection: Motor collection to query
146
+ _translator: JavaScript expression translator
147
+ _order_by_clauses: Sort specifications
148
+ _select_clause: Field projection
149
+ _skip_clause: Documents to skip
150
+ _take_clause: Maximum documents to return
151
+ _where_clauses: Filter conditions
152
+
153
+ Example:
154
+ ```python
155
+ # This Python expression:
156
+ query.where(lambda x: x.price > 10).order_by(lambda x: x.name).take(5)
157
+
158
+ # Gets translated to Motor operations:
159
+ collection.find({"$where": "this.price > 10"}).sort("name", 1).limit(5)
160
+ ```
161
+ """
162
+
163
+ def __init__(self, collection: AsyncIOMotorCollection, translator: JavaScriptExpressionTranslator):
164
+ """
165
+ Initialize the Motor query builder.
166
+
167
+ Args:
168
+ collection: Motor async collection to query
169
+ translator: JavaScript expression translator for filter expressions
170
+ """
171
+ self._collection = collection
172
+ self._translator = translator
173
+ self._order_by_clauses: dict[str, int] = {}
174
+ self._select_clause: Optional[list[str]] = None
175
+ self._skip_clause: Optional[int] = None
176
+ self._take_clause: Optional[int] = None
177
+ self._where_clauses: list[str] = []
178
+
179
+ def build(self, expression: expr) -> AsyncIOMotorCursor:
180
+ """
181
+ Build a Motor cursor from the Python AST expression.
182
+
183
+ Walks the AST tree, collects query parameters, and returns
184
+ a configured Motor cursor ready for async iteration.
185
+
186
+ Args:
187
+ expression: Python AST expression representing the query
188
+
189
+ Returns:
190
+ Configured Motor cursor with filters, sorts, skip, and limit applied
191
+
192
+ Example:
193
+ ```python
194
+ builder = MotorQueryBuilder(collection, translator)
195
+ cursor = builder.build(query_expression)
196
+ async for doc in cursor:
197
+ print(doc)
198
+ ```
199
+ """
200
+ self.visit(expression)
201
+
202
+ # Build cursor with projection
203
+ cursor = self._collection.find(projection=self._select_clause)
204
+
205
+ # Apply sorting
206
+ if len(self._order_by_clauses) > 0:
207
+ cursor = cursor.sort(list(self._order_by_clauses.items()))
208
+
209
+ # Apply where clauses
210
+ if len(self._where_clauses) > 0:
211
+ # Use $where for JavaScript expressions
212
+ cursor = self._collection.find({"$where": " && ".join(self._where_clauses)}, projection=self._select_clause)
213
+ if len(self._order_by_clauses) > 0:
214
+ cursor = cursor.sort(list(self._order_by_clauses.items()))
215
+
216
+ # Apply skip
217
+ if self._skip_clause is not None:
218
+ cursor = cursor.skip(self._skip_clause)
219
+
220
+ # Apply limit
221
+ if self._take_clause is not None:
222
+ cursor = cursor.limit(self._take_clause)
223
+
224
+ return cursor
225
+
226
+ def visit_Call(self, node: ast.Call):
227
+ """
228
+ Visit a method call node in the AST.
229
+
230
+ Translates LINQ-style method calls (.where, .order_by, etc.)
231
+ into Motor query parameters.
232
+
233
+ Args:
234
+ node: AST Call node representing a method invocation
235
+ """
236
+ if not isinstance(node.func, ast.Attribute):
237
+ return
238
+
239
+ clause = node.func.attr
240
+ expression = node.args[0] if len(node.args) > 0 else None
241
+
242
+ # Recursively visit the receiver (the object being called)
243
+ self.visit(node.func.value)
244
+
245
+ if expression is None:
246
+ return
247
+
248
+ # Translate lambda expressions to JavaScript
249
+ javascript = self._translator.translate(expression)
250
+
251
+ if clause == "distinct_by":
252
+ # Not directly supported in projection, would need aggregation
253
+ pass
254
+ elif clause == "first":
255
+ self._where_clauses.append(javascript)
256
+ self._take_clause = 1
257
+ elif clause == "last":
258
+ self._where_clauses.append(javascript)
259
+ self._take_clause = 1
260
+ # Default sort by created_at descending (could be any field)
261
+ if "created_at" not in self._order_by_clauses:
262
+ self._order_by_clauses["created_at"] = pymongo.DESCENDING
263
+ elif clause == "order_by":
264
+ self._order_by_clauses[javascript.replace("this.", "")] = pymongo.ASCENDING
265
+ elif clause == "order_by_descending":
266
+ self._order_by_clauses[javascript.replace("this.", "")] = pymongo.DESCENDING
267
+ elif clause == "select" and isinstance(expression, ast.Lambda) and isinstance(expression.body, ast.List):
268
+ # Project specific fields
269
+ self._select_clause = [self._translator.translate(elt).replace("this.", "") for elt in expression.body.elts]
270
+ elif clause == "skip" and isinstance(expression, ast.Constant):
271
+ if isinstance(expression.value, int):
272
+ self._skip_clause = expression.value
273
+ elif clause == "take" and isinstance(expression, ast.Constant):
274
+ if isinstance(expression.value, int):
275
+ self._take_clause = expression.value
276
+ elif clause == "where":
277
+ self._where_clauses.append(javascript)
278
+
279
+
280
+ class MotorQueryProvider(QueryProvider):
281
+ """
282
+ Represents the Motor (async) implementation of the QueryProvider.
283
+
284
+ This provider creates MotorQuery instances and executes them asynchronously
285
+ against a Motor collection.
286
+
287
+ Attributes:
288
+ _collection: Motor async collection to query
289
+ _entity_type: Type of entities in the collection
290
+ _serializer: JSON serializer for entity conversion
291
+
292
+ Example:
293
+ ```python
294
+ provider = MotorQueryProvider(motor_collection, Product, serializer)
295
+ query = provider.create_query(Product, expression)
296
+ results = await provider.execute_async(expression, List[Product])
297
+ ```
298
+ """
299
+
300
+ def __init__(self, collection: AsyncIOMotorCollection, entity_type: type, serializer: "JsonSerializer"):
301
+ """
302
+ Initialize the Motor query provider.
303
+
304
+ Args:
305
+ collection: Motor async collection
306
+ entity_type: Type of entities being queried
307
+ serializer: JSON serializer for entity conversion
308
+ """
309
+ self._collection = collection
310
+ self._entity_type = entity_type
311
+ self._serializer = serializer
312
+
313
+ def create_query(self, element_type: type, expression: expr) -> Queryable:
314
+ """
315
+ Create a new queryable instance.
316
+
317
+ Args:
318
+ element_type: Type of elements in the query
319
+ expression: AST expression for the query
320
+
321
+ Returns:
322
+ New MotorQuery instance
323
+ """
324
+ return MotorQuery(self, expression)
325
+
326
+ def execute(self, expression: expr, query_type: type) -> Any:
327
+ """
328
+ Sync execute method (not used, kept for interface compatibility).
329
+
330
+ Motor repositories are async-only. Use execute_async() instead.
331
+
332
+ Raises:
333
+ NotImplementedError: Always, as Motor is async-only
334
+ """
335
+ raise NotImplementedError("Motor repositories are async-only. Use execute_async() instead.")
336
+
337
+ async def execute_async(self, expression: expr, query_type: type) -> Any:
338
+ """
339
+ Execute the query asynchronously using Motor.
340
+
341
+ Builds the Motor cursor from the expression and executes it,
342
+ returning either a list or single result based on query_type.
343
+
344
+ Args:
345
+ expression: AST expression representing the query
346
+ query_type: Expected return type (List[T] or T)
347
+
348
+ Returns:
349
+ Query results (list or single entity)
350
+
351
+ Example:
352
+ ```python
353
+ # Returns list
354
+ results = await provider.execute_async(expr, List[Product])
355
+
356
+ # Returns single result
357
+ result = await provider.execute_async(expr, Product)
358
+ ```
359
+ """
360
+ # Build Motor cursor from expression
361
+ cursor = MotorQueryBuilder(self._collection, JavaScriptExpressionTranslator()).build(expression)
362
+
363
+ # Determine if we're expecting a list or single result
364
+ # Check if query_type is List or list type annotation
365
+ is_list_type = False
366
+ if query_type == List or query_type == list:
367
+ is_list_type = True
368
+ elif hasattr(query_type, "__origin__"):
369
+ # Handle List[T] from typing
370
+ is_list_type = query_type.__origin__ in (list, List)
371
+
372
+ if is_list_type:
373
+ # Return list of deserialized entities
374
+ documents = [doc async for doc in cursor]
375
+ return [self._deserialize_entity(doc) for doc in documents]
376
+ else:
377
+ # Return single deserialized entity
378
+ results = await cursor.to_list(length=1)
379
+ return self._deserialize_entity(results[0]) if results else None
380
+
381
+ def _deserialize_entity(self, doc: dict) -> Any:
382
+ """
383
+ Deserialize a MongoDB document to an entity instance.
384
+
385
+ Args:
386
+ doc: MongoDB document dictionary
387
+
388
+ Returns:
389
+ Deserialized entity instance
390
+ """
391
+ # Remove MongoDB's _id field
392
+ doc.pop("_id", None)
393
+
394
+ # Serialize dict back to JSON for deserialization
395
+ json_str = self._serializer.serialize_to_text(doc)
396
+
397
+ # Deserialize using the entity type
398
+ return self._serializer.deserialize_from_text(json_str, self._entity_type)
@@ -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, self._serializer))
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