pivot 0.2.1__tar.gz → 0.2.2__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.
Files changed (108) hide show
  1. {pivot-0.2.1 → pivot-0.2.2}/PKG-INFO +1 -1
  2. {pivot-0.2.1 → pivot-0.2.2}/pyproject.toml +1 -1
  3. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/sinks.py +76 -10
  4. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/executor/worker.py +39 -2
  5. pivot-0.2.2/src/pivot/loaders.py +888 -0
  6. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/show/data.py +3 -4
  7. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/types.py +5 -0
  8. pivot-0.2.1/src/pivot/loaders.py +0 -389
  9. {pivot-0.2.1 → pivot-0.2.2}/LICENSE +0 -0
  10. {pivot-0.2.1 → pivot-0.2.2}/README.md +0 -0
  11. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/__init__.py +0 -0
  12. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/ast_utils.py +0 -0
  13. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/AGENTS.md +0 -0
  14. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/CLAUDE.md +0 -0
  15. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/__init__.py +0 -0
  16. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/_run_common.py +0 -0
  17. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/check_ignore.py +0 -0
  18. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/checkout.py +0 -0
  19. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/commit.py +0 -0
  20. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/completion.py +0 -0
  21. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/config.py +0 -0
  22. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/console.py +0 -0
  23. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/dag.py +0 -0
  24. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/data.py +0 -0
  25. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/decorators.py +0 -0
  26. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/doctor.py +0 -0
  27. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/errors.py +0 -0
  28. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/fingerprint.py +0 -0
  29. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/helpers.py +0 -0
  30. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/history.py +0 -0
  31. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/import_cmd.py +0 -0
  32. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/init.py +0 -0
  33. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/list.py +0 -0
  34. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/metrics.py +0 -0
  35. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/params.py +0 -0
  36. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/plots.py +0 -0
  37. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/remote.py +0 -0
  38. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/repro.py +0 -0
  39. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/run.py +0 -0
  40. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/status.py +0 -0
  41. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/targets.py +0 -0
  42. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/track.py +0 -0
  43. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/update.py +0 -0
  44. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/cli/verify.py +0 -0
  45. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/compose.py +0 -0
  46. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/config/__init__.py +0 -0
  47. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/config/io.py +0 -0
  48. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/config/models.py +0 -0
  49. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/dag/__init__.py +0 -0
  50. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/dag/render.py +0 -0
  51. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/decorators.py +0 -0
  52. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/discovery.py +0 -0
  53. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/__init__.py +0 -0
  54. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/agent_rpc.py +0 -0
  55. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/engine.py +0 -0
  56. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/graph.py +0 -0
  57. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/scheduler.py +0 -0
  58. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/sources.py +0 -0
  59. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/types.py +0 -0
  60. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/watch.py +0 -0
  61. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/engine/worker_pool.py +0 -0
  62. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/exceptions.py +0 -0
  63. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/executor/AGENTS.md +0 -0
  64. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/executor/CLAUDE.md +0 -0
  65. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/executor/__init__.py +0 -0
  66. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/executor/commit.py +0 -0
  67. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/executor/core.py +0 -0
  68. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/explain.py +0 -0
  69. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/fingerprint.py +0 -0
  70. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/git.py +0 -0
  71. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/ignore.py +0 -0
  72. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/import_artifact.py +0 -0
  73. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/metrics.py +0 -0
  74. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/names.py +0 -0
  75. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/outputs.py +0 -0
  76. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/parameters.py +0 -0
  77. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/path_policy.py +0 -0
  78. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/project.py +0 -0
  79. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/py.typed +0 -0
  80. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/registry.py +0 -0
  81. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/remote/AGENTS.md +0 -0
  82. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/remote/__init__.py +0 -0
  83. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/remote/config.py +0 -0
  84. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/remote/git_archive.py +0 -0
  85. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/remote/github.py +0 -0
  86. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/remote/storage.py +0 -0
  87. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/remote/sync.py +0 -0
  88. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/run_history.py +0 -0
  89. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/show/__init__.py +0 -0
  90. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/show/common.py +0 -0
  91. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/show/metrics.py +0 -0
  92. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/show/params.py +0 -0
  93. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/show/plots.py +0 -0
  94. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/skip.py +0 -0
  95. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/stage_def.py +0 -0
  96. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/status.py +0 -0
  97. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/AGENTS.md +0 -0
  98. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/CLAUDE.md +0 -0
  99. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/__init__.py +0 -0
  100. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/artifact_lock.py +0 -0
  101. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/cache.py +0 -0
  102. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/lock.py +0 -0
  103. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/presentation.py +0 -0
  104. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/restore.py +0 -0
  105. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/state.py +0 -0
  106. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/store.py +0 -0
  107. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/storage/track.py +0 -0
  108. {pivot-0.2.1 → pivot-0.2.2}/src/pivot/yaml_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pivot
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: High-performance Python pipeline tool with automatic code change detection
5
5
  Author: Sami Jawhar
6
6
  Author-email: Sami Jawhar <sami@metr.org>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pivot"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "High-performance Python pipeline tool with automatic code change detection"
5
5
  requires-python = ">=3.13,<3.14"
6
6
  readme = "README.md"
@@ -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
- # Always buffer for failed-stage output; stream if --show-output
254
+ # Buffer for the report; stream if --show-output
191
255
  stage = event["stage"]
192
- buf = self._stage_logs.setdefault(stage, [])
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
- buf = self._stage_logs.setdefault(stage, [])
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:
@@ -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
@@ -819,6 +820,42 @@ def _write_output_with_store(
819
820
  _reject_path_inside_project(ref, value, project_root, dependency_paths)
820
821
  output_path = store.prepare_output(ref)
821
822
  writer = cast("Any", ref.format)
823
+
824
+ output_label = (
825
+ f"its {type(writer).__name__} output"
826
+ if ref.identity.key is None
827
+ else f"output {ref.identity.key!r}"
828
+ )
829
+
830
+ def save_output(output_value: Any, destination: pathlib.Path) -> None:
831
+ with warnings.catch_warnings(record=True) as caught:
832
+ warnings.simplefilter("always", loaders.LossyOutputWarning)
833
+ try:
834
+ writer.save(output_value, destination)
835
+ except TypeError as e:
836
+ if isinstance(writer, loaders.YAML):
837
+ raise TypeError(
838
+ f"Stage '{ref.identity.producer}' cannot write {output_label}: {e}"
839
+ ) from e
840
+ raise
841
+ for caught_warning in caught:
842
+ if issubclass(caught_warning.category, loaders.LossyOutputWarning):
843
+ logger.warning(
844
+ "Stage '%s' wrote %s %s: %s",
845
+ ref.identity.producer,
846
+ output_label,
847
+ types.LOSSY_OUTPUT_PHRASE,
848
+ caught_warning.message,
849
+ )
850
+ else:
851
+ warnings.warn_explicit(
852
+ caught_warning.message,
853
+ caught_warning.category,
854
+ caught_warning.filename,
855
+ caught_warning.lineno,
856
+ source=caught_warning.source,
857
+ )
858
+
822
859
  if ref.tag is types.ArtifactTag.DIRECTORY:
823
860
  if not isinstance(value, dict):
824
861
  raise RuntimeError(
@@ -845,11 +882,11 @@ def _write_output_with_store(
845
882
  members.append((output_path / normalized, item_value))
846
883
  for full_path, item_value in members:
847
884
  full_path.parent.mkdir(parents=True, exist_ok=True)
848
- writer.save(item_value, full_path)
885
+ save_output(item_value, full_path)
849
886
  return output_path
850
887
 
851
888
  output_path.parent.mkdir(parents=True, exist_ok=True)
852
- writer.save(value, output_path)
889
+ save_output(value, output_path)
853
890
  return output_path
854
891
 
855
892