lsst-pipe-base 29.2025.2900__py3-none-any.whl → 29.2025.3100__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 (27) hide show
  1. lsst/pipe/base/_datasetQueryConstraints.py +1 -1
  2. lsst/pipe/base/all_dimensions_quantum_graph_builder.py +6 -4
  3. lsst/pipe/base/connectionTypes.py +19 -19
  4. lsst/pipe/base/connections.py +2 -2
  5. lsst/pipe/base/exec_fixup_data_id.py +131 -0
  6. lsst/pipe/base/execution_graph_fixup.py +69 -0
  7. lsst/pipe/base/log_capture.py +227 -0
  8. lsst/pipe/base/mp_graph_executor.py +774 -0
  9. lsst/pipe/base/quantum_graph_builder.py +43 -42
  10. lsst/pipe/base/quantum_graph_executor.py +125 -0
  11. lsst/pipe/base/quantum_reports.py +334 -0
  12. lsst/pipe/base/script/transfer_from_graph.py +41 -29
  13. lsst/pipe/base/separable_pipeline_executor.py +296 -0
  14. lsst/pipe/base/simple_pipeline_executor.py +674 -0
  15. lsst/pipe/base/single_quantum_executor.py +636 -0
  16. lsst/pipe/base/taskFactory.py +18 -12
  17. lsst/pipe/base/version.py +1 -1
  18. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/METADATA +1 -1
  19. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/RECORD +27 -18
  20. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/WHEEL +0 -0
  21. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/entry_points.txt +0 -0
  22. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/licenses/COPYRIGHT +0 -0
  23. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/licenses/LICENSE +0 -0
  24. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/licenses/bsd_license.txt +0 -0
  25. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/licenses/gpl-v3.0.txt +0 -0
  26. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/top_level.txt +0 -0
  27. {lsst_pipe_base-29.2025.2900.dist-info → lsst_pipe_base-29.2025.3100.dist-info}/zip-safe +0 -0
@@ -26,7 +26,7 @@
26
26
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
 
28
28
  """Symbols defined in this package should be imported from
29
- `all_dimensions_quantum_graph_builder` instead; it only appears in the docs
29
+ `.all_dimensions_quantum_graph_builder` instead; it only appears in the docs
30
30
  due to limitations in Sphinx.
31
31
  """
32
32
 
@@ -65,13 +65,14 @@ if TYPE_CHECKING:
65
65
 
66
66
  @final
67
67
  class AllDimensionsQuantumGraphBuilder(QuantumGraphBuilder):
68
- """An implementation of `QuantumGraphBuilder` that uses a single large
69
- query for data IDs covering all dimensions in the pipeline.
68
+ """An implementation of `.quantum_graph_builder.QuantumGraphBuilder` that
69
+ uses a single large query for data IDs covering all dimensions in the
70
+ pipeline.
70
71
 
71
72
  Parameters
72
73
  ----------
73
74
  pipeline_graph : `.pipeline_graph.PipelineGraph`
74
- Pipeline to build a `QuantumGraph` from, as a graph. Will be resolved
75
+ Pipeline to build a `.QuantumGraph` from, as a graph. Will be resolved
75
76
  in-place with the given butler (any existing resolution is ignored).
76
77
  butler : `lsst.daf.butler.Butler`
77
78
  Client for the data repository. Should be read-only.
@@ -92,7 +93,8 @@ class AllDimensionsQuantumGraphBuilder(QuantumGraphBuilder):
92
93
  are constrained by the ``where`` argument or pipeline data ID will be
93
94
  filled in automatically.
94
95
  **kwargs
95
- Additional keyword arguments forwarded to `QuantumGraphBuilder`.
96
+ Additional keyword arguments forwarded to
97
+ `.quantum_graph_builder.QuantumGraphBuilder`.
96
98
 
97
99
  Notes
98
100
  -----
@@ -26,7 +26,7 @@
26
26
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
 
28
28
  """Module defining connection types to be used within a
29
- `PipelineTaskConnections` class.
29
+ `.PipelineTaskConnections` class.
30
30
  """
31
31
 
32
32
  __all__ = ["BaseConnection", "InitInput", "InitOutput", "Input", "Output", "PrerequisiteInput"]
@@ -53,7 +53,7 @@ class BaseConnection:
53
53
  Indicates if this connection should expect to contain multiple objects
54
54
  of the given dataset type. Tasks with more than one connection with
55
55
  ``multiple=True`` with the same dimensions may want to implement
56
- `PipelineTaskConnections.adjustQuantum` to ensure those datasets are
56
+ `.PipelineTaskConnections.adjustQuantum` to ensure those datasets are
57
57
  consistent (i.e. zip-iterable) in `PipelineTask.runQuantum()` and
58
58
  notify the execution system as early as possible of outputs that will
59
59
  not be produced because the corresponding input is missing.
@@ -121,7 +121,7 @@ class DimensionedConnection(BaseConnection):
121
121
  Indicates if this connection should expect to contain multiple objects
122
122
  of the given dataset type. Tasks with more than one connection with
123
123
  ``multiple=True`` with the same dimensions may want to implement
124
- `PipelineTaskConnections.adjustQuantum` to ensure those datasets are
124
+ `.PipelineTaskConnections.adjustQuantum` to ensure those datasets are
125
125
  consistent (i.e. zip-iterable) in `PipelineTask.runQuantum` and notify
126
126
  the execution system as early as possible of outputs that will not be
127
127
  produced because the corresponding input is missing.
@@ -161,7 +161,7 @@ class BaseInput(DimensionedConnection):
161
161
  Indicates if this connection should expect to contain multiple objects
162
162
  of the given dataset type. Tasks with more than one connection with
163
163
  ``multiple=True`` with the same dimensions may want to implement
164
- `PipelineTaskConnections.adjustQuantum` to ensure those datasets are
164
+ `.PipelineTaskConnections.adjustQuantum` to ensure those datasets are
165
165
  consistent (i.e. zip-iterable) in `PipelineTask.runQuantum` and notify
166
166
  the execution system as early as possible of outputs that will not be
167
167
  produced because the corresponding input is missing.
@@ -175,14 +175,14 @@ class BaseInput(DimensionedConnection):
175
175
  minimum : `bool`
176
176
  Minimum number of datasets required for this connection, per quantum.
177
177
  This is checked in the base implementation of
178
- `PipelineTaskConnections.adjustQuantum`, which raises `NoWorkFound` if
178
+ `.PipelineTaskConnections.adjustQuantum`, which raises `NoWorkFound` if
179
179
  the minimum is not met for `Input` connections (causing the quantum to
180
180
  be pruned, skipped, or never created, depending on the context), and
181
181
  `FileNotFoundError` for `PrerequisiteInput` connections (causing
182
182
  QuantumGraph generation to fail). `PipelineTask` implementations may
183
- provide custom `~PipelineTaskConnections.adjustQuantum` implementations
184
- for more fine-grained or configuration-driven constraints, as long as
185
- they are compatible with this minium.
183
+ provide custom `~.PipelineTaskConnections.adjustQuantum`
184
+ implementations for more fine-grained or configuration-driven
185
+ constraints, as long as they are compatible with this minium.
186
186
 
187
187
  Raises
188
188
  ------
@@ -216,7 +216,7 @@ class Input(BaseInput):
216
216
  Indicates if this connection should expect to contain multiple objects
217
217
  of the given dataset type. Tasks with more than one connection with
218
218
  ``multiple=True`` with the same dimensions may want to implement
219
- `PipelineTaskConnections.adjustQuantum` to ensure those datasets are
219
+ `.PipelineTaskConnections.adjustQuantum` to ensure those datasets are
220
220
  consistent (i.e. zip-iterable) in `PipelineTask.runQuantum` and notify
221
221
  the execution system as early as possible of outputs that will not be
222
222
  produced because the corresponding input is missing.
@@ -230,14 +230,14 @@ class Input(BaseInput):
230
230
  minimum : `bool`
231
231
  Minimum number of datasets required for this connection, per quantum.
232
232
  This is checked in the base implementation of
233
- `PipelineTaskConnections.adjustQuantum`, which raises `NoWorkFound` if
233
+ `.PipelineTaskConnections.adjustQuantum`, which raises `NoWorkFound` if
234
234
  the minimum is not met for `Input` connections (causing the quantum to
235
235
  be pruned, skipped, or never created, depending on the context), and
236
236
  `FileNotFoundError` for `PrerequisiteInput` connections (causing
237
237
  QuantumGraph generation to fail). `PipelineTask` implementations may
238
- provide custom `~PipelineTaskConnections.adjustQuantum` implementations
239
- for more fine-grained or configuration-driven constraints, as long as
240
- they are compatible with this minium.
238
+ provide custom `~.PipelineTaskConnections.adjustQuantum`
239
+ implementations for more fine-grained or configuration-driven
240
+ constraints, as long as they are compatible with this minium.
241
241
  deferGraphConstraint : `bool`, optional
242
242
  If `True`, do not include this dataset type's existence in the initial
243
243
  query that starts the QuantumGraph generation process. This can be
@@ -286,7 +286,7 @@ class PrerequisiteInput(BaseInput):
286
286
  Indicates if this connection should expect to contain multiple objects
287
287
  of the given dataset type. Tasks with more than one connection with
288
288
  ``multiple=True`` with the same dimensions may want to implement
289
- `PipelineTaskConnections.adjustQuantum` to ensure those datasets are
289
+ `.PipelineTaskConnections.adjustQuantum` to ensure those datasets are
290
290
  consistent (i.e. zip-iterable) in `PipelineTask.runQuantum` and notify
291
291
  the execution system as early as possible of outputs that will not be
292
292
  produced because the corresponding input is missing.
@@ -296,12 +296,12 @@ class PrerequisiteInput(BaseInput):
296
296
  minimum : `bool`
297
297
  Minimum number of datasets required for this connection, per quantum.
298
298
  This is checked in the base implementation of
299
- `PipelineTaskConnections.adjustQuantum`, which raises
299
+ `.PipelineTaskConnections.adjustQuantum`, which raises
300
300
  `FileNotFoundError` (causing QuantumGraph generation to fail).
301
- `PipelineTask` implementations may
302
- provide custom `~PipelineTaskConnections.adjustQuantum` implementations
303
- for more fine-grained or configuration-driven constraints, as long as
304
- they are compatible with this minium.
301
+ `PipelineTask` implementations may provide custom
302
+ `~.PipelineTaskConnections.adjustQuantum` implementations for more
303
+ fine-grained or configuration-driven constraints, as long as they are
304
+ compatible with this minium.
305
305
  lookupFunction : `typing.Callable`, optional
306
306
  An optional callable function that will look up PrerequisiteInputs
307
307
  using the DatasetType, registry, quantum dataId, and input collections
@@ -1063,8 +1063,8 @@ def iterConnections(
1063
1063
  class AdjustQuantumHelper:
1064
1064
  """Helper class for calling `PipelineTaskConnections.adjustQuantum`.
1065
1065
 
1066
- This class holds `input` and `output` mappings in the form used by
1067
- `Quantum` and execution harness code, i.e. with
1066
+ This class holds `inputs` and `outputs` mappings in the form used by
1067
+ `lsst.daf.butler.Quantum` and execution harness code, i.e. with
1068
1068
  `~lsst.daf.butler.DatasetType` keys, translating them to and from the
1069
1069
  connection-oriented mappings used inside `PipelineTaskConnections`.
1070
1070
  """
@@ -0,0 +1,131 @@
1
+ # This file is part of pipe_base.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (http://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # This software is dual licensed under the GNU General Public License and also
10
+ # under a 3-clause BSD license. Recipients may choose which of these licenses
11
+ # to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12
+ # respectively. If you choose the GPL option then the following text applies
13
+ # (but note that there is still no warranty even if you opt for BSD instead):
14
+ #
15
+ # This program is free software: you can redistribute it and/or modify
16
+ # it under the terms of the GNU General Public License as published by
17
+ # the Free Software Foundation, either version 3 of the License, or
18
+ # (at your option) any later version.
19
+ #
20
+ # This program is distributed in the hope that it will be useful,
21
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ # GNU General Public License for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+
28
+ __all__ = ["ExecutionGraphFixup"]
29
+
30
+ import contextlib
31
+ import itertools
32
+ from collections import defaultdict
33
+ from collections.abc import Sequence
34
+ from typing import Any
35
+
36
+ import networkx as nx
37
+
38
+ from .execution_graph_fixup import ExecutionGraphFixup
39
+ from .graph import QuantumGraph, QuantumNode
40
+
41
+
42
+ class ExecFixupDataId(ExecutionGraphFixup):
43
+ """Implementation of ExecutionGraphFixup for ordering of tasks based
44
+ on DataId values.
45
+
46
+ This class is a trivial implementation mostly useful as an example,
47
+ though it can be used to make actual fixup instances by defining
48
+ a method that instantiates it, e.g.::
49
+
50
+ # lsst/ap/verify/ci_fixup.py
51
+
52
+ from lsst.pipe.base.exec_fixup_data_id import ExecFixupDataId
53
+
54
+
55
+ def assoc_fixup():
56
+ return ExecFixupDataId(
57
+ taskLabel="ap_assoc", dimensions=("visit", "detector")
58
+ )
59
+
60
+ and then executing pipetask::
61
+
62
+ pipetask run --graph-fixup=lsst.ap.verify.ci_fixup.assoc_fixup ...
63
+
64
+ This will add new dependencies between quanta executed by the task with
65
+ label "ap_assoc". Quanta with higher visit number will depend on quanta
66
+ with lower visit number and their execution will wait until lower visit
67
+ number finishes.
68
+
69
+ Parameters
70
+ ----------
71
+ taskLabel : `str`
72
+ The label of the task for which to add dependencies.
73
+ dimensions : `str` or sequence [`str`]
74
+ One or more dimension names, quanta execution will be ordered
75
+ according to values of these dimensions.
76
+ reverse : `bool`, optional
77
+ If `False` (default) then quanta with higher values of dimensions
78
+ will be executed after quanta with lower values, otherwise the order
79
+ is reversed.
80
+ """
81
+
82
+ def __init__(self, taskLabel: str, dimensions: str | Sequence[str], reverse: bool = False):
83
+ self.taskLabel = taskLabel
84
+ self.dimensions = dimensions
85
+ self.reverse = reverse
86
+ if isinstance(self.dimensions, str):
87
+ self.dimensions = (self.dimensions,)
88
+ else:
89
+ self.dimensions = tuple(self.dimensions)
90
+
91
+ def _key(self, qnode: QuantumNode) -> tuple[Any, ...]:
92
+ """Produce comparison key for quantum data.
93
+
94
+ Parameters
95
+ ----------
96
+ qnode : `QuantumNode`
97
+ An individual node in a `~lsst.pipe.base.QuantumGraph`
98
+
99
+ Returns
100
+ -------
101
+ key : `tuple`
102
+ """
103
+ dataId = qnode.quantum.dataId
104
+ assert dataId is not None, "Quantum DataId cannot be None"
105
+ key = tuple(dataId[dim] for dim in self.dimensions)
106
+ return key
107
+
108
+ def fixupQuanta(self, graph: QuantumGraph) -> QuantumGraph:
109
+ taskDef = graph.findTaskDefByLabel(self.taskLabel)
110
+ if taskDef is None:
111
+ raise ValueError(f"Cannot find task with label {self.taskLabel}")
112
+ quanta = list(graph.getNodesForTask(taskDef))
113
+ keyQuanta = defaultdict(list)
114
+ for q in quanta:
115
+ key = self._key(q)
116
+ keyQuanta[key].append(q)
117
+ keys = sorted(keyQuanta.keys(), reverse=self.reverse)
118
+ networkGraph = graph.graph
119
+
120
+ for prev_key, key in itertools.pairwise(keys):
121
+ for prev_node in keyQuanta[prev_key]:
122
+ for node in keyQuanta[key]:
123
+ # remove any existing edges between the two nodes, but
124
+ # don't fail if there are not any. Both directions need
125
+ # tried because in a directed graph, order maters
126
+ for edge in ((node, prev_node), (prev_node, node)):
127
+ with contextlib.suppress(nx.NetworkXException):
128
+ networkGraph.remove_edge(*edge)
129
+
130
+ networkGraph.add_edge(prev_node, node)
131
+ return graph
@@ -0,0 +1,69 @@
1
+ # This file is part of pipe_base.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (http://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # This software is dual licensed under the GNU General Public License and also
10
+ # under a 3-clause BSD license. Recipients may choose which of these licenses
11
+ # to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12
+ # respectively. If you choose the GPL option then the following text applies
13
+ # (but note that there is still no warranty even if you opt for BSD instead):
14
+ #
15
+ # This program is free software: you can redistribute it and/or modify
16
+ # it under the terms of the GNU General Public License as published by
17
+ # the Free Software Foundation, either version 3 of the License, or
18
+ # (at your option) any later version.
19
+ #
20
+ # This program is distributed in the hope that it will be useful,
21
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ # GNU General Public License for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+
28
+ __all__ = ["ExecutionGraphFixup"]
29
+
30
+ from abc import ABC, abstractmethod
31
+
32
+ from .graph import QuantumGraph
33
+
34
+
35
+ class ExecutionGraphFixup(ABC):
36
+ """Interface for classes which update quantum graphs before execution.
37
+
38
+ Primary goal of this class is to modify quanta dependencies which may not
39
+ be possible to reflect in a quantum graph using standard tools. One known
40
+ use case for that is to guarantee particular execution order of visits in
41
+ CI jobs for cases when outcome depends on the processing order of visits
42
+ (e.g. AP association pipeline).
43
+
44
+ Instances of this class receive pre-ordered sequence of quanta
45
+ (`.QuantumGraph` instances) and they are allowed to modify quanta data in
46
+ place, for example update ``dependencies`` field to add additional
47
+ dependencies. Returned list of quanta will be re-ordered once again by the
48
+ graph executor to reflect new dependencies.
49
+ """
50
+
51
+ @abstractmethod
52
+ def fixupQuanta(self, graph: QuantumGraph) -> QuantumGraph:
53
+ """Update quanta in a graph.
54
+
55
+ Potentially anything in the graph could be changed if it does not
56
+ break executor assumptions. If modifications result in a dependency
57
+ cycle the executor will raise an exception.
58
+
59
+ Parameters
60
+ ----------
61
+ graph : `.QuantumGraph`
62
+ Quantum Graph that will be executed by the executor.
63
+
64
+ Returns
65
+ -------
66
+ graph : `.QuantumGraph`
67
+ Modified graph.
68
+ """
69
+ raise NotImplementedError
@@ -0,0 +1,227 @@
1
+ # This file is part of pipe_Base.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (http://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # This software is dual licensed under the GNU General Public License and also
10
+ # under a 3-clause BSD license. Recipients may choose which of these licenses
11
+ # to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12
+ # respectively. If you choose the GPL option then the following text applies
13
+ # (but note that there is still no warranty even if you opt for BSD instead):
14
+ #
15
+ # This program is free software: you can redistribute it and/or modify
16
+ # it under the terms of the GNU General Public License as published by
17
+ # the Free Software Foundation, either version 3 of the License, or
18
+ # (at your option) any later version.
19
+ #
20
+ # This program is distributed in the hope that it will be useful,
21
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ # GNU General Public License for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+
28
+ from __future__ import annotations
29
+
30
+ __all__ = ["LogCapture"]
31
+
32
+ import logging
33
+ import os
34
+ import shutil
35
+ import tempfile
36
+ from collections.abc import Iterator
37
+ from contextlib import contextmanager, suppress
38
+ from logging import FileHandler
39
+
40
+ from lsst.daf.butler import Butler, FileDataset, LimitedButler, Quantum
41
+ from lsst.daf.butler.logging import ButlerLogRecordHandler, ButlerLogRecords, ButlerMDC, JsonLogFormatter
42
+
43
+ from ._status import InvalidQuantumError
44
+ from .pipeline_graph import TaskNode
45
+
46
+ _LOG = logging.getLogger(__name__)
47
+
48
+
49
+ class _LogCaptureFlag:
50
+ """Simple flag to enable/disable log-to-butler saving."""
51
+
52
+ store: bool = True
53
+
54
+
55
+ class LogCapture:
56
+ """Class handling capture of logging messages and their export to butler.
57
+
58
+ Parameters
59
+ ----------
60
+ butler : `~lsst.daf.butler.LimitedButler`
61
+ Data butler with limited API.
62
+ full_butler : `~lsst.daf.butler.Butler` or `None`
63
+ Data butler with full API, or `None` if full Butler is not available.
64
+ If not none, then this must be the same instance as ``butler``.
65
+ """
66
+
67
+ stream_json_logs = True
68
+ """If True each log record is written to a temporary file and ingested
69
+ when quantum completes. If False the records are accumulated in memory
70
+ and stored in butler on quantum completion. If full butler is not available
71
+ then temporary file is not used."""
72
+
73
+ def __init__(
74
+ self,
75
+ butler: LimitedButler,
76
+ full_butler: Butler | None,
77
+ ):
78
+ self.butler = butler
79
+ self.full_butler = full_butler
80
+
81
+ @classmethod
82
+ def from_limited(cls, butler: LimitedButler) -> LogCapture:
83
+ return cls(butler, None)
84
+
85
+ @classmethod
86
+ def from_full(cls, butler: Butler) -> LogCapture:
87
+ return cls(butler, butler)
88
+
89
+ @contextmanager
90
+ def capture_logging(self, task_node: TaskNode, /, quantum: Quantum) -> Iterator[_LogCaptureFlag]:
91
+ """Configure logging system to capture logs for execution of this task.
92
+
93
+ Parameters
94
+ ----------
95
+ task_node : `~lsst.pipe.base.pipeline_graph.TaskNode`
96
+ The task definition.
97
+ quantum : `~lsst.daf.butler.Quantum`
98
+ Single Quantum instance.
99
+
100
+ Notes
101
+ -----
102
+ Expected to be used as a context manager to ensure that logging
103
+ records are inserted into the butler once the quantum has been
104
+ executed:
105
+
106
+ .. code-block:: py
107
+
108
+ with self.capture_logging(task_node, quantum):
109
+ # Run quantum and capture logs.
110
+
111
+ Ths method can also setup logging to attach task- or
112
+ quantum-specific information to log messages. Potentially this can
113
+ take into account some info from task configuration as well.
114
+ """
115
+ # include quantum dataId and task label into MDC
116
+ mdc = {"LABEL": task_node.label, "RUN": ""}
117
+ if quantum.dataId:
118
+ mdc["LABEL"] += f":{quantum.dataId}"
119
+ if self.full_butler is not None:
120
+ mdc["RUN"] = self.full_butler.run or ""
121
+ ctx = _LogCaptureFlag()
122
+ log_dataset_name = (
123
+ task_node.log_output.dataset_type_name if task_node.log_output is not None else None
124
+ )
125
+
126
+ # Add a handler to the root logger to capture execution log output.
127
+ if log_dataset_name is not None:
128
+ # Either accumulate into ButlerLogRecords or stream JSON records to
129
+ # file and ingest that (ingest is possible only with full butler).
130
+ if self.stream_json_logs and self.full_butler is not None:
131
+ # Create the log file in a temporary directory rather than
132
+ # creating a temporary file. This is necessary because
133
+ # temporary files are created with restrictive permissions
134
+ # and during file ingest these permissions persist in the
135
+ # datastore. Using a temp directory allows us to create
136
+ # a file with umask default permissions.
137
+ tmpdir = tempfile.mkdtemp(prefix="butler-temp-logs-")
138
+
139
+ # Construct a file to receive the log records and "touch" it.
140
+ log_file = os.path.join(tmpdir, f"butler-log-{task_node.label}.json")
141
+ with open(log_file, "w"):
142
+ pass
143
+ log_handler_file = FileHandler(log_file)
144
+ log_handler_file.setFormatter(JsonLogFormatter())
145
+ logging.getLogger().addHandler(log_handler_file)
146
+
147
+ try:
148
+ with ButlerMDC.set_mdc(mdc):
149
+ yield ctx
150
+ finally:
151
+ # Ensure that the logs are stored in butler.
152
+ logging.getLogger().removeHandler(log_handler_file)
153
+ log_handler_file.close()
154
+ if ctx.store:
155
+ self._ingest_log_records(quantum, log_dataset_name, log_file)
156
+ shutil.rmtree(tmpdir, ignore_errors=True)
157
+
158
+ else:
159
+ log_handler_memory = ButlerLogRecordHandler()
160
+ logging.getLogger().addHandler(log_handler_memory)
161
+
162
+ try:
163
+ with ButlerMDC.set_mdc(mdc):
164
+ yield ctx
165
+ finally:
166
+ # Ensure that the logs are stored in butler.
167
+ logging.getLogger().removeHandler(log_handler_memory)
168
+ if ctx.store:
169
+ self._store_log_records(quantum, log_dataset_name, log_handler_memory)
170
+ log_handler_memory.records.clear()
171
+
172
+ else:
173
+ with ButlerMDC.set_mdc(mdc):
174
+ yield ctx
175
+
176
+ def _store_log_records(
177
+ self, quantum: Quantum, dataset_type: str, log_handler: ButlerLogRecordHandler
178
+ ) -> None:
179
+ # DatasetRef has to be in the Quantum outputs, can lookup by name.
180
+ try:
181
+ [ref] = quantum.outputs[dataset_type]
182
+ except LookupError as exc:
183
+ raise InvalidQuantumError(
184
+ f"Quantum outputs is missing log output dataset type {dataset_type};"
185
+ " this could happen due to inconsistent options between QuantumGraph generation"
186
+ " and execution"
187
+ ) from exc
188
+
189
+ self.butler.put(log_handler.records, ref)
190
+
191
+ def _ingest_log_records(self, quantum: Quantum, dataset_type: str, filename: str) -> None:
192
+ # If we are logging to an external file we must always try to
193
+ # close it.
194
+ assert self.full_butler is not None, "Expected to have full butler for ingest"
195
+ ingested = False
196
+ try:
197
+ # DatasetRef has to be in the Quantum outputs, can lookup by name.
198
+ try:
199
+ [ref] = quantum.outputs[dataset_type]
200
+ except LookupError as exc:
201
+ raise InvalidQuantumError(
202
+ f"Quantum outputs is missing log output dataset type {dataset_type};"
203
+ " this could happen due to inconsistent options between QuantumGraph generation"
204
+ " and execution"
205
+ ) from exc
206
+
207
+ # Need to ingest this file directly into butler.
208
+ dataset = FileDataset(path=filename, refs=ref)
209
+ try:
210
+ self.full_butler.ingest(dataset, transfer="move")
211
+ ingested = True
212
+ except NotImplementedError:
213
+ # Some datastores can't receive files (e.g. in-memory datastore
214
+ # when testing), we store empty list for those just to have a
215
+ # dataset. Alternative is to read the file as a
216
+ # ButlerLogRecords object and put it.
217
+ _LOG.info(
218
+ "Log records could not be stored in this butler because the"
219
+ " datastore can not ingest files, empty record list is stored instead."
220
+ )
221
+ records = ButlerLogRecords.from_records([])
222
+ self.full_butler.put(records, ref)
223
+ finally:
224
+ # remove file if it is not ingested
225
+ if not ingested:
226
+ with suppress(OSError):
227
+ os.remove(filename)