continuo-python-runtime 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl

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.
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: continuo-python-runtime
3
- Version: 0.6.0
3
+ Version: 0.8.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
@@ -41,15 +41,16 @@ Five artifacts come out of this repository:
41
41
  - **The `continuo-engine-contract` PyPI package** — the `WarehouseAdapter`
42
42
  port, the contract schema, the shared SQL/type/config guards, and the
43
43
  sentinel result-block format. Adapter authors outside this repo pin it.
44
- - **The two engine-adapter PyPI packages** (`continuo-postgres-adapter`,
45
- `continuo-trino-adapter`) — one `WarehouseAdapter` implementation per
44
+ - **The three engine-adapter PyPI packages** (`continuo-postgres-adapter`,
45
+ `continuo-trino-adapter`, `continuo-duckdb-adapter`) — one `WarehouseAdapter` implementation per
46
46
  warehouse engine, each published independently under the same tag. A
47
47
  domain repo normally never installs these directly (the engine image
48
48
  already has the matching one baked in); they exist as standalone PyPI
49
49
  packages for the "build your own container" shape (see below) and for
50
50
  third-party adapter authors to reference.
51
51
  - **Per-engine base images**, one per warehouse engine
52
- (`continuo-python-runtime-postgres`, `continuo-python-runtime-trino`), that
52
+ (`continuo-python-runtime-postgres`, `continuo-python-runtime-trino`,
53
+ `continuo-python-runtime-duckdb`), that
53
54
  domain repos build `FROM`. Each image bakes in the runtime and a single
54
55
  `WarehouseAdapter` for that engine, and serves both roles that adapter has:
55
56
  the node harness (`ENTRYPOINT ["continuo-runtime"]`, `CMD ["run"]`) and the
@@ -57,15 +58,17 @@ Five artifacts come out of this repository:
57
58
  - **`template/`** — a copy-ready domain repo: `Dockerfile`, `contracts/`,
58
59
  `scripts/`, and the `release.yml` CI/CD workflow.
59
60
 
60
- One `vX.Y.Z` git tag releases all of it: `publish-pypi.yml` builds all four
61
- PyPI distributions into a single `dist/` and publishes them together, and
62
- `images.yml` builds and pushes both engine images — each installing its
63
- matching pinned adapter version from that same release — multi-arch under
64
- the same tag.
61
+ One `vX.Y.Z` git tag releases all of it: `publish-pypi.yml` builds all five
62
+ PyPI distributions into a single `dist/`, verifies them (and full engine
63
+ images built from them) on amd64 and arm64, and only then publishes them
64
+ together. It then builds all three engine images natively per architecture —
65
+ each installing its matching pinned adapter version from that same release —
66
+ pulls and smoke-tests the exact pushed digests, and finally gives the verified
67
+ amd64 + arm64 digests one multi-arch tag. See `CONTRIBUTING.md` (Releasing).
65
68
 
66
69
  ### What this repo owns
67
70
 
68
- This repository owns the entire python-node surface: the engine contract, both
71
+ This repository owns the entire python-node surface: the engine contract, the three
69
72
  engine adapters, the validation runner, and the node harness. The former
70
73
  `continuo-validation` repository was merged in — there is no longer a separate
71
74
  validation-side port, adapter class, entry-point group, or image. One
@@ -80,16 +83,18 @@ validation-side port, adapter class, entry-point group, or image. One
80
83
  | `continuo-engine-contract` | `continuo_engine_contract` | this repo, `contract/` | The `WarehouseAdapter` port, contract schema, the SQL/type/config guards adapters must run, and the result-block format. Published to PyPI. |
81
84
  | `continuo-postgres-adapter` | `continuo_postgres_adapter` | this repo, `adapters/postgres/` | `PostgresAdapter` — one class, both roles. Published to PyPI. |
82
85
  | `continuo-trino-adapter` | `continuo_trino_adapter` | this repo, `adapters/trino/` | `TrinoAdapter` — one class, both roles, for Trino/Iceberg. Published to PyPI. |
86
+ | `continuo-duckdb-adapter` | `continuo_duckdb_adapter` | this repo, `adapters/duckdb/` | `DuckDBAdapter` — one class, both roles, for DuckDB on a DuckLake (Postgres catalog, Parquet on S3). Published to PyPI. |
83
87
 
84
- All four are uv workspace members (`[tool.uv.workspace]` in the root
88
+ All five are uv workspace members (`[tool.uv.workspace]` in the root
85
89
  `pyproject.toml`), so `uv sync --all-packages --all-groups` at the repo root
86
90
  installs everything for local development.
87
91
 
88
- **All four packages in the table above are published to PyPI**, under the
89
- same `vX.Y.Z` tag. The two engine images then **install the matching pinned
92
+ **All five packages in the table above are published to PyPI**, under the
93
+ same `vX.Y.Z` tag. The three engine images then **install the matching pinned
90
94
  adapter version from PyPI** — `Dockerfile.postgres` installs
91
95
  `continuo-postgres-adapter==X.Y.Z`, `Dockerfile.trino` installs
92
- `continuo-trino-adapter==X.Y.Z` — rather than building it from this repo's
96
+ `continuo-trino-adapter==X.Y.Z` and `Dockerfile.duckdb` installs
97
+ `continuo-duckdb-adapter==X.Y.Z` — rather than building it from this repo's
93
98
  source tree, so each image still ships exactly one adapter and the runtime
94
99
  still discovers it through the `continuo_engine.adapters` entry-point group
95
100
  at run time. The image **name** (`continuo-python-runtime-<engine>`) and the
@@ -97,7 +102,7 @@ adapter's pip **distribution** name (`continuo-<engine>-adapter`) are two
97
102
  different artifacts of the same adapter — same engine, same version, same
98
103
  runtime behavior, different packaging; see "Build your own container" below
99
104
  for a build shape that installs the pip package directly instead of `FROM`
100
- the image. All four packages are still built, type-checked, and tested by CI
105
+ the image. All five packages are still built, type-checked, and tested by CI
101
106
  on every change.
102
107
 
103
108
  ### The result block is a frozen wire contract
@@ -149,7 +154,7 @@ the pre-flight check once before your next release; it reports every affected
149
154
  read at once:
150
155
 
151
156
  ```bash
152
- continuo-runtime validate contracts/ --dialect postgres # or trino
157
+ continuo-runtime validate contracts/ --dialect postgres # or trino, duckdb
153
158
  ```
154
159
 
155
160
  The runtime image does not re-run this gate, so a read that passes here is
@@ -179,6 +184,12 @@ produced, never how it is checked or written.
179
184
  header but not declared are governed by the same `extra_columns` policy as
180
185
  a script node's output — `raise` (default) fails the run, `warn` drops
181
186
  them and logs a warning. See `template/contracts/example_csv.yml`.
187
+ - **`python-api`** — a script node that fetches its own data (typically over
188
+ HTTP) and declares no `reads:`. `ctx.read` has nothing to return. It may name
189
+ one Kubernetes Secret with `secret_ref: continuo-api-<name>`; continuo
190
+ attaches that Secret to the node's pod, so each of its keys is an env var the
191
+ script reads with `os.environ[...]`. The contract never holds a secret value
192
+ or env var name; the Secret itself is created in the cluster by the operator.
182
193
 
183
194
  ## The script API
184
195
 
@@ -275,9 +286,9 @@ A domain repo picks its warehouse engine by which base image it builds
275
286
  `FROM`:
276
287
 
277
288
  ```dockerfile
278
- FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.6.0
289
+ FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.8.0
279
290
  # or
280
- FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.6.0
291
+ FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.8.0
281
292
  ```
282
293
 
283
294
  The engine is part of the image **name**; the tag is the bare version, so
@@ -288,7 +299,7 @@ pinned PyPI version of the `continuo-postgres-adapter` or
288
299
  `continuo-trino-adapter` package built from this repo's `adapters/postgres/`
289
300
  or `adapters/trino/` source (see the table above) — registered under the
290
301
  `continuo_engine.adapters` entry-point group (entry names `postgres` /
291
- `trino`). The runtime discovers it via `discover_adapter()` at run time, so a
302
+ `trino` / `duckdb`). The runtime discovers it via `discover_adapter()` at run time, so a
292
303
  single image serves every node in the service and the release-time
293
304
  validation Job for it. The executor injects the warehouse connection as
294
305
  environment variables (engine-native, e.g.
@@ -6,25 +6,26 @@ continuo_python_runtime/context.py,sha256=K-9XXYlQ4OZ250rCOq6EJNq7fg_D5sK6tOcu7D
6
6
  continuo_python_runtime/csv_loader.py,sha256=a5Hwu-YIzIiko8X1_vqShsnzjs2-OsKEUSCHLXg_dio,3346
7
7
  continuo_python_runtime/csv_source.py,sha256=7g8Tu6ieiJLvWH4Ep4nwYlOdGkjeePr12K5UHuO8TNc,4136
8
8
  continuo_python_runtime/errors.py,sha256=nfZzfBhQ2ajm7USAWKKaGY2uUlh8eNjNPNYYuaOJgZE,1032
9
- continuo_python_runtime/harness.py,sha256=1NDlhHVoDF9Q1jg3LQmhA56XgRZCBUHzcknntgOwqV0,13011
9
+ continuo_python_runtime/harness.py,sha256=vsoahI38OCaCEpg9Il7xcHYcsXp6A5cxY_fjCC8xGJk,13046
10
10
  continuo_python_runtime/hashing.py,sha256=70iXQb0CmjGqniTPofXY6WapydQ7aK2XOk5dzpEf-zk,3354
11
11
  continuo_python_runtime/lint.py,sha256=M59UtCHHVQnJKNolIkvkRk_AgGAFvxD9zkKAMMK-7GI,12984
12
12
  continuo_python_runtime/types.py,sha256=_ssxNE2Lq5782n9_Ihs-u_6ybCCy8XaL6iYbE35tBug,5057
13
13
  continuo_python_runtime/contract/__init__.py,sha256=S79x6j_P0CmaSuCGzeWoK1U0507KvD2MZXwQl9DFWMM,37
14
- continuo_python_runtime/contract/kinds.py,sha256=nFyCZplwpxYK8AKzKRfJp0-V49jFVZcJAW8Q4-3ezHE,4338
15
- continuo_python_runtime/contract/loader.py,sha256=1JARlZJSgadN0h_wvvhFV9q5nqwoDbtvwVsAGDSiXg8,14491
16
- continuo_python_runtime/contract/merge.py,sha256=QjHavaFe_HviR3vPtAmF5-4_3XdmgNbhl-JmrpVM8iI,7323
17
- continuo_python_runtime/contract/model.py,sha256=-tf5DFqRidMx_Pt8NWyJZ9i8Yystg9-YqBjqHAeX4Xg,1043
14
+ continuo_python_runtime/contract/kinds.py,sha256=Ht-hCT7R5A1tEnPlbGL-b9FyJQBvpDWrXK9PBt7yA28,4938
15
+ continuo_python_runtime/contract/loader.py,sha256=9HsV7dJhCwudjOPXBapYFKiZtslyXIkiZ_WfeuLh5-E,14858
16
+ continuo_python_runtime/contract/merge.py,sha256=Zw--_-v3ERE02Nu7qqLtbHa3Hr__0a1SF-2PRqUfXAY,7594
17
+ continuo_python_runtime/contract/model.py,sha256=WB_Qw4koP8BEwuiAZwIEsKFQRa_6QHHBK4xi4eGvPQo,1082
18
18
  continuo_python_runtime/contract/paths.py,sha256=SRn70pJkDAIpXw0IcMU2iNKPKvct8w_SZP2ec-lV91M,1458
19
+ continuo_python_runtime/contract/secret_ref.py,sha256=96jh2G81clpabiEWGIsxbd9r5GKEGLb4Sa1-WJ9G59Q,1131
19
20
  continuo_python_runtime/csv_readers/__init__.py,sha256=9TKvjC37C77uRaJQZuO0--zVj2OvuNwpJE6nWx7j2S8,806
20
21
  continuo_python_runtime/csv_readers/https.py,sha256=tMJ9k5F0b9SAydpon0ZlOznfjvjhF2BBLltyVmvRneE,4998
21
22
  continuo_python_runtime/csv_readers/s3.py,sha256=zmvHs3TxlN2alPKrdaHzPyREmkve2nBiAsHRXXe1mE0,2246
22
23
  continuo_python_runtime/validation/__init__.py,sha256=hz5oGXsoaUoygR0_pBby9PS6cEfrre518EE0HGB58eE,79
23
24
  continuo_python_runtime/validation/runner.py,sha256=7pbksf7MXfTNcdklCOkOXg9qfRiJtyI_dbgbL3a9LWk,14072
24
25
  continuo_python_runtime/validation/s3.py,sha256=q8nRrT9R7L1-M4dSlDnkL7CUJMPmq86MHY_UKYFlEf8,1785
25
- continuo_python_runtime-0.6.0.dist-info/METADATA,sha256=kX73HGlQoFnSWLVicVKPNyVo-ou6HDH4360MHrcduaM,18810
26
- continuo_python_runtime-0.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
27
- continuo_python_runtime-0.6.0.dist-info/entry_points.txt,sha256=ZvIwSm9f9B9ZmZMf6CyF5Zt4QONn8rqaIk-u-B1CF2Y,70
28
- continuo_python_runtime-0.6.0.dist-info/licenses/LICENSE,sha256=hfXfFCk-8gnps4YvHA6bjCNaSYqOrq2gAdqkXYycAQc,11345
29
- continuo_python_runtime-0.6.0.dist-info/licenses/NOTICE,sha256=ykgYyQkAMx3uas90FZHzt0aTca9IgJQCLpZ4d74Pl3U,465
30
- continuo_python_runtime-0.6.0.dist-info/RECORD,,
26
+ continuo_python_runtime-0.8.0.dist-info/METADATA,sha256=q2tb518F9n_Bo4LIzbhhGSGm5yJPxbrOkWJKJ6y9gxQ,19874
27
+ continuo_python_runtime-0.8.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
28
+ continuo_python_runtime-0.8.0.dist-info/entry_points.txt,sha256=ZvIwSm9f9B9ZmZMf6CyF5Zt4QONn8rqaIk-u-B1CF2Y,70
29
+ continuo_python_runtime-0.8.0.dist-info/licenses/LICENSE,sha256=hfXfFCk-8gnps4YvHA6bjCNaSYqOrq2gAdqkXYycAQc,11345
30
+ continuo_python_runtime-0.8.0.dist-info/licenses/NOTICE,sha256=ykgYyQkAMx3uas90FZHzt0aTca9IgJQCLpZ4d74Pl3U,465
31
+ continuo_python_runtime-0.8.0.dist-info/RECORD,,