continuo-python-runtime 0.6.0__tar.gz → 0.7.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 (139) hide show
  1. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CHANGELOG.md +9 -0
  2. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/PKG-INFO +9 -3
  3. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/README.md +8 -2
  4. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/kinds.py +18 -3
  5. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/loader.py +9 -0
  6. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/merge.py +7 -1
  7. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/model.py +2 -1
  8. continuo_python_runtime-0.7.0/continuo_python_runtime/contract/secret_ref.py +31 -0
  9. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/harness.py +2 -1
  10. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/docs/boundary-contract.md +9 -0
  11. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/image-requirements-postgres.txt +1 -1
  12. continuo_python_runtime-0.7.0/image-requirements-trino.txt +2 -0
  13. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/pyproject.toml +1 -1
  14. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/Dockerfile +1 -1
  15. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_kinds.py +50 -1
  16. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_loader.py +45 -0
  17. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_merge.py +49 -0
  18. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_model.py +2 -2
  19. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_contract_loader.py +1 -1
  20. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_harness.py +49 -0
  21. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/uv.lock +1 -1
  22. continuo_python_runtime-0.6.0/image-requirements-trino.txt +0 -2
  23. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.dockerignore +0 -0
  24. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/CODEOWNERS +0 -0
  25. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  26. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  27. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  28. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/dependabot.yml +0 -0
  30. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/ci.yml +0 -0
  31. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/dco.yml +0 -0
  32. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/images.yml +0 -0
  33. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/publish-pypi.yml +0 -0
  34. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/release.yml +0 -0
  35. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/security.yml +0 -0
  36. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.gitignore +0 -0
  37. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.gitleaks.toml +0 -0
  38. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CLAUDE.md +0 -0
  39. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CODE_OF_CONDUCT.md +0 -0
  40. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CONTRIBUTING.md +0 -0
  41. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/DCO +0 -0
  42. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/Dockerfile.postgres +0 -0
  43. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/Dockerfile.trino +0 -0
  44. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/LICENSE +0 -0
  45. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/NOTICE +0 -0
  46. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/SECURITY.md +0 -0
  47. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/README.md +0 -0
  48. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/continuo_postgres_adapter/__init__.py +0 -0
  49. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/continuo_postgres_adapter/adapter.py +0 -0
  50. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/pyproject.toml +0 -0
  51. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/__init__.py +0 -0
  52. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/conftest.py +0 -0
  53. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_adapter_postgres_validation.py +0 -0
  54. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_adapter_runtime_postgres.py +0 -0
  55. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_integration_postgres_validation.py +0 -0
  56. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_integration_runtime_postgres.py +0 -0
  57. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/README.md +0 -0
  58. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/continuo_trino_adapter/__init__.py +0 -0
  59. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/continuo_trino_adapter/adapter.py +0 -0
  60. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/pyproject.toml +0 -0
  61. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/__init__.py +0 -0
  62. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_adapter_runtime_trino.py +0 -0
  63. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_adapter_trino_validation.py +0 -0
  64. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_integration_runtime_trino.py +0 -0
  65. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_integration_trino_validation.py +0 -0
  66. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/__init__.py +0 -0
  67. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/cli.py +0 -0
  68. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/closure.py +0 -0
  69. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/conform.py +0 -0
  70. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/context.py +0 -0
  71. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/__init__.py +0 -0
  72. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/paths.py +0 -0
  73. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_loader.py +0 -0
  74. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_readers/__init__.py +0 -0
  75. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_readers/https.py +0 -0
  76. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_readers/s3.py +0 -0
  77. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_source.py +0 -0
  78. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/errors.py +0 -0
  79. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/hashing.py +0 -0
  80. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/lint.py +0 -0
  81. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/types.py +0 -0
  82. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/validation/__init__.py +0 -0
  83. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/validation/runner.py +0 -0
  84. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/validation/s3.py +0 -0
  85. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/LICENSE +0 -0
  86. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/NOTICE +0 -0
  87. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/README.md +0 -0
  88. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/__init__.py +0 -0
  89. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/config.py +0 -0
  90. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/port.py +0 -0
  91. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/result.py +0 -0
  92. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/sql.py +0 -0
  93. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/types.py +0 -0
  94. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/pyproject.toml +0 -0
  95. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/__init__.py +0 -0
  96. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_config.py +0 -0
  97. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_contract_types.py +0 -0
  98. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_port.py +0 -0
  99. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_result.py +0 -0
  100. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_sql.py +0 -0
  101. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/scripts/check_version_bumps.py +0 -0
  102. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/scripts/security-scan.sh +0 -0
  103. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/.github/workflows/release.yml +0 -0
  104. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/Dockerfile.pip +0 -0
  105. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/README.md +0 -0
  106. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/contracts/example.yml +0 -0
  107. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/contracts/example_csv.yml +0 -0
  108. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/requirements.lock +0 -0
  109. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/scripts/example.py +0 -0
  110. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/__init__.py +0 -0
  111. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/conftest.py +0 -0
  112. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/__init__.py +0 -0
  113. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/node_smoke/contracts/smoke.yml +0 -0
  114. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/node_smoke/scripts/smoke.py +0 -0
  115. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/postgres-stack/docker-compose.yml +0 -0
  116. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/trino-stack/catalog/iceberg.properties +0 -0
  117. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/trino-stack/docker-compose.yml +0 -0
  118. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_adapter_naming.py +0 -0
  119. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_cli.py +0 -0
  120. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_closure.py +0 -0
  121. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_conform.py +0 -0
  122. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_context.py +0 -0
  123. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_contract_pin_consistency.py +0 -0
  124. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_loader.py +0 -0
  125. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_readers_https.py +0 -0
  126. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_readers_integration.py +0 -0
  127. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_source.py +0 -0
  128. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_errors.py +0 -0
  129. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_hashing.py +0 -0
  130. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_image_requirements_sync.py +0 -0
  131. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_image_smoke_validation.py +0 -0
  132. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_kind_registries.py +0 -0
  133. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_lint.py +0 -0
  134. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_no_legacy_names.py +0 -0
  135. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_package.py +0 -0
  136. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_template.py +0 -0
  137. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_types.py +0 -0
  138. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_validation_runner.py +0 -0
  139. {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_validation_s3.py +0 -0
@@ -5,6 +5,15 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.7.0] - 2026-09-29
9
+
10
+ ### Added
11
+
12
+ - `python-api` node kind: a script node with no declared reads. It may name a
13
+ Kubernetes Secret with `secret_ref` (must match `continuo-api-*`), which
14
+ continuo attaches to the node's pod as env vars. `secret_ref` is rejected on
15
+ every other kind. Existing contracts and their content hashes are unchanged.
16
+
8
17
  ## [0.6.0] - 2026-09-28
9
18
 
10
19
  ### Breaking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: continuo-python-runtime
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -179,6 +179,12 @@ produced, never how it is checked or written.
179
179
  header but not declared are governed by the same `extra_columns` policy as
180
180
  a script node's output — `raise` (default) fails the run, `warn` drops
181
181
  them and logs a warning. See `template/contracts/example_csv.yml`.
182
+ - **`python-api`** — a script node that fetches its own data (typically over
183
+ HTTP) and declares no `reads:`. `ctx.read` has nothing to return. It may name
184
+ one Kubernetes Secret with `secret_ref: continuo-api-<name>`; continuo
185
+ attaches that Secret to the node's pod, so each of its keys is an env var the
186
+ script reads with `os.environ[...]`. The contract never holds a secret value
187
+ or env var name; the Secret itself is created in the cluster by the operator.
182
188
 
183
189
  ## The script API
184
190
 
@@ -275,9 +281,9 @@ A domain repo picks its warehouse engine by which base image it builds
275
281
  `FROM`:
276
282
 
277
283
  ```dockerfile
278
- FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.6.0
284
+ FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.7.0
279
285
  # or
280
- FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.6.0
286
+ FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.7.0
281
287
  ```
282
288
 
283
289
  The engine is part of the image **name**; the tag is the bare version, so
@@ -159,6 +159,12 @@ produced, never how it is checked or written.
159
159
  header but not declared are governed by the same `extra_columns` policy as
160
160
  a script node's output — `raise` (default) fails the run, `warn` drops
161
161
  them and logs a warning. See `template/contracts/example_csv.yml`.
162
+ - **`python-api`** — a script node that fetches its own data (typically over
163
+ HTTP) and declares no `reads:`. `ctx.read` has nothing to return. It may name
164
+ one Kubernetes Secret with `secret_ref: continuo-api-<name>`; continuo
165
+ attaches that Secret to the node's pod, so each of its keys is an env var the
166
+ script reads with `os.environ[...]`. The contract never holds a secret value
167
+ or env var name; the Secret itself is created in the cluster by the operator.
162
168
 
163
169
  ## The script API
164
170
 
@@ -255,9 +261,9 @@ A domain repo picks its warehouse engine by which base image it builds
255
261
  `FROM`:
256
262
 
257
263
  ```dockerfile
258
- FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.6.0
264
+ FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.7.0
259
265
  # or
260
- FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.6.0
266
+ FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.7.0
261
267
  ```
262
268
 
263
269
  The engine is part of the image **name**; the tag is the bare version, so
@@ -1,8 +1,8 @@
1
1
  """Per-kind contract rules: which fields a node of each kind must carry.
2
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.
3
+ Each kind is one ``KindRules`` entry composed of a script policy, a reads rule,
4
+ and whether it may name a Secret. Adding a kind means adding one entry to
5
+ ``RULES``; nothing that parses a contract branches on the kind's name.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -88,17 +88,32 @@ class CsvRead(ReadsRule):
88
88
  return dict(reads)
89
89
 
90
90
 
91
+ class NoReads(ReadsRule):
92
+ """No declared reads: the node's script fetches its own data."""
93
+
94
+ def validate(
95
+ self, reads: Any, label: str, *, dialect: str | None, check_reads: bool
96
+ ) -> dict[str, str]:
97
+ if reads is None or (isinstance(reads, dict) and not reads):
98
+ return {}
99
+ raise ContractError(
100
+ f"{label}: a python-api node declares no 'reads' (its script fetches its own data)"
101
+ )
102
+
103
+
91
104
  @dataclass(frozen=True)
92
105
  class KindRules:
93
106
  """What a node of one kind must declare."""
94
107
 
95
108
  script_required: bool
96
109
  reads: ReadsRule
110
+ secret_allowed: bool = False
97
111
 
98
112
 
99
113
  RULES: dict[str, KindRules] = {
100
114
  "python-node": KindRules(script_required=True, reads=SqlReads()),
101
115
  "python-csv": KindRules(script_required=False, reads=CsvRead()),
116
+ "python-api": KindRules(script_required=True, reads=NoReads(), secret_allowed=True),
102
117
  }
103
118
 
104
119
 
@@ -21,6 +21,7 @@ from continuo_python_runtime.contract.model import (
21
21
  Column,
22
22
  Node,
23
23
  )
24
+ from continuo_python_runtime.contract.secret_ref import validate_secret_ref
24
25
  from continuo_python_runtime.errors import ContractError
25
26
  from continuo_python_runtime.types import parse_sql_type
26
27
 
@@ -38,6 +39,7 @@ _ALLOWED_KEYS = {
38
39
  "output_columns",
39
40
  "config",
40
41
  "content_hash",
42
+ "secret_ref",
41
43
  }
42
44
 
43
45
  _REQUIRED_STRING_FIELDS = ("schema", "table", "owner", "schedule")
@@ -221,6 +223,12 @@ def parse_node(
221
223
  raw.get("reads"), label, dialect=dialect, check_reads=check_reads
222
224
  )
223
225
 
226
+ secret_ref = ""
227
+ if "secret_ref" in raw:
228
+ if not rules.secret_allowed:
229
+ raise ContractError(f"{label}: 'secret_ref' is not allowed for kind {kind}")
230
+ secret_ref = validate_secret_ref(raw["secret_ref"], label)
231
+
224
232
  raw_columns = raw.get("output_columns")
225
233
  if not isinstance(raw_columns, list) or not raw_columns:
226
234
  raise ContractError(f"{label}: 'output_columns' must be a non-empty list")
@@ -285,6 +293,7 @@ def parse_node(
285
293
  config=config,
286
294
  content_hash=content_hash,
287
295
  kind=kind,
296
+ secret_ref=secret_ref,
288
297
  )
289
298
 
290
299
 
@@ -22,7 +22,7 @@ def node_entry(node: Node) -> dict:
22
22
  four (source_hash, shared_code_hash, config_hash, content_hash). The
23
23
  nullable field is always present in output_columns.
24
24
  """
25
- return {
25
+ entry = {
26
26
  "schema": node.schema,
27
27
  "table": node.table,
28
28
  "owner": node.owner,
@@ -43,6 +43,11 @@ def node_entry(node: Node) -> dict:
43
43
  "extra_columns": node.extra_columns,
44
44
  "config": dict(node.config),
45
45
  }
46
+ # Only a kind that may name a Secret ever carries one; emitting the key
47
+ # only when set keeps every other node's config_hash unchanged.
48
+ if node.secret_ref:
49
+ entry["secret_ref"] = node.secret_ref
50
+ return entry
46
51
 
47
52
 
48
53
  def _lint_node_closure(
@@ -131,6 +136,7 @@ def _csv_hash_inputs(node: Node, repo_root: Path) -> tuple[bytes, list[bytes]]:
131
136
  _HASH_INPUTS: dict[str, Callable[[Node, Path], tuple[bytes, list[bytes]]]] = {
132
137
  "python-node": _script_hash_inputs,
133
138
  "python-csv": _csv_hash_inputs,
139
+ "python-api": _script_hash_inputs,
134
140
  }
135
141
 
136
142
 
@@ -6,7 +6,7 @@ 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-node", "python-csv"})
9
+ KINDS = frozenset({"python-node", "python-csv", "python-api"})
10
10
  DEFAULT_KIND = "python-node"
11
11
  CONTRACT_VERSION = 1
12
12
 
@@ -37,6 +37,7 @@ class Node:
37
37
  config: dict[str, Any] = field(default_factory=dict)
38
38
  content_hash: str | None = None
39
39
  kind: str = DEFAULT_KIND
40
+ secret_ref: str = ""
40
41
 
41
42
  @property
42
43
  def relation(self) -> str:
@@ -0,0 +1,31 @@
1
+ """The name of the Kubernetes Secret a node's pod receives as env vars.
2
+
3
+ The ``continuo-api-`` prefix keeps a contract from naming any other Secret in
4
+ the namespace (the warehouse credentials, the platform's own credentials).
5
+ continuo's topology-controller and execution-controller enforce the same
6
+ pattern; this check gives the author the error in CI instead.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ from continuo_python_runtime.errors import ContractError
14
+
15
+ SECRET_REF_PATTERN = re.compile(r"^continuo-api-[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
16
+ SECRET_REF_MAX_LEN = 253
17
+
18
+
19
+ def validate_secret_ref(value: object, label: str) -> str:
20
+ """Return ``value`` if it is a valid ``continuo-api-*`` Secret name."""
21
+ if (
22
+ not isinstance(value, str)
23
+ or len(value) > SECRET_REF_MAX_LEN
24
+ or not SECRET_REF_PATTERN.fullmatch(value)
25
+ ):
26
+ raise ContractError(
27
+ f"{label}: 'secret_ref' must be a Kubernetes Secret name starting with "
28
+ f"'continuo-api-' (lowercase letters, digits, '-'; at most "
29
+ f"{SECRET_REF_MAX_LEN} chars), got {value!r}"
30
+ )
31
+ return value
@@ -1,6 +1,6 @@
1
1
  """Container entrypoint: dispatches a single node's script and writes its output.
2
2
 
3
- ``run_node`` is the sole write sink for a python-node node. It resolves the
3
+ ``run_node`` is the sole write sink for a python node. It resolves the
4
4
  node from the contract, loads and executes its script inside a
5
5
  :class:`~continuo_python_runtime.context.RunContext`, conforms the result to
6
6
  the declared schema, and writes it through the runtime adapter. Exactly one
@@ -227,6 +227,7 @@ def _produce_from_csv(
227
227
 
228
228
  _PRODUCERS: dict[str, Callable[[Node, Any, Path, CsvSourceReader | None], Any]] = {
229
229
  "python-node": _produce_from_script,
230
+ "python-api": _produce_from_script,
230
231
  "python-csv": _produce_from_csv,
231
232
  }
232
233
 
@@ -52,6 +52,15 @@ s3://<bucket>/<service>/<release_id>/contract.yaml
52
52
  release gate on a missing declared column exactly as a script node fails
53
53
  it on a missing bind. §13.4 below still applies unchanged to
54
54
  `kind: python-node` nodes.
55
+ - **`python-api`** is the third `kind`: a script-backed node, like
56
+ `python-node`, that declares no reads and may name a Kubernetes Secret
57
+ through an optional `secret_ref` (must match `continuo-api-*`; a trailing
58
+ newline is rejected). Only this kind may set it. Its script and import closure are hashed and
59
+ linted exactly as for `python-node`. When `secret_ref` is set, it appears
60
+ on the merged wire entry as an extra key (absent otherwise, so no other
61
+ node's wire entry or `config_hash` changes), and it is part of
62
+ `config_hash`, so `content_hash` changes when the referenced Secret name
63
+ changes. `source_hash` does not.
55
64
  - **Reads must be single-statement SELECTs with every table reference
56
65
  schema-qualified** (`analytics.table_a`, never `table_a`) — the resolver
57
66
  raises `UnqualifiedTableReference` and rejects the whole release otherwise
@@ -1,2 +1,2 @@
1
- continuo-python-runtime==0.6.0
1
+ continuo-python-runtime==0.7.0
2
2
  continuo-postgres-adapter==0.2.2
@@ -0,0 +1,2 @@
1
+ continuo-python-runtime==0.7.0
2
+ continuo-trino-adapter==0.2.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "continuo-python-runtime"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "Runtime harness, contract tooling, and CI lint for Continuo python nodes."
5
5
  authors = [{ name = "Simone Carolini" }]
6
6
  maintainers = [{ name = "Simone Carolini" }]
@@ -1,5 +1,5 @@
1
1
  # Tags are v<version>; one image per engine (continuo-python-runtime-postgres / -trino).
2
- FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.6.0
2
+ FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.7.0
3
3
  COPY contracts/ /app/contracts/
4
4
  COPY scripts/ /app/scripts/
5
5
  # COPY every OTHER directory your scripts import from, too. CI folds a script's
@@ -1,7 +1,8 @@
1
1
  import pytest
2
2
 
3
- from continuo_python_runtime.contract.kinds import RULES, CsvRead, SqlReads, rules_for
3
+ from continuo_python_runtime.contract.kinds import RULES, CsvRead, NoReads, SqlReads, rules_for
4
4
  from continuo_python_runtime.contract.model import KINDS
5
+ from continuo_python_runtime.contract.secret_ref import validate_secret_ref
5
6
  from continuo_python_runtime.errors import ContractError
6
7
 
7
8
 
@@ -45,3 +46,51 @@ def test_csv_read_requires_exactly_a_csv_key():
45
46
  def test_csv_read_validates_the_uri():
46
47
  with pytest.raises(ContractError, match="invalid csv uri"):
47
48
  CsvRead().validate({"csv": "http://insecure/x.csv"}, "L", dialect=None, check_reads=True)
49
+
50
+
51
+ def test_python_api_requires_a_script_declares_no_reads_and_may_name_a_secret():
52
+ rules = RULES["python-api"]
53
+ assert rules.script_required is True
54
+ assert isinstance(rules.reads, NoReads)
55
+ assert rules.secret_allowed is True
56
+
57
+
58
+ def test_only_python_api_may_name_a_secret():
59
+ assert {k for k, r in RULES.items() if r.secret_allowed} == {"python-api"}
60
+
61
+
62
+ @pytest.mark.parametrize("reads", [None, {}])
63
+ def test_no_reads_accepts_absent_or_empty(reads):
64
+ assert NoReads().validate(reads, "L", dialect=None, check_reads=True) == {}
65
+
66
+
67
+ @pytest.mark.parametrize("reads", [{"x": "select 1"}, {"csv": "s3://b/k"}, [], "select 1"])
68
+ def test_no_reads_rejects_any_declared_read(reads):
69
+ with pytest.raises(ContractError, match="a python-api node declares no 'reads'"):
70
+ NoReads().validate(reads, "L", dialect=None, check_reads=True)
71
+
72
+
73
+ @pytest.mark.parametrize(
74
+ "name",
75
+ [
76
+ "continuo-api-fx",
77
+ "continuo-api-a",
78
+ "continuo-api-stripe-2",
79
+ "continuo-api-" + "a" * 240,
80
+ ],
81
+ )
82
+ def test_secret_ref_accepts_valid_names(name):
83
+ assert validate_secret_ref(name, "L") == name
84
+
85
+
86
+ @pytest.mark.parametrize(
87
+ "name",
88
+ [
89
+ "", "continuo-api-", "continuo-api-FX", "continuo-api--x-", "api-fx",
90
+ "continuo-app-credentials", "continuo-api-x_y", " continuo-api-fx",
91
+ "continuo-api-" + "a" * 241, "continuo-api-fx\n", 7, None,
92
+ ],
93
+ )
94
+ def test_secret_ref_rejects_invalid_names(name):
95
+ with pytest.raises(ContractError, match="'secret_ref'"):
96
+ validate_secret_ref(name, "L")
@@ -682,3 +682,48 @@ def test_load_contract_dir_threads_check_reads_to_parse_node(monkeypatch, tmp_pa
682
682
  assert calls == []
683
683
  load_contract_dir(tmp_path)
684
684
  assert calls == [VALID["reads"]["ids"]]
685
+
686
+
687
+ def _api_entry(**overrides):
688
+ entry = {
689
+ "schema": "analytics", "table": "fx", "owner": "m", "schedule": "daily",
690
+ "criticality": "SECONDARY", "kind": "python-api", "script": "scripts/fx.py",
691
+ "output_columns": [{"name": "rate", "type": "DOUBLE PRECISION"}],
692
+ }
693
+ entry.update(overrides)
694
+ return entry
695
+
696
+
697
+ def test_python_api_without_reads_loads_with_empty_reads():
698
+ node = parse_node(_api_entry(), "c.yml")
699
+ assert node.kind == "python-api"
700
+ assert node.reads == {}
701
+ assert node.secret_ref == ""
702
+
703
+
704
+ def test_python_api_with_secret_ref_keeps_it():
705
+ node = parse_node(_api_entry(secret_ref="continuo-api-fx"), "c.yml")
706
+ assert node.secret_ref == "continuo-api-fx"
707
+
708
+
709
+ def test_python_api_with_reads_is_rejected():
710
+ with pytest.raises(ContractError, match="declares no 'reads'"):
711
+ parse_node(_api_entry(reads={"x": "select 1"}), "c.yml")
712
+
713
+
714
+ def test_python_api_still_requires_a_script():
715
+ entry = _api_entry()
716
+ del entry["script"]
717
+ with pytest.raises(ContractError, match="required field 'script'"):
718
+ parse_node(entry, "c.yml")
719
+
720
+
721
+ def test_secret_ref_on_python_node_is_rejected():
722
+ entry = _api_entry(kind="python-node", reads={"x": "select 1"}, secret_ref="continuo-api-fx")
723
+ with pytest.raises(ContractError, match="'secret_ref' is not allowed for kind python-node"):
724
+ parse_node(entry, "c.yml")
725
+
726
+
727
+ def test_bad_secret_ref_is_rejected_naming_the_node():
728
+ with pytest.raises(ContractError, match=r"c\.yml \(analytics\.fx\): 'secret_ref'"):
729
+ parse_node(_api_entry(secret_ref="continuo-app-credentials"), "c.yml")
@@ -481,3 +481,52 @@ def test_csv_node_needs_no_script_file_on_disk(tmp_path):
481
481
  )
482
482
  doc = build_wire_contract(tmp_path / "contracts", tmp_path, "svc")
483
483
  assert len(doc["nodes"]) == 1
484
+
485
+
486
+ def _api_repo(tmp_path, **extra):
487
+ tmp_path.mkdir(parents=True, exist_ok=True)
488
+ (tmp_path / "contracts").mkdir()
489
+ (tmp_path / "scripts").mkdir()
490
+ (tmp_path / "scripts" / "fx.py").write_text(
491
+ "import pyarrow as pa\n\ndef run(ctx):\n return pa.table({'rate': [1.0]})\n"
492
+ )
493
+ node = {
494
+ "schema": "analytics", "table": "fx", "owner": "m", "schedule": "daily",
495
+ "criticality": "SECONDARY", "kind": "python-api", "script": "scripts/fx.py",
496
+ "output_columns": [{"name": "rate", "type": "DOUBLE PRECISION"}],
497
+ }
498
+ node.update(extra)
499
+ (tmp_path / "contracts" / "fx.yml").write_text(yaml.safe_dump({"nodes": [node]}))
500
+ return tmp_path
501
+
502
+
503
+ def test_python_api_wire_entry_has_empty_reads_and_no_secret_key_when_unset(tmp_path):
504
+ repo = _api_repo(tmp_path)
505
+ (entry,) = build_wire_contract(repo / "contracts", repo, "s")["nodes"]
506
+ assert set(entry) == WIRE_ENTRY_KEYS
507
+ assert entry["kind"] == "python-api"
508
+ assert entry["reads"] == {}
509
+ assert entry["source_hash"] == sha256((repo / "scripts" / "fx.py").read_bytes()).hexdigest()
510
+
511
+
512
+ def test_python_api_wire_entry_carries_secret_ref_when_set(tmp_path):
513
+ repo = _api_repo(tmp_path, secret_ref="continuo-api-fx")
514
+ (entry,) = build_wire_contract(repo / "contracts", repo, "s")["nodes"]
515
+ assert set(entry) == WIRE_ENTRY_KEYS | {"secret_ref"}
516
+ assert entry["secret_ref"] == "continuo-api-fx"
517
+
518
+
519
+ def test_secret_ref_is_part_of_the_config_hash(tmp_path):
520
+ a = _api_repo(tmp_path / "a", secret_ref="continuo-api-fx")
521
+ b = _api_repo(tmp_path / "b", secret_ref="continuo-api-other")
522
+ (ea,) = build_wire_contract(a / "contracts", a, "s")["nodes"]
523
+ (eb,) = build_wire_contract(b / "contracts", b, "s")["nodes"]
524
+ assert ea["source_hash"] == eb["source_hash"]
525
+ assert ea["config_hash"] != eb["config_hash"]
526
+
527
+
528
+ def test_python_api_script_is_held_to_the_closure_lint(tmp_path):
529
+ repo = _api_repo(tmp_path)
530
+ (repo / "scripts" / "fx.py").write_text("import psycopg2\n\ndef run(ctx):\n return None\n")
531
+ with pytest.raises(ContractError, match="lint violations"):
532
+ build_wire_contract(repo / "contracts", repo, "s")
@@ -41,5 +41,5 @@ def test_kind_accepts_python_csv():
41
41
  assert n.kind == "python-csv"
42
42
 
43
43
 
44
- def test_kinds_frozenset_contains_both_kinds():
45
- assert KINDS == frozenset({"python-node", "python-csv"})
44
+ def test_kinds_frozenset_contains_every_kind():
45
+ assert KINDS == frozenset({"python-node", "python-csv", "python-api"})
@@ -33,5 +33,5 @@ def test_explicit_python_node_kind_is_accepted():
33
33
 
34
34
 
35
35
  def test_python_model_kind_is_rejected():
36
- with pytest.raises(ContractError, match=r"'kind' must be one of \['python-csv', 'python-node'\], got 'python-model'"):
36
+ with pytest.raises(ContractError, match=r"'kind' must be one of \['python-api', 'python-csv', 'python-node'\], got 'python-model'"):
37
37
  parse_node(_base_node(kind="python-model"), "t.yml")
@@ -666,3 +666,52 @@ def test_adapter_construction_failure_emits_single_load_error_block(monkeypatch,
666
666
  out = capsys.readouterr().out
667
667
  assert out.count("===CONTINUO_VALIDATION_RESULT_BEGIN===") == 1
668
668
  assert '"message":"LoadError:' in out
669
+
670
+
671
+ def _api_repo(tmp_path, script_body):
672
+ (tmp_path / "contracts").mkdir()
673
+ (tmp_path / "scripts").mkdir()
674
+ (tmp_path / "scripts" / "fx.py").write_text(script_body)
675
+ (tmp_path / "contracts" / "fx.yml").write_text(yaml.safe_dump({"nodes": [{
676
+ "schema": "analytics", "table": "fx", "owner": "m", "schedule": "daily",
677
+ "criticality": "SECONDARY", "kind": "python-api", "script": "scripts/fx.py",
678
+ "secret_ref": "continuo-api-fx",
679
+ "output_columns": [{"name": "rate", "type": "DOUBLE PRECISION"}],
680
+ }]}))
681
+ return tmp_path
682
+
683
+
684
+ def _api_env(repo):
685
+ return {
686
+ "NODE_ID": "analytics.fx", "TABLE_NAME": "fx", "TARGET_SCHEMA": "analytics",
687
+ "CONTRACT_DIR": str(repo / "contracts"), "APP_ROOT": str(repo),
688
+ }
689
+
690
+
691
+ def _sentinel_body(out):
692
+ return json.loads(out.split("BEGIN===\n")[1].split("\n===CONTINUO")[0])
693
+
694
+
695
+ def test_python_api_node_runs_its_script_and_loads_the_result(tmp_path, capsys):
696
+ repo = _api_repo(
697
+ tmp_path,
698
+ "import pyarrow as pa\n\ndef run(ctx):\n return pa.table({'rate': [1.08]})\n",
699
+ )
700
+ adapter = FakeWarehouseAdapter()
701
+ assert run_node(_api_env(repo), adapter=adapter) == 0
702
+ assert adapter.loaded[2].column("rate").to_pylist() == [1.08]
703
+ assert _sentinel_body(capsys.readouterr().out)["status"] == "success"
704
+
705
+
706
+ def test_python_api_ctx_read_is_a_read_error(tmp_path, capsys):
707
+ repo = _api_repo(tmp_path, "def run(ctx):\n return ctx.read('anything')\n")
708
+ assert run_node(_api_env(repo), adapter=FakeWarehouseAdapter()) == 1
709
+ assert "ReadError" in capsys.readouterr().out
710
+
711
+
712
+ def test_python_api_missing_env_key_is_a_script_error_naming_the_key(tmp_path, capsys, monkeypatch):
713
+ monkeypatch.delenv("FX_API_KEY", raising=False)
714
+ repo = _api_repo(tmp_path, "import os\n\ndef run(ctx):\n return os.environ['FX_API_KEY']\n")
715
+ assert run_node(_api_env(repo), adapter=FakeWarehouseAdapter()) == 1
716
+ out = capsys.readouterr().out
717
+ assert "ScriptError" in out and "FX_API_KEY" in out
@@ -209,7 +209,7 @@ test = [
209
209
 
210
210
  [[package]]
211
211
  name = "continuo-python-runtime"
212
- version = "0.6.0"
212
+ version = "0.7.0"
213
213
  source = { editable = "." }
214
214
  dependencies = [
215
215
  { name = "boto3" },
@@ -1,2 +0,0 @@
1
- continuo-python-runtime==0.6.0
2
- continuo-trino-adapter==0.2.1