mongo-aggro 0.1.0__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 +188 -0
- mongo_aggro/accumulators.py +474 -0
- mongo_aggro/base.py +195 -0
- mongo_aggro/operators.py +247 -0
- mongo_aggro/stages.py +990 -0
- mongo_aggro-0.1.0.dist-info/METADATA +537 -0
- mongo_aggro-0.1.0.dist-info/RECORD +9 -0
- mongo_aggro-0.1.0.dist-info/WHEEL +4 -0
- mongo_aggro-0.1.0.dist-info/licenses/LICENSE +21 -0
mongo_aggro/__init__.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mongo Aggro - MongoDB Aggregation Pipeline Builder with Pydantic.
|
|
3
|
+
|
|
4
|
+
A Python package for building MongoDB aggregation pipelines with
|
|
5
|
+
strong type checking using Pydantic models.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from mongo_aggro import Pipeline, Match, Unwind, Group, Sort, Limit
|
|
9
|
+
>>>
|
|
10
|
+
>>> pipeline = Pipeline()
|
|
11
|
+
>>> pipeline.add_stage(Match(query={"status": "active"}))
|
|
12
|
+
>>> pipeline.add_stage(Unwind(path="items"))
|
|
13
|
+
>>> pipeline.add_stage(Group(id="$category", count={"$sum": 1}))
|
|
14
|
+
>>> pipeline.add_stage(Sort(fields={"count": -1}))
|
|
15
|
+
>>> pipeline.add_stage(Limit(count=10))
|
|
16
|
+
>>>
|
|
17
|
+
>>> # Pass directly to MongoDB - no need to call any methods
|
|
18
|
+
>>> results = collection.aggregate(pipeline)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .accumulators import (
|
|
22
|
+
Accumulate,
|
|
23
|
+
Accumulator,
|
|
24
|
+
AddToSet,
|
|
25
|
+
Avg,
|
|
26
|
+
BottomN,
|
|
27
|
+
Count_,
|
|
28
|
+
First,
|
|
29
|
+
FirstN,
|
|
30
|
+
Last,
|
|
31
|
+
LastN,
|
|
32
|
+
Max,
|
|
33
|
+
MaxN,
|
|
34
|
+
MergeObjects,
|
|
35
|
+
Min,
|
|
36
|
+
MinN,
|
|
37
|
+
Push,
|
|
38
|
+
StdDevPop,
|
|
39
|
+
StdDevSamp,
|
|
40
|
+
Sum,
|
|
41
|
+
TopN,
|
|
42
|
+
merge_accumulators,
|
|
43
|
+
)
|
|
44
|
+
from .base import (
|
|
45
|
+
ASCENDING,
|
|
46
|
+
DESCENDING,
|
|
47
|
+
AggregationInput,
|
|
48
|
+
BaseStage,
|
|
49
|
+
Pipeline,
|
|
50
|
+
SortSpec,
|
|
51
|
+
)
|
|
52
|
+
from .operators import (
|
|
53
|
+
All,
|
|
54
|
+
And,
|
|
55
|
+
ElemMatch,
|
|
56
|
+
Eq,
|
|
57
|
+
Exists,
|
|
58
|
+
Expr,
|
|
59
|
+
Gt,
|
|
60
|
+
Gte,
|
|
61
|
+
In,
|
|
62
|
+
Lt,
|
|
63
|
+
Lte,
|
|
64
|
+
Ne,
|
|
65
|
+
Nin,
|
|
66
|
+
Nor,
|
|
67
|
+
Not,
|
|
68
|
+
Or,
|
|
69
|
+
QueryOperator,
|
|
70
|
+
Regex,
|
|
71
|
+
Size,
|
|
72
|
+
Type,
|
|
73
|
+
)
|
|
74
|
+
from .stages import (
|
|
75
|
+
AddFields,
|
|
76
|
+
Bucket,
|
|
77
|
+
BucketAuto,
|
|
78
|
+
Count,
|
|
79
|
+
Densify,
|
|
80
|
+
Documents,
|
|
81
|
+
Facet,
|
|
82
|
+
Fill,
|
|
83
|
+
GeoNear,
|
|
84
|
+
GraphLookup,
|
|
85
|
+
Group,
|
|
86
|
+
Limit,
|
|
87
|
+
Lookup,
|
|
88
|
+
Match,
|
|
89
|
+
Merge,
|
|
90
|
+
Out,
|
|
91
|
+
Project,
|
|
92
|
+
Redact,
|
|
93
|
+
ReplaceRoot,
|
|
94
|
+
ReplaceWith,
|
|
95
|
+
Sample,
|
|
96
|
+
Set,
|
|
97
|
+
SetWindowFields,
|
|
98
|
+
Skip,
|
|
99
|
+
Sort,
|
|
100
|
+
SortByCount,
|
|
101
|
+
UnionWith,
|
|
102
|
+
Unset,
|
|
103
|
+
Unwind,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
__all__ = [
|
|
107
|
+
# Base classes and types
|
|
108
|
+
"Pipeline",
|
|
109
|
+
"BaseStage",
|
|
110
|
+
"SortSpec",
|
|
111
|
+
"AggregationInput",
|
|
112
|
+
# Sort direction constants
|
|
113
|
+
"ASCENDING",
|
|
114
|
+
"DESCENDING",
|
|
115
|
+
# Query operators
|
|
116
|
+
"QueryOperator",
|
|
117
|
+
"And",
|
|
118
|
+
"Or",
|
|
119
|
+
"Not",
|
|
120
|
+
"Nor",
|
|
121
|
+
"Expr",
|
|
122
|
+
"Eq",
|
|
123
|
+
"Ne",
|
|
124
|
+
"Gt",
|
|
125
|
+
"Gte",
|
|
126
|
+
"Lt",
|
|
127
|
+
"Lte",
|
|
128
|
+
"In",
|
|
129
|
+
"Nin",
|
|
130
|
+
"Regex",
|
|
131
|
+
"Exists",
|
|
132
|
+
"Type",
|
|
133
|
+
"ElemMatch",
|
|
134
|
+
"Size",
|
|
135
|
+
"All",
|
|
136
|
+
# Accumulators
|
|
137
|
+
"Accumulator",
|
|
138
|
+
"Sum",
|
|
139
|
+
"Avg",
|
|
140
|
+
"Min",
|
|
141
|
+
"Max",
|
|
142
|
+
"First",
|
|
143
|
+
"Last",
|
|
144
|
+
"Push",
|
|
145
|
+
"AddToSet",
|
|
146
|
+
"StdDevPop",
|
|
147
|
+
"StdDevSamp",
|
|
148
|
+
"Count_",
|
|
149
|
+
"MergeObjects",
|
|
150
|
+
"Accumulate",
|
|
151
|
+
"TopN",
|
|
152
|
+
"BottomN",
|
|
153
|
+
"FirstN",
|
|
154
|
+
"LastN",
|
|
155
|
+
"MaxN",
|
|
156
|
+
"MinN",
|
|
157
|
+
"merge_accumulators",
|
|
158
|
+
# Stages
|
|
159
|
+
"Match",
|
|
160
|
+
"Project",
|
|
161
|
+
"Group",
|
|
162
|
+
"Sort",
|
|
163
|
+
"Limit",
|
|
164
|
+
"Skip",
|
|
165
|
+
"Unwind",
|
|
166
|
+
"Lookup",
|
|
167
|
+
"AddFields",
|
|
168
|
+
"Set",
|
|
169
|
+
"Unset",
|
|
170
|
+
"Count",
|
|
171
|
+
"SortByCount",
|
|
172
|
+
"Facet",
|
|
173
|
+
"Bucket",
|
|
174
|
+
"BucketAuto",
|
|
175
|
+
"ReplaceRoot",
|
|
176
|
+
"ReplaceWith",
|
|
177
|
+
"Sample",
|
|
178
|
+
"Out",
|
|
179
|
+
"Merge",
|
|
180
|
+
"Redact",
|
|
181
|
+
"UnionWith",
|
|
182
|
+
"GeoNear",
|
|
183
|
+
"GraphLookup",
|
|
184
|
+
"SetWindowFields",
|
|
185
|
+
"Densify",
|
|
186
|
+
"Fill",
|
|
187
|
+
"Documents",
|
|
188
|
+
]
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""Accumulator operators for MongoDB $group stage."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Accumulator(BaseModel):
|
|
9
|
+
"""Base class for accumulator operators used in $group stage."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(
|
|
12
|
+
populate_by_name=True,
|
|
13
|
+
extra="forbid",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
name: str = Field(..., description="Output field name")
|
|
17
|
+
|
|
18
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Sum(Accumulator):
|
|
23
|
+
"""
|
|
24
|
+
$sum accumulator - sums numeric values.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> Sum(name="totalQuantity", field="quantity").model_dump()
|
|
28
|
+
{"totalQuantity": {"$sum": "$quantity"}}
|
|
29
|
+
|
|
30
|
+
>>> Sum(name="count", value=1).model_dump()
|
|
31
|
+
{"count": {"$sum": 1}}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
field: str | None = Field(
|
|
35
|
+
default=None, description="Field path to sum (without $)"
|
|
36
|
+
)
|
|
37
|
+
value: int | float | None = Field(
|
|
38
|
+
default=None, description="Literal value to sum (e.g., 1 for counting)"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
42
|
+
if self.field is not None:
|
|
43
|
+
field_path = (
|
|
44
|
+
f"${self.field}"
|
|
45
|
+
if not self.field.startswith("$")
|
|
46
|
+
else self.field
|
|
47
|
+
)
|
|
48
|
+
return {self.name: {"$sum": field_path}}
|
|
49
|
+
return {self.name: {"$sum": self.value}}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Avg(Accumulator):
|
|
53
|
+
"""
|
|
54
|
+
$avg accumulator - calculates average of numeric values.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> Avg(name="avgPrice", field="price").model_dump()
|
|
58
|
+
{"avgPrice": {"$avg": "$price"}}
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
field: str = Field(..., description="Field path to average (without $)")
|
|
62
|
+
|
|
63
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
64
|
+
field_path = (
|
|
65
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
66
|
+
)
|
|
67
|
+
return {self.name: {"$avg": field_path}}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Min(Accumulator):
|
|
71
|
+
"""
|
|
72
|
+
$min accumulator - returns minimum value.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> Min(name="minPrice", field="price").model_dump()
|
|
76
|
+
{"minPrice": {"$min": "$price"}}
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
field: str = Field(..., description="Field path (without $)")
|
|
80
|
+
|
|
81
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
82
|
+
field_path = (
|
|
83
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
84
|
+
)
|
|
85
|
+
return {self.name: {"$min": field_path}}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Max(Accumulator):
|
|
89
|
+
"""
|
|
90
|
+
$max accumulator - returns maximum value.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> Max(name="maxPrice", field="price").model_dump()
|
|
94
|
+
{"maxPrice": {"$max": "$price"}}
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
field: str = Field(..., description="Field path (without $)")
|
|
98
|
+
|
|
99
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
100
|
+
field_path = (
|
|
101
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
102
|
+
)
|
|
103
|
+
return {self.name: {"$max": field_path}}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class First(Accumulator):
|
|
107
|
+
"""
|
|
108
|
+
$first accumulator - returns first value in group.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> First(name="firstItem", field="item").model_dump()
|
|
112
|
+
{"firstItem": {"$first": "$item"}}
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
field: str = Field(..., description="Field path (without $)")
|
|
116
|
+
|
|
117
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
118
|
+
field_path = (
|
|
119
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
120
|
+
)
|
|
121
|
+
return {self.name: {"$first": field_path}}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Last(Accumulator):
|
|
125
|
+
"""
|
|
126
|
+
$last accumulator - returns last value in group.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> Last(name="lastItem", field="item").model_dump()
|
|
130
|
+
{"lastItem": {"$last": "$item"}}
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
field: str = Field(..., description="Field path (without $)")
|
|
134
|
+
|
|
135
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
136
|
+
field_path = (
|
|
137
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
138
|
+
)
|
|
139
|
+
return {self.name: {"$last": field_path}}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Push(Accumulator):
|
|
143
|
+
"""
|
|
144
|
+
$push accumulator - creates array of values.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> Push(name="items", field="item").model_dump()
|
|
148
|
+
{"items": {"$push": "$item"}}
|
|
149
|
+
|
|
150
|
+
>>> Push(
|
|
151
|
+
name="details",
|
|
152
|
+
expression={"name": "$name", "qty": "$qty"}
|
|
153
|
+
).model_dump()
|
|
154
|
+
{"details": {"$push": {"name": "$name", "qty": "$qty"}}}
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
field: str | None = Field(
|
|
158
|
+
default=None, description="Field path to push (without $)"
|
|
159
|
+
)
|
|
160
|
+
expression: dict[str, Any] | None = Field(
|
|
161
|
+
default=None, description="Expression to push"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
165
|
+
if self.expression is not None:
|
|
166
|
+
return {self.name: {"$push": self.expression}}
|
|
167
|
+
field_path = (
|
|
168
|
+
f"${self.field}"
|
|
169
|
+
if self.field and not self.field.startswith("$")
|
|
170
|
+
else self.field
|
|
171
|
+
)
|
|
172
|
+
return {self.name: {"$push": field_path}}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AddToSet(Accumulator):
|
|
176
|
+
"""
|
|
177
|
+
$addToSet accumulator - creates array of unique values.
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> AddToSet(name="uniqueTags", field="tag").model_dump()
|
|
181
|
+
{"uniqueTags": {"$addToSet": "$tag"}}
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
field: str = Field(..., description="Field path (without $)")
|
|
185
|
+
|
|
186
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
187
|
+
field_path = (
|
|
188
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
189
|
+
)
|
|
190
|
+
return {self.name: {"$addToSet": field_path}}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class StdDevPop(Accumulator):
|
|
194
|
+
"""
|
|
195
|
+
$stdDevPop accumulator - population standard deviation.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> StdDevPop(name="stdDev", field="score").model_dump()
|
|
199
|
+
{"stdDev": {"$stdDevPop": "$score"}}
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
field: str = Field(..., description="Field path (without $)")
|
|
203
|
+
|
|
204
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
205
|
+
field_path = (
|
|
206
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
207
|
+
)
|
|
208
|
+
return {self.name: {"$stdDevPop": field_path}}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class StdDevSamp(Accumulator):
|
|
212
|
+
"""
|
|
213
|
+
$stdDevSamp accumulator - sample standard deviation.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> StdDevSamp(name="stdDevSample", field="score").model_dump()
|
|
217
|
+
{"stdDevSample": {"$stdDevSamp": "$score"}}
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
field: str = Field(..., description="Field path (without $)")
|
|
221
|
+
|
|
222
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
223
|
+
field_path = (
|
|
224
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
225
|
+
)
|
|
226
|
+
return {self.name: {"$stdDevSamp": field_path}}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Count_(Accumulator):
|
|
230
|
+
"""
|
|
231
|
+
$count accumulator - counts documents in group (MongoDB 5.0+).
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
>>> Count_(name="totalDocs").model_dump()
|
|
235
|
+
{"totalDocs": {"$count": {}}}
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
239
|
+
return {self.name: {"$count": {}}}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class MergeObjects(Accumulator):
|
|
243
|
+
"""
|
|
244
|
+
$mergeObjects accumulator - merges documents into single document.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
>>> MergeObjects(name="merged", field="details").model_dump()
|
|
248
|
+
{"merged": {"$mergeObjects": "$details"}}
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
field: str = Field(..., description="Field path (without $)")
|
|
252
|
+
|
|
253
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
254
|
+
field_path = (
|
|
255
|
+
f"${self.field}" if not self.field.startswith("$") else self.field
|
|
256
|
+
)
|
|
257
|
+
return {self.name: {"$mergeObjects": field_path}}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Accumulate(Accumulator):
|
|
261
|
+
"""
|
|
262
|
+
$accumulator - custom JavaScript accumulator (MongoDB 4.4+).
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
>>> Accumulate( # noqa
|
|
266
|
+
... name="custom",
|
|
267
|
+
... init="function() { return { count: 0 } }",
|
|
268
|
+
... accumulate="function(state, val) { state.count++; return state }",
|
|
269
|
+
... merge="function(s1, s2) { return { count: s1.count + s2.count } }",
|
|
270
|
+
... finalize="function(state) { return state.count }",
|
|
271
|
+
... lang="js"
|
|
272
|
+
... ).model_dump()
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
init: str = Field(..., description="Init function")
|
|
276
|
+
accumulate: str = Field(..., description="Accumulate function")
|
|
277
|
+
merge: str = Field(..., description="Merge function")
|
|
278
|
+
finalize: str | None = Field(default=None, description="Finalize function")
|
|
279
|
+
init_args: list[Any] | None = Field(
|
|
280
|
+
default=None,
|
|
281
|
+
validation_alias="initArgs",
|
|
282
|
+
serialization_alias="initArgs",
|
|
283
|
+
description="Init function arguments",
|
|
284
|
+
)
|
|
285
|
+
accumulate_args: list[Any] | None = Field(
|
|
286
|
+
default=None,
|
|
287
|
+
validation_alias="accumulateArgs",
|
|
288
|
+
serialization_alias="accumulateArgs",
|
|
289
|
+
description="Accumulate function arguments",
|
|
290
|
+
)
|
|
291
|
+
lang: str = Field(default="js", description="Language (js)")
|
|
292
|
+
|
|
293
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
294
|
+
result: dict[str, Any] = {
|
|
295
|
+
"init": self.init,
|
|
296
|
+
"accumulate": self.accumulate,
|
|
297
|
+
"merge": self.merge,
|
|
298
|
+
"lang": self.lang,
|
|
299
|
+
}
|
|
300
|
+
if self.finalize is not None:
|
|
301
|
+
result["finalize"] = self.finalize
|
|
302
|
+
if self.init_args is not None:
|
|
303
|
+
result["initArgs"] = self.init_args
|
|
304
|
+
if self.accumulate_args is not None:
|
|
305
|
+
result["accumulateArgs"] = self.accumulate_args
|
|
306
|
+
return {self.name: {"$accumulator": result}}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class TopN(Accumulator):
|
|
310
|
+
"""
|
|
311
|
+
$topN accumulator - returns top N elements (MongoDB 5.2+).
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
>>> TopN(
|
|
315
|
+
... name="top3",
|
|
316
|
+
... n=3,
|
|
317
|
+
... sort_by={"score": -1},
|
|
318
|
+
... output="$item"
|
|
319
|
+
... ).model_dump()
|
|
320
|
+
{
|
|
321
|
+
"top3": {
|
|
322
|
+
"$topN": {"n": 3, "sortBy": {"score": -1},
|
|
323
|
+
"output": "$item"}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
n: int = Field(..., gt=0, description="Number of results")
|
|
329
|
+
sort_by: dict[str, int] = Field(
|
|
330
|
+
...,
|
|
331
|
+
validation_alias="sortBy",
|
|
332
|
+
serialization_alias="sortBy",
|
|
333
|
+
description="Sort specification",
|
|
334
|
+
)
|
|
335
|
+
output: str | dict[str, Any] = Field(..., description="Output expression")
|
|
336
|
+
|
|
337
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
338
|
+
return {
|
|
339
|
+
self.name: {
|
|
340
|
+
"$topN": {
|
|
341
|
+
"n": self.n,
|
|
342
|
+
"sortBy": self.sort_by,
|
|
343
|
+
"output": self.output,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class BottomN(Accumulator):
|
|
350
|
+
"""
|
|
351
|
+
$bottomN accumulator - returns bottom N elements (MongoDB 5.2+).
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> BottomN(
|
|
355
|
+
... name="bottom3",
|
|
356
|
+
... n=3,
|
|
357
|
+
... sort_by={"score": -1},
|
|
358
|
+
... output="$item"
|
|
359
|
+
... ).model_dump()
|
|
360
|
+
{
|
|
361
|
+
"bottom3": {
|
|
362
|
+
"$bottomN": {"n": 3, "sortBy": {"score": -1},
|
|
363
|
+
"output": "$item"}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
n: int = Field(..., gt=0, description="Number of results")
|
|
369
|
+
sort_by: dict[str, int] = Field(
|
|
370
|
+
...,
|
|
371
|
+
validation_alias="sortBy",
|
|
372
|
+
serialization_alias="sortBy",
|
|
373
|
+
description="Sort specification",
|
|
374
|
+
)
|
|
375
|
+
output: str | dict[str, Any] = Field(..., description="Output expression")
|
|
376
|
+
|
|
377
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
378
|
+
return {
|
|
379
|
+
self.name: {
|
|
380
|
+
"$bottomN": {
|
|
381
|
+
"n": self.n,
|
|
382
|
+
"sortBy": self.sort_by,
|
|
383
|
+
"output": self.output,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class FirstN(Accumulator):
|
|
390
|
+
"""
|
|
391
|
+
$firstN accumulator - returns first N elements (MongoDB 5.2+).
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
>>> FirstN(name="first3", n=3, input="$item").model_dump()
|
|
395
|
+
{"first3": {"$firstN": {"n": 3, "input": "$item"}}}
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
n: int = Field(..., gt=0, description="Number of results")
|
|
399
|
+
input: str | dict[str, Any] = Field(..., description="Input expression")
|
|
400
|
+
|
|
401
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
402
|
+
return {self.name: {"$firstN": {"n": self.n, "input": self.input}}}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class LastN(Accumulator):
|
|
406
|
+
"""
|
|
407
|
+
$lastN accumulator - returns last N elements (MongoDB 5.2+).
|
|
408
|
+
|
|
409
|
+
Example:
|
|
410
|
+
>>> LastN(name="last3", n=3, input="$item").model_dump()
|
|
411
|
+
{"last3": {"$lastN": {"n": 3, "input": "$item"}}}
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
n: int = Field(..., gt=0, description="Number of results")
|
|
415
|
+
input: str | dict[str, Any] = Field(..., description="Input expression")
|
|
416
|
+
|
|
417
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
418
|
+
return {self.name: {"$lastN": {"n": self.n, "input": self.input}}}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class MaxN(Accumulator):
|
|
422
|
+
"""
|
|
423
|
+
$maxN accumulator - returns N maximum values (MongoDB 5.2+).
|
|
424
|
+
|
|
425
|
+
Example:
|
|
426
|
+
>>> MaxN(name="top3Scores", n=3, input="$score").model_dump()
|
|
427
|
+
{"top3Scores": {"$maxN": {"n": 3, "input": "$score"}}}
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
n: int = Field(..., gt=0, description="Number of results")
|
|
431
|
+
input: str | dict[str, Any] = Field(..., description="Input expression")
|
|
432
|
+
|
|
433
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
434
|
+
return {self.name: {"$maxN": {"n": self.n, "input": self.input}}}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class MinN(Accumulator):
|
|
438
|
+
"""
|
|
439
|
+
$minN accumulator - returns N minimum values (MongoDB 5.2+).
|
|
440
|
+
|
|
441
|
+
Example:
|
|
442
|
+
>>> MinN(name="lowest3", n=3, input="$score").model_dump()
|
|
443
|
+
{"lowest3": {"$minN": {"n": 3, "input": "$score"}}}
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
n: int = Field(..., gt=0, description="Number of results")
|
|
447
|
+
input: str | dict[str, Any] = Field(..., description="Input expression")
|
|
448
|
+
|
|
449
|
+
def model_dump(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
450
|
+
return {self.name: {"$minN": {"n": self.n, "input": self.input}}}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def merge_accumulators(*accumulators: Accumulator) -> dict[str, Any]:
|
|
454
|
+
"""
|
|
455
|
+
Merge multiple accumulators into a single dictionary for Group stage.
|
|
456
|
+
|
|
457
|
+
Example:
|
|
458
|
+
>>> merge_accumulators(
|
|
459
|
+
... Sum(name="total", field="amount"),
|
|
460
|
+
... Avg(name="average", field="amount"),
|
|
461
|
+
... Count_(name="count")
|
|
462
|
+
... )
|
|
463
|
+
{
|
|
464
|
+
"total": {
|
|
465
|
+
"$sum": "$amount"},
|
|
466
|
+
"average": {"$avg": "$amount"},
|
|
467
|
+
"count": {"$count": {}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
"""
|
|
471
|
+
result: dict[str, Any] = {}
|
|
472
|
+
for acc in accumulators:
|
|
473
|
+
result.update(acc.model_dump())
|
|
474
|
+
return result
|