lsst-pipe-base 29.2025.4000__tar.gz → 29.2025.4100__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 (171) hide show
  1. {lsst_pipe_base-29.2025.4000/python/lsst_pipe_base.egg-info → lsst_pipe_base-29.2025.4100}/PKG-INFO +1 -1
  2. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/_task_metadata.py +15 -0
  3. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph/_versionDeserializers.py +6 -5
  4. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph/graph.py +2 -1
  5. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph/graphSummary.py +30 -0
  6. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/io.py +1 -1
  7. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph/_common.py +23 -1
  8. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph/_multiblock.py +174 -103
  9. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph/_predicted.py +82 -6
  10. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph_builder.py +4 -4
  11. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_reports.py +45 -0
  12. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/mocks/_storage_class.py +45 -0
  13. lsst_pipe_base-29.2025.4100/python/lsst/pipe/base/version.py +2 -0
  14. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100/python/lsst_pipe_base.egg-info}/PKG-INFO +1 -1
  15. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_pipeline_graph.py +5 -0
  16. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_predicted_qg.py +4 -4
  17. lsst_pipe_base-29.2025.4000/python/lsst/pipe/base/version.py +0 -2
  18. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/COPYRIGHT +0 -0
  19. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/LICENSE +0 -0
  20. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/MANIFEST.in +0 -0
  21. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/README.md +0 -0
  22. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/bsd_license.txt +0 -0
  23. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/CHANGES.rst +0 -0
  24. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/creating-a-pipeline.rst +0 -0
  25. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/creating-a-pipelinetask.rst +0 -0
  26. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/creating-a-task.rst +0 -0
  27. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/index.rst +0 -0
  28. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/task-framework-overview.rst +0 -0
  29. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/task-retargeting-howto.rst +0 -0
  30. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/testing-a-pipeline-task.rst +0 -0
  31. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/testing-pipelines-with-mocks.rst +0 -0
  32. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/doc/lsst.pipe.base/working-with-pipeline-graphs.rst +0 -0
  33. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/gpl-v3.0.txt +0 -0
  34. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/pyproject.toml +0 -0
  35. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/__init__.py +0 -0
  36. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/__init__.py +0 -0
  37. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/__init__.py +0 -0
  38. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/_datasetQueryConstraints.py +0 -0
  39. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/_dataset_handle.py +0 -0
  40. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/_instrument.py +0 -0
  41. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/_observation_dimension_packer.py +0 -0
  42. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/_quantumContext.py +0 -0
  43. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/_status.py +0 -0
  44. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/all_dimensions_quantum_graph_builder.py +0 -0
  45. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/automatic_connection_constants.py +0 -0
  46. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/caching_limited_butler.py +0 -0
  47. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/cli/__init__.py +0 -0
  48. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/cli/_get_cli_subcommands.py +0 -0
  49. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/cli/cmd/__init__.py +0 -0
  50. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/cli/cmd/commands.py +0 -0
  51. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/cli/opt/__init__.py +0 -0
  52. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/cli/opt/arguments.py +0 -0
  53. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/cli/opt/options.py +0 -0
  54. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/config.py +0 -0
  55. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/configOverrides.py +0 -0
  56. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/connectionTypes.py +0 -0
  57. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/connections.py +0 -0
  58. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/dot_tools.py +0 -0
  59. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/exec_fixup_data_id.py +0 -0
  60. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/execution_graph_fixup.py +0 -0
  61. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/execution_reports.py +0 -0
  62. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/formatters/__init__.py +0 -0
  63. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/formatters/pexConfig.py +0 -0
  64. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph/__init__.py +0 -0
  65. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph/_implDetails.py +0 -0
  66. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph/_loadHelpers.py +0 -0
  67. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph/quantumNode.py +0 -0
  68. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/graph_walker.py +0 -0
  69. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/log_capture.py +0 -0
  70. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/mermaid_tools.py +0 -0
  71. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/mp_graph_executor.py +0 -0
  72. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline.py +0 -0
  73. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipelineIR.py +0 -0
  74. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipelineTask.py +0 -0
  75. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/__init__.py +0 -0
  76. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/__main__.py +0 -0
  77. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_dataset_types.py +0 -0
  78. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_edges.py +0 -0
  79. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_exceptions.py +0 -0
  80. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_mapping_views.py +0 -0
  81. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_nodes.py +0 -0
  82. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_pipeline_graph.py +0 -0
  83. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_task_subsets.py +0 -0
  84. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/_tasks.py +0 -0
  85. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/expressions.py +0 -0
  86. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/__init__.py +0 -0
  87. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_dot.py +0 -0
  88. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_formatting.py +0 -0
  89. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_layout.py +0 -0
  90. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_merge.py +0 -0
  91. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_mermaid.py +0 -0
  92. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_options.py +0 -0
  93. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_printer.py +0 -0
  94. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_show.py +0 -0
  95. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/pipeline_graph/visualization/_status_annotator.py +0 -0
  96. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/prerequisite_helpers.py +0 -0
  97. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/py.typed +0 -0
  98. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph/__init__.py +0 -0
  99. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph/visualization.py +0 -0
  100. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph_executor.py +0 -0
  101. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_graph_skeleton.py +0 -0
  102. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/quantum_provenance_graph.py +0 -0
  103. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/script/__init__.py +0 -0
  104. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/script/register_instrument.py +0 -0
  105. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/script/retrieve_artifacts_for_quanta.py +0 -0
  106. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/script/transfer_from_graph.py +0 -0
  107. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/script/utils.py +0 -0
  108. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/script/zip_from_graph.py +0 -0
  109. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/separable_pipeline_executor.py +0 -0
  110. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/simple_pipeline_executor.py +0 -0
  111. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/single_quantum_executor.py +0 -0
  112. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/struct.py +0 -0
  113. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/task.py +0 -0
  114. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/taskFactory.py +0 -0
  115. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/testUtils.py +0 -0
  116. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/__init__.py +0 -0
  117. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/in_memory_limited_butler.py +0 -0
  118. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/mocks/__init__.py +0 -0
  119. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/mocks/_data_id_match.py +0 -0
  120. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/mocks/_pipeline_task.py +0 -0
  121. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/mocks/_repo.py +0 -0
  122. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/no_dimensions.py +0 -0
  123. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/pipelineStepTester.py +0 -0
  124. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/simpleQGraph.py +0 -0
  125. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/tests/util.py +0 -0
  126. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst/pipe/base/utils.py +0 -0
  127. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst_pipe_base.egg-info/SOURCES.txt +0 -0
  128. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst_pipe_base.egg-info/dependency_links.txt +0 -0
  129. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst_pipe_base.egg-info/entry_points.txt +0 -0
  130. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst_pipe_base.egg-info/requires.txt +0 -0
  131. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst_pipe_base.egg-info/top_level.txt +0 -0
  132. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/python/lsst_pipe_base.egg-info/zip-safe +0 -0
  133. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/setup.cfg +0 -0
  134. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_adjust_all_quanta.py +0 -0
  135. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_caching_limited_butler.py +0 -0
  136. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_cliCmdRegisterInstrument.py +0 -0
  137. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_configOverrides.py +0 -0
  138. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_config_formatter.py +0 -0
  139. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_connections.py +0 -0
  140. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_dataid_match.py +0 -0
  141. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_dataset_handle.py +0 -0
  142. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_dot_tools.py +0 -0
  143. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_dynamic_connections.py +0 -0
  144. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_execution_reports.py +0 -0
  145. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_execution_storage_class_conversion.py +0 -0
  146. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_graphBuilder.py +0 -0
  147. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_graph_walker.py +0 -0
  148. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_init_output_run.py +0 -0
  149. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_instrument.py +0 -0
  150. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_mermaid.py +0 -0
  151. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_mp_graph_executor.py +0 -0
  152. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_pipeline.py +0 -0
  153. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_pipelineIR.py +0 -0
  154. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_pipelineLoadSubset.py +0 -0
  155. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_pipelineTask.py +0 -0
  156. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_pipeline_graph_expressions.py +0 -0
  157. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_qg_builder_dimensions.py +0 -0
  158. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_quantumGraph.py +0 -0
  159. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_quantum_provenance_graph.py +0 -0
  160. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_quantum_reports.py +0 -0
  161. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_quantum_success_caveats.py +0 -0
  162. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_script_utils.py +0 -0
  163. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_separable_pipeline_executor.py +0 -0
  164. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_simple_pipeline_executor.py +0 -0
  165. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_single_quantum_executor.py +0 -0
  166. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_struct.py +0 -0
  167. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_task.py +0 -0
  168. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_task_factory.py +0 -0
  169. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_taskmetadata.py +0 -0
  170. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_testUtils.py +0 -0
  171. {lsst_pipe_base-29.2025.4000 → lsst_pipe_base-29.2025.4100}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-pipe-base
3
- Version: 29.2025.4000
3
+ Version: 29.2025.4100
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: BSD 3-Clause License
@@ -686,6 +686,21 @@ class TaskMetadata(BaseModel):
686
686
  """See `pydantic.BaseModel.model_json_schema`."""
687
687
  return super().model_json_schema(*args, **kwargs)
688
688
 
689
+ @classmethod
690
+ def model_validate(cls, *args: Any, **kwargs: Any) -> Any:
691
+ """See `pydantic.BaseModel.model_validate`."""
692
+ return super().model_validate(*args, **kwargs)
693
+
694
+ @classmethod
695
+ def model_validate_json(cls, *args: Any, **kwargs: Any) -> Any:
696
+ """See `pydantic.BaseModel.model_validate_json`."""
697
+ return super().model_validate_json(*args, **kwargs)
698
+
699
+ @classmethod
700
+ def model_validate_strings(cls, *args: Any, **kwargs: Any) -> Any:
701
+ """See `pydantic.BaseModel.model_validate_strings`."""
702
+ return super().model_validate_strings(*args, **kwargs)
703
+
689
704
 
690
705
  # Needed because a TaskMetadata can contain a TaskMetadata.
691
706
  TaskMetadata.model_rebuild()
@@ -38,7 +38,7 @@ from collections import defaultdict
38
38
  from collections.abc import Callable
39
39
  from dataclasses import dataclass
40
40
  from types import SimpleNamespace
41
- from typing import TYPE_CHECKING, ClassVar, cast
41
+ from typing import TYPE_CHECKING, ClassVar
42
42
 
43
43
  import networkx as nx
44
44
 
@@ -50,6 +50,7 @@ from lsst.daf.butler import (
50
50
  Quantum,
51
51
  SerializedDimensionRecord,
52
52
  )
53
+ from lsst.daf.butler._rubin import generate_uuidv7
53
54
  from lsst.utils import doImportType
54
55
 
55
56
  from ..config import PipelineTaskConfig
@@ -242,7 +243,7 @@ class DeserializerV1(DeserializerBase):
242
243
 
243
244
  # reconstruct node
244
245
  qNode = pickle.loads(dump)
245
- object.__setattr__(qNode, "nodeId", uuid.uuid4())
246
+ object.__setattr__(qNode, "nodeId", generate_uuidv7())
246
247
 
247
248
  # read the saved node, name. If it has been loaded, attach it, if
248
249
  # not read in the taskDef first, and then load it
@@ -376,7 +377,7 @@ class DeserializerV2(DeserializerBase):
376
377
 
377
378
  # reconstruct node
378
379
  qNode = pickle.loads(dump)
379
- object.__setattr__(qNode, "nodeId", uuid.uuid4())
380
+ object.__setattr__(qNode, "nodeId", generate_uuidv7())
380
381
 
381
382
  # read the saved node, name. If it has been loaded, attach it, if
382
383
  # not read in the taskDef first, and then load it
@@ -599,11 +600,11 @@ class DeserializerV3(DeserializerBase):
599
600
  # initInputRefs and initOutputRefs are optional
600
601
  if (refs := taskDefDump.get("initInputRefs")) is not None:
601
602
  initInputRefs[recreatedTaskDef.label] = [
602
- cast(DatasetRef, DatasetRef.from_json(ref, universe=universe)) for ref in refs
603
+ DatasetRef.from_json(ref, universe=universe) for ref in refs
603
604
  ]
604
605
  if (refs := taskDefDump.get("initOutputRefs")) is not None:
605
606
  initOutputRefs[recreatedTaskDef.label] = [
606
- cast(DatasetRef, DatasetRef.from_json(ref, universe=universe)) for ref in refs
607
+ DatasetRef.from_json(ref, universe=universe) for ref in refs
607
608
  ]
608
609
 
609
610
  # rebuild the mappings that associate dataset type names with
@@ -59,6 +59,7 @@ from lsst.daf.butler import (
59
59
  Quantum,
60
60
  QuantumBackedButler,
61
61
  )
62
+ from lsst.daf.butler._rubin import generate_uuidv7
62
63
  from lsst.daf.butler.datastore.record_data import DatastoreRecordData
63
64
  from lsst.daf.butler.persistence_context import PersistenceContextVars
64
65
  from lsst.daf.butler.registry import ConflictingDefinitionError
@@ -246,7 +247,7 @@ class QuantumGraph:
246
247
  "associated value in the mapping"
247
248
  )
248
249
  else:
249
- nodeId = uuid.uuid4()
250
+ nodeId = generate_uuidv7()
250
251
 
251
252
  inits = quantum.initInputs.values()
252
253
  inputs = quantum.inputs.values()
@@ -75,6 +75,21 @@ class QgraphTaskSummary(pydantic.BaseModel):
75
75
  """See `pydantic.BaseModel.model_json_schema`."""
76
76
  return super().model_json_schema(*args, **kwargs)
77
77
 
78
+ @classmethod
79
+ def model_validate(cls, *args: Any, **kwargs: Any) -> Any:
80
+ """See `pydantic.BaseModel.model_validate`."""
81
+ return super().model_validate(*args, **kwargs)
82
+
83
+ @classmethod
84
+ def model_validate_json(cls, *args: Any, **kwargs: Any) -> Any:
85
+ """See `pydantic.BaseModel.model_validate_json`."""
86
+ return super().model_validate_json(*args, **kwargs)
87
+
88
+ @classmethod
89
+ def model_validate_strings(cls, *args: Any, **kwargs: Any) -> Any:
90
+ """See `pydantic.BaseModel.model_validate_strings`."""
91
+ return super().model_validate_strings(*args, **kwargs)
92
+
78
93
 
79
94
  class QgraphSummary(pydantic.BaseModel):
80
95
  """Report for the QuantumGraph creation or reading."""
@@ -129,3 +144,18 @@ class QgraphSummary(pydantic.BaseModel):
129
144
  def model_json_schema(cls, *args: Any, **kwargs: Any) -> Any:
130
145
  """See `pydantic.BaseModel.model_json_schema`."""
131
146
  return super().model_json_schema(*args, **kwargs)
147
+
148
+ @classmethod
149
+ def model_validate(cls, *args: Any, **kwargs: Any) -> Any:
150
+ """See `pydantic.BaseModel.model_validate`."""
151
+ return super().model_validate(*args, **kwargs)
152
+
153
+ @classmethod
154
+ def model_validate_json(cls, *args: Any, **kwargs: Any) -> Any:
155
+ """See `pydantic.BaseModel.model_validate_json`."""
156
+ return super().model_validate_json(*args, **kwargs)
157
+
158
+ @classmethod
159
+ def model_validate_strings(cls, *args: Any, **kwargs: Any) -> Any:
160
+ """See `pydantic.BaseModel.model_validate_strings`."""
161
+ return super().model_validate_strings(*args, **kwargs)
@@ -650,7 +650,7 @@ class SerializedTaskSubset(pydantic.BaseModel):
650
650
  """
651
651
  members = set(self.tasks)
652
652
  if label in steps:
653
- steps.set_dimensions(label, self.dimensions)
653
+ steps._dimensions_by_label[label] = frozenset(self.dimensions)
654
654
  return TaskSubset(xgraph, label, members, self.description, steps)
655
655
 
656
656
 
@@ -229,6 +229,21 @@ class HeaderModel(pydantic.BaseModel):
229
229
  """See `pydantic.BaseModel.model_json_schema`."""
230
230
  return super().model_json_schema(*args, **kwargs)
231
231
 
232
+ @classmethod
233
+ def model_validate(cls, *args: Any, **kwargs: Any) -> Any:
234
+ """See `pydantic.BaseModel.model_validate`."""
235
+ return super().model_validate(*args, **kwargs)
236
+
237
+ @classmethod
238
+ def model_validate_json(cls, *args: Any, **kwargs: Any) -> Any:
239
+ """See `pydantic.BaseModel.model_validate_json`."""
240
+ return super().model_validate_json(*args, **kwargs)
241
+
242
+ @classmethod
243
+ def model_validate_strings(cls, *args: Any, **kwargs: Any) -> Any:
244
+ """See `pydantic.BaseModel.model_validate_strings`."""
245
+ return super().model_validate_strings(*args, **kwargs)
246
+
232
247
 
233
248
  class QuantumInfo(TypedDict):
234
249
  """A typed dictionary that annotates the attributes of the NetworkX graph
@@ -493,6 +508,7 @@ class BaseQuantumGraphReader:
493
508
  *,
494
509
  address_filename: str,
495
510
  graph_type: str,
511
+ n_addresses: int,
496
512
  page_size: int | None = None,
497
513
  import_mode: TaskImportMode = TaskImportMode.ASSUME_CONSISTENT_EDGES,
498
514
  ) -> Iterator[Self]:
@@ -506,6 +522,8 @@ class BaseQuantumGraphReader:
506
522
  Base filename for the address file.
507
523
  graph_type : `str`
508
524
  Value to expect for `HeaderModel.graph_type`.
525
+ n_addresses : `int`
526
+ Number of addresses to expect per row in the address file.
509
527
  page_size : `int`, optional
510
528
  Approximate number of bytes to read at once from address files.
511
529
  Note that this does not set a page size for *all* reads, but it
@@ -539,7 +557,11 @@ class BaseQuantumGraphReader:
539
557
  )
540
558
  pipeline_graph = serialized_pipeline_graph.deserialize(import_mode)
541
559
  with AddressReader.open_in_zip(
542
- zf, address_filename, page_size=page_size, int_size=header.int_size
560
+ zf,
561
+ address_filename,
562
+ page_size=page_size,
563
+ int_size=header.int_size,
564
+ n_addresses=n_addresses,
543
565
  ) as address_reader:
544
566
  yield cls(
545
567
  header=header,
@@ -40,14 +40,13 @@ __all__ = (
40
40
  )
41
41
 
42
42
  import dataclasses
43
- import itertools
44
43
  import logging
45
44
  import uuid
46
45
  from collections.abc import Iterator
47
46
  from contextlib import contextmanager
48
47
  from io import BufferedReader, BytesIO
49
48
  from operator import attrgetter
50
- from typing import IO, TYPE_CHECKING, ClassVar, Protocol, TypeVar
49
+ from typing import IO, TYPE_CHECKING, Protocol, TypeAlias, TypeVar
51
50
 
52
51
  import pydantic
53
52
 
@@ -61,6 +60,11 @@ _LOG = logging.getLogger(__name__)
61
60
  _T = TypeVar("_T", bound=pydantic.BaseModel)
62
61
 
63
62
 
63
+ UUID_int: TypeAlias = int
64
+
65
+ MAX_UUID_INT: UUID_int = 2**128
66
+
67
+
64
68
  DEFAULT_PAGE_SIZE: int = 5_000_000
65
69
  """Default page size for reading chunks of quantum graph files.
66
70
 
@@ -255,13 +259,51 @@ class AddressWriter:
255
259
  self.write(stream, int_size=int_size)
256
260
 
257
261
 
262
+ @dataclasses.dataclass
263
+ class AddressPage:
264
+ """A page of addresses in the `AddressReader`."""
265
+
266
+ file_offset: int
267
+ """Offset in bytes to this page from the beginning of the file."""
268
+
269
+ begin: int
270
+ """Index of the first row in this page."""
271
+
272
+ n_rows: int
273
+ """Number of rows in this page."""
274
+
275
+ read: bool = False
276
+ """Whether this page has already been read."""
277
+
278
+ @property
279
+ def end(self) -> int:
280
+ """One past the last row index in this page."""
281
+ return self.begin + self.n_rows
282
+
283
+
284
+ @dataclasses.dataclass
285
+ class PageBounds:
286
+ """A page index and the UUID interval that page covers."""
287
+
288
+ page_index: int
289
+ """Index into the page array."""
290
+
291
+ uuid_int_begin: UUID_int
292
+ """Integer representation of the smallest UUID in this page."""
293
+
294
+ uuid_int_end: UUID_int
295
+ """One larger than the integer representation of the largest UUID in this
296
+ page.
297
+ """
298
+
299
+ def __str__(self) -> str:
300
+ return f"{self.page_index} [{self.uuid_int_begin:x}:{self.uuid_int_end:x}]"
301
+
302
+
258
303
  @dataclasses.dataclass
259
304
  class AddressReader:
260
305
  """A helper object for reading address files for multi-block files."""
261
306
 
262
- MAX_UUID_INT: ClassVar[int] = 2**128
263
- """The maximum value of a UUID's integer form."""
264
-
265
307
  stream: IO[bytes]
266
308
  """Stream to read from."""
267
309
 
@@ -269,30 +311,28 @@ class AddressReader:
269
311
  """Size of each integer in bytes."""
270
312
 
271
313
  n_rows: int
272
- """Number of address rows in the file (also the number of UUIDs)."""
314
+ """Number of rows in the file."""
273
315
 
274
316
  n_addresses: int
275
317
  """Number of addresses in each row."""
276
318
 
277
- start_index: int
278
- """Index of the first row."""
319
+ rows_per_page: int
320
+ """Number of addresses in each page."""
279
321
 
280
- rows: dict[uuid.UUID, AddressRow]
322
+ rows: dict[uuid.UUID, AddressRow] = dataclasses.field(default_factory=dict)
281
323
  """Rows that have already been read."""
282
324
 
283
- rows_per_page: int
284
- """Minimum number of rows to read at once."""
285
-
286
- unread_pages: dict[int, int]
287
- """Pages that have not yet been read, as a mapping from page index to the
288
- number of rows in that page.
325
+ rows_by_index: dict[int, AddressRow] = dataclasses.field(default_factory=dict)
326
+ """Rows that have already been read, keyed by integer index."""
289
327
 
290
- Values are always `rows_per_page` with the possible exception of the last
291
- page.
292
- """
328
+ pages: list[AddressPage] = dataclasses.field(default_factory=list)
329
+ page_bounds: dict[int, PageBounds] = dataclasses.field(default_factory=dict)
330
+ """Mapping from page index to page boundary information."""
293
331
 
294
332
  @classmethod
295
- def from_stream(cls, stream: IO[bytes], page_size: int, start_index: int = 0) -> AddressReader:
333
+ def from_stream(
334
+ cls, stream: IO[bytes], *, page_size: int, n_addresses: int, int_size: int
335
+ ) -> AddressReader:
296
336
  """Construct from a stream by reading the header.
297
337
 
298
338
  Parameters
@@ -302,27 +342,47 @@ class AddressReader:
302
342
  page_size : `int`
303
343
  Approximate number of bytes to read at a time when searching for an
304
344
  address.
305
- start_index : `int`, optional
306
- Value of the first index in the file.
345
+ n_addresses : `int`
346
+ Number of addresses to expect per row. This is checked against
347
+ the size embedded in the file.
348
+ int_size : `int`
349
+ Number of bytes to use for all integers. This is checked against
350
+ the size embedded in the file.
307
351
  """
308
- int_size = int.from_bytes(stream.read(1))
309
- n_rows = int.from_bytes(stream.read(int_size))
310
- n_addresses = int.from_bytes(stream.read(int_size))
311
- rows_per_page = max(page_size // cls.compute_row_size(int_size, n_addresses), 1)
312
- n_full_pages, last_rows_per_page = divmod(n_rows, rows_per_page)
313
- unread_pages = dict.fromkeys(range(n_full_pages), rows_per_page)
314
- if last_rows_per_page := n_rows % rows_per_page:
315
- unread_pages[n_full_pages] = last_rows_per_page
316
- return cls(
317
- stream,
318
- int_size=int_size,
319
- n_rows=n_rows,
320
- n_addresses=n_addresses,
321
- start_index=start_index,
322
- rows={},
323
- rows_per_page=rows_per_page,
324
- unread_pages=unread_pages,
325
- )
352
+ header_size = cls.compute_header_size(int_size)
353
+ row_size = cls.compute_row_size(int_size, n_addresses)
354
+ # Read the raw header page.
355
+ header_page_data = stream.read(header_size)
356
+ if len(header_page_data) < header_size:
357
+ raise InvalidQuantumGraphFileError("Address file unexpectedly truncated.")
358
+ # Interpret the raw header data and initialize the reader instance.
359
+ header_page_stream = BytesIO(header_page_data)
360
+ file_int_size = int.from_bytes(header_page_stream.read(1))
361
+ if file_int_size != int_size:
362
+ raise InvalidQuantumGraphFileError(
363
+ f"int size in address file ({file_int_size}) does not match int size in header ({int_size})."
364
+ )
365
+ n_rows = int.from_bytes(header_page_stream.read(int_size))
366
+ file_n_addresses = int.from_bytes(header_page_stream.read(int_size))
367
+ if file_n_addresses != n_addresses:
368
+ raise InvalidQuantumGraphFileError(
369
+ f"Incorrect number of addresses per row: expected {n_addresses}, got {file_n_addresses}."
370
+ )
371
+ rows_per_page = max(page_size // row_size, 1)
372
+ # Construct an instance.
373
+ self = cls(stream, int_size, n_rows, n_addresses, rows_per_page=rows_per_page)
374
+ # Calculate positions of each page of rows.
375
+ row_index = 0
376
+ file_offset = header_size
377
+ while row_index < n_rows:
378
+ self.pages.append(AddressPage(file_offset=file_offset, begin=row_index, n_rows=rows_per_page))
379
+ row_index += rows_per_page
380
+ file_offset += rows_per_page * row_size
381
+ if row_index != n_rows:
382
+ # Last page was too big.
383
+ self.pages[-1].n_rows -= row_index - n_rows
384
+ assert sum(p.n_rows for p in self.pages) == n_rows, "Bad logic setting page row counts."
385
+ return self
326
386
 
327
387
  @classmethod
328
388
  @contextmanager
@@ -330,9 +390,10 @@ class AddressReader:
330
390
  cls,
331
391
  zf: zipfile.ZipFile,
332
392
  name: str,
393
+ *,
333
394
  page_size: int,
334
- int_size: int | None = None,
335
- start_index: int = 0,
395
+ n_addresses: int,
396
+ int_size: int,
336
397
  ) -> Iterator[AddressReader]:
337
398
  """Make a reader for an address file in a zip archive.
338
399
 
@@ -345,11 +406,12 @@ class AddressReader:
345
406
  page_size : `int`
346
407
  Approximate number of bytes to read at a time when searching for an
347
408
  address.
348
- int_size : `int`, optional
409
+ n_addresses : `int`
410
+ Number of addresses to expect per row. This is checked against
411
+ the size embedded in the file.
412
+ int_size : `int`
349
413
  Number of bytes to use for all integers. This is checked against
350
414
  the size embedded in the file.
351
- start_index : `int`, optional
352
- Value of the first index in the file.
353
415
 
354
416
  Returns
355
417
  -------
@@ -357,12 +419,7 @@ class AddressReader:
357
419
  Context manager that returns a reader when entered.
358
420
  """
359
421
  with zf.open(f"{name}.addr", mode="r") as stream:
360
- result = cls.from_stream(stream, page_size=page_size, start_index=start_index)
361
- if int_size is not None and result.int_size != int_size:
362
- raise InvalidQuantumGraphFileError(
363
- "int size in address file does not match int size in header."
364
- )
365
- yield result
422
+ yield cls.from_stream(stream, page_size=page_size, n_addresses=n_addresses, int_size=int_size)
366
423
 
367
424
  @staticmethod
368
425
  def compute_header_size(int_size: int) -> int:
@@ -409,11 +466,6 @@ class AddressReader:
409
466
  )
410
467
  )
411
468
 
412
- @property
413
- def header_size(self) -> int:
414
- """The size (in bytes) of the header of this address file."""
415
- return self.compute_header_size(self.int_size)
416
-
417
469
  @property
418
470
  def row_size(self) -> int:
419
471
  """The size (in bytes) of each row of this address file."""
@@ -427,16 +479,26 @@ class AddressReader:
427
479
  rows : `dict` [ `uuid.UUID`, `AddressRow` ]
428
480
  Mapping of loaded address rows, keyed by UUID.
429
481
  """
482
+ # Skip any pages from the beginning that have already been read; this
483
+ # nicely handles both the case where we already read everything (or
484
+ # there was nothing to read) while giving us a page with a file offset
485
+ # to start from.
486
+ for page in self.pages:
487
+ if not page.read:
488
+ break
489
+ else:
490
+ return self.rows
491
+ # Read the entire rest of the file into memory.
492
+ self.stream.seek(page.file_offset)
493
+ data = self.stream.read()
494
+ buffer = BytesIO(data)
430
495
  # Shortcut out if we've already read everything, but don't bother
431
496
  # optimizing previous partial reads.
432
- if self.unread_pages:
433
- self.stream.seek(self.header_size)
434
- data = self.stream.read()
435
- buffer = BytesIO(data)
436
- _LOG.debug("Reading all %d address rows.", self.n_rows)
437
- for _ in range(self.n_rows):
438
- self._read_row(buffer)
439
- self.unread_pages.clear()
497
+ while len(self.rows) < self.n_rows:
498
+ self._read_row(buffer)
499
+ # Delete all pages; they don't matter anymore, and that's easier than
500
+ # updating them to reflect the reads we've done.
501
+ self.pages.clear()
440
502
  return self.rows
441
503
 
442
504
  def find(self, key: uuid.UUID) -> AddressRow:
@@ -452,54 +514,63 @@ class AddressReader:
452
514
  row : `AddressRow`
453
515
  Addresses for the given UUID.
454
516
  """
455
- if (row := self.rows.get(key)) is not None:
517
+ match key:
518
+ case uuid.UUID():
519
+ return self._find_uuid(key)
520
+ case _:
521
+ raise TypeError(f"Invalid argument: {key}.")
522
+
523
+ def _find_uuid(self, target: uuid.UUID) -> AddressRow:
524
+ if (row := self.rows.get(target)) is not None:
456
525
  return row
457
- guess_index_float = (key.int / self.MAX_UUID_INT) * self.n_rows + self.start_index
458
- guess_page_float = (guess_index_float - self.start_index) / self.rows_per_page
459
- guess_page = int(guess_page_float)
460
- _LOG.debug(
461
- "Searching for %s, starting at index %s of %s (%s rows per page).",
462
- key,
463
- guess_index_float,
464
- self.n_rows,
465
- self.rows_per_page,
466
- )
467
- for page in self._page_search_path(guess_page):
468
- if page in self.unread_pages:
469
- self._read_page(page)
470
- if (row := self.rows.get(key)) is not None:
471
- return row
472
- elif not self.unread_pages:
473
- raise LookupError(f"Address for UUID {key} not found.")
474
- raise AssertionError("Logic error in page tracking.")
475
-
476
- def _read_page(self, page_index: int) -> None:
477
- rows_in_page = self.unread_pages[page_index]
478
- _LOG.debug(
479
- "Reading page %s (rows %s:%s).",
480
- page_index,
481
- page_index * self.rows_per_page,
482
- page_index * self.rows_per_page + rows_in_page,
483
- )
484
- self.stream.seek(page_index * self.rows_per_page * self.row_size + self.header_size)
485
- data = self.stream.read(self.row_size * rows_in_page)
486
- page_stream = BytesIO(data)
487
- for _ in range(rows_in_page):
488
- self._read_row(page_stream)
489
- del self.unread_pages[page_index]
526
+ if self.n_rows == 0 or not self.pages:
527
+ raise LookupError(f"Address for {target} not found.")
528
+
529
+ # Use a binary search to find the page containing the target UUID.
530
+ left = 0
531
+ right = len(self.pages) - 1
532
+ while left <= right:
533
+ mid = left + ((right - left) // 2)
534
+ self._read_page(mid)
535
+ if (row := self.rows.get(target)) is not None:
536
+ return row
537
+ bounds = self.page_bounds[mid]
538
+ if target.int < bounds.uuid_int_begin:
539
+ right = mid - 1
540
+ elif target.int > bounds.uuid_int_end:
541
+ left = mid + 1
542
+ else:
543
+ # Should have been on this page, but it wasn't.
544
+ raise LookupError(f"Address for {target} not found.")
545
+
546
+ # Ran out of pages to search.
547
+ raise LookupError(f"Address for {target} not found.")
548
+
549
+ def _read_page(self, page_index: int, page_stream: BytesIO | None = None) -> bool:
550
+ page = self.pages[page_index]
551
+ if page.read:
552
+ return False
553
+ if page_stream is None:
554
+ self.stream.seek(page.file_offset)
555
+ page_stream = BytesIO(self.stream.read(page.n_rows * self.row_size))
556
+ row = self._read_row(page_stream)
557
+ uuid_int_begin = row.key.int
558
+ for _ in range(1, page.n_rows):
559
+ row = self._read_row(page_stream)
560
+ uuid_int_end = row.key.int + 1 # Python's loop scoping rules are actually useful here!
561
+ page.read = True
562
+ bounds = PageBounds(page_index=page_index, uuid_int_begin=uuid_int_begin, uuid_int_end=uuid_int_end)
563
+ self.page_bounds[page_index] = bounds
564
+ _LOG.debug("Read page %s with rows [%s:%s].", bounds, page.begin, page.end)
565
+ return True
490
566
 
491
567
  def _read_row(self, page_stream: BytesIO) -> AddressRow:
492
568
  row = AddressRow.read(page_stream, self.n_addresses, self.int_size)
493
569
  self.rows[row.key] = row
570
+ self.rows_by_index[row.index] = row
494
571
  _LOG.debug("Read address row %s.", row)
495
572
  return row
496
573
 
497
- def _page_search_path(self, mid: int) -> Iterator[int]:
498
- yield mid
499
- for abs_offset in itertools.count(1):
500
- yield mid + abs_offset
501
- yield mid - abs_offset
502
-
503
574
 
504
575
  @dataclasses.dataclass
505
576
  class MultiblockWriter: