continuo-python-runtime 0.4.1__tar.gz → 0.6.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.
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/images.yml +3 -3
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/security.yml +2 -2
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.gitignore +1 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/CHANGELOG.md +60 -0
- continuo_python_runtime-0.6.0/CLAUDE.md +37 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/PKG-INFO +5 -5
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/README.md +3 -3
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/pyproject.toml +2 -2
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/conftest.py +12 -2
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_integration_runtime_postgres.py +1 -1
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/pyproject.toml +3 -3
- continuo_python_runtime-0.6.0/continuo_python_runtime/contract/kinds.py +109 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/loader.py +16 -63
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/merge.py +26 -13
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/model.py +3 -2
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/harness.py +28 -14
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/validation/runner.py +21 -7
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/docs/boundary-contract.md +3 -3
- continuo_python_runtime-0.6.0/image-requirements-postgres.txt +2 -0
- continuo_python_runtime-0.6.0/image-requirements-trino.txt +2 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/pyproject.toml +3 -3
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/Dockerfile +1 -1
- continuo_python_runtime-0.6.0/template/requirements.lock +225 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/conftest.py +12 -2
- continuo_python_runtime-0.6.0/tests/contract/test_kinds.py +47 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/test_loader.py +10 -10
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/test_merge.py +1 -1
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/test_model.py +4 -4
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/trino-stack/docker-compose.yml +14 -3
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_cli.py +1 -1
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_contract_loader.py +8 -3
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_harness.py +2 -2
- continuo_python_runtime-0.6.0/tests/test_kind_registries.py +22 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_no_legacy_names.py +10 -2
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_validation_runner.py +87 -7
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/uv.lock +38 -38
- continuo_python_runtime-0.4.1/docs/superpowers/plans/2026-07-31-python-runtime.md +0 -1307
- continuo_python_runtime-0.4.1/docs/superpowers/plans/2026-08-07-step-3b-config-three-part-hash.md +0 -852
- continuo_python_runtime-0.4.1/docs/superpowers/specs/2026-07-31-python-runtime-design.md +0 -273
- continuo_python_runtime-0.4.1/image-requirements-postgres.txt +0 -2
- continuo_python_runtime-0.4.1/image-requirements-trino.txt +0 -2
- continuo_python_runtime-0.4.1/template/requirements.lock +0 -22
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.dockerignore +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/CODEOWNERS +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/dependabot.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/ci.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/dco.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/publish-pypi.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/release.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.gitleaks.toml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/CODE_OF_CONDUCT.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/CONTRIBUTING.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/DCO +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/Dockerfile.postgres +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/Dockerfile.trino +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/LICENSE +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/NOTICE +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/SECURITY.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/README.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/continuo_postgres_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/continuo_postgres_adapter/adapter.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_adapter_postgres_validation.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_adapter_runtime_postgres.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_integration_postgres_validation.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/README.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/continuo_trino_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/continuo_trino_adapter/adapter.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_adapter_runtime_trino.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_adapter_trino_validation.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_integration_runtime_trino.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_integration_trino_validation.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/cli.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/closure.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/conform.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/context.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/paths.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_loader.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_readers/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_readers/https.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_readers/s3.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_source.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/errors.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/hashing.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/lint.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/types.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/validation/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/validation/s3.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/LICENSE +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/NOTICE +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/README.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/config.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/port.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/result.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/sql.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/types.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/pyproject.toml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_config.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_contract_types.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_port.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_result.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_sql.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/scripts/check_version_bumps.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/scripts/security-scan.sh +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/.github/workflows/release.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/Dockerfile.pip +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/README.md +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/contracts/example.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/contracts/example_csv.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/scripts/example.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/__init__.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/node_smoke/contracts/smoke.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/node_smoke/scripts/smoke.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/postgres-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/trino-stack/catalog/iceberg.properties +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_adapter_naming.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_closure.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_conform.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_context.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_contract_pin_consistency.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_loader.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_readers_https.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_readers_integration.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_source.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_errors.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_hashing.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_image_requirements_sync.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_image_smoke_validation.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_lint.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_package.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_template.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_types.py +0 -0
- {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_validation_s3.py +0 -0
{continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/images.yml
RENAMED
|
@@ -83,7 +83,7 @@ jobs:
|
|
|
83
83
|
run: |
|
|
84
84
|
docker run --rm --network host \
|
|
85
85
|
-v "$PWD/tests/smoke/node_smoke:/app" \
|
|
86
|
-
-e NODE_ID=python-
|
|
86
|
+
-e NODE_ID=python-node.smoke.analytics.smoke \
|
|
87
87
|
-e TABLE_NAME=smoke \
|
|
88
88
|
-e TARGET_SCHEMA=analytics \
|
|
89
89
|
-e POSTGRES_HOST=localhost \
|
|
@@ -134,7 +134,7 @@ jobs:
|
|
|
134
134
|
run: |
|
|
135
135
|
docker run --rm \
|
|
136
136
|
-v "$PWD/tests/smoke/node_smoke:/app" \
|
|
137
|
-
-e NODE_ID=python-
|
|
137
|
+
-e NODE_ID=python-node.smoke.analytics.smoke \
|
|
138
138
|
-e TABLE_NAME=smoke \
|
|
139
139
|
-e TARGET_SCHEMA=analytics \
|
|
140
140
|
-e TRINO_HOST=host.docker.internal \
|
|
@@ -204,7 +204,7 @@ jobs:
|
|
|
204
204
|
# they are published, spinning out every retry. (The image build itself
|
|
205
205
|
# installs inside python:3.14-slim, so it is unaffected; this is only for
|
|
206
206
|
# the host-side probe below.)
|
|
207
|
-
- uses: actions/setup-python@
|
|
207
|
+
- uses: actions/setup-python@v7
|
|
208
208
|
with:
|
|
209
209
|
python-version: '3.14'
|
|
210
210
|
- name: Wait until the pinned versions are installable from PyPI
|
{continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/security.yml
RENAMED
|
@@ -96,7 +96,7 @@ jobs:
|
|
|
96
96
|
- name: Upload SARIF to code scanning
|
|
97
97
|
if: always()
|
|
98
98
|
continue-on-error: true
|
|
99
|
-
uses: github/codeql-action/upload-sarif@
|
|
99
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
100
100
|
with:
|
|
101
101
|
sarif_file: sarif/
|
|
102
102
|
category: gitleaks
|
|
@@ -124,7 +124,7 @@ jobs:
|
|
|
124
124
|
- name: Upload SARIF to code scanning
|
|
125
125
|
if: always()
|
|
126
126
|
continue-on-error: true
|
|
127
|
-
uses: github/codeql-action/upload-sarif@
|
|
127
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
128
128
|
with:
|
|
129
129
|
sarif_file: sarif/
|
|
130
130
|
category: trivy-fs
|
|
@@ -5,6 +5,66 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-09-28
|
|
9
|
+
|
|
10
|
+
### Breaking
|
|
11
|
+
|
|
12
|
+
- The script node kind is named `python-node` (the default when `kind` is
|
|
13
|
+
omitted). A contract declaring `kind: python-model` is rejected at load.
|
|
14
|
+
Re-running `continuo-runtime merge` changes every python node's
|
|
15
|
+
`config_hash`, so each node is re-validated once on its next release.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Contract rules, hash inputs and run producers are looked up per kind from
|
|
20
|
+
registries pinned to `KINDS`.
|
|
21
|
+
|
|
22
|
+
## [0.5.0] - 2026-09-09
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `check_binds` validation op: EXPLAINs a dbt test's compiled SQL against the
|
|
27
|
+
candidate schema and creates nothing, so a test that names a dropped or
|
|
28
|
+
renamed column fails. Reads the SQL from `CANDIDATE_SQL_URI`, like
|
|
29
|
+
`build_from_sql`.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `continuo-postgres-adapter` and `continuo-trino-adapter` 0.2.0 → 0.2.1.
|
|
34
|
+
|
|
35
|
+
## [0.4.1] - 2026-08-25
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Bring-your-own engine adapters: `continuo-postgres-adapter` and
|
|
40
|
+
`continuo-trino-adapter` are separate installable packages, so an install
|
|
41
|
+
can supply its own engine adapter independently of the runtime.
|
|
42
|
+
- `scripts/check_version_bumps.py`: fails a release when a package's source
|
|
43
|
+
changed since the last tag but its version did not.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Deterministic python-node container builds: pinned per-engine
|
|
48
|
+
image-requirements and hashed template Dockerfiles.
|
|
49
|
+
- `continuo-engine-contract` 0.7.2 → 0.7.3.
|
|
50
|
+
|
|
51
|
+
## [0.4.0] - 2026-08-21
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- `python-csv` node kind: a contract-only CSV loader (no user script) that
|
|
56
|
+
materializes the declared table from the contract's CSV URI, with a
|
|
57
|
+
validation header check.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- `continuo-engine-contract` 0.7.1 → 0.7.2.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- `release.yml`'s github-release job now checks out the repository; without the
|
|
66
|
+
checkout it could not read the tag's files.
|
|
67
|
+
|
|
8
68
|
## [0.3.1] - 2026-08-21
|
|
9
69
|
|
|
10
70
|
### Added
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# continuo-python-runtime
|
|
2
|
+
|
|
3
|
+
The validation/execution runtime that continuo's executor runs as a Kubernetes
|
|
4
|
+
Job. Python 3.12, uv workspace: the runtime (`continuo_python_runtime`), the
|
|
5
|
+
engine contract (`contract/`), and the engine adapters (`adapters/postgres`,
|
|
6
|
+
`adapters/trino`).
|
|
7
|
+
|
|
8
|
+
## CHANGELOG is not optional
|
|
9
|
+
|
|
10
|
+
Every user-facing change keeps `CHANGELOG.md` current. Two steps, both required:
|
|
11
|
+
|
|
12
|
+
- **On every PR** that changes behavior, the CLI/op surface, a package version,
|
|
13
|
+
or a container: add a bullet under `## [Unreleased]` (Keep a Changelog style —
|
|
14
|
+
Added / Changed / Fixed / Removed). No entry, no merge.
|
|
15
|
+
- **On every release**: rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`,
|
|
16
|
+
add a fresh empty `## [Unreleased]` above it, and tag. `release.yml` reads the
|
|
17
|
+
section matching the pushed tag for the GitHub Release notes, so a release with
|
|
18
|
+
no matching section ships with generic auto-notes and the changelog silently
|
|
19
|
+
loses that version.
|
|
20
|
+
|
|
21
|
+
If the changelog is missing a shipped version, backfill it from that tag's
|
|
22
|
+
merged PRs before adding anything new.
|
|
23
|
+
|
|
24
|
+
## Releasing
|
|
25
|
+
|
|
26
|
+
A release is one `chore(release):` commit that bumps versions and updates the
|
|
27
|
+
changelog, then a `vX.Y.Z` tag on it. Bump the version of every package whose
|
|
28
|
+
source changed since the last tag — `scripts/check_version_bumps.py` fails the
|
|
29
|
+
PyPI publish otherwise. The tag builds and publishes the `-postgres` / `-trino`
|
|
30
|
+
images and the PyPI packages. See `CONTRIBUTING.md` for the full steps.
|
|
31
|
+
|
|
32
|
+
## Conventions
|
|
33
|
+
|
|
34
|
+
- Standard `logging` for diagnostics, never `print` — except the CLI's
|
|
35
|
+
sentinel-framed result blocks, which own stdout.
|
|
36
|
+
- In-repo packages are pinned exactly, not with ranges (see `pyproject.toml`).
|
|
37
|
+
- Commits are signed off (`git commit -s`); CI enforces the DCO.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: continuo-python-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Runtime harness, contract tooling, and CI lint for Continuo python nodes.
|
|
5
5
|
Author: Simone Carolini
|
|
6
6
|
Maintainer: Simone Carolini
|
|
@@ -11,7 +11,7 @@ Classifier: Development Status :: 4 - Beta
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Python: >=3.14
|
|
14
|
-
Requires-Dist: boto3==1.43.
|
|
14
|
+
Requires-Dist: boto3==1.43.85
|
|
15
15
|
Requires-Dist: continuo-engine-contract==0.7.3
|
|
16
16
|
Requires-Dist: pyarrow==25.0.1
|
|
17
17
|
Requires-Dist: pyyaml==6.0.3
|
|
@@ -163,7 +163,7 @@ Every rule below (`extra_columns`, `output_columns`, "Conform rules") applies
|
|
|
163
163
|
to both kinds identically — `kind` only changes how the pre-conform table is
|
|
164
164
|
produced, never how it is checked or written.
|
|
165
165
|
|
|
166
|
-
- **`python-
|
|
166
|
+
- **`python-node`** (the default; the field may be omitted) — a script node.
|
|
167
167
|
It requires `script:` and a `reads:` map of one or more named SQL queries,
|
|
168
168
|
as described in "The script API" below.
|
|
169
169
|
- **`python-csv`** — a contract-only node: it has no script and its `reads:`
|
|
@@ -275,9 +275,9 @@ A domain repo picks its warehouse engine by which base image it builds
|
|
|
275
275
|
`FROM`:
|
|
276
276
|
|
|
277
277
|
```dockerfile
|
|
278
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.
|
|
278
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.6.0
|
|
279
279
|
# or
|
|
280
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.
|
|
280
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.6.0
|
|
281
281
|
```
|
|
282
282
|
|
|
283
283
|
The engine is part of the image **name**; the tag is the bare version, so
|
|
@@ -143,7 +143,7 @@ Every rule below (`extra_columns`, `output_columns`, "Conform rules") applies
|
|
|
143
143
|
to both kinds identically — `kind` only changes how the pre-conform table is
|
|
144
144
|
produced, never how it is checked or written.
|
|
145
145
|
|
|
146
|
-
- **`python-
|
|
146
|
+
- **`python-node`** (the default; the field may be omitted) — a script node.
|
|
147
147
|
It requires `script:` and a `reads:` map of one or more named SQL queries,
|
|
148
148
|
as described in "The script API" below.
|
|
149
149
|
- **`python-csv`** — a contract-only node: it has no script and its `reads:`
|
|
@@ -255,9 +255,9 @@ A domain repo picks its warehouse engine by which base image it builds
|
|
|
255
255
|
`FROM`:
|
|
256
256
|
|
|
257
257
|
```dockerfile
|
|
258
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.
|
|
258
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.6.0
|
|
259
259
|
# or
|
|
260
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.
|
|
260
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.6.0
|
|
261
261
|
```
|
|
262
262
|
|
|
263
263
|
The engine is part of the image **name**; the tag is the bare version, so
|
{continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/pyproject.toml
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[project]
|
|
2
|
-
version = "0.2.
|
|
2
|
+
version = "0.2.2"
|
|
3
3
|
name = "continuo-postgres-adapter"
|
|
4
4
|
description = "Postgres warehouse adapter for Continuo: validation and python-node runtime."
|
|
5
5
|
authors = [{ name = "Simone Carolini" }]
|
|
@@ -27,7 +27,7 @@ postgres = "continuo_postgres_adapter.adapter:PostgresAdapter"
|
|
|
27
27
|
|
|
28
28
|
[dependency-groups]
|
|
29
29
|
dev = [
|
|
30
|
-
"ruff==0.16.
|
|
30
|
+
"ruff==0.16.5",
|
|
31
31
|
"mypy==2.3.1",
|
|
32
32
|
]
|
|
33
33
|
test = [
|
{continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/conftest.py
RENAMED
|
@@ -43,8 +43,18 @@ def minio_container():
|
|
|
43
43
|
"-p", "0:9000",
|
|
44
44
|
"-e", "MINIO_ROOT_USER=minioadmin",
|
|
45
45
|
"-e", "MINIO_ROOT_PASSWORD=minioadmin",
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
# ghcr mirror of Bitnami's minio image (Docker Hub's minio/minio
|
|
47
|
+
# is no longer anonymously pullable). Its ENTRYPOINT is
|
|
48
|
+
# Bitnami's own setup script, not the `minio` binary, so it
|
|
49
|
+
# must be overridden for these args to reach `minio` directly;
|
|
50
|
+
# its data dir also has to be Bitnami's own
|
|
51
|
+
# /bitnami/minio/data (world-root-group-writable for the
|
|
52
|
+
# image's non-root user) since plain /data doesn't exist in
|
|
53
|
+
# this image and the binary can't create it at the
|
|
54
|
+
# filesystem root.
|
|
55
|
+
"--entrypoint", "minio",
|
|
56
|
+
"ghcr.io/carolsimone/continuo-minio:2025.7.23",
|
|
57
|
+
"server", "/bitnami/minio/data", "--address", ":9000",
|
|
48
58
|
],
|
|
49
59
|
check=True, capture_output=True, text=True, timeout=60,
|
|
50
60
|
)
|
|
@@ -354,7 +354,7 @@ def test_run_node_csv_kind_loads_minio_csv_into_postgres(clean_schema, csv_minio
|
|
|
354
354
|
doubles here -- the S3CsvSourceReader from csv_readers.reader_for talks
|
|
355
355
|
to the real minio container, and PostgresAdapter writes to the real
|
|
356
356
|
postgres stack), then conform()/ensure_table()/load() proceed exactly as
|
|
357
|
-
for a python-
|
|
357
|
+
for a python-node node.
|
|
358
358
|
"""
|
|
359
359
|
monkeypatch.setenv("S3_ENDPOINT_URL", csv_minio)
|
|
360
360
|
repo = _csv_contract_dir(tmp_path, clean_schema)
|
{continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/pyproject.toml
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[project]
|
|
2
|
-
version = "0.2.
|
|
2
|
+
version = "0.2.1"
|
|
3
3
|
name = "continuo-trino-adapter"
|
|
4
4
|
description = "Trino warehouse adapter for Continuo: validation and python-node runtime."
|
|
5
5
|
authors = [{ name = "Simone Carolini" }]
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
# newest. This adapter subclasses the contract's WarehouseAdapter port
|
|
14
14
|
# directly, so the pair must stay the tested one.
|
|
15
15
|
"continuo-engine-contract==0.7.3",
|
|
16
|
-
"trino==0.
|
|
16
|
+
"trino==0.339.0",
|
|
17
17
|
"pyarrow==25.0.1",
|
|
18
18
|
]
|
|
19
19
|
classifiers = [
|
|
@@ -27,7 +27,7 @@ trino = "continuo_trino_adapter.adapter:TrinoAdapter"
|
|
|
27
27
|
|
|
28
28
|
[dependency-groups]
|
|
29
29
|
dev = [
|
|
30
|
-
"ruff==0.16.
|
|
30
|
+
"ruff==0.16.5",
|
|
31
31
|
"mypy==2.3.1",
|
|
32
32
|
]
|
|
33
33
|
test = [
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Per-kind contract rules: which fields a node of each kind must carry.
|
|
2
|
+
|
|
3
|
+
Each kind is one ``KindRules`` entry composed of a script policy and a reads
|
|
4
|
+
rule. Adding a kind means adding one entry to ``RULES``; nothing that parses a
|
|
5
|
+
contract branches on the kind's name.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from continuo_engine_contract.sql import ensure_single_read # type: ignore[import-untyped]
|
|
15
|
+
from sqlglot.errors import TokenError
|
|
16
|
+
|
|
17
|
+
from continuo_python_runtime.contract.model import KINDS
|
|
18
|
+
from continuo_python_runtime.csv_source import parse_csv_uri
|
|
19
|
+
from continuo_python_runtime.errors import ContractError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReadsRule(ABC):
|
|
23
|
+
"""Validates a node's ``reads`` mapping and returns it unchanged."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def validate(
|
|
27
|
+
self, reads: Any, label: str, *, dialect: str | None, check_reads: bool
|
|
28
|
+
) -> dict[str, str]: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SqlReads(ReadsRule):
|
|
32
|
+
"""A non-empty mapping of read name -> single SELECT."""
|
|
33
|
+
|
|
34
|
+
def validate(
|
|
35
|
+
self, reads: Any, label: str, *, dialect: str | None, check_reads: bool
|
|
36
|
+
) -> dict[str, str]:
|
|
37
|
+
if not isinstance(reads, dict) or not reads:
|
|
38
|
+
raise ContractError(
|
|
39
|
+
f"{label}: 'reads' must be a non-empty mapping of name -> SQL"
|
|
40
|
+
)
|
|
41
|
+
for name, sql in reads.items():
|
|
42
|
+
if not isinstance(name, str) or not name.strip():
|
|
43
|
+
raise ContractError(
|
|
44
|
+
f"{label}: 'reads' name {name!r} must be a non-empty string"
|
|
45
|
+
)
|
|
46
|
+
if not isinstance(sql, str) or not sql.strip():
|
|
47
|
+
raise ContractError(
|
|
48
|
+
f"{label}: 'reads.{name}' must be a non-empty SQL string"
|
|
49
|
+
)
|
|
50
|
+
if not check_reads:
|
|
51
|
+
continue
|
|
52
|
+
try:
|
|
53
|
+
ensure_single_read(sql, dialect)
|
|
54
|
+
except (ValueError, TokenError) as exc:
|
|
55
|
+
# ensure_single_read's own message is phrased for check_binds
|
|
56
|
+
# (its only other caller today), so it's wrapped rather than
|
|
57
|
+
# surfaced bare here. TokenError is also caught: an unterminated
|
|
58
|
+
# string literal or comment fails sqlglot's tokenizer with a
|
|
59
|
+
# TokenError, a SqlglotError sibling of ParseError and not a
|
|
60
|
+
# subclass of ValueError -- despite ensure_single_read's
|
|
61
|
+
# docstring promising every rejection is a ValueError. Only
|
|
62
|
+
# TokenError, not the broader SqlglotError, is caught here: by
|
|
63
|
+
# the time control reaches this point `dialect` has already
|
|
64
|
+
# been validated once in load_contract_dir, so any other
|
|
65
|
+
# SqlglotError a future sqlglot version might raise from this
|
|
66
|
+
# call should surface as itself, not get relabeled as a
|
|
67
|
+
# rejected read.
|
|
68
|
+
raise ContractError(
|
|
69
|
+
f"{label}: 'reads.{name}' must be a single read query ({exc})"
|
|
70
|
+
) from exc
|
|
71
|
+
return dict(reads)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CsvRead(ReadsRule):
|
|
75
|
+
"""Exactly ``{csv: <uri>}``, the uri in the grammar ``parse_csv_uri`` accepts."""
|
|
76
|
+
|
|
77
|
+
def validate(
|
|
78
|
+
self, reads: Any, label: str, *, dialect: str | None, check_reads: bool
|
|
79
|
+
) -> dict[str, str]:
|
|
80
|
+
if not isinstance(reads, dict) or set(reads) != {"csv"}:
|
|
81
|
+
raise ContractError(
|
|
82
|
+
f"{label}: a python-csv node's 'reads' must be exactly {{csv: <uri>}}"
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
parse_csv_uri(reads["csv"])
|
|
86
|
+
except (ValueError, TypeError) as exc:
|
|
87
|
+
raise ContractError(f"{label}: invalid csv uri: {exc}") from exc
|
|
88
|
+
return dict(reads)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class KindRules:
|
|
93
|
+
"""What a node of one kind must declare."""
|
|
94
|
+
|
|
95
|
+
script_required: bool
|
|
96
|
+
reads: ReadsRule
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
RULES: dict[str, KindRules] = {
|
|
100
|
+
"python-node": KindRules(script_required=True, reads=SqlReads()),
|
|
101
|
+
"python-csv": KindRules(script_required=False, reads=CsvRead()),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def rules_for(kind: object, label: str) -> KindRules:
|
|
106
|
+
"""Return the rules for ``kind``; raise ``ContractError`` for any other value."""
|
|
107
|
+
if isinstance(kind, str) and kind in RULES:
|
|
108
|
+
return RULES[kind]
|
|
109
|
+
raise ContractError(f"{label}: 'kind' must be one of {sorted(KINDS)}, got {kind!r}")
|
|
@@ -11,18 +11,16 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
import yaml
|
|
14
|
-
from continuo_engine_contract.sql import ensure_single_read # type: ignore[import-untyped]
|
|
15
14
|
from sqlglot import Dialect
|
|
16
|
-
from sqlglot.errors import TokenError
|
|
17
15
|
|
|
16
|
+
from continuo_python_runtime.contract.kinds import rules_for
|
|
18
17
|
from continuo_python_runtime.contract.model import (
|
|
19
18
|
CRITICALITIES,
|
|
19
|
+
DEFAULT_KIND,
|
|
20
20
|
EXTRA_COLUMNS_POLICIES,
|
|
21
|
-
KINDS,
|
|
22
21
|
Column,
|
|
23
22
|
Node,
|
|
24
23
|
)
|
|
25
|
-
from continuo_python_runtime.csv_source import parse_csv_uri
|
|
26
24
|
from continuo_python_runtime.errors import ContractError
|
|
27
25
|
from continuo_python_runtime.types import parse_sql_type
|
|
28
26
|
|
|
@@ -173,11 +171,8 @@ def parse_node(
|
|
|
173
171
|
if unknown:
|
|
174
172
|
raise ContractError(f"{label}: unknown key(s) {sorted(unknown)}")
|
|
175
173
|
|
|
176
|
-
kind = raw.get("kind",
|
|
177
|
-
|
|
178
|
-
raise ContractError(
|
|
179
|
-
f"{label}: 'kind' must be one of {sorted(KINDS)}, got {kind!r}"
|
|
180
|
-
)
|
|
174
|
+
kind = raw.get("kind", DEFAULT_KIND)
|
|
175
|
+
rules = rules_for(kind, label)
|
|
181
176
|
|
|
182
177
|
for field in _REQUIRED_STRING_FIELDS:
|
|
183
178
|
value = raw.get(field)
|
|
@@ -191,20 +186,20 @@ def parse_node(
|
|
|
191
186
|
owner = raw["owner"]
|
|
192
187
|
schedule = raw["schedule"]
|
|
193
188
|
|
|
194
|
-
if
|
|
195
|
-
if "script" in raw:
|
|
196
|
-
raise ContractError(
|
|
197
|
-
f"{label}: 'script' is forbidden for kind python-csv "
|
|
198
|
-
"(csv nodes are contract-only)"
|
|
199
|
-
)
|
|
200
|
-
script = ""
|
|
201
|
-
else:
|
|
189
|
+
if rules.script_required:
|
|
202
190
|
raw_script = raw.get("script")
|
|
203
191
|
if not isinstance(raw_script, str) or not raw_script.strip():
|
|
204
192
|
raise ContractError(
|
|
205
193
|
f"{label}: required field 'script' must be a non-empty string"
|
|
206
194
|
)
|
|
207
195
|
script = raw_script
|
|
196
|
+
else:
|
|
197
|
+
if "script" in raw:
|
|
198
|
+
raise ContractError(
|
|
199
|
+
f"{label}: 'script' is forbidden for kind {kind} "
|
|
200
|
+
"(it is a contract-only kind)"
|
|
201
|
+
)
|
|
202
|
+
script = ""
|
|
208
203
|
|
|
209
204
|
criticality = raw.get("criticality")
|
|
210
205
|
if not isinstance(criticality, str) or criticality not in CRITICALITIES:
|
|
@@ -222,51 +217,9 @@ def parse_node(
|
|
|
222
217
|
f"got {extra_columns!r}"
|
|
223
218
|
)
|
|
224
219
|
|
|
225
|
-
reads =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
raise ContractError(
|
|
229
|
-
f"{label}: a python-csv node's 'reads' must be exactly {{csv: <uri>}}"
|
|
230
|
-
)
|
|
231
|
-
try:
|
|
232
|
-
parse_csv_uri(reads["csv"])
|
|
233
|
-
except (ValueError, TypeError) as exc:
|
|
234
|
-
raise ContractError(f"{label}: invalid csv uri: {exc}") from exc
|
|
235
|
-
else:
|
|
236
|
-
if not isinstance(reads, dict) or not reads:
|
|
237
|
-
raise ContractError(
|
|
238
|
-
f"{label}: 'reads' must be a non-empty mapping of name -> SQL"
|
|
239
|
-
)
|
|
240
|
-
for name, sql in reads.items():
|
|
241
|
-
if not isinstance(name, str) or not name.strip():
|
|
242
|
-
raise ContractError(
|
|
243
|
-
f"{label}: 'reads' name {name!r} must be a non-empty string"
|
|
244
|
-
)
|
|
245
|
-
if not isinstance(sql, str) or not sql.strip():
|
|
246
|
-
raise ContractError(
|
|
247
|
-
f"{label}: 'reads.{name}' must be a non-empty SQL string"
|
|
248
|
-
)
|
|
249
|
-
if not check_reads:
|
|
250
|
-
continue
|
|
251
|
-
try:
|
|
252
|
-
ensure_single_read(sql, dialect)
|
|
253
|
-
except (ValueError, TokenError) as exc:
|
|
254
|
-
# ensure_single_read's own message is phrased for check_binds
|
|
255
|
-
# (its only other caller today), so it's wrapped rather than
|
|
256
|
-
# surfaced bare here. TokenError is also caught: an unterminated
|
|
257
|
-
# string literal or comment fails sqlglot's tokenizer with a
|
|
258
|
-
# TokenError, a SqlglotError sibling of ParseError and not a
|
|
259
|
-
# subclass of ValueError -- despite ensure_single_read's
|
|
260
|
-
# docstring promising every rejection is a ValueError. Only
|
|
261
|
-
# TokenError, not the broader SqlglotError, is caught here: by
|
|
262
|
-
# the time control reaches this point `dialect` has already
|
|
263
|
-
# been validated once in load_contract_dir, so any other
|
|
264
|
-
# SqlglotError a future sqlglot version might raise from this
|
|
265
|
-
# call should surface as itself, not get relabeled as a
|
|
266
|
-
# rejected read.
|
|
267
|
-
raise ContractError(
|
|
268
|
-
f"{label}: 'reads.{name}' must be a single read query ({exc})"
|
|
269
|
-
) from exc
|
|
220
|
+
reads = rules.reads.validate(
|
|
221
|
+
raw.get("reads"), label, dialect=dialect, check_reads=check_reads
|
|
222
|
+
)
|
|
270
223
|
|
|
271
224
|
raw_columns = raw.get("output_columns")
|
|
272
225
|
if not isinstance(raw_columns, list) or not raw_columns:
|
|
@@ -325,7 +278,7 @@ def parse_node(
|
|
|
325
278
|
schedule=schedule,
|
|
326
279
|
criticality=criticality,
|
|
327
280
|
script=script,
|
|
328
|
-
reads=
|
|
281
|
+
reads=reads,
|
|
329
282
|
output_columns=tuple(columns),
|
|
330
283
|
description=description,
|
|
331
284
|
extra_columns=extra_columns,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Contract v1 merger: wire contract builder."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import yaml
|
|
@@ -110,6 +111,29 @@ def _lint_node_closure(
|
|
|
110
111
|
)
|
|
111
112
|
|
|
112
113
|
|
|
114
|
+
def _script_hash_inputs(node: Node, repo_root: Path) -> tuple[bytes, list[bytes]]:
|
|
115
|
+
"""A script node's source is its script plus its in-repo import closure,
|
|
116
|
+
every file of which is held to the closure lint."""
|
|
117
|
+
script_path = resolve_script_path(node.script, repo_root, context=node.relation)
|
|
118
|
+
script_bytes = script_path.read_bytes()
|
|
119
|
+
closure = resolve_closure(script_path, repo_root)
|
|
120
|
+
member_bytes = [member.read_bytes() for member in closure]
|
|
121
|
+
_lint_node_closure(node, repo_root, script_path, script_bytes, closure, member_bytes)
|
|
122
|
+
return script_bytes, member_bytes
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _csv_hash_inputs(node: Node, repo_root: Path) -> tuple[bytes, list[bytes]]:
|
|
126
|
+
"""A csv node has no script and no import closure: its source is the uri.
|
|
127
|
+
New file content at the same uri is new data, not a new node version."""
|
|
128
|
+
return node.reads["csv"].encode(), []
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
_HASH_INPUTS: dict[str, Callable[[Node, Path], tuple[bytes, list[bytes]]]] = {
|
|
132
|
+
"python-node": _script_hash_inputs,
|
|
133
|
+
"python-csv": _csv_hash_inputs,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
113
137
|
def build_wire_contract(
|
|
114
138
|
contract_dir: Path, repo_root: Path, service: str, *, dialect: str | None = None
|
|
115
139
|
) -> dict:
|
|
@@ -129,19 +153,8 @@ def build_wire_contract(
|
|
|
129
153
|
for node in nodes:
|
|
130
154
|
entry = node_entry(node)
|
|
131
155
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# the uri -- new file content at the same uri is new data, not a
|
|
135
|
-
# new node version.
|
|
136
|
-
uri_bytes = node.reads["csv"].encode()
|
|
137
|
-
entry.update(hash_parts(entry, uri_bytes, []))
|
|
138
|
-
else:
|
|
139
|
-
script_path = resolve_script_path(node.script, repo_root, context=node.relation)
|
|
140
|
-
script_bytes = script_path.read_bytes()
|
|
141
|
-
closure = resolve_closure(script_path, repo_root)
|
|
142
|
-
member_bytes = [member.read_bytes() for member in closure]
|
|
143
|
-
_lint_node_closure(node, repo_root, script_path, script_bytes, closure, member_bytes)
|
|
144
|
-
entry.update(hash_parts(entry, script_bytes, member_bytes))
|
|
156
|
+
primary_bytes, member_bytes = _HASH_INPUTS[node.kind](node, repo_root)
|
|
157
|
+
entry.update(hash_parts(entry, primary_bytes, member_bytes))
|
|
145
158
|
|
|
146
159
|
wire_nodes.append(entry)
|
|
147
160
|
|
|
@@ -6,7 +6,8 @@ from typing import Any
|
|
|
6
6
|
# Module-level constants
|
|
7
7
|
CRITICALITIES = frozenset({"REGULATORY", "CORE", "SECONDARY"})
|
|
8
8
|
EXTRA_COLUMNS_POLICIES = frozenset({"raise", "warn"})
|
|
9
|
-
KINDS = frozenset({"python-
|
|
9
|
+
KINDS = frozenset({"python-node", "python-csv"})
|
|
10
|
+
DEFAULT_KIND = "python-node"
|
|
10
11
|
CONTRACT_VERSION = 1
|
|
11
12
|
|
|
12
13
|
|
|
@@ -35,7 +36,7 @@ class Node:
|
|
|
35
36
|
extra_columns: str = "raise"
|
|
36
37
|
config: dict[str, Any] = field(default_factory=dict)
|
|
37
38
|
content_hash: str | None = None
|
|
38
|
-
kind: str =
|
|
39
|
+
kind: str = DEFAULT_KIND
|
|
39
40
|
|
|
40
41
|
@property
|
|
41
42
|
def relation(self) -> str:
|