mongo-pipebuilder 0.2.2__py3-none-any.whl → 0.2.3__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 +11 -20
- {mongo_pipebuilder-0.2.2.dist-info → mongo_pipebuilder-0.2.3.dist-info}/METADATA +176 -6
- mongo_pipebuilder-0.2.3.dist-info/RECORD +7 -0
- {mongo_pipebuilder-0.2.2.dist-info → mongo_pipebuilder-0.2.3.dist-info}/licenses/LICENSE +4 -0
- mongo_pipebuilder-0.2.2.dist-info/RECORD +0 -7
- {mongo_pipebuilder-0.2.2.dist-info → mongo_pipebuilder-0.2.3.dist-info}/WHEEL +0 -0
- {mongo_pipebuilder-0.2.2.dist-info → mongo_pipebuilder-0.2.3.dist-info}/top_level.txt +0 -0
mongo_pipebuilder/__init__.py
CHANGED
mongo_pipebuilder/builder.py
CHANGED
|
@@ -33,8 +33,7 @@ class PipelineBuilder:
|
|
|
33
33
|
Self for method chaining
|
|
34
34
|
|
|
35
35
|
Raises:
|
|
36
|
-
TypeError: If conditions is not a dictionary
|
|
37
|
-
ValueError: If conditions is None
|
|
36
|
+
TypeError: If conditions is None or not a dictionary
|
|
38
37
|
|
|
39
38
|
Example:
|
|
40
39
|
>>> builder.match({"status": "active", "age": {"$gte": 18}})
|
|
@@ -636,23 +635,15 @@ class PipelineBuilder:
|
|
|
636
635
|
"Only one output stage is allowed."
|
|
637
636
|
)
|
|
638
637
|
|
|
639
|
-
#
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
# If $merge exists, it must be the last stage
|
|
649
|
-
if has_merge:
|
|
650
|
-
merge_index = stage_types.index("$merge")
|
|
651
|
-
if merge_index != len(stage_types) - 1:
|
|
652
|
-
raise ValueError(
|
|
653
|
-
f"$merge stage must be the last stage in the pipeline. "
|
|
654
|
-
f"Found at position {merge_index + 1} of {len(stage_types)}."
|
|
655
|
-
)
|
|
638
|
+
# Check if $out or $merge exist and validate position
|
|
639
|
+
for stage_name in ["$out", "$merge"]:
|
|
640
|
+
if stage_name in stage_types:
|
|
641
|
+
stage_index = stage_types.index(stage_name)
|
|
642
|
+
if stage_index != len(stage_types) - 1:
|
|
643
|
+
raise ValueError(
|
|
644
|
+
f"{stage_name} stage must be the last stage in the pipeline. "
|
|
645
|
+
f"Found at position {stage_index + 1} of {len(stage_types)}."
|
|
646
|
+
)
|
|
656
647
|
|
|
657
648
|
return True
|
|
658
649
|
|
|
@@ -669,7 +660,7 @@ class PipelineBuilder:
|
|
|
669
660
|
>>> builder.get_stage_types()
|
|
670
661
|
['$match', '$limit']
|
|
671
662
|
"""
|
|
672
|
-
return [
|
|
663
|
+
return [next(iter(stage)) for stage in self._stages]
|
|
673
664
|
|
|
674
665
|
def has_stage(self, stage_type: str) -> bool:
|
|
675
666
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongo-pipebuilder
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Type-safe, fluent MongoDB aggregation pipeline builder
|
|
5
5
|
Author-email: seligoroff <seligoroff@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,6 +28,12 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
# mongo-pipebuilder
|
|
30
30
|
|
|
31
|
+
[](https://badge.fury.io/py/mongo-pipebuilder)
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
[](https://github.com/psf/black)
|
|
35
|
+
[](https://github.com/seligoroff/mongo-pipebuilder)
|
|
36
|
+
|
|
31
37
|
Type-safe, fluent MongoDB aggregation pipeline builder for Python.
|
|
32
38
|
|
|
33
39
|
## Overview
|
|
@@ -36,11 +42,11 @@ Type-safe, fluent MongoDB aggregation pipeline builder for Python.
|
|
|
36
42
|
|
|
37
43
|
## Features
|
|
38
44
|
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
45
|
+
- **Type-safe**: Full type hints support with IDE autocomplete
|
|
46
|
+
- **Fluent interface**: Chain methods for readable, maintainable code
|
|
47
|
+
- **Zero dependencies**: Pure Python, lightweight package
|
|
48
|
+
- **Extensible**: Easy to add custom stages via `add_stage()`
|
|
49
|
+
- **Well tested**: Comprehensive test suite with 96%+ coverage
|
|
44
50
|
|
|
45
51
|
## Installation
|
|
46
52
|
|
|
@@ -259,6 +265,22 @@ group_index = stage_types.index("$group")
|
|
|
259
265
|
builder.insert_at(group_index, {"$addFields": {"x": 1}})
|
|
260
266
|
```
|
|
261
267
|
|
|
268
|
+
##### `copy() -> PipelineBuilder`
|
|
269
|
+
|
|
270
|
+
Creates an independent copy of the builder with current stages. Useful for creating immutable variants and composing pipelines.
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
builder1 = PipelineBuilder().match({"status": "active"})
|
|
274
|
+
builder2 = builder1.copy()
|
|
275
|
+
builder2.limit(10)
|
|
276
|
+
|
|
277
|
+
# Original unchanged
|
|
278
|
+
assert len(builder1) == 1
|
|
279
|
+
assert len(builder2) == 2
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
See [Composing and Reusing Pipelines](#composing-and-reusing-pipelines) for practical examples.
|
|
283
|
+
|
|
262
284
|
##### `validate() -> bool`
|
|
263
285
|
|
|
264
286
|
Validates the pipeline before execution. Checks that:
|
|
@@ -340,6 +362,150 @@ pipeline = (
|
|
|
340
362
|
)
|
|
341
363
|
```
|
|
342
364
|
|
|
365
|
+
### Composing and Reusing Pipelines
|
|
366
|
+
|
|
367
|
+
The `copy()` method allows you to create immutable variants of pipelines, enabling safe composition and reuse. This is useful when you need to:
|
|
368
|
+
- Create multiple variants from a base pipeline
|
|
369
|
+
- Compose pipelines functionally
|
|
370
|
+
- Cache base pipelines safely
|
|
371
|
+
- Pass pipelines to functions without side effects
|
|
372
|
+
|
|
373
|
+
#### Example: Building Multiple Variants from a Base Pipeline
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from mongo_pipebuilder import PipelineBuilder
|
|
377
|
+
|
|
378
|
+
# Base pipeline with common filtering and joining
|
|
379
|
+
base_pipeline = (
|
|
380
|
+
PipelineBuilder()
|
|
381
|
+
.match({"status": "published", "deleted": False})
|
|
382
|
+
.lookup(
|
|
383
|
+
from_collection="authors",
|
|
384
|
+
local_field="authorId",
|
|
385
|
+
foreign_field="_id",
|
|
386
|
+
as_field="author"
|
|
387
|
+
)
|
|
388
|
+
.unwind("author", preserve_null_and_empty_arrays=True)
|
|
389
|
+
.project({
|
|
390
|
+
"title": 1,
|
|
391
|
+
"authorName": "$author.name",
|
|
392
|
+
"publishedAt": 1
|
|
393
|
+
})
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Create variants with different sorting and limits
|
|
397
|
+
recent_posts = base_pipeline.copy().sort({"publishedAt": -1}).limit(10).build()
|
|
398
|
+
popular_posts = base_pipeline.copy().sort({"views": -1}).limit(5).build()
|
|
399
|
+
author_posts = base_pipeline.copy().match({"authorName": "John Doe"}).build()
|
|
400
|
+
|
|
401
|
+
# Base pipeline remains unchanged
|
|
402
|
+
assert len(base_pipeline) == 4 # Still has 4 stages
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### Example: Functional Composition Pattern
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
def add_pagination(builder, page: int, page_size: int = 10):
|
|
409
|
+
"""Add pagination to a pipeline."""
|
|
410
|
+
return builder.copy().skip(page * page_size).limit(page_size)
|
|
411
|
+
|
|
412
|
+
def add_sorting(builder, sort_field: str, ascending: bool = True):
|
|
413
|
+
"""Add sorting to a pipeline."""
|
|
414
|
+
return builder.copy().sort({sort_field: 1 if ascending else -1})
|
|
415
|
+
|
|
416
|
+
# Compose pipelines functionally
|
|
417
|
+
base = PipelineBuilder().match({"status": "active"})
|
|
418
|
+
|
|
419
|
+
# Create different variants
|
|
420
|
+
page1 = add_pagination(add_sorting(base, "createdAt"), page=0)
|
|
421
|
+
page2 = add_pagination(add_sorting(base, "createdAt"), page=1)
|
|
422
|
+
sorted_by_name = add_sorting(base, "name", ascending=True)
|
|
423
|
+
|
|
424
|
+
# All variants are independent
|
|
425
|
+
assert len(base) == 1 # Base unchanged
|
|
426
|
+
assert len(page1) == 3 # match + sort + skip + limit
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### Example: Caching Base Pipelines
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
from functools import lru_cache
|
|
433
|
+
|
|
434
|
+
@lru_cache(maxsize=100)
|
|
435
|
+
def get_base_pipeline(user_id: str):
|
|
436
|
+
"""Cache base pipeline for a user."""
|
|
437
|
+
return (
|
|
438
|
+
PipelineBuilder()
|
|
439
|
+
.match({"userId": user_id, "status": "active"})
|
|
440
|
+
.lookup(
|
|
441
|
+
from_collection="profiles",
|
|
442
|
+
local_field="userId",
|
|
443
|
+
foreign_field="_id",
|
|
444
|
+
as_field="profile"
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Reuse cached base pipeline with different modifications
|
|
449
|
+
user_id = "12345"
|
|
450
|
+
base = get_base_pipeline(user_id)
|
|
451
|
+
|
|
452
|
+
# Create multiple queries from cached base
|
|
453
|
+
recent = base.copy().sort({"createdAt": -1}).limit(10).build()
|
|
454
|
+
by_category = base.copy().match({"category": "tech"}).build()
|
|
455
|
+
with_stats = base.copy().group({"_id": "$category"}, {"count": {"$sum": 1}}).build()
|
|
456
|
+
|
|
457
|
+
# Base pipeline is safely cached and reused
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
#### Example: Pipeline Factories
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
class PipelineFactory:
|
|
464
|
+
"""Factory for creating common pipeline patterns."""
|
|
465
|
+
|
|
466
|
+
@staticmethod
|
|
467
|
+
def base_article_pipeline():
|
|
468
|
+
"""Base pipeline for articles."""
|
|
469
|
+
return (
|
|
470
|
+
PipelineBuilder()
|
|
471
|
+
.match({"status": "published"})
|
|
472
|
+
.lookup(
|
|
473
|
+
from_collection="authors",
|
|
474
|
+
local_field="authorId",
|
|
475
|
+
foreign_field="_id",
|
|
476
|
+
as_field="author"
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def with_author_filter(builder, author_name: str):
|
|
482
|
+
"""Add author filter to pipeline."""
|
|
483
|
+
return builder.copy().match({"author.name": author_name})
|
|
484
|
+
|
|
485
|
+
@staticmethod
|
|
486
|
+
def with_date_range(builder, start_date: str, end_date: str):
|
|
487
|
+
"""Add date range filter to pipeline."""
|
|
488
|
+
return builder.copy().match({
|
|
489
|
+
"publishedAt": {"$gte": start_date, "$lte": end_date}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
# Usage
|
|
493
|
+
base = PipelineFactory.base_article_pipeline()
|
|
494
|
+
johns_articles = PipelineFactory.with_author_filter(base, "John Doe")
|
|
495
|
+
recent_johns = PipelineFactory.with_date_range(
|
|
496
|
+
johns_articles,
|
|
497
|
+
start_date="2024-01-01",
|
|
498
|
+
end_date="2024-12-31"
|
|
499
|
+
).sort({"publishedAt": -1}).limit(10).build()
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Key Benefits:**
|
|
503
|
+
- Safe reuse: Base pipelines remain unchanged
|
|
504
|
+
- Functional composition: Build pipelines from smaller parts
|
|
505
|
+
- Caching friendly: Base pipelines can be safely cached
|
|
506
|
+
- No side effects: Functions can safely modify copies
|
|
507
|
+
- Thread-safe: Multiple threads can use copies independently
|
|
508
|
+
|
|
343
509
|
## Development
|
|
344
510
|
|
|
345
511
|
### Project Structure
|
|
@@ -374,3 +540,7 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) for development guidelines.
|
|
|
374
540
|
MIT License - see [LICENSE](LICENSE) file for details.
|
|
375
541
|
|
|
376
542
|
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mongo_pipebuilder/__init__.py,sha256=82PaAyv4VoEvfvVhlYnMTPnZEMiOI24Q4Nw9RSrEjdA,336
|
|
2
|
+
mongo_pipebuilder/builder.py,sha256=GivmjNqk2K5v3fX1TWMFFH7jx3WxlWWhlggWoRxCNl4,26875
|
|
3
|
+
mongo_pipebuilder-0.2.3.dist-info/licenses/LICENSE,sha256=ITJa-Zkh2Qc1_xRiHcfkL5zsmTicbSxqsMih4cjtBM4,1093
|
|
4
|
+
mongo_pipebuilder-0.2.3.dist-info/METADATA,sha256=eh6i_U365QAqWhERNAsFOgEVpjokTt4VEEJH8SqDAtA,14661
|
|
5
|
+
mongo_pipebuilder-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
mongo_pipebuilder-0.2.3.dist-info/top_level.txt,sha256=wLn7H_v-qaNIws5FeBbKPZBCmYFYgFEhPaLjoCWcisc,18
|
|
7
|
+
mongo_pipebuilder-0.2.3.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
mongo_pipebuilder/__init__.py,sha256=VF9G-Gp0kabZMoEmeQnSnSqHT3hLqxlEHY-FBeXfnVA,336
|
|
2
|
-
mongo_pipebuilder/builder.py,sha256=qvyQd1k9YbaIV0tRixQnsNr7yuE-_SSGN5tBSwDAk5E,27199
|
|
3
|
-
mongo_pipebuilder-0.2.2.dist-info/licenses/LICENSE,sha256=xAHmf48PmIziXYIdaJzRYeYpXFUPIb70SsSPhAHdggY,1089
|
|
4
|
-
mongo_pipebuilder-0.2.2.dist-info/METADATA,sha256=QZJAl4akVEiT9ucIaviwAwJuvTWFL9XyBEyKb_LI6jc,9127
|
|
5
|
-
mongo_pipebuilder-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
mongo_pipebuilder-0.2.2.dist-info/top_level.txt,sha256=wLn7H_v-qaNIws5FeBbKPZBCmYFYgFEhPaLjoCWcisc,18
|
|
7
|
-
mongo_pipebuilder-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|