pivot 0.0.2__tar.gz → 0.2.0__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 (114) hide show
  1. pivot-0.2.0/LICENSE +21 -0
  2. pivot-0.2.0/PKG-INFO +157 -0
  3. pivot-0.2.0/README.md +76 -0
  4. pivot-0.2.0/pyproject.toml +95 -0
  5. pivot-0.2.0/src/pivot/__init__.py +52 -0
  6. pivot-0.2.0/src/pivot/ast_utils.py +98 -0
  7. pivot-0.2.0/src/pivot/cli/AGENTS.md +116 -0
  8. pivot-0.2.0/src/pivot/cli/CLAUDE.md +1 -0
  9. pivot-0.2.0/src/pivot/cli/__init__.py +193 -0
  10. pivot-0.2.0/src/pivot/cli/_run_common.py +462 -0
  11. pivot-0.2.0/src/pivot/cli/check_ignore.py +97 -0
  12. pivot-0.2.0/src/pivot/cli/checkout.py +459 -0
  13. pivot-0.2.0/src/pivot/cli/commit.py +38 -0
  14. pivot-0.2.0/src/pivot/cli/completion.py +320 -0
  15. pivot-0.2.0/src/pivot/cli/config.py +157 -0
  16. pivot-0.2.0/src/pivot/cli/console.py +161 -0
  17. pivot-0.2.0/src/pivot/cli/dag.py +112 -0
  18. pivot-0.2.0/src/pivot/cli/data.py +277 -0
  19. pivot-0.2.0/src/pivot/cli/decorators.py +253 -0
  20. pivot-0.2.0/src/pivot/cli/doctor.py +358 -0
  21. pivot-0.2.0/src/pivot/cli/errors.py +21 -0
  22. pivot-0.2.0/src/pivot/cli/fingerprint.py +27 -0
  23. pivot-0.2.0/src/pivot/cli/helpers.py +183 -0
  24. pivot-0.2.0/src/pivot/cli/history.py +143 -0
  25. pivot-0.2.0/src/pivot/cli/import_cmd.py +31 -0
  26. pivot-0.2.0/src/pivot/cli/init.py +95 -0
  27. pivot-0.2.0/src/pivot/cli/list.py +114 -0
  28. pivot-0.2.0/src/pivot/cli/metrics.py +123 -0
  29. pivot-0.2.0/src/pivot/cli/params.py +102 -0
  30. pivot-0.2.0/src/pivot/cli/plots.py +112 -0
  31. pivot-0.2.0/src/pivot/cli/remote.py +372 -0
  32. pivot-0.2.0/src/pivot/cli/repro.py +995 -0
  33. pivot-0.2.0/src/pivot/cli/run.py +459 -0
  34. pivot-0.2.0/src/pivot/cli/status.py +466 -0
  35. pivot-0.2.0/src/pivot/cli/targets.py +302 -0
  36. pivot-0.2.0/src/pivot/cli/track.py +219 -0
  37. pivot-0.2.0/src/pivot/cli/update.py +127 -0
  38. pivot-0.2.0/src/pivot/cli/verify.py +371 -0
  39. pivot-0.2.0/src/pivot/compose.py +1440 -0
  40. pivot-0.2.0/src/pivot/config/__init__.py +36 -0
  41. pivot-0.2.0/src/pivot/config/io.py +377 -0
  42. pivot-0.2.0/src/pivot/config/models.py +304 -0
  43. pivot-0.2.0/src/pivot/dag/__init__.py +11 -0
  44. pivot-0.2.0/src/pivot/dag/render.py +375 -0
  45. pivot-0.2.0/src/pivot/decorators.py +24 -0
  46. pivot-0.2.0/src/pivot/discovery.py +595 -0
  47. pivot-0.2.0/src/pivot/engine/__init__.py +7 -0
  48. pivot-0.2.0/src/pivot/engine/agent_rpc.py +863 -0
  49. pivot-0.2.0/src/pivot/engine/engine.py +2744 -0
  50. pivot-0.2.0/src/pivot/engine/graph.py +535 -0
  51. pivot-0.2.0/src/pivot/engine/scheduler.py +214 -0
  52. pivot-0.2.0/src/pivot/engine/sinks.py +446 -0
  53. pivot-0.2.0/src/pivot/engine/sources.py +211 -0
  54. pivot-0.2.0/src/pivot/engine/types.py +291 -0
  55. pivot-0.2.0/src/pivot/engine/watch.py +159 -0
  56. pivot-0.2.0/src/pivot/engine/worker_pool.py +90 -0
  57. pivot-0.2.0/src/pivot/exceptions.py +378 -0
  58. pivot-0.2.0/src/pivot/executor/AGENTS.md +38 -0
  59. pivot-0.2.0/src/pivot/executor/CLAUDE.md +1 -0
  60. pivot-0.2.0/src/pivot/executor/__init__.py +8 -0
  61. pivot-0.2.0/src/pivot/executor/commit.py +228 -0
  62. pivot-0.2.0/src/pivot/executor/core.py +461 -0
  63. pivot-0.2.0/src/pivot/executor/worker.py +1443 -0
  64. pivot-0.2.0/src/pivot/explain.py +298 -0
  65. pivot-0.2.0/src/pivot/fingerprint.py +2635 -0
  66. pivot-0.2.0/src/pivot/git.py +308 -0
  67. pivot-0.2.0/src/pivot/ignore.py +363 -0
  68. pivot-0.2.0/src/pivot/import_artifact.py +501 -0
  69. pivot-0.2.0/src/pivot/loaders.py +389 -0
  70. pivot-0.2.0/src/pivot/metrics.py +117 -0
  71. pivot-0.2.0/src/pivot/names.py +40 -0
  72. pivot-0.2.0/src/pivot/outputs.py +25 -0
  73. pivot-0.2.0/src/pivot/parameters.py +751 -0
  74. pivot-0.2.0/src/pivot/path_policy.py +214 -0
  75. pivot-0.2.0/src/pivot/project.py +125 -0
  76. pivot-0.2.0/src/pivot/py.typed +1 -0
  77. pivot-0.2.0/src/pivot/registry.py +355 -0
  78. pivot-0.2.0/src/pivot/remote/AGENTS.md +60 -0
  79. pivot-0.2.0/src/pivot/remote/__init__.py +6 -0
  80. pivot-0.2.0/src/pivot/remote/config.py +159 -0
  81. pivot-0.2.0/src/pivot/remote/git_archive.py +156 -0
  82. pivot-0.2.0/src/pivot/remote/github.py +181 -0
  83. pivot-0.2.0/src/pivot/remote/storage.py +659 -0
  84. pivot-0.2.0/src/pivot/remote/sync.py +526 -0
  85. pivot-0.2.0/src/pivot/run_history.py +185 -0
  86. pivot-0.2.0/src/pivot/show/__init__.py +6 -0
  87. pivot-0.2.0/src/pivot/show/common.py +136 -0
  88. pivot-0.2.0/src/pivot/show/data.py +768 -0
  89. pivot-0.2.0/src/pivot/show/metrics.py +351 -0
  90. pivot-0.2.0/src/pivot/show/params.py +222 -0
  91. pivot-0.2.0/src/pivot/show/plots.py +208 -0
  92. pivot-0.2.0/src/pivot/skip.py +238 -0
  93. pivot-0.2.0/src/pivot/stage_def.py +83 -0
  94. pivot-0.2.0/src/pivot/status.py +567 -0
  95. pivot-0.2.0/src/pivot/storage/AGENTS.md +63 -0
  96. pivot-0.2.0/src/pivot/storage/CLAUDE.md +1 -0
  97. pivot-0.2.0/src/pivot/storage/__init__.py +7 -0
  98. pivot-0.2.0/src/pivot/storage/artifact_lock.py +166 -0
  99. pivot-0.2.0/src/pivot/storage/cache.py +823 -0
  100. pivot-0.2.0/src/pivot/storage/lock.py +345 -0
  101. pivot-0.2.0/src/pivot/storage/presentation.py +86 -0
  102. pivot-0.2.0/src/pivot/storage/restore.py +421 -0
  103. pivot-0.2.0/src/pivot/storage/state.py +1075 -0
  104. pivot-0.2.0/src/pivot/storage/store.py +335 -0
  105. pivot-0.2.0/src/pivot/storage/track.py +170 -0
  106. pivot-0.2.0/src/pivot/types.py +956 -0
  107. pivot-0.2.0/src/pivot/yaml_config.py +13 -0
  108. pivot-0.0.2/PKG-INFO +0 -7
  109. pivot-0.0.2/pivot.egg-info/PKG-INFO +0 -7
  110. pivot-0.0.2/pivot.egg-info/SOURCES.txt +0 -5
  111. pivot-0.0.2/pivot.egg-info/dependency_links.txt +0 -1
  112. pivot-0.0.2/pivot.egg-info/top_level.txt +0 -1
  113. pivot-0.0.2/pyproject.toml +0 -13
  114. pivot-0.0.2/setup.cfg +0 -4
pivot-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sami Jawhar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pivot-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: pivot
3
+ Version: 0.2.0
4
+ Summary: High-performance Python pipeline tool with automatic code change detection
5
+ Author: Sami Jawhar
6
+ Author-email: Sami Jawhar <sami@metr.org>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Operating System :: POSIX
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: aiohttp>=3.9
13
+ Requires-Dist: anyio>=4.11
14
+ Requires-Dist: click>=8.0
15
+ Requires-Dist: cloudpickle>=3.0
16
+ Requires-Dist: deepmerge>=2.0
17
+ Requires-Dist: dulwich>=0.22.0
18
+ Requires-Dist: filelock>=3.12.0
19
+ Requires-Dist: flatten-dict>=0.4.0
20
+ Requires-Dist: grandalf>=0.8
21
+ Requires-Dist: lmdb>=1.4.0
22
+ Requires-Dist: loky>=3.4
23
+ Requires-Dist: networkx>=3.0
24
+ Requires-Dist: pathspec>=0.12.0
25
+ Requires-Dist: pandas>=2.0
26
+ Requires-Dist: pydantic>=2.13
27
+ Requires-Dist: pygtrie>=2.5.0
28
+ Requires-Dist: pyyaml>=6.0
29
+ Requires-Dist: rich>=13.0
30
+ Requires-Dist: ruamel-yaml>=0.18.0
31
+ Requires-Dist: tabulate>=0.9.0
32
+ Requires-Dist: tqdm>=4.66
33
+ Requires-Dist: watchfiles>=1.0
34
+ Requires-Dist: wcwidth>=0.2.0
35
+ Requires-Dist: xxhash>=3.0
36
+ Requires-Dist: basedpyright>=1.0 ; extra == 'dev'
37
+ Requires-Dist: botocore-stubs>=1.35.0 ; extra == 'dev'
38
+ Requires-Dist: hypothesis>=6.140 ; extra == 'dev'
39
+ Requires-Dist: joblib>=1.4.0 ; extra == 'dev'
40
+ Requires-Dist: joblib-stubs>=1.5.0 ; extra == 'dev'
41
+ Requires-Dist: matplotlib-stubs>=0.3 ; extra == 'dev'
42
+ Requires-Dist: mutmut>=2.5.1,<3 ; extra == 'dev'
43
+ Requires-Dist: mypy>=1.19.1 ; extra == 'dev'
44
+ Requires-Dist: pandas-stubs>=2.0 ; extra == 'dev'
45
+ Requires-Dist: pivot[dvc] ; extra == 'dev'
46
+ Requires-Dist: pivot[matplotlib] ; extra == 'dev'
47
+ Requires-Dist: pivot[s3] ; extra == 'dev'
48
+ Requires-Dist: pytest-aioboto3>=0.1.0 ; extra == 'dev'
49
+ Requires-Dist: pytest-asyncio>=0.24.0 ; extra == 'dev'
50
+ Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
51
+ Requires-Dist: pytest-mock>=3.15.1 ; extra == 'dev'
52
+ Requires-Dist: pytest-rerunfailures>=16.1 ; extra == 'dev'
53
+ Requires-Dist: pytest-watcher>=0.6.2 ; extra == 'dev'
54
+ Requires-Dist: pytest-xdist>=3.8.0 ; extra == 'dev'
55
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
56
+ Requires-Dist: ruff>=0.8.0 ; extra == 'dev'
57
+ Requires-Dist: types-aioboto3[all]>=13.0.0 ; extra == 'dev'
58
+ Requires-Dist: mkdocs>=1.6.0 ; extra == 'docs'
59
+ Requires-Dist: mkdocs-material>=9.5.0 ; extra == 'docs'
60
+ Requires-Dist: mkdocstrings[python]>=0.26.0 ; extra == 'docs'
61
+ Requires-Dist: mkdocs-gen-files>=0.5.0 ; extra == 'docs'
62
+ Requires-Dist: mkdocs-literate-nav>=0.6.0 ; extra == 'docs'
63
+ Requires-Dist: mkdocs-section-index>=0.3.9 ; extra == 'docs'
64
+ Requires-Dist: griffe>=1.3.0 ; extra == 'docs'
65
+ Requires-Dist: dvc>=3.0 ; extra == 'dvc'
66
+ Requires-Dist: matplotlib>=3.5 ; extra == 'matplotlib'
67
+ Requires-Dist: aioboto3>=13.0.0 ; extra == 's3'
68
+ Requires-Dist: pivot-tui ; extra == 'tui'
69
+ Requires-Python: >=3.13, <3.14
70
+ Project-URL: Documentation, https://sjawhar.github.io/pivot/
71
+ Project-URL: Homepage, https://github.com/sjawhar/pivot
72
+ Project-URL: Issues, https://github.com/sjawhar/pivot/issues
73
+ Project-URL: Repository, https://github.com/sjawhar/pivot
74
+ Provides-Extra: dev
75
+ Provides-Extra: docs
76
+ Provides-Extra: dvc
77
+ Provides-Extra: matplotlib
78
+ Provides-Extra: s3
79
+ Provides-Extra: tui
80
+ Description-Content-Type: text/markdown
81
+
82
+ # Pivot: High-Performance Python Pipeline Tool
83
+
84
+ **Change your code. Pivot knows what to run.**
85
+
86
+ Pivot is a Python pipeline tool with automatic code change detection. Stages are plain
87
+ Python functions; you wire them together by calling them inside a `Pipeline` block, and
88
+ Pivot works out what needs to re-run.
89
+
90
+ - **Automatic code change detection** using Python introspection
91
+ - **No paths in pipeline code**: Pivot chooses where every output is stored
92
+ - **Per-stage lock files** for fast parallel writes
93
+ - **Warm worker pools** with preloaded imports
94
+ - **Content-addressable caching** with S3 remote storage
95
+
96
+ **Python:** 3.13+ | **Platform:** Unix (Linux/macOS)
97
+
98
+ ## Quick Start
99
+
100
+ ```bash
101
+ pip install pivot
102
+ mkdir my_project && cd my_project
103
+ git init && pivot init
104
+ ```
105
+
106
+ Put a raw input under `data/raw/`:
107
+
108
+ ```bash
109
+ mkdir -p data/raw
110
+ printf 'name,value\nalice,100\nbob,200\ncarol,\n' > data/raw/records.csv
111
+ ```
112
+
113
+ Define stages and the pipeline in `pipeline.py`:
114
+
115
+ ```python
116
+ # pipeline.py
117
+ import pandas as pd
118
+
119
+ from pivot import Pipeline, StageParams, stage
120
+
121
+
122
+ class CleanParams(StageParams):
123
+ min_value: int = 0
124
+
125
+
126
+ @stage
127
+ def clean(records: pd.DataFrame, params: CleanParams) -> pd.DataFrame:
128
+ kept = records.dropna()
129
+ return kept[kept["value"] >= params.min_value]
130
+
131
+
132
+ @stage
133
+ def summarize(rows: pd.DataFrame) -> dict:
134
+ return {"count": len(rows), "total": int(rows["value"].sum())}
135
+
136
+
137
+ with Pipeline("demo") as pipeline:
138
+ records = pipeline.input("records.csv")
139
+ cleaned = clean(records, params=CleanParams(min_value=50))
140
+ summarize(cleaned)
141
+ ```
142
+
143
+ Then run it; Pivot resolves the DAG from the handles passed between stages:
144
+
145
+ ```bash
146
+ pivot repro # runs clean, then summarize
147
+ pivot repro # nothing changed: both stages are cached
148
+ pivot repro --watch # re-run automatically on file changes
149
+ ```
150
+
151
+ Pivot writes outputs to `data/demo/clean.jsonl` and `data/demo/summarize.yaml`.
152
+
153
+ ## Learn More
154
+
155
+ - [Documentation](https://sjawhar.github.io/pivot/)
156
+ - [Source code](https://github.com/sjawhar/pivot)
157
+ - Interactive TUI: `pip install pivot[tui]`, then `pivot repro --tui`
pivot-0.2.0/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Pivot: High-Performance Python Pipeline Tool
2
+
3
+ **Change your code. Pivot knows what to run.**
4
+
5
+ Pivot is a Python pipeline tool with automatic code change detection. Stages are plain
6
+ Python functions; you wire them together by calling them inside a `Pipeline` block, and
7
+ Pivot works out what needs to re-run.
8
+
9
+ - **Automatic code change detection** using Python introspection
10
+ - **No paths in pipeline code**: Pivot chooses where every output is stored
11
+ - **Per-stage lock files** for fast parallel writes
12
+ - **Warm worker pools** with preloaded imports
13
+ - **Content-addressable caching** with S3 remote storage
14
+
15
+ **Python:** 3.13+ | **Platform:** Unix (Linux/macOS)
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ pip install pivot
21
+ mkdir my_project && cd my_project
22
+ git init && pivot init
23
+ ```
24
+
25
+ Put a raw input under `data/raw/`:
26
+
27
+ ```bash
28
+ mkdir -p data/raw
29
+ printf 'name,value\nalice,100\nbob,200\ncarol,\n' > data/raw/records.csv
30
+ ```
31
+
32
+ Define stages and the pipeline in `pipeline.py`:
33
+
34
+ ```python
35
+ # pipeline.py
36
+ import pandas as pd
37
+
38
+ from pivot import Pipeline, StageParams, stage
39
+
40
+
41
+ class CleanParams(StageParams):
42
+ min_value: int = 0
43
+
44
+
45
+ @stage
46
+ def clean(records: pd.DataFrame, params: CleanParams) -> pd.DataFrame:
47
+ kept = records.dropna()
48
+ return kept[kept["value"] >= params.min_value]
49
+
50
+
51
+ @stage
52
+ def summarize(rows: pd.DataFrame) -> dict:
53
+ return {"count": len(rows), "total": int(rows["value"].sum())}
54
+
55
+
56
+ with Pipeline("demo") as pipeline:
57
+ records = pipeline.input("records.csv")
58
+ cleaned = clean(records, params=CleanParams(min_value=50))
59
+ summarize(cleaned)
60
+ ```
61
+
62
+ Then run it; Pivot resolves the DAG from the handles passed between stages:
63
+
64
+ ```bash
65
+ pivot repro # runs clean, then summarize
66
+ pivot repro # nothing changed: both stages are cached
67
+ pivot repro --watch # re-run automatically on file changes
68
+ ```
69
+
70
+ Pivot writes outputs to `data/demo/clean.jsonl` and `data/demo/summarize.yaml`.
71
+
72
+ ## Learn More
73
+
74
+ - [Documentation](https://sjawhar.github.io/pivot/)
75
+ - [Source code](https://github.com/sjawhar/pivot)
76
+ - Interactive TUI: `pip install pivot[tui]`, then `pivot repro --tui`
@@ -0,0 +1,95 @@
1
+ [project]
2
+ name = "pivot"
3
+ version = "0.2.0"
4
+ description = "High-performance Python pipeline tool with automatic code change detection"
5
+ requires-python = ">=3.13,<3.14"
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Sami Jawhar", email = "sami@metr.org" }]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Operating System :: POSIX",
13
+ "Programming Language :: Python :: 3.13",
14
+ ]
15
+ dependencies = [
16
+ "aiohttp>=3.9",
17
+ "anyio>=4.11",
18
+ "click>=8.0",
19
+ "cloudpickle>=3.0",
20
+ "deepmerge>=2.0",
21
+ "dulwich>=0.22.0",
22
+ "filelock>=3.12.0",
23
+ "flatten-dict>=0.4.0",
24
+ "grandalf>=0.8",
25
+ "lmdb>=1.4.0",
26
+ "loky>=3.4",
27
+ "networkx>=3.0",
28
+ "pathspec>=0.12.0",
29
+ "pandas>=2.0",
30
+ "pydantic>=2.13",
31
+ "pygtrie>=2.5.0",
32
+ "pyyaml>=6.0",
33
+ "rich>=13.0",
34
+ "ruamel.yaml>=0.18.0",
35
+ "tabulate>=0.9.0",
36
+ "tqdm>=4.66",
37
+ "watchfiles>=1.0",
38
+ "wcwidth>=0.2.0",
39
+ "xxhash>=3.0",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ dvc = ["dvc>=3.0"]
44
+ matplotlib = ["matplotlib>=3.5"]
45
+ s3 = ["aioboto3>=13.0.0"]
46
+ tui = ["pivot-tui"]
47
+ docs = [
48
+ "mkdocs>=1.6.0",
49
+ "mkdocs-material>=9.5.0",
50
+ "mkdocstrings[python]>=0.26.0",
51
+ "mkdocs-gen-files>=0.5.0",
52
+ "mkdocs-literate-nav>=0.6.0",
53
+ "mkdocs-section-index>=0.3.9",
54
+ "griffe>=1.3.0",
55
+ ]
56
+ dev = [
57
+ "basedpyright>=1.0",
58
+ "botocore-stubs>=1.35.0",
59
+ "hypothesis>=6.140",
60
+ "joblib>=1.4.0",
61
+ "joblib-stubs>=1.5.0",
62
+ "matplotlib-stubs>=0.3",
63
+ "mutmut>=2.5.1,<3",
64
+ "mypy>=1.19.1",
65
+ "pandas-stubs>=2.0",
66
+ "pivot[dvc]",
67
+ "pivot[matplotlib]",
68
+ "pivot[s3]",
69
+ "pytest-aioboto3>=0.1.0",
70
+ "pytest-asyncio>=0.24.0",
71
+ "pytest-cov>=4.0",
72
+ "pytest-mock>=3.15.1",
73
+ "pytest-rerunfailures>=16.1",
74
+ "pytest-watcher>=0.6.2",
75
+ "pytest-xdist>=3.8.0",
76
+ "pytest>=7.0",
77
+ "ruff>=0.8.0",
78
+ "types-aioboto3[all]>=13.0.0",
79
+ ]
80
+
81
+ [project.scripts]
82
+ pivot = "pivot.cli:main"
83
+
84
+ [project.urls]
85
+ Homepage = "https://github.com/sjawhar/pivot"
86
+ Documentation = "https://sjawhar.github.io/pivot/"
87
+ Repository = "https://github.com/sjawhar/pivot"
88
+ Issues = "https://github.com/sjawhar/pivot/issues"
89
+
90
+ [build-system]
91
+ requires = ["uv_build>=0.9.18,<0.10.0"]
92
+ build-backend = "uv_build"
93
+
94
+ [tool.uv.sources]
95
+ pivot-tui = { workspace = true }
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ __version__ = "0.2.0a1"
6
+
7
+ # Public API - only exports that users need when writing pipelines.
8
+ # Internal modules are accessible via their full paths.
9
+
10
+ if TYPE_CHECKING:
11
+ from pivot import loaders as loaders
12
+ from pivot import stage_def as stage_def
13
+ from pivot.compose import Pipeline as Pipeline
14
+ from pivot.compose import incremental as incremental
15
+ from pivot.compose import metric as metric
16
+ from pivot.compose import plot as plot
17
+ from pivot.compose import stage as stage
18
+ from pivot.decorators import no_fingerprint as no_fingerprint
19
+ from pivot.stage_def import StageParams as StageParams
20
+
21
+ # Lazy import mapping for runtime: (module_path, attr_name or None for module import)
22
+ _LAZY_IMPORTS: dict[str, tuple[str, str | None]] = {
23
+ "no_fingerprint": ("pivot.decorators", "no_fingerprint"),
24
+ "loaders": ("pivot.loaders", None),
25
+ "stage_def": ("pivot.stage_def", None),
26
+ "Pipeline": ("pivot.compose", "Pipeline"),
27
+ "stage": ("pivot.compose", "stage"),
28
+ "metric": ("pivot.compose", "metric"),
29
+ "incremental": ("pivot.compose", "incremental"),
30
+ "plot": ("pivot.compose", "plot"),
31
+ "StageParams": ("pivot.stage_def", "StageParams"),
32
+ }
33
+
34
+
35
+ def __getattr__(name: str) -> object:
36
+ """Lazily import public API members on first access."""
37
+ if name in _LAZY_IMPORTS:
38
+ module_path, attr_name = _LAZY_IMPORTS[name]
39
+ import importlib
40
+
41
+ module = importlib.import_module(module_path)
42
+ value = module if attr_name is None else getattr(module, attr_name)
43
+ # Cache in module globals for subsequent access
44
+ globals()[name] = value
45
+ return value
46
+ msg = f"module {__name__!r} has no attribute {name!r}"
47
+ raise AttributeError(msg)
48
+
49
+
50
+ def __dir__() -> list[str]:
51
+ """List available attributes including lazy imports."""
52
+ return list(globals().keys()) + list(_LAZY_IMPORTS.keys())
@@ -0,0 +1,98 @@
1
+ import ast
2
+ import contextlib
3
+ import inspect
4
+ import textwrap
5
+ import weakref
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ # Cache for extract_module_attr_usage results using weak references.
10
+ # This avoids repeated AST parsing for the same function when processing module dependencies.
11
+ # Note: WeakKeyDictionary is not thread-safe. Fingerprinting runs single-threaded
12
+ # per process (multiprocessing uses separate memory spaces), so this is safe.
13
+ _module_attr_cache: weakref.WeakKeyDictionary[Callable[..., Any], list[tuple[str, str]]] = (
14
+ weakref.WeakKeyDictionary()
15
+ )
16
+
17
+
18
+ def extract_module_attr_usage(func: Callable[..., Any]) -> list[tuple[str, str]]:
19
+ """Extract module.attr patterns (e.g., 'np.array') from function AST."""
20
+ # WeakKeyDictionary raises TypeError for non-weakly-referenceable functions (builtins)
21
+ with contextlib.suppress(TypeError):
22
+ cached = _module_attr_cache.get(func)
23
+ if cached is not None:
24
+ return cached
25
+
26
+ result = _extract_module_attr_usage_impl(func)
27
+
28
+ with contextlib.suppress(TypeError):
29
+ _module_attr_cache[func] = result
30
+
31
+ return result
32
+
33
+
34
+ def _extract_module_attr_usage_impl(func: Callable[..., Any]) -> list[tuple[str, str]]:
35
+ """Internal implementation of extract_module_attr_usage."""
36
+ try:
37
+ source = inspect.getsource(func)
38
+ except (OSError, TypeError):
39
+ return []
40
+
41
+ try:
42
+ tree = ast.parse(textwrap.dedent(source))
43
+ except SyntaxError:
44
+ return []
45
+
46
+ attrs = list[tuple[str, str]]()
47
+
48
+ for node in ast.walk(tree):
49
+ if isinstance(node, ast.Attribute):
50
+ chain = [node.attr]
51
+ current = node.value
52
+
53
+ while isinstance(current, ast.Attribute):
54
+ chain.append(current.attr)
55
+ current = current.value
56
+
57
+ if isinstance(current, ast.Name):
58
+ module_name = current.id
59
+ # Reverse chain to get correct order (module.sub.attr)
60
+ attr_path = ".".join(reversed(chain))
61
+ attrs.append((module_name, attr_path))
62
+
63
+ return list(dict.fromkeys(attrs))
64
+
65
+
66
+ def get_function_ast(func: Callable[..., Any]) -> ast.FunctionDef | ast.AsyncFunctionDef:
67
+ """Parse function to AST node."""
68
+ try:
69
+ source = inspect.getsource(func)
70
+ except (OSError, TypeError) as e:
71
+ raise ValueError(f"Cannot get source for {func}") from e
72
+
73
+ tree = ast.parse(textwrap.dedent(source))
74
+
75
+ for node in ast.walk(tree):
76
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
77
+ return node
78
+
79
+ raise ValueError(f"No FunctionDef found in source for {func}")
80
+
81
+
82
+ def normalize_ast(node: ast.AST) -> ast.AST:
83
+ """Remove docstrings and metadata for stable AST comparison."""
84
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and (
85
+ node.body
86
+ and isinstance(node.body[0], ast.Expr)
87
+ and isinstance(node.body[0].value, ast.Constant)
88
+ and isinstance(node.body[0].value.value, str)
89
+ ):
90
+ node.body = node.body[1:]
91
+ # Add Pass() if body becomes empty (functions must have non-empty body)
92
+ if not node.body:
93
+ node.body = [ast.Pass()]
94
+
95
+ for child in ast.iter_child_nodes(node):
96
+ normalize_ast(child)
97
+
98
+ return node
@@ -0,0 +1,116 @@
1
+ # Pivot CLI - Development Guidelines
2
+
3
+ ## The `pivot_command` Decorator (Critical)
4
+
5
+ **All new CLI commands MUST use `@cli_decorators.pivot_command()` instead of `@click.command()`.**
6
+
7
+ The `pivot_command` decorator wraps Click commands with two essential behaviors:
8
+
9
+ 1. **Auto-discovery** - Discovers `pipeline.py` (`discovery.discover_pipeline()`) and stores the Pipeline in the Click context before the command runs
10
+ 2. **Error handling** - Converts `PivotError` exceptions to user-friendly `ClickException` messages
11
+
12
+ ```python
13
+ from pivot.cli import decorators as cli_decorators
14
+
15
+ # Standard command - auto-discovers stages before running
16
+ @cli_decorators.pivot_command()
17
+ def list_cmd() -> None:
18
+ """List registered stages."""
19
+ # Pipeline is guaranteed to be in context here
20
+ stages = cli_helpers.list_stages()
21
+ ...
22
+
23
+ # Command that doesn't need the registry
24
+ @cli_decorators.pivot_command(auto_discover=False)
25
+ def init() -> None:
26
+ """Initialize new project."""
27
+ # No discovery needed - creating new project
28
+ ...
29
+ ```
30
+
31
+ ### When to use `auto_discover=False`
32
+
33
+ Set `auto_discover=False` only for commands that don't use the stage registry:
34
+
35
+ | Command | auto_discover | Reason |
36
+ |---------|---------------|--------|
37
+ | run | True (default) | Need registry to find stages |
38
+ | list, dag | True (default), allow_all=True | Need registry; --all for multi-pipeline |
39
+ | checkout | True (default) | Need registry; handles None pipeline gracefully |
40
+ | track | True (default) | Handles None pipeline gracefully (no overlap check) |
41
+ | status | True (default), allow_all=True | Handles None pipeline gracefully (skips stages section) |
42
+ | init, doctor, check-ignore | False | No stage data needed |
43
+ | history, show | False | Read run history only |
44
+ | get | False | Restores files and `.pvt` targets from git; discovers the pipeline itself for stage targets, and a broken `pipeline.py` only stops stage targets |
45
+ | import, update | False | Work from git revisions, lock files and `.pvt` files |
46
+ | push, pull, fetch | True (default) | Need registry for output cache filtering |
47
+
48
+ **Principle:** The pipeline registry is the canonical source of stage metadata. Commands that inspect stage properties (outputs, tags, deps) must use auto-discovery. Only commands that truly don't touch stage data should use `auto_discover=False`.
49
+
50
+ ### Why this matters
51
+
52
+ Without auto-discovery, commands fail if called before any other pivot command:
53
+
54
+ ```bash
55
+ # In a fresh shell, this would fail without auto-discovery:
56
+ $ pivot list
57
+ No stages registered. # Wrong! Should discover pipeline.py first
58
+ ```
59
+
60
+ The decorator ensures consistent behavior regardless of command order.
61
+
62
+ ### For group subcommands
63
+
64
+ Commands under a `@click.group()` (like `pivot metrics show`) can't use `pivot_command`. Use `@with_error_handling` and call `ensure_stages_registered()` explicitly:
65
+
66
+ ```python
67
+ from pivot.cli._run_common import ensure_stages_registered
68
+
69
+ @metrics.command("show")
70
+ @cli_decorators.with_error_handling
71
+ def metrics_show() -> None:
72
+ ensure_stages_registered() # Manual discovery for group subcommands
73
+ ...
74
+ ```
75
+
76
+ ## Input Validation (Critical)
77
+
78
+ **Validate inputs as early as possible** - use Click's built-in validation in option/argument decorators.
79
+
80
+ ### Numeric Options
81
+
82
+ Always use `click.IntRange` or `click.FloatRange` for numeric inputs:
83
+
84
+ ```python
85
+ # Good - validates at argument parsing time
86
+ @click.option("--precision", type=click.IntRange(min=0), default=5)
87
+ @click.option("--jobs", type=click.IntRange(min=1), default=20)
88
+ @click.option("--debounce", type=click.IntRange(min=0), default=300)
89
+
90
+ # Bad - allows invalid values through
91
+ @click.option("--precision", type=int, default=5) # Allows negative!
92
+ ```
93
+
94
+ ### Path Options
95
+
96
+ Use `click.Path` with appropriate parameters:
97
+
98
+ ```python
99
+ @click.option("--output", type=click.Path(path_type=pathlib.Path))
100
+ @click.option("--config", type=click.Path(exists=True, dir_okay=False))
101
+ ```
102
+
103
+ ### Choice Options
104
+
105
+ Use `click.Choice` for limited valid values:
106
+
107
+ ```python
108
+ @click.option("--format", type=click.Choice(["json", "yaml", "csv"]))
109
+ ```
110
+
111
+ ### Why Early Validation Matters
112
+
113
+ 1. **Better error messages** - Click provides user-friendly error messages automatically
114
+ 2. **Fail fast** - Don't waste time processing before discovering invalid input
115
+ 3. **Type safety** - Validated inputs have correct types in the function body
116
+ 4. **Consistency** - Users get the same error format for all validation failures
@@ -0,0 +1 @@
1
+ @AGENTS.md