mongo-pipebuilder 0.3.1__py3-none-any.whl → 0.5.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 +304 -140
- {mongo_pipebuilder-0.3.1.dist-info → mongo_pipebuilder-0.5.0.dist-info}/METADATA +88 -6
- mongo_pipebuilder-0.5.0.dist-info/RECORD +7 -0
- {mongo_pipebuilder-0.3.1.dist-info → mongo_pipebuilder-0.5.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.5.0.dist-info}/licenses/LICENSE +0 -0
- {mongo_pipebuilder-0.3.1.dist-info → mongo_pipebuilder-0.5.0.dist-info}/top_level.txt +0 -0
mongo_pipebuilder/__init__.py
CHANGED
mongo_pipebuilder/builder.py
CHANGED
|
@@ -10,13 +10,10 @@ import copy
|
|
|
10
10
|
import difflib
|
|
11
11
|
import json
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, List, Optional, Union
|
|
13
|
+
from typing import Any, Dict, Iterable, 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 (Self is in typing from 3.11)
|
|
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": {
|
|
@@ -551,13 +686,42 @@ class PipelineBuilder:
|
|
|
551
686
|
self._stages.append(stage)
|
|
552
687
|
return self
|
|
553
688
|
|
|
689
|
+
def add_stages(self, stages: Iterable[Dict[str, Any]]) -> Self:
|
|
690
|
+
"""
|
|
691
|
+
Add multiple pipeline stages at once (e.g. a subpipeline from another builder).
|
|
692
|
+
|
|
693
|
+
Empty dict stages are skipped, as with add_stage. Each element must be a
|
|
694
|
+
dictionary.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
stages: Iterable of stage dictionaries (e.g. list, or result of .build()).
|
|
698
|
+
|
|
699
|
+
Returns:
|
|
700
|
+
Self for method chaining.
|
|
701
|
+
|
|
702
|
+
Raises:
|
|
703
|
+
TypeError: If stages is None or any element is not a dictionary.
|
|
704
|
+
|
|
705
|
+
Example:
|
|
706
|
+
>>> builder.add_stages([{"$match": {"x": 1}}, {"$limit": 10}])
|
|
707
|
+
>>> builder.add_stages(other_builder.build())
|
|
708
|
+
"""
|
|
709
|
+
if stages is None:
|
|
710
|
+
raise TypeError("stages must not be None")
|
|
711
|
+
for stage in stages:
|
|
712
|
+
if not isinstance(stage, dict):
|
|
713
|
+
raise TypeError("All stages must be dictionaries")
|
|
714
|
+
if stage:
|
|
715
|
+
self._stages.append(stage)
|
|
716
|
+
return self
|
|
717
|
+
|
|
554
718
|
def __len__(self) -> int:
|
|
555
719
|
"""
|
|
556
720
|
Return the number of stages in the pipeline.
|
|
557
|
-
|
|
721
|
+
|
|
558
722
|
Returns:
|
|
559
723
|
Number of stages
|
|
560
|
-
|
|
724
|
+
|
|
561
725
|
Example:
|
|
562
726
|
>>> builder = PipelineBuilder()
|
|
563
727
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -569,10 +733,10 @@ class PipelineBuilder:
|
|
|
569
733
|
def __repr__(self) -> str:
|
|
570
734
|
"""
|
|
571
735
|
Return a string representation of the builder for debugging.
|
|
572
|
-
|
|
736
|
+
|
|
573
737
|
Returns:
|
|
574
738
|
String representation showing stage count and preview
|
|
575
|
-
|
|
739
|
+
|
|
576
740
|
Example:
|
|
577
741
|
>>> builder = PipelineBuilder()
|
|
578
742
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -582,7 +746,7 @@ class PipelineBuilder:
|
|
|
582
746
|
stages_count = len(self._stages)
|
|
583
747
|
if stages_count == 0:
|
|
584
748
|
return "PipelineBuilder(stages=0)"
|
|
585
|
-
|
|
749
|
+
|
|
586
750
|
stage_types = [list(stage.keys())[0] for stage in self._stages[:3]]
|
|
587
751
|
stages_preview = ", ".join(stage_types)
|
|
588
752
|
if stages_count > 3:
|
|
@@ -592,10 +756,10 @@ class PipelineBuilder:
|
|
|
592
756
|
def clear(self) -> Self:
|
|
593
757
|
"""
|
|
594
758
|
Clear all stages from the pipeline.
|
|
595
|
-
|
|
759
|
+
|
|
596
760
|
Returns:
|
|
597
761
|
Self for method chaining
|
|
598
|
-
|
|
762
|
+
|
|
599
763
|
Example:
|
|
600
764
|
>>> builder = PipelineBuilder()
|
|
601
765
|
>>> builder.match({"status": "active"}).clear()
|
|
@@ -608,10 +772,10 @@ class PipelineBuilder:
|
|
|
608
772
|
def copy(self) -> "PipelineBuilder":
|
|
609
773
|
"""
|
|
610
774
|
Create a copy of the builder with current stages.
|
|
611
|
-
|
|
775
|
+
|
|
612
776
|
Returns:
|
|
613
777
|
New PipelineBuilder instance with copied stages
|
|
614
|
-
|
|
778
|
+
|
|
615
779
|
Example:
|
|
616
780
|
>>> builder1 = PipelineBuilder().match({"status": "active"})
|
|
617
781
|
>>> builder2 = builder1.copy()
|
|
@@ -628,17 +792,17 @@ class PipelineBuilder:
|
|
|
628
792
|
def validate(self) -> bool:
|
|
629
793
|
"""
|
|
630
794
|
Validate the pipeline before execution.
|
|
631
|
-
|
|
795
|
+
|
|
632
796
|
Checks that the pipeline is not empty and has valid structure.
|
|
633
797
|
Validates critical MongoDB rules:
|
|
634
798
|
- $out and $merge stages must be the last stage in the pipeline
|
|
635
|
-
|
|
799
|
+
|
|
636
800
|
Returns:
|
|
637
801
|
True if pipeline is valid
|
|
638
|
-
|
|
802
|
+
|
|
639
803
|
Raises:
|
|
640
804
|
ValueError: If pipeline is empty or has validation errors
|
|
641
|
-
|
|
805
|
+
|
|
642
806
|
Example:
|
|
643
807
|
>>> builder = PipelineBuilder()
|
|
644
808
|
>>> builder.match({"status": "active"}).validate()
|
|
@@ -648,20 +812,20 @@ class PipelineBuilder:
|
|
|
648
812
|
"""
|
|
649
813
|
if not self._stages:
|
|
650
814
|
raise ValueError("Pipeline cannot be empty")
|
|
651
|
-
|
|
815
|
+
|
|
652
816
|
# Validate that $out and $merge are the last stages (critical MongoDB rule)
|
|
653
817
|
stage_types = self.get_stage_types()
|
|
654
|
-
|
|
818
|
+
|
|
655
819
|
# Check if $out or $merge exist
|
|
656
820
|
has_out = "$out" in stage_types
|
|
657
821
|
has_merge = "$merge" in stage_types
|
|
658
|
-
|
|
822
|
+
|
|
659
823
|
if has_out and has_merge:
|
|
660
824
|
raise ValueError(
|
|
661
825
|
"Pipeline cannot contain both $out and $merge stages. "
|
|
662
826
|
"Only one output stage is allowed."
|
|
663
827
|
)
|
|
664
|
-
|
|
828
|
+
|
|
665
829
|
# Check if $out or $merge exist and validate position
|
|
666
830
|
for stage_name in ["$out", "$merge"]:
|
|
667
831
|
if stage_name in stage_types:
|
|
@@ -671,16 +835,16 @@ class PipelineBuilder:
|
|
|
671
835
|
f"{stage_name} stage must be the last stage in the pipeline. "
|
|
672
836
|
f"Found at position {stage_index + 1} of {len(stage_types)}."
|
|
673
837
|
)
|
|
674
|
-
|
|
838
|
+
|
|
675
839
|
return True
|
|
676
840
|
|
|
677
841
|
def get_stage_types(self) -> List[str]:
|
|
678
842
|
"""
|
|
679
843
|
Get a list of stage types in the pipeline.
|
|
680
|
-
|
|
844
|
+
|
|
681
845
|
Returns:
|
|
682
846
|
List of stage type strings (e.g., ["$match", "$lookup", "$limit"])
|
|
683
|
-
|
|
847
|
+
|
|
684
848
|
Example:
|
|
685
849
|
>>> builder = PipelineBuilder()
|
|
686
850
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -692,16 +856,16 @@ class PipelineBuilder:
|
|
|
692
856
|
def has_stage(self, stage_type: str) -> bool:
|
|
693
857
|
"""
|
|
694
858
|
Check if the pipeline contains a specific stage type.
|
|
695
|
-
|
|
859
|
+
|
|
696
860
|
Args:
|
|
697
861
|
stage_type: Type of stage to check (e.g., "$match", "$lookup")
|
|
698
|
-
|
|
862
|
+
|
|
699
863
|
Returns:
|
|
700
864
|
True if the stage type is present in the pipeline
|
|
701
|
-
|
|
865
|
+
|
|
702
866
|
Raises:
|
|
703
867
|
TypeError: If stage_type is not a string
|
|
704
|
-
|
|
868
|
+
|
|
705
869
|
Example:
|
|
706
870
|
>>> builder = PipelineBuilder()
|
|
707
871
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -718,16 +882,16 @@ class PipelineBuilder:
|
|
|
718
882
|
def prepend(self, stage: Dict[str, Any]) -> Self:
|
|
719
883
|
"""
|
|
720
884
|
Add a stage at the beginning of the pipeline.
|
|
721
|
-
|
|
885
|
+
|
|
722
886
|
Args:
|
|
723
887
|
stage: Dictionary with a MongoDB aggregation stage
|
|
724
|
-
|
|
888
|
+
|
|
725
889
|
Returns:
|
|
726
890
|
Self for method chaining
|
|
727
|
-
|
|
891
|
+
|
|
728
892
|
Raises:
|
|
729
893
|
TypeError: If stage is not a dictionary
|
|
730
|
-
|
|
894
|
+
|
|
731
895
|
Example:
|
|
732
896
|
>>> builder = PipelineBuilder()
|
|
733
897
|
>>> builder.match({"status": "active"})
|
|
@@ -746,18 +910,18 @@ class PipelineBuilder:
|
|
|
746
910
|
def insert_at(self, position: int, stage: Dict[str, Any]) -> Self:
|
|
747
911
|
"""
|
|
748
912
|
Insert a stage at a specific position in the pipeline.
|
|
749
|
-
|
|
913
|
+
|
|
750
914
|
Args:
|
|
751
915
|
position: Index where to insert (0-based)
|
|
752
916
|
stage: Dictionary with a MongoDB aggregation stage to insert
|
|
753
|
-
|
|
917
|
+
|
|
754
918
|
Returns:
|
|
755
919
|
Self for method chaining
|
|
756
|
-
|
|
920
|
+
|
|
757
921
|
Raises:
|
|
758
922
|
TypeError: If stage is not a dictionary
|
|
759
923
|
IndexError: If position is out of range [0, len(stages)]
|
|
760
|
-
|
|
924
|
+
|
|
761
925
|
Example:
|
|
762
926
|
>>> builder = PipelineBuilder()
|
|
763
927
|
>>> builder.match({"status": "active"}).group({"_id": "$category"}, {})
|
|
@@ -771,28 +935,28 @@ class PipelineBuilder:
|
|
|
771
935
|
raise TypeError(f"stage must be a dict, got {type(stage)}")
|
|
772
936
|
if not stage:
|
|
773
937
|
return self
|
|
774
|
-
|
|
938
|
+
|
|
775
939
|
if position < 0 or position > len(self._stages):
|
|
776
940
|
raise IndexError(
|
|
777
941
|
f"Position {position} out of range [0, {len(self._stages)}]"
|
|
778
942
|
)
|
|
779
|
-
|
|
943
|
+
|
|
780
944
|
self._stages.insert(position, stage)
|
|
781
945
|
return self
|
|
782
946
|
|
|
783
947
|
def get_stage_at(self, index: int) -> Dict[str, Any]:
|
|
784
948
|
"""
|
|
785
949
|
Get a specific stage from the pipeline by index.
|
|
786
|
-
|
|
950
|
+
|
|
787
951
|
Args:
|
|
788
952
|
index: Zero-based index of the stage to retrieve
|
|
789
|
-
|
|
953
|
+
|
|
790
954
|
Returns:
|
|
791
955
|
Dictionary representing the stage at the given index
|
|
792
|
-
|
|
956
|
+
|
|
793
957
|
Raises:
|
|
794
958
|
IndexError: If index is out of range
|
|
795
|
-
|
|
959
|
+
|
|
796
960
|
Example:
|
|
797
961
|
>>> builder = PipelineBuilder()
|
|
798
962
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -810,16 +974,16 @@ class PipelineBuilder:
|
|
|
810
974
|
def pretty_print(self, indent: int = 2, ensure_ascii: bool = False) -> str:
|
|
811
975
|
"""
|
|
812
976
|
Return a formatted JSON string representation of the pipeline.
|
|
813
|
-
|
|
977
|
+
|
|
814
978
|
Useful for debugging and understanding pipeline structure.
|
|
815
|
-
|
|
979
|
+
|
|
816
980
|
Args:
|
|
817
981
|
indent: Number of spaces for indentation (default: 2)
|
|
818
982
|
ensure_ascii: If False, non-ASCII characters are output as-is (default: False)
|
|
819
|
-
|
|
983
|
+
|
|
820
984
|
Returns:
|
|
821
985
|
Formatted JSON string of the pipeline
|
|
822
|
-
|
|
986
|
+
|
|
823
987
|
Example:
|
|
824
988
|
>>> builder = PipelineBuilder()
|
|
825
989
|
>>> builder.match({"status": "active"}).limit(10)
|
|
@@ -876,23 +1040,23 @@ class PipelineBuilder:
|
|
|
876
1040
|
) -> None:
|
|
877
1041
|
"""
|
|
878
1042
|
Save the pipeline to a JSON file.
|
|
879
|
-
|
|
1043
|
+
|
|
880
1044
|
Useful for debugging, comparison with other pipelines, or versioning.
|
|
881
|
-
|
|
1045
|
+
|
|
882
1046
|
Args:
|
|
883
1047
|
filepath: Path to the output JSON file (str or Path)
|
|
884
1048
|
indent: Number of spaces for indentation (default: 2)
|
|
885
1049
|
ensure_ascii: If False, non-ASCII characters are output as-is (default: False)
|
|
886
1050
|
metadata: Optional metadata to include in the JSON file
|
|
887
|
-
|
|
1051
|
+
|
|
888
1052
|
Raises:
|
|
889
1053
|
IOError: If file cannot be written
|
|
890
|
-
|
|
1054
|
+
|
|
891
1055
|
Example:
|
|
892
1056
|
>>> builder = PipelineBuilder()
|
|
893
1057
|
>>> builder.match({"status": "active"}).limit(10)
|
|
894
1058
|
>>> builder.to_json_file("debug_pipeline.json")
|
|
895
|
-
|
|
1059
|
+
|
|
896
1060
|
>>> # With metadata
|
|
897
1061
|
>>> builder.to_json_file(
|
|
898
1062
|
... "pipeline.json",
|
|
@@ -901,33 +1065,33 @@ class PipelineBuilder:
|
|
|
901
1065
|
"""
|
|
902
1066
|
filepath = Path(filepath)
|
|
903
1067
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
904
|
-
|
|
1068
|
+
|
|
905
1069
|
output: Dict[str, Any] = {
|
|
906
1070
|
"pipeline": self._stages,
|
|
907
1071
|
}
|
|
908
1072
|
if metadata:
|
|
909
1073
|
output["metadata"] = metadata
|
|
910
|
-
|
|
1074
|
+
|
|
911
1075
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
912
1076
|
json.dump(output, f, indent=indent, ensure_ascii=ensure_ascii)
|
|
913
1077
|
|
|
914
1078
|
def compare_with(self, other: "PipelineBuilder", context_lines: int = 3) -> str:
|
|
915
1079
|
"""
|
|
916
1080
|
Compare this pipeline with another pipeline and return a unified diff.
|
|
917
|
-
|
|
1081
|
+
|
|
918
1082
|
This is useful when migrating legacy pipelines (e.g., templates) to builder code.
|
|
919
|
-
|
|
1083
|
+
|
|
920
1084
|
Args:
|
|
921
1085
|
other: Another PipelineBuilder instance to compare with
|
|
922
1086
|
context_lines: Number of context lines in the unified diff (default: 3)
|
|
923
|
-
|
|
1087
|
+
|
|
924
1088
|
Returns:
|
|
925
1089
|
Unified diff as a string. Returns "No differences." if pipelines are identical.
|
|
926
|
-
|
|
1090
|
+
|
|
927
1091
|
Raises:
|
|
928
1092
|
TypeError: If other is not a PipelineBuilder
|
|
929
1093
|
ValueError: If context_lines is negative
|
|
930
|
-
|
|
1094
|
+
|
|
931
1095
|
Example:
|
|
932
1096
|
>>> legacy = PipelineBuilder().match({"a": 1})
|
|
933
1097
|
>>> new = PipelineBuilder().match({"a": 2})
|
|
@@ -960,10 +1124,10 @@ class PipelineBuilder:
|
|
|
960
1124
|
def build(self) -> List[Dict[str, Any]]:
|
|
961
1125
|
"""
|
|
962
1126
|
Return the completed pipeline.
|
|
963
|
-
|
|
1127
|
+
|
|
964
1128
|
Returns:
|
|
965
1129
|
List of dictionaries with aggregation pipeline stages
|
|
966
|
-
|
|
1130
|
+
|
|
967
1131
|
Example:
|
|
968
1132
|
>>> pipeline = builder.build()
|
|
969
1133
|
>>> collection.aggregate(pipeline)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongo-pipebuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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,16 +11,14 @@ 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
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
19
|
Classifier: Topic :: Database
|
|
22
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.9
|
|
24
22
|
Description-Content-Type: text/markdown
|
|
25
23
|
License-File: LICENSE
|
|
26
24
|
Requires-Dist: typing_extensions>=4.0.0; python_version < "3.11"
|
|
@@ -29,7 +27,7 @@ Dynamic: license-file
|
|
|
29
27
|
# mongo-pipebuilder
|
|
30
28
|
|
|
31
29
|
[](https://badge.fury.io/py/mongo-pipebuilder)
|
|
32
|
-
[](https://www.python.org/downloads/)
|
|
33
31
|
[](https://opensource.org/licenses/MIT)
|
|
34
32
|
[](https://github.com/psf/black)
|
|
35
33
|
[](https://github.com/seligoroff/mongo-pipebuilder)
|
|
@@ -98,6 +96,15 @@ Adds a `$match` stage to filter documents.
|
|
|
98
96
|
.match({"status": "active", "age": {"$gte": 18}})
|
|
99
97
|
```
|
|
100
98
|
|
|
99
|
+
##### `match_expr(expr: Dict[str, Any]) -> Self`
|
|
100
|
+
|
|
101
|
+
Adds a `$match` stage with an `$expr` condition (expression-based filter; useful for comparing fields or using variables from `let` in subpipelines).
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
.match_expr({"$eq": ["$id", "$$teamId"]})
|
|
105
|
+
.match_expr({"$and": [{"$gte": ["$field", "$other"]}, {"$lte": ["$score", 100]}]})
|
|
106
|
+
```
|
|
107
|
+
|
|
101
108
|
##### `lookup(from_collection: str, local_field: str, foreign_field: str, as_field: str, pipeline: Optional[List[Dict[str, Any]]] = None) -> Self`
|
|
102
109
|
|
|
103
110
|
Adds a `$lookup` stage to join with another collection.
|
|
@@ -112,6 +119,43 @@ Adds a `$lookup` stage to join with another collection.
|
|
|
112
119
|
)
|
|
113
120
|
```
|
|
114
121
|
|
|
122
|
+
##### `lookup_let(from_collection: str, let: Dict[str, Any], pipeline: Union[List[Dict[str, Any]], PipelineBuilder], as_field: str) -> Self`
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# With list of stages
|
|
128
|
+
.lookup_let(
|
|
129
|
+
from_collection="teams",
|
|
130
|
+
let={"teamId": "$idTeam"},
|
|
131
|
+
pipeline=[
|
|
132
|
+
{"$match": {"$expr": {"$eq": ["$_id", "$$teamId"]}}},
|
|
133
|
+
{"$project": {"name": 1, "_id": 0}}
|
|
134
|
+
],
|
|
135
|
+
as_field="team"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# With PipelineBuilder for the subpipeline (optionally using match_expr)
|
|
139
|
+
sub = PipelineBuilder().match_expr({"$eq": ["$_id", "$$teamId"]}).project({"name": 1, "_id": 0})
|
|
140
|
+
.lookup_let("teams", {"teamId": "$idTeam"}, sub, as_field="team")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
##### `union_with(coll: str, pipeline: Optional[Union[List[Dict[str, Any]], PipelineBuilder]] = None) -> Self`
|
|
144
|
+
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Union with another collection (no subpipeline)
|
|
149
|
+
.union_with("other_coll")
|
|
150
|
+
|
|
151
|
+
# With subpipeline as list of stages
|
|
152
|
+
.union_with("logs", [{"$match": {"level": "error"}}, {"$limit": 100}])
|
|
153
|
+
|
|
154
|
+
# With PipelineBuilder for the subpipeline
|
|
155
|
+
sub = PipelineBuilder().match({"source": "individual"}).project({"name": 1})
|
|
156
|
+
.union_with("sso_individual_statistics", sub)
|
|
157
|
+
```
|
|
158
|
+
|
|
115
159
|
##### `add_fields(fields: Dict[str, Any]) -> Self`
|
|
116
160
|
|
|
117
161
|
Adds a `$addFields` stage to add or modify fields.
|
|
@@ -237,6 +281,19 @@ Adds a custom stage for advanced use cases.
|
|
|
237
281
|
}})
|
|
238
282
|
```
|
|
239
283
|
|
|
284
|
+
##### `add_stages(stages: Iterable[Dict[str, Any]]) -> Self`
|
|
285
|
+
|
|
286
|
+
Adds multiple stages at once (e.g. a subpipeline from another builder). Empty dicts are skipped. Useful to avoid loops when inserting a ready-made list of stages.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
# From a list
|
|
290
|
+
.add_stages([{"$match": {"level": "error"}}, {"$limit": 100}])
|
|
291
|
+
|
|
292
|
+
# From another builder
|
|
293
|
+
sub = PipelineBuilder().match({"source": "api"}).project({"name": 1})
|
|
294
|
+
.add_stages(sub.build())
|
|
295
|
+
```
|
|
296
|
+
|
|
240
297
|
##### `prepend(stage: Dict[str, Any]) -> Self`
|
|
241
298
|
|
|
242
299
|
Adds a stage at the beginning of the pipeline.
|
|
@@ -411,6 +468,31 @@ pipeline = (
|
|
|
411
468
|
)
|
|
412
469
|
```
|
|
413
470
|
|
|
471
|
+
### Lookup by expression (lookup_let)
|
|
472
|
+
|
|
473
|
+
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()`:
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
sub = (
|
|
477
|
+
PipelineBuilder()
|
|
478
|
+
.match_expr({"$eq": ["$_id", "$$teamId"]})
|
|
479
|
+
.project({"name": 1, "slug": 1, "_id": 0})
|
|
480
|
+
)
|
|
481
|
+
pipeline = (
|
|
482
|
+
PipelineBuilder()
|
|
483
|
+
.match({"status": "active"})
|
|
484
|
+
.lookup_let(
|
|
485
|
+
from_collection="teams",
|
|
486
|
+
let={"teamId": "$idTeam"},
|
|
487
|
+
pipeline=sub,
|
|
488
|
+
as_field="team"
|
|
489
|
+
)
|
|
490
|
+
.unwind("team", preserve_null_and_empty_arrays=True)
|
|
491
|
+
.project({"title": 1, "teamName": "$team.name"})
|
|
492
|
+
.build()
|
|
493
|
+
)
|
|
494
|
+
```
|
|
495
|
+
|
|
414
496
|
### Aggregation with Grouping
|
|
415
497
|
|
|
416
498
|
```python
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mongo_pipebuilder/__init__.py,sha256=cn4_tymz00r4iA4ES-qe6lP3k9K6TvuUsagf6_Ajaxc,336
|
|
2
|
+
mongo_pipebuilder/builder.py,sha256=r_umLKj79j7dmBDKoauo5GC2h7ZFRrc1uYd3_aKcwO0,39834
|
|
3
|
+
mongo_pipebuilder-0.5.0.dist-info/licenses/LICENSE,sha256=-ZkZpDLHDQAc-YBIojJ6eDsMwxwx5pRuQz3RHnl9Y8w,1104
|
|
4
|
+
mongo_pipebuilder-0.5.0.dist-info/METADATA,sha256=aUjgx7ENKyXI3eNyv2gt3YmgL2HeRDpUCav-ucJGBFg,20389
|
|
5
|
+
mongo_pipebuilder-0.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
mongo_pipebuilder-0.5.0.dist-info/top_level.txt,sha256=wLn7H_v-qaNIws5FeBbKPZBCmYFYgFEhPaLjoCWcisc,18
|
|
7
|
+
mongo_pipebuilder-0.5.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
|