Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,19 @@ updates:
directory: /
schedule:
interval: weekly

- package-ecosystem: pip
directory: /ports/python
schedule:
interval: weekly
groups:
python:
patterns:
- "*"
update-types:
- minor
- patch
ignore:
- dependency-name: "*"
update-types:
- version-update:semver-major
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,88 @@ jobs:
- run: go test -race ./...
- run: go vet ./...

python:
name: Python ${{ matrix.python }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
defaults:
run:
working-directory: ports/python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python }}
cache: pip
cache-dependency-path: ports/python/pyproject.toml
- run: python -m pip install ".[dev]"
- run: ruff format --check .
- run: ruff check .
- run: mypy src
- run: pytest

python-package:
name: Python package
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: ports/python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: ports/python/pyproject.toml
- run: python -m pip install build twine
- run: python -m build
- run: twine check dist/*
- name: Test wheel installation
run: |
python -m venv .wheel-venv
.wheel-venv/bin/python -m pip install dist/*.whl
.wheel-venv/bin/python -c "from importlib.resources import files; import worldcut; assert files('worldcut').joinpath('py.typed').is_file(); assert worldcut.ENGINE_VERSION == '0.1.2'"
.wheel-venv/bin/worldcut-py --help
.wheel-venv/bin/worldcut-py ../../examples/coherent-deployment.json
- name: Test source distribution independently
run: |
mkdir .sdist-test
tar -xzf dist/*.tar.gz -C .sdist-test --strip-components=1
python -m venv .sdist-venv
.sdist-venv/bin/python -m pip install "./.sdist-test[dev]"
cd .sdist-test
../.sdist-venv/bin/pytest

required:
name: Required checks
if: always()
needs:
- node
- benchmark
- go
- python
- python-package
runs-on: ubuntu-latest
steps:
- name: Require successful CI
env:
NODE_RESULT: ${{ needs.node.result }}
BENCHMARK_RESULT: ${{ needs.benchmark.result }}
GO_RESULT: ${{ needs.go.result }}
PYTHON_RESULT: ${{ needs.python.result }}
PYTHON_PACKAGE_RESULT: ${{ needs.python-package.result }}
run: |
test "$NODE_RESULT" = "success"
test "$BENCHMARK_RESULT" = "success"
test "$GO_RESULT" = "success"
test "$PYTHON_RESULT" = "success"
test "$PYTHON_PACKAGE_RESULT" = "success"
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
with:
languages: javascript-typescript
languages: javascript-typescript,python
- name: Analyze
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Language-neutral protocol semantics and golden vectors are under
| --- | --- | --- |
| TypeScript | 0.1 / 0.1.2 | Reference package with documented integrations |
| [Go](ports/go) | 0.1 / 0.1.2 | Independent conformant verifier and CLI; integrations not yet included |
| [Python](ports/python) | 0.1 / 0.1.2 | Independent conformant verifier and CLI; integrations not yet included |

## Run the examples

Expand Down
12 changes: 12 additions & 0 deletions ports/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/.venv/
/.wheel-venv/
/.sdist-venv/
/.sdist-test/
/dist/
/.pytest_cache/
/.mypy_cache/
/.ruff_cache/
/.hypothesis/
*.egg-info/
__pycache__/
*.py[cod]
53 changes: 53 additions & 0 deletions ports/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# WorldCut Python

Independent Python implementation of WorldCut protocol **0.1**, engine
**0.1.2**, and canonicalization **worldcut-json-v1**. Python 3.11 or newer is
required.

## Install

```sh
python -m pip install worldcut
```

## Library

```python
from pathlib import Path

from worldcut import parse_input, verify

parsed = parse_input(Path("verification.json").read_bytes())
result = verify(parsed)
print(result["verdict"])
print(result["verificationRecordDigest"])
```

`ParsedInput` is an immutable validated snapshot. Every verification returns a
fresh result, so mutating one result cannot affect later verification.

## CLI

```sh
worldcut-py verification.json
worldcut-py --require-satisfied verification.json
```

The second form exits with status 2 unless the verdict is
`CONTRACT_SATISFIED`.

## Validate

```sh
python -m pip install -e ".[dev]"
ruff format --check .
ruff check .
mypy src
pytest
python -m build
twine check dist/*
```

The source distribution includes the complete mirrored conformance corpus and
can run its tests without files from the parent repository. This port includes
the verifier and CLI only; integrations are intentionally not included yet.
70 changes: 70 additions & 0 deletions ports/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"

[project]
name = "worldcut"
version = "0.1.0"
description = "Independent Python verifier for the WorldCut decision-coherence protocol"
readme = "README.md"
requires-python = ">=3.11"
license = "Apache-2.0"
authors = [{ name = "Jason Doyle" }]
keywords = ["consistency", "distributed-systems", "provenance", "verification"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = ["rfc8785==0.1.4"]

[project.optional-dependencies]
dev = [
"build>=1.2.2",
"hypothesis>=6.130",
"mypy>=1.15",
"pytest>=8.3",
"ruff>=0.11",
"twine>=6.1",
]

[project.scripts]
worldcut-py = "worldcut.cli:main"

[project.urls]
Homepage = "https://github.com/Jason-Doyle/WorldCut"
Issues = "https://github.com/Jason-Doyle/WorldCut/issues"
Repository = "https://github.com/Jason-Doyle/WorldCut"

[tool.hatch.build.targets.wheel]
packages = ["src/worldcut"]

[tool.hatch.build.targets.sdist]
include = [
"/src/worldcut",
"/tests",
"/README.md",
"/pyproject.toml",
]

[tool.pytest.ini_options]
addopts = "-ra --strict-config --strict-markers"
testpaths = ["tests"]

[tool.mypy]
python_version = "3.11"
strict = true
files = ["src"]

[tool.ruff]
target-version = "py311"
line-length = 88

[tool.ruff.lint]
select = ["B", "E", "F", "I", "RUF", "SIM", "UP"]
30 changes: 30 additions & 0 deletions ports/python/src/worldcut/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Independent Python implementation of the WorldCut 0.1 verifier."""

from .canonicalization import canonical_json, sha256_digest
from .errors import WorldCutError, WorldCutErrorCode, WorldCutInputError
from .models import ParsedInput, VerificationResult
from .validation import PROTOCOL_VERSION, parse_input
from .verifier import (
CANONICALIZATION,
ENGINE_VERSION,
verify,
verify_json,
)

__all__ = [
"CANONICALIZATION",
"ENGINE_VERSION",
"PROTOCOL_VERSION",
"ParsedInput",
"VerificationResult",
"WorldCutError",
"WorldCutErrorCode",
"WorldCutInputError",
"canonical_json",
"parse_input",
"sha256_digest",
"verify",
"verify_json",
]

__version__ = "0.1.0"
82 changes: 82 additions & 0 deletions ports/python/src/worldcut/canonicalization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

import hashlib
import math

import rfc8785

from .models import JsonValue

_MAX_SAFE_INTEGER = 9_007_199_254_740_991


def _assert_valid_unicode(value: str, field: str) -> None:
for character in value:
code_point = ord(character)
if 0xD800 <= code_point <= 0xDFFF:
raise TypeError(f"{field} contains an unpaired surrogate")


def _snapshot_json(value: object, field: str, ancestors: set[int]) -> JsonValue:
if value is None or isinstance(value, (str, bool)):
if isinstance(value, str):
_assert_valid_unicode(value, field)
return value
if isinstance(value, int) and not isinstance(value, bool):
if abs(value) > _MAX_SAFE_INTEGER:
raise TypeError(f"{field} contains an integer outside the safe domain")
return value
if isinstance(value, float):
if not math.isfinite(value):
raise TypeError(f"{field} contains a non-finite number")
return value
if type(value) is list:
identity = id(value)
if identity in ancestors:
raise TypeError(f"{field} must not contain cycles")
ancestors.add(identity)
array_result = [
_snapshot_json(item, f"{field}[{index}]", ancestors)
for index, item in enumerate(value)
]
ancestors.remove(identity)
return array_result
if type(value) is dict:
identity = id(value)
if identity in ancestors:
raise TypeError(f"{field} must not contain cycles")
ancestors.add(identity)
object_result: dict[str, JsonValue] = {}
for raw_key, item in value.items():
if not isinstance(raw_key, str):
raise TypeError(f"{field} must use string object keys")
_assert_valid_unicode(raw_key, f"{field} key")
object_result[raw_key] = _snapshot_json(
item, f"{field}.{raw_key}", ancestors
)
ancestors.remove(identity)
return object_result
raise TypeError(f"{field} contains unsupported {type(value).__name__} data")


def canonical_json(value: JsonValue) -> str:
"""Return the worldcut-json-v1 canonical representation of JSON data."""

snapshot = _snapshot_json(value, "value", set())
try:
return rfc8785.dumps(snapshot).decode("utf-8")
except (rfc8785.CanonicalizationError, UnicodeError) as error:
raise TypeError(f"cannot canonicalize JSON data: {error}") from error


def sha256_digest(value: JsonValue) -> str:
"""Return the lowercase SHA-256 digest of canonical JSON data."""

return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()


def utf16_sort_key(value: str) -> bytes:
"""Produce the protocol's raw UTF-16 code-unit ordering key."""

_assert_valid_unicode(value, "text")
return value.encode("utf-16-be")
Loading