mongo-pipebuilder 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/LICENSE +0 -0
- mongo_pipebuilder-0.4.0/MANIFEST.in +3 -0
- {mongo_pipebuilder-0.3.0/src/mongo_pipebuilder.egg-info → mongo_pipebuilder-0.4.0}/PKG-INFO +123 -5
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/README.md +121 -2
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/pyproject.toml +46 -5
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/setup.cfg +0 -0
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/src/mongo_pipebuilder/__init__.py +1 -1
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/src/mongo_pipebuilder/builder.py +371 -134
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0/src/mongo_pipebuilder.egg-info}/PKG-INFO +123 -5
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/src/mongo_pipebuilder.egg-info/SOURCES.txt +4 -0
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/src/mongo_pipebuilder.egg-info/dependency_links.txt +0 -0
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/src/mongo_pipebuilder.egg-info/requires.txt +0 -0
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/src/mongo_pipebuilder.egg-info/top_level.txt +0 -0
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/tests/test_builder.py +9 -10
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/tests/test_builder_debug.py +102 -50
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/tests/test_builder_insert.py +25 -24
- mongo_pipebuilder-0.4.0/tests/test_builder_lookup_let.py +167 -0
- mongo_pipebuilder-0.4.0/tests/test_builder_match_expr.py +69 -0
- mongo_pipebuilder-0.4.0/tests/test_builder_union_with.py +95 -0
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/tests/test_builder_validation.py +13 -14
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/tests/test_builder_validation_existing.py +8 -1
- {mongo_pipebuilder-0.3.0 → mongo_pipebuilder-0.4.0}/tests/test_builder_validation_new.py +1 -0
|
File without changes
|
|
@@ -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.
|
|
@@ -252,7 +297,7 @@ builder.prepend({"$match": {"deleted": False}})
|
|
|
252
297
|
Inserts a stage at a specific position (0-based index) in the pipeline.
|
|
253
298
|
|
|
254
299
|
```python
|
|
255
|
-
builder.match({"status": "active"}).group(
|
|
300
|
+
builder.match({"status": "active"}).group("$category", {"count": {"$sum": 1}})
|
|
256
301
|
builder.insert_at(1, {"$sort": {"name": 1}})
|
|
257
302
|
# Pipeline: [{"$match": {...}}, {"$sort": {...}}, {"$group": {...}}]
|
|
258
303
|
```
|
|
@@ -327,6 +372,15 @@ print(builder.pretty_print())
|
|
|
327
372
|
# ]
|
|
328
373
|
```
|
|
329
374
|
|
|
375
|
+
##### `pretty_print_stage(stage: Union[int, Dict[str, Any]], indent: int = 2, ensure_ascii: bool = False) -> str`
|
|
376
|
+
|
|
377
|
+
Returns a formatted JSON string representation of a single stage (by index or by dict).
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
builder = PipelineBuilder().match({"status": "active"}).limit(10)
|
|
381
|
+
print(builder.pretty_print_stage(0)) # Prints the $match stage
|
|
382
|
+
```
|
|
383
|
+
|
|
330
384
|
##### `to_json_file(filepath: Union[str, Path], indent: int = 2, ensure_ascii: bool = False, metadata: Optional[Dict[str, Any]] = None) -> None`
|
|
331
385
|
|
|
332
386
|
Saves the pipeline to a JSON file. Useful for debugging, comparison, or versioning.
|
|
@@ -345,6 +399,17 @@ builder.to_json_file(
|
|
|
345
399
|
)
|
|
346
400
|
```
|
|
347
401
|
|
|
402
|
+
##### `compare_with(other: PipelineBuilder, context_lines: int = 3) -> str`
|
|
403
|
+
|
|
404
|
+
Returns a unified diff between two pipelines (useful for comparing “new” builder pipelines vs legacy/template pipelines).
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
legacy = PipelineBuilder().match({"status": "active"}).limit(10)
|
|
408
|
+
new = PipelineBuilder().match({"status": "inactive"}).limit(10)
|
|
409
|
+
|
|
410
|
+
print(new.compare_with(legacy))
|
|
411
|
+
```
|
|
412
|
+
|
|
348
413
|
##### `build() -> List[Dict[str, Any]]`
|
|
349
414
|
|
|
350
415
|
Returns the complete pipeline as a list of stage dictionaries.
|
|
@@ -391,6 +456,31 @@ pipeline = (
|
|
|
391
456
|
)
|
|
392
457
|
```
|
|
393
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
|
+
|
|
394
484
|
### Aggregation with Grouping
|
|
395
485
|
|
|
396
486
|
```python
|
|
@@ -500,11 +590,39 @@ base = get_base_pipeline(user_id)
|
|
|
500
590
|
# Create multiple queries from cached base
|
|
501
591
|
recent = base.copy().sort({"createdAt": -1}).limit(10).build()
|
|
502
592
|
by_category = base.copy().match({"category": "tech"}).build()
|
|
503
|
-
with_stats = base.copy().group(
|
|
593
|
+
with_stats = base.copy().group("$category", {"count": {"$sum": 1}}).build()
|
|
504
594
|
|
|
505
595
|
# Base pipeline is safely cached and reused
|
|
506
596
|
```
|
|
507
597
|
|
|
598
|
+
## Best Practices
|
|
599
|
+
|
|
600
|
+
### Array `_id` after `$group`: prefer `$arrayElemAt` and materialize fields
|
|
601
|
+
|
|
602
|
+
If you use `$group` with an array `_id` (e.g. `["_idSeason", "_idTournament"]`), avoid relying on `$_id` later in the pipeline.
|
|
603
|
+
Instead, **extract elements with `$arrayElemAt` and store them into explicit fields**, then use those fields in subsequent stages.
|
|
604
|
+
|
|
605
|
+
```python
|
|
606
|
+
pipeline = (
|
|
607
|
+
PipelineBuilder()
|
|
608
|
+
.group(
|
|
609
|
+
group_by=["$idSeason", "$idTournament"],
|
|
610
|
+
accumulators={"idTeams": {"$addToSet": "$idTeam"}},
|
|
611
|
+
)
|
|
612
|
+
.project({
|
|
613
|
+
"idSeason": {"$arrayElemAt": ["$_id", 0]},
|
|
614
|
+
"idTournament": {"$arrayElemAt": ["$_id", 1]},
|
|
615
|
+
"idTeams": 1,
|
|
616
|
+
# Optional: preserve array _id explicitly if you really need it later
|
|
617
|
+
# "_id": "$_id",
|
|
618
|
+
})
|
|
619
|
+
.build()
|
|
620
|
+
)
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
This pattern reduces surprises and helps avoid errors like:
|
|
624
|
+
`$first's argument must be an array, but is object`.
|
|
625
|
+
|
|
508
626
|
#### Example: Pipeline Factories
|
|
509
627
|
|
|
510
628
|
```python
|
|
@@ -70,6 +70,15 @@ Adds a `$match` stage to filter documents.
|
|
|
70
70
|
.match({"status": "active", "age": {"$gte": 18}})
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
##### `match_expr(expr: Dict[str, Any]) -> Self`
|
|
74
|
+
|
|
75
|
+
Adds a `$match` stage with an `$expr` condition (expression-based filter; useful for comparing fields or using variables from `let` in subpipelines).
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
.match_expr({"$eq": ["$id", "$$teamId"]})
|
|
79
|
+
.match_expr({"$and": [{"$gte": ["$field", "$other"]}, {"$lte": ["$score", 100]}]})
|
|
80
|
+
```
|
|
81
|
+
|
|
73
82
|
##### `lookup(from_collection: str, local_field: str, foreign_field: str, as_field: str, pipeline: Optional[List[Dict[str, Any]]] = None) -> Self`
|
|
74
83
|
|
|
75
84
|
Adds a `$lookup` stage to join with another collection.
|
|
@@ -84,6 +93,43 @@ Adds a `$lookup` stage to join with another collection.
|
|
|
84
93
|
)
|
|
85
94
|
```
|
|
86
95
|
|
|
96
|
+
##### `lookup_let(from_collection: str, let: Dict[str, Any], pipeline: Union[List[Dict[str, Any]], PipelineBuilder], as_field: str) -> Self`
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# With list of stages
|
|
102
|
+
.lookup_let(
|
|
103
|
+
from_collection="teams",
|
|
104
|
+
let={"teamId": "$idTeam"},
|
|
105
|
+
pipeline=[
|
|
106
|
+
{"$match": {"$expr": {"$eq": ["$_id", "$$teamId"]}}},
|
|
107
|
+
{"$project": {"name": 1, "_id": 0}}
|
|
108
|
+
],
|
|
109
|
+
as_field="team"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# With PipelineBuilder for the subpipeline (optionally using match_expr)
|
|
113
|
+
sub = PipelineBuilder().match_expr({"$eq": ["$_id", "$$teamId"]}).project({"name": 1, "_id": 0})
|
|
114
|
+
.lookup_let("teams", {"teamId": "$idTeam"}, sub, as_field="team")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
##### `union_with(coll: str, pipeline: Optional[Union[List[Dict[str, Any]], PipelineBuilder]] = None) -> Self`
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# Union with another collection (no subpipeline)
|
|
123
|
+
.union_with("other_coll")
|
|
124
|
+
|
|
125
|
+
# With subpipeline as list of stages
|
|
126
|
+
.union_with("logs", [{"$match": {"level": "error"}}, {"$limit": 100}])
|
|
127
|
+
|
|
128
|
+
# With PipelineBuilder for the subpipeline
|
|
129
|
+
sub = PipelineBuilder().match({"source": "individual"}).project({"name": 1})
|
|
130
|
+
.union_with("sso_individual_statistics", sub)
|
|
131
|
+
```
|
|
132
|
+
|
|
87
133
|
##### `add_fields(fields: Dict[str, Any]) -> Self`
|
|
88
134
|
|
|
89
135
|
Adds a `$addFields` stage to add or modify fields.
|
|
@@ -224,7 +270,7 @@ builder.prepend({"$match": {"deleted": False}})
|
|
|
224
270
|
Inserts a stage at a specific position (0-based index) in the pipeline.
|
|
225
271
|
|
|
226
272
|
```python
|
|
227
|
-
builder.match({"status": "active"}).group(
|
|
273
|
+
builder.match({"status": "active"}).group("$category", {"count": {"$sum": 1}})
|
|
228
274
|
builder.insert_at(1, {"$sort": {"name": 1}})
|
|
229
275
|
# Pipeline: [{"$match": {...}}, {"$sort": {...}}, {"$group": {...}}]
|
|
230
276
|
```
|
|
@@ -299,6 +345,15 @@ print(builder.pretty_print())
|
|
|
299
345
|
# ]
|
|
300
346
|
```
|
|
301
347
|
|
|
348
|
+
##### `pretty_print_stage(stage: Union[int, Dict[str, Any]], indent: int = 2, ensure_ascii: bool = False) -> str`
|
|
349
|
+
|
|
350
|
+
Returns a formatted JSON string representation of a single stage (by index or by dict).
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
builder = PipelineBuilder().match({"status": "active"}).limit(10)
|
|
354
|
+
print(builder.pretty_print_stage(0)) # Prints the $match stage
|
|
355
|
+
```
|
|
356
|
+
|
|
302
357
|
##### `to_json_file(filepath: Union[str, Path], indent: int = 2, ensure_ascii: bool = False, metadata: Optional[Dict[str, Any]] = None) -> None`
|
|
303
358
|
|
|
304
359
|
Saves the pipeline to a JSON file. Useful for debugging, comparison, or versioning.
|
|
@@ -317,6 +372,17 @@ builder.to_json_file(
|
|
|
317
372
|
)
|
|
318
373
|
```
|
|
319
374
|
|
|
375
|
+
##### `compare_with(other: PipelineBuilder, context_lines: int = 3) -> str`
|
|
376
|
+
|
|
377
|
+
Returns a unified diff between two pipelines (useful for comparing “new” builder pipelines vs legacy/template pipelines).
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
legacy = PipelineBuilder().match({"status": "active"}).limit(10)
|
|
381
|
+
new = PipelineBuilder().match({"status": "inactive"}).limit(10)
|
|
382
|
+
|
|
383
|
+
print(new.compare_with(legacy))
|
|
384
|
+
```
|
|
385
|
+
|
|
320
386
|
##### `build() -> List[Dict[str, Any]]`
|
|
321
387
|
|
|
322
388
|
Returns the complete pipeline as a list of stage dictionaries.
|
|
@@ -363,6 +429,31 @@ pipeline = (
|
|
|
363
429
|
)
|
|
364
430
|
```
|
|
365
431
|
|
|
432
|
+
### Lookup by expression (lookup_let)
|
|
433
|
+
|
|
434
|
+
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()`:
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
sub = (
|
|
438
|
+
PipelineBuilder()
|
|
439
|
+
.match_expr({"$eq": ["$_id", "$$teamId"]})
|
|
440
|
+
.project({"name": 1, "slug": 1, "_id": 0})
|
|
441
|
+
)
|
|
442
|
+
pipeline = (
|
|
443
|
+
PipelineBuilder()
|
|
444
|
+
.match({"status": "active"})
|
|
445
|
+
.lookup_let(
|
|
446
|
+
from_collection="teams",
|
|
447
|
+
let={"teamId": "$idTeam"},
|
|
448
|
+
pipeline=sub,
|
|
449
|
+
as_field="team"
|
|
450
|
+
)
|
|
451
|
+
.unwind("team", preserve_null_and_empty_arrays=True)
|
|
452
|
+
.project({"title": 1, "teamName": "$team.name"})
|
|
453
|
+
.build()
|
|
454
|
+
)
|
|
455
|
+
```
|
|
456
|
+
|
|
366
457
|
### Aggregation with Grouping
|
|
367
458
|
|
|
368
459
|
```python
|
|
@@ -472,11 +563,39 @@ base = get_base_pipeline(user_id)
|
|
|
472
563
|
# Create multiple queries from cached base
|
|
473
564
|
recent = base.copy().sort({"createdAt": -1}).limit(10).build()
|
|
474
565
|
by_category = base.copy().match({"category": "tech"}).build()
|
|
475
|
-
with_stats = base.copy().group(
|
|
566
|
+
with_stats = base.copy().group("$category", {"count": {"$sum": 1}}).build()
|
|
476
567
|
|
|
477
568
|
# Base pipeline is safely cached and reused
|
|
478
569
|
```
|
|
479
570
|
|
|
571
|
+
## Best Practices
|
|
572
|
+
|
|
573
|
+
### Array `_id` after `$group`: prefer `$arrayElemAt` and materialize fields
|
|
574
|
+
|
|
575
|
+
If you use `$group` with an array `_id` (e.g. `["_idSeason", "_idTournament"]`), avoid relying on `$_id` later in the pipeline.
|
|
576
|
+
Instead, **extract elements with `$arrayElemAt` and store them into explicit fields**, then use those fields in subsequent stages.
|
|
577
|
+
|
|
578
|
+
```python
|
|
579
|
+
pipeline = (
|
|
580
|
+
PipelineBuilder()
|
|
581
|
+
.group(
|
|
582
|
+
group_by=["$idSeason", "$idTournament"],
|
|
583
|
+
accumulators={"idTeams": {"$addToSet": "$idTeam"}},
|
|
584
|
+
)
|
|
585
|
+
.project({
|
|
586
|
+
"idSeason": {"$arrayElemAt": ["$_id", 0]},
|
|
587
|
+
"idTournament": {"$arrayElemAt": ["$_id", 1]},
|
|
588
|
+
"idTeams": 1,
|
|
589
|
+
# Optional: preserve array _id explicitly if you really need it later
|
|
590
|
+
# "_id": "$_id",
|
|
591
|
+
})
|
|
592
|
+
.build()
|
|
593
|
+
)
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
This pattern reduces surprises and helps avoid errors like:
|
|
597
|
+
`$first's argument must be an array, but is object`.
|
|
598
|
+
|
|
480
599
|
#### Example: Pipeline Factories
|
|
481
600
|
|
|
482
601
|
```python
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mongo-pipebuilder"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Type-safe, fluent MongoDB aggregation pipeline builder"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"typing_extensions>=4.0.0; python_version<'3.11'",
|
|
13
13
|
]
|
|
14
|
-
license =
|
|
14
|
+
license = "MIT"
|
|
15
15
|
authors = [
|
|
16
16
|
{name = "seligoroff", email = "seligoroff@gmail.com"}
|
|
17
17
|
]
|
|
@@ -19,7 +19,6 @@ keywords = ["mongodb", "aggregation", "pipeline", "builder", "query"]
|
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Development Status :: 3 - Alpha",
|
|
21
21
|
"Intended Audience :: Developers",
|
|
22
|
-
"License :: OSI Approved :: MIT License",
|
|
23
22
|
"Programming Language :: Python :: 3",
|
|
24
23
|
"Programming Language :: Python :: 3.8",
|
|
25
24
|
"Programming Language :: Python :: 3.9",
|
|
@@ -51,12 +50,13 @@ addopts = [
|
|
|
51
50
|
"--cov=src/mongo_pipebuilder",
|
|
52
51
|
"--cov-report=term-missing",
|
|
53
52
|
"--cov-report=html:tests/htmlcov",
|
|
54
|
-
"--cov-report=json:coverage.json",
|
|
55
|
-
"--cov-report=xml:coverage.xml",
|
|
53
|
+
"--cov-report=json:tests/coverage.json",
|
|
54
|
+
"--cov-report=xml:tests/coverage.xml",
|
|
56
55
|
"--cov-branch",
|
|
57
56
|
]
|
|
58
57
|
|
|
59
58
|
[tool.coverage.run]
|
|
59
|
+
data_file = "tests/.coverage"
|
|
60
60
|
source = ["src/mongo_pipebuilder"]
|
|
61
61
|
omit = [
|
|
62
62
|
"*/tests/*",
|
|
@@ -99,3 +99,44 @@ warn_no_return = true
|
|
|
99
99
|
module = "tests.*"
|
|
100
100
|
disallow_untyped_defs = false
|
|
101
101
|
|
|
102
|
+
[tool.ruff]
|
|
103
|
+
line-length = 110
|
|
104
|
+
target-version = "py311"
|
|
105
|
+
# В Ruff лучше использовать .gitignore, но если нужно строго как было:
|
|
106
|
+
#respect-gitignore = false
|
|
107
|
+
#extend-exclude = ["venv", "deprecated"]
|
|
108
|
+
|
|
109
|
+
[tool.ruff.lint]
|
|
110
|
+
# E, F — база (Flake8), B — Bugbear (ошибки логики), I — Isort (импорты)
|
|
111
|
+
select = ["E", "F", "W", "B", "C901", "I", "Q", "T20", "COM"]
|
|
112
|
+
ignore = [
|
|
113
|
+
"B008", # Аргументы-функции в дефолтных значениях (часто нужно для FastAPI Depends)
|
|
114
|
+
"E266", # Слишком много # в комментариях
|
|
115
|
+
"E712", # Сравнение с True через == (иногда нужно в SQLAlchemy)
|
|
116
|
+
"COM812", # Конфликт с форматтером (запятые)
|
|
117
|
+
"COM819", # Trailing comma в type hints
|
|
118
|
+
"E203", # Пробел перед двоеточием (совместимость с Black)
|
|
119
|
+
"Q003", # Кавычки в f-строках
|
|
120
|
+
"PT006", # Типы параметров pytest
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[tool.ruff.lint.per-file-ignores]
|
|
124
|
+
# Игнорируем специфичные ошибки в тестах, скриптах, примерах и инициализации
|
|
125
|
+
"tests/*" = ["T201"]
|
|
126
|
+
"examples/*" = ["T201"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
[tool.ruff.lint.mccabe]
|
|
130
|
+
max-complexity = 12
|
|
131
|
+
|
|
132
|
+
[tool.ruff.lint.isort]
|
|
133
|
+
# Автоматически определяет внутренние модули, чтобы не мешать их с pip-пакетами
|
|
134
|
+
known-first-party = [
|
|
135
|
+
"api", "cli", "constants", "core", "helpers",
|
|
136
|
+
"infrastructure", "schemas", "scripts", "services",
|
|
137
|
+
"startup", "tasks", "tests"
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
[tool.ruff.format]
|
|
141
|
+
quote-style = "double"
|
|
142
|
+
indent-style = "space"
|
|
File without changes
|