lsst-pipe-base 30.0.1rc1__py3-none-any.whl → 30.2025.5200__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 +20 -31
  2. lsst/pipe/base/_quantumContext.py +3 -3
  3. lsst/pipe/base/_status.py +10 -43
  4. lsst/pipe/base/_task_metadata.py +2 -2
  5. lsst/pipe/base/all_dimensions_quantum_graph_builder.py +3 -8
  6. lsst/pipe/base/automatic_connection_constants.py +1 -20
  7. lsst/pipe/base/cli/cmd/__init__.py +2 -18
  8. lsst/pipe/base/cli/cmd/commands.py +4 -149
  9. lsst/pipe/base/connectionTypes.py +160 -72
  10. lsst/pipe/base/connections.py +9 -6
  11. lsst/pipe/base/execution_reports.py +5 -0
  12. lsst/pipe/base/graph/graph.py +10 -11
  13. lsst/pipe/base/graph/quantumNode.py +4 -4
  14. lsst/pipe/base/graph_walker.py +10 -8
  15. lsst/pipe/base/log_capture.py +5 -9
  16. lsst/pipe/base/mp_graph_executor.py +15 -51
  17. lsst/pipe/base/pipeline.py +6 -5
  18. lsst/pipe/base/pipelineIR.py +8 -2
  19. lsst/pipe/base/pipelineTask.py +7 -5
  20. lsst/pipe/base/pipeline_graph/_dataset_types.py +2 -2
  21. lsst/pipe/base/pipeline_graph/_edges.py +22 -32
  22. lsst/pipe/base/pipeline_graph/_mapping_views.py +7 -4
  23. lsst/pipe/base/pipeline_graph/_pipeline_graph.py +7 -14
  24. lsst/pipe/base/pipeline_graph/expressions.py +2 -2
  25. lsst/pipe/base/pipeline_graph/io.py +10 -7
  26. lsst/pipe/base/pipeline_graph/visualization/_dot.py +12 -13
  27. lsst/pipe/base/pipeline_graph/visualization/_layout.py +18 -16
  28. lsst/pipe/base/pipeline_graph/visualization/_merge.py +7 -4
  29. lsst/pipe/base/pipeline_graph/visualization/_printer.py +10 -10
  30. lsst/pipe/base/pipeline_graph/visualization/_status_annotator.py +0 -7
  31. lsst/pipe/base/prerequisite_helpers.py +1 -2
  32. lsst/pipe/base/quantum_graph/_common.py +20 -19
  33. lsst/pipe/base/quantum_graph/_multiblock.py +31 -37
  34. lsst/pipe/base/quantum_graph/_predicted.py +13 -111
  35. lsst/pipe/base/quantum_graph/_provenance.py +45 -1136
  36. lsst/pipe/base/quantum_graph/aggregator/__init__.py +1 -0
  37. lsst/pipe/base/quantum_graph/aggregator/_communicators.py +289 -204
  38. lsst/pipe/base/quantum_graph/aggregator/_config.py +9 -87
  39. lsst/pipe/base/quantum_graph/aggregator/_ingester.py +12 -13
  40. lsst/pipe/base/quantum_graph/aggregator/_scanner.py +235 -49
  41. lsst/pipe/base/quantum_graph/aggregator/_structs.py +116 -6
  42. lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +39 -29
  43. lsst/pipe/base/quantum_graph/aggregator/_writer.py +351 -34
  44. lsst/pipe/base/quantum_graph/visualization.py +1 -5
  45. lsst/pipe/base/quantum_graph_builder.py +8 -21
  46. lsst/pipe/base/quantum_graph_executor.py +13 -116
  47. lsst/pipe/base/quantum_graph_skeleton.py +29 -31
  48. lsst/pipe/base/quantum_provenance_graph.py +12 -29
  49. lsst/pipe/base/separable_pipeline_executor.py +3 -19
  50. lsst/pipe/base/single_quantum_executor.py +42 -67
  51. lsst/pipe/base/struct.py +0 -4
  52. lsst/pipe/base/testUtils.py +3 -3
  53. lsst/pipe/base/tests/mocks/_storage_class.py +1 -2
  54. lsst/pipe/base/version.py +1 -1
  55. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/METADATA +3 -3
  56. lsst_pipe_base-30.2025.5200.dist-info/RECORD +125 -0
  57. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/WHEEL +1 -1
  58. lsst/pipe/base/log_on_close.py +0 -76
  59. lsst/pipe/base/quantum_graph/aggregator/_workers.py +0 -303
  60. lsst/pipe/base/quantum_graph/formatter.py +0 -171
  61. lsst/pipe/base/quantum_graph/ingest_graph.py +0 -413
  62. lsst_pipe_base-30.0.1rc1.dist-info/RECORD +0 -129
  63. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/entry_points.txt +0 -0
  64. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/licenses/COPYRIGHT +0 -0
  65. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/licenses/LICENSE +0 -0
  66. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/licenses/bsd_license.txt +0 -0
  67. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/licenses/gpl-v3.0.txt +0 -0
  68. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.dist-info}/top_level.txt +0 -0
  69. {lsst_pipe_base-30.0.1rc1.dist-info → lsst_pipe_base-30.2025.5200.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