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.
@@ -9,6 +9,6 @@ Author: seligoroff
9
9
 
10
10
  from mongo_pipebuilder.builder import PipelineBuilder
11
11
 
12
- __version__ = "0.2.2"
12
+ __version__ = "0.2.3"
13
13
  __all__ = ["PipelineBuilder"]
14
14
 
@@ -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
- # If $out exists, it must be the last stage
640
- if has_out:
641
- out_index = stage_types.index("$out")
642
- if out_index != len(stage_types) - 1:
643
- raise ValueError(
644
- f"$out stage must be the last stage in the pipeline. "
645
- f"Found at position {out_index + 1} of {len(stage_types)}."
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 [list(stage.keys())[0] for stage in self._stages]
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.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
+ [![PyPI version](https://badge.fury.io/py/mongo-pipebuilder.svg)](https://badge.fury.io/py/mongo-pipebuilder)
32
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
35
+ [![Test Coverage](https://img.shields.io/badge/coverage-96%25-green.svg)](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
- - **Type-safe**: Full type hints support with IDE autocomplete
40
- - **Fluent interface**: Chain methods for readable, maintainable code
41
- - **Zero dependencies**: Pure Python, lightweight package
42
- - **Extensible**: Easy to add custom stages via `add_stage()`
43
- - **Well tested**: Comprehensive test suite with 96%+ coverage
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,,
@@ -21,3 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
23
 
24
+
25
+
26
+
27
+
@@ -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,,