mongo-pipebuilder 0.2.1__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.1"
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}})
@@ -158,36 +157,47 @@ class PipelineBuilder:
158
157
  self._stages.append({"$project": fields})
159
158
  return self
160
159
 
161
- def group(self, group_by: Dict[str, Any], accumulators: Dict[str, Any]) -> Self:
160
+ def group(self, group_by: Union[str, Dict[str, Any], Any], accumulators: Dict[str, Any]) -> Self:
162
161
  """
163
162
  Add a $group stage for grouping documents.
164
163
 
165
164
  Args:
166
- group_by: Expression for grouping (becomes _id)
165
+ group_by: Expression for grouping (becomes _id). Can be:
166
+ - A string (field path, e.g., "$category")
167
+ - A dict (composite key, e.g., {"category": "$category"})
168
+ - Any other value (null, number, etc.)
167
169
  accumulators: Dictionary with accumulators (sum, avg, count, etc.)
168
170
 
169
171
  Returns:
170
172
  Self for method chaining
171
173
 
172
174
  Raises:
173
- TypeError: If arguments are not dictionaries
174
- ValueError: If both group_by and accumulators are empty
175
+ TypeError: If accumulators is not a dictionary
176
+ ValueError: If both group_by and accumulators are empty (when group_by is dict/str)
175
177
 
176
178
  Example:
177
179
  >>> builder.group(
178
- ... group_by={"category": "$category"},
180
+ ... group_by="$category", # String field path
181
+ ... accumulators={"total": {"$sum": "$amount"}}
182
+ ... )
183
+ >>> builder.group(
184
+ ... group_by={"category": "$category"}, # Composite key
179
185
  ... accumulators={"total": {"$sum": "$amount"}}
180
186
  ... )
181
187
  """
182
- if not isinstance(group_by, dict):
183
- raise TypeError(f"group_by must be a dict, got {type(group_by)}")
184
188
  if not isinstance(accumulators, dict):
185
189
  raise TypeError(f"accumulators must be a dict, got {type(accumulators)}")
186
190
 
187
- # Empty group_by is technically valid in MongoDB (groups all into one document)
188
- # But if both are empty, it's likely an error
189
- if not group_by and not accumulators:
190
- raise ValueError("group_by and accumulators cannot both be empty")
191
+ # Validate empty cases
192
+ # group_by can be None, empty string, empty dict, etc. - all are valid in MongoDB
193
+ # But if it's a string and empty, or dict and empty, and accumulators is also empty,
194
+ # it's likely an error
195
+ if isinstance(group_by, dict):
196
+ if not group_by and not accumulators:
197
+ raise ValueError("group_by and accumulators cannot both be empty")
198
+ elif isinstance(group_by, str):
199
+ if not group_by and not accumulators:
200
+ raise ValueError("group_by and accumulators cannot both be empty")
191
201
 
192
202
  group_stage = {"_id": group_by, **accumulators}
193
203
  self._stages.append({"$group": group_stage})
@@ -625,23 +635,15 @@ class PipelineBuilder:
625
635
  "Only one output stage is allowed."
626
636
  )
627
637
 
628
- # If $out exists, it must be the last stage
629
- if has_out:
630
- out_index = stage_types.index("$out")
631
- if out_index != len(stage_types) - 1:
632
- raise ValueError(
633
- f"$out stage must be the last stage in the pipeline. "
634
- f"Found at position {out_index + 1} of {len(stage_types)}."
635
- )
636
-
637
- # If $merge exists, it must be the last stage
638
- if has_merge:
639
- merge_index = stage_types.index("$merge")
640
- if merge_index != len(stage_types) - 1:
641
- raise ValueError(
642
- f"$merge stage must be the last stage in the pipeline. "
643
- f"Found at position {merge_index + 1} of {len(stage_types)}."
644
- )
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
+ )
645
647
 
646
648
  return True
647
649
 
@@ -658,7 +660,7 @@ class PipelineBuilder:
658
660
  >>> builder.get_stage_types()
659
661
  ['$match', '$limit']
660
662
  """
661
- return [list(stage.keys())[0] for stage in self._stages]
663
+ return [next(iter(stage)) for stage in self._stages]
662
664
 
663
665
  def has_stage(self, stage_type: str) -> bool:
664
666
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mongo-pipebuilder
3
- Version: 0.2.1
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
@@ -373,3 +539,8 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) for development guidelines.
373
539
 
374
540
  MIT License - see [LICENSE](LICENSE) file for details.
375
541
 
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,,
@@ -20,3 +20,8 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
+
24
+
25
+
26
+
27
+
@@ -1,7 +0,0 @@
1
- mongo_pipebuilder/__init__.py,sha256=eOj_NuBMA9YbVFPmm13UV25RR5_QK2ctrx8QXV3yGTU,336
2
- mongo_pipebuilder/builder.py,sha256=kurxcQ5IXErefUQjqQ5XAzkfZEv9siN-PufmWoFA0aE,26545
3
- mongo_pipebuilder-0.2.1.dist-info/licenses/LICENSE,sha256=GLx_6hrvLsyIL34dpRYvjCSIXyYD8PzhBR09opTrixI,1088
4
- mongo_pipebuilder-0.2.1.dist-info/METADATA,sha256=Kjav6gd0U9MREeLM3tcLz2b5RfBlCyjnJP0H9gG97XM,9126
5
- mongo_pipebuilder-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- mongo_pipebuilder-0.2.1.dist-info/top_level.txt,sha256=wLn7H_v-qaNIws5FeBbKPZBCmYFYgFEhPaLjoCWcisc,18
7
- mongo_pipebuilder-0.2.1.dist-info/RECORD,,