mongo-pipebuilder 0.3.1__py3-none-any.whl → 0.4.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_pipebuilder/__init__.py +1 -1
- mongo_pipebuilder/builder.py +274 -139
- {mongo_pipebuilder-0.3.1.dist-info → mongo_pipebuilder-0.4.0.dist-info}/METADATA +73 -3
- mongo_pipebuilder-0.4.0.dist-info/RECORD +7 -0
- {mongo_pipebuilder-0.3.1.dist-info → mongo_pipebuilder-0.4.0.dist-info}/WHEEL +1 -1
- mongo_pipebuilder-0.3.1.dist-info/RECORD +0 -7
- {mongo_pipebuilder-0.3.1.dist-info → mongo_pipebuilder-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {mongo_pipebuilder-0.3.1.dist-info → mongo_pipebuilder-0.4.0.dist-info}/top_level.txt +0 -0
mongo_pipebuilder/__init__.py
CHANGED
mongo_pipebuilder/builder.py
CHANGED
|
@@ -12,11 +12,8 @@ import json
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any, Dict, List, Optional, Union
|
|
14
14
|
|
|
15
|
-
# For compatibility with Python < 3.11
|
|
16
|
-
|
|
17
|
-
from typing import Self
|
|
18
|
-
except ImportError:
|
|
19
|
-
from typing_extensions import Self
|
|
15
|
+
# For compatibility with Python < 3.11 and mypy with python_version 3.8
|
|
16
|
+
from typing_extensions import Self
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
class PipelineBuilder:
|
|
@@ -29,16 +26,16 @@ class PipelineBuilder:
|
|
|
29
26
|
def match(self, conditions: Dict[str, Any]) -> Self:
|
|
30
27
|
"""
|
|
31
28
|
Add a $match stage for filtering documents.
|
|
32
|
-
|
|
29
|
+
|
|
33
30
|
Args:
|
|
34
31
|
conditions: Dictionary with filtering conditions
|
|
35
|
-
|
|
32
|
+
|
|
36
33
|
Returns:
|
|
37
34
|
Self for method chaining
|
|
38
|
-
|
|
35
|
+
|
|
39
36
|
Raises:
|
|
40
37
|
TypeError: If conditions is None or not a dictionary
|
|
41
|
-
|
|
38
|
+
|
|
42
39
|
Example:
|
|
43
40
|
>>> builder.match({"status": "active", "age": {"$gte": 18}})
|
|
44
41
|
"""
|
|
@@ -50,6 +47,29 @@ class PipelineBuilder:
|
|
|
50
47
|
self._stages.append({"$match": conditions})
|
|
51
48
|
return self
|
|
52
49
|
|
|
50
|
+
def match_expr(self, expr: Dict[str, Any]) -> Self:
|
|
51
|
+
"""
|
|
52
|
+
Add a $match stage with $expr condition (expression-based filter).
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
expr: The expression for $expr (e.g. {"$eq": ["$id", "$$teamId"]}).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Self for method chaining.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
TypeError: If expr is None or not a dict.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> builder.match_expr({"$eq": ["$id", "$$teamId"]})
|
|
65
|
+
"""
|
|
66
|
+
if expr is None:
|
|
67
|
+
raise TypeError("expr cannot be None, use empty dict {} instead")
|
|
68
|
+
if not isinstance(expr, dict):
|
|
69
|
+
raise TypeError(f"expr must be a dict, got {type(expr)}")
|
|
70
|
+
self._stages.append({"$match": {"$expr": expr}})
|
|
71
|
+
return self
|
|
72
|
+
|
|
53
73
|
def lookup(
|
|
54
74
|
self,
|
|
55
75
|
from_collection: str,
|
|
@@ -60,21 +80,21 @@ class PipelineBuilder:
|
|
|
60
80
|
) -> Self:
|
|
61
81
|
"""
|
|
62
82
|
Add a $lookup stage for joining with another collection.
|
|
63
|
-
|
|
83
|
+
|
|
64
84
|
Args:
|
|
65
85
|
from_collection: Name of the collection to join with
|
|
66
86
|
local_field: Field in the current collection
|
|
67
87
|
foreign_field: Field in the target collection
|
|
68
88
|
as_field: Name of the field for join results
|
|
69
89
|
pipeline: Optional nested pipeline for filtering
|
|
70
|
-
|
|
90
|
+
|
|
71
91
|
Returns:
|
|
72
92
|
Self for method chaining
|
|
73
|
-
|
|
93
|
+
|
|
74
94
|
Raises:
|
|
75
95
|
TypeError: If pipeline is not None and not a list, or if string fields are not strings
|
|
76
96
|
ValueError: If required string fields are empty
|
|
77
|
-
|
|
97
|
+
|
|
78
98
|
Example:
|
|
79
99
|
>>> builder.lookup(
|
|
80
100
|
... from_collection="users",
|
|
@@ -92,14 +112,14 @@ class PipelineBuilder:
|
|
|
92
112
|
raise ValueError("foreign_field must be a non-empty string")
|
|
93
113
|
if not isinstance(as_field, str) or not as_field:
|
|
94
114
|
raise ValueError("as_field must be a non-empty string")
|
|
95
|
-
|
|
115
|
+
|
|
96
116
|
# Validate pipeline
|
|
97
117
|
if pipeline is not None:
|
|
98
118
|
if not isinstance(pipeline, list):
|
|
99
119
|
raise TypeError(f"pipeline must be a list, got {type(pipeline)}")
|
|
100
120
|
if not all(isinstance(stage, dict) for stage in pipeline):
|
|
101
121
|
raise TypeError("All pipeline stages must be dictionaries")
|
|
102
|
-
|
|
122
|
+
|
|
103
123
|
lookup_stage: Dict[str, Any] = {
|
|
104
124
|
"from": from_collection,
|
|
105
125
|
"localField": local_field,
|
|
@@ -111,19 +131,133 @@ class PipelineBuilder:
|
|
|
111
131
|
self._stages.append({"$lookup": lookup_stage})
|
|
112
132
|
return self
|
|
113
133
|
|
|
134
|
+
def lookup_let(
|
|
135
|
+
self,
|
|
136
|
+
from_collection: str,
|
|
137
|
+
let: Dict[str, Any],
|
|
138
|
+
pipeline: Union[List[Dict[str, Any]], "PipelineBuilder"],
|
|
139
|
+
as_field: str,
|
|
140
|
+
) -> Self:
|
|
141
|
+
"""
|
|
142
|
+
Add a $lookup stage with let and pipeline (join by expression, variables from document).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
from_collection: Name of the collection to join with.
|
|
146
|
+
let: Variables for the subpipeline (available in pipeline as $$key).
|
|
147
|
+
pipeline: Subpipeline as list of stages or PipelineBuilder (will call .build()).
|
|
148
|
+
as_field: Name of the field for join results.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Self for method chaining.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
TypeError: If from_collection, as_field are not strings; if let is not a dict;
|
|
155
|
+
if pipeline is None or not a list/PipelineBuilder; if pipeline list has non-dict stages.
|
|
156
|
+
ValueError: If from_collection or as_field are empty; if pipeline list is empty.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> builder.lookup_let(
|
|
160
|
+
... from_collection="teams",
|
|
161
|
+
... let={"teamId": "$idTeam"},
|
|
162
|
+
... pipeline=[{"$match": {"$expr": {"$eq": ["$_id", "$$teamId"]}}}],
|
|
163
|
+
... as_field="team"
|
|
164
|
+
... )
|
|
165
|
+
"""
|
|
166
|
+
if not isinstance(from_collection, str):
|
|
167
|
+
raise TypeError("from_collection must be a string")
|
|
168
|
+
if not from_collection:
|
|
169
|
+
raise ValueError("from_collection must be a non-empty string")
|
|
170
|
+
if let is None:
|
|
171
|
+
raise TypeError("let cannot be None")
|
|
172
|
+
if not isinstance(let, dict):
|
|
173
|
+
raise TypeError("let must be a dict")
|
|
174
|
+
if not isinstance(as_field, str):
|
|
175
|
+
raise TypeError("as_field must be a string")
|
|
176
|
+
if not as_field:
|
|
177
|
+
raise ValueError("as_field must be a non-empty string")
|
|
178
|
+
if pipeline is None:
|
|
179
|
+
raise TypeError("pipeline cannot be None")
|
|
180
|
+
if isinstance(pipeline, PipelineBuilder):
|
|
181
|
+
pipeline = pipeline.build()
|
|
182
|
+
if not isinstance(pipeline, list):
|
|
183
|
+
raise TypeError("pipeline must be a list or PipelineBuilder")
|
|
184
|
+
if not pipeline:
|
|
185
|
+
raise ValueError("pipeline cannot be empty")
|
|
186
|
+
if not all(isinstance(stage, dict) for stage in pipeline):
|
|
187
|
+
raise TypeError("All pipeline stages must be dictionaries")
|
|
188
|
+
|
|
189
|
+
lookup_stage: Dict[str, Any] = {
|
|
190
|
+
"from": from_collection,
|
|
191
|
+
"let": let,
|
|
192
|
+
"pipeline": pipeline,
|
|
193
|
+
"as": as_field,
|
|
194
|
+
}
|
|
195
|
+
self._stages.append({"$lookup": lookup_stage})
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
def union_with(
|
|
199
|
+
self,
|
|
200
|
+
coll: str,
|
|
201
|
+
pipeline: Optional[Union[List[Dict[str, Any]], "PipelineBuilder"]] = None,
|
|
202
|
+
) -> Self:
|
|
203
|
+
"""
|
|
204
|
+
Add a $unionWith stage to combine documents from another collection.
|
|
205
|
+
|
|
206
|
+
Merges all documents from the current pipeline with documents from the
|
|
207
|
+
given collection. If pipeline is provided, it is run on the other
|
|
208
|
+
collection before merging; you can pass a list of stages or a
|
|
209
|
+
PipelineBuilder (its .build() is used internally).
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
coll: Name of the collection to union with.
|
|
213
|
+
pipeline: Optional subpipeline (list of stages or PipelineBuilder).
|
|
214
|
+
Defaults to [].
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Self for method chaining.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
TypeError: If coll is not a string; if pipeline is not None and not
|
|
221
|
+
a list or PipelineBuilder; if pipeline list contains non-dict
|
|
222
|
+
stages.
|
|
223
|
+
ValueError: If coll is empty.
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
>>> builder.union_with("other_coll")
|
|
227
|
+
>>> builder.union_with("logs", [{"$match": {"level": "error"}}])
|
|
228
|
+
>>> sub = PipelineBuilder().match({"source": "x"}).project({"n": 1})
|
|
229
|
+
>>> builder.union_with("stats", sub)
|
|
230
|
+
"""
|
|
231
|
+
if not isinstance(coll, str):
|
|
232
|
+
raise TypeError("coll must be a string")
|
|
233
|
+
if not coll:
|
|
234
|
+
raise ValueError("coll must be a non-empty string")
|
|
235
|
+
pipeline_list: List[Dict[str, Any]] = []
|
|
236
|
+
if pipeline is not None:
|
|
237
|
+
if isinstance(pipeline, PipelineBuilder):
|
|
238
|
+
pipeline_list = pipeline.build()
|
|
239
|
+
elif isinstance(pipeline, list):
|
|
240
|
+
if not all(isinstance(stage, dict) for stage in pipeline):
|
|
241
|
+
raise TypeError("All pipeline stages must be dictionaries")
|
|
242
|
+
pipeline_list = pipeline
|
|
243
|
+
else:
|
|
244
|
+
raise TypeError("pipeline must be a list or PipelineBuilder")
|
|
245
|
+
self._stages.append({"$unionWith": {"coll": coll, "pipeline": pipeline_list}})
|
|
246
|
+
return self
|
|
247
|
+
|
|
114
248
|
def add_fields(self, fields: Dict[str, Any]) -> Self:
|
|
115
249
|
"""
|
|
116
250
|
Add an $addFields stage for adding or modifying fields.
|
|
117
|
-
|
|
251
|
+
|
|
118
252
|
Args:
|
|
119
253
|
fields: Dictionary with new fields and their expressions
|
|
120
|
-
|
|
254
|
+
|
|
121
255
|
Returns:
|
|
122
256
|
Self for method chaining
|
|
123
|
-
|
|
257
|
+
|
|
124
258
|
Raises:
|
|
125
259
|
TypeError: If fields is not a dictionary
|
|
126
|
-
|
|
260
|
+
|
|
127
261
|
Example:
|
|
128
262
|
>>> builder.add_fields({
|
|
129
263
|
... "fullName": {"$concat": ["$firstName", " ", "$lastName"]}
|
|
@@ -140,16 +274,16 @@ class PipelineBuilder:
|
|
|
140
274
|
def project(self, fields: Dict[str, Any]) -> Self:
|
|
141
275
|
"""
|
|
142
276
|
Add a $project stage for reshaping documents.
|
|
143
|
-
|
|
277
|
+
|
|
144
278
|
Args:
|
|
145
279
|
fields: Dictionary with fields to include/exclude or transform
|
|
146
|
-
|
|
280
|
+
|
|
147
281
|
Returns:
|
|
148
282
|
Self for method chaining
|
|
149
|
-
|
|
283
|
+
|
|
150
284
|
Raises:
|
|
151
285
|
TypeError: If fields is not a dictionary
|
|
152
|
-
|
|
286
|
+
|
|
153
287
|
Example:
|
|
154
288
|
>>> builder.project({"name": 1, "email": 1, "_id": 0})
|
|
155
289
|
"""
|
|
@@ -164,21 +298,21 @@ class PipelineBuilder:
|
|
|
164
298
|
def group(self, group_by: Union[str, Dict[str, Any], Any], accumulators: Dict[str, Any]) -> Self:
|
|
165
299
|
"""
|
|
166
300
|
Add a $group stage for grouping documents.
|
|
167
|
-
|
|
301
|
+
|
|
168
302
|
Args:
|
|
169
303
|
group_by: Expression for grouping (becomes _id). Can be:
|
|
170
304
|
- A string (field path, e.g., "$category")
|
|
171
305
|
- A dict (composite key, e.g., {"category": "$category"})
|
|
172
306
|
- Any other value (null, number, etc.)
|
|
173
307
|
accumulators: Dictionary with accumulators (sum, avg, count, etc.)
|
|
174
|
-
|
|
308
|
+
|
|
175
309
|
Returns:
|
|
176
310
|
Self for method chaining
|
|
177
|
-
|
|
311
|
+
|
|
178
312
|
Raises:
|
|
179
313
|
TypeError: If accumulators is not a dictionary
|
|
180
314
|
ValueError: If both group_by and accumulators are empty (when group_by is dict/str)
|
|
181
|
-
|
|
315
|
+
|
|
182
316
|
Example:
|
|
183
317
|
>>> builder.group(
|
|
184
318
|
... group_by="$category", # String field path
|
|
@@ -212,9 +346,10 @@ class PipelineBuilder:
|
|
|
212
346
|
"- Composite key: builder.group(group_by={'category': '$category'}, accumulators={...})\n"
|
|
213
347
|
"\n"
|
|
214
348
|
"Why this matters: {'_id': expr} would create a nested _id object in MongoDB, and later\n"
|
|
215
|
-
"operators like $first/$last on '$_id' may fail with:
|
|
349
|
+
"operators like $first/$last on '$_id' may fail with: "
|
|
350
|
+
"\"$first's argument must be an array, but is object\"."
|
|
216
351
|
)
|
|
217
|
-
|
|
352
|
+
|
|
218
353
|
# Validate empty cases
|
|
219
354
|
# group_by can be None, empty string, empty dict, etc. - all are valid in MongoDB
|
|
220
355
|
# But if it's a string and empty, or dict and empty, and accumulators is also empty,
|
|
@@ -225,7 +360,7 @@ class PipelineBuilder:
|
|
|
225
360
|
elif isinstance(group_by, str):
|
|
226
361
|
if not group_by and not accumulators:
|
|
227
362
|
raise ValueError("group_by and accumulators cannot both be empty")
|
|
228
|
-
|
|
363
|
+
|
|
229
364
|
group_stage = {"_id": group_by, **accumulators}
|
|
230
365
|
self._stages.append({"$group": group_stage})
|
|
231
366
|
return self
|
|
@@ -238,19 +373,19 @@ class PipelineBuilder:
|
|
|
238
373
|
) -> Self:
|
|
239
374
|
"""
|
|
240
375
|
Add an $unwind stage for unwinding arrays.
|
|
241
|
-
|
|
376
|
+
|
|
242
377
|
Args:
|
|
243
378
|
path: Path to the array field
|
|
244
379
|
preserve_null_and_empty_arrays: Preserve documents with null/empty arrays
|
|
245
380
|
include_array_index: Name of the field for array element index
|
|
246
|
-
|
|
381
|
+
|
|
247
382
|
Returns:
|
|
248
383
|
Self for method chaining
|
|
249
|
-
|
|
384
|
+
|
|
250
385
|
Raises:
|
|
251
386
|
TypeError: If path is not a string
|
|
252
387
|
ValueError: If path is empty
|
|
253
|
-
|
|
388
|
+
|
|
254
389
|
Example:
|
|
255
390
|
>>> builder.unwind("tags", preserve_null_and_empty_arrays=True)
|
|
256
391
|
>>> builder.unwind("items", include_array_index="itemIndex")
|
|
@@ -259,7 +394,7 @@ class PipelineBuilder:
|
|
|
259
394
|
raise TypeError(f"path must be a string, got {type(path)}")
|
|
260
395
|
if not path:
|
|
261
396
|
raise ValueError("path cannot be empty")
|
|
262
|
-
|
|
397
|
+
|
|
263
398
|
unwind_stage: Dict[str, Any] = {"path": path}
|
|
264
399
|
if preserve_null_and_empty_arrays:
|
|
265
400
|
unwind_stage["preserveNullAndEmptyArrays"] = True
|
|
@@ -271,16 +406,16 @@ class PipelineBuilder:
|
|
|
271
406
|
def sort(self, fields: Dict[str, int]) -> Self:
|
|
272
407
|
"""
|
|
273
408
|
Add a $sort stage for sorting documents.
|
|
274
|
-
|
|
409
|
+
|
|
275
410
|
Args:
|
|
276
411
|
fields: Dictionary with fields and sort direction (1 - asc, -1 - desc)
|
|
277
|
-
|
|
412
|
+
|
|
278
413
|
Returns:
|
|
279
414
|
Self for method chaining
|
|
280
|
-
|
|
415
|
+
|
|
281
416
|
Raises:
|
|
282
417
|
TypeError: If fields is not a dictionary
|
|
283
|
-
|
|
418
|
+
|
|
284
419
|
Example:
|
|
285
420
|
>>> builder.sort({"createdAt": -1, "name": 1})
|
|
286
421
|
"""
|
|
@@ -295,17 +430,17 @@ class PipelineBuilder:
|
|
|
295
430
|
def limit(self, limit: int) -> Self:
|
|
296
431
|
"""
|
|
297
432
|
Add a $limit stage to limit the number of documents.
|
|
298
|
-
|
|
433
|
+
|
|
299
434
|
Args:
|
|
300
435
|
limit: Maximum number of documents
|
|
301
|
-
|
|
436
|
+
|
|
302
437
|
Returns:
|
|
303
438
|
Self for method chaining
|
|
304
|
-
|
|
439
|
+
|
|
305
440
|
Raises:
|
|
306
441
|
TypeError: If limit is not an integer
|
|
307
442
|
ValueError: If limit is negative
|
|
308
|
-
|
|
443
|
+
|
|
309
444
|
Example:
|
|
310
445
|
>>> builder.limit(10)
|
|
311
446
|
"""
|
|
@@ -320,17 +455,17 @@ class PipelineBuilder:
|
|
|
320
455
|
def skip(self, skip: int) -> Self:
|
|
321
456
|
"""
|
|
322
457
|
Add a $skip stage to skip documents.
|
|
323
|
-
|
|
458
|
+
|
|
324
459
|
Args:
|
|
325
460
|
skip: Number of documents to skip
|
|
326
|
-
|
|
461
|
+
|
|
327
462
|
Returns:
|
|
328
463
|
Self for method chaining
|
|
329
|
-
|
|
464
|
+
|
|
330
465
|
Raises:
|
|
331
466
|
TypeError: If skip is not an integer
|
|
332
467
|
ValueError: If skip is negative
|
|
333
|
-
|
|
468
|
+
|
|
334
469
|
Example:
|
|
335
470
|
>>> builder.skip(20)
|
|
336
471
|
"""
|
|
@@ -345,24 +480,24 @@ class PipelineBuilder:
|
|
|
345
480
|
def unset(self, fields: Union[str, List[str]]) -> Self:
|
|
346
481
|
"""
|
|
347
482
|
Add a $unset stage to remove fields from documents.
|
|
348
|
-
|
|
483
|
+
|
|
349
484
|
Args:
|
|
350
485
|
fields: Field name or list of field names to remove
|
|
351
|
-
|
|
486
|
+
|
|
352
487
|
Returns:
|
|
353
488
|
Self for method chaining
|
|
354
|
-
|
|
489
|
+
|
|
355
490
|
Raises:
|
|
356
491
|
TypeError: If fields is not a string or list of strings
|
|
357
492
|
ValueError: If fields is empty
|
|
358
|
-
|
|
493
|
+
|
|
359
494
|
Example:
|
|
360
495
|
>>> builder.unset("temp_field")
|
|
361
496
|
>>> builder.unset(["field1", "field2", "field3"])
|
|
362
497
|
"""
|
|
363
498
|
if fields is None:
|
|
364
499
|
raise TypeError("fields cannot be None")
|
|
365
|
-
|
|
500
|
+
|
|
366
501
|
if isinstance(fields, str):
|
|
367
502
|
if not fields:
|
|
368
503
|
raise ValueError("fields cannot be an empty string")
|
|
@@ -378,23 +513,23 @@ class PipelineBuilder:
|
|
|
378
513
|
self._stages.append({"$unset": fields if len(fields) > 1 else fields[0]})
|
|
379
514
|
else:
|
|
380
515
|
raise TypeError(f"fields must be a string or list of strings, got {type(fields)}")
|
|
381
|
-
|
|
516
|
+
|
|
382
517
|
return self
|
|
383
518
|
|
|
384
519
|
def replace_root(self, new_root: Dict[str, Any]) -> Self:
|
|
385
520
|
"""
|
|
386
521
|
Add a $replaceRoot stage to replace the root document.
|
|
387
|
-
|
|
522
|
+
|
|
388
523
|
Args:
|
|
389
524
|
new_root: Expression for the new root document (must contain 'newRoot' key)
|
|
390
|
-
|
|
525
|
+
|
|
391
526
|
Returns:
|
|
392
527
|
Self for method chaining
|
|
393
|
-
|
|
528
|
+
|
|
394
529
|
Raises:
|
|
395
530
|
TypeError: If new_root is not a dictionary
|
|
396
531
|
ValueError: If new_root is empty or missing 'newRoot' key
|
|
397
|
-
|
|
532
|
+
|
|
398
533
|
Example:
|
|
399
534
|
>>> builder.replace_root({"newRoot": "$embedded"})
|
|
400
535
|
>>> builder.replace_root({"newRoot": {"$mergeObjects": ["$doc1", "$doc2"]}})
|
|
@@ -407,48 +542,48 @@ class PipelineBuilder:
|
|
|
407
542
|
raise ValueError("new_root cannot be empty")
|
|
408
543
|
if "newRoot" not in new_root:
|
|
409
544
|
raise ValueError("new_root must contain 'newRoot' key")
|
|
410
|
-
|
|
545
|
+
|
|
411
546
|
self._stages.append({"$replaceRoot": new_root})
|
|
412
547
|
return self
|
|
413
548
|
|
|
414
549
|
def replace_with(self, replacement: Any) -> Self:
|
|
415
550
|
"""
|
|
416
551
|
Add a $replaceWith stage (alias for $replaceRoot in MongoDB 4.2+).
|
|
417
|
-
|
|
552
|
+
|
|
418
553
|
Args:
|
|
419
554
|
replacement: Expression for the replacement document
|
|
420
|
-
|
|
555
|
+
|
|
421
556
|
Returns:
|
|
422
557
|
Self for method chaining
|
|
423
|
-
|
|
558
|
+
|
|
424
559
|
Raises:
|
|
425
560
|
ValueError: If replacement is None
|
|
426
|
-
|
|
561
|
+
|
|
427
562
|
Example:
|
|
428
563
|
>>> builder.replace_with("$embedded")
|
|
429
564
|
>>> builder.replace_with({"$mergeObjects": ["$doc1", "$doc2"]})
|
|
430
565
|
"""
|
|
431
566
|
if replacement is None:
|
|
432
567
|
raise ValueError("replacement cannot be None")
|
|
433
|
-
|
|
568
|
+
|
|
434
569
|
self._stages.append({"$replaceWith": replacement})
|
|
435
570
|
return self
|
|
436
571
|
|
|
437
572
|
def facet(self, facets: Dict[str, List[Dict[str, Any]]]) -> Self:
|
|
438
573
|
"""
|
|
439
574
|
Add a $facet stage for parallel execution of multiple sub-pipelines.
|
|
440
|
-
|
|
575
|
+
|
|
441
576
|
Args:
|
|
442
577
|
facets: Dictionary where keys are output field names and values are
|
|
443
578
|
lists of pipeline stages for each sub-pipeline
|
|
444
|
-
|
|
579
|
+
|
|
445
580
|
Returns:
|
|
446
581
|
Self for method chaining
|
|
447
|
-
|
|
582
|
+
|
|
448
583
|
Raises:
|
|
449
584
|
TypeError: If facets is not a dictionary
|
|
450
585
|
ValueError: If facets is empty or contains invalid values
|
|
451
|
-
|
|
586
|
+
|
|
452
587
|
Example:
|
|
453
588
|
>>> builder.facet({
|
|
454
589
|
... "items": [{"$skip": 10}, {"$limit": 20}],
|
|
@@ -461,31 +596,31 @@ class PipelineBuilder:
|
|
|
461
596
|
raise TypeError(f"facets must be a dict, got {type(facets)}")
|
|
462
597
|
if not facets:
|
|
463
598
|
raise ValueError("facets cannot be empty")
|
|
464
|
-
|
|
599
|
+
|
|
465
600
|
# Validate that all values are lists of dictionaries
|
|
466
601
|
for key, value in facets.items():
|
|
467
602
|
if not isinstance(value, list):
|
|
468
603
|
raise TypeError(f"facet '{key}' must be a list, got {type(value)}")
|
|
469
604
|
if not all(isinstance(stage, dict) for stage in value):
|
|
470
605
|
raise TypeError(f"all stages in facet '{key}' must be dictionaries")
|
|
471
|
-
|
|
606
|
+
|
|
472
607
|
self._stages.append({"$facet": facets})
|
|
473
608
|
return self
|
|
474
609
|
|
|
475
610
|
def count(self, field_name: str = "count") -> Self:
|
|
476
611
|
"""
|
|
477
612
|
Add a $count stage to count documents.
|
|
478
|
-
|
|
613
|
+
|
|
479
614
|
Args:
|
|
480
615
|
field_name: Name of the field for the count result
|
|
481
|
-
|
|
616
|
+
|
|
482
617
|
Returns:
|
|
483
618
|
Self for method chaining
|
|
484
|
-
|
|
619
|
+
|
|
485
620
|
Raises:
|
|
486
621
|
TypeError: If field_name is not a string
|
|
487
622
|
ValueError: If field_name is empty
|
|
488
|
-
|
|
623
|
+
|
|
489
624
|
Example:
|
|
490
625
|
>>> builder.match({"status": "active"}).count("active_count")
|
|
491
626
|
"""
|
|
@@ -495,26 +630,26 @@ class PipelineBuilder:
|
|
|
495
630
|
raise TypeError(f"field_name must be a string, got {type(field_name)}")
|
|
496
631
|
if not field_name:
|
|
497
632
|
raise ValueError("field_name cannot be empty")
|
|
498
|
-
|
|
633
|
+
|
|
499
634
|
self._stages.append({"$count": field_name})
|
|
500
635
|
return self
|
|
501
636
|
|
|
502
637
|
def set_field(self, fields: Dict[str, Any]) -> Self:
|
|
503
638
|
"""
|
|
504
639
|
Add a $set stage (alias for $addFields in MongoDB 3.4+).
|
|
505
|
-
|
|
640
|
+
|
|
506
641
|
Functionally equivalent to add_fields(), but $set is a more intuitive alias.
|
|
507
|
-
|
|
642
|
+
|
|
508
643
|
Args:
|
|
509
644
|
fields: Dictionary with fields and their values/expressions
|
|
510
|
-
|
|
645
|
+
|
|
511
646
|
Returns:
|
|
512
647
|
Self for method chaining
|
|
513
|
-
|
|
648
|
+
|
|
514
649
|
Raises:
|
|
515
650
|
TypeError: If fields is not a dictionary
|
|
516
651
|
ValueError: If fields is empty
|
|
517
|
-
|
|
652
|
+
|
|
518
653
|
Example:
|
|
519
654
|
>>> builder.set_field({"status": "active", "updatedAt": "$$NOW"})
|
|
520
655
|
"""
|
|
@@ -525,20 +660,20 @@ class PipelineBuilder:
|
|
|
525
660
|
if not fields:
|
|
526
661
|
# Empty dict - valid case, skip (same as add_fields behavior)
|
|
527
662
|
return self
|
|
528
|
-
|
|
663
|
+
|
|
529
664
|
self._stages.append({"$set": fields})
|
|
530
665
|
return self
|
|
531
666
|
|
|
532
667
|
def add_stage(self, stage: Dict[str, Any]) -> Self:
|
|
533
668
|
"""
|
|
534
669
|
Add an arbitrary pipeline stage for advanced use cases.
|
|
535
|
-
|
|
670
|
+
|
|
536
671
|
Args:
|
|
537
672
|
stage: Dictionary with an arbitrary MongoDB aggregation stage
|
|
538
|
-
|
|
673
|
+
|
|
539
674
|
Returns:
|
|
540
675
|
Self for method chaining
|
|
541
|
-
|
|
676
|
+
|
|
542
677
|
Example:
|
|
543
678
|
>>> builder.add_stage({
|
|
544
679
|
... "$facet": {
|
|
@@ -554,10 +689,10 @@ class PipelineBuilder:
|
|
|
554
689
|
def __len__(self) -> int:
|
|
555
690
|
"""
|
|
556
691
|
Return the number of stages in the pipeline.
|
|
557
|
-
|
|
692
|
+
|
|
558
693
|
Returns:
|
|
559
694
|
Number of stages
|
|
560
|
-
|
|
695
|
+
|
|
561
696
|
Example:
|
|
562
697
|
>>> builder = PipelineBuilder()
|
|
563
698
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -569,10 +704,10 @@ class PipelineBuilder:
|
|
|
569
704
|
def __repr__(self) -> str:
|
|
570
705
|
"""
|
|
571
706
|
Return a string representation of the builder for debugging.
|
|
572
|
-
|
|
707
|
+
|
|
573
708
|
Returns:
|
|
574
709
|
String representation showing stage count and preview
|
|
575
|
-
|
|
710
|
+
|
|
576
711
|
Example:
|
|
577
712
|
>>> builder = PipelineBuilder()
|
|
578
713
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -582,7 +717,7 @@ class PipelineBuilder:
|
|
|
582
717
|
stages_count = len(self._stages)
|
|
583
718
|
if stages_count == 0:
|
|
584
719
|
return "PipelineBuilder(stages=0)"
|
|
585
|
-
|
|
720
|
+
|
|
586
721
|
stage_types = [list(stage.keys())[0] for stage in self._stages[:3]]
|
|
587
722
|
stages_preview = ", ".join(stage_types)
|
|
588
723
|
if stages_count > 3:
|
|
@@ -592,10 +727,10 @@ class PipelineBuilder:
|
|
|
592
727
|
def clear(self) -> Self:
|
|
593
728
|
"""
|
|
594
729
|
Clear all stages from the pipeline.
|
|
595
|
-
|
|
730
|
+
|
|
596
731
|
Returns:
|
|
597
732
|
Self for method chaining
|
|
598
|
-
|
|
733
|
+
|
|
599
734
|
Example:
|
|
600
735
|
>>> builder = PipelineBuilder()
|
|
601
736
|
>>> builder.match({"status": "active"}).clear()
|
|
@@ -608,10 +743,10 @@ class PipelineBuilder:
|
|
|
608
743
|
def copy(self) -> "PipelineBuilder":
|
|
609
744
|
"""
|
|
610
745
|
Create a copy of the builder with current stages.
|
|
611
|
-
|
|
746
|
+
|
|
612
747
|
Returns:
|
|
613
748
|
New PipelineBuilder instance with copied stages
|
|
614
|
-
|
|
749
|
+
|
|
615
750
|
Example:
|
|
616
751
|
>>> builder1 = PipelineBuilder().match({"status": "active"})
|
|
617
752
|
>>> builder2 = builder1.copy()
|
|
@@ -628,17 +763,17 @@ class PipelineBuilder:
|
|
|
628
763
|
def validate(self) -> bool:
|
|
629
764
|
"""
|
|
630
765
|
Validate the pipeline before execution.
|
|
631
|
-
|
|
766
|
+
|
|
632
767
|
Checks that the pipeline is not empty and has valid structure.
|
|
633
768
|
Validates critical MongoDB rules:
|
|
634
769
|
- $out and $merge stages must be the last stage in the pipeline
|
|
635
|
-
|
|
770
|
+
|
|
636
771
|
Returns:
|
|
637
772
|
True if pipeline is valid
|
|
638
|
-
|
|
773
|
+
|
|
639
774
|
Raises:
|
|
640
775
|
ValueError: If pipeline is empty or has validation errors
|
|
641
|
-
|
|
776
|
+
|
|
642
777
|
Example:
|
|
643
778
|
>>> builder = PipelineBuilder()
|
|
644
779
|
>>> builder.match({"status": "active"}).validate()
|
|
@@ -648,20 +783,20 @@ class PipelineBuilder:
|
|
|
648
783
|
"""
|
|
649
784
|
if not self._stages:
|
|
650
785
|
raise ValueError("Pipeline cannot be empty")
|
|
651
|
-
|
|
786
|
+
|
|
652
787
|
# Validate that $out and $merge are the last stages (critical MongoDB rule)
|
|
653
788
|
stage_types = self.get_stage_types()
|
|
654
|
-
|
|
789
|
+
|
|
655
790
|
# Check if $out or $merge exist
|
|
656
791
|
has_out = "$out" in stage_types
|
|
657
792
|
has_merge = "$merge" in stage_types
|
|
658
|
-
|
|
793
|
+
|
|
659
794
|
if has_out and has_merge:
|
|
660
795
|
raise ValueError(
|
|
661
796
|
"Pipeline cannot contain both $out and $merge stages. "
|
|
662
797
|
"Only one output stage is allowed."
|
|
663
798
|
)
|
|
664
|
-
|
|
799
|
+
|
|
665
800
|
# Check if $out or $merge exist and validate position
|
|
666
801
|
for stage_name in ["$out", "$merge"]:
|
|
667
802
|
if stage_name in stage_types:
|
|
@@ -671,16 +806,16 @@ class PipelineBuilder:
|
|
|
671
806
|
f"{stage_name} stage must be the last stage in the pipeline. "
|
|
672
807
|
f"Found at position {stage_index + 1} of {len(stage_types)}."
|
|
673
808
|
)
|
|
674
|
-
|
|
809
|
+
|
|
675
810
|
return True
|
|
676
811
|
|
|
677
812
|
def get_stage_types(self) -> List[str]:
|
|
678
813
|
"""
|
|
679
814
|
Get a list of stage types in the pipeline.
|
|
680
|
-
|
|
815
|
+
|
|
681
816
|
Returns:
|
|
682
817
|
List of stage type strings (e.g., ["$match", "$lookup", "$limit"])
|
|
683
|
-
|
|
818
|
+
|
|
684
819
|
Example:
|
|
685
820
|
>>> builder = PipelineBuilder()
|
|
686
821
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -692,16 +827,16 @@ class PipelineBuilder:
|
|
|
692
827
|
def has_stage(self, stage_type: str) -> bool:
|
|
693
828
|
"""
|
|
694
829
|
Check if the pipeline contains a specific stage type.
|
|
695
|
-
|
|
830
|
+
|
|
696
831
|
Args:
|
|
697
832
|
stage_type: Type of stage to check (e.g., "$match", "$lookup")
|
|
698
|
-
|
|
833
|
+
|
|
699
834
|
Returns:
|
|
700
835
|
True if the stage type is present in the pipeline
|
|
701
|
-
|
|
836
|
+
|
|
702
837
|
Raises:
|
|
703
838
|
TypeError: If stage_type is not a string
|
|
704
|
-
|
|
839
|
+
|
|
705
840
|
Example:
|
|
706
841
|
>>> builder = PipelineBuilder()
|
|
707
842
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -718,16 +853,16 @@ class PipelineBuilder:
|
|
|
718
853
|
def prepend(self, stage: Dict[str, Any]) -> Self:
|
|
719
854
|
"""
|
|
720
855
|
Add a stage at the beginning of the pipeline.
|
|
721
|
-
|
|
856
|
+
|
|
722
857
|
Args:
|
|
723
858
|
stage: Dictionary with a MongoDB aggregation stage
|
|
724
|
-
|
|
859
|
+
|
|
725
860
|
Returns:
|
|
726
861
|
Self for method chaining
|
|
727
|
-
|
|
862
|
+
|
|
728
863
|
Raises:
|
|
729
864
|
TypeError: If stage is not a dictionary
|
|
730
|
-
|
|
865
|
+
|
|
731
866
|
Example:
|
|
732
867
|
>>> builder = PipelineBuilder()
|
|
733
868
|
>>> builder.match({"status": "active"})
|
|
@@ -746,18 +881,18 @@ class PipelineBuilder:
|
|
|
746
881
|
def insert_at(self, position: int, stage: Dict[str, Any]) -> Self:
|
|
747
882
|
"""
|
|
748
883
|
Insert a stage at a specific position in the pipeline.
|
|
749
|
-
|
|
884
|
+
|
|
750
885
|
Args:
|
|
751
886
|
position: Index where to insert (0-based)
|
|
752
887
|
stage: Dictionary with a MongoDB aggregation stage to insert
|
|
753
|
-
|
|
888
|
+
|
|
754
889
|
Returns:
|
|
755
890
|
Self for method chaining
|
|
756
|
-
|
|
891
|
+
|
|
757
892
|
Raises:
|
|
758
893
|
TypeError: If stage is not a dictionary
|
|
759
894
|
IndexError: If position is out of range [0, len(stages)]
|
|
760
|
-
|
|
895
|
+
|
|
761
896
|
Example:
|
|
762
897
|
>>> builder = PipelineBuilder()
|
|
763
898
|
>>> builder.match({"status": "active"}).group({"_id": "$category"}, {})
|
|
@@ -771,28 +906,28 @@ class PipelineBuilder:
|
|
|
771
906
|
raise TypeError(f"stage must be a dict, got {type(stage)}")
|
|
772
907
|
if not stage:
|
|
773
908
|
return self
|
|
774
|
-
|
|
909
|
+
|
|
775
910
|
if position < 0 or position > len(self._stages):
|
|
776
911
|
raise IndexError(
|
|
777
912
|
f"Position {position} out of range [0, {len(self._stages)}]"
|
|
778
913
|
)
|
|
779
|
-
|
|
914
|
+
|
|
780
915
|
self._stages.insert(position, stage)
|
|
781
916
|
return self
|
|
782
917
|
|
|
783
918
|
def get_stage_at(self, index: int) -> Dict[str, Any]:
|
|
784
919
|
"""
|
|
785
920
|
Get a specific stage from the pipeline by index.
|
|
786
|
-
|
|
921
|
+
|
|
787
922
|
Args:
|
|
788
923
|
index: Zero-based index of the stage to retrieve
|
|
789
|
-
|
|
924
|
+
|
|
790
925
|
Returns:
|
|
791
926
|
Dictionary representing the stage at the given index
|
|
792
|
-
|
|
927
|
+
|
|
793
928
|
Raises:
|
|
794
929
|
IndexError: If index is out of range
|
|
795
|
-
|
|
930
|
+
|
|
796
931
|
Example:
|
|
797
932
|
>>> builder = PipelineBuilder()
|
|
798
933
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -810,16 +945,16 @@ class PipelineBuilder:
|
|
|
810
945
|
def pretty_print(self, indent: int = 2, ensure_ascii: bool = False) -> str:
|
|
811
946
|
"""
|
|
812
947
|
Return a formatted JSON string representation of the pipeline.
|
|
813
|
-
|
|
948
|
+
|
|
814
949
|
Useful for debugging and understanding pipeline structure.
|
|
815
|
-
|
|
950
|
+
|
|
816
951
|
Args:
|
|
817
952
|
indent: Number of spaces for indentation (default: 2)
|
|
818
953
|
ensure_ascii: If False, non-ASCII characters are output as-is (default: False)
|
|
819
|
-
|
|
954
|
+
|
|
820
955
|
Returns:
|
|
821
956
|
Formatted JSON string of the pipeline
|
|
822
|
-
|
|
957
|
+
|
|
823
958
|
Example:
|
|
824
959
|
>>> builder = PipelineBuilder()
|
|
825
960
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -876,23 +1011,23 @@ class PipelineBuilder:
|
|
|
876
1011
|
) -> None:
|
|
877
1012
|
"""
|
|
878
1013
|
Save the pipeline to a JSON file.
|
|
879
|
-
|
|
1014
|
+
|
|
880
1015
|
Useful for debugging, comparison with other pipelines, or versioning.
|
|
881
|
-
|
|
1016
|
+
|
|
882
1017
|
Args:
|
|
883
1018
|
filepath: Path to the output JSON file (str or Path)
|
|
884
1019
|
indent: Number of spaces for indentation (default: 2)
|
|
885
1020
|
ensure_ascii: If False, non-ASCII characters are output as-is (default: False)
|
|
886
1021
|
metadata: Optional metadata to include in the JSON file
|
|
887
|
-
|
|
1022
|
+
|
|
888
1023
|
Raises:
|
|
889
1024
|
IOError: If file cannot be written
|
|
890
|
-
|
|
1025
|
+
|
|
891
1026
|
Example:
|
|
892
1027
|
>>> builder = PipelineBuilder()
|
|
893
1028
|
>>> builder.match({"status": "active"}).limit(10)
|
|
894
1029
|
>>> builder.to_json_file("debug_pipeline.json")
|
|
895
|
-
|
|
1030
|
+
|
|
896
1031
|
>>> # With metadata
|
|
897
1032
|
>>> builder.to_json_file(
|
|
898
1033
|
... "pipeline.json",
|
|
@@ -901,33 +1036,33 @@ class PipelineBuilder:
|
|
|
901
1036
|
"""
|
|
902
1037
|
filepath = Path(filepath)
|
|
903
1038
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
904
|
-
|
|
1039
|
+
|
|
905
1040
|
output: Dict[str, Any] = {
|
|
906
1041
|
"pipeline": self._stages,
|
|
907
1042
|
}
|
|
908
1043
|
if metadata:
|
|
909
1044
|
output["metadata"] = metadata
|
|
910
|
-
|
|
1045
|
+
|
|
911
1046
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
912
1047
|
json.dump(output, f, indent=indent, ensure_ascii=ensure_ascii)
|
|
913
1048
|
|
|
914
1049
|
def compare_with(self, other: "PipelineBuilder", context_lines: int = 3) -> str:
|
|
915
1050
|
"""
|
|
916
1051
|
Compare this pipeline with another pipeline and return a unified diff.
|
|
917
|
-
|
|
1052
|
+
|
|
918
1053
|
This is useful when migrating legacy pipelines (e.g., templates) to builder code.
|
|
919
|
-
|
|
1054
|
+
|
|
920
1055
|
Args:
|
|
921
1056
|
other: Another PipelineBuilder instance to compare with
|
|
922
1057
|
context_lines: Number of context lines in the unified diff (default: 3)
|
|
923
|
-
|
|
1058
|
+
|
|
924
1059
|
Returns:
|
|
925
1060
|
Unified diff as a string. Returns "No differences." if pipelines are identical.
|
|
926
|
-
|
|
1061
|
+
|
|
927
1062
|
Raises:
|
|
928
1063
|
TypeError: If other is not a PipelineBuilder
|
|
929
1064
|
ValueError: If context_lines is negative
|
|
930
|
-
|
|
1065
|
+
|
|
931
1066
|
Example:
|
|
932
1067
|
>>> legacy = PipelineBuilder().match({"a": 1})
|
|
933
1068
|
>>> new = PipelineBuilder().match({"a": 2})
|
|
@@ -960,10 +1095,10 @@ class PipelineBuilder:
|
|
|
960
1095
|
def build(self) -> List[Dict[str, Any]]:
|
|
961
1096
|
"""
|
|
962
1097
|
Return the completed pipeline.
|
|
963
|
-
|
|
1098
|
+
|
|
964
1099
|
Returns:
|
|
965
1100
|
List of dictionaries with aggregation pipeline stages
|
|
966
|
-
|
|
1101
|
+
|
|
967
1102
|
Example:
|
|
968
1103
|
>>> pipeline = builder.build()
|
|
969
1104
|
>>> collection.aggregate(pipeline)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongo-pipebuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Type-safe, fluent MongoDB aggregation pipeline builder
|
|
5
5
|
Author-email: seligoroff <seligoroff@gmail.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/seligoroff/mongo-pipebuilder
|
|
8
8
|
Project-URL: Documentation, https://github.com/seligoroff/mongo-pipebuilder#readme
|
|
9
9
|
Project-URL: Repository, https://github.com/seligoroff/mongo-pipebuilder
|
|
@@ -11,7 +11,6 @@ Project-URL: Issues, https://github.com/seligoroff/mongo-pipebuilder/issues
|
|
|
11
11
|
Keywords: mongodb,aggregation,pipeline,builder,query
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.8
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -98,6 +97,15 @@ Adds a `$match` stage to filter documents.
|
|
|
98
97
|
.match({"status": "active", "age": {"$gte": 18}})
|
|
99
98
|
```
|
|
100
99
|
|
|
100
|
+
##### `match_expr(expr: Dict[str, Any]) -> Self`
|
|
101
|
+
|
|
102
|
+
Adds a `$match` stage with an `$expr` condition (expression-based filter; useful for comparing fields or using variables from `let` in subpipelines).
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
.match_expr({"$eq": ["$id", "$$teamId"]})
|
|
106
|
+
.match_expr({"$and": [{"$gte": ["$field", "$other"]}, {"$lte": ["$score", 100]}]})
|
|
107
|
+
```
|
|
108
|
+
|
|
101
109
|
##### `lookup(from_collection: str, local_field: str, foreign_field: str, as_field: str, pipeline: Optional[List[Dict[str, Any]]] = None) -> Self`
|
|
102
110
|
|
|
103
111
|
Adds a `$lookup` stage to join with another collection.
|
|
@@ -112,6 +120,43 @@ Adds a `$lookup` stage to join with another collection.
|
|
|
112
120
|
)
|
|
113
121
|
```
|
|
114
122
|
|
|
123
|
+
##### `lookup_let(from_collection: str, let: Dict[str, Any], pipeline: Union[List[Dict[str, Any]], PipelineBuilder], as_field: str) -> Self`
|
|
124
|
+
|
|
125
|
+
Adds a `$lookup` stage with `let` and `pipeline` (join by expression; variables from the current document are available in the subpipeline as `$$var`). Use this when the join condition is an expression (e.g. `$expr`) rather than equality of two fields.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# With list of stages
|
|
129
|
+
.lookup_let(
|
|
130
|
+
from_collection="teams",
|
|
131
|
+
let={"teamId": "$idTeam"},
|
|
132
|
+
pipeline=[
|
|
133
|
+
{"$match": {"$expr": {"$eq": ["$_id", "$$teamId"]}}},
|
|
134
|
+
{"$project": {"name": 1, "_id": 0}}
|
|
135
|
+
],
|
|
136
|
+
as_field="team"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# With PipelineBuilder for the subpipeline (optionally using match_expr)
|
|
140
|
+
sub = PipelineBuilder().match_expr({"$eq": ["$_id", "$$teamId"]}).project({"name": 1, "_id": 0})
|
|
141
|
+
.lookup_let("teams", {"teamId": "$idTeam"}, sub, as_field="team")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
##### `union_with(coll: str, pipeline: Optional[Union[List[Dict[str, Any]], PipelineBuilder]] = None) -> Self`
|
|
145
|
+
|
|
146
|
+
Adds a `$unionWith` stage to combine documents from the current pipeline with documents from another collection. Optionally runs a subpipeline on the other collection before merging.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# Union with another collection (no subpipeline)
|
|
150
|
+
.union_with("other_coll")
|
|
151
|
+
|
|
152
|
+
# With subpipeline as list of stages
|
|
153
|
+
.union_with("logs", [{"$match": {"level": "error"}}, {"$limit": 100}])
|
|
154
|
+
|
|
155
|
+
# With PipelineBuilder for the subpipeline
|
|
156
|
+
sub = PipelineBuilder().match({"source": "individual"}).project({"name": 1})
|
|
157
|
+
.union_with("sso_individual_statistics", sub)
|
|
158
|
+
```
|
|
159
|
+
|
|
115
160
|
##### `add_fields(fields: Dict[str, Any]) -> Self`
|
|
116
161
|
|
|
117
162
|
Adds a `$addFields` stage to add or modify fields.
|
|
@@ -411,6 +456,31 @@ pipeline = (
|
|
|
411
456
|
)
|
|
412
457
|
```
|
|
413
458
|
|
|
459
|
+
### Lookup by expression (lookup_let)
|
|
460
|
+
|
|
461
|
+
When the join condition is an expression (e.g. `$expr`) rather than matching two fields, use `lookup_let`. The subpipeline can be built with `match_expr()`:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
sub = (
|
|
465
|
+
PipelineBuilder()
|
|
466
|
+
.match_expr({"$eq": ["$_id", "$$teamId"]})
|
|
467
|
+
.project({"name": 1, "slug": 1, "_id": 0})
|
|
468
|
+
)
|
|
469
|
+
pipeline = (
|
|
470
|
+
PipelineBuilder()
|
|
471
|
+
.match({"status": "active"})
|
|
472
|
+
.lookup_let(
|
|
473
|
+
from_collection="teams",
|
|
474
|
+
let={"teamId": "$idTeam"},
|
|
475
|
+
pipeline=sub,
|
|
476
|
+
as_field="team"
|
|
477
|
+
)
|
|
478
|
+
.unwind("team", preserve_null_and_empty_arrays=True)
|
|
479
|
+
.project({"title": 1, "teamName": "$team.name"})
|
|
480
|
+
.build()
|
|
481
|
+
)
|
|
482
|
+
```
|
|
483
|
+
|
|
414
484
|
### Aggregation with Grouping
|
|
415
485
|
|
|
416
486
|
```python
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mongo_pipebuilder/__init__.py,sha256=3iWmQvRAT2QZHXURN9AHoMPn-7FjwH9ig8QyTUCVLh4,336
|
|
2
|
+
mongo_pipebuilder/builder.py,sha256=_c-5uuNwWJigKzzIcOXXkPY9oD_UOC0lomhx03yJz9U,38834
|
|
3
|
+
mongo_pipebuilder-0.4.0.dist-info/licenses/LICENSE,sha256=-ZkZpDLHDQAc-YBIojJ6eDsMwxwx5pRuQz3RHnl9Y8w,1104
|
|
4
|
+
mongo_pipebuilder-0.4.0.dist-info/METADATA,sha256=IAtv0lDGEIiQ-OlFLn1LR6fDFtgC1xj_PSH3Ak31lE4,20002
|
|
5
|
+
mongo_pipebuilder-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
mongo_pipebuilder-0.4.0.dist-info/top_level.txt,sha256=wLn7H_v-qaNIws5FeBbKPZBCmYFYgFEhPaLjoCWcisc,18
|
|
7
|
+
mongo_pipebuilder-0.4.0.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
mongo_pipebuilder/__init__.py,sha256=dvekji4j1j9v5MzJOJIqyO2znWVia1opBn8Y1Sc_Y3k,336
|
|
2
|
-
mongo_pipebuilder/builder.py,sha256=Fz7oUiB9FpqnIwnGgamof2ZEBaUGjfYSuB7mYCJO9Qc,34731
|
|
3
|
-
mongo_pipebuilder-0.3.1.dist-info/licenses/LICENSE,sha256=-ZkZpDLHDQAc-YBIojJ6eDsMwxwx5pRuQz3RHnl9Y8w,1104
|
|
4
|
-
mongo_pipebuilder-0.3.1.dist-info/METADATA,sha256=hYFQkwz1xtJK-MPAV2Vp4PuwKLvC7CLc5U-emp4yOzw,17478
|
|
5
|
-
mongo_pipebuilder-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
mongo_pipebuilder-0.3.1.dist-info/top_level.txt,sha256=wLn7H_v-qaNIws5FeBbKPZBCmYFYgFEhPaLjoCWcisc,18
|
|
7
|
-
mongo_pipebuilder-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|