etlplus 0.8.4__tar.gz → 0.8.6__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 (150) hide show
  1. {etlplus-0.8.4/etlplus.egg-info → etlplus-0.8.6}/PKG-INFO +1 -1
  2. {etlplus-0.8.4 → etlplus-0.8.6/etlplus.egg-info}/PKG-INFO +1 -1
  3. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/cli/test_u_cli_handlers.py +313 -11
  4. {etlplus-0.8.4 → etlplus-0.8.6}/.coveragerc +0 -0
  5. {etlplus-0.8.4 → etlplus-0.8.6}/.editorconfig +0 -0
  6. {etlplus-0.8.4 → etlplus-0.8.6}/.gitattributes +0 -0
  7. {etlplus-0.8.4 → etlplus-0.8.6}/.github/actions/python-bootstrap/action.yml +0 -0
  8. {etlplus-0.8.4 → etlplus-0.8.6}/.github/workflows/ci.yml +0 -0
  9. {etlplus-0.8.4 → etlplus-0.8.6}/.gitignore +0 -0
  10. {etlplus-0.8.4 → etlplus-0.8.6}/.pre-commit-config.yaml +0 -0
  11. {etlplus-0.8.4 → etlplus-0.8.6}/.ruff.toml +0 -0
  12. {etlplus-0.8.4 → etlplus-0.8.6}/CODE_OF_CONDUCT.md +0 -0
  13. {etlplus-0.8.4 → etlplus-0.8.6}/CONTRIBUTING.md +0 -0
  14. {etlplus-0.8.4 → etlplus-0.8.6}/DEMO.md +0 -0
  15. {etlplus-0.8.4 → etlplus-0.8.6}/LICENSE +0 -0
  16. {etlplus-0.8.4 → etlplus-0.8.6}/MANIFEST.in +0 -0
  17. {etlplus-0.8.4 → etlplus-0.8.6}/Makefile +0 -0
  18. {etlplus-0.8.4 → etlplus-0.8.6}/README.md +0 -0
  19. {etlplus-0.8.4 → etlplus-0.8.6}/REFERENCES.md +0 -0
  20. {etlplus-0.8.4 → etlplus-0.8.6}/docs/README.md +0 -0
  21. {etlplus-0.8.4 → etlplus-0.8.6}/docs/pipeline-guide.md +0 -0
  22. {etlplus-0.8.4 → etlplus-0.8.6}/docs/snippets/installation_version.md +0 -0
  23. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/__init__.py +0 -0
  24. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/__main__.py +0 -0
  25. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/__version__.py +0 -0
  26. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/README.md +0 -0
  27. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/__init__.py +0 -0
  28. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/auth.py +0 -0
  29. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/config.py +0 -0
  30. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/endpoint_client.py +0 -0
  31. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/errors.py +0 -0
  32. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/pagination/__init__.py +0 -0
  33. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/pagination/client.py +0 -0
  34. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/pagination/config.py +0 -0
  35. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/pagination/paginator.py +0 -0
  36. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/rate_limiting/__init__.py +0 -0
  37. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/rate_limiting/config.py +0 -0
  38. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  39. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/request_manager.py +0 -0
  40. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/retry_manager.py +0 -0
  41. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/transport.py +0 -0
  42. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/api/types.py +0 -0
  43. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/__init__.py +0 -0
  44. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/commands.py +0 -0
  45. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/constants.py +0 -0
  46. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/handlers.py +0 -0
  47. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/io.py +0 -0
  48. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/main.py +0 -0
  49. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/options.py +0 -0
  50. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/state.py +0 -0
  51. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/cli/types.py +0 -0
  52. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/config/__init__.py +0 -0
  53. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/config/connector.py +0 -0
  54. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/config/jobs.py +0 -0
  55. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/config/pipeline.py +0 -0
  56. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/config/profile.py +0 -0
  57. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/config/types.py +0 -0
  58. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/config/utils.py +0 -0
  59. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/database/__init__.py +0 -0
  60. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/database/ddl.py +0 -0
  61. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/database/engine.py +0 -0
  62. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/database/orm.py +0 -0
  63. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/database/schema.py +0 -0
  64. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/database/types.py +0 -0
  65. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/enums.py +0 -0
  66. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/extract.py +0 -0
  67. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/file.py +0 -0
  68. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/load.py +0 -0
  69. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/mixins.py +0 -0
  70. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/py.typed +0 -0
  71. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/run.py +0 -0
  72. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/run_helpers.py +0 -0
  73. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/templates/__init__.py +0 -0
  74. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/templates/ddl.sql.j2 +0 -0
  75. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/templates/view.sql.j2 +0 -0
  76. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/transform.py +0 -0
  77. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/types.py +0 -0
  78. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/utils.py +0 -0
  79. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/validate.py +0 -0
  80. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/validation/__init__.py +0 -0
  81. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus/validation/utils.py +0 -0
  82. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus.egg-info/SOURCES.txt +0 -0
  83. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus.egg-info/dependency_links.txt +0 -0
  84. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus.egg-info/entry_points.txt +0 -0
  85. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus.egg-info/requires.txt +0 -0
  86. {etlplus-0.8.4 → etlplus-0.8.6}/etlplus.egg-info/top_level.txt +0 -0
  87. {etlplus-0.8.4 → etlplus-0.8.6}/examples/README.md +0 -0
  88. {etlplus-0.8.4 → etlplus-0.8.6}/examples/configs/ddl_spec.yml +0 -0
  89. {etlplus-0.8.4 → etlplus-0.8.6}/examples/configs/pipeline.yml +0 -0
  90. {etlplus-0.8.4 → etlplus-0.8.6}/examples/data/sample.csv +0 -0
  91. {etlplus-0.8.4 → etlplus-0.8.6}/examples/data/sample.json +0 -0
  92. {etlplus-0.8.4 → etlplus-0.8.6}/examples/data/sample.xml +0 -0
  93. {etlplus-0.8.4 → etlplus-0.8.6}/examples/data/sample.xsd +0 -0
  94. {etlplus-0.8.4 → etlplus-0.8.6}/examples/data/sample.yaml +0 -0
  95. {etlplus-0.8.4 → etlplus-0.8.6}/examples/quickstart_python.py +0 -0
  96. {etlplus-0.8.4 → etlplus-0.8.6}/pyproject.toml +0 -0
  97. {etlplus-0.8.4 → etlplus-0.8.6}/pytest.ini +0 -0
  98. {etlplus-0.8.4 → etlplus-0.8.6}/setup.cfg +0 -0
  99. {etlplus-0.8.4 → etlplus-0.8.6}/setup.py +0 -0
  100. {etlplus-0.8.4 → etlplus-0.8.6}/tests/__init__.py +0 -0
  101. {etlplus-0.8.4 → etlplus-0.8.6}/tests/conftest.py +0 -0
  102. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/conftest.py +0 -0
  103. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_cli.py +0 -0
  104. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_examples_data_parity.py +0 -0
  105. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_pagination_strategy.py +0 -0
  106. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_pipeline_smoke.py +0 -0
  107. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  108. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_run.py +0 -0
  109. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  110. {etlplus-0.8.4 → etlplus-0.8.6}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  111. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/conftest.py +0 -0
  112. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_auth.py +0 -0
  113. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_config.py +0 -0
  114. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_endpoint_client.py +0 -0
  115. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_mocks.py +0 -0
  116. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_pagination_client.py +0 -0
  117. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_pagination_config.py +0 -0
  118. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_paginator.py +0 -0
  119. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  120. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_rate_limiter.py +0 -0
  121. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_request_manager.py +0 -0
  122. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_retry_manager.py +0 -0
  123. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_transport.py +0 -0
  124. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/api/test_u_types.py +0 -0
  125. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/cli/conftest.py +0 -0
  126. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/cli/test_u_cli_main.py +0 -0
  127. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/cli/test_u_cli_state.py +0 -0
  128. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/config/test_u_config_utils.py +0 -0
  129. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/config/test_u_connector.py +0 -0
  130. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/config/test_u_jobs.py +0 -0
  131. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/config/test_u_pipeline.py +0 -0
  132. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/conftest.py +0 -0
  133. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/database/test_u_database_ddl.py +0 -0
  134. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/database/test_u_database_engine.py +0 -0
  135. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/database/test_u_database_orm.py +0 -0
  136. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/database/test_u_database_schema.py +0 -0
  137. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_enums.py +0 -0
  138. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_extract.py +0 -0
  139. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_file.py +0 -0
  140. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_load.py +0 -0
  141. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_main.py +0 -0
  142. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_mixins.py +0 -0
  143. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_run.py +0 -0
  144. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_run_helpers.py +0 -0
  145. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_transform.py +0 -0
  146. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_utils.py +0 -0
  147. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_validate.py +0 -0
  148. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/test_u_version.py +0 -0
  149. {etlplus-0.8.4 → etlplus-0.8.6}/tests/unit/validation/test_u_validation_utils.py +0 -0
  150. {etlplus-0.8.4 → etlplus-0.8.6}/tools/update_demo_snippets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -374,7 +374,7 @@ class TestCheckHandler:
374
374
  dummy_cfg: PipelineConfig,
375
375
  capture_io: dict[str, list],
376
376
  ) -> None:
377
- """Test that ``check`` prints requested sections."""
377
+ """Test that :func:`check_handler` prints requested sections."""
378
378
  monkeypatch.setattr(
379
379
  handlers,
380
380
  'load_pipeline_config',
@@ -399,7 +399,9 @@ class TestExtractHandler:
399
399
  monkeypatch: pytest.MonkeyPatch,
400
400
  capture_io: dict[str, list],
401
401
  ) -> None:
402
- """Test that non-stdin sources invoke extract and emit results."""
402
+ """
403
+ Test that :func:`extract_handler` uses extract for non-stdin sources.
404
+ """
403
405
  observed: dict[str, object] = {}
404
406
 
405
407
  def fake_extract(
@@ -439,7 +441,7 @@ class TestExtractHandler:
439
441
  capture_io: dict[str, list],
440
442
  ) -> None:
441
443
  """
442
- Test that explicit format hints are forwarded for file extractions.
444
+ Test that :func:`extract_handler` forwards explicit file format hints.
443
445
  """
444
446
  captured: dict[str, object] = {}
445
447
 
@@ -472,7 +474,9 @@ class TestExtractHandler:
472
474
  monkeypatch: pytest.MonkeyPatch,
473
475
  capture_io: dict[str, list],
474
476
  ) -> None:
475
- """Test that stdin source bypasses extract and emits parsed data."""
477
+ """
478
+ Test that :func:`extract_handler` reads stdin and emits parsed data.
479
+ """
476
480
  monkeypatch.setattr(
477
481
  handlers.cli_io,
478
482
  'read_stdin_text',
@@ -504,13 +508,14 @@ class TestExtractHandler:
504
508
  ]
505
509
  assert capture_io['emit_or_write'] == []
506
510
 
507
- def test_suppresses_emit_when_output_written(
511
+ def test_writes_output_file_and_skips_emit(
508
512
  self,
509
513
  monkeypatch: pytest.MonkeyPatch,
510
514
  capture_io: dict[str, list],
511
515
  ) -> None:
512
516
  """
513
- Test that Extract skips stdout emission when output file is provided.
517
+ Test that :func:`extract_handler` writes to a file and skips stdout
518
+ emission.
514
519
  """
515
520
  observed: dict[str, object] = {}
516
521
 
@@ -554,7 +559,7 @@ class TestLoadHandler:
554
559
  monkeypatch: pytest.MonkeyPatch,
555
560
  capture_io: dict[str, list],
556
561
  ) -> None:
557
- """Test that file target streams payload."""
562
+ """Test that :func:`load_handler` streams payload for file targets."""
558
563
  recorded: dict[str, object] = {}
559
564
 
560
565
  def fake_materialize(
@@ -601,7 +606,7 @@ class TestLoadHandler:
601
606
  capture_io: dict[str, list],
602
607
  ) -> None:
603
608
  """
604
- Test that stdin payloads are parsed and routed through :func:`load`.
609
+ Test that :func:`load_handler` parses stdin and routes through load.
605
610
  """
606
611
  read_calls = {'count': 0}
607
612
 
@@ -685,7 +690,8 @@ class TestLoadHandler:
685
690
  capture_io: dict[str, list],
686
691
  ) -> None:
687
692
  """
688
- Test that writing to a file skips stdout emission after :func:`load`.
693
+ Test that :func:`load_handler` writes to a file and skips stdout
694
+ emission.
689
695
  """
690
696
  load_record: dict[str, object] = {}
691
697
 
@@ -740,7 +746,7 @@ class TestRenderHandler:
740
746
  self,
741
747
  capsys: pytest.CaptureFixture[str],
742
748
  ) -> None:
743
- """Test that missing configs/specs surfaces a helpful error."""
749
+ """Test that :func:`render_handler` reports missing specs."""
744
750
 
745
751
  assert (
746
752
  handlers.render_handler(
@@ -762,7 +768,7 @@ class TestRenderHandler:
762
768
  widget_spec_paths: tuple[Path, Path],
763
769
  capsys: pytest.CaptureFixture[str],
764
770
  ) -> None:
765
- """Test rendering a standalone spec to a file."""
771
+ """Test that :func:`render_handler` writes SQL for standalone specs."""
766
772
  spec_path, output_path = widget_spec_paths
767
773
  assert (
768
774
  handlers.render_handler(
@@ -788,14 +794,310 @@ class TestRenderHandler:
788
794
  class TestRunHandler:
789
795
  """Unit test suite for :func:`run_handler`."""
790
796
 
797
+ def test_emits_pipeline_summary_without_job(
798
+ self,
799
+ monkeypatch: pytest.MonkeyPatch,
800
+ dummy_cfg: PipelineConfig,
801
+ capture_io: dict[str, list],
802
+ ) -> None:
803
+ """Test that :func:`run_handler` emits a summary when no job set."""
804
+ monkeypatch.setattr(
805
+ handlers,
806
+ 'load_pipeline_config',
807
+ lambda path, substitute: dummy_cfg,
808
+ )
809
+
810
+ assert (
811
+ handlers.run_handler(
812
+ config='pipeline.yml',
813
+ job=None,
814
+ pipeline=None,
815
+ pretty=True,
816
+ )
817
+ == 0
818
+ )
819
+
820
+ assert capture_io['emit_json'] == [
821
+ (
822
+ (
823
+ {
824
+ 'name': dummy_cfg.name,
825
+ 'version': dummy_cfg.version,
826
+ 'sources': ['s1'],
827
+ 'targets': ['t1'],
828
+ 'jobs': ['j1'],
829
+ },
830
+ ),
831
+ {'pretty': True},
832
+ ),
833
+ ]
834
+
835
+ def test_runs_job_and_emits_result(
836
+ self,
837
+ monkeypatch: pytest.MonkeyPatch,
838
+ dummy_cfg: PipelineConfig,
839
+ capture_io: dict[str, list],
840
+ ) -> None:
841
+ """
842
+ Test that :func:`run_handler` executes a named job and emits status.
843
+ """
844
+ monkeypatch.setattr(
845
+ handlers,
846
+ 'load_pipeline_config',
847
+ lambda path, substitute: dummy_cfg,
848
+ )
849
+ run_calls: dict[str, object] = {}
850
+
851
+ def fake_run(*, job: str, config_path: str) -> dict[str, object]:
852
+ run_calls['params'] = (job, config_path)
853
+ return {'job': job, 'ok': True}
854
+
855
+ monkeypatch.setattr(handlers, 'run', fake_run)
856
+
857
+ assert (
858
+ handlers.run_handler(
859
+ config='pipeline.yml',
860
+ job='job1',
861
+ pretty=False,
862
+ )
863
+ == 0
864
+ )
865
+ assert run_calls['params'] == ('job1', 'pipeline.yml')
866
+ assert capture_io['emit_json'] == [
867
+ (
868
+ ({'status': 'ok', 'result': {'job': 'job1', 'ok': True}},),
869
+ {'pretty': False},
870
+ ),
871
+ ]
872
+
791
873
 
792
874
  class TestTransformHandler:
793
875
  """Unit test suite for :func:`transform_handler`."""
794
876
 
877
+ def test_emits_result_without_target(
878
+ self,
879
+ monkeypatch: pytest.MonkeyPatch,
880
+ capture_io: dict[str, list],
881
+ ) -> None:
882
+ """Test that :func:`transform_handler` emits results with no target."""
883
+ resolve_calls: list[tuple[object, str | None, bool]] = []
884
+
885
+ def fake_resolve(
886
+ source: object,
887
+ *,
888
+ format_hint: str | None,
889
+ format_explicit: bool,
890
+ ) -> object:
891
+ resolve_calls.append((source, format_hint, format_explicit))
892
+ if source == 'data.json':
893
+ return [{'id': 1}]
894
+ return {'select': ['id']}
895
+
896
+ monkeypatch.setattr(
897
+ handlers.cli_io,
898
+ 'resolve_cli_payload',
899
+ fake_resolve,
900
+ )
901
+ monkeypatch.setattr(
902
+ handlers,
903
+ 'transform',
904
+ lambda payload, ops: {'rows': payload, 'ops': ops},
905
+ )
906
+
907
+ assert (
908
+ handlers.transform_handler(
909
+ source='data.json',
910
+ operations='ops.json',
911
+ source_format='json',
912
+ target=None,
913
+ pretty=False,
914
+ )
915
+ == 0
916
+ )
917
+ assert resolve_calls == [
918
+ ('data.json', 'json', True),
919
+ ('ops.json', None, True),
920
+ ]
921
+ assert capture_io['emit_json'] == [
922
+ (
923
+ ({'rows': [{'id': 1}], 'ops': {'select': ['id']}},),
924
+ {'pretty': False},
925
+ ),
926
+ ]
927
+
928
+ def test_writes_target_file(
929
+ self,
930
+ monkeypatch: pytest.MonkeyPatch,
931
+ capsys: pytest.CaptureFixture[str],
932
+ ) -> None:
933
+ """Test that :func:`transform_handler` writes data to a target file."""
934
+ monkeypatch.setattr(
935
+ handlers.cli_io,
936
+ 'resolve_cli_payload',
937
+ lambda source, **_kwargs: {'source': source}
938
+ if source == 'data.json'
939
+ else {'select': ['id']},
940
+ )
941
+ monkeypatch.setattr(
942
+ handlers,
943
+ 'transform',
944
+ lambda payload, ops: {'payload': payload, 'ops': ops},
945
+ )
946
+ write_calls: dict[str, object] = {}
947
+
948
+ def fake_write(
949
+ path: str,
950
+ data: object,
951
+ *,
952
+ file_format: str | None,
953
+ ) -> None:
954
+ write_calls['params'] = (path, data, file_format)
955
+
956
+ monkeypatch.setattr(handlers.File, 'write_file', fake_write)
957
+
958
+ assert (
959
+ handlers.transform_handler(
960
+ source='data.json',
961
+ operations='ops.json',
962
+ target='out.json',
963
+ target_format='json',
964
+ pretty=True,
965
+ )
966
+ == 0
967
+ )
968
+ assert write_calls['params'] == (
969
+ 'out.json',
970
+ {
971
+ 'payload': {'source': 'data.json'},
972
+ 'ops': {'select': ['id']},
973
+ },
974
+ 'json',
975
+ )
976
+ assert (
977
+ 'Data transformed and saved to out.json' in capsys.readouterr().out
978
+ )
979
+
795
980
 
796
981
  class TestValidateHandler:
797
982
  """Unit test suite for :func:`validate_handler`."""
798
983
 
984
+ def test_emits_result_without_target(
985
+ self,
986
+ monkeypatch: pytest.MonkeyPatch,
987
+ capture_io: dict[str, list],
988
+ ) -> None:
989
+ """Test that :func:`validate_handler` emits results with no target."""
990
+ monkeypatch.setattr(
991
+ handlers.cli_io,
992
+ 'resolve_cli_payload',
993
+ lambda source, **_kwargs: {'source': source}
994
+ if source == 'data.json'
995
+ else {'id': {'required': True}},
996
+ )
997
+ monkeypatch.setattr(
998
+ handlers,
999
+ 'validate',
1000
+ lambda payload, rules: {'data': payload, 'rules': rules},
1001
+ )
1002
+
1003
+ assert (
1004
+ handlers.validate_handler(
1005
+ source='data.json',
1006
+ rules='rules.json',
1007
+ pretty=False,
1008
+ )
1009
+ == 0
1010
+ )
1011
+ assert capture_io['emit_json'] == [
1012
+ (
1013
+ (
1014
+ {
1015
+ 'data': {'source': 'data.json'},
1016
+ 'rules': {'id': {'required': True}},
1017
+ },
1018
+ ),
1019
+ {'pretty': False},
1020
+ ),
1021
+ ]
1022
+
1023
+ def test_reports_missing_data_for_target(
1024
+ self,
1025
+ monkeypatch: pytest.MonkeyPatch,
1026
+ capsys: pytest.CaptureFixture[str],
1027
+ ) -> None:
1028
+ """Test that :func:`validate_handler` reports missing output data."""
1029
+ monkeypatch.setattr(
1030
+ handlers.cli_io,
1031
+ 'resolve_cli_payload',
1032
+ lambda source, **_kwargs: {'source': source}
1033
+ if source == 'data.json'
1034
+ else {'id': {'required': True}},
1035
+ )
1036
+ monkeypatch.setattr(
1037
+ handlers,
1038
+ 'validate',
1039
+ lambda *_args, **_kwargs: {'data': None},
1040
+ )
1041
+
1042
+ assert (
1043
+ handlers.validate_handler(
1044
+ source='data.json',
1045
+ rules='rules.json',
1046
+ target='out.json',
1047
+ pretty=True,
1048
+ )
1049
+ == 0
1050
+ )
1051
+ assert (
1052
+ 'Validation failed, no data to save for out.json'
1053
+ in capsys.readouterr().err
1054
+ )
1055
+
1056
+ def test_writes_target_file(
1057
+ self,
1058
+ monkeypatch: pytest.MonkeyPatch,
1059
+ ) -> None:
1060
+ """Test that: func: `validate_handler` writes data to a target file."""
1061
+ monkeypatch.setattr(
1062
+ handlers.cli_io,
1063
+ 'resolve_cli_payload',
1064
+ lambda source, **_kwargs: {'source': source}
1065
+ if source == 'data.json'
1066
+ else {'id': {'required': True}},
1067
+ )
1068
+ monkeypatch.setattr(
1069
+ handlers,
1070
+ 'validate',
1071
+ lambda *_args, **_kwargs: {'data': {'id': 1}},
1072
+ )
1073
+ write_calls: dict[str, object] = {}
1074
+
1075
+ def fake_write(
1076
+ data: object,
1077
+ path: str | None,
1078
+ *,
1079
+ success_message: str,
1080
+ ) -> bool:
1081
+ write_calls['params'] = (data, path, success_message)
1082
+ return True
1083
+
1084
+ monkeypatch.setattr(handlers.cli_io, 'write_json_output', fake_write)
1085
+
1086
+ assert (
1087
+ handlers.validate_handler(
1088
+ source='data.json',
1089
+ rules='rules.json',
1090
+ target='out.json',
1091
+ pretty=True,
1092
+ )
1093
+ == 0
1094
+ )
1095
+ assert write_calls['params'] == (
1096
+ {'id': 1},
1097
+ 'out.json',
1098
+ 'Validation result saved to',
1099
+ )
1100
+
799
1101
 
800
1102
  @pytest.mark.parametrize(
801
1103
  'kwargs,expected_keys',
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes