continuo-python-runtime 0.6.0__py3-none-any.whl → 0.7.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.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
@@ -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.7.0.dist-info/METADATA,sha256=Uo2XMCZawYbKgpER3qDn-eF_0cKJUZTultaxxcBAhuQ,19281
27
+ continuo_python_runtime-0.7.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
28
+ continuo_python_runtime-0.7.0.dist-info/entry_points.txt,sha256=ZvIwSm9f9B9ZmZMf6CyF5Zt4QONn8rqaIk-u-B1CF2Y,70
29
+ continuo_python_runtime-0.7.0.dist-info/licenses/LICENSE,sha256=hfXfFCk-8gnps4YvHA6bjCNaSYqOrq2gAdqkXYycAQc,11345
30
+ continuo_python_runtime-0.7.0.dist-info/licenses/NOTICE,sha256=ykgYyQkAMx3uas90FZHzt0aTca9IgJQCLpZ4d74Pl3U,465
31
+ continuo_python_runtime-0.7.0.dist-info/RECORD,,