triangulum 0.40.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.
@@ -0,0 +1,10 @@
1
+ # Bundled JS entrypoints, built by `python tasks.py entrypoints`
2
+ /src/triangulum/js/*.js
3
+ # Bun binaries, downloaded by `python tasks.py download`
4
+ /vendor/
5
+ # Build output
6
+ /dist/
7
+ *.egg-info/
8
+ __pycache__/
9
+ .pytest_cache/
10
+ .venv/
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.5
2
+ Name: triangulum
3
+ Version: 0.40.2
4
+ Summary: Triangulum is the CodeZero validation layer
5
+ Project-URL: Homepage, https://github.com/code0-tech/triangulum
6
+ Project-URL: Changelog, https://github.com/code0-tech/triangulum/releases
7
+ Author: CodeZero
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: tucana>=0.0.83
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Triangulum Python Package
14
+
15
+ Python bindings for the [Triangulum](https://github.com/code0-tech/triangulum) validation layer. This package wraps the TypeScript library and a bundled [Bun](https://bun.sh) runtime to validate flows using the TypeScript compiler.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install triangulum # or: uv add triangulum
21
+ ```
22
+
23
+ Platform specific wheels with a bundled bun binary are published for:
24
+
25
+ - `macosx_11_0_arm64` (macOS Apple Silicon)
26
+ - `macosx_10_12_x86_64` (macOS Intel)
27
+ - `manylinux_2_17_x86_64` (Linux x64)
28
+ - `musllinux_1_2_x86_64` (Linux x64 musl)
29
+ - `manylinux_2_17_aarch64` (Linux ARM64)
30
+ - `musllinux_1_2_aarch64` (Linux ARM64 musl)
31
+
32
+ For any other platform pip falls back to the pure python wheel, which requires `bun` on `PATH`.
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from triangulum import Validation
38
+
39
+ result = Validation(flow, function_definitions, data_types).validate()
40
+
41
+ result.is_valid # => True / False
42
+ result.return_type # => "void"
43
+ result.diagnostics # => [Diagnostic, ...]
44
+ ```
45
+
46
+ The arguments are [Tucana](https://github.com/code0-tech/tucana) protobuf messages:
47
+
48
+ - `flow` — `tucana.generated.shared.flow_pb2.ValidationFlow`
49
+ - `function_definitions` — `list[tucana.generated.shared.function_pb2.FunctionDefinition]`
50
+ - `data_types` — `list[tucana.generated.shared.data_type_pb2.DefinitionDataType]`
51
+
52
+ ### Diagnostics
53
+
54
+ Each diagnostic contains:
55
+
56
+ | Field | Description |
57
+ |---|---|
58
+ | `message` | Human-readable error description |
59
+ | `code` | TypeScript diagnostic code |
60
+ | `severity` | `"error"` or `"warning"` |
61
+ | `node_id` | ID of the node that caused the error |
62
+ | `parameter_index` | Index of the parameter that caused the error |
63
+
64
+ ## Development
65
+
66
+ Prerequisites: [Bun](https://bun.sh) installed locally, and the TypeScript package built in the repository root (`npm ci && npm run build`, plus `npm ci` in `entrypoint/`).
67
+
68
+ ```bash
69
+ cd python
70
+ uv sync
71
+ uv run python tasks.py entrypoints # bundles the TypeScript entrypoint into JS
72
+ uv run pytest
73
+ ```
74
+
75
+ ### Building wheels
76
+
77
+ ```bash
78
+ uv run python tasks.py wheels
79
+ ```
80
+
81
+ This bundles the entrypoint, downloads the bun binaries (with SHA256 checksum verification) for all supported platforms and writes the sdist, the pure python wheel and one wheel per platform to `dist/`.
82
+
83
+ Bun's version and checksums live in [`bun.json`](../bun.json) in the repository root, shared with the ruby gem.
84
+
85
+ ## License
86
+
87
+ See [LICENSE](../LICENSE).
@@ -0,0 +1,75 @@
1
+ # Triangulum Python Package
2
+
3
+ Python bindings for the [Triangulum](https://github.com/code0-tech/triangulum) validation layer. This package wraps the TypeScript library and a bundled [Bun](https://bun.sh) runtime to validate flows using the TypeScript compiler.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install triangulum # or: uv add triangulum
9
+ ```
10
+
11
+ Platform specific wheels with a bundled bun binary are published for:
12
+
13
+ - `macosx_11_0_arm64` (macOS Apple Silicon)
14
+ - `macosx_10_12_x86_64` (macOS Intel)
15
+ - `manylinux_2_17_x86_64` (Linux x64)
16
+ - `musllinux_1_2_x86_64` (Linux x64 musl)
17
+ - `manylinux_2_17_aarch64` (Linux ARM64)
18
+ - `musllinux_1_2_aarch64` (Linux ARM64 musl)
19
+
20
+ For any other platform pip falls back to the pure python wheel, which requires `bun` on `PATH`.
21
+
22
+ ## Usage
23
+
24
+ ```python
25
+ from triangulum import Validation
26
+
27
+ result = Validation(flow, function_definitions, data_types).validate()
28
+
29
+ result.is_valid # => True / False
30
+ result.return_type # => "void"
31
+ result.diagnostics # => [Diagnostic, ...]
32
+ ```
33
+
34
+ The arguments are [Tucana](https://github.com/code0-tech/tucana) protobuf messages:
35
+
36
+ - `flow` — `tucana.generated.shared.flow_pb2.ValidationFlow`
37
+ - `function_definitions` — `list[tucana.generated.shared.function_pb2.FunctionDefinition]`
38
+ - `data_types` — `list[tucana.generated.shared.data_type_pb2.DefinitionDataType]`
39
+
40
+ ### Diagnostics
41
+
42
+ Each diagnostic contains:
43
+
44
+ | Field | Description |
45
+ |---|---|
46
+ | `message` | Human-readable error description |
47
+ | `code` | TypeScript diagnostic code |
48
+ | `severity` | `"error"` or `"warning"` |
49
+ | `node_id` | ID of the node that caused the error |
50
+ | `parameter_index` | Index of the parameter that caused the error |
51
+
52
+ ## Development
53
+
54
+ Prerequisites: [Bun](https://bun.sh) installed locally, and the TypeScript package built in the repository root (`npm ci && npm run build`, plus `npm ci` in `entrypoint/`).
55
+
56
+ ```bash
57
+ cd python
58
+ uv sync
59
+ uv run python tasks.py entrypoints # bundles the TypeScript entrypoint into JS
60
+ uv run pytest
61
+ ```
62
+
63
+ ### Building wheels
64
+
65
+ ```bash
66
+ uv run python tasks.py wheels
67
+ ```
68
+
69
+ This bundles the entrypoint, downloads the bun binaries (with SHA256 checksum verification) for all supported platforms and writes the sdist, the pure python wheel and one wheel per platform to `dist/`.
70
+
71
+ Bun's version and checksums live in [`bun.json`](../bun.json) in the repository root, shared with the ruby gem.
72
+
73
+ ## License
74
+
75
+ See [LICENSE](../LICENSE).
@@ -0,0 +1,134 @@
1
+ """Wheel build hook that bundles a platform specific bun binary.
2
+
3
+ The pure python wheel built without ``TRIANGULUM_WHEEL_PLATFORM`` expects ``bun``
4
+ on ``PATH``. Setting the variable to one of the keys of :data:`BUN_PLATFORMS`
5
+ produces a platform specific wheel with the matching bun binary embedded at
6
+ ``triangulum/exe/bun``, which mirrors the platform gems built by ``gem/``.
7
+
8
+ Binaries are not downloaded during the build; run ``python tasks.py download``
9
+ first (``tasks.py wheels`` does both).
10
+ """
11
+
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import shutil
16
+ import stat
17
+ import urllib.request
18
+ import zipfile
19
+ from pathlib import Path
20
+
21
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
22
+
23
+ HERE = Path(__file__).parent
24
+
25
+ # Bun release metadata is shared with the ruby gem, see <repo root>/bun.json.
26
+ # It only exists in a repository checkout, not in the published sdist, so the
27
+ # metadata is loaded lazily and only platform wheels depend on it.
28
+ BUN_METADATA_PATH = HERE.parent / "bun.json"
29
+
30
+ VENDOR_DIR = HERE / "vendor" / "exe"
31
+
32
+ ENTRYPOINTS = ("single-validation",)
33
+
34
+
35
+ def _load_metadata():
36
+ if not BUN_METADATA_PATH.is_file():
37
+ raise RuntimeError(
38
+ f"{BUN_METADATA_PATH} not found. Platform wheels can only be built "
39
+ f"from a triangulum repository checkout."
40
+ )
41
+ return json.loads(BUN_METADATA_PATH.read_text())
42
+
43
+
44
+ def bun_version():
45
+ return _load_metadata()["version"]
46
+
47
+
48
+ def bun_platforms():
49
+ """wheel platform tag => (bun release zip filename, sha256 checksum)"""
50
+ return {p["wheel"]: (p["zip"], p["sha256"]) for p in _load_metadata()["platforms"]}
51
+
52
+
53
+ def bun_download_url(filename):
54
+ return f"https://github.com/oven-sh/bun/releases/download/bun-{bun_version()}/{filename}"
55
+
56
+
57
+ def bun_path(platform):
58
+ return VENDOR_DIR / platform / "bun"
59
+
60
+
61
+ def download_bun(platform):
62
+ """Download and extract the bun binary for ``platform``, verifying its checksum."""
63
+ zip_filename, expected_checksum = bun_platforms()[platform]
64
+ destination = bun_path(platform)
65
+
66
+ if destination.is_file():
67
+ return destination
68
+
69
+ url = bun_download_url(zip_filename)
70
+ print(f"Downloading {destination} from {url} ...")
71
+
72
+ with urllib.request.urlopen(url) as response:
73
+ zip_data = response.read()
74
+
75
+ actual_checksum = hashlib.sha256(zip_data).hexdigest()
76
+ if actual_checksum != expected_checksum:
77
+ raise RuntimeError(
78
+ f"Checksum mismatch for {zip_filename}: "
79
+ f"expected {expected_checksum}, got {actual_checksum}"
80
+ )
81
+
82
+ destination.parent.mkdir(parents=True, exist_ok=True)
83
+ tmp_zip = destination.with_suffix(".zip")
84
+ tmp_zip.write_bytes(zip_data)
85
+ try:
86
+ with zipfile.ZipFile(tmp_zip) as archive:
87
+ entry = next(
88
+ (e for e in archive.namelist() if Path(e).name == "bun"),
89
+ None,
90
+ )
91
+ if entry is None:
92
+ raise RuntimeError(f"bun binary not found in {zip_filename}")
93
+
94
+ with archive.open(entry) as source, destination.open("wb") as target:
95
+ shutil.copyfileobj(source, target)
96
+ finally:
97
+ tmp_zip.unlink()
98
+
99
+ destination.chmod(destination.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
100
+
101
+ return destination
102
+
103
+
104
+ class CustomBuildHook(BuildHookInterface):
105
+ def initialize(self, version, build_data):
106
+ # An editable install is used for development, where the entrypoints are
107
+ # bundled separately via `python tasks.py entrypoints`.
108
+ for entrypoint in ENTRYPOINTS if version != "editable" else ():
109
+ bundle = HERE / "src" / "triangulum" / "js" / f"{entrypoint}.js"
110
+ if not bundle.is_file():
111
+ raise RuntimeError(
112
+ f"{bundle} is missing. Run `python tasks.py entrypoints` before building."
113
+ )
114
+
115
+ platform = os.environ.get("TRIANGULUM_WHEEL_PLATFORM")
116
+ if not platform:
117
+ return
118
+
119
+ platforms = bun_platforms()
120
+ if platform not in platforms:
121
+ raise RuntimeError(
122
+ f"Unknown TRIANGULUM_WHEEL_PLATFORM {platform!r}, "
123
+ f"expected one of {', '.join(sorted(platforms))}"
124
+ )
125
+
126
+ binary = bun_path(platform)
127
+ if not binary.is_file():
128
+ raise RuntimeError(
129
+ f"{binary} is missing. Run `python tasks.py download` before building."
130
+ )
131
+
132
+ build_data["pure_python"] = False
133
+ build_data["tag"] = f"py3-none-{platform}"
134
+ build_data["force_include"][str(binary)] = "triangulum/exe/bun"
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "triangulum"
7
+ version = "0.40.2"
8
+ description = "Triangulum is the CodeZero validation layer"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "CodeZero" }]
13
+ dependencies = [
14
+ "tucana>=0.0.83",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/code0-tech/triangulum"
19
+ Changelog = "https://github.com/code0-tech/triangulum/releases"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "build>=1.2",
24
+ "hatchling>=1.27",
25
+ "pytest>=8.0",
26
+ ]
27
+
28
+ [tool.hatch.build]
29
+ # The bundled JS entrypoints are gitignored build artifacts, but belong into the
30
+ # sdist and every wheel.
31
+ artifacts = ["src/triangulum/js/*.js"]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/triangulum"]
35
+
36
+ [tool.hatch.build.targets.wheel.hooks.custom]
37
+ path = "hatch_build.py"
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["src/triangulum", "tests", "hatch_build.py", "tasks.py", "README.md"]
41
+
42
+ [tool.pytest.ini_options]
43
+ # Project root on sys.path so ``import tests.factories`` resolves.
44
+ pythonpath = ["."]
45
+ testpaths = ["tests"]
@@ -0,0 +1,13 @@
1
+ """Python bindings for the Triangulum validation layer."""
2
+
3
+ from triangulum.errors import BunNotFound, Error, TriangulumFailed
4
+ from triangulum.validation import Diagnostic, Result, Validation
5
+
6
+ __all__ = [
7
+ "BunNotFound",
8
+ "Diagnostic",
9
+ "Error",
10
+ "Result",
11
+ "TriangulumFailed",
12
+ "Validation",
13
+ ]
@@ -0,0 +1,13 @@
1
+ """Exceptions raised by triangulum."""
2
+
3
+
4
+ class Error(Exception):
5
+ """Base class for every triangulum error."""
6
+
7
+
8
+ class TriangulumFailed(Error):
9
+ """The bun subprocess exited with a failure."""
10
+
11
+
12
+ class BunNotFound(Error):
13
+ """No bun executable is available to run the entrypoints with."""
@@ -0,0 +1,73 @@
1
+ """Runs the bundled TypeScript entrypoints through bun."""
2
+
3
+ import base64
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from triangulum.errors import BunNotFound, TriangulumFailed
9
+
10
+ JS_DIR = Path(__file__).parent / "js"
11
+
12
+ # Platform wheels embed the matching bun binary here, see hatch_build.py. The
13
+ # pure python wheel does not, and falls back to bun on PATH.
14
+ BUNDLED_BUN = Path(__file__).parent / "exe" / "bun"
15
+
16
+
17
+ def bun():
18
+ """Path to the bun executable to run the entrypoints with."""
19
+ if BUNDLED_BUN.is_file():
20
+ return str(BUNDLED_BUN)
21
+
22
+ found = shutil.which("bun")
23
+ if found is None:
24
+ raise BunNotFound(
25
+ "No bundled bun binary found and no 'bun' on PATH. Install bun "
26
+ "(https://bun.sh) or use a platform specific triangulum wheel."
27
+ )
28
+
29
+ return found
30
+
31
+
32
+ def run_ts_triangulum(entrypoint, payload):
33
+ """Run ``entrypoint`` with ``payload`` on stdin and return its stdout."""
34
+ process = subprocess.run(
35
+ [bun(), "run", str(entrypoint)],
36
+ input=payload,
37
+ capture_output=True,
38
+ text=True,
39
+ check=False,
40
+ )
41
+
42
+ if process.returncode != 0:
43
+ if process.returncode < 0:
44
+ status_info = f"SIGNAL: {-process.returncode}"
45
+ else:
46
+ status_info = f"STATUS: {process.returncode}"
47
+
48
+ raise TriangulumFailed(
49
+ f"{status_info}\n\nOUT:\n{process.stdout}\n\nERR:\n{process.stderr}"
50
+ )
51
+
52
+ return process.stdout
53
+
54
+
55
+ def _encode(message):
56
+ return base64.b64encode(message.SerializeToString()).decode("ascii")
57
+
58
+
59
+ def serialize_input(flow, function_definitions, data_types):
60
+ """Serialize the protobuf messages into the entrypoint's stdin format.
61
+
62
+ One base64 encoded message per line, the three groups separated by an empty
63
+ line. Mirrors ``Triangulum::Validation#serialize_input`` in the ruby gem.
64
+ """
65
+ lines = [_encode(flow), ""]
66
+
67
+ lines += [_encode(fd) for fd in function_definitions]
68
+ lines.append("")
69
+
70
+ lines += [_encode(dt) for dt in data_types]
71
+ lines.append("")
72
+
73
+ return "\n".join(lines)
File without changes