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.
- lsst/pipe/base/_instrument.py +31 -20
- lsst/pipe/base/_quantumContext.py +3 -3
- lsst/pipe/base/_status.py +43 -10
- lsst/pipe/base/_task_metadata.py +2 -2
- lsst/pipe/base/all_dimensions_quantum_graph_builder.py +8 -3
- lsst/pipe/base/automatic_connection_constants.py +20 -1
- lsst/pipe/base/cli/cmd/__init__.py +18 -2
- lsst/pipe/base/cli/cmd/commands.py +149 -4
- lsst/pipe/base/connectionTypes.py +72 -160
- lsst/pipe/base/connections.py +6 -9
- lsst/pipe/base/execution_reports.py +0 -5
- lsst/pipe/base/graph/graph.py +11 -10
- lsst/pipe/base/graph/quantumNode.py +4 -4
- lsst/pipe/base/graph_walker.py +8 -10
- lsst/pipe/base/log_capture.py +40 -80
- lsst/pipe/base/log_on_close.py +76 -0
- lsst/pipe/base/mp_graph_executor.py +51 -15
- lsst/pipe/base/pipeline.py +5 -6
- lsst/pipe/base/pipelineIR.py +2 -8
- lsst/pipe/base/pipelineTask.py +5 -7
- lsst/pipe/base/pipeline_graph/_dataset_types.py +2 -2
- lsst/pipe/base/pipeline_graph/_edges.py +32 -22
- lsst/pipe/base/pipeline_graph/_mapping_views.py +4 -7
- lsst/pipe/base/pipeline_graph/_pipeline_graph.py +14 -7
- lsst/pipe/base/pipeline_graph/expressions.py +2 -2
- lsst/pipe/base/pipeline_graph/io.py +7 -10
- lsst/pipe/base/pipeline_graph/visualization/_dot.py +13 -12
- lsst/pipe/base/pipeline_graph/visualization/_layout.py +16 -18
- lsst/pipe/base/pipeline_graph/visualization/_merge.py +4 -7
- lsst/pipe/base/pipeline_graph/visualization/_printer.py +10 -10
- lsst/pipe/base/pipeline_graph/visualization/_status_annotator.py +7 -0
- lsst/pipe/base/prerequisite_helpers.py +2 -1
- lsst/pipe/base/quantum_graph/_common.py +19 -20
- lsst/pipe/base/quantum_graph/_multiblock.py +37 -31
- lsst/pipe/base/quantum_graph/_predicted.py +113 -15
- lsst/pipe/base/quantum_graph/_provenance.py +1136 -45
- lsst/pipe/base/quantum_graph/aggregator/__init__.py +0 -1
- lsst/pipe/base/quantum_graph/aggregator/_communicators.py +204 -289
- lsst/pipe/base/quantum_graph/aggregator/_config.py +87 -9
- lsst/pipe/base/quantum_graph/aggregator/_ingester.py +13 -12
- lsst/pipe/base/quantum_graph/aggregator/_scanner.py +49 -235
- lsst/pipe/base/quantum_graph/aggregator/_structs.py +6 -116
- lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +29 -39
- lsst/pipe/base/quantum_graph/aggregator/_workers.py +303 -0
- lsst/pipe/base/quantum_graph/aggregator/_writer.py +34 -351
- lsst/pipe/base/quantum_graph/formatter.py +171 -0
- lsst/pipe/base/quantum_graph/ingest_graph.py +413 -0
- lsst/pipe/base/quantum_graph/visualization.py +5 -1
- lsst/pipe/base/quantum_graph_builder.py +33 -9
- lsst/pipe/base/quantum_graph_executor.py +116 -13
- lsst/pipe/base/quantum_graph_skeleton.py +31 -35
- lsst/pipe/base/quantum_provenance_graph.py +29 -12
- lsst/pipe/base/separable_pipeline_executor.py +19 -3
- lsst/pipe/base/single_quantum_executor.py +67 -42
- lsst/pipe/base/struct.py +4 -0
- lsst/pipe/base/testUtils.py +3 -3
- lsst/pipe/base/tests/mocks/_storage_class.py +2 -1
- lsst/pipe/base/version.py +1 -1
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/METADATA +3 -3
- lsst_pipe_base-30.0.1.dist-info/RECORD +129 -0
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/WHEEL +1 -1
- lsst_pipe_base-30.0.0rc2.dist-info/RECORD +0 -125
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/entry_points.txt +0 -0
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/LICENSE +0 -0
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/bsd_license.txt +0 -0
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/licenses/gpl-v3.0.txt +0 -0
- {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.1.dist-info}/top_level.txt +0 -0
- {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,
|
|
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 :
|
|
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
|
|
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 :
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
|
|
547
|
+
_expect_not_none(
|
|
551
548
|
self.dimensions,
|
|
552
549
|
f"Serialized dataset type {key.name!r} has no dimensions.",
|
|
553
550
|
),
|
|
554
|
-
storageClass=
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
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[
|
|
74
|
+
self._active_columns: dict[int, set[K]] = {}
|
|
77
75
|
# Mapping from node key to its column.
|
|
78
|
-
self._locations: dict[
|
|
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:
|
|
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[
|
|
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[
|
|
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:
|
|
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[
|
|
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[
|
|
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
|
|
296
|
+
class LayoutRow[K]:
|
|
299
297
|
"""Information about a single text-art row in a graph."""
|
|
300
298
|
|
|
301
|
-
node:
|
|
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,
|
|
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,
|
|
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[
|
|
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
|
|
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[
|
|
232
|
-
children: Iterable[
|
|
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
|
|
33
|
+
from typing import TextIO
|
|
34
34
|
|
|
35
|
-
from ._layout import
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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[[
|
|
235
|
-
get_symbol: Callable[[
|
|
236
|
-
get_style: Callable[[
|
|
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[
|
|
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 :
|
|
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
|
|
85
|
-
DatasetTypeName
|
|
86
|
-
ConnectionName
|
|
87
|
-
DatasetIndex
|
|
88
|
-
QuantumIndex
|
|
89
|
-
DatastoreName
|
|
90
|
-
DimensionElementName
|
|
91
|
-
DataCoordinateValues
|
|
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(
|
|
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[
|
|
599
|
-
) ->
|
|
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[
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
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(
|
|
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(
|
|
243
|
-
row = AddressRow(key,
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|
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
|
|
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
|
|
812
|
+
Object with a ``decompress`` method that takes and returns `bytes`.
|
|
807
813
|
|
|
808
814
|
Returns
|
|
809
815
|
-------
|