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.
Files changed (143) hide show
  1. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/images.yml +3 -3
  2. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/security.yml +2 -2
  3. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.gitignore +1 -0
  4. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/CHANGELOG.md +60 -0
  5. continuo_python_runtime-0.6.0/CLAUDE.md +37 -0
  6. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/PKG-INFO +5 -5
  7. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/README.md +3 -3
  8. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/pyproject.toml +2 -2
  9. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/conftest.py +12 -2
  10. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_integration_runtime_postgres.py +1 -1
  11. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/pyproject.toml +3 -3
  12. continuo_python_runtime-0.6.0/continuo_python_runtime/contract/kinds.py +109 -0
  13. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/loader.py +16 -63
  14. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/merge.py +26 -13
  15. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/model.py +3 -2
  16. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/harness.py +28 -14
  17. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/validation/runner.py +21 -7
  18. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/docs/boundary-contract.md +3 -3
  19. continuo_python_runtime-0.6.0/image-requirements-postgres.txt +2 -0
  20. continuo_python_runtime-0.6.0/image-requirements-trino.txt +2 -0
  21. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/pyproject.toml +3 -3
  22. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/Dockerfile +1 -1
  23. continuo_python_runtime-0.6.0/template/requirements.lock +225 -0
  24. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/conftest.py +12 -2
  25. continuo_python_runtime-0.6.0/tests/contract/test_kinds.py +47 -0
  26. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/test_loader.py +10 -10
  27. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/test_merge.py +1 -1
  28. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/test_model.py +4 -4
  29. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/trino-stack/docker-compose.yml +14 -3
  30. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_cli.py +1 -1
  31. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_contract_loader.py +8 -3
  32. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_harness.py +2 -2
  33. continuo_python_runtime-0.6.0/tests/test_kind_registries.py +22 -0
  34. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_no_legacy_names.py +10 -2
  35. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_validation_runner.py +87 -7
  36. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/uv.lock +38 -38
  37. continuo_python_runtime-0.4.1/docs/superpowers/plans/2026-07-31-python-runtime.md +0 -1307
  38. continuo_python_runtime-0.4.1/docs/superpowers/plans/2026-08-07-step-3b-config-three-part-hash.md +0 -852
  39. continuo_python_runtime-0.4.1/docs/superpowers/specs/2026-07-31-python-runtime-design.md +0 -273
  40. continuo_python_runtime-0.4.1/image-requirements-postgres.txt +0 -2
  41. continuo_python_runtime-0.4.1/image-requirements-trino.txt +0 -2
  42. continuo_python_runtime-0.4.1/template/requirements.lock +0 -22
  43. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.dockerignore +0 -0
  44. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/CODEOWNERS +0 -0
  45. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  46. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  47. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  48. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  49. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/dependabot.yml +0 -0
  50. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/ci.yml +0 -0
  51. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/dco.yml +0 -0
  52. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/publish-pypi.yml +0 -0
  53. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.github/workflows/release.yml +0 -0
  54. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/.gitleaks.toml +0 -0
  55. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/CODE_OF_CONDUCT.md +0 -0
  56. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/CONTRIBUTING.md +0 -0
  57. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/DCO +0 -0
  58. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/Dockerfile.postgres +0 -0
  59. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/Dockerfile.trino +0 -0
  60. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/LICENSE +0 -0
  61. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/NOTICE +0 -0
  62. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/SECURITY.md +0 -0
  63. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/README.md +0 -0
  64. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/continuo_postgres_adapter/__init__.py +0 -0
  65. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/continuo_postgres_adapter/adapter.py +0 -0
  66. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/__init__.py +0 -0
  67. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_adapter_postgres_validation.py +0 -0
  68. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_adapter_runtime_postgres.py +0 -0
  69. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/postgres/tests/test_integration_postgres_validation.py +0 -0
  70. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/README.md +0 -0
  71. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/continuo_trino_adapter/__init__.py +0 -0
  72. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/continuo_trino_adapter/adapter.py +0 -0
  73. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/__init__.py +0 -0
  74. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_adapter_runtime_trino.py +0 -0
  75. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_adapter_trino_validation.py +0 -0
  76. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_integration_runtime_trino.py +0 -0
  77. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/adapters/trino/tests/test_integration_trino_validation.py +0 -0
  78. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/__init__.py +0 -0
  79. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/cli.py +0 -0
  80. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/closure.py +0 -0
  81. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/conform.py +0 -0
  82. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/context.py +0 -0
  83. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/__init__.py +0 -0
  84. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/contract/paths.py +0 -0
  85. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_loader.py +0 -0
  86. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_readers/__init__.py +0 -0
  87. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_readers/https.py +0 -0
  88. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_readers/s3.py +0 -0
  89. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/csv_source.py +0 -0
  90. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/errors.py +0 -0
  91. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/hashing.py +0 -0
  92. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/lint.py +0 -0
  93. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/types.py +0 -0
  94. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/validation/__init__.py +0 -0
  95. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/continuo_python_runtime/validation/s3.py +0 -0
  96. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/LICENSE +0 -0
  97. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/NOTICE +0 -0
  98. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/README.md +0 -0
  99. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/__init__.py +0 -0
  100. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/config.py +0 -0
  101. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/port.py +0 -0
  102. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/result.py +0 -0
  103. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/sql.py +0 -0
  104. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/continuo_engine_contract/types.py +0 -0
  105. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/pyproject.toml +0 -0
  106. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/__init__.py +0 -0
  107. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_config.py +0 -0
  108. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_contract_types.py +0 -0
  109. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_port.py +0 -0
  110. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_result.py +0 -0
  111. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/contract/tests/test_sql.py +0 -0
  112. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/scripts/check_version_bumps.py +0 -0
  113. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/scripts/security-scan.sh +0 -0
  114. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/.github/workflows/release.yml +0 -0
  115. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/Dockerfile.pip +0 -0
  116. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/README.md +0 -0
  117. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/contracts/example.yml +0 -0
  118. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/contracts/example_csv.yml +0 -0
  119. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/template/scripts/example.py +0 -0
  120. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/__init__.py +0 -0
  121. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/contract/__init__.py +0 -0
  122. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/node_smoke/contracts/smoke.yml +0 -0
  123. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/node_smoke/scripts/smoke.py +0 -0
  124. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/postgres-stack/docker-compose.yml +0 -0
  125. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/smoke/trino-stack/catalog/iceberg.properties +0 -0
  126. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_adapter_naming.py +0 -0
  127. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_closure.py +0 -0
  128. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_conform.py +0 -0
  129. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_context.py +0 -0
  130. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_contract_pin_consistency.py +0 -0
  131. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_loader.py +0 -0
  132. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_readers_https.py +0 -0
  133. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_readers_integration.py +0 -0
  134. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_csv_source.py +0 -0
  135. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_errors.py +0 -0
  136. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_hashing.py +0 -0
  137. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_image_requirements_sync.py +0 -0
  138. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_image_smoke_validation.py +0 -0
  139. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_lint.py +0 -0
  140. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_package.py +0 -0
  141. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_template.py +0 -0
  142. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_types.py +0 -0
  143. {continuo_python_runtime-0.4.1 → continuo_python_runtime-0.6.0}/tests/test_validation_s3.py +0 -0
@@ -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-model.smoke.analytics.smoke \
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-model.smoke.analytics.smoke \
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@v5
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
@@ -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@v3
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@v3
127
+ uses: github/codeql-action/upload-sarif@v4
128
128
  with:
129
129
  sarif_file: sarif/
130
130
  category: trivy-fs
@@ -4,6 +4,7 @@ __pycache__/
4
4
  .venv/
5
5
  dist/
6
6
  .superpowers/
7
+ docs/superpowers/
7
8
  .pytest_cache/
8
9
  .mypy_cache/
9
10
  .ruff_cache/
@@ -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.4.1
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.74
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-model`** (the default; the field may be omitted) — a script node.
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.4.1
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.4.1
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-model`** (the default; the field may be omitted) — a script node.
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.4.1
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.4.1
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
@@ -1,5 +1,5 @@
1
1
  [project]
2
- version = "0.2.0"
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.3",
30
+ "ruff==0.16.5",
31
31
  "mypy==2.3.1",
32
32
  ]
33
33
  test = [
@@ -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
- "minio/minio:latest",
47
- "server", "/data", "--address", ":9000",
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-model node.
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)
@@ -1,5 +1,5 @@
1
1
  [project]
2
- version = "0.2.0"
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.338.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.3",
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", "python-model")
177
- if not isinstance(kind, str) or kind not in KINDS:
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 kind == "python-csv":
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 = raw.get("reads")
226
- if kind == "python-csv":
227
- if not isinstance(reads, dict) or set(reads) != {"csv"}:
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=dict(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
- if node.kind == "python-csv":
133
- # A csv node has no script and no import closure: its source IS
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-model", "python-csv"})
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 = "python-model"
39
+ kind: str = DEFAULT_KIND
39
40
 
40
41
  @property
41
42
  def relation(self) -> str: