sheaffoundry 0.1.0a1__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.
- sheaffoundry-0.1.0a1/.gitignore +5 -0
- sheaffoundry-0.1.0a1/PKG-INFO +41 -0
- sheaffoundry-0.1.0a1/README.md +22 -0
- sheaffoundry-0.1.0a1/pyproject.toml +29 -0
- sheaffoundry-0.1.0a1/src/sheaffoundry/__init__.py +4 -0
- sheaffoundry-0.1.0a1/src/sheaffoundry/plan.py +74 -0
- sheaffoundry-0.1.0a1/tests/test_plan.py +17 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sheaffoundry
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Deterministic assembly plans for named inputs and reproducible build receipts.
|
|
5
|
+
Project-URL: Repository, https://github.com/SheafLab/sheaffoundry-python
|
|
6
|
+
Author: SheafLab
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: assembly,plans,provenance,reproducibility,sheaflab
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# sheaffoundry
|
|
21
|
+
|
|
22
|
+
`sheaffoundry` models a small, reviewable assembly plan. A plan names its
|
|
23
|
+
inputs, orders assembly steps, and emits a deterministic receipt suitable for
|
|
24
|
+
logs, test fixtures, or provenance records.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from sheaffoundry import AssemblyPlan, Input, Step
|
|
28
|
+
|
|
29
|
+
plan = AssemblyPlan(
|
|
30
|
+
"preview-image",
|
|
31
|
+
inputs=(Input("source", "sha256:abc"), Input("palette", "v3")),
|
|
32
|
+
steps=(Step("decode"), Step("render", needs=("decode",))),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
receipt = plan.receipt()
|
|
36
|
+
assert receipt.step_order == ("decode", "render")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Steps form a directed acyclic graph. The package validates missing dependencies
|
|
40
|
+
and cycles, then uses lexical tie-breaking to produce a stable execution order.
|
|
41
|
+
There are no runtime dependencies.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# sheaffoundry
|
|
2
|
+
|
|
3
|
+
`sheaffoundry` models a small, reviewable assembly plan. A plan names its
|
|
4
|
+
inputs, orders assembly steps, and emits a deterministic receipt suitable for
|
|
5
|
+
logs, test fixtures, or provenance records.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from sheaffoundry import AssemblyPlan, Input, Step
|
|
9
|
+
|
|
10
|
+
plan = AssemblyPlan(
|
|
11
|
+
"preview-image",
|
|
12
|
+
inputs=(Input("source", "sha256:abc"), Input("palette", "v3")),
|
|
13
|
+
steps=(Step("decode"), Step("render", needs=("decode",))),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
receipt = plan.receipt()
|
|
17
|
+
assert receipt.step_order == ("decode", "render")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Steps form a directed acyclic graph. The package validates missing dependencies
|
|
21
|
+
and cycles, then uses lexical tie-breaking to produce a stable execution order.
|
|
22
|
+
There are no runtime dependencies.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sheaffoundry"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Deterministic assembly plans for named inputs and reproducible build receipts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "SheafLab" }]
|
|
13
|
+
keywords = ["assembly", "provenance", "plans", "reproducibility", "sheaflab"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/SheafLab/sheaffoundry-python"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/sheaffoundry"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Dependency-checked, deterministic assembly plans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class Input:
|
|
10
|
+
name: str
|
|
11
|
+
reference: str
|
|
12
|
+
|
|
13
|
+
def __post_init__(self) -> None:
|
|
14
|
+
if not self.name.strip() or not self.reference.strip():
|
|
15
|
+
raise ValueError("inputs require non-empty names and references")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class Step:
|
|
20
|
+
name: str
|
|
21
|
+
needs: tuple[str, ...] = ()
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if not self.name.strip():
|
|
25
|
+
raise ValueError("steps require a non-empty name")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Receipt:
|
|
30
|
+
plan: str
|
|
31
|
+
inputs: tuple[Input, ...]
|
|
32
|
+
step_order: tuple[str, ...]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class AssemblyPlan:
|
|
37
|
+
name: str
|
|
38
|
+
inputs: tuple[Input, ...] = ()
|
|
39
|
+
steps: tuple[Step, ...] = ()
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
if not self.name.strip():
|
|
43
|
+
raise ValueError("plans require a non-empty name")
|
|
44
|
+
if len({item.name for item in self.inputs}) != len(self.inputs):
|
|
45
|
+
raise ValueError("input names must be unique")
|
|
46
|
+
if len({step.name for step in self.steps}) != len(self.steps):
|
|
47
|
+
raise ValueError("step names must be unique")
|
|
48
|
+
known = {step.name for step in self.steps}
|
|
49
|
+
missing = sorted({dependency for step in self.steps for dependency in step.needs} - known)
|
|
50
|
+
if missing:
|
|
51
|
+
raise ValueError(f"unknown step dependencies: {', '.join(missing)}")
|
|
52
|
+
self.execution_order()
|
|
53
|
+
|
|
54
|
+
def execution_order(self) -> tuple[str, ...]:
|
|
55
|
+
"""Topologically order steps with deterministic tie-breaking."""
|
|
56
|
+
|
|
57
|
+
waiting = {step.name: set(step.needs) for step in self.steps}
|
|
58
|
+
ordered: list[str] = []
|
|
59
|
+
while waiting:
|
|
60
|
+
ready = sorted(name for name, needs in waiting.items() if not needs)
|
|
61
|
+
if not ready:
|
|
62
|
+
raise ValueError("step dependencies contain a cycle")
|
|
63
|
+
for name in ready:
|
|
64
|
+
del waiting[name]
|
|
65
|
+
ordered.append(name)
|
|
66
|
+
completed = set(ready)
|
|
67
|
+
for needs in waiting.values():
|
|
68
|
+
needs.difference_update(completed)
|
|
69
|
+
return tuple(ordered)
|
|
70
|
+
|
|
71
|
+
def receipt(self) -> Receipt:
|
|
72
|
+
"""Capture the plan's stable inputs and execution order."""
|
|
73
|
+
|
|
74
|
+
return Receipt(self.name, tuple(sorted(self.inputs, key=lambda item: item.name)), self.execution_order())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from sheaffoundry import AssemblyPlan, Input, Step
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AssemblyPlanTests(unittest.TestCase):
|
|
7
|
+
def test_execution_order_is_stable(self) -> None:
|
|
8
|
+
plan = AssemblyPlan("demo", steps=(Step("publish", ("build",)), Step("build"), Step("audit")))
|
|
9
|
+
self.assertEqual(plan.execution_order(), ("audit", "build", "publish"))
|
|
10
|
+
|
|
11
|
+
def test_receipt_sorts_inputs(self) -> None:
|
|
12
|
+
plan = AssemblyPlan("demo", inputs=(Input("z", "3"), Input("a", "1")))
|
|
13
|
+
self.assertEqual(tuple(item.name for item in plan.receipt().inputs), ("a", "z"))
|
|
14
|
+
|
|
15
|
+
def test_cycle_is_rejected(self) -> None:
|
|
16
|
+
with self.assertRaises(ValueError):
|
|
17
|
+
AssemblyPlan("cycle", steps=(Step("a", ("b",)), Step("b", ("a",))))
|