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.
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CHANGELOG.md +9 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/PKG-INFO +9 -3
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/README.md +8 -2
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/kinds.py +18 -3
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/loader.py +9 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/merge.py +7 -1
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/model.py +2 -1
- continuo_python_runtime-0.7.0/continuo_python_runtime/contract/secret_ref.py +31 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/harness.py +2 -1
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/docs/boundary-contract.md +9 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/image-requirements-postgres.txt +1 -1
- continuo_python_runtime-0.7.0/image-requirements-trino.txt +2 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/pyproject.toml +1 -1
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/Dockerfile +1 -1
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_kinds.py +50 -1
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_loader.py +45 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_merge.py +49 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_model.py +2 -2
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_contract_loader.py +1 -1
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_harness.py +49 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/uv.lock +1 -1
- continuo_python_runtime-0.6.0/image-requirements-trino.txt +0 -2
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.dockerignore +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/CODEOWNERS +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/dependabot.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/ci.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/dco.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/images.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/publish-pypi.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/release.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/security.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.gitignore +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.gitleaks.toml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CLAUDE.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CODE_OF_CONDUCT.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/CONTRIBUTING.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/DCO +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/Dockerfile.postgres +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/Dockerfile.trino +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/LICENSE +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/NOTICE +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/SECURITY.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/README.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/continuo_postgres_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/continuo_postgres_adapter/adapter.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/pyproject.toml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/conftest.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_adapter_postgres_validation.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_adapter_runtime_postgres.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_integration_postgres_validation.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/test_integration_runtime_postgres.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/README.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/continuo_trino_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/continuo_trino_adapter/adapter.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/pyproject.toml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_adapter_runtime_trino.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_adapter_trino_validation.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_integration_runtime_trino.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/test_integration_trino_validation.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/cli.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/closure.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/conform.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/context.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/contract/paths.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_loader.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_readers/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_readers/https.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_readers/s3.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/csv_source.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/errors.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/hashing.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/lint.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/types.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/validation/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/validation/runner.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/validation/s3.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/LICENSE +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/NOTICE +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/README.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/config.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/port.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/result.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/sql.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/continuo_engine_contract/types.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/pyproject.toml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_config.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_contract_types.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_port.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_result.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_sql.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/scripts/check_version_bumps.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/scripts/security-scan.sh +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/.github/workflows/release.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/Dockerfile.pip +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/README.md +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/contracts/example.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/contracts/example_csv.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/requirements.lock +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/scripts/example.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/conftest.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/__init__.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/node_smoke/contracts/smoke.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/node_smoke/scripts/smoke.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/postgres-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/trino-stack/catalog/iceberg.properties +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/smoke/trino-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_adapter_naming.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_cli.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_closure.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_conform.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_context.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_contract_pin_consistency.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_loader.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_readers_https.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_readers_integration.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_source.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_errors.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_hashing.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_image_requirements_sync.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_image_smoke_validation.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_kind_registries.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_lint.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_no_legacy_names.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_package.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_template.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_types.py +0 -0
- {continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_validation_runner.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
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
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/harness.py
RENAMED
|
@@ -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
|
|
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
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/image-requirements-postgres.txt
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
continuo-python-runtime==0.
|
|
1
|
+
continuo-python-runtime==0.7.0
|
|
2
2
|
continuo-postgres-adapter==0.2.2
|
|
@@ -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.
|
|
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
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_kinds.py
RENAMED
|
@@ -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")
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_loader.py
RENAMED
|
@@ -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")
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_merge.py
RENAMED
|
@@ -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")
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/contract/test_model.py
RENAMED
|
@@ -41,5 +41,5 @@ def test_kind_accepts_python_csv():
|
|
|
41
41
|
assert n.kind == "python-csv"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
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"})
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_contract_loader.py
RENAMED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/ISSUE_TEMPLATE/config.yml
RENAMED
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/images.yml
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/publish-pypi.yml
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/release.yml
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/.github/workflows/security.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/pyproject.toml
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/__init__.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/postgres/tests/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/pyproject.toml
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/adapters/trino/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/__init__.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/cli.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/closure.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/conform.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/errors.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/hashing.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/lint.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/continuo_python_runtime/types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/contract/tests/test_result.py
RENAMED
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/scripts/check_version_bumps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/contracts/example.yml
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/template/contracts/example_csv.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_adapter_naming.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_csv_readers_https.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_image_smoke_validation.py
RENAMED
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_kind_registries.py
RENAMED
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_no_legacy_names.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{continuo_python_runtime-0.6.0 → continuo_python_runtime-0.7.0}/tests/test_validation_runner.py
RENAMED
|
File without changes
|
|
File without changes
|