pivot 0.2.1__tar.gz → 0.2.3__tar.gz
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.
- {pivot-0.2.1 → pivot-0.2.3}/PKG-INFO +1 -1
- {pivot-0.2.1 → pivot-0.2.3}/pyproject.toml +1 -1
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/checkout.py +20 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/engine.py +1 -1
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/sinks.py +76 -10
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/executor/commit.py +1 -1
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/executor/core.py +1 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/executor/worker.py +60 -10
- pivot-0.2.3/src/pivot/loaders.py +888 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/remote/storage.py +57 -7
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/show/data.py +3 -4
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/AGENTS.md +10 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/cache.py +362 -262
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/restore.py +12 -14
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/store.py +22 -3
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/types.py +5 -0
- pivot-0.2.1/src/pivot/loaders.py +0 -389
- {pivot-0.2.1 → pivot-0.2.3}/LICENSE +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/README.md +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/ast_utils.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/AGENTS.md +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/CLAUDE.md +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/_run_common.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/check_ignore.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/commit.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/completion.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/config.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/console.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/dag.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/data.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/decorators.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/doctor.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/errors.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/fingerprint.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/helpers.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/history.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/import_cmd.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/init.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/list.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/metrics.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/params.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/plots.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/remote.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/repro.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/run.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/status.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/targets.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/track.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/update.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/cli/verify.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/compose.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/config/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/config/io.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/config/models.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/dag/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/dag/render.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/decorators.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/discovery.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/agent_rpc.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/graph.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/scheduler.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/sources.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/types.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/watch.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/engine/worker_pool.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/exceptions.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/executor/AGENTS.md +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/executor/CLAUDE.md +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/executor/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/explain.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/fingerprint.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/git.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/ignore.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/import_artifact.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/metrics.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/names.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/outputs.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/parameters.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/path_policy.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/project.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/py.typed +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/registry.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/remote/AGENTS.md +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/remote/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/remote/config.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/remote/git_archive.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/remote/github.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/remote/sync.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/run_history.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/show/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/show/common.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/show/metrics.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/show/params.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/show/plots.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/skip.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/stage_def.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/status.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/CLAUDE.md +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/__init__.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/artifact_lock.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/lock.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/presentation.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/state.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/storage/track.py +0 -0
- {pivot-0.2.1 → pivot-0.2.3}/src/pivot/yaml_config.py +0 -0
|
@@ -89,6 +89,9 @@ def _restore_path_sync(
|
|
|
89
89
|
) -> tuple[RestoreResult, str]:
|
|
90
90
|
"""Restore a file or directory from cache (sync version).
|
|
91
91
|
|
|
92
|
+
Holds the restore lock for path across the existence check and the restore, so a
|
|
93
|
+
concurrent checkout never compares against, or deletes, a half-restored output.
|
|
94
|
+
|
|
92
95
|
Returns:
|
|
93
96
|
Tuple of (result, path_name) for the caller to handle output.
|
|
94
97
|
|
|
@@ -97,6 +100,22 @@ def _restore_path_sync(
|
|
|
97
100
|
"already exists" without --force). Cache misses return ("missing", name)
|
|
98
101
|
instead of raising.
|
|
99
102
|
"""
|
|
103
|
+
with cache.restore_lock(path, state_dir) as lock_token:
|
|
104
|
+
return _restore_path_locked(
|
|
105
|
+
path, output_hash, cache_dir, checkout_modes, behavior, state_dir, lock_token
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _restore_path_locked(
|
|
110
|
+
path: pathlib.Path,
|
|
111
|
+
output_hash: HashInfo,
|
|
112
|
+
cache_dir: pathlib.Path,
|
|
113
|
+
checkout_modes: list[cache.CheckoutMode],
|
|
114
|
+
behavior: CheckoutBehavior,
|
|
115
|
+
state_dir: pathlib.Path | None,
|
|
116
|
+
lock_token: str | None,
|
|
117
|
+
) -> tuple[RestoreResult, str]:
|
|
118
|
+
"""Body of _restore_path_sync; the caller holds the restore lock for path."""
|
|
100
119
|
keep_ignored = False
|
|
101
120
|
if path.exists():
|
|
102
121
|
match behavior:
|
|
@@ -146,6 +165,7 @@ def _restore_path_sync(
|
|
|
146
165
|
cache_dir,
|
|
147
166
|
checkout_modes=checkout_modes,
|
|
148
167
|
state_dir=state_dir,
|
|
168
|
+
lock_token=lock_token,
|
|
149
169
|
keep_ignored=keep_ignored,
|
|
150
170
|
)
|
|
151
171
|
if not success:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import dataclasses
|
|
3
4
|
import sys
|
|
4
5
|
import time
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
@@ -10,8 +11,8 @@ import rich.live
|
|
|
10
11
|
import rich.markup
|
|
11
12
|
import rich.text
|
|
12
13
|
|
|
13
|
-
from pivot.engine.types import StageCompleted
|
|
14
|
-
from pivot.types import DisplayCategory, categorize_stage_result
|
|
14
|
+
from pivot.engine.types import LogLine, StageCompleted
|
|
15
|
+
from pivot.types import LOSSY_OUTPUT_PHRASE, DisplayCategory, categorize_stage_result
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from pivot.engine.types import OutputEvent
|
|
@@ -84,6 +85,20 @@ def _categorize(event: StageCompleted) -> DisplayCategory:
|
|
|
84
85
|
return categorize_stage_result(event["status"])
|
|
85
86
|
|
|
86
87
|
|
|
88
|
+
@dataclasses.dataclass
|
|
89
|
+
class _StageWarnings:
|
|
90
|
+
"""A stage's ``WARNING:`` lines for the report.
|
|
91
|
+
|
|
92
|
+
Pivot's own output-write warnings are always kept and print first. The stage's own
|
|
93
|
+
warnings are counted per distinct line (in first-seen order); the report shows the
|
|
94
|
+
first ``_MAX_LOG_LINES_PER_STAGE`` distinct lines and counts the rest.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
stage: dict[str, int] = dataclasses.field(default_factory=dict[str, int])
|
|
98
|
+
lossy_outputs: list[str] = dataclasses.field(default_factory=list[str])
|
|
99
|
+
dropped: int = 0
|
|
100
|
+
|
|
101
|
+
|
|
87
102
|
def _print_completions(
|
|
88
103
|
console: rich.console.Console,
|
|
89
104
|
events: list[StageCompleted],
|
|
@@ -91,13 +106,19 @@ def _print_completions(
|
|
|
91
106
|
total: int,
|
|
92
107
|
max_name_width: int,
|
|
93
108
|
stage_logs: dict[str, list[str]] | None = None,
|
|
109
|
+
stage_warnings: dict[str, _StageWarnings] | None = None,
|
|
94
110
|
) -> None:
|
|
95
|
-
"""Print buffered completions sorted by index with skip collapsing.
|
|
111
|
+
"""Print buffered completions sorted by index with skip collapsing.
|
|
112
|
+
|
|
113
|
+
A failed stage shows its captured output; any other stage shows the ``WARNING:``
|
|
114
|
+
lines in ``stage_warnings`` (pass None when those already streamed).
|
|
115
|
+
"""
|
|
96
116
|
if not events:
|
|
97
117
|
return
|
|
98
118
|
|
|
99
119
|
sorted_events = sorted(events, key=lambda e: e["index"])
|
|
100
120
|
logs = stage_logs or {}
|
|
121
|
+
warnings_by_stage = stage_warnings or {}
|
|
101
122
|
|
|
102
123
|
i = 0
|
|
103
124
|
while i < len(sorted_events):
|
|
@@ -152,12 +173,53 @@ def _print_completions(
|
|
|
152
173
|
if event["reason"]:
|
|
153
174
|
for detail in _format_error_detail(event["reason"], total=total):
|
|
154
175
|
console.print(detail)
|
|
176
|
+
elif event["stage"] in warnings_by_stage:
|
|
177
|
+
buffered = warnings_by_stage[event["stage"]]
|
|
178
|
+
stage_lines = [
|
|
179
|
+
line if count == 1 else f"{line} (×{count})"
|
|
180
|
+
for line, count in buffered.stage.items()
|
|
181
|
+
]
|
|
182
|
+
for warning_line in [*buffered.lossy_outputs, *stage_lines]:
|
|
183
|
+
escaped = rich.markup.escape(warning_line)
|
|
184
|
+
console.print(f" [yellow]{escaped}[/yellow]")
|
|
185
|
+
if buffered.dropped:
|
|
186
|
+
console.print(
|
|
187
|
+
f" [yellow]… {buffered.dropped} more warnings "
|
|
188
|
+
+ "(run with --show-output)[/yellow]"
|
|
189
|
+
)
|
|
155
190
|
i += 1
|
|
156
191
|
|
|
157
192
|
|
|
158
193
|
_MAX_LOG_LINES_PER_STAGE = 50
|
|
159
194
|
|
|
160
195
|
|
|
196
|
+
def _buffer_log_line(
|
|
197
|
+
event: LogLine,
|
|
198
|
+
stage_logs: dict[str, list[str]],
|
|
199
|
+
stage_warnings: dict[str, _StageWarnings],
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Keep a stage's output for a failure report and its warnings for every report.
|
|
202
|
+
|
|
203
|
+
Warnings have their own buffer: output writes log them after the stage's own output,
|
|
204
|
+
which can already fill the log buffer.
|
|
205
|
+
"""
|
|
206
|
+
line = event["line"]
|
|
207
|
+
buf = stage_logs.setdefault(event["stage"], [])
|
|
208
|
+
if len(buf) < _MAX_LOG_LINES_PER_STAGE:
|
|
209
|
+
buf.append(line)
|
|
210
|
+
if not line.startswith("WARNING:"):
|
|
211
|
+
return
|
|
212
|
+
buffered = stage_warnings.setdefault(event["stage"], _StageWarnings())
|
|
213
|
+
if LOSSY_OUTPUT_PHRASE in line and line.startswith("WARNING: Stage '"):
|
|
214
|
+
buffered.lossy_outputs.append(line)
|
|
215
|
+
elif line in buffered.stage:
|
|
216
|
+
buffered.stage[line] += 1
|
|
217
|
+
elif len(buffered.stage) < _MAX_LOG_LINES_PER_STAGE:
|
|
218
|
+
buffered.stage[line] = 1
|
|
219
|
+
else:
|
|
220
|
+
buffered.dropped += 1
|
|
221
|
+
|
|
222
|
+
|
|
161
223
|
class StaticConsoleSink:
|
|
162
224
|
"""Pipe/CI sink: buffers completions, prints a sorted report when each run ends."""
|
|
163
225
|
|
|
@@ -166,6 +228,7 @@ class StaticConsoleSink:
|
|
|
166
228
|
_max_name_width: int
|
|
167
229
|
_completion_buffer: list[StageCompleted]
|
|
168
230
|
_stage_logs: dict[str, list[str]]
|
|
231
|
+
_stage_warnings: dict[str, _StageWarnings]
|
|
169
232
|
_total: int
|
|
170
233
|
|
|
171
234
|
def __init__(self, *, console: rich.console.Console, show_output: bool = False) -> None:
|
|
@@ -174,6 +237,7 @@ class StaticConsoleSink:
|
|
|
174
237
|
self._max_name_width = 0
|
|
175
238
|
self._completion_buffer = list[StageCompleted]()
|
|
176
239
|
self._stage_logs = dict[str, list[str]]()
|
|
240
|
+
self._stage_warnings = dict[str, _StageWarnings]()
|
|
177
241
|
self._total = 0
|
|
178
242
|
|
|
179
243
|
async def handle(self, event: OutputEvent) -> None:
|
|
@@ -187,11 +251,9 @@ class StaticConsoleSink:
|
|
|
187
251
|
self._max_name_width = max(self._max_name_width, len(event["stage"]))
|
|
188
252
|
self._completion_buffer.append(event)
|
|
189
253
|
case "log_line":
|
|
190
|
-
#
|
|
254
|
+
# Buffer for the report; stream if --show-output
|
|
191
255
|
stage = event["stage"]
|
|
192
|
-
|
|
193
|
-
if len(buf) < _MAX_LOG_LINES_PER_STAGE:
|
|
194
|
-
buf.append(event["line"])
|
|
256
|
+
_buffer_log_line(event, self._stage_logs, self._stage_warnings)
|
|
195
257
|
if self._show_output:
|
|
196
258
|
line = rich.markup.escape(event["line"])
|
|
197
259
|
if event["is_stderr"]:
|
|
@@ -218,10 +280,12 @@ class StaticConsoleSink:
|
|
|
218
280
|
total=self._total,
|
|
219
281
|
max_name_width=self._max_name_width,
|
|
220
282
|
stage_logs=self._stage_logs,
|
|
283
|
+
stage_warnings=None if self._show_output else self._stage_warnings,
|
|
221
284
|
)
|
|
222
285
|
self._max_name_width = 0
|
|
223
286
|
self._completion_buffer.clear()
|
|
224
287
|
self._stage_logs.clear()
|
|
288
|
+
self._stage_warnings.clear()
|
|
225
289
|
self._total = 0
|
|
226
290
|
|
|
227
291
|
async def close(self) -> None:
|
|
@@ -254,6 +318,7 @@ class LiveConsoleSink:
|
|
|
254
318
|
_max_name_width: int
|
|
255
319
|
_completion_buffer: list[StageCompleted]
|
|
256
320
|
_stage_logs: dict[str, list[str]]
|
|
321
|
+
_stage_warnings: dict[str, _StageWarnings]
|
|
257
322
|
_total: int
|
|
258
323
|
_live: rich.live.Live | None
|
|
259
324
|
|
|
@@ -268,6 +333,7 @@ class LiveConsoleSink:
|
|
|
268
333
|
self._max_name_width = 0
|
|
269
334
|
self._completion_buffer = list[StageCompleted]()
|
|
270
335
|
self._stage_logs = dict[str, list[str]]()
|
|
336
|
+
self._stage_warnings = dict[str, _StageWarnings]()
|
|
271
337
|
self._total = 0
|
|
272
338
|
self._live = None
|
|
273
339
|
|
|
@@ -372,9 +438,7 @@ class LiveConsoleSink:
|
|
|
372
438
|
self._update_live()
|
|
373
439
|
case "log_line":
|
|
374
440
|
stage = event["stage"]
|
|
375
|
-
|
|
376
|
-
if len(buf) < _MAX_LOG_LINES_PER_STAGE:
|
|
377
|
-
buf.append(event["line"])
|
|
441
|
+
_buffer_log_line(event, self._stage_logs, self._stage_warnings)
|
|
378
442
|
if self._show_output:
|
|
379
443
|
line = rich.markup.escape(event["line"])
|
|
380
444
|
if event["is_stderr"]:
|
|
@@ -405,6 +469,7 @@ class LiveConsoleSink:
|
|
|
405
469
|
total=self._total,
|
|
406
470
|
max_name_width=self._max_name_width,
|
|
407
471
|
stage_logs=self._stage_logs,
|
|
472
|
+
stage_warnings=None if self._show_output else self._stage_warnings,
|
|
408
473
|
)
|
|
409
474
|
self._running.clear()
|
|
410
475
|
self._completed_count = 0
|
|
@@ -412,6 +477,7 @@ class LiveConsoleSink:
|
|
|
412
477
|
self._max_name_width = 0
|
|
413
478
|
self._completion_buffer.clear()
|
|
414
479
|
self._stage_logs.clear()
|
|
480
|
+
self._stage_warnings.clear()
|
|
415
481
|
self._total = 0
|
|
416
482
|
|
|
417
483
|
async def close(self) -> None:
|
|
@@ -150,7 +150,7 @@ def commit_stages(
|
|
|
150
150
|
identity_key = types.identity_key(out.identity)
|
|
151
151
|
try:
|
|
152
152
|
out_path = store.resolve_display_path(out)
|
|
153
|
-
hash_info = store.commit(out, out_path)
|
|
153
|
+
hash_info = store.commit(out, out_path, state_dir=default_state_dir)
|
|
154
154
|
file_hash_entries.extend(worker.output_file_hash_entries(out_path, hash_info))
|
|
155
155
|
output_hashes_by_key[identity_key] = hash_info
|
|
156
156
|
output_hashes_by_identity[out.identity] = hash_info
|
|
@@ -18,6 +18,7 @@ import random
|
|
|
18
18
|
import sys
|
|
19
19
|
import threading
|
|
20
20
|
import traceback
|
|
21
|
+
import warnings
|
|
21
22
|
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, TypedDict, cast, overload, override
|
|
22
23
|
|
|
23
24
|
import pydantic
|
|
@@ -153,6 +154,7 @@ class WorkerStageInfo(TypedDict):
|
|
|
153
154
|
params_arg_name: str | None
|
|
154
155
|
project_root: pathlib.Path
|
|
155
156
|
state_dir: pathlib.Path
|
|
157
|
+
project_state_dir: pathlib.Path
|
|
156
158
|
collection_params: dict[str, str]
|
|
157
159
|
incremental_inputs: dict[str, types.ArtifactIdentity]
|
|
158
160
|
|
|
@@ -193,6 +195,7 @@ def execute_stage(
|
|
|
193
195
|
_ = cache_dir
|
|
194
196
|
ring_buffer = _OutputRingBuffer()
|
|
195
197
|
state_dir = stage_info["state_dir"]
|
|
198
|
+
project_state_dir = stage_info["project_state_dir"]
|
|
196
199
|
project_root = stage_info["project_root"]
|
|
197
200
|
store = store_mod.store_from_spec(stage_info["store_spec"])
|
|
198
201
|
|
|
@@ -275,7 +278,7 @@ def execute_stage(
|
|
|
275
278
|
stage_outs,
|
|
276
279
|
lock_data["output_hashes"],
|
|
277
280
|
state_db,
|
|
278
|
-
state_dir=
|
|
281
|
+
state_dir=project_state_dir,
|
|
279
282
|
file_hash_entries=restored_entries,
|
|
280
283
|
)
|
|
281
284
|
if restored:
|
|
@@ -330,7 +333,7 @@ def execute_stage(
|
|
|
330
333
|
stage_outs,
|
|
331
334
|
lock_data["output_hashes"],
|
|
332
335
|
state_db,
|
|
333
|
-
state_dir=
|
|
336
|
+
state_dir=project_state_dir,
|
|
334
337
|
file_hash_entries=restored_entries,
|
|
335
338
|
)
|
|
336
339
|
if restored:
|
|
@@ -351,7 +354,7 @@ def execute_stage(
|
|
|
351
354
|
stage_outs,
|
|
352
355
|
store,
|
|
353
356
|
state_db,
|
|
354
|
-
state_dir=
|
|
357
|
+
state_dir=project_state_dir,
|
|
355
358
|
)
|
|
356
359
|
if run_cache_skip is not None:
|
|
357
360
|
if no_commit:
|
|
@@ -404,12 +407,15 @@ def execute_stage(
|
|
|
404
407
|
stage_info["incremental_inputs"],
|
|
405
408
|
lock_data,
|
|
406
409
|
state_dir,
|
|
410
|
+
project_state_dir,
|
|
407
411
|
project_root,
|
|
408
412
|
)
|
|
409
413
|
if no_commit:
|
|
410
414
|
output_hashes = _hash_output_paths(output_paths)
|
|
411
415
|
else:
|
|
412
|
-
output_hashes = _commit_outputs_with_store(
|
|
416
|
+
output_hashes = _commit_outputs_with_store(
|
|
417
|
+
output_paths, store, project_state_dir
|
|
418
|
+
)
|
|
413
419
|
for ref, path in output_paths.items():
|
|
414
420
|
file_hash_entries.extend(
|
|
415
421
|
output_file_hash_entries(
|
|
@@ -819,6 +825,42 @@ def _write_output_with_store(
|
|
|
819
825
|
_reject_path_inside_project(ref, value, project_root, dependency_paths)
|
|
820
826
|
output_path = store.prepare_output(ref)
|
|
821
827
|
writer = cast("Any", ref.format)
|
|
828
|
+
|
|
829
|
+
output_label = (
|
|
830
|
+
f"its {type(writer).__name__} output"
|
|
831
|
+
if ref.identity.key is None
|
|
832
|
+
else f"output {ref.identity.key!r}"
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
def save_output(output_value: Any, destination: pathlib.Path) -> None:
|
|
836
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
837
|
+
warnings.simplefilter("always", loaders.LossyOutputWarning)
|
|
838
|
+
try:
|
|
839
|
+
writer.save(output_value, destination)
|
|
840
|
+
except TypeError as e:
|
|
841
|
+
if isinstance(writer, loaders.YAML):
|
|
842
|
+
raise TypeError(
|
|
843
|
+
f"Stage '{ref.identity.producer}' cannot write {output_label}: {e}"
|
|
844
|
+
) from e
|
|
845
|
+
raise
|
|
846
|
+
for caught_warning in caught:
|
|
847
|
+
if issubclass(caught_warning.category, loaders.LossyOutputWarning):
|
|
848
|
+
logger.warning(
|
|
849
|
+
"Stage '%s' wrote %s %s: %s",
|
|
850
|
+
ref.identity.producer,
|
|
851
|
+
output_label,
|
|
852
|
+
types.LOSSY_OUTPUT_PHRASE,
|
|
853
|
+
caught_warning.message,
|
|
854
|
+
)
|
|
855
|
+
else:
|
|
856
|
+
warnings.warn_explicit(
|
|
857
|
+
caught_warning.message,
|
|
858
|
+
caught_warning.category,
|
|
859
|
+
caught_warning.filename,
|
|
860
|
+
caught_warning.lineno,
|
|
861
|
+
source=caught_warning.source,
|
|
862
|
+
)
|
|
863
|
+
|
|
822
864
|
if ref.tag is types.ArtifactTag.DIRECTORY:
|
|
823
865
|
if not isinstance(value, dict):
|
|
824
866
|
raise RuntimeError(
|
|
@@ -845,11 +887,11 @@ def _write_output_with_store(
|
|
|
845
887
|
members.append((output_path / normalized, item_value))
|
|
846
888
|
for full_path, item_value in members:
|
|
847
889
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
848
|
-
|
|
890
|
+
save_output(item_value, full_path)
|
|
849
891
|
return output_path
|
|
850
892
|
|
|
851
893
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
852
|
-
|
|
894
|
+
save_output(value, output_path)
|
|
853
895
|
return output_path
|
|
854
896
|
|
|
855
897
|
|
|
@@ -885,7 +927,8 @@ def _prepare_incremental_inputs(
|
|
|
885
927
|
incremental_inputs: dict[str, types.ArtifactIdentity],
|
|
886
928
|
lock_data: LockData | None,
|
|
887
929
|
store: store_mod.Store,
|
|
888
|
-
|
|
930
|
+
stage_state_dir: pathlib.Path,
|
|
931
|
+
project_state_dir: pathlib.Path,
|
|
889
932
|
) -> dict[str, Any]:
|
|
890
933
|
"""Restore previous incremental outputs and load them for the stage call."""
|
|
891
934
|
if not incremental_inputs:
|
|
@@ -903,7 +946,7 @@ def _prepare_incremental_inputs(
|
|
|
903
946
|
continue
|
|
904
947
|
|
|
905
948
|
output_hash = lock_data["output_hashes"].get(identity)
|
|
906
|
-
stage_lock = lock.StageLock(stage_name, lock.get_stages_dir(
|
|
949
|
+
stage_lock = lock.StageLock(stage_name, lock.get_stages_dir(stage_state_dir))
|
|
907
950
|
if output_hash is None:
|
|
908
951
|
raise exceptions.CacheRestoreError(
|
|
909
952
|
f"Cache missing for incremental output '{types.identity_key(identity)}'. "
|
|
@@ -915,7 +958,7 @@ def _prepare_incremental_inputs(
|
|
|
915
958
|
if not store.restore_from_cache(
|
|
916
959
|
ref,
|
|
917
960
|
output_hash,
|
|
918
|
-
state_dir=
|
|
961
|
+
state_dir=project_state_dir,
|
|
919
962
|
checkout_modes=[config.CheckoutMode.COPY],
|
|
920
963
|
):
|
|
921
964
|
raise exceptions.CacheRestoreError(
|
|
@@ -941,6 +984,7 @@ def _run_stage_function_with_store(
|
|
|
941
984
|
incremental_inputs: dict[str, types.ArtifactIdentity],
|
|
942
985
|
lock_data: LockData | None,
|
|
943
986
|
state_dir: pathlib.Path,
|
|
987
|
+
project_state_dir: pathlib.Path,
|
|
944
988
|
project_root: pathlib.Path,
|
|
945
989
|
) -> dict[types.ArtifactRef, pathlib.Path]:
|
|
946
990
|
with (
|
|
@@ -969,6 +1013,7 @@ def _run_stage_function_with_store(
|
|
|
969
1013
|
lock_data,
|
|
970
1014
|
store,
|
|
971
1015
|
state_dir,
|
|
1016
|
+
project_state_dir,
|
|
972
1017
|
)
|
|
973
1018
|
)
|
|
974
1019
|
|
|
@@ -1014,12 +1059,17 @@ def _hash_output_paths(
|
|
|
1014
1059
|
def _commit_outputs_with_store(
|
|
1015
1060
|
outputs_by_ref: dict[types.ArtifactRef, pathlib.Path],
|
|
1016
1061
|
store: store_mod.Store,
|
|
1062
|
+
project_state_dir: pathlib.Path,
|
|
1017
1063
|
) -> dict[str, HashInfo]:
|
|
1018
1064
|
output_hashes = dict[str, HashInfo]()
|
|
1019
1065
|
for ref, path in outputs_by_ref.items():
|
|
1020
1066
|
if not path.exists():
|
|
1021
1067
|
raise exceptions.OutputMissingError(f"Stage did not produce output: {path}")
|
|
1022
|
-
|
|
1068
|
+
if isinstance(store, store_mod.WorkspaceStore):
|
|
1069
|
+
output_hash = store.commit(ref, path, state_dir=project_state_dir)
|
|
1070
|
+
else:
|
|
1071
|
+
output_hash = store.commit(ref, path)
|
|
1072
|
+
output_hashes[types.identity_key(ref.identity)] = output_hash
|
|
1023
1073
|
return output_hashes
|
|
1024
1074
|
|
|
1025
1075
|
|