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.
Files changed (18) hide show
  1. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/LICENSE +11 -0
  2. {mongo_pipebuilder-0.2.3/src/mongo_pipebuilder.egg-info → mongo_pipebuilder-0.3.0}/PKG-INFO +60 -1
  3. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/README.md +59 -0
  4. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/pyproject.toml +1 -1
  5. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder/__init__.py +1 -1
  6. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder/builder.py +102 -0
  7. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0/src/mongo_pipebuilder.egg-info}/PKG-INFO +60 -1
  8. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_debug.py +246 -0
  9. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_insert.py +1 -0
  10. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_validation.py +11 -0
  11. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_validation_existing.py +1 -0
  12. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/tests/test_builder_validation_new.py +1 -0
  13. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/setup.cfg +0 -0
  14. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/SOURCES.txt +0 -0
  15. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/dependency_links.txt +0 -0
  16. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/requires.txt +0 -0
  17. {mongo_pipebuilder-0.2.3 → mongo_pipebuilder-0.3.0}/src/mongo_pipebuilder.egg-info/top_level.txt +0 -0
  18. {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.2.3
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
+
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mongo-pipebuilder"
7
- version = "0.2.3"
7
+ version = "0.3.0"
8
8
  description = "Type-safe, fluent MongoDB aggregation pipeline builder"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -9,6 +9,6 @@ Author: seligoroff
9
9
 
10
10
  from mongo_pipebuilder.builder import PipelineBuilder
11
11
 
12
- __version__ = "0.2.3"
12
+ __version__ = "0.3.0"
13
13
  __all__ = ["PipelineBuilder"]
14
14
 
@@ -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.2.3
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
+
@@ -257,3 +257,4 @@ class TestPrependAndInsertAtIntegration:
257
257
  assert pipeline[3] == {"$limit": 10}
258
258
 
259
259
 
260
+