lsst-pipe-base 30.0.0rc2__py3-none-any.whl → 30.0.1__py3-none-any.whl

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 (69) hide show
  1. lsst/pipe/base/_instrument.py +31 -20
  2. lsst/pipe/base/_quantumContext.py +3 -3
  3. lsst/pipe/base/_status.py +43 -10
  4. lsst/pipe/base/_task_metadata.py +2 -2
  5. lsst/pipe/base/all_dimensions_quantum_graph_builder.py +8 -3
  6. lsst/pipe/base/automatic_connection_constants.py +20 -1
  7. lsst/pipe/base/cli/cmd/__init__.py +18 -2
  8. lsst/pipe/base/cli/cmd/commands.py +149 -4
  9. lsst/pipe/base/connectionTypes.py +72 -160
  10. lsst/pipe/base/connections.py +6 -9
  11. lsst/pipe/base/execution_reports.py +0 -5
  12. lsst/pipe/base/graph/graph.py +11 -10
  13. lsst/pipe/base/graph/quantumNode.py +4 -4
  14. lsst/pipe/base/graph_walker.py +8 -10
  15. lsst/pipe/base/log_capture.py +40 -80
  16. lsst/pipe/base/log_on_close.py +76 -0
  17. lsst/pipe/base/mp_graph_executor.py +51 -15
  18. lsst/pipe/base/pipeline.py +5 -6
  19. lsst/pipe/base/pipelineIR.py +2 -8
  20. lsst/pipe/base/pipelineTask.py +5 -7
  21. lsst/pipe/base/pipeline_graph/_dataset_types.py +2 -2
  22. lsst/pipe/base/pipeline_graph/_edges.py +32 -22
  23. lsst/pipe/base/pipeline_graph/_mapping_views.py +4 -7
  24. lsst/pipe/base/pipeline_graph/_pipeline_graph.py +14 -7
  25. lsst/pipe/base/pipeline_graph/expressions.py +2 -2
  26. lsst/pipe/base/pipeline_graph/io.py +7 -10
  27. lsst/pipe/base/pipeline_graph/visualization/_dot.py +13 -12
  28. lsst/pipe/base/pipeline_graph/visualization/_layout.py +16 -18
  29. lsst/pipe/base/pipeline_graph/visualization/_merge.py +4 -7
  30. lsst/pipe/base/pipeline_graph/visualization/_printer.py +10 -10
  31. lsst/pipe/base/pipeline_graph/visualization/_status_annotator.py +7 -0
  32. lsst/pipe/base/prerequisite_helpers.py +2 -1
  33. lsst/pipe/base/quantum_graph/_common.py +19 -20
  34. lsst/pipe/base/quantum_graph/_multiblock.py +37 -31
  35. lsst/pipe/base/quantum_graph/_predicted.py +113 -15
  36. lsst/pipe/base/quantum_graph/_provenance.py +1136 -45
  37. lsst/pipe/base/quantum_graph/aggregator/__init__.py +0 -1
  38. lsst/pipe/base/quantum_graph/aggregator/_communicators.py +204 -289
  39. lsst/pipe/base/quantum_graph/aggregator/_config.py +87 -9
  40. lsst/pipe/base/quantum_graph/aggregator/_ingester.py +13 -12
  41. lsst/pipe/base/quantum_graph/aggregator/_scanner.py +49 -235
  42. lsst/pipe/base/quantum_graph/aggregator/_structs.py +6 -116
  43. lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +29 -39
  44. lsst/pipe/base/quantum_graph/aggregator/_workers.py +303 -0
  45. lsst/pipe/base/quantum_graph/aggregator/_writer.py +34 -351
  46. lsst/pipe/base/quantum_graph/formatter.py +171 -0
  47. lsst/pipe/base/quantum_graph/ingest_graph.py +413 -0
  48. lsst/pipe/base/quantum_graph/visualization.py +5 -1
  49. lsst/pipe/base/quantum_graph_builder.py +33 -9
  50. lsst/pipe/base/quantum_graph_executor.py +116 -13
  51. lsst/pipe/base/quantum_graph_skeleton.py +31 -35
  52. lsst/pipe/base/quantum_provenance_graph.py +29 -12
  53. lsst/pipe/base/separable_pipeline_executor.py +19 -3
  54. lsst/pipe/base/single_quantum_executor.py +67 -42
  55. lsst/pipe/base/struct.py +4 -0
  56. lsst/pipe/base/testUtils.py +3 -3
  57. lsst/pipe/base/tests/mocks/_storage_class.py +2 -1
  58. lsst/pipe/base/version.py +1 -1
  59. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/METADATA +3 -3
  60. lsst_pipe_base-30.0.1.dist-info/RECORD +129 -0
  61. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/WHEEL +1 -1
  62. lsst_pipe_base-30.0.0rc2.dist-info/RECORD +0 -125
  63. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/entry_points.txt +0 -0
  64. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/COPYRIGHT +0 -0
  65. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/LICENSE +0 -0
  66. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/bsd_license.txt +0 -0
  67. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/gpl-v3.0.txt +0 -0
  68. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/top_level.txt +0 -0
  69. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/zip-safe +0 -0
@@ -33,7 +33,7 @@ import itertools
33
33
  import json
34
34
  import logging
35
35
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence, Set
36
- from typing import TYPE_CHECKING, Any, BinaryIO, Literal, TypeVar, cast
36
+ from typing import TYPE_CHECKING, Any, BinaryIO, Literal, cast
37
37
 
38
38
  import networkx
39
39
  import networkx.algorithms.bipartite
@@ -79,9 +79,6 @@ if TYPE_CHECKING:
79
79
  from ..pipeline import TaskDef
80
80
  from ..pipelineTask import PipelineTask
81
81
 
82
-
83
- _G = TypeVar("_G", bound=networkx.DiGraph | networkx.MultiDiGraph)
84
-
85
82
  _LOG = logging.getLogger("lsst.pipe.base.pipeline_graph")
86
83
 
87
84
 
@@ -897,6 +894,10 @@ class PipelineGraph:
897
894
  New config objects or overrides to apply to copies of the current
898
895
  config objects, with task labels as the keywords.
899
896
 
897
+ Returns
898
+ -------
899
+ None
900
+
900
901
  Raises
901
902
  ------
902
903
  ValueError
@@ -1632,7 +1633,7 @@ class PipelineGraph:
1632
1633
 
1633
1634
  Returns
1634
1635
  -------
1635
- subgraphs : `Iterable` [ `PipelineGraph` ]
1636
+ subgraphs : `~collections.abc.Iterable` [ `PipelineGraph` ]
1636
1637
  An iterable over component subgraphs that could be run
1637
1638
  independently (they have only overall inputs in common). May be a
1638
1639
  lazy iterator.
@@ -1755,6 +1756,10 @@ class PipelineGraph:
1755
1756
  not considered part of the pipeline graph in other respects, but it
1756
1757
  does get written with other provenance datasets).
1757
1758
 
1759
+ Returns
1760
+ -------
1761
+ None
1762
+
1758
1763
  Raises
1759
1764
  ------
1760
1765
  lsst.daf.butler.MissingDatasetTypeError
@@ -2179,7 +2184,9 @@ class PipelineGraph:
2179
2184
  ]
2180
2185
  return networkx.algorithms.bipartite.projected_graph(networkx.DiGraph(bipartite_xgraph), task_keys)
2181
2186
 
2182
- def _transform_xgraph_state(self, xgraph: _G, skip_edges: bool) -> _G:
2187
+ def _transform_xgraph_state[G: networkx.DiGraph | networkx.MultiDiGraph](
2188
+ self, xgraph: G, skip_edges: bool
2189
+ ) -> G:
2183
2190
  """Transform networkx graph attributes in-place from the internal
2184
2191
  "instance" attributes to the documented exported attributes.
2185
2192
 
@@ -2228,7 +2235,7 @@ class PipelineGraph:
2228
2235
 
2229
2236
  Parameters
2230
2237
  ----------
2231
- updates : `Mapping` [ `str`, `TaskNode` ]
2238
+ updates : `~collections.abc.Mapping` [ `str`, `TaskNode` ]
2232
2239
  New task nodes with task label keys. All keys must be task labels
2233
2240
  that are already present in the graph.
2234
2241
  check_edges_unchanged : `bool`, optional
@@ -43,7 +43,7 @@ __all__ = (
43
43
 
44
44
  import dataclasses
45
45
  import functools
46
- from typing import TYPE_CHECKING, Any, Literal, TypeAlias
46
+ from typing import TYPE_CHECKING, Any, Literal
47
47
 
48
48
  from lsst.daf.butler.queries.expressions.parser.ply import lex, yacc
49
49
 
@@ -268,4 +268,4 @@ def parse(expression: str) -> Node:
268
268
  return _ParserYacc().parse(expression)
269
269
 
270
270
 
271
- Node: TypeAlias = IdentifierNode | DirectionNode | NotNode | UnionNode | IntersectionNode
271
+ type Node = IdentifierNode | DirectionNode | NotNode | UnionNode | IntersectionNode
@@ -33,11 +33,10 @@ __all__ = (
33
33
  "SerializedTaskInitNode",
34
34
  "SerializedTaskNode",
35
35
  "SerializedTaskSubset",
36
- "expect_not_none",
37
36
  )
38
37
 
39
38
  from collections.abc import Mapping
40
- from typing import Any, TypeVar
39
+ from typing import Any
41
40
 
42
41
  import networkx
43
42
  import pydantic
@@ -53,14 +52,12 @@ from ._pipeline_graph import PipelineGraph
53
52
  from ._task_subsets import StepDefinitions, TaskSubset
54
53
  from ._tasks import TaskImportMode, TaskInitNode, TaskNode
55
54
 
56
- _U = TypeVar("_U")
57
-
58
55
  _IO_VERSION_INFO = (0, 0, 1)
59
56
  """Version tuple embedded in saved PipelineGraphs.
60
57
  """
61
58
 
62
59
 
63
- def expect_not_none(value: _U | None, msg: str) -> _U:
60
+ def _expect_not_none[U](value: U | None, msg: str) -> U:
64
61
  """Check that a value is not `None` and return it.
65
62
 
66
63
  Parameters
@@ -418,7 +415,7 @@ class SerializedTaskNode(pydantic.BaseModel):
418
415
  init = self.init.deserialize(
419
416
  init_key,
420
417
  task_class_name=self.task_class,
421
- config_str=expect_not_none(
418
+ config_str=_expect_not_none(
422
419
  self.config_str, f"No serialized config file for task with label {key.name!r}."
423
420
  ),
424
421
  dataset_type_keys=dataset_type_keys,
@@ -547,16 +544,16 @@ class SerializedDatasetTypeNode(pydantic.BaseModel):
547
544
  if self.dimensions is not None:
548
545
  dataset_type = DatasetType(
549
546
  key.name,
550
- expect_not_none(
547
+ _expect_not_none(
551
548
  self.dimensions,
552
549
  f"Serialized dataset type {key.name!r} has no dimensions.",
553
550
  ),
554
- storageClass=expect_not_none(
551
+ storageClass=_expect_not_none(
555
552
  self.storage_class,
556
553
  f"Serialized dataset type {key.name!r} has no storage class.",
557
554
  ),
558
555
  isCalibration=self.is_calibration,
559
- universe=expect_not_none(
556
+ universe=_expect_not_none(
560
557
  universe,
561
558
  f"Serialized dataset type {key.name!r} has dimensions, "
562
559
  "but no dimension universe was stored.",
@@ -747,7 +744,7 @@ class SerializedPipelineGraph(pydantic.BaseModel):
747
744
  if self.dimensions is not None:
748
745
  universe = DimensionUniverse(
749
746
  config=DimensionConfig(
750
- expect_not_none(
747
+ _expect_not_none(
751
748
  self.dimensions,
752
749
  "Serialized pipeline graph has not been resolved; "
753
750
  "load it is a MutablePipelineGraph instead.",
@@ -66,7 +66,7 @@ def show_dot(
66
66
  ----------
67
67
  pipeline_graph : `PipelineGraph`
68
68
  Pipeline graph to show.
69
- stream : `TextIO`, optional
69
+ stream : `io.TextIO`, optional
70
70
  Stream to write the DOT representation to.
71
71
  label_edge_connections : `bool`, optional
72
72
  If `True`, label edges with their connection names.
@@ -167,21 +167,22 @@ def _render_dataset_type_node(
167
167
 
168
168
  Parameters
169
169
  ----------
170
- node_key : NodeKey
171
- The key for the node
172
- node_data : Mapping[str, Any]
173
- The data associated with the node
174
- options : NodeAttributeOptions
175
- Options for rendering the node
176
- stream : TextIO
177
- The stream to write the node to
170
+ node_key : `NodeKey`
171
+ The key for the node.
172
+ node_data : `~collections.abc.Mapping` [`str`, `typing.Any`]
173
+ The data associated with the node.
174
+ options : `NodeAttributeOptions`
175
+ Options for rendering the node.
176
+ stream : `io.TextIO`
177
+ The stream to write the node to.
178
+ overflow_ref : `int`, optional
178
179
 
179
180
  Returns
180
181
  -------
181
182
  overflow_ref : int
182
- The reference number for the next overflow node
183
+ The reference number for the next overflow node.
183
184
  overflow_ids : str | None
184
- The ID of the overflow node, if any
185
+ The ID of the overflow node, if any.
185
186
  """
186
187
  labels, label_extras, common_prefix = _format_label(str(node_key), _LABEL_MAX_LINES_SOFT)
187
188
  if len(labels) + len(label_extras) <= _LABEL_MAX_LINES_HARD:
@@ -271,7 +272,7 @@ def _render_edge(from_node_id: str, to_node_id: str, stream: TextIO, **kwargs: A
271
272
  The unique ID of the node the edge is going to
272
273
  stream : TextIO
273
274
  The stream to write the edge to
274
- kwargs : Any
275
+ **kwargs : Any
275
276
  Additional keyword arguments to pass to the edge
276
277
  """
277
278
  if kwargs:
@@ -30,7 +30,7 @@ __all__ = ("ColumnSelector", "Layout", "LayoutRow")
30
30
 
31
31
  import dataclasses
32
32
  from collections.abc import Iterable, Iterator, Mapping, Set
33
- from typing import Generic, TextIO, TypeVar
33
+ from typing import TextIO
34
34
 
35
35
  import networkx
36
36
  import networkx.algorithms.components
@@ -38,10 +38,8 @@ import networkx.algorithms.dag
38
38
  import networkx.algorithms.shortest_paths
39
39
  import networkx.algorithms.traversal
40
40
 
41
- _K = TypeVar("_K")
42
41
 
43
-
44
- class Layout(Generic[_K]):
42
+ class Layout[K]:
45
43
  """A class that positions nodes and edges in text-art graph visualizations.
46
44
 
47
45
  Parameters
@@ -73,9 +71,9 @@ class Layout(Generic[_K]):
73
71
  # to be close to that text when possible (or maybe it's historical, and
74
72
  # it's just a lot of work to re-invert the algorithm now that it's
75
73
  # written).
76
- self._active_columns: dict[int, set[_K]] = {}
74
+ self._active_columns: dict[int, set[K]] = {}
77
75
  # Mapping from node key to its column.
78
- self._locations: dict[_K, int] = {}
76
+ self._locations: dict[K, int] = {}
79
77
  # Minimum and maximum column (may go negative; will be shifted as
80
78
  # needed before actual display).
81
79
  self._x_min = 0
@@ -116,7 +114,7 @@ class Layout(Generic[_K]):
116
114
  for component_xgraph, component_order in component_xgraphs_and_orders:
117
115
  self._add_connected_graph(component_xgraph, component_order)
118
116
 
119
- def _add_single_node(self, node: _K) -> None:
117
+ def _add_single_node(self, node: K) -> None:
120
118
  """Add a single node to the layout."""
121
119
  assert node not in self._locations
122
120
  if not self._locations:
@@ -184,7 +182,7 @@ class Layout(Generic[_K]):
184
182
  return x + 1
185
183
 
186
184
  def _add_connected_graph(
187
- self, xgraph: networkx.DiGraph | networkx.MultiDiGraph, order: list[_K] | None = None
185
+ self, xgraph: networkx.DiGraph | networkx.MultiDiGraph, order: list[K] | None = None
188
186
  ) -> None:
189
187
  """Add a subgraph whose nodes are connected.
190
188
 
@@ -202,7 +200,7 @@ class Layout(Generic[_K]):
202
200
  # "backbone" of our layout; we'll step through this path and add
203
201
  # recurse via calls to `_add_graph` on the nodes that we think should
204
202
  # go between the backbone nodes.
205
- backbone: list[_K] = networkx.algorithms.dag.dag_longest_path(xgraph, topo_order=order)
203
+ backbone: list[K] = networkx.algorithms.dag.dag_longest_path(xgraph, topo_order=order)
206
204
  # Add the first backbone node and any ancestors according to the full
207
205
  # graph (it can't have ancestors in this _subgraph_ because they'd have
208
206
  # been part of the longest path themselves, but the subgraph doesn't
@@ -237,7 +235,7 @@ class Layout(Generic[_K]):
237
235
  remaining.remove_nodes_from(self._locations.keys())
238
236
  self._add_graph(remaining)
239
237
 
240
- def _add_blockers_of(self, node: _K) -> None:
238
+ def _add_blockers_of(self, node: K) -> None:
241
239
  """Add all nodes that are ancestors of the given node according to the
242
240
  full graph.
243
241
  """
@@ -251,7 +249,7 @@ class Layout(Generic[_K]):
251
249
  return (self._x_max - self._x_min) // 2
252
250
 
253
251
  @property
254
- def nodes(self) -> Iterable[_K]:
252
+ def nodes(self) -> Iterable[K]:
255
253
  """The graph nodes in the order they appear in the layout."""
256
254
  return self._locations.keys()
257
255
 
@@ -277,7 +275,7 @@ class Layout(Generic[_K]):
277
275
  return (self._x_max - x) // 2
278
276
 
279
277
  def __iter__(self) -> Iterator[LayoutRow]:
280
- active_edges: dict[_K, set[_K]] = {}
278
+ active_edges: dict[K, set[K]] = {}
281
279
  for node, node_x in self._locations.items():
282
280
  row = LayoutRow(node, self._external_location(node_x))
283
281
  for origin, destinations in active_edges.items():
@@ -295,20 +293,20 @@ class Layout(Generic[_K]):
295
293
 
296
294
 
297
295
  @dataclasses.dataclass
298
- class LayoutRow(Generic[_K]):
296
+ class LayoutRow[K]:
299
297
  """Information about a single text-art row in a graph."""
300
298
 
301
- node: _K
299
+ node: K
302
300
  """Key for the node in the exported NetworkX graph."""
303
301
 
304
302
  x: int
305
303
  """Column of the node's symbol and its outgoing edges."""
306
304
 
307
- connecting: list[tuple[int, _K]] = dataclasses.field(default_factory=list)
305
+ connecting: list[tuple[int, K]] = dataclasses.field(default_factory=list)
308
306
  """The columns and node keys of edges that terminate at this row.
309
307
  """
310
308
 
311
- continuing: list[tuple[int, _K, frozenset[_K]]] = dataclasses.field(default_factory=list)
309
+ continuing: list[tuple[int, K, frozenset[K]]] = dataclasses.field(default_factory=list)
312
310
  """The columns and node keys of edges that continue through this row.
313
311
  """
314
312
 
@@ -337,11 +335,11 @@ class ColumnSelector:
337
335
  out in that case because it's applied to all candidate columns.
338
336
  """
339
337
 
340
- def __call__(
338
+ def __call__[K](
341
339
  self,
342
340
  connecting_x: list[int],
343
341
  node_x: int,
344
- active_columns: Mapping[int, Set[_K]],
342
+ active_columns: Mapping[int, Set[K]],
345
343
  x_min: int,
346
344
  x_max: int,
347
345
  ) -> int:
@@ -38,7 +38,7 @@ import hashlib
38
38
  from collections import defaultdict
39
39
  from collections.abc import Iterable
40
40
  from functools import cached_property
41
- from typing import Any, TypeVar
41
+ from typing import Any
42
42
 
43
43
  import networkx
44
44
  import networkx.algorithms.dag
@@ -49,9 +49,6 @@ from lsst.daf.butler import DimensionGroup
49
49
  from .._nodes import NodeKey, NodeType
50
50
  from ._options import NodeAttributeOptions
51
51
 
52
- _P = TypeVar("_P")
53
- _C = TypeVar("_C")
54
-
55
52
 
56
53
  class MergedNodeKey(frozenset[NodeKey]):
57
54
  """A key for NetworkX graph nodes that represent multiple similar tasks
@@ -225,11 +222,11 @@ class _MergeKey:
225
222
  """
226
223
 
227
224
  @classmethod
228
- def from_node_state(
225
+ def from_node_state[P, C](
229
226
  cls,
230
227
  state: dict[str, Any],
231
- parents: Iterable[_P],
232
- children: Iterable[_C],
228
+ parents: Iterable[P],
229
+ children: Iterable[C],
233
230
  options: NodeAttributeOptions,
234
231
  ) -> _MergeKey:
235
232
  """Construct from a NetworkX node attribute state dictionary.
@@ -30,9 +30,9 @@ __all__ = ("Printer", "make_colorama_printer", "make_default_printer", "make_sim
30
30
 
31
31
  import sys
32
32
  from collections.abc import Callable, Sequence
33
- from typing import Generic, TextIO
33
+ from typing import TextIO
34
34
 
35
- from ._layout import _K, Layout, LayoutRow
35
+ from ._layout import Layout, LayoutRow
36
36
 
37
37
  _CHAR_DECOMPOSITION = {
38
38
  # This mapping provides the "logic" for how to decompose the relevant
@@ -170,7 +170,7 @@ class PrintRow:
170
170
  return "".join(self._cells)
171
171
 
172
172
 
173
- def _default_get_text(node: _K, x: int, style: tuple[str, str]) -> str:
173
+ def _default_get_text[K](node: K, x: int, style: tuple[str, str]) -> str:
174
174
  """Return the default text to associate with a node.
175
175
 
176
176
  This function is the default value for the ``get_text`` argument to
@@ -179,7 +179,7 @@ def _default_get_text(node: _K, x: int, style: tuple[str, str]) -> str:
179
179
  return str(node)
180
180
 
181
181
 
182
- def _default_get_symbol(node: _K, x: int) -> str:
182
+ def _default_get_symbol[K](node: K, x: int) -> str:
183
183
  """Return the default symbol for a node.
184
184
 
185
185
  This function is the default value for the ``get_symbol`` argument to
@@ -188,7 +188,7 @@ def _default_get_symbol(node: _K, x: int) -> str:
188
188
  return "⬤"
189
189
 
190
190
 
191
- def _default_get_style(node: _K, x: int) -> tuple[str, str]:
191
+ def _default_get_style[K](node: K, x: int) -> tuple[str, str]:
192
192
  """Get the default styling suffix/prefix strings.
193
193
 
194
194
  This function is the default value for the ``get_style`` argument to
@@ -197,7 +197,7 @@ def _default_get_style(node: _K, x: int) -> tuple[str, str]:
197
197
  return "", ""
198
198
 
199
199
 
200
- class Printer(Generic[_K]):
200
+ class Printer[K]:
201
201
  """High-level tool for drawing a text-based DAG visualization.
202
202
 
203
203
  Parameters
@@ -231,9 +231,9 @@ class Printer(Generic[_K]):
231
231
  *,
232
232
  pad: str = " ",
233
233
  make_blank_row: Callable[[int, str], PrintRow] = PrintRow,
234
- get_text: Callable[[_K, int, tuple[str, str]], str] = _default_get_text,
235
- get_symbol: Callable[[_K, int], str] = _default_get_symbol,
236
- get_style: Callable[[_K, int], tuple[str, str]] = _default_get_style,
234
+ get_text: Callable[[K, int, tuple[str, str]], str] = _default_get_text,
235
+ get_symbol: Callable[[K, int], str] = _default_get_symbol,
236
+ get_style: Callable[[K, int], tuple[str, str]] = _default_get_style,
237
237
  ):
238
238
  self.width = layout_width * 2 + 1
239
239
  self.pad = pad
@@ -245,7 +245,7 @@ class Printer(Generic[_K]):
245
245
  def print_row(
246
246
  self,
247
247
  stream: TextIO,
248
- layout_row: LayoutRow[_K],
248
+ layout_row: LayoutRow[K],
249
249
  ) -> None:
250
250
  """Print a single row of the DAG visualization to a file-like object.
251
251
 
@@ -200,6 +200,13 @@ class QuantumGraphExecutionStatusAnnotator:
200
200
  """Annotates a networkx graph with task and dataset status information from
201
201
  a quantum graph execution summary, implementing the StatusAnnotator
202
202
  protocol to update the graph with status data.
203
+
204
+ Parameters
205
+ ----------
206
+ *args : `typing.Any`
207
+ Arbitrary arguments.
208
+ **kwargs : `typing.Any`
209
+ Arbitrary keyword arguments.
203
210
  """
204
211
 
205
212
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -252,7 +252,8 @@ class PrerequisiteFinder:
252
252
  Sequence of collections to search, in order.
253
253
  data_id : `lsst.daf.butler.DataCoordinate`
254
254
  Data ID for the quantum.
255
- skypix_bounds : `Mapping` [ `str`, `lsst.sphgeom.RangeSet` ]
255
+ skypix_bounds : `~collections.abc.Mapping` \
256
+ [ `str`, `lsst.sphgeom.RangeSet` ]
256
257
  The spatial bounds of this quantum in various skypix dimensions.
257
258
  Keys are skypix dimension names (a superset of those in
258
259
  `dataset_skypix`) and values are sets of integer pixel ID ranges.
@@ -50,9 +50,7 @@ from typing import (
50
50
  TYPE_CHECKING,
51
51
  Any,
52
52
  Self,
53
- TypeAlias,
54
53
  TypedDict,
55
- TypeVar,
56
54
  )
57
55
 
58
56
  import networkx
@@ -81,18 +79,16 @@ if TYPE_CHECKING:
81
79
  # These aliases make it a lot easier how the various pydantic models are
82
80
  # structured, but they're too verbose to be worth exporting to code outside the
83
81
  # quantum_graph subpackage.
84
- TaskLabel: TypeAlias = str
85
- DatasetTypeName: TypeAlias = str
86
- ConnectionName: TypeAlias = str
87
- DatasetIndex: TypeAlias = int
88
- QuantumIndex: TypeAlias = int
89
- DatastoreName: TypeAlias = str
90
- DimensionElementName: TypeAlias = str
91
- DataCoordinateValues: TypeAlias = list[DataIdValue]
82
+ type TaskLabel = str
83
+ type DatasetTypeName = str
84
+ type ConnectionName = str
85
+ type DatasetIndex = int
86
+ type QuantumIndex = int
87
+ type DatastoreName = str
88
+ type DimensionElementName = str
89
+ type DataCoordinateValues = list[DataIdValue]
92
90
 
93
91
 
94
- _T = TypeVar("_T", bound=pydantic.BaseModel)
95
-
96
92
  FORMAT_VERSION: int = 1
97
93
  """
98
94
  File format version number for new files.
@@ -448,14 +444,17 @@ class BaseQuantumGraphWriter:
448
444
  uri: ResourcePathExpression,
449
445
  header: HeaderModel,
450
446
  pipeline_graph: PipelineGraph,
451
- indices: dict[uuid.UUID, int],
452
447
  *,
453
448
  address_filename: str,
454
- compressor: Compressor,
455
449
  cdict_data: bytes | None = None,
450
+ zstd_level: int = 10,
456
451
  ) -> Iterator[Self]:
457
- uri = ResourcePath(uri)
458
- address_writer = AddressWriter(indices)
452
+ uri = ResourcePath(uri, forceDirectory=False)
453
+ address_writer = AddressWriter()
454
+ if uri.isLocal:
455
+ os.makedirs(uri.dirname().ospath, exist_ok=True)
456
+ cdict = zstandard.ZstdCompressionDict(cdict_data) if cdict_data is not None else None
457
+ compressor = zstandard.ZstdCompressor(level=zstd_level, dict_data=cdict)
459
458
  with uri.open(mode="wb") as stream:
460
459
  with zipfile.ZipFile(stream, mode="w", compression=zipfile.ZIP_STORED) as zf:
461
460
  self = cls(zf, compressor, address_writer, header.int_size)
@@ -594,9 +593,9 @@ class BaseQuantumGraphReader:
594
593
  )
595
594
 
596
595
  @staticmethod
597
- def _read_single_block_static(
598
- name: str, model_type: type[_T], zf: zipfile.ZipFile, decompressor: Decompressor
599
- ) -> _T:
596
+ def _read_single_block_static[T: pydantic.BaseModel](
597
+ name: str, model_type: type[T], zf: zipfile.ZipFile, decompressor: Decompressor
598
+ ) -> T:
600
599
  """Read a single compressed JSON block from a 'file' in a zip archive.
601
600
 
602
601
  Parameters
@@ -619,7 +618,7 @@ class BaseQuantumGraphReader:
619
618
  json_data = decompressor.decompress(compressed_data)
620
619
  return model_type.model_validate_json(json_data)
621
620
 
622
- def _read_single_block(self, name: str, model_type: type[_T]) -> _T:
621
+ def _read_single_block[T: pydantic.BaseModel](self, name: str, model_type: type[T]) -> T:
623
622
  """Read a single compressed JSON block from a 'file' in a zip archive.
624
623
 
625
624
  Parameters
@@ -43,25 +43,22 @@ import dataclasses
43
43
  import logging
44
44
  import tempfile
45
45
  import uuid
46
- from collections.abc import Iterator
46
+ import zipfile
47
+ from collections.abc import Iterator, Set
47
48
  from contextlib import contextmanager
48
49
  from io import BufferedReader, BytesIO
49
50
  from operator import attrgetter
50
- from typing import IO, TYPE_CHECKING, Protocol, TypeAlias, TypeVar
51
+ from typing import IO, Protocol, TypeVar
51
52
 
52
53
  import pydantic
53
54
 
54
- if TYPE_CHECKING:
55
- import zipfile
56
-
57
-
58
55
  _LOG = logging.getLogger(__name__)
59
56
 
60
57
 
61
58
  _T = TypeVar("_T", bound=pydantic.BaseModel)
62
59
 
63
60
 
64
- UUID_int: TypeAlias = int
61
+ type UUID_int = int
65
62
 
66
63
  MAX_UUID_INT: UUID_int = 2**128
67
64
 
@@ -77,7 +74,7 @@ individual quanta (especially for execution).
77
74
 
78
75
 
79
76
  class Compressor(Protocol):
80
- """A protocol for objects with a `compress` method that takes and returns
77
+ """A protocol for objects with a ``compress`` method that takes and returns
81
78
  `bytes`.
82
79
  """
83
80
 
@@ -205,21 +202,14 @@ class AddressRow:
205
202
  class AddressWriter:
206
203
  """A helper object for writing address files for multi-block files."""
207
204
 
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
205
  addresses: list[dict[uuid.UUID, Address]] = dataclasses.field(default_factory=list)
216
206
  """Addresses to store with each UUID.
217
207
 
218
- Every key in one of these dictionaries must have an entry in `indices`.
208
+ Every key in one of these dictionaries must have an entry in ``indices``.
219
209
  The converse is not true.
220
210
  """
221
211
 
222
- def write(self, stream: IO[bytes], int_size: int) -> None:
212
+ def write(self, stream: IO[bytes], int_size: int, all_ids: Set[uuid.UUID] | None = None) -> None:
223
213
  """Write all addresses to a file-like object.
224
214
 
225
215
  Parameters
@@ -228,19 +218,18 @@ class AddressWriter:
228
218
  Binary file-like object.
229
219
  int_size : `int`
230
220
  Number of bytes to use for all integers.
221
+ all_ids : `~collections.abc.Set` [`uuid.UUID`], optional
222
+ Set of the union of all UUIDs in any dictionary from a call to
223
+ `get_all_ids`.
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
+ if all_ids is None:
226
+ all_ids = self.get_all_ids()
238
227
  stream.write(int_size.to_bytes(1))
239
- stream.write(len(self.indices).to_bytes(int_size))
228
+ stream.write(len(all_ids).to_bytes(int_size))
240
229
  stream.write(len(self.addresses).to_bytes(int_size))
241
230
  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])
231
+ for n, key in enumerate(sorted(all_ids, key=attrgetter("int"))):
232
+ row = AddressRow(key, n, [m.get(key, empty_address) for m in self.addresses])
244
233
  _LOG.debug("Wrote address %s.", row)
245
234
  row.write(stream, int_size)
246
235
 
@@ -256,8 +245,25 @@ class AddressWriter:
256
245
  int_size : `int`
257
246
  Number of bytes to use for all integers.
258
247
  """
259
- with zf.open(f"{name}.addr", mode="w") as stream:
260
- self.write(stream, int_size=int_size)
248
+ all_ids = self.get_all_ids()
249
+ zip_info = zipfile.ZipInfo(f"{name}.addr")
250
+ row_size = AddressReader.compute_row_size(int_size, len(self.addresses))
251
+ zip_info.file_size = AddressReader.compute_header_size(int_size) + len(all_ids) * row_size
252
+ with zf.open(zip_info, mode="w") as stream:
253
+ self.write(stream, int_size=int_size, all_ids=all_ids)
254
+
255
+ def get_all_ids(self) -> Set[uuid.UUID]:
256
+ """Return all IDs used by any address dictionary.
257
+
258
+ Returns
259
+ -------
260
+ all_ids : `~collections.abc.Set` [`uuid.UUID`]
261
+ Set of all IDs.
262
+ """
263
+ all_ids: set[uuid.UUID] = set()
264
+ for address_map in self.addresses:
265
+ all_ids.update(address_map.keys())
266
+ return all_ids
261
267
 
262
268
 
263
269
  @dataclasses.dataclass
@@ -656,7 +662,7 @@ class MultiblockWriter:
656
662
  model : `pydantic.BaseModel`
657
663
  Model to convert to JSON and compress.
658
664
  compressor : `Compressor`
659
- Object with a `compress` method that takes and returns `bytes`.
665
+ Object with a ``compress`` method that takes and returns `bytes`.
660
666
 
661
667
  Returns
662
668
  -------
@@ -753,7 +759,7 @@ class MultiblockReader:
753
759
  model_type : `type` [ `pydantic.BaseModel` ]
754
760
  Pydantic model to validate JSON with.
755
761
  decompressor : `Decompressor`
756
- Object with a `decompress` method that takes and returns `bytes`.
762
+ Object with a ``decompress`` method that takes and returns `bytes`.
757
763
  int_size : `int`
758
764
  Number of bytes to use for all integers.
759
765
  page_size : `int`
@@ -803,7 +809,7 @@ class MultiblockReader:
803
809
  model_type : `type` [ `pydantic.BaseModel` ]
804
810
  Pydantic model to validate JSON with.
805
811
  decompressor : `Decompressor`
806
- Object with a `decompress` method that takes and returns `bytes`.
812
+ Object with a ``decompress`` method that takes and returns `bytes`.
807
813
 
808
814
  Returns
809
815
  -------