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.
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/PKG-INFO +1 -1
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/pyproject.toml +2 -2
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/__init__.py +1 -1
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +6 -2
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/__init__.py +5 -0
- neuroglia_python-0.7.2/src/neuroglia/data/infrastructure/mongo/motor_query.py +316 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +68 -11
- neuroglia_python-0.7.2/src/neuroglia/expressions/javascript_expression_translator.py +207 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/configuration/data_access_layer.py +108 -16
- neuroglia_python-0.7.0/src/neuroglia/expressions/javascript_expression_translator.py +0 -147
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/LICENSE +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/README.md +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/application/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/application/background_scheduler.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/abstractions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/leader_election.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/coordination/redis_backend.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/module_loader.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/operation_result.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/problem_details.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/type_extensions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/type_finder.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/core/type_registry.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/abstractions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/exceptions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queries/generic/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queries/generic/list_query.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/queryable.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/abstractions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/controller.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/state_machine.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/resources/watcher.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/data/unit_of_work.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/dependency_injection/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/dependency_injection/service_provider.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/eventing/tracing.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/expressions/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/extensions/mediator_extensions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/abstractions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/hosting/web.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/cache_repository.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/http_service_client.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/integration/models.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/logging/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/logging/logger.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mapping/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mapping/mapper.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/mediator.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/metrics_middleware.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/simple.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mediation/tracing_middleware.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mvc/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/mvc/controller_base.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/framework.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/logging.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/metrics.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/otel_sdk.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/settings.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/observability/tracing.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/py.typed +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/reactive/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/reactive/rx_async.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/abstractions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/serialization/json.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/utils/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/utils/camel_model.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/utils/case_conversion.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/validation/__init__.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/validation/business_rules.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/validation/exceptions.py +0 -0
- {neuroglia_python-0.7.0 → neuroglia_python-0.7.2}/src/neuroglia/validation/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "neuroglia-python"
|
|
3
|
-
version = "0.7.
|
|
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 =
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
+
inferred_type = base.__args__[0]
|
|
145
153
|
break
|
|
146
154
|
except (AttributeError, IndexError):
|
|
147
|
-
|
|
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
|
|