lsst-pipe-base 30.2026.200__tar.gz → 30.2026.300__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 (190) hide show
  1. {lsst_pipe_base-30.2026.200/python/lsst_pipe_base.egg-info → lsst_pipe_base-30.2026.300}/PKG-INFO +1 -1
  2. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/_instrument.py +6 -5
  3. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/log_capture.py +8 -4
  4. lsst_pipe_base-30.2026.300/python/lsst/pipe/base/log_on_close.py +79 -0
  5. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/mp_graph_executor.py +51 -15
  6. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/_common.py +4 -3
  7. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/_multiblock.py +6 -16
  8. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/_predicted.py +104 -10
  9. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/_provenance.py +657 -6
  10. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/aggregator/_communicators.py +18 -50
  11. lsst_pipe_base-30.2026.300/python/lsst/pipe/base/quantum_graph/aggregator/_scanner.py +317 -0
  12. lsst_pipe_base-30.2026.300/python/lsst/pipe/base/quantum_graph/aggregator/_structs.py +67 -0
  13. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +10 -5
  14. lsst_pipe_base-30.2026.300/python/lsst/pipe/base/quantum_graph/aggregator/_writer.py +184 -0
  15. lsst_pipe_base-30.2026.300/python/lsst/pipe/base/quantum_graph/formatter.py +101 -0
  16. lsst_pipe_base-30.2026.300/python/lsst/pipe/base/quantum_graph_executor.py +229 -0
  17. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/separable_pipeline_executor.py +19 -2
  18. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/single_quantum_executor.py +53 -35
  19. lsst_pipe_base-30.2026.300/python/lsst/pipe/base/version.py +2 -0
  20. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300/python/lsst_pipe_base.egg-info}/PKG-INFO +1 -1
  21. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst_pipe_base.egg-info/SOURCES.txt +2 -0
  22. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_separable_pipeline_executor.py +101 -13
  23. lsst_pipe_base-30.2026.200/python/lsst/pipe/base/quantum_graph/aggregator/_scanner.py +0 -511
  24. lsst_pipe_base-30.2026.200/python/lsst/pipe/base/quantum_graph/aggregator/_structs.py +0 -177
  25. lsst_pipe_base-30.2026.200/python/lsst/pipe/base/quantum_graph/aggregator/_writer.py +0 -501
  26. lsst_pipe_base-30.2026.200/python/lsst/pipe/base/quantum_graph_executor.py +0 -126
  27. lsst_pipe_base-30.2026.200/python/lsst/pipe/base/version.py +0 -2
  28. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/COPYRIGHT +0 -0
  29. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/LICENSE +0 -0
  30. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/MANIFEST.in +0 -0
  31. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/README.md +0 -0
  32. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/bsd_license.txt +0 -0
  33. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/CHANGES.rst +0 -0
  34. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/creating-a-pipeline.rst +0 -0
  35. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/creating-a-pipelinetask.rst +0 -0
  36. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/creating-a-task.rst +0 -0
  37. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/index.rst +0 -0
  38. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/task-framework-overview.rst +0 -0
  39. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/task-retargeting-howto.rst +0 -0
  40. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/testing-a-pipeline-task.rst +0 -0
  41. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/testing-pipelines-with-mocks.rst +0 -0
  42. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/doc/lsst.pipe.base/working-with-pipeline-graphs.rst +0 -0
  43. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/gpl-v3.0.txt +0 -0
  44. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/pyproject.toml +0 -0
  45. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/__init__.py +0 -0
  46. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/__init__.py +0 -0
  47. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/__init__.py +0 -0
  48. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/_datasetQueryConstraints.py +0 -0
  49. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/_dataset_handle.py +0 -0
  50. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/_observation_dimension_packer.py +0 -0
  51. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/_quantumContext.py +0 -0
  52. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/_status.py +0 -0
  53. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/_task_metadata.py +0 -0
  54. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/all_dimensions_quantum_graph_builder.py +0 -0
  55. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/automatic_connection_constants.py +0 -0
  56. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/caching_limited_butler.py +0 -0
  57. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/cli/__init__.py +0 -0
  58. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/cli/_get_cli_subcommands.py +0 -0
  59. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/cli/cmd/__init__.py +0 -0
  60. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/cli/cmd/commands.py +0 -0
  61. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/cli/opt/__init__.py +0 -0
  62. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/cli/opt/arguments.py +0 -0
  63. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/cli/opt/options.py +0 -0
  64. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/config.py +0 -0
  65. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/configOverrides.py +0 -0
  66. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/connectionTypes.py +0 -0
  67. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/connections.py +0 -0
  68. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/dot_tools.py +0 -0
  69. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/exec_fixup_data_id.py +0 -0
  70. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/execution_graph_fixup.py +0 -0
  71. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/execution_reports.py +0 -0
  72. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/formatters/__init__.py +0 -0
  73. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/formatters/pexConfig.py +0 -0
  74. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph/__init__.py +0 -0
  75. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph/_implDetails.py +0 -0
  76. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph/_loadHelpers.py +0 -0
  77. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph/_versionDeserializers.py +0 -0
  78. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph/graph.py +0 -0
  79. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph/graphSummary.py +0 -0
  80. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph/quantumNode.py +0 -0
  81. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/graph_walker.py +0 -0
  82. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/mermaid_tools.py +0 -0
  83. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline.py +0 -0
  84. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipelineIR.py +0 -0
  85. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipelineTask.py +0 -0
  86. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/__init__.py +0 -0
  87. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/__main__.py +0 -0
  88. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_dataset_types.py +0 -0
  89. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_edges.py +0 -0
  90. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_exceptions.py +0 -0
  91. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_mapping_views.py +0 -0
  92. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_nodes.py +0 -0
  93. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_pipeline_graph.py +0 -0
  94. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_task_subsets.py +0 -0
  95. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/_tasks.py +0 -0
  96. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/expressions.py +0 -0
  97. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/io.py +0 -0
  98. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/__init__.py +0 -0
  99. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_dot.py +0 -0
  100. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_formatting.py +0 -0
  101. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_layout.py +0 -0
  102. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_merge.py +0 -0
  103. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_mermaid.py +0 -0
  104. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_options.py +0 -0
  105. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_printer.py +0 -0
  106. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_show.py +0 -0
  107. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/pipeline_graph/visualization/_status_annotator.py +0 -0
  108. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/prerequisite_helpers.py +0 -0
  109. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/py.typed +0 -0
  110. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/__init__.py +0 -0
  111. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/aggregator/__init__.py +0 -0
  112. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/aggregator/_config.py +0 -0
  113. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/aggregator/_ingester.py +0 -0
  114. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/aggregator/_progress.py +0 -0
  115. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph/visualization.py +0 -0
  116. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph_builder.py +0 -0
  117. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_graph_skeleton.py +0 -0
  118. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_provenance_graph.py +0 -0
  119. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/quantum_reports.py +0 -0
  120. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/resource_usage.py +0 -0
  121. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/script/__init__.py +0 -0
  122. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/script/register_instrument.py +0 -0
  123. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/script/retrieve_artifacts_for_quanta.py +0 -0
  124. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/script/transfer_from_graph.py +0 -0
  125. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/script/utils.py +0 -0
  126. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/script/zip_from_graph.py +0 -0
  127. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/simple_pipeline_executor.py +0 -0
  128. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/struct.py +0 -0
  129. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/task.py +0 -0
  130. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/taskFactory.py +0 -0
  131. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/testUtils.py +0 -0
  132. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/__init__.py +0 -0
  133. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/in_memory_limited_butler.py +0 -0
  134. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/mocks/__init__.py +0 -0
  135. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/mocks/_data_id_match.py +0 -0
  136. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/mocks/_pipeline_task.py +0 -0
  137. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/mocks/_repo.py +0 -0
  138. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/mocks/_storage_class.py +0 -0
  139. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/no_dimensions.py +0 -0
  140. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/pipelineStepTester.py +0 -0
  141. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/simpleQGraph.py +0 -0
  142. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/tests/util.py +0 -0
  143. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst/pipe/base/utils.py +0 -0
  144. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst_pipe_base.egg-info/dependency_links.txt +0 -0
  145. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst_pipe_base.egg-info/entry_points.txt +0 -0
  146. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst_pipe_base.egg-info/requires.txt +0 -0
  147. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst_pipe_base.egg-info/top_level.txt +0 -0
  148. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/python/lsst_pipe_base.egg-info/zip-safe +0 -0
  149. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/setup.cfg +0 -0
  150. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_adjust_all_quanta.py +0 -0
  151. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_aggregator.py +0 -0
  152. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_caching_limited_butler.py +0 -0
  153. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_cliCmdRegisterInstrument.py +0 -0
  154. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_configOverrides.py +0 -0
  155. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_config_formatter.py +0 -0
  156. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_connections.py +0 -0
  157. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_dataid_match.py +0 -0
  158. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_dataset_handle.py +0 -0
  159. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_deferredDatasetRef.py +0 -0
  160. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_dot_tools.py +0 -0
  161. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_dynamic_connections.py +0 -0
  162. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_execution_reports.py +0 -0
  163. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_execution_storage_class_conversion.py +0 -0
  164. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_graphBuilder.py +0 -0
  165. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_graph_walker.py +0 -0
  166. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_init_output_run.py +0 -0
  167. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_instrument.py +0 -0
  168. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_mermaid.py +0 -0
  169. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_mp_graph_executor.py +0 -0
  170. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_pipeline.py +0 -0
  171. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_pipelineIR.py +0 -0
  172. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_pipelineLoadSubset.py +0 -0
  173. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_pipelineTask.py +0 -0
  174. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_pipeline_graph.py +0 -0
  175. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_pipeline_graph_expressions.py +0 -0
  176. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_predicted_qg.py +0 -0
  177. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_qg_builder_dimensions.py +0 -0
  178. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_quantumGraph.py +0 -0
  179. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_quantum_provenance_graph.py +0 -0
  180. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_quantum_reports.py +0 -0
  181. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_quantum_success_caveats.py +0 -0
  182. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_script_utils.py +0 -0
  183. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_simple_pipeline_executor.py +0 -0
  184. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_single_quantum_executor.py +0 -0
  185. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_struct.py +0 -0
  186. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_task.py +0 -0
  187. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_task_factory.py +0 -0
  188. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_taskmetadata.py +0 -0
  189. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_testUtils.py +0 -0
  190. {lsst_pipe_base-30.2026.200 → lsst_pipe_base-30.2026.300}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-pipe-base
3
- Version: 30.2026.200
3
+ Version: 30.2026.300
4
4
  Summary: Pipeline infrastructure for the Rubin Science Pipelines.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License-Expression: BSD-3-Clause OR GPL-3.0-or-later
@@ -31,7 +31,6 @@ __all__ = ("Instrument",)
31
31
 
32
32
  import contextlib
33
33
  import datetime
34
- import os.path
35
34
  from abc import ABCMeta, abstractmethod
36
35
  from collections.abc import Sequence
37
36
  from typing import TYPE_CHECKING, Any, Self, cast, final
@@ -39,6 +38,7 @@ from typing import TYPE_CHECKING, Any, Self, cast, final
39
38
  from lsst.daf.butler import DataCoordinate, DataId, DimensionPacker, DimensionRecord, Formatter
40
39
  from lsst.daf.butler.registry import DataIdError
41
40
  from lsst.pex.config import Config, RegistryField
41
+ from lsst.resources import ResourcePath, ResourcePathExpression
42
42
  from lsst.utils import doImportType
43
43
  from lsst.utils.introspection import get_full_type_name
44
44
 
@@ -65,7 +65,7 @@ class Instrument(metaclass=ABCMeta):
65
65
  the base class.
66
66
  """
67
67
 
68
- configPaths: Sequence[str] = ()
68
+ configPaths: Sequence[ResourcePathExpression] = ()
69
69
  """Paths to config files to read for specific Tasks.
70
70
 
71
71
  The paths in this list should contain files of the form `task.py`, for
@@ -366,9 +366,10 @@ class Instrument(metaclass=ABCMeta):
366
366
  Config instance to which overrides should be applied.
367
367
  """
368
368
  for root in self.configPaths:
369
- path = os.path.join(root, f"{name}.py")
370
- if os.path.exists(path):
371
- config.load(path)
369
+ resource = ResourcePath(root, forceDirectory=True, forceAbsolute=True)
370
+ uri = resource.join(f"{name}.py", forceDirectory=False)
371
+ if uri.exists():
372
+ config.load(uri)
372
373
 
373
374
  @staticmethod
374
375
  def formatCollectionTimestamp(timestamp: str | datetime.datetime) -> str:
@@ -163,7 +163,9 @@ class LogCapture:
163
163
  return cls(butler, butler)
164
164
 
165
165
  @contextmanager
166
- def capture_logging(self, task_node: TaskNode, /, quantum: Quantum) -> Iterator[_LogCaptureContext]:
166
+ def capture_logging(
167
+ self, task_node: TaskNode, /, quantum: Quantum, records: ButlerLogRecords | None = None
168
+ ) -> Iterator[_LogCaptureContext]:
167
169
  """Configure logging system to capture logs for execution of this task.
168
170
 
169
171
  Parameters
@@ -172,6 +174,9 @@ class LogCapture:
172
174
  The task definition.
173
175
  quantum : `~lsst.daf.butler.Quantum`
174
176
  Single Quantum instance.
177
+ records : `lsst.daf.butler.logging.ButlerLogRecords`, optional
178
+ Log record container to append to and save. If provided, streaming
179
+ mode is disabled (since we'll be saving logs in memory anyway).
175
180
 
176
181
  Notes
177
182
  -----
@@ -213,7 +218,7 @@ class LogCapture:
213
218
  ) from exc
214
219
  # Either accumulate into ButlerLogRecords or stream JSON records to
215
220
  # file and ingest that (ingest is possible only with full butler).
216
- if self.stream_json_logs and self.full_butler is not None:
221
+ if self.stream_json_logs and self.full_butler is not None and records is None:
217
222
  with TemporaryForIngest(self.full_butler, ref) as temporary:
218
223
  log_handler_file = FileHandler(temporary.ospath)
219
224
  log_handler_file.setFormatter(JsonLogFormatter())
@@ -236,7 +241,7 @@ class LogCapture:
236
241
  temporary.ingest()
237
242
 
238
243
  else:
239
- log_handler_memory = ButlerLogRecordHandler()
244
+ log_handler_memory = ButlerLogRecordHandler(records)
240
245
  logging.getLogger().addHandler(log_handler_memory)
241
246
 
242
247
  try:
@@ -255,7 +260,6 @@ class LogCapture:
255
260
  logging.getLogger().removeHandler(log_handler_memory)
256
261
  if ctx.store:
257
262
  self._store_log_records(quantum, log_dataset_name, log_handler_memory)
258
- log_handler_memory.records.clear()
259
263
 
260
264
  else:
261
265
  with ButlerMDC.set_mdc(mdc):
@@ -0,0 +1,79 @@
1
+ # This file is part of pipe_base.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (http://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # This software is dual licensed under the GNU General Public License and also
10
+ # under a 3-clause BSD license. Recipients may choose which of these licenses
11
+ # to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12
+ # respectively. If you choose the GPL option then the following text applies
13
+ # (but note that there is still no warranty even if you opt for BSD instead):
14
+ #
15
+ # This program is free software: you can redistribute it and/or modify
16
+ # it under the terms of the GNU General Public License as published by
17
+ # the Free Software Foundation, either version 3 of the License, or
18
+ # (at your option) any later version.
19
+ #
20
+ # This program is distributed in the hope that it will be useful,
21
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ # GNU General Public License for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+
28
+ from __future__ import annotations
29
+
30
+ __all__ = ("LogOnClose",)
31
+
32
+ from collections.abc import Callable, Iterator
33
+ from contextlib import AbstractContextManager, contextmanager
34
+ from typing import TypeVar
35
+
36
+ from lsst.utils.logging import VERBOSE
37
+
38
+ _T = TypeVar("_T")
39
+
40
+
41
+ class LogOnClose:
42
+ """A factory for context manager wrappers that emit a log message when
43
+ they are closed.
44
+
45
+ Parameters
46
+ ----------
47
+ log_func : `~collections.abc.Callable` [ `int`, `str` ]
48
+ Callable that takes an integer log level and a string message and emits
49
+ a log message. Note that placeholder formatting is not supported.
50
+ """
51
+
52
+ def __init__(self, log_func: Callable[[int, str], None]):
53
+ self.log_func = log_func
54
+
55
+ def wrap(
56
+ self,
57
+ cm: AbstractContextManager[_T],
58
+ msg: str,
59
+ level: int = VERBOSE,
60
+ ) -> AbstractContextManager[_T]:
61
+ """Wrap a context manager to log when it is exited.
62
+
63
+ Parameters
64
+ ----------
65
+ cm : `contextlib.AbstractContextManager`
66
+ Context manager to wrap.
67
+ msg : `str`
68
+ Log message.
69
+ level : `int`, optional
70
+ Log level.
71
+ """
72
+
73
+ @contextmanager
74
+ def wrapper() -> Iterator[_T]:
75
+ with cm as result:
76
+ yield result
77
+ self.log_func(level, msg)
78
+
79
+ return wrapper()
@@ -39,20 +39,24 @@ import sys
39
39
  import threading
40
40
  import time
41
41
  import uuid
42
+ from contextlib import ExitStack
42
43
  from typing import Literal, cast
43
44
 
44
45
  import networkx
45
46
 
46
47
  from lsst.daf.butler import DataCoordinate, Quantum
47
48
  from lsst.daf.butler.cli.cliLog import CliLog
49
+ from lsst.daf.butler.logging import ButlerLogRecords
48
50
  from lsst.utils.threads import disable_implicit_threading
49
51
 
50
52
  from ._status import InvalidQuantumError, RepeatableQuantumError
53
+ from ._task_metadata import TaskMetadata
51
54
  from .execution_graph_fixup import ExecutionGraphFixup
52
55
  from .graph import QuantumGraph
53
56
  from .graph_walker import GraphWalker
57
+ from .log_on_close import LogOnClose
54
58
  from .pipeline_graph import TaskNode
55
- from .quantum_graph import PredictedQuantumGraph, PredictedQuantumInfo
59
+ from .quantum_graph import PredictedQuantumGraph, PredictedQuantumInfo, ProvenanceQuantumGraphWriter
56
60
  from .quantum_graph_executor import QuantumExecutor, QuantumGraphExecutor
57
61
  from .quantum_reports import ExecutionStatus, QuantumReport, Report
58
62
 
@@ -515,7 +519,9 @@ class MPGraphExecutor(QuantumGraphExecutor):
515
519
  start_method = "spawn"
516
520
  self._start_method = start_method
517
521
 
518
- def execute(self, graph: QuantumGraph | PredictedQuantumGraph) -> None:
522
+ def execute(
523
+ self, graph: QuantumGraph | PredictedQuantumGraph, *, provenance_graph_file: str | None = None
524
+ ) -> None:
519
525
  # Docstring inherited from QuantumGraphExecutor.execute
520
526
  old_graph: QuantumGraph | None = None
521
527
  if isinstance(graph, QuantumGraph):
@@ -525,14 +531,31 @@ class MPGraphExecutor(QuantumGraphExecutor):
525
531
  new_graph = graph
526
532
  xgraph = self._make_xgraph(new_graph, old_graph)
527
533
  self._report = Report(qgraphSummary=new_graph._make_summary())
528
- try:
529
- if self._num_proc > 1:
530
- self._execute_quanta_mp(xgraph, self._report)
531
- else:
532
- self._execute_quanta_in_process(xgraph, self._report)
533
- except Exception as exc:
534
- self._report.set_exception(exc)
535
- raise
534
+ with ExitStack() as exit_stack:
535
+ provenance_writer: ProvenanceQuantumGraphWriter | None = None
536
+ if provenance_graph_file is not None:
537
+ if provenance_graph_file is not None and self._num_proc > 1:
538
+ raise NotImplementedError(
539
+ "Provenance writing is not implemented for multiprocess execution."
540
+ )
541
+ provenance_writer = ProvenanceQuantumGraphWriter(
542
+ provenance_graph_file,
543
+ exit_stack=exit_stack,
544
+ log_on_close=LogOnClose(_LOG.log),
545
+ predicted=new_graph,
546
+ )
547
+ try:
548
+ if self._num_proc > 1:
549
+ self._execute_quanta_mp(xgraph, self._report)
550
+ else:
551
+ self._execute_quanta_in_process(xgraph, self._report, provenance_writer)
552
+ except Exception as exc:
553
+ self._report.set_exception(exc)
554
+ raise
555
+ if provenance_writer is not None:
556
+ provenance_writer.write_overall_inputs()
557
+ provenance_writer.write_packages()
558
+ provenance_writer.write_init_outputs(assume_existence=True)
536
559
 
537
560
  def _make_xgraph(
538
561
  self, new_graph: PredictedQuantumGraph, old_graph: QuantumGraph | None
@@ -576,7 +599,9 @@ class MPGraphExecutor(QuantumGraphExecutor):
576
599
  raise MPGraphExecutorError("Updated execution graph has dependency cycle.")
577
600
  return xgraph
578
601
 
579
- def _execute_quanta_in_process(self, xgraph: networkx.DiGraph, report: Report) -> None:
602
+ def _execute_quanta_in_process(
603
+ self, xgraph: networkx.DiGraph, report: Report, provenance_writer: ProvenanceQuantumGraphWriter | None
604
+ ) -> None:
580
605
  """Execute all Quanta in current process.
581
606
 
582
607
  Parameters
@@ -589,6 +614,9 @@ class MPGraphExecutor(QuantumGraphExecutor):
589
614
  `.quantum_graph.PredictedQuantumGraph.quantum_only_xgraph`.
590
615
  report : `Report`
591
616
  Object for reporting execution status.
617
+ provenance_writer : `.quantum_graph.ProvenanceQuantumGraphWriter` or \
618
+ `None`
619
+ Object for recording provenance.
592
620
  """
593
621
 
594
622
  def tiebreaker_sort_key(quantum_id: uuid.UUID) -> tuple:
@@ -606,16 +634,19 @@ class MPGraphExecutor(QuantumGraphExecutor):
606
634
 
607
635
  _LOG.debug("Executing %s (%s@%s)", quantum_id, task_node.label, data_id)
608
636
  fail_exit_code: int | None = None
637
+ task_metadata: TaskMetadata | None = None
638
+ task_logs = ButlerLogRecords([])
609
639
  try:
610
640
  # For some exception types we want to exit immediately with
611
641
  # exception-specific exit code, but we still want to start
612
642
  # debugger before exiting if debugging is enabled.
613
643
  try:
614
- _, quantum_report = self._quantum_executor.execute(
615
- task_node, quantum, quantum_id=quantum_id
644
+ execution_result = self._quantum_executor.execute(
645
+ task_node, quantum, quantum_id=quantum_id, log_records=task_logs
616
646
  )
617
- if quantum_report:
618
- report.quantaReports.append(quantum_report)
647
+ if execution_result.report:
648
+ report.quantaReports.append(execution_result.report)
649
+ task_metadata = execution_result.task_metadata
619
650
  success_count += 1
620
651
  walker.finish(quantum_id)
621
652
  except RepeatableQuantumError as exc:
@@ -701,6 +732,11 @@ class MPGraphExecutor(QuantumGraphExecutor):
701
732
  )
702
733
  failed_count += 1
703
734
 
735
+ if provenance_writer is not None:
736
+ provenance_writer.write_quantum_provenance(
737
+ quantum_id, metadata=task_metadata, logs=task_logs
738
+ )
739
+
704
740
  _LOG.info(
705
741
  "Executed %d quanta successfully, %d failed and %d remain out of total %d quanta.",
706
742
  success_count,
@@ -448,14 +448,15 @@ class BaseQuantumGraphWriter:
448
448
  uri: ResourcePathExpression,
449
449
  header: HeaderModel,
450
450
  pipeline_graph: PipelineGraph,
451
- indices: dict[uuid.UUID, int],
452
451
  *,
453
452
  address_filename: str,
454
- compressor: Compressor,
455
453
  cdict_data: bytes | None = None,
454
+ zstd_level: int = 10,
456
455
  ) -> Iterator[Self]:
457
456
  uri = ResourcePath(uri)
458
- address_writer = AddressWriter(indices)
457
+ address_writer = AddressWriter()
458
+ cdict = zstandard.ZstdCompressionDict(cdict_data) if cdict_data is not None else None
459
+ compressor = zstandard.ZstdCompressor(level=zstd_level, dict_data=cdict)
459
460
  with uri.open(mode="wb") as stream:
460
461
  with zipfile.ZipFile(stream, mode="w", compression=zipfile.ZIP_STORED) as zf:
461
462
  self = cls(zf, compressor, address_writer, header.int_size)
@@ -205,13 +205,6 @@ class AddressRow:
205
205
  class AddressWriter:
206
206
  """A helper object for writing address files for multi-block files."""
207
207
 
208
- indices: dict[uuid.UUID, int] = dataclasses.field(default_factory=dict)
209
- """Mapping from UUID to internal integer ID.
210
-
211
- The internal integer ID must always correspond to the index into the
212
- sorted list of all UUIDs, but this `dict` need not be sorted itself.
213
- """
214
-
215
208
  addresses: list[dict[uuid.UUID, Address]] = dataclasses.field(default_factory=list)
216
209
  """Addresses to store with each UUID.
217
210
 
@@ -229,18 +222,15 @@ class AddressWriter:
229
222
  int_size : `int`
230
223
  Number of bytes to use for all integers.
231
224
  """
232
- for n, address_map in enumerate(self.addresses):
233
- if not self.indices.keys() >= address_map.keys():
234
- raise AssertionError(
235
- f"Logic bug in quantum graph I/O: address map {n} of {len(self.addresses)} has IDs "
236
- f"{address_map.keys() - self.indices.keys()} not in the index map."
237
- )
225
+ indices: set[uuid.UUID] = set()
226
+ for address_map in self.addresses:
227
+ indices.update(address_map.keys())
238
228
  stream.write(int_size.to_bytes(1))
239
- stream.write(len(self.indices).to_bytes(int_size))
229
+ stream.write(len(indices).to_bytes(int_size))
240
230
  stream.write(len(self.addresses).to_bytes(int_size))
241
231
  empty_address = Address()
242
- for key in sorted(self.indices.keys(), key=attrgetter("int")):
243
- row = AddressRow(key, self.indices[key], [m.get(key, empty_address) for m in self.addresses])
232
+ for n, key in enumerate(sorted(indices, key=attrgetter("int"))):
233
+ row = AddressRow(key, n, [m.get(key, empty_address) for m in self.addresses])
244
234
  _LOG.debug("Wrote address %s.", row)
245
235
  row.write(stream, int_size)
246
236
 
@@ -66,6 +66,7 @@ from lsst.daf.butler import (
66
66
  DimensionDataExtractor,
67
67
  DimensionGroup,
68
68
  DimensionRecordSetDeserializer,
69
+ DimensionUniverse,
69
70
  LimitedButler,
70
71
  Quantum,
71
72
  QuantumBackedButler,
@@ -877,6 +878,49 @@ class PredictedQuantumGraph(BaseQuantumGraph):
877
878
  page_size=page_size,
878
879
  ).assemble()
879
880
 
881
+ @classmethod
882
+ def make_empty(
883
+ cls,
884
+ universe: DimensionUniverse,
885
+ *,
886
+ output_run: str,
887
+ inputs: Iterable[str] = (),
888
+ output: str | None = None,
889
+ add_packages: bool = True,
890
+ ) -> PredictedQuantumGraph:
891
+ """Make an empty quantum graph with no tasks.
892
+
893
+ Parameters
894
+ ----------
895
+ universe : `lsst.daf.butler.DimensionUniverse`
896
+ Definitions for all butler dimensions.
897
+ output_run : `str`
898
+ Output run collection.
899
+ inputs : `~collections.abc.Iterable` [`str`], optional
900
+ Iterable of input collection names.
901
+ output : `str` or `None`, optional
902
+ Output chained collection.
903
+ add_packages : `bool`, optional
904
+ Whether to add the special init quantum that writes the 'packages'
905
+ dataset. The default (`True`) is consistent with
906
+ `~..quantum_graph_builder.QuantumGraphBuilder` behavior when there
907
+ are no regular quanta generated.
908
+
909
+ Returns
910
+ -------
911
+ quantum_graph : `PredictedQuantumGraph`
912
+ An empty quantum graph.
913
+ """
914
+ return cls(
915
+ PredictedQuantumGraphComponents.make_empty(
916
+ universe,
917
+ output_run=output_run,
918
+ inputs=inputs,
919
+ output=output,
920
+ add_packages=add_packages,
921
+ )
922
+ )
923
+
880
924
  @property
881
925
  def quanta_by_task(self) -> Mapping[str, Mapping[DataCoordinate, uuid.UUID]]:
882
926
  """A nested mapping of all quanta, keyed first by task name and then by
@@ -1541,6 +1585,63 @@ class PredictedQuantumGraphComponents:
1541
1585
  This does not include special "init" quanta.
1542
1586
  """
1543
1587
 
1588
+ @classmethod
1589
+ def make_empty(
1590
+ cls,
1591
+ universe: DimensionUniverse,
1592
+ *,
1593
+ output_run: str,
1594
+ inputs: Iterable[str] = (),
1595
+ output: str | None = None,
1596
+ add_packages: bool = True,
1597
+ ) -> PredictedQuantumGraphComponents:
1598
+ """Make components for an empty quantum graph with no tasks.
1599
+
1600
+ Parameters
1601
+ ----------
1602
+ universe : `lsst.daf.butler.DimensionUniverse`
1603
+ Definitions for all butler dimensions.
1604
+ output_run : `str`
1605
+ Output run collection.
1606
+ inputs : `~collections.abc.Iterable` [`str`], optional
1607
+ Iterable of input collection names.
1608
+ output : `str` or `None`, optional
1609
+ Output chained collection.
1610
+ add_packages : `bool`, optional
1611
+ Whether to add the special init quantum that writes the 'packages'
1612
+ dataset. The default (`True`) is consistent with
1613
+ `~..quantum_graph_builder.QuantumGraphBuilder` behavior when there
1614
+ are no regular quanta generated.
1615
+
1616
+ Returns
1617
+ -------
1618
+ components : `PredictedQuantumGraphComponents`
1619
+ Components that can be used to build or write an empty quantum
1620
+ graph.
1621
+ """
1622
+ components = cls(pipeline_graph=PipelineGraph(universe=universe))
1623
+ components.header.inputs = list(inputs)
1624
+ components.header.output_run = output_run
1625
+ components.header.output = output
1626
+ if add_packages:
1627
+ components.init_quanta.root = [
1628
+ PredictedQuantumDatasetsModel.model_construct(
1629
+ quantum_id=generate_uuidv7(),
1630
+ task_label="",
1631
+ outputs={
1632
+ acc.PACKAGES_INIT_OUTPUT_NAME: [
1633
+ PredictedDatasetModel(
1634
+ dataset_id=generate_uuidv7(),
1635
+ dataset_type_name=acc.PACKAGES_INIT_OUTPUT_NAME,
1636
+ data_coordinate=[],
1637
+ run=output_run,
1638
+ )
1639
+ ]
1640
+ },
1641
+ )
1642
+ ]
1643
+ return components
1644
+
1544
1645
  def make_dataset_ref(self, predicted: PredictedDatasetModel) -> DatasetRef:
1545
1646
  """Make a `lsst.daf.butler.DatasetRef` from information in the
1546
1647
  predicted quantum graph.
@@ -1793,7 +1894,6 @@ class PredictedQuantumGraphComponents:
1793
1894
  f"Unsupported extension {ext!r} for quantum graph; "
1794
1895
  "expected '.qg' (or '.qgraph' to force the old format)."
1795
1896
  )
1796
- cdict: zstandard.ZstdCompressionDict | None = None
1797
1897
  cdict_data: bytes | None = None
1798
1898
  quantum_datasets_json: dict[uuid.UUID, bytes] = {}
1799
1899
  if len(self.quantum_datasets) < zstd_dict_n_inputs:
@@ -1807,26 +1907,20 @@ class PredictedQuantumGraphComponents:
1807
1907
  for quantum_model in itertools.islice(self.quantum_datasets.values(), zstd_dict_n_inputs)
1808
1908
  }
1809
1909
  try:
1810
- cdict = zstandard.train_dictionary(
1910
+ cdict_data = zstandard.train_dictionary(
1811
1911
  zstd_dict_size,
1812
1912
  list(quantum_datasets_json.values()),
1813
1913
  level=zstd_level,
1814
- )
1914
+ ).as_bytes()
1815
1915
  except zstandard.ZstdError as err:
1816
1916
  warnings.warn(f"Not using a compression dictionary: {err}.")
1817
- cdict = None
1818
- else:
1819
- cdict_data = cdict.as_bytes()
1820
- compressor = zstandard.ZstdCompressor(level=zstd_level, dict_data=cdict)
1821
- indices = {quantum_id: n for n, quantum_id in enumerate(sorted(self.quantum_datasets.keys()))}
1822
1917
  with BaseQuantumGraphWriter.open(
1823
1918
  uri,
1824
1919
  header=self.header,
1825
1920
  pipeline_graph=self.pipeline_graph,
1826
- indices=indices,
1827
1921
  address_filename="quanta",
1828
- compressor=compressor,
1829
1922
  cdict_data=cdict_data,
1923
+ zstd_level=zstd_level,
1830
1924
  ) as writer:
1831
1925
  writer.write_single_model("thin_graph", self.thin_graph)
1832
1926
  if self.dimension_data is None: