lsst-pipe-base 30.0.1rc1__py3-none-any.whl → 30.2025.5100__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 +20 -31
- lsst/pipe/base/_quantumContext.py +3 -3
- lsst/pipe/base/_status.py +10 -43
- lsst/pipe/base/_task_metadata.py +2 -2
- lsst/pipe/base/all_dimensions_quantum_graph_builder.py +3 -8
- lsst/pipe/base/automatic_connection_constants.py +1 -20
- lsst/pipe/base/cli/cmd/__init__.py +2 -18
- lsst/pipe/base/cli/cmd/commands.py +4 -149
- lsst/pipe/base/connectionTypes.py +160 -72
- lsst/pipe/base/connections.py +9 -6
- lsst/pipe/base/execution_reports.py +5 -0
- lsst/pipe/base/graph/graph.py +10 -11
- lsst/pipe/base/graph/quantumNode.py +4 -4
- lsst/pipe/base/graph_walker.py +10 -8
- lsst/pipe/base/log_capture.py +80 -40
- lsst/pipe/base/mp_graph_executor.py +15 -51
- lsst/pipe/base/pipeline.py +6 -5
- lsst/pipe/base/pipelineIR.py +8 -2
- lsst/pipe/base/pipelineTask.py +7 -5
- lsst/pipe/base/pipeline_graph/_dataset_types.py +2 -2
- lsst/pipe/base/pipeline_graph/_edges.py +22 -32
- lsst/pipe/base/pipeline_graph/_mapping_views.py +7 -4
- lsst/pipe/base/pipeline_graph/_pipeline_graph.py +7 -14
- lsst/pipe/base/pipeline_graph/expressions.py +2 -2
- lsst/pipe/base/pipeline_graph/io.py +10 -7
- lsst/pipe/base/pipeline_graph/visualization/_dot.py +12 -13
- lsst/pipe/base/pipeline_graph/visualization/_layout.py +18 -16
- lsst/pipe/base/pipeline_graph/visualization/_merge.py +7 -4
- lsst/pipe/base/pipeline_graph/visualization/_printer.py +10 -10
- lsst/pipe/base/pipeline_graph/visualization/_status_annotator.py +0 -7
- lsst/pipe/base/prerequisite_helpers.py +1 -2
- lsst/pipe/base/quantum_graph/_common.py +20 -19
- lsst/pipe/base/quantum_graph/_multiblock.py +31 -37
- lsst/pipe/base/quantum_graph/_predicted.py +13 -111
- lsst/pipe/base/quantum_graph/_provenance.py +45 -1136
- lsst/pipe/base/quantum_graph/aggregator/__init__.py +1 -0
- lsst/pipe/base/quantum_graph/aggregator/_communicators.py +289 -204
- lsst/pipe/base/quantum_graph/aggregator/_config.py +9 -87
- lsst/pipe/base/quantum_graph/aggregator/_ingester.py +12 -13
- lsst/pipe/base/quantum_graph/aggregator/_scanner.py +235 -49
- lsst/pipe/base/quantum_graph/aggregator/_structs.py +116 -6
- lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +39 -29
- lsst/pipe/base/quantum_graph/aggregator/_writer.py +351 -34
- lsst/pipe/base/quantum_graph/visualization.py +1 -5
- lsst/pipe/base/quantum_graph_builder.py +8 -21
- lsst/pipe/base/quantum_graph_executor.py +13 -116
- lsst/pipe/base/quantum_graph_skeleton.py +29 -31
- lsst/pipe/base/quantum_provenance_graph.py +12 -29
- lsst/pipe/base/separable_pipeline_executor.py +3 -19
- lsst/pipe/base/single_quantum_executor.py +42 -67
- lsst/pipe/base/struct.py +0 -4
- lsst/pipe/base/testUtils.py +3 -3
- lsst/pipe/base/tests/mocks/_storage_class.py +1 -2
- lsst/pipe/base/version.py +1 -1
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/METADATA +3 -3
- lsst_pipe_base-30.2025.5100.dist-info/RECORD +125 -0
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/WHEEL +1 -1
- lsst/pipe/base/log_on_close.py +0 -76
- lsst/pipe/base/quantum_graph/aggregator/_workers.py +0 -303
- lsst/pipe/base/quantum_graph/formatter.py +0 -171
- lsst/pipe/base/quantum_graph/ingest_graph.py +0 -413
- lsst_pipe_base-30.0.1rc1.dist-info/RECORD +0 -129
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/entry_points.txt +0 -0
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/licenses/LICENSE +0 -0
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/licenses/bsd_license.txt +0 -0
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/licenses/gpl-v3.0.txt +0 -0
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/top_level.txt +0 -0
- {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5100.dist-info}/zip-safe +0 -0
|
@@ -1,303 +0,0 @@
|
|
|
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__ = ("Event", "Queue", "SpawnWorkerFactory", "ThreadWorkerFactory", "Worker", "WorkerFactory")
|
|
31
|
-
|
|
32
|
-
import multiprocessing.context
|
|
33
|
-
import multiprocessing.synchronize
|
|
34
|
-
import queue
|
|
35
|
-
import threading
|
|
36
|
-
from abc import ABC, abstractmethod
|
|
37
|
-
from collections.abc import Callable
|
|
38
|
-
from typing import Any, Literal, overload
|
|
39
|
-
|
|
40
|
-
_TINY_TIMEOUT = 0.01
|
|
41
|
-
|
|
42
|
-
type Event = threading.Event | multiprocessing.synchronize.Event
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class Worker(ABC):
|
|
46
|
-
"""A thin abstraction over `threading.Thread` and `multiprocessing.Process`
|
|
47
|
-
that also provides a variable to track whether it reported successful
|
|
48
|
-
completion.
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
def __init__(self) -> None:
|
|
52
|
-
self.successful = False
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
@abstractmethod
|
|
56
|
-
def name(self) -> str:
|
|
57
|
-
"""Name of the worker, as assigned at creation."""
|
|
58
|
-
raise NotImplementedError()
|
|
59
|
-
|
|
60
|
-
@abstractmethod
|
|
61
|
-
def join(self, timeout: float | None = None) -> None:
|
|
62
|
-
"""Wait for the worker to finish.
|
|
63
|
-
|
|
64
|
-
Parameters
|
|
65
|
-
----------
|
|
66
|
-
timeout : `float`, optional
|
|
67
|
-
How long to wait in seconds. If the timeout is exceeded,
|
|
68
|
-
`is_alive` can be used to see whether the worker finished or not.
|
|
69
|
-
"""
|
|
70
|
-
raise NotImplementedError()
|
|
71
|
-
|
|
72
|
-
@abstractmethod
|
|
73
|
-
def is_alive(self) -> bool:
|
|
74
|
-
"""Return whether the worker is still running."""
|
|
75
|
-
raise NotImplementedError()
|
|
76
|
-
|
|
77
|
-
def kill(self) -> None:
|
|
78
|
-
"""Kill the worker, if possible."""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class Queue[T](ABC):
|
|
82
|
-
"""A thin abstraction over `queue.Queue` and `multiprocessing.Queue` that
|
|
83
|
-
provides better control over disorderly shutdowns.
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
@overload
|
|
87
|
-
def get(self, *, block: Literal[True]) -> T: ...
|
|
88
|
-
|
|
89
|
-
@overload
|
|
90
|
-
def get(self, *, timeout: float | None = None, block: bool = False) -> T | None: ...
|
|
91
|
-
|
|
92
|
-
@abstractmethod
|
|
93
|
-
def get(self, *, timeout: float | None = None, block: bool = False) -> T | None:
|
|
94
|
-
"""Get an object or return `None` if the queue is empty.
|
|
95
|
-
|
|
96
|
-
Parameters
|
|
97
|
-
----------
|
|
98
|
-
timeout : `float` or `None`, optional
|
|
99
|
-
Maximum number of seconds to wait while blocking.
|
|
100
|
-
block : `bool`, optional
|
|
101
|
-
Whether to block until an object is available.
|
|
102
|
-
|
|
103
|
-
Returns
|
|
104
|
-
-------
|
|
105
|
-
obj : `object` or `None`
|
|
106
|
-
Object from the queue, or `None` if it was empty. Note that this
|
|
107
|
-
is different from the behavior of the built-in Python queues,
|
|
108
|
-
which raise `queue.Empty` instead.
|
|
109
|
-
"""
|
|
110
|
-
raise NotImplementedError()
|
|
111
|
-
|
|
112
|
-
@abstractmethod
|
|
113
|
-
def put(self, item: T) -> None:
|
|
114
|
-
"""Add an object to the queue.
|
|
115
|
-
|
|
116
|
-
Parameters
|
|
117
|
-
----------
|
|
118
|
-
item : `object`
|
|
119
|
-
Item to add.
|
|
120
|
-
"""
|
|
121
|
-
raise NotImplementedError()
|
|
122
|
-
|
|
123
|
-
def clear(self) -> bool:
|
|
124
|
-
"""Clear out all objects currently on the queue.
|
|
125
|
-
|
|
126
|
-
This does not guarantee that more objects will not be added later.
|
|
127
|
-
"""
|
|
128
|
-
found_anything: bool = False
|
|
129
|
-
while self.get() is not None:
|
|
130
|
-
found_anything = True
|
|
131
|
-
return found_anything
|
|
132
|
-
|
|
133
|
-
def kill(self) -> None:
|
|
134
|
-
"""Prepare a queue for a disorderly shutdown, without assuming that
|
|
135
|
-
any other workers using it are still alive and functioning.
|
|
136
|
-
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class WorkerFactory(ABC):
|
|
140
|
-
"""A simple abstract interface that can be implemented by both threading
|
|
141
|
-
and multiprocessing.
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
@abstractmethod
|
|
145
|
-
def make_queue(self) -> Queue[Any]:
|
|
146
|
-
"""Make an empty queue that can be used to pass objects between
|
|
147
|
-
workers created by this factory.
|
|
148
|
-
"""
|
|
149
|
-
raise NotImplementedError()
|
|
150
|
-
|
|
151
|
-
@abstractmethod
|
|
152
|
-
def make_event(self) -> Event:
|
|
153
|
-
"""Make an event that can be used to communicate a boolean state change
|
|
154
|
-
to workers created by this factory.
|
|
155
|
-
"""
|
|
156
|
-
raise NotImplementedError()
|
|
157
|
-
|
|
158
|
-
@abstractmethod
|
|
159
|
-
def make_worker(
|
|
160
|
-
self, target: Callable[..., None], args: tuple[Any, ...], name: str | None = None
|
|
161
|
-
) -> Worker:
|
|
162
|
-
"""Make a worker that runs the given callable.
|
|
163
|
-
|
|
164
|
-
Parameters
|
|
165
|
-
----------
|
|
166
|
-
target : `~collections.abc.Callable`
|
|
167
|
-
A callable to invoke on the worker.
|
|
168
|
-
args : `tuple`
|
|
169
|
-
Positional arguments to pass to the callable.
|
|
170
|
-
name : `str`, optional
|
|
171
|
-
Human-readable name for the worker.
|
|
172
|
-
|
|
173
|
-
Returns
|
|
174
|
-
-------
|
|
175
|
-
worker : `Worker`
|
|
176
|
-
Process or thread that is already running the given callable.
|
|
177
|
-
"""
|
|
178
|
-
raise NotImplementedError()
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
class _ThreadWorker(Worker):
|
|
182
|
-
"""An implementation of `Worker` backed by the `threading` module."""
|
|
183
|
-
|
|
184
|
-
def __init__(self, thread: threading.Thread):
|
|
185
|
-
super().__init__()
|
|
186
|
-
self._thread = thread
|
|
187
|
-
|
|
188
|
-
@property
|
|
189
|
-
def name(self) -> str:
|
|
190
|
-
return self._thread.name
|
|
191
|
-
|
|
192
|
-
def join(self, timeout: float | None = None) -> None:
|
|
193
|
-
self._thread.join(timeout=timeout)
|
|
194
|
-
|
|
195
|
-
def is_alive(self) -> bool:
|
|
196
|
-
return self._thread.is_alive()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class _ThreadQueue[T](Queue[T]):
|
|
200
|
-
def __init__(self) -> None:
|
|
201
|
-
self._impl = queue.Queue[T]()
|
|
202
|
-
|
|
203
|
-
@overload
|
|
204
|
-
def get(self, *, block: Literal[True]) -> T: ...
|
|
205
|
-
|
|
206
|
-
@overload
|
|
207
|
-
def get(self, *, timeout: float | None = None, block: bool = False) -> T | None: ...
|
|
208
|
-
|
|
209
|
-
def get(self, *, timeout: float | None = None, block: bool = False) -> T | None:
|
|
210
|
-
try:
|
|
211
|
-
return self._impl.get(block=block, timeout=timeout)
|
|
212
|
-
except queue.Empty:
|
|
213
|
-
return None
|
|
214
|
-
|
|
215
|
-
def put(self, item: T) -> None:
|
|
216
|
-
self._impl.put(item, block=False)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class ThreadWorkerFactory(WorkerFactory):
|
|
220
|
-
"""An implementation of `WorkerFactory` backed by the `threading`
|
|
221
|
-
module.
|
|
222
|
-
"""
|
|
223
|
-
|
|
224
|
-
def make_queue(self) -> Queue[Any]:
|
|
225
|
-
return _ThreadQueue()
|
|
226
|
-
|
|
227
|
-
def make_event(self) -> Event:
|
|
228
|
-
return threading.Event()
|
|
229
|
-
|
|
230
|
-
def make_worker(
|
|
231
|
-
self, target: Callable[..., None], args: tuple[Any, ...], name: str | None = None
|
|
232
|
-
) -> Worker:
|
|
233
|
-
thread = threading.Thread(target=target, args=args, name=name)
|
|
234
|
-
thread.start()
|
|
235
|
-
return _ThreadWorker(thread)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
class _ProcessWorker(Worker):
|
|
239
|
-
"""An implementation of `Worker` backed by the `multiprocessing` module."""
|
|
240
|
-
|
|
241
|
-
def __init__(self, process: multiprocessing.context.SpawnProcess):
|
|
242
|
-
super().__init__()
|
|
243
|
-
self._process = process
|
|
244
|
-
|
|
245
|
-
@property
|
|
246
|
-
def name(self) -> str:
|
|
247
|
-
return self._process.name
|
|
248
|
-
|
|
249
|
-
def join(self, timeout: float | None = None) -> None:
|
|
250
|
-
self._process.join(timeout=timeout)
|
|
251
|
-
|
|
252
|
-
def is_alive(self) -> bool:
|
|
253
|
-
return self._process.is_alive()
|
|
254
|
-
|
|
255
|
-
def kill(self) -> None:
|
|
256
|
-
"""Kill the worker, if possible."""
|
|
257
|
-
self._process.kill()
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
class _ProcessQueue[T](Queue[T]):
|
|
261
|
-
def __init__(self, impl: multiprocessing.Queue):
|
|
262
|
-
self._impl = impl
|
|
263
|
-
|
|
264
|
-
@overload
|
|
265
|
-
def get(self, *, block: Literal[True]) -> T: ...
|
|
266
|
-
|
|
267
|
-
@overload
|
|
268
|
-
def get(self, *, timeout: float | None = None, block: bool = False) -> T | None: ...
|
|
269
|
-
|
|
270
|
-
def get(self, *, timeout: float | None = None, block: bool = False) -> T | None:
|
|
271
|
-
try:
|
|
272
|
-
return self._impl.get(block=block, timeout=timeout)
|
|
273
|
-
except queue.Empty:
|
|
274
|
-
return None
|
|
275
|
-
|
|
276
|
-
def put(self, item: T) -> None:
|
|
277
|
-
self._impl.put(item, block=False)
|
|
278
|
-
|
|
279
|
-
def kill(self) -> None:
|
|
280
|
-
self._impl.cancel_join_thread()
|
|
281
|
-
self._impl.close()
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
class SpawnWorkerFactory(WorkerFactory):
|
|
285
|
-
"""An implementation of `WorkerFactory` backed by the `multiprocessing`
|
|
286
|
-
module, with new processes started by spawning.
|
|
287
|
-
"""
|
|
288
|
-
|
|
289
|
-
def __init__(self) -> None:
|
|
290
|
-
self._ctx = multiprocessing.get_context("spawn")
|
|
291
|
-
|
|
292
|
-
def make_queue(self) -> Queue[Any]:
|
|
293
|
-
return _ProcessQueue(self._ctx.Queue())
|
|
294
|
-
|
|
295
|
-
def make_event(self) -> Event:
|
|
296
|
-
return self._ctx.Event()
|
|
297
|
-
|
|
298
|
-
def make_worker(
|
|
299
|
-
self, target: Callable[..., None], args: tuple[Any, ...], name: str | None = None
|
|
300
|
-
) -> Worker:
|
|
301
|
-
process = self._ctx.Process(target=target, args=args, name=name)
|
|
302
|
-
process.start()
|
|
303
|
-
return _ProcessWorker(process)
|
|
@@ -1,171 +0,0 @@
|
|
|
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__ = ("ProvenanceFormatter",)
|
|
31
|
-
|
|
32
|
-
import uuid
|
|
33
|
-
from typing import Any, ClassVar
|
|
34
|
-
|
|
35
|
-
import pydantic
|
|
36
|
-
|
|
37
|
-
from lsst.daf.butler import FormatterV2
|
|
38
|
-
from lsst.daf.butler.logging import ButlerLogRecords
|
|
39
|
-
from lsst.pex.config import Config
|
|
40
|
-
from lsst.resources import ResourcePath
|
|
41
|
-
from lsst.utils.logging import getLogger
|
|
42
|
-
from lsst.utils.packages import Packages
|
|
43
|
-
|
|
44
|
-
from .._task_metadata import TaskMetadata
|
|
45
|
-
from ..pipeline_graph import TaskImportMode
|
|
46
|
-
from ._provenance import ProvenanceQuantumGraphReader
|
|
47
|
-
|
|
48
|
-
_LOG = getLogger(__file__)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class _ProvenanceFormatterParameters(pydantic.BaseModel):
|
|
52
|
-
"""A Pydantic model for validating and applying defaults to the
|
|
53
|
-
read parameters of `ProvenanceFormatter`.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
import_mode: TaskImportMode = TaskImportMode.DO_NOT_IMPORT
|
|
57
|
-
quanta: list[uuid.UUID] | None = None
|
|
58
|
-
datasets: list[uuid.UUID] | None = None
|
|
59
|
-
read_init_quanta: bool = True
|
|
60
|
-
|
|
61
|
-
@pydantic.field_validator("quanta", mode="before")
|
|
62
|
-
@classmethod
|
|
63
|
-
def quanta_to_list(cls, v: Any) -> list[uuid.UUID] | None:
|
|
64
|
-
return list(v) if v is not None else None
|
|
65
|
-
|
|
66
|
-
@pydantic.field_validator("datasets", mode="before")
|
|
67
|
-
@classmethod
|
|
68
|
-
def datasets_to_list(cls, v: Any) -> list[uuid.UUID] | None:
|
|
69
|
-
return list(v) if v is not None else None
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def nodes(self) -> list[uuid.UUID]:
|
|
73
|
-
if self.quanta is not None:
|
|
74
|
-
if self.datasets is not None:
|
|
75
|
-
return self.quanta + self.datasets
|
|
76
|
-
else:
|
|
77
|
-
return self.quanta
|
|
78
|
-
elif self.datasets is not None:
|
|
79
|
-
return self.datasets
|
|
80
|
-
raise ValueError("'datasets' and/or 'quanta' parameters are required for this component")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class ProvenanceFormatter(FormatterV2):
|
|
84
|
-
"""Butler interface for reading `ProvenanceQuantumGraph` objects."""
|
|
85
|
-
|
|
86
|
-
default_extension: ClassVar[str] = ".qg"
|
|
87
|
-
can_read_from_uri: ClassVar[bool] = True
|
|
88
|
-
|
|
89
|
-
def read_from_uri(self, uri: ResourcePath, component: str | None = None, expected_size: int = -1) -> Any:
|
|
90
|
-
match self._dataset_ref.datasetType.storageClass_name:
|
|
91
|
-
case "TaskMetadata" | "PropertySet":
|
|
92
|
-
return self._read_metadata(uri)
|
|
93
|
-
case "ButlerLogRecords":
|
|
94
|
-
return self._read_log(uri)
|
|
95
|
-
case "Config":
|
|
96
|
-
return self._read_config(uri)
|
|
97
|
-
case "ProvenanceQuantumGraph":
|
|
98
|
-
pass
|
|
99
|
-
case unexpected:
|
|
100
|
-
raise ValueError(f"Unsupported storage class {unexpected!r} for ProvenanceFormatter.")
|
|
101
|
-
parameters = _ProvenanceFormatterParameters.model_validate(self.file_descriptor.parameters or {})
|
|
102
|
-
with ProvenanceQuantumGraphReader.open(uri, import_mode=parameters.import_mode) as reader:
|
|
103
|
-
match component:
|
|
104
|
-
case None:
|
|
105
|
-
if parameters.read_init_quanta:
|
|
106
|
-
reader.read_init_quanta()
|
|
107
|
-
reader.read_quanta(parameters.quanta)
|
|
108
|
-
reader.read_datasets(parameters.datasets)
|
|
109
|
-
return reader.graph
|
|
110
|
-
case "metadata":
|
|
111
|
-
return reader.fetch_metadata(parameters.nodes)
|
|
112
|
-
case "logs":
|
|
113
|
-
return reader.fetch_logs(parameters.nodes)
|
|
114
|
-
case "packages":
|
|
115
|
-
return reader.fetch_packages()
|
|
116
|
-
raise AssertionError(f"Unexpected component {component!r}.")
|
|
117
|
-
|
|
118
|
-
def _read_metadata(self, uri: ResourcePath) -> TaskMetadata:
|
|
119
|
-
with ProvenanceQuantumGraphReader.open(uri, import_mode=TaskImportMode.DO_NOT_IMPORT) as reader:
|
|
120
|
-
try:
|
|
121
|
-
attempts = reader.fetch_metadata([self._dataset_ref.id])[self._dataset_ref.id]
|
|
122
|
-
except LookupError:
|
|
123
|
-
raise FileNotFoundError(
|
|
124
|
-
f"No dataset with ID {self._dataset_ref.id} present in this graph."
|
|
125
|
-
) from None
|
|
126
|
-
if not attempts:
|
|
127
|
-
raise FileNotFoundError(
|
|
128
|
-
f"No metadata dataset {self._dataset_ref} stored in this graph "
|
|
129
|
-
"(no attempts for this quantum)."
|
|
130
|
-
)
|
|
131
|
-
if attempts[-1] is None:
|
|
132
|
-
raise FileNotFoundError(
|
|
133
|
-
f"No metadata dataset {self._dataset_ref} stored in this graph "
|
|
134
|
-
"(most recent attempt failed and did not write metadata)."
|
|
135
|
-
)
|
|
136
|
-
return attempts[-1]
|
|
137
|
-
|
|
138
|
-
def _read_log(self, uri: ResourcePath) -> ButlerLogRecords:
|
|
139
|
-
with ProvenanceQuantumGraphReader.open(uri, import_mode=TaskImportMode.DO_NOT_IMPORT) as reader:
|
|
140
|
-
try:
|
|
141
|
-
attempts = reader.fetch_logs([self._dataset_ref.id])[self._dataset_ref.id]
|
|
142
|
-
except LookupError:
|
|
143
|
-
raise FileNotFoundError(
|
|
144
|
-
f"No dataset with ID {self._dataset_ref.id} present in this graph."
|
|
145
|
-
) from None
|
|
146
|
-
if not attempts:
|
|
147
|
-
raise FileNotFoundError(
|
|
148
|
-
f"No log dataset {self._dataset_ref} stored in this graph (no attempts for this quantum)."
|
|
149
|
-
)
|
|
150
|
-
if attempts[-1] is None:
|
|
151
|
-
raise FileNotFoundError(
|
|
152
|
-
f"No log dataset {self._dataset_ref} stored in this graph "
|
|
153
|
-
"(most recent attempt failed and did not write logs)."
|
|
154
|
-
)
|
|
155
|
-
return attempts[-1]
|
|
156
|
-
|
|
157
|
-
def _read_packages(self, uri: ResourcePath) -> Packages:
|
|
158
|
-
with ProvenanceQuantumGraphReader.open(uri, import_mode=TaskImportMode.DO_NOT_IMPORT) as reader:
|
|
159
|
-
return reader.fetch_packages()
|
|
160
|
-
|
|
161
|
-
def _read_config(self, uri: ResourcePath) -> Config:
|
|
162
|
-
task_label = self._dataset_ref.datasetType.name.removesuffix("_config")
|
|
163
|
-
with ProvenanceQuantumGraphReader.open(
|
|
164
|
-
uri, import_mode=TaskImportMode.ASSUME_CONSISTENT_EDGES
|
|
165
|
-
) as reader:
|
|
166
|
-
try:
|
|
167
|
-
return reader.pipeline_graph.tasks[task_label].config.copy()
|
|
168
|
-
except KeyError:
|
|
169
|
-
raise FileNotFoundError(
|
|
170
|
-
f"No task with label {task_label!r} found in the pipeline graph."
|
|
171
|
-
) from None
|