mongo-pipebuilder 0.2.3__tar.gz → 0.3.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.2.3 → mongo_pipebuilder-0.3.0}/LICENSE +11 -0
- {mongo_pipebuilder-0.2.3/src/mongo_pipebuilder.egg-info → mongo_pipebuilder-0.3.0}/PKG-INFO +60 -1
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/README.md +59 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/pyproject.toml +1 -1
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder/__init__.py +1 -1
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder/builder.py +102 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0/src/mongo_pipebuilder.egg-info}/PKG-INFO +60 -1
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_debug.py +246 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_insert.py +1 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_validation.py +11 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_validation_existing.py +1 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_validation_new.py +1 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/setup.cfg +0 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/SOURCES.txt +0 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/dependency_links.txt +0 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/requires.txt +0 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/top_level.txt +0 -0
- {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongo-pipebuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Type-safe, fluent MongoDB aggregation pipeline builder
|
|
5
5
|
Author-email: seligoroff <seligoroff@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -297,6 +297,54 @@ builder.add_stage({"$out": "output"}).match({"status": "active"})
|
|
|
297
297
|
builder.validate() # Raises ValueError: $out stage must be the last stage
|
|
298
298
|
```
|
|
299
299
|
|
|
300
|
+
##### `get_stage_at(index: int) -> Dict[str, Any]`
|
|
301
|
+
|
|
302
|
+
Gets a specific stage from the pipeline by index. Returns a copy of the stage.
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
builder = PipelineBuilder()
|
|
306
|
+
builder.match({"status": "active"}).limit(10)
|
|
307
|
+
stage = builder.get_stage_at(0) # Returns {"$match": {"status": "active"}}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
##### `pretty_print(indent: int = 2, ensure_ascii: bool = False) -> str`
|
|
311
|
+
|
|
312
|
+
Returns a formatted JSON string representation of the pipeline. Useful for debugging.
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
builder = PipelineBuilder()
|
|
316
|
+
builder.match({"status": "active"}).limit(10)
|
|
317
|
+
print(builder.pretty_print())
|
|
318
|
+
# [
|
|
319
|
+
# {
|
|
320
|
+
# "$match": {
|
|
321
|
+
# "status": "active"
|
|
322
|
+
# }
|
|
323
|
+
# },
|
|
324
|
+
# {
|
|
325
|
+
# "$limit": 10
|
|
326
|
+
# }
|
|
327
|
+
# ]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
##### `to_json_file(filepath: Union[str, Path], indent: int = 2, ensure_ascii: bool = False, metadata: Optional[Dict[str, Any]] = None) -> None`
|
|
331
|
+
|
|
332
|
+
Saves the pipeline to a JSON file. Useful for debugging, comparison, or versioning.
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
builder = PipelineBuilder()
|
|
336
|
+
builder.match({"status": "active"}).limit(10)
|
|
337
|
+
|
|
338
|
+
# Basic usage
|
|
339
|
+
builder.to_json_file("debug_pipeline.json")
|
|
340
|
+
|
|
341
|
+
# With metadata
|
|
342
|
+
builder.to_json_file(
|
|
343
|
+
"pipeline.json",
|
|
344
|
+
metadata={"version": "1.0", "author": "developer"}
|
|
345
|
+
)
|
|
346
|
+
```
|
|
347
|
+
|
|
300
348
|
##### `build() -> List[Dict[str, Any]]`
|
|
301
349
|
|
|
302
350
|
Returns the complete pipeline as a list of stage dictionaries.
|
|
@@ -544,3 +592,14 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
544
592
|
|
|
545
593
|
|
|
546
594
|
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
@@ -269,6 +269,54 @@ builder.add_stage({"$out": "output"}).match({"status": "active"})
|
|
|
269
269
|
builder.validate() # Raises ValueError: $out stage must be the last stage
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
+
##### `get_stage_at(index: int) -> Dict[str, Any]`
|
|
273
|
+
|
|
274
|
+
Gets a specific stage from the pipeline by index. Returns a copy of the stage.
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
builder = PipelineBuilder()
|
|
278
|
+
builder.match({"status": "active"}).limit(10)
|
|
279
|
+
stage = builder.get_stage_at(0) # Returns {"$match": {"status": "active"}}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
##### `pretty_print(indent: int = 2, ensure_ascii: bool = False) -> str`
|
|
283
|
+
|
|
284
|
+
Returns a formatted JSON string representation of the pipeline. Useful for debugging.
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
builder = PipelineBuilder()
|
|
288
|
+
builder.match({"status": "active"}).limit(10)
|
|
289
|
+
print(builder.pretty_print())
|
|
290
|
+
# [
|
|
291
|
+
# {
|
|
292
|
+
# "$match": {
|
|
293
|
+
# "status": "active"
|
|
294
|
+
# }
|
|
295
|
+
# },
|
|
296
|
+
# {
|
|
297
|
+
# "$limit": 10
|
|
298
|
+
# }
|
|
299
|
+
# ]
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
##### `to_json_file(filepath: Union[str, Path], indent: int = 2, ensure_ascii: bool = False, metadata: Optional[Dict[str, Any]] = None) -> None`
|
|
303
|
+
|
|
304
|
+
Saves the pipeline to a JSON file. Useful for debugging, comparison, or versioning.
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
builder = PipelineBuilder()
|
|
308
|
+
builder.match({"status": "active"}).limit(10)
|
|
309
|
+
|
|
310
|
+
# Basic usage
|
|
311
|
+
builder.to_json_file("debug_pipeline.json")
|
|
312
|
+
|
|
313
|
+
# With metadata
|
|
314
|
+
builder.to_json_file(
|
|
315
|
+
"pipeline.json",
|
|
316
|
+
metadata={"version": "1.0", "author": "developer"}
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
272
320
|
##### `build() -> List[Dict[str, Any]]`
|
|
273
321
|
|
|
274
322
|
Returns the complete pipeline as a list of stage dictionaries.
|
|
@@ -516,3 +564,14 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
516
564
|
|
|
517
565
|
|
|
518
566
|
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
|
|
@@ -6,6 +6,8 @@ Builder Pattern implementation for safe construction of MongoDB aggregation pipe
|
|
|
6
6
|
|
|
7
7
|
Author: seligoroff
|
|
8
8
|
"""
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
9
11
|
from typing import Any, Dict, List, Optional, Union
|
|
10
12
|
|
|
11
13
|
# For compatibility with Python < 3.11
|
|
@@ -753,6 +755,106 @@ class PipelineBuilder:
|
|
|
753
755
|
self._stages.insert(position, stage)
|
|
754
756
|
return self
|
|
755
757
|
|
|
758
|
+
def get_stage_at(self, index: int) -> Dict[str, Any]:
|
|
759
|
+
"""
|
|
760
|
+
Get a specific stage from the pipeline by index.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
index: Zero-based index of the stage to retrieve
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
Dictionary representing the stage at the given index
|
|
767
|
+
|
|
768
|
+
Raises:
|
|
769
|
+
IndexError: If index is out of range
|
|
770
|
+
|
|
771
|
+
Example:
|
|
772
|
+
>>> builder = PipelineBuilder()
|
|
773
|
+
>>> builder.match({"status": "active"}).limit(10)
|
|
774
|
+
>>> stage = builder.get_stage_at(0)
|
|
775
|
+
>>> stage
|
|
776
|
+
{"$match": {"status": "active"}}
|
|
777
|
+
"""
|
|
778
|
+
if index < 0 or index >= len(self._stages):
|
|
779
|
+
raise IndexError(
|
|
780
|
+
f"Index {index} out of range [0, {len(self._stages)}]"
|
|
781
|
+
)
|
|
782
|
+
return self._stages[index].copy()
|
|
783
|
+
|
|
784
|
+
def pretty_print(self, indent: int = 2, ensure_ascii: bool = False) -> str:
|
|
785
|
+
"""
|
|
786
|
+
Return a formatted JSON string representation of the pipeline.
|
|
787
|
+
|
|
788
|
+
Useful for debugging and understanding pipeline structure.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
indent: Number of spaces for indentation (default: 2)
|
|
792
|
+
ensure_ascii: If False, non-ASCII characters are output as-is (default: False)
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
Formatted JSON string of the pipeline
|
|
796
|
+
|
|
797
|
+
Example:
|
|
798
|
+
>>> builder = PipelineBuilder()
|
|
799
|
+
>>> builder.match({"status": "active"}).limit(10)
|
|
800
|
+
>>> print(builder.pretty_print())
|
|
801
|
+
[
|
|
802
|
+
{
|
|
803
|
+
"$match": {
|
|
804
|
+
"status": "active"
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
"$limit": 10
|
|
809
|
+
}
|
|
810
|
+
]
|
|
811
|
+
"""
|
|
812
|
+
return json.dumps(self._stages, indent=indent, ensure_ascii=ensure_ascii)
|
|
813
|
+
|
|
814
|
+
def to_json_file(
|
|
815
|
+
self,
|
|
816
|
+
filepath: Union[str, Path],
|
|
817
|
+
indent: int = 2,
|
|
818
|
+
ensure_ascii: bool = False,
|
|
819
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
820
|
+
) -> None:
|
|
821
|
+
"""
|
|
822
|
+
Save the pipeline to a JSON file.
|
|
823
|
+
|
|
824
|
+
Useful for debugging, comparison with other pipelines, or versioning.
|
|
825
|
+
|
|
826
|
+
Args:
|
|
827
|
+
filepath: Path to the output JSON file (str or Path)
|
|
828
|
+
indent: Number of spaces for indentation (default: 2)
|
|
829
|
+
ensure_ascii: If False, non-ASCII characters are output as-is (default: False)
|
|
830
|
+
metadata: Optional metadata to include in the JSON file
|
|
831
|
+
|
|
832
|
+
Raises:
|
|
833
|
+
IOError: If file cannot be written
|
|
834
|
+
|
|
835
|
+
Example:
|
|
836
|
+
>>> builder = PipelineBuilder()
|
|
837
|
+
>>> builder.match({"status": "active"}).limit(10)
|
|
838
|
+
>>> builder.to_json_file("debug_pipeline.json")
|
|
839
|
+
|
|
840
|
+
>>> # With metadata
|
|
841
|
+
>>> builder.to_json_file(
|
|
842
|
+
... "pipeline.json",
|
|
843
|
+
... metadata={"version": "1.0", "author": "developer"}
|
|
844
|
+
... )
|
|
845
|
+
"""
|
|
846
|
+
filepath = Path(filepath)
|
|
847
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
848
|
+
|
|
849
|
+
output: Dict[str, Any] = {
|
|
850
|
+
"pipeline": self._stages,
|
|
851
|
+
}
|
|
852
|
+
if metadata:
|
|
853
|
+
output["metadata"] = metadata
|
|
854
|
+
|
|
855
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
856
|
+
json.dump(output, f, indent=indent, ensure_ascii=ensure_ascii)
|
|
857
|
+
|
|
756
858
|
def build(self) -> List[Dict[str, Any]]:
|
|
757
859
|
"""
|
|
758
860
|
Return the completed pipeline.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongo-pipebuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Type-safe, fluent MongoDB aggregation pipeline builder
|
|
5
5
|
Author-email: seligoroff <seligoroff@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -297,6 +297,54 @@ builder.add_stage({"$out": "output"}).match({"status": "active"})
|
|
|
297
297
|
builder.validate() # Raises ValueError: $out stage must be the last stage
|
|
298
298
|
```
|
|
299
299
|
|
|
300
|
+
##### `get_stage_at(index: int) -> Dict[str, Any]`
|
|
301
|
+
|
|
302
|
+
Gets a specific stage from the pipeline by index. Returns a copy of the stage.
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
builder = PipelineBuilder()
|
|
306
|
+
builder.match({"status": "active"}).limit(10)
|
|
307
|
+
stage = builder.get_stage_at(0) # Returns {"$match": {"status": "active"}}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
##### `pretty_print(indent: int = 2, ensure_ascii: bool = False) -> str`
|
|
311
|
+
|
|
312
|
+
Returns a formatted JSON string representation of the pipeline. Useful for debugging.
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
builder = PipelineBuilder()
|
|
316
|
+
builder.match({"status": "active"}).limit(10)
|
|
317
|
+
print(builder.pretty_print())
|
|
318
|
+
# [
|
|
319
|
+
# {
|
|
320
|
+
# "$match": {
|
|
321
|
+
# "status": "active"
|
|
322
|
+
# }
|
|
323
|
+
# },
|
|
324
|
+
# {
|
|
325
|
+
# "$limit": 10
|
|
326
|
+
# }
|
|
327
|
+
# ]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
##### `to_json_file(filepath: Union[str, Path], indent: int = 2, ensure_ascii: bool = False, metadata: Optional[Dict[str, Any]] = None) -> None`
|
|
331
|
+
|
|
332
|
+
Saves the pipeline to a JSON file. Useful for debugging, comparison, or versioning.
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
builder = PipelineBuilder()
|
|
336
|
+
builder.match({"status": "active"}).limit(10)
|
|
337
|
+
|
|
338
|
+
# Basic usage
|
|
339
|
+
builder.to_json_file("debug_pipeline.json")
|
|
340
|
+
|
|
341
|
+
# With metadata
|
|
342
|
+
builder.to_json_file(
|
|
343
|
+
"pipeline.json",
|
|
344
|
+
metadata={"version": "1.0", "author": "developer"}
|
|
345
|
+
)
|
|
346
|
+
```
|
|
347
|
+
|
|
300
348
|
##### `build() -> List[Dict[str, Any]]`
|
|
301
349
|
|
|
302
350
|
Returns the complete pipeline as a list of stage dictionaries.
|
|
@@ -544,3 +592,14 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
544
592
|
|
|
545
593
|
|
|
546
594
|
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
@@ -5,6 +5,10 @@ Tests for methods from Proposal 2 (debug/validation) and Proposal 4 (pipeline an
|
|
|
5
5
|
|
|
6
6
|
Author: seligoroff
|
|
7
7
|
"""
|
|
8
|
+
import json
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
8
12
|
import pytest
|
|
9
13
|
from mongo_pipebuilder import PipelineBuilder
|
|
10
14
|
|
|
@@ -289,3 +293,245 @@ class TestPipelineBuilderAnalysis:
|
|
|
289
293
|
for stage_type in stage_types:
|
|
290
294
|
assert builder.has_stage(stage_type) is True
|
|
291
295
|
|
|
296
|
+
|
|
297
|
+
class TestPipelineBuilderDebugMethods:
|
|
298
|
+
"""Tests for Phase 1 debug methods: pretty_print, to_json_file, get_stage_at."""
|
|
299
|
+
|
|
300
|
+
def test_get_stage_at_valid_index(self):
|
|
301
|
+
"""Test get_stage_at() with valid index."""
|
|
302
|
+
builder = PipelineBuilder()
|
|
303
|
+
builder.match({"status": "active"}).limit(10).sort({"name": 1})
|
|
304
|
+
|
|
305
|
+
stage0 = builder.get_stage_at(0)
|
|
306
|
+
assert stage0 == {"$match": {"status": "active"}}
|
|
307
|
+
|
|
308
|
+
stage1 = builder.get_stage_at(1)
|
|
309
|
+
assert stage1 == {"$limit": 10}
|
|
310
|
+
|
|
311
|
+
stage2 = builder.get_stage_at(2)
|
|
312
|
+
assert stage2 == {"$sort": {"name": 1}}
|
|
313
|
+
|
|
314
|
+
def test_get_stage_at_returns_copy(self):
|
|
315
|
+
"""Test that get_stage_at() returns a copy, not reference."""
|
|
316
|
+
builder = PipelineBuilder()
|
|
317
|
+
builder.match({"status": "active"})
|
|
318
|
+
|
|
319
|
+
stage = builder.get_stage_at(0)
|
|
320
|
+
stage["$match"]["new_field"] = "value"
|
|
321
|
+
|
|
322
|
+
# Original should be unchanged
|
|
323
|
+
original_stage = builder.get_stage_at(0)
|
|
324
|
+
assert "new_field" not in original_stage["$match"]
|
|
325
|
+
|
|
326
|
+
def test_get_stage_at_invalid_index_negative(self):
|
|
327
|
+
"""Test get_stage_at() raises IndexError for negative index."""
|
|
328
|
+
builder = PipelineBuilder()
|
|
329
|
+
builder.match({"status": "active"})
|
|
330
|
+
|
|
331
|
+
with pytest.raises(IndexError, match="Index -1 out of range"):
|
|
332
|
+
builder.get_stage_at(-1)
|
|
333
|
+
|
|
334
|
+
def test_get_stage_at_invalid_index_too_large(self):
|
|
335
|
+
"""Test get_stage_at() raises IndexError for index too large."""
|
|
336
|
+
builder = PipelineBuilder()
|
|
337
|
+
builder.match({"status": "active"})
|
|
338
|
+
|
|
339
|
+
with pytest.raises(IndexError, match="Index 10 out of range"):
|
|
340
|
+
builder.get_stage_at(10)
|
|
341
|
+
|
|
342
|
+
def test_get_stage_at_empty_builder(self):
|
|
343
|
+
"""Test get_stage_at() raises IndexError on empty builder."""
|
|
344
|
+
builder = PipelineBuilder()
|
|
345
|
+
|
|
346
|
+
with pytest.raises(IndexError, match="Index 0 out of range"):
|
|
347
|
+
builder.get_stage_at(0)
|
|
348
|
+
|
|
349
|
+
def test_pretty_print_empty_builder(self):
|
|
350
|
+
"""Test pretty_print() with empty builder."""
|
|
351
|
+
builder = PipelineBuilder()
|
|
352
|
+
result = builder.pretty_print()
|
|
353
|
+
|
|
354
|
+
assert result == "[]"
|
|
355
|
+
# Should be valid JSON
|
|
356
|
+
json.loads(result)
|
|
357
|
+
|
|
358
|
+
def test_pretty_print_single_stage(self):
|
|
359
|
+
"""Test pretty_print() with single stage."""
|
|
360
|
+
builder = PipelineBuilder()
|
|
361
|
+
builder.match({"status": "active"})
|
|
362
|
+
result = builder.pretty_print()
|
|
363
|
+
|
|
364
|
+
# Should be valid JSON
|
|
365
|
+
parsed = json.loads(result)
|
|
366
|
+
assert parsed == [{"$match": {"status": "active"}}]
|
|
367
|
+
|
|
368
|
+
# Should contain expected content
|
|
369
|
+
assert "$match" in result
|
|
370
|
+
assert "status" in result
|
|
371
|
+
assert "active" in result
|
|
372
|
+
|
|
373
|
+
def test_pretty_print_multiple_stages(self):
|
|
374
|
+
"""Test pretty_print() with multiple stages."""
|
|
375
|
+
builder = PipelineBuilder()
|
|
376
|
+
builder.match({"status": "active"}).limit(10).sort({"name": 1})
|
|
377
|
+
result = builder.pretty_print()
|
|
378
|
+
|
|
379
|
+
# Should be valid JSON
|
|
380
|
+
parsed = json.loads(result)
|
|
381
|
+
assert len(parsed) == 3
|
|
382
|
+
assert parsed[0] == {"$match": {"status": "active"}}
|
|
383
|
+
assert parsed[1] == {"$limit": 10}
|
|
384
|
+
assert parsed[2] == {"$sort": {"name": 1}}
|
|
385
|
+
|
|
386
|
+
def test_pretty_print_custom_indent(self):
|
|
387
|
+
"""Test pretty_print() with custom indent."""
|
|
388
|
+
builder = PipelineBuilder()
|
|
389
|
+
builder.match({"status": "active"})
|
|
390
|
+
result = builder.pretty_print(indent=4)
|
|
391
|
+
|
|
392
|
+
# Should be valid JSON
|
|
393
|
+
parsed = json.loads(result)
|
|
394
|
+
assert parsed == [{"$match": {"status": "active"}}]
|
|
395
|
+
|
|
396
|
+
# Should use 4 spaces for indentation
|
|
397
|
+
lines = result.split("\n")
|
|
398
|
+
if len(lines) > 1:
|
|
399
|
+
assert lines[1].startswith(" ") # 4 spaces
|
|
400
|
+
|
|
401
|
+
def test_pretty_print_ensure_ascii(self):
|
|
402
|
+
"""Test pretty_print() with ensure_ascii=True."""
|
|
403
|
+
builder = PipelineBuilder()
|
|
404
|
+
builder.match({"name": "тест"}) # Non-ASCII characters
|
|
405
|
+
result_ascii = builder.pretty_print(ensure_ascii=True)
|
|
406
|
+
result_no_ascii = builder.pretty_print(ensure_ascii=False)
|
|
407
|
+
|
|
408
|
+
# Both should be valid JSON
|
|
409
|
+
json.loads(result_ascii)
|
|
410
|
+
json.loads(result_no_ascii)
|
|
411
|
+
|
|
412
|
+
# Non-ASCII version should contain original characters
|
|
413
|
+
assert "тест" in result_no_ascii
|
|
414
|
+
|
|
415
|
+
def test_to_json_file_basic(self):
|
|
416
|
+
"""Test to_json_file() saves pipeline correctly."""
|
|
417
|
+
builder = PipelineBuilder()
|
|
418
|
+
builder.match({"status": "active"}).limit(10)
|
|
419
|
+
|
|
420
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
421
|
+
filepath = Path(tmpdir) / "test_pipeline.json"
|
|
422
|
+
builder.to_json_file(filepath)
|
|
423
|
+
|
|
424
|
+
# File should exist
|
|
425
|
+
assert filepath.exists()
|
|
426
|
+
|
|
427
|
+
# Should contain valid JSON
|
|
428
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
429
|
+
data = json.load(f)
|
|
430
|
+
|
|
431
|
+
assert "pipeline" in data
|
|
432
|
+
assert data["pipeline"] == [
|
|
433
|
+
{"$match": {"status": "active"}},
|
|
434
|
+
{"$limit": 10}
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
def test_to_json_file_with_metadata(self):
|
|
438
|
+
"""Test to_json_file() with metadata."""
|
|
439
|
+
builder = PipelineBuilder()
|
|
440
|
+
builder.match({"status": "active"})
|
|
441
|
+
|
|
442
|
+
metadata = {
|
|
443
|
+
"version": "1.0",
|
|
444
|
+
"author": "developer",
|
|
445
|
+
"description": "Test pipeline"
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
449
|
+
filepath = Path(tmpdir) / "test_pipeline.json"
|
|
450
|
+
builder.to_json_file(filepath, metadata=metadata)
|
|
451
|
+
|
|
452
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
453
|
+
data = json.load(f)
|
|
454
|
+
|
|
455
|
+
assert "pipeline" in data
|
|
456
|
+
assert "metadata" in data
|
|
457
|
+
assert data["metadata"] == metadata
|
|
458
|
+
|
|
459
|
+
def test_to_json_file_creates_directory(self):
|
|
460
|
+
"""Test to_json_file() creates parent directories if they don't exist."""
|
|
461
|
+
builder = PipelineBuilder()
|
|
462
|
+
builder.match({"status": "active"})
|
|
463
|
+
|
|
464
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
465
|
+
filepath = Path(tmpdir) / "nested" / "path" / "test_pipeline.json"
|
|
466
|
+
|
|
467
|
+
# Directory shouldn't exist yet
|
|
468
|
+
assert not filepath.parent.exists()
|
|
469
|
+
|
|
470
|
+
builder.to_json_file(filepath)
|
|
471
|
+
|
|
472
|
+
# File and directory should be created
|
|
473
|
+
assert filepath.exists()
|
|
474
|
+
assert filepath.parent.exists()
|
|
475
|
+
|
|
476
|
+
def test_to_json_file_string_path(self):
|
|
477
|
+
"""Test to_json_file() accepts string path."""
|
|
478
|
+
builder = PipelineBuilder()
|
|
479
|
+
builder.match({"status": "active"})
|
|
480
|
+
|
|
481
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
482
|
+
filepath = str(Path(tmpdir) / "test_pipeline.json")
|
|
483
|
+
builder.to_json_file(filepath)
|
|
484
|
+
|
|
485
|
+
assert Path(filepath).exists()
|
|
486
|
+
|
|
487
|
+
def test_to_json_file_custom_indent(self):
|
|
488
|
+
"""Test to_json_file() with custom indent."""
|
|
489
|
+
builder = PipelineBuilder()
|
|
490
|
+
builder.match({"status": "active"})
|
|
491
|
+
|
|
492
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
493
|
+
filepath = Path(tmpdir) / "test_pipeline.json"
|
|
494
|
+
builder.to_json_file(filepath, indent=4)
|
|
495
|
+
|
|
496
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
497
|
+
content = f.read()
|
|
498
|
+
lines = content.split("\n")
|
|
499
|
+
if len(lines) > 1:
|
|
500
|
+
assert lines[1].startswith(" ") # 4 spaces
|
|
501
|
+
|
|
502
|
+
def test_pretty_print_and_to_json_file_consistency(self):
|
|
503
|
+
"""Test that pretty_print() and to_json_file() produce consistent output."""
|
|
504
|
+
builder = PipelineBuilder()
|
|
505
|
+
builder.match({"status": "active"}).limit(10)
|
|
506
|
+
|
|
507
|
+
pretty_output = builder.pretty_print()
|
|
508
|
+
|
|
509
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
510
|
+
filepath = Path(tmpdir) / "test_pipeline.json"
|
|
511
|
+
builder.to_json_file(filepath)
|
|
512
|
+
|
|
513
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
514
|
+
file_data = json.load(f)
|
|
515
|
+
|
|
516
|
+
# Pipeline in file should match pretty_print output when parsed
|
|
517
|
+
pretty_parsed = json.loads(pretty_output)
|
|
518
|
+
assert file_data["pipeline"] == pretty_parsed
|
|
519
|
+
|
|
520
|
+
def test_get_stage_at_with_complex_stage(self):
|
|
521
|
+
"""Test get_stage_at() with complex stage (e.g., lookup)."""
|
|
522
|
+
builder = PipelineBuilder()
|
|
523
|
+
builder.match({"status": "active"})
|
|
524
|
+
builder.lookup(
|
|
525
|
+
from_collection="users",
|
|
526
|
+
local_field="userId",
|
|
527
|
+
foreign_field="_id",
|
|
528
|
+
as_field="user"
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
stage = builder.get_stage_at(1)
|
|
532
|
+
assert "$lookup" in stage
|
|
533
|
+
assert stage["$lookup"]["from"] == "users"
|
|
534
|
+
assert stage["$lookup"]["localField"] == "userId"
|
|
535
|
+
assert stage["$lookup"]["foreignField"] == "_id"
|
|
536
|
+
assert stage["$lookup"]["as"] == "user"
|
|
537
|
+
|
|
File without changes
|
{mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/requires.txt
RENAMED
|
File without changes
|
{mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|