mongo-aggro 0.1.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- mongo_aggro/__init__.py +400 -0
- mongo_aggro/accumulators.py +30 -12
- mongo_aggro/base.py +49 -9
- mongo_aggro/expressions/__init__.py +396 -0
- mongo_aggro/expressions/arithmetic.py +329 -0
- mongo_aggro/expressions/array.py +425 -0
- mongo_aggro/expressions/base.py +180 -0
- mongo_aggro/expressions/bitwise.py +84 -0
- mongo_aggro/expressions/comparison.py +161 -0
- mongo_aggro/expressions/conditional.py +117 -0
- mongo_aggro/expressions/date.py +665 -0
- mongo_aggro/expressions/encrypted.py +116 -0
- mongo_aggro/expressions/logical.py +72 -0
- mongo_aggro/expressions/object.py +122 -0
- mongo_aggro/expressions/set.py +150 -0
- mongo_aggro/expressions/size.py +48 -0
- mongo_aggro/expressions/string.py +365 -0
- mongo_aggro/expressions/trigonometry.py +283 -0
- mongo_aggro/expressions/type.py +205 -0
- mongo_aggro/expressions/variable.py +73 -0
- mongo_aggro/expressions/window.py +327 -0
- mongo_aggro/operators/__init__.py +65 -0
- mongo_aggro/operators/array.py +41 -0
- mongo_aggro/operators/base.py +15 -0
- mongo_aggro/operators/bitwise.py +81 -0
- mongo_aggro/operators/comparison.py +82 -0
- mongo_aggro/operators/element.py +32 -0
- mongo_aggro/operators/geo.py +171 -0
- mongo_aggro/operators/logical.py +111 -0
- mongo_aggro/operators/misc.py +102 -0
- mongo_aggro/operators/regex.py +25 -0
- mongo_aggro/stages/__init__.py +110 -0
- mongo_aggro/stages/array.py +69 -0
- mongo_aggro/stages/change.py +109 -0
- mongo_aggro/stages/core.py +170 -0
- mongo_aggro/stages/geo.py +93 -0
- mongo_aggro/stages/group.py +154 -0
- mongo_aggro/stages/join.py +221 -0
- mongo_aggro/stages/misc.py +45 -0
- mongo_aggro/stages/output.py +136 -0
- mongo_aggro/stages/search.py +315 -0
- mongo_aggro/stages/session.py +111 -0
- mongo_aggro/stages/stats.py +152 -0
- mongo_aggro/stages/transform.py +136 -0
- mongo_aggro/stages/window.py +139 -0
- mongo_aggro-0.2.2.dist-info/METADATA +193 -0
- mongo_aggro-0.2.2.dist-info/RECORD +49 -0
- {mongo_aggro-0.1.0.dist-info → mongo_aggro-0.2.2.dist-info}/WHEEL +1 -1
- mongo_aggro/operators.py +0 -247
- mongo_aggro/stages.py +0 -990
- mongo_aggro-0.1.0.dist-info/METADATA +0 -537
- mongo_aggro-0.1.0.dist-info/RECORD +0 -9
- {mongo_aggro-0.1.0.dist-info → mongo_aggro-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Base class and utilities for MongoDB query operators."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class QueryOperator(BaseModel):
|
|
7
|
+
"""Base class for query operators used in $match and other stages."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(
|
|
10
|
+
populate_by_name=True,
|
|
11
|
+
extra="forbid",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["QueryOperator"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Bitwise query operators for MongoDB aggregation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mongo_aggro.operators.base import QueryOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BitsAllClear(QueryOperator):
|
|
11
|
+
"""
|
|
12
|
+
$bitsAllClear operator - matches where all bit positions are 0.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> BitsAllClear(mask=35).model_dump()
|
|
16
|
+
{"$bitsAllClear": 35}
|
|
17
|
+
|
|
18
|
+
>>> BitsAllClear(mask=[1, 5]).model_dump()
|
|
19
|
+
{"$bitsAllClear": [1, 5]}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
mask: int | list[int] = Field(
|
|
23
|
+
..., description="Bitmask or array of bit positions"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
27
|
+
return {"$bitsAllClear": self.mask}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BitsAllSet(QueryOperator):
|
|
31
|
+
"""
|
|
32
|
+
$bitsAllSet operator - matches where all bit positions are 1.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> BitsAllSet(mask=35).model_dump()
|
|
36
|
+
{"$bitsAllSet": 35}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
mask: int | list[int] = Field(
|
|
40
|
+
..., description="Bitmask or array of bit positions"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
44
|
+
return {"$bitsAllSet": self.mask}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BitsAnyClear(QueryOperator):
|
|
48
|
+
"""
|
|
49
|
+
$bitsAnyClear operator - matches where any bit position is 0.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> BitsAnyClear(mask=35).model_dump()
|
|
53
|
+
{"$bitsAnyClear": 35}
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
mask: int | list[int] = Field(
|
|
57
|
+
..., description="Bitmask or array of bit positions"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
61
|
+
return {"$bitsAnyClear": self.mask}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BitsAnySet(QueryOperator):
|
|
65
|
+
"""
|
|
66
|
+
$bitsAnySet operator - matches where any bit position is 1.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> BitsAnySet(mask=35).model_dump()
|
|
70
|
+
{"$bitsAnySet": 35}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
mask: int | list[int] = Field(
|
|
74
|
+
..., description="Bitmask or array of bit positions"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
78
|
+
return {"$bitsAnySet": self.mask}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["BitsAllClear", "BitsAllSet", "BitsAnyClear", "BitsAnySet"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Comparison query operators for MongoDB aggregation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mongo_aggro.operators.base import QueryOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Eq(QueryOperator):
|
|
11
|
+
"""$eq comparison operator."""
|
|
12
|
+
|
|
13
|
+
value: Any = Field(..., description="Value to compare")
|
|
14
|
+
|
|
15
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
16
|
+
return {"$eq": self.value}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Ne(QueryOperator):
|
|
20
|
+
"""$ne (not equal) comparison operator."""
|
|
21
|
+
|
|
22
|
+
value: Any = Field(..., description="Value to compare")
|
|
23
|
+
|
|
24
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
25
|
+
return {"$ne": self.value}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Gt(QueryOperator):
|
|
29
|
+
"""$gt (greater than) comparison operator."""
|
|
30
|
+
|
|
31
|
+
value: Any = Field(..., description="Value to compare")
|
|
32
|
+
|
|
33
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
34
|
+
return {"$gt": self.value}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Gte(QueryOperator):
|
|
38
|
+
"""$gte (greater than or equal) comparison operator."""
|
|
39
|
+
|
|
40
|
+
value: Any = Field(..., description="Value to compare")
|
|
41
|
+
|
|
42
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
43
|
+
return {"$gte": self.value}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Lt(QueryOperator):
|
|
47
|
+
"""$lt (less than) comparison operator."""
|
|
48
|
+
|
|
49
|
+
value: Any = Field(..., description="Value to compare")
|
|
50
|
+
|
|
51
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
52
|
+
return {"$lt": self.value}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Lte(QueryOperator):
|
|
56
|
+
"""$lte (less than or equal) comparison operator."""
|
|
57
|
+
|
|
58
|
+
value: Any = Field(..., description="Value to compare")
|
|
59
|
+
|
|
60
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
61
|
+
return {"$lte": self.value}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class In(QueryOperator):
|
|
65
|
+
"""$in operator - matches any value in the array."""
|
|
66
|
+
|
|
67
|
+
values: list[Any] = Field(..., description="List of values to match")
|
|
68
|
+
|
|
69
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
70
|
+
return {"$in": self.values}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Nin(QueryOperator):
|
|
74
|
+
"""$nin operator - matches none of the values in the array."""
|
|
75
|
+
|
|
76
|
+
values: list[Any] = Field(..., description="List of values to exclude")
|
|
77
|
+
|
|
78
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
79
|
+
return {"$nin": self.values}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["Eq", "Ne", "Gt", "Gte", "Lt", "Lte", "In", "Nin"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Element query operators for MongoDB aggregation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mongo_aggro.operators.base import QueryOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Exists(QueryOperator):
|
|
11
|
+
"""$exists operator - matches documents where field exists/doesn't."""
|
|
12
|
+
|
|
13
|
+
exists: bool = Field(
|
|
14
|
+
default=True, description="True if field should exist"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
18
|
+
return {"$exists": self.exists}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Type(QueryOperator):
|
|
22
|
+
"""$type operator - matches documents where field is of specified type."""
|
|
23
|
+
|
|
24
|
+
bson_type: str | int | list[str | int] = Field(
|
|
25
|
+
..., description="BSON type(s) to match"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
29
|
+
return {"$type": self.bson_type}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = ["Exists", "Type"]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Geospatial query operators for MongoDB aggregation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mongo_aggro.operators.base import QueryOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GeoIntersects(QueryOperator):
|
|
11
|
+
"""
|
|
12
|
+
$geoIntersects operator - matches geometries that intersect.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> GeoIntersects(geometry={
|
|
16
|
+
... "type": "Polygon",
|
|
17
|
+
... "coordinates": [[[-100, 60], [-100, 0], [100, 0], [100, 60]]]
|
|
18
|
+
... }).model_dump()
|
|
19
|
+
{"$geoIntersects": {"$geometry": {...}}}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
geometry: dict[str, Any] = Field(
|
|
23
|
+
..., description="GeoJSON geometry object"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
27
|
+
return {"$geoIntersects": {"$geometry": self.geometry}}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GeoWithin(QueryOperator):
|
|
31
|
+
"""
|
|
32
|
+
$geoWithin operator - matches geometries within a bounding region.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> GeoWithin(geometry={
|
|
36
|
+
... "type": "Polygon",
|
|
37
|
+
... "coordinates": [[[-100, 60], [-100, 0], [100, 0], [100, 60]]]
|
|
38
|
+
... }).model_dump()
|
|
39
|
+
{"$geoWithin": {"$geometry": {...}}}
|
|
40
|
+
|
|
41
|
+
>>> # Using legacy shapes
|
|
42
|
+
>>> GeoWithin(box=[[-100, -100], [100, 100]]).model_dump()
|
|
43
|
+
{"$geoWithin": {"$box": [[-100, -100], [100, 100]]}}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
geometry: dict[str, Any] | None = Field(
|
|
47
|
+
default=None, description="GeoJSON geometry object"
|
|
48
|
+
)
|
|
49
|
+
box: list[list[float]] | None = Field(
|
|
50
|
+
default=None, description="Legacy box coordinates [[x1,y1], [x2,y2]]"
|
|
51
|
+
)
|
|
52
|
+
polygon: list[list[float]] | None = Field(
|
|
53
|
+
default=None, description="Legacy polygon coordinates"
|
|
54
|
+
)
|
|
55
|
+
center: list[Any] | None = Field(
|
|
56
|
+
default=None, description="Legacy center coordinates [[x,y], radius]"
|
|
57
|
+
)
|
|
58
|
+
center_sphere: list[Any] | None = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
serialization_alias="centerSphere",
|
|
61
|
+
description="Center sphere [[x,y], radius]",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
65
|
+
result: dict[str, Any] = {}
|
|
66
|
+
if self.geometry is not None:
|
|
67
|
+
result["$geometry"] = self.geometry
|
|
68
|
+
if self.box is not None:
|
|
69
|
+
result["$box"] = self.box
|
|
70
|
+
if self.polygon is not None:
|
|
71
|
+
result["$polygon"] = self.polygon
|
|
72
|
+
if self.center is not None:
|
|
73
|
+
result["$center"] = self.center
|
|
74
|
+
if self.center_sphere is not None:
|
|
75
|
+
result["$centerSphere"] = self.center_sphere
|
|
76
|
+
return {"$geoWithin": result}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Near(QueryOperator):
|
|
80
|
+
"""
|
|
81
|
+
$near operator - matches geospatial objects near a point.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> Near(
|
|
85
|
+
... geometry={"type": "Point", "coordinates": [-73.9667, 40.78]},
|
|
86
|
+
... max_distance=5000,
|
|
87
|
+
... min_distance=1000
|
|
88
|
+
... ).model_dump()
|
|
89
|
+
{"$near": {"$geometry": {...}, "$maxDistance": 5000, "$minDistance": 1000}}
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
geometry: dict[str, Any] | None = Field(
|
|
93
|
+
default=None, description="GeoJSON point"
|
|
94
|
+
)
|
|
95
|
+
max_distance: float | None = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
serialization_alias="$maxDistance",
|
|
98
|
+
description="Maximum distance in meters",
|
|
99
|
+
)
|
|
100
|
+
min_distance: float | None = Field(
|
|
101
|
+
default=None,
|
|
102
|
+
serialization_alias="$minDistance",
|
|
103
|
+
description="Minimum distance in meters",
|
|
104
|
+
)
|
|
105
|
+
# Legacy 2d index format
|
|
106
|
+
legacy_point: list[float] | None = Field(
|
|
107
|
+
default=None, description="Legacy [x, y] coordinates"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
111
|
+
if self.legacy_point is not None:
|
|
112
|
+
result: dict[str, Any] = {"$near": self.legacy_point}
|
|
113
|
+
if self.max_distance is not None:
|
|
114
|
+
result["$maxDistance"] = self.max_distance
|
|
115
|
+
return result
|
|
116
|
+
result = {}
|
|
117
|
+
if self.geometry is not None:
|
|
118
|
+
result["$geometry"] = self.geometry
|
|
119
|
+
if self.max_distance is not None:
|
|
120
|
+
result["$maxDistance"] = self.max_distance
|
|
121
|
+
if self.min_distance is not None:
|
|
122
|
+
result["$minDistance"] = self.min_distance
|
|
123
|
+
return {"$near": result}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class NearSphere(QueryOperator):
|
|
127
|
+
"""
|
|
128
|
+
$nearSphere operator - matches geospatial objects near a point on sphere.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> NearSphere(
|
|
132
|
+
... geometry={"type": "Point", "coordinates": [-73.9667, 40.78]},
|
|
133
|
+
... max_distance=5000
|
|
134
|
+
... ).model_dump()
|
|
135
|
+
{"$nearSphere": {"$geometry": {...}, "$maxDistance": 5000}}
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
geometry: dict[str, Any] | None = Field(
|
|
139
|
+
default=None, description="GeoJSON point"
|
|
140
|
+
)
|
|
141
|
+
max_distance: float | None = Field(
|
|
142
|
+
default=None,
|
|
143
|
+
serialization_alias="$maxDistance",
|
|
144
|
+
description="Maximum distance in meters",
|
|
145
|
+
)
|
|
146
|
+
min_distance: float | None = Field(
|
|
147
|
+
default=None,
|
|
148
|
+
serialization_alias="$minDistance",
|
|
149
|
+
description="Minimum distance in meters",
|
|
150
|
+
)
|
|
151
|
+
legacy_point: list[float] | None = Field(
|
|
152
|
+
default=None, description="Legacy [x, y] coordinates"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
156
|
+
if self.legacy_point is not None:
|
|
157
|
+
result: dict[str, Any] = {"$nearSphere": self.legacy_point}
|
|
158
|
+
if self.max_distance is not None:
|
|
159
|
+
result["$maxDistance"] = self.max_distance
|
|
160
|
+
return result
|
|
161
|
+
result = {}
|
|
162
|
+
if self.geometry is not None:
|
|
163
|
+
result["$geometry"] = self.geometry
|
|
164
|
+
if self.max_distance is not None:
|
|
165
|
+
result["$maxDistance"] = self.max_distance
|
|
166
|
+
if self.min_distance is not None:
|
|
167
|
+
result["$minDistance"] = self.min_distance
|
|
168
|
+
return {"$nearSphere": result}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
__all__ = ["GeoIntersects", "GeoWithin", "Near", "NearSphere"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Logical query operators for MongoDB aggregation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mongo_aggro.operators.base import QueryOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class And(QueryOperator):
|
|
11
|
+
"""
|
|
12
|
+
Logical AND operator for combining multiple conditions.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> And(conditions=[
|
|
16
|
+
... {"status": "active"},
|
|
17
|
+
... {"age": {"$gt": 18}}
|
|
18
|
+
... ]).model_dump()
|
|
19
|
+
{"$and": [{"status": "active"}, {"age": {"$gt": 18}}]}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
conditions: list[dict[str, Any]] = Field(
|
|
23
|
+
..., description="List of conditions to AND together"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
27
|
+
return {"$and": self.conditions}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Or(QueryOperator):
|
|
31
|
+
"""
|
|
32
|
+
Logical OR operator for combining multiple conditions.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> Or(conditions=[
|
|
36
|
+
... {"status": "active"},
|
|
37
|
+
... {"status": "pending"}
|
|
38
|
+
... ]).model_dump()
|
|
39
|
+
{"$or": [{"status": "active"}, {"status": "pending"}]}
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
conditions: list[dict[str, Any]] = Field(
|
|
43
|
+
..., description="List of conditions to OR together"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
47
|
+
return {"$or": self.conditions}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Not(QueryOperator):
|
|
51
|
+
"""
|
|
52
|
+
Logical NOT operator for negating a condition.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> Not(condition={"$regex": "^test"}).model_dump()
|
|
56
|
+
{"$not": {"$regex": "^test"}}
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
condition: dict[str, Any] = Field(..., description="Condition to negate")
|
|
60
|
+
|
|
61
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
62
|
+
return {"$not": self.condition}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Nor(QueryOperator):
|
|
66
|
+
"""
|
|
67
|
+
Logical NOR operator - matches documents that fail all conditions.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> Nor(conditions=[
|
|
71
|
+
... {"price": {"$gt": 1000}},
|
|
72
|
+
... {"rating": {"$lt": 3}}
|
|
73
|
+
... ]).model_dump()
|
|
74
|
+
{"$nor": [{"price": {"$gt": 1000}}, {"rating": {"$lt": 3}}]}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
conditions: list[dict[str, Any]] = Field(
|
|
78
|
+
..., description="List of conditions to NOR together"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
82
|
+
return {"$nor": self.conditions}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Expr(QueryOperator):
|
|
86
|
+
"""
|
|
87
|
+
$expr operator for using aggregation expressions in queries.
|
|
88
|
+
|
|
89
|
+
Accepts both raw dicts and expression objects (EqExpr, AndExpr, etc.).
|
|
90
|
+
Expression objects are automatically serialized via model_dump().
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> Expr(expression={"$eq": ["$field1", "$field2"]}).model_dump()
|
|
94
|
+
{"$expr": {"$eq": ["$field1", "$field2"]}}
|
|
95
|
+
|
|
96
|
+
>>> from mongo_aggro.expressions import F, EqExpr
|
|
97
|
+
>>> Expr(expression=(F("status") == "active")).model_dump()
|
|
98
|
+
{"$expr": {"$eq": ["$status", "active"]}}
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
expression: Any = Field(
|
|
102
|
+
..., description="Aggregation expression (dict or ExpressionBase)"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
106
|
+
from mongo_aggro.base import serialize_value
|
|
107
|
+
|
|
108
|
+
return {"$expr": serialize_value(self.expression)}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = ["And", "Or", "Not", "Nor", "Expr"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Miscellaneous query operators for MongoDB aggregation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mongo_aggro.operators.base import QueryOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Mod(QueryOperator):
|
|
11
|
+
"""
|
|
12
|
+
$mod operator - matches where field % divisor == remainder.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> Mod(divisor=4, remainder=0).model_dump()
|
|
16
|
+
{"$mod": [4, 0]}
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
divisor: int = Field(..., description="The divisor value")
|
|
20
|
+
remainder: int = Field(..., description="The remainder value")
|
|
21
|
+
|
|
22
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
23
|
+
return {"$mod": [self.divisor, self.remainder]}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JsonSchema(QueryOperator):
|
|
27
|
+
"""
|
|
28
|
+
$jsonSchema operator - validates documents against JSON Schema.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> JsonSchema(json_schema={
|
|
32
|
+
... "bsonType": "object",
|
|
33
|
+
... "required": ["name", "email"],
|
|
34
|
+
... "properties": {
|
|
35
|
+
... "name": {"bsonType": "string"},
|
|
36
|
+
... "email": {"bsonType": "string"}
|
|
37
|
+
... }
|
|
38
|
+
... }).model_dump()
|
|
39
|
+
{"$jsonSchema": {...}}
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
json_schema: dict[str, Any] = Field(
|
|
43
|
+
..., description="JSON Schema document"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
47
|
+
return {"$jsonSchema": self.json_schema}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Where(QueryOperator):
|
|
51
|
+
"""
|
|
52
|
+
$where operator - matches using JavaScript expression.
|
|
53
|
+
|
|
54
|
+
Note: $where is slow and should be avoided when possible.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> Where(expression="this.credits == this.debits").model_dump()
|
|
58
|
+
{"$where": "this.credits == this.debits"}
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
expression: str = Field(..., description="JavaScript expression string")
|
|
62
|
+
|
|
63
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
64
|
+
return {"$where": self.expression}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Text(QueryOperator):
|
|
68
|
+
"""
|
|
69
|
+
$text operator - performs text search on indexed fields.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> Text(search="coffee shop", language="en").model_dump()
|
|
73
|
+
{"$text": {"$search": "coffee shop", "$language": "en"}}
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
search: str = Field(..., description="Text to search for")
|
|
77
|
+
language: str | None = Field(
|
|
78
|
+
default=None, description="Language for text search"
|
|
79
|
+
)
|
|
80
|
+
case_sensitive: bool | None = Field(
|
|
81
|
+
default=None,
|
|
82
|
+
serialization_alias="$caseSensitive",
|
|
83
|
+
description="Enable case sensitivity",
|
|
84
|
+
)
|
|
85
|
+
diacritic_sensitive: bool | None = Field(
|
|
86
|
+
default=None,
|
|
87
|
+
serialization_alias="$diacriticSensitive",
|
|
88
|
+
description="Enable diacritic sensitivity",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
92
|
+
result: dict[str, Any] = {"$search": self.search}
|
|
93
|
+
if self.language is not None:
|
|
94
|
+
result["$language"] = self.language
|
|
95
|
+
if self.case_sensitive is not None:
|
|
96
|
+
result["$caseSensitive"] = self.case_sensitive
|
|
97
|
+
if self.diacritic_sensitive is not None:
|
|
98
|
+
result["$diacriticSensitive"] = self.diacritic_sensitive
|
|
99
|
+
return {"$text": result}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = ["Mod", "JsonSchema", "Where", "Text"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Regex query operators for MongoDB aggregation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mongo_aggro.operators.base import QueryOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Regex(QueryOperator):
|
|
11
|
+
"""$regex operator for pattern matching."""
|
|
12
|
+
|
|
13
|
+
pattern: str = Field(..., description="Regular expression pattern")
|
|
14
|
+
options: str | None = Field(
|
|
15
|
+
default=None, description="Regex options (i, m, x, s)"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
19
|
+
result: dict[str, Any] = {"$regex": self.pattern}
|
|
20
|
+
if self.options:
|
|
21
|
+
result["$options"] = self.options
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["Regex"]
|