continuo-python-runtime 0.7.0__py3-none-any.whl → 0.9.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.
- continuo_python_runtime/contract/kinds.py +12 -5
- continuo_python_runtime/contract/loader.py +82 -1
- continuo_python_runtime/contract/merge.py +11 -2
- continuo_python_runtime/contract/model.py +7 -0
- continuo_python_runtime/harness.py +100 -4
- continuo_python_runtime/shape.py +95 -0
- {continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/METADATA +98 -29
- {continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/RECORD +12 -11
- {continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/WHEEL +0 -0
- {continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/entry_points.txt +0 -0
- {continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Per-kind contract rules: which fields a node of each kind must carry.
|
|
2
2
|
|
|
3
3
|
Each kind is one ``KindRules`` entry composed of a script policy, a reads rule,
|
|
4
|
-
|
|
5
|
-
``RULES``; nothing that parses a contract
|
|
4
|
+
whether it may name a Secret, and which write modes it may declare. Adding a
|
|
5
|
+
kind means adding one entry to ``RULES``; nothing that parses a contract
|
|
6
|
+
branches on the kind's name.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
@@ -14,7 +15,7 @@ from typing import Any
|
|
|
14
15
|
from continuo_engine_contract.sql import ensure_single_read # type: ignore[import-untyped]
|
|
15
16
|
from sqlglot.errors import TokenError
|
|
16
17
|
|
|
17
|
-
from continuo_python_runtime.contract.model import KINDS
|
|
18
|
+
from continuo_python_runtime.contract.model import DEFAULT_MODE_TYPE, KINDS, MODE_TYPES
|
|
18
19
|
from continuo_python_runtime.csv_source import parse_csv_uri
|
|
19
20
|
from continuo_python_runtime.errors import ContractError
|
|
20
21
|
|
|
@@ -108,12 +109,18 @@ class KindRules:
|
|
|
108
109
|
script_required: bool
|
|
109
110
|
reads: ReadsRule
|
|
110
111
|
secret_allowed: bool = False
|
|
112
|
+
# The write modes a node of this kind may declare. A new kind gets replace
|
|
113
|
+
# only unless its entry opts into more.
|
|
114
|
+
mode_types: frozenset[str] = frozenset({DEFAULT_MODE_TYPE})
|
|
111
115
|
|
|
112
116
|
|
|
113
117
|
RULES: dict[str, KindRules] = {
|
|
114
|
-
"python-node": KindRules(script_required=True, reads=SqlReads()),
|
|
118
|
+
"python-node": KindRules(script_required=True, reads=SqlReads(), mode_types=MODE_TYPES),
|
|
119
|
+
# A csv drop is a snapshot of its source: replace only (spec D2).
|
|
115
120
|
"python-csv": KindRules(script_required=False, reads=CsvRead()),
|
|
116
|
-
"python-api": KindRules(
|
|
121
|
+
"python-api": KindRules(
|
|
122
|
+
script_required=True, reads=NoReads(), secret_allowed=True, mode_types=MODE_TYPES
|
|
123
|
+
),
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
|
|
@@ -17,7 +17,9 @@ from continuo_python_runtime.contract.kinds import rules_for
|
|
|
17
17
|
from continuo_python_runtime.contract.model import (
|
|
18
18
|
CRITICALITIES,
|
|
19
19
|
DEFAULT_KIND,
|
|
20
|
+
DEFAULT_MODE_TYPE,
|
|
20
21
|
EXTRA_COLUMNS_POLICIES,
|
|
22
|
+
MODE_TYPES,
|
|
21
23
|
Column,
|
|
22
24
|
Node,
|
|
23
25
|
)
|
|
@@ -40,6 +42,8 @@ _ALLOWED_KEYS = {
|
|
|
40
42
|
"config",
|
|
41
43
|
"content_hash",
|
|
42
44
|
"secret_ref",
|
|
45
|
+
"mode_type",
|
|
46
|
+
"primary_key",
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
_REQUIRED_STRING_FIELDS = ("schema", "table", "owner", "schedule")
|
|
@@ -140,6 +144,67 @@ def _validate_config(raw: Any, label: str) -> dict[str, Any]:
|
|
|
140
144
|
return raw
|
|
141
145
|
|
|
142
146
|
|
|
147
|
+
def _validate_primary_key(
|
|
148
|
+
raw: dict[str, Any], mode_type: str, columns: list[Column], label: str
|
|
149
|
+
) -> tuple[str, ...]:
|
|
150
|
+
"""Validate ``primary_key`` against ``mode_type`` and the declared columns.
|
|
151
|
+
|
|
152
|
+
Required for ``upsert`` and rejected otherwise: a key on ``append`` or
|
|
153
|
+
``replace`` would look enforced when it is not. Names keep their order,
|
|
154
|
+
which is what the wire entry carries. Each must be spelled exactly as an
|
|
155
|
+
output column (postgres quotes identifiers, so ``Rate_Date`` is not
|
|
156
|
+
``rate_date``) declared ``nullable: false``, because a NULL key never
|
|
157
|
+
matches an existing row. Repeats are refused case-insensitively too, so a
|
|
158
|
+
key cannot name two columns that differ only in case. A floating-point
|
|
159
|
+
column is refused as well: the warehouse treats ``0.0`` and ``-0.0`` as
|
|
160
|
+
one key, so ``ensure_unique_key`` would reject the rows at write time --
|
|
161
|
+
failing here instead keeps an unwritable upsert out of the contract.
|
|
162
|
+
"""
|
|
163
|
+
if mode_type != "upsert":
|
|
164
|
+
if "primary_key" in raw:
|
|
165
|
+
raise ContractError(
|
|
166
|
+
f"{label}: 'primary_key' is only allowed when mode_type is upsert "
|
|
167
|
+
f"(got {mode_type})"
|
|
168
|
+
)
|
|
169
|
+
return ()
|
|
170
|
+
if "primary_key" not in raw:
|
|
171
|
+
raise ContractError(f"{label}: 'primary_key' is required when mode_type is upsert")
|
|
172
|
+
raw_key = raw["primary_key"]
|
|
173
|
+
if not isinstance(raw_key, list) or not raw_key:
|
|
174
|
+
raise ContractError(
|
|
175
|
+
f"{label}: 'primary_key' must be a non-empty list of output column names"
|
|
176
|
+
)
|
|
177
|
+
by_name = {column.name: column for column in columns}
|
|
178
|
+
seen: set[str] = set()
|
|
179
|
+
for name in raw_key:
|
|
180
|
+
if not isinstance(name, str) or not name.strip():
|
|
181
|
+
raise ContractError(
|
|
182
|
+
f"{label}: 'primary_key' entry {name!r} must be a non-empty string"
|
|
183
|
+
)
|
|
184
|
+
folded = name.lower() # same folding as continuo's parser
|
|
185
|
+
if folded in seen:
|
|
186
|
+
raise ContractError(
|
|
187
|
+
f"{label}: 'primary_key' names {name!r} more than once "
|
|
188
|
+
"(names are compared case-insensitively)"
|
|
189
|
+
)
|
|
190
|
+
seen.add(folded)
|
|
191
|
+
column = by_name.get(name)
|
|
192
|
+
if column is None:
|
|
193
|
+
raise ContractError(
|
|
194
|
+
f"{label}: 'primary_key' column {name!r} is not in output_columns"
|
|
195
|
+
)
|
|
196
|
+
if column.nullable:
|
|
197
|
+
raise ContractError(
|
|
198
|
+
f"{label}: 'primary_key' column {name!r} must be declared nullable: false"
|
|
199
|
+
)
|
|
200
|
+
if parse_sql_type(column.type).base == "DOUBLE_PRECISION":
|
|
201
|
+
raise ContractError(
|
|
202
|
+
f"{label}: 'primary_key' column {name!r} is floating-point "
|
|
203
|
+
f"({column.type}); a floating-point column cannot be a primary key"
|
|
204
|
+
)
|
|
205
|
+
return tuple(raw_key)
|
|
206
|
+
|
|
207
|
+
|
|
143
208
|
def parse_node(
|
|
144
209
|
raw: dict[str, Any],
|
|
145
210
|
source: str,
|
|
@@ -158,7 +223,8 @@ def parse_node(
|
|
|
158
223
|
``check_reads=False`` skips *only* that :func:`ensure_single_read` call —
|
|
159
224
|
the read-shape gate — leaving every other rule here (required fields,
|
|
160
225
|
criticality, the ``reads`` map's own shape, output-column types and
|
|
161
|
-
uniqueness, ``
|
|
226
|
+
uniqueness, ``mode_type`` and ``primary_key``, ``config``,
|
|
227
|
+
``content_hash``) in force. It exists for
|
|
162
228
|
:func:`~continuo_python_runtime.harness.run_node`; see
|
|
163
229
|
:func:`load_contract_dir` for why the runtime opts out.
|
|
164
230
|
"""
|
|
@@ -229,6 +295,17 @@ def parse_node(
|
|
|
229
295
|
raise ContractError(f"{label}: 'secret_ref' is not allowed for kind {kind}")
|
|
230
296
|
secret_ref = validate_secret_ref(raw["secret_ref"], label)
|
|
231
297
|
|
|
298
|
+
mode_type = raw.get("mode_type", DEFAULT_MODE_TYPE)
|
|
299
|
+
if not isinstance(mode_type, str) or mode_type not in MODE_TYPES:
|
|
300
|
+
raise ContractError(
|
|
301
|
+
f"{label}: 'mode_type' must be one of {sorted(MODE_TYPES)}, got {mode_type!r}"
|
|
302
|
+
)
|
|
303
|
+
if mode_type not in rules.mode_types:
|
|
304
|
+
raise ContractError(
|
|
305
|
+
f"{label}: 'mode_type' {mode_type} is not allowed for kind {kind} "
|
|
306
|
+
f"(allowed: {sorted(rules.mode_types)})"
|
|
307
|
+
)
|
|
308
|
+
|
|
232
309
|
raw_columns = raw.get("output_columns")
|
|
233
310
|
if not isinstance(raw_columns, list) or not raw_columns:
|
|
234
311
|
raise ContractError(f"{label}: 'output_columns' must be a non-empty list")
|
|
@@ -269,6 +346,8 @@ def parse_node(
|
|
|
269
346
|
seen_names.add(name)
|
|
270
347
|
columns.append(Column(name=name, type=col_type, nullable=nullable))
|
|
271
348
|
|
|
349
|
+
primary_key = _validate_primary_key(raw, mode_type, columns, label)
|
|
350
|
+
|
|
272
351
|
description = raw.get("description", "")
|
|
273
352
|
if not isinstance(description, str):
|
|
274
353
|
raise ContractError(f"{label}: 'description' must be a string")
|
|
@@ -294,6 +373,8 @@ def parse_node(
|
|
|
294
373
|
content_hash=content_hash,
|
|
295
374
|
kind=kind,
|
|
296
375
|
secret_ref=secret_ref,
|
|
376
|
+
mode_type=mode_type,
|
|
377
|
+
primary_key=primary_key,
|
|
297
378
|
)
|
|
298
379
|
|
|
299
380
|
|
|
@@ -7,7 +7,7 @@ import yaml
|
|
|
7
7
|
|
|
8
8
|
from continuo_python_runtime.closure import resolve_closure
|
|
9
9
|
from continuo_python_runtime.contract.loader import load_contract_dir
|
|
10
|
-
from continuo_python_runtime.contract.model import CONTRACT_VERSION, Node
|
|
10
|
+
from continuo_python_runtime.contract.model import CONTRACT_VERSION, DEFAULT_MODE_TYPE, Node
|
|
11
11
|
from continuo_python_runtime.contract.paths import resolve_script_path
|
|
12
12
|
from continuo_python_runtime.errors import ContractError
|
|
13
13
|
from continuo_python_runtime.hashing import hash_parts
|
|
@@ -20,7 +20,10 @@ def node_entry(node: Node) -> dict:
|
|
|
20
20
|
Returns the node as a dict with all fields, output_columns as list of
|
|
21
21
|
dicts. The entry carries no hash fields: build_wire_contract adds all
|
|
22
22
|
four (source_hash, shared_code_hash, config_hash, content_hash). The
|
|
23
|
-
nullable field is always present in output_columns.
|
|
23
|
+
nullable field is always present in output_columns. ``secret_ref``,
|
|
24
|
+
``mode_type`` and ``primary_key`` appear only when they differ from their
|
|
25
|
+
default, so a node that does not use them has the entry (and hashes) it
|
|
26
|
+
had before they existed.
|
|
24
27
|
"""
|
|
25
28
|
entry = {
|
|
26
29
|
"schema": node.schema,
|
|
@@ -47,6 +50,12 @@ def node_entry(node: Node) -> dict:
|
|
|
47
50
|
# only when set keeps every other node's config_hash unchanged.
|
|
48
51
|
if node.secret_ref:
|
|
49
52
|
entry["secret_ref"] = node.secret_ref
|
|
53
|
+
# Same reasoning: every replace node's entry, config_hash and content_hash
|
|
54
|
+
# stay byte-identical, so contract_version stays 1.
|
|
55
|
+
if node.mode_type != DEFAULT_MODE_TYPE:
|
|
56
|
+
entry["mode_type"] = node.mode_type
|
|
57
|
+
if node.mode_type == "upsert":
|
|
58
|
+
entry["primary_key"] = list(node.primary_key)
|
|
50
59
|
return entry
|
|
51
60
|
|
|
52
61
|
|
|
@@ -8,6 +8,11 @@ CRITICALITIES = frozenset({"REGULATORY", "CORE", "SECONDARY"})
|
|
|
8
8
|
EXTRA_COLUMNS_POLICIES = frozenset({"raise", "warn"})
|
|
9
9
|
KINDS = frozenset({"python-node", "python-csv", "python-api"})
|
|
10
10
|
DEFAULT_KIND = "python-node"
|
|
11
|
+
# How the harness writes a node's table on each run: replace empties it first,
|
|
12
|
+
# append keeps the rows of earlier runs, upsert overwrites rows whose
|
|
13
|
+
# primary_key exists and keeps the rest.
|
|
14
|
+
MODE_TYPES = frozenset({"append", "upsert", "replace"})
|
|
15
|
+
DEFAULT_MODE_TYPE = "replace"
|
|
11
16
|
CONTRACT_VERSION = 1
|
|
12
17
|
|
|
13
18
|
|
|
@@ -38,6 +43,8 @@ class Node:
|
|
|
38
43
|
content_hash: str | None = None
|
|
39
44
|
kind: str = DEFAULT_KIND
|
|
40
45
|
secret_ref: str = ""
|
|
46
|
+
mode_type: str = DEFAULT_MODE_TYPE
|
|
47
|
+
primary_key: tuple[str, ...] = ()
|
|
41
48
|
|
|
42
49
|
@property
|
|
43
50
|
def relation(self) -> str:
|
|
@@ -3,7 +3,8 @@
|
|
|
3
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
|
-
the declared schema, and writes it through the runtime adapter
|
|
6
|
+
the declared schema, and writes it through the runtime adapter with the
|
|
7
|
+
node's ``mode_type``: ``load`` (replace), ``append`` or ``upsert``. Exactly one
|
|
7
8
|
sentinel-framed result block is printed to stdout per run; every other
|
|
8
9
|
diagnostic goes to stderr via ``logging``.
|
|
9
10
|
"""
|
|
@@ -17,11 +18,15 @@ import os
|
|
|
17
18
|
import sys
|
|
18
19
|
import uuid
|
|
19
20
|
from collections.abc import Callable, Mapping
|
|
21
|
+
from dataclasses import dataclass
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
from types import ModuleType
|
|
22
24
|
from typing import Any
|
|
23
25
|
|
|
26
|
+
from continuo_engine_contract.keys import DuplicateKeyError # type: ignore[import-untyped]
|
|
24
27
|
from continuo_engine_contract.port import ( # type: ignore[import-untyped]
|
|
28
|
+
AppendCapable,
|
|
29
|
+
UpsertCapable,
|
|
25
30
|
discover_adapter,
|
|
26
31
|
)
|
|
27
32
|
from continuo_engine_contract.result import result_block # type: ignore[import-untyped]
|
|
@@ -33,7 +38,14 @@ from continuo_python_runtime.contract.model import Node
|
|
|
33
38
|
from continuo_python_runtime.contract.paths import resolve_script_path
|
|
34
39
|
from continuo_python_runtime.csv_loader import produce_csv
|
|
35
40
|
from continuo_python_runtime.csv_source import CsvSourceReader
|
|
36
|
-
from continuo_python_runtime.errors import
|
|
41
|
+
from continuo_python_runtime.errors import (
|
|
42
|
+
ConformError,
|
|
43
|
+
ContractError,
|
|
44
|
+
HarnessError,
|
|
45
|
+
LoadError,
|
|
46
|
+
ScriptError,
|
|
47
|
+
)
|
|
48
|
+
from continuo_python_runtime.shape import shape_diff
|
|
37
49
|
|
|
38
50
|
logger = logging.getLogger("continuo_python_runtime.harness")
|
|
39
51
|
|
|
@@ -232,6 +244,75 @@ _PRODUCERS: dict[str, Callable[[Node, Any, Path, CsvSourceReader | None], Any]]
|
|
|
232
244
|
}
|
|
233
245
|
|
|
234
246
|
|
|
247
|
+
def _write_replace(adapter: Any, schema: str, table: str, data: Any, key: tuple[str, ...]) -> None:
|
|
248
|
+
adapter.load(schema, table, data)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _write_append(adapter: Any, schema: str, table: str, data: Any, key: tuple[str, ...]) -> None:
|
|
252
|
+
adapter.append(schema, table, data)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _write_upsert(adapter: Any, schema: str, table: str, data: Any, key: tuple[str, ...]) -> None:
|
|
256
|
+
adapter.upsert(schema, table, data, key)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass(frozen=True)
|
|
260
|
+
class _Writer:
|
|
261
|
+
"""How one ``mode_type`` is written.
|
|
262
|
+
|
|
263
|
+
``capability`` is the engine-contract interface the adapter must declare
|
|
264
|
+
(``None``: every ``WarehouseAdapter`` can, through ``load``). Every
|
|
265
|
+
capability extends ``TableIntrospection``, so a capability-gated write is
|
|
266
|
+
also a shape-checked one.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
capability: type | None
|
|
270
|
+
write: Callable[[Any, str, str, Any, tuple[str, ...]], None]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# One entry per mode_type, beside _PRODUCERS: a new mode is one capability
|
|
274
|
+
# interface in the engine contract plus one entry here.
|
|
275
|
+
_WRITERS: dict[str, _Writer] = {
|
|
276
|
+
"replace": _Writer(capability=None, write=_write_replace),
|
|
277
|
+
"append": _Writer(capability=AppendCapable, write=_write_append),
|
|
278
|
+
"upsert": _Writer(capability=UpsertCapable, write=_write_upsert),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _writer_for(adapter: Any, node: Node) -> _Writer:
|
|
283
|
+
"""The node's writer, if the adapter declares the capability its mode needs.
|
|
284
|
+
|
|
285
|
+
Called right after the config tripwire, before the script runs or the
|
|
286
|
+
warehouse is touched: an engine that cannot append fails in the first
|
|
287
|
+
second, not after the whole run.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
LoadError: ``<Adapter> does not support <mode>``.
|
|
291
|
+
"""
|
|
292
|
+
writer = _WRITERS[node.mode_type]
|
|
293
|
+
if writer.capability is not None and not isinstance(adapter, writer.capability):
|
|
294
|
+
raise LoadError(f"{type(adapter).__name__} does not support {node.mode_type}")
|
|
295
|
+
return writer
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _check_shape(adapter: Any, node: Node, schema: str, table: str) -> None:
|
|
299
|
+
"""Fail unless the live target table still has the node's declared shape.
|
|
300
|
+
|
|
301
|
+
Runs before an append or upsert only: those keep earlier rows, so a table
|
|
302
|
+
whose columns or nullability drifted (changed outside the contract, or
|
|
303
|
+
created before the release gate recorded shapes) must not be written. It
|
|
304
|
+
inspects the table actually written -- ``TARGET_SCHEMA.TABLE_NAME`` -- not
|
|
305
|
+
``node.relation``.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
LoadError: Naming every missing column, unexpected column and
|
|
309
|
+
nullability mismatch.
|
|
310
|
+
"""
|
|
311
|
+
diff = shape_diff(node.output_columns, adapter.table_columns(schema, table))
|
|
312
|
+
if diff:
|
|
313
|
+
raise LoadError(diff.message(f"{schema}.{table}"))
|
|
314
|
+
|
|
315
|
+
|
|
235
316
|
def run_node(
|
|
236
317
|
env: Mapping[str, str], adapter: Any = None, reader: CsvSourceReader | None = None
|
|
237
318
|
) -> int:
|
|
@@ -276,6 +357,7 @@ def run_node(
|
|
|
276
357
|
raise LoadError(f"adapter construction failed: {exc}") from exc
|
|
277
358
|
|
|
278
359
|
_validate_config_early(active_adapter, node)
|
|
360
|
+
writer = _writer_for(active_adapter, node)
|
|
279
361
|
|
|
280
362
|
table = _PRODUCERS[node.kind](node, active_adapter, app_root, reader)
|
|
281
363
|
|
|
@@ -285,11 +367,25 @@ def run_node(
|
|
|
285
367
|
{"name": c.name, "type": c.type, "nullable": c.nullable}
|
|
286
368
|
for c in node.output_columns
|
|
287
369
|
]
|
|
370
|
+
# Spelled exactly as in output_columns (the loader enforces it). Empty
|
|
371
|
+
# for every mode but upsert, so primary_key reaches ensure_table only
|
|
372
|
+
# for upsert nodes.
|
|
373
|
+
key = node.primary_key
|
|
288
374
|
try:
|
|
289
|
-
|
|
290
|
-
|
|
375
|
+
if key:
|
|
376
|
+
active_adapter.ensure_table(
|
|
377
|
+
target_schema, table_name, columns, config=node.config, primary_key=key
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
active_adapter.ensure_table(target_schema, table_name, columns, config=node.config)
|
|
381
|
+
if writer.capability is not None:
|
|
382
|
+
_check_shape(active_adapter, node, target_schema, table_name)
|
|
383
|
+
writer.write(active_adapter, target_schema, table_name, conformed, key)
|
|
291
384
|
except HarnessError:
|
|
292
385
|
raise
|
|
386
|
+
except DuplicateKeyError as exc:
|
|
387
|
+
# The script's rows repeat a key: its output is at fault, not the warehouse.
|
|
388
|
+
raise ConformError(str(exc)) from exc
|
|
293
389
|
except Exception as exc:
|
|
294
390
|
raise LoadError(f"failed to write {target_schema}.{table_name}: {exc}") from exc
|
|
295
391
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Compare a live table's columns with a node's declared output columns.
|
|
2
|
+
|
|
3
|
+
The harness runs this before every append or upsert: those modes keep earlier
|
|
4
|
+
rows, so writing into a table whose shape drifted from the contract would mix
|
|
5
|
+
two shapes in one table. Names are compared case-insensitively (Trino folds
|
|
6
|
+
names, DuckDB matches them case-insensitively) and nullability exactly. Order
|
|
7
|
+
is ignored, because every insert names its columns; types are ignored,
|
|
8
|
+
because engines report them in their own spelling (Trino and DuckDB drop a
|
|
9
|
+
VARCHAR's length) and there is no engine-to-contract type mapping. Type
|
|
10
|
+
changes are the release gate's job.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import NamedTuple
|
|
17
|
+
|
|
18
|
+
from continuo_engine_contract.port import LiveColumn # type: ignore[import-untyped]
|
|
19
|
+
|
|
20
|
+
from continuo_python_runtime.contract.model import Column
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NullabilityDiff(NamedTuple):
|
|
24
|
+
"""A column whose nullability differs: as declared vs as the live table has it."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
declared: bool
|
|
28
|
+
live: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _yaml_bool(value: bool) -> str:
|
|
32
|
+
return "true" if value else "false"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ShapeDiff:
|
|
37
|
+
"""What separates a live table from the declared columns; falsy when nothing does."""
|
|
38
|
+
|
|
39
|
+
missing: tuple[str, ...] = ()
|
|
40
|
+
unexpected: tuple[str, ...] = ()
|
|
41
|
+
nullability: tuple[NullabilityDiff, ...] = ()
|
|
42
|
+
|
|
43
|
+
def __bool__(self) -> bool:
|
|
44
|
+
return bool(self.missing or self.unexpected or self.nullability)
|
|
45
|
+
|
|
46
|
+
def message(self, target: str) -> str:
|
|
47
|
+
"""One line naming every difference, for a LoadError about *target* (``schema.table``)."""
|
|
48
|
+
parts: list[str] = []
|
|
49
|
+
if self.missing:
|
|
50
|
+
parts.append(f"missing column(s) {list(self.missing)}")
|
|
51
|
+
if self.unexpected:
|
|
52
|
+
parts.append(f"unexpected column(s) {list(self.unexpected)}")
|
|
53
|
+
parts.extend(
|
|
54
|
+
f"column {diff.name} is nullable: {_yaml_bool(diff.live)} in the table "
|
|
55
|
+
f"but nullable: {_yaml_bool(diff.declared)} in the contract"
|
|
56
|
+
for diff in self.nullability
|
|
57
|
+
)
|
|
58
|
+
return (
|
|
59
|
+
f"table {target} does not match the contract's output_columns: {'; '.join(parts)}. "
|
|
60
|
+
"An append or upsert table keeps its rows, so its shape cannot change: "
|
|
61
|
+
"write to a new table (a _v2 name) under a new contract"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def shape_diff(
|
|
66
|
+
contract_columns: Sequence[Column], live_columns: Sequence[LiveColumn]
|
|
67
|
+
) -> ShapeDiff:
|
|
68
|
+
"""Compare the declared columns with a live table's.
|
|
69
|
+
|
|
70
|
+
Each declared column is paired with a live column of the exact same name
|
|
71
|
+
if there is one, else with one whose name matches case-insensitively, so
|
|
72
|
+
two live columns that differ only in case pair with the right declared
|
|
73
|
+
column.
|
|
74
|
+
"""
|
|
75
|
+
remaining = list(live_columns)
|
|
76
|
+
missing: list[str] = []
|
|
77
|
+
nullability: list[NullabilityDiff] = []
|
|
78
|
+
for declared in contract_columns:
|
|
79
|
+
live = next((column for column in remaining if column.name == declared.name), None)
|
|
80
|
+
if live is None:
|
|
81
|
+
folded = declared.name.casefold()
|
|
82
|
+
live = next(
|
|
83
|
+
(column for column in remaining if column.name.casefold() == folded), None
|
|
84
|
+
)
|
|
85
|
+
if live is None:
|
|
86
|
+
missing.append(declared.name)
|
|
87
|
+
continue
|
|
88
|
+
remaining.remove(live)
|
|
89
|
+
if live.nullable != declared.nullable:
|
|
90
|
+
nullability.append(NullabilityDiff(declared.name, declared.nullable, live.nullable))
|
|
91
|
+
return ShapeDiff(
|
|
92
|
+
missing=tuple(missing),
|
|
93
|
+
unexpected=tuple(column.name for column in remaining),
|
|
94
|
+
nullability=tuple(nullability),
|
|
95
|
+
)
|
{continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: continuo-python-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -12,7 +12,7 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Python: >=3.14
|
|
14
14
|
Requires-Dist: boto3==1.43.85
|
|
15
|
-
Requires-Dist: continuo-engine-contract==0.
|
|
15
|
+
Requires-Dist: continuo-engine-contract==0.8.0
|
|
16
16
|
Requires-Dist: pyarrow==25.0.1
|
|
17
17
|
Requires-Dist: pyyaml==6.0.3
|
|
18
18
|
Requires-Dist: sqlglot==30.17.0
|
|
@@ -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
|
|
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
|
|
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,20 +58,23 @@ 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
|
|
61
|
-
PyPI distributions into a single `dist
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
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
|
|
72
75
|
`WarehouseAdapter` per engine serves both the data plane (`fetch` /
|
|
73
|
-
`ensure_table` / `load
|
|
76
|
+
`ensure_table` / `load`, plus the opt-in `table_columns` / `append` / `upsert`
|
|
77
|
+
capabilities) and validation (`ensure_schema` / `drop_schema` /
|
|
74
78
|
`build_empty_from_sql` / `build_empty_from_columns` / `clone_empty_from_prod` /
|
|
75
79
|
`check_binds`), and one image per engine runs both roles.
|
|
76
80
|
|
|
@@ -80,16 +84,18 @@ validation-side port, adapter class, entry-point group, or image. One
|
|
|
80
84
|
| `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
85
|
| `continuo-postgres-adapter` | `continuo_postgres_adapter` | this repo, `adapters/postgres/` | `PostgresAdapter` — one class, both roles. Published to PyPI. |
|
|
82
86
|
| `continuo-trino-adapter` | `continuo_trino_adapter` | this repo, `adapters/trino/` | `TrinoAdapter` — one class, both roles, for Trino/Iceberg. Published to PyPI. |
|
|
87
|
+
| `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
88
|
|
|
84
|
-
All
|
|
89
|
+
All five are uv workspace members (`[tool.uv.workspace]` in the root
|
|
85
90
|
`pyproject.toml`), so `uv sync --all-packages --all-groups` at the repo root
|
|
86
91
|
installs everything for local development.
|
|
87
92
|
|
|
88
|
-
**All
|
|
89
|
-
same `vX.Y.Z` tag. The
|
|
93
|
+
**All five packages in the table above are published to PyPI**, under the
|
|
94
|
+
same `vX.Y.Z` tag. The three engine images then **install the matching pinned
|
|
90
95
|
adapter version from PyPI** — `Dockerfile.postgres` installs
|
|
91
96
|
`continuo-postgres-adapter==X.Y.Z`, `Dockerfile.trino` installs
|
|
92
|
-
`continuo-trino-adapter==X.Y.Z`
|
|
97
|
+
`continuo-trino-adapter==X.Y.Z` and `Dockerfile.duckdb` installs
|
|
98
|
+
`continuo-duckdb-adapter==X.Y.Z` — rather than building it from this repo's
|
|
93
99
|
source tree, so each image still ships exactly one adapter and the runtime
|
|
94
100
|
still discovers it through the `continuo_engine.adapters` entry-point group
|
|
95
101
|
at run time. The image **name** (`continuo-python-runtime-<engine>`) and the
|
|
@@ -97,7 +103,7 @@ adapter's pip **distribution** name (`continuo-<engine>-adapter`) are two
|
|
|
97
103
|
different artifacts of the same adapter — same engine, same version, same
|
|
98
104
|
runtime behavior, different packaging; see "Build your own container" below
|
|
99
105
|
for a build shape that installs the pip package directly instead of `FROM`
|
|
100
|
-
the image. All
|
|
106
|
+
the image. All five packages are still built, type-checked, and tested by CI
|
|
101
107
|
on every change.
|
|
102
108
|
|
|
103
109
|
### The result block is a frozen wire contract
|
|
@@ -116,9 +122,13 @@ the Go parser has not been taught is a production outage, not a refactor.
|
|
|
116
122
|
service name (one service name per domain repo).
|
|
117
123
|
3. Configure repository variables in GitHub (Settings → Secrets and
|
|
118
124
|
variables → Actions): `REGISTRY` (your Docker registry), `BUCKET` (your
|
|
119
|
-
S3 bucket for contract artifacts), `RELEASE_ENDPOINT` (the
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
S3 bucket for contract artifacts), `RELEASE_ENDPOINT` (the base
|
|
126
|
+
URL of your continuo install, `scheme://host[:port]` with no path, the origin of continuo's
|
|
127
|
+
`auth.publicUrl`). The
|
|
128
|
+
workflow calls `<RELEASE_ENDPOINT>/api/v1/releases` with its GitHub Actions
|
|
129
|
+
OIDC token, so the repository must be bound to your service in continuo's
|
|
130
|
+
`ciAuth.bindings` (see [Releasing from CI](https://github.com/carolsimone/continuo/blob/main/deploy/README.md#releasing-from-ci-github-actions)),
|
|
131
|
+
and an operator bootstraps the service's first release.
|
|
122
132
|
4. Configure repository secrets: `AWS_ACCESS_KEY_ID` and
|
|
123
133
|
`AWS_SECRET_ACCESS_KEY` for the S3 upload. The template workflow pushes
|
|
124
134
|
the built image to GHCR using the workflow's own `GITHUB_TOKEN` (granted
|
|
@@ -149,7 +159,7 @@ the pre-flight check once before your next release; it reports every affected
|
|
|
149
159
|
read at once:
|
|
150
160
|
|
|
151
161
|
```bash
|
|
152
|
-
continuo-runtime validate contracts/ --dialect postgres # or trino
|
|
162
|
+
continuo-runtime validate contracts/ --dialect postgres # or trino, duckdb
|
|
153
163
|
```
|
|
154
164
|
|
|
155
165
|
The runtime image does not re-run this gate, so a read that passes here is
|
|
@@ -186,6 +196,62 @@ produced, never how it is checked or written.
|
|
|
186
196
|
script reads with `os.environ[...]`. The contract never holds a secret value
|
|
187
197
|
or env var name; the Secret itself is created in the cluster by the operator.
|
|
188
198
|
|
|
199
|
+
## Write modes
|
|
200
|
+
|
|
201
|
+
`mode_type:` chooses how the harness writes a node's table on each run:
|
|
202
|
+
|
|
203
|
+
| `mode_type` | Each run | Rows from earlier runs |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `replace` (default) | empties the table, inserts this run's rows | gone |
|
|
206
|
+
| `append` | inserts this run's rows | kept |
|
|
207
|
+
| `upsert` | inserts rows with a new key, overwrites rows whose key exists | kept unless overwritten; never deleted |
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
- schema: fx
|
|
211
|
+
table: fx_rates
|
|
212
|
+
kind: python-api
|
|
213
|
+
mode_type: upsert
|
|
214
|
+
primary_key: [rate_date, base, quote]
|
|
215
|
+
output_columns:
|
|
216
|
+
- {name: rate_date, type: DATE, nullable: false}
|
|
217
|
+
- {name: base, type: VARCHAR(3), nullable: false}
|
|
218
|
+
- {name: quote, type: VARCHAR(3), nullable: false}
|
|
219
|
+
- {name: rate, type: DECIMAL(12,6)}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- `python-csv` nodes only `replace`.
|
|
223
|
+
- `primary_key` is required for `upsert` and rejected otherwise: a non-empty
|
|
224
|
+
list of output columns declared `nullable: false` (a NULL key never
|
|
225
|
+
matches), each spelled exactly as in `output_columns`, names unique
|
|
226
|
+
case-insensitively. A Postgres `replace` table that
|
|
227
|
+
wants uniqueness uses `config.indexes` with `unique: true`.
|
|
228
|
+
- Every write is atomic: all of the run's rows or none. An `append` or
|
|
229
|
+
`upsert` run with zero rows writes nothing (a `replace` run with zero rows
|
|
230
|
+
still empties the table).
|
|
231
|
+
- `append` is a plain insert: a run that committed but was reported failed
|
|
232
|
+
and is retried stores its rows twice. `upsert` is idempotent.
|
|
233
|
+
- An `upsert` run whose own rows repeat a key fails with `ConformError` and
|
|
234
|
+
writes nothing.
|
|
235
|
+
- Before an `append` or `upsert` the harness compares the live table with
|
|
236
|
+
`output_columns` — names case-insensitively, nullability exactly; order and
|
|
237
|
+
types are not compared — and fails with `LoadError` naming every missing
|
|
238
|
+
column, unexpected column and nullability mismatch. Changing an `append` or
|
|
239
|
+
`upsert` table (columns, types, nullability, key or mode) needs a new table
|
|
240
|
+
name (`_v2`) under a new contract; continuo rejects the release otherwise.
|
|
241
|
+
Adding a nullable column is no exception: the existing rows cannot be
|
|
242
|
+
backfilled.
|
|
243
|
+
- The installed engine must support the mode; all three shipped adapters do.
|
|
244
|
+
A bring-your-own adapter declares `AppendCapable` / `UpsertCapable` from
|
|
245
|
+
`continuo-engine-contract` (and can run its conformance suite); without them
|
|
246
|
+
an `append`/`upsert` node fails with `LoadError: <Adapter> does not support
|
|
247
|
+
<mode>` before its script runs.
|
|
248
|
+
- Engines: on Postgres the key is the table's `PRIMARY KEY`, created with the
|
|
249
|
+
table; DuckLake and Trino/Iceberg have no constraints, so the key is
|
|
250
|
+
guaranteed by the write path only. On Trino an `upsert` table needs Iceberg
|
|
251
|
+
`format_version` 2 (the default).
|
|
252
|
+
- `full_refresh` stays refused for every python node: a rebuild would destroy
|
|
253
|
+
an accumulating table.
|
|
254
|
+
|
|
189
255
|
## The script API
|
|
190
256
|
|
|
191
257
|
A node script is a Python file with exactly one required entry point:
|
|
@@ -272,8 +338,8 @@ text.
|
|
|
272
338
|
| `ContractError` | Contract missing or invalid, node not found for `NODE_ID`, or the declared script is missing/unreachable. | The contract yaml or the `script:` path. |
|
|
273
339
|
| `ReadError` | `ctx.read()` was called with an undeclared name, or a declared read failed at the warehouse. | The `reads:` map, or the upstream query/warehouse access. |
|
|
274
340
|
| `ScriptError` | `run()` raised, has no callable `run`, or returned a value that isn't Arrow-convertible. | The node script. |
|
|
275
|
-
| `ConformError` | Structural mismatch (extra/missing/duplicate columns), a strict-cast failure, a not-null violation,
|
|
276
|
-
| `LoadError` | Adapter construction failed, or the DDL/INSERT failed at the warehouse during the write. | Warehouse connectivity/permissions, or the target table. |
|
|
341
|
+
| `ConformError` | Structural mismatch (extra/missing/duplicate columns), a strict-cast failure, a not-null violation, a VARCHAR/CHAR overflow, or an `upsert` run whose rows repeat a primary key. | The script's output shape, or the `output_columns` declaration. |
|
|
342
|
+
| `LoadError` | Adapter construction failed, the installed engine does not support the node's `mode_type`, the live table's shape differs from `output_columns` (append/upsert), or the DDL/INSERT failed at the warehouse during the write. | Warehouse connectivity/permissions, the engine image, or the target table. |
|
|
277
343
|
|
|
278
344
|
## Engine selection
|
|
279
345
|
|
|
@@ -281,20 +347,23 @@ A domain repo picks its warehouse engine by which base image it builds
|
|
|
281
347
|
`FROM`:
|
|
282
348
|
|
|
283
349
|
```dockerfile
|
|
284
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.
|
|
350
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.9.0
|
|
351
|
+
# or
|
|
352
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.9.0
|
|
285
353
|
# or
|
|
286
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-
|
|
354
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.9.0
|
|
287
355
|
```
|
|
288
356
|
|
|
289
357
|
The engine is part of the image **name**; the tag is the bare version, so
|
|
290
358
|
Continuo's Helm chart can pin an image as `<name>:vX.Y.Z@sha256:<digest>`.
|
|
291
359
|
|
|
292
360
|
Each image bakes in exactly one `WarehouseAdapter` for that engine — the
|
|
293
|
-
pinned PyPI version of the `continuo-postgres-adapter
|
|
294
|
-
`continuo-trino-adapter` package built from this
|
|
295
|
-
|
|
361
|
+
pinned PyPI version of the `continuo-postgres-adapter`,
|
|
362
|
+
`continuo-trino-adapter` or `continuo-duckdb-adapter` package built from this
|
|
363
|
+
repo's `adapters/postgres/`, `adapters/trino/` or `adapters/duckdb/` source
|
|
364
|
+
(see the table above) — registered under the
|
|
296
365
|
`continuo_engine.adapters` entry-point group (entry names `postgres` /
|
|
297
|
-
`trino`). The runtime discovers it via `discover_adapter()` at run time, so a
|
|
366
|
+
`trino` / `duckdb`). The runtime discovers it via `discover_adapter()` at run time, so a
|
|
298
367
|
single image serves every node in the service and the release-time
|
|
299
368
|
validation Job for it. The executor injects the warehouse connection as
|
|
300
369
|
environment variables (engine-native, e.g.
|
|
@@ -6,15 +6,16 @@ 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=
|
|
9
|
+
continuo_python_runtime/harness.py,sha256=vm2wL7cOHT2uvrIgDZkNXAV7aT-6x7dQoCKirr41oJ0,16798
|
|
10
10
|
continuo_python_runtime/hashing.py,sha256=70iXQb0CmjGqniTPofXY6WapydQ7aK2XOk5dzpEf-zk,3354
|
|
11
11
|
continuo_python_runtime/lint.py,sha256=M59UtCHHVQnJKNolIkvkRk_AgGAFvxD9zkKAMMK-7GI,12984
|
|
12
|
+
continuo_python_runtime/shape.py,sha256=C-5ClSxPYgoO9pUiGqw7pCnmlBbsEl-sIm7Twbf-H4o,3693
|
|
12
13
|
continuo_python_runtime/types.py,sha256=_ssxNE2Lq5782n9_Ihs-u_6ybCCy8XaL6iYbE35tBug,5057
|
|
13
14
|
continuo_python_runtime/contract/__init__.py,sha256=S79x6j_P0CmaSuCGzeWoK1U0507KvD2MZXwQl9DFWMM,37
|
|
14
|
-
continuo_python_runtime/contract/kinds.py,sha256=
|
|
15
|
-
continuo_python_runtime/contract/loader.py,sha256=
|
|
16
|
-
continuo_python_runtime/contract/merge.py,sha256=
|
|
17
|
-
continuo_python_runtime/contract/model.py,sha256=
|
|
15
|
+
continuo_python_runtime/contract/kinds.py,sha256=nirtluv9GU0sZzGF8_-qRIdbeCRtBul_ppNnr6CIEC0,5320
|
|
16
|
+
continuo_python_runtime/contract/loader.py,sha256=uDlOYN6V7KhYhHk2aK34MHxSo3Z8riZw3FZ31Xf1Aqk,18386
|
|
17
|
+
continuo_python_runtime/contract/merge.py,sha256=vN1hA0WSHbUnDyyobW1Z2YYZvU4a0eqwDLS8d_xNGQo,8124
|
|
18
|
+
continuo_python_runtime/contract/model.py,sha256=Dq95CuAcSopumNqP9SlnrxXKhDVeYA55JwBKorhYde4,1435
|
|
18
19
|
continuo_python_runtime/contract/paths.py,sha256=SRn70pJkDAIpXw0IcMU2iNKPKvct8w_SZP2ec-lV91M,1458
|
|
19
20
|
continuo_python_runtime/contract/secret_ref.py,sha256=96jh2G81clpabiEWGIsxbd9r5GKEGLb4Sa1-WJ9G59Q,1131
|
|
20
21
|
continuo_python_runtime/csv_readers/__init__.py,sha256=9TKvjC37C77uRaJQZuO0--zVj2OvuNwpJE6nWx7j2S8,806
|
|
@@ -23,9 +24,9 @@ continuo_python_runtime/csv_readers/s3.py,sha256=zmvHs3TxlN2alPKrdaHzPyREmkve2nB
|
|
|
23
24
|
continuo_python_runtime/validation/__init__.py,sha256=hz5oGXsoaUoygR0_pBby9PS6cEfrre518EE0HGB58eE,79
|
|
24
25
|
continuo_python_runtime/validation/runner.py,sha256=7pbksf7MXfTNcdklCOkOXg9qfRiJtyI_dbgbL3a9LWk,14072
|
|
25
26
|
continuo_python_runtime/validation/s3.py,sha256=q8nRrT9R7L1-M4dSlDnkL7CUJMPmq86MHY_UKYFlEf8,1785
|
|
26
|
-
continuo_python_runtime-0.
|
|
27
|
-
continuo_python_runtime-0.
|
|
28
|
-
continuo_python_runtime-0.
|
|
29
|
-
continuo_python_runtime-0.
|
|
30
|
-
continuo_python_runtime-0.
|
|
31
|
-
continuo_python_runtime-0.
|
|
27
|
+
continuo_python_runtime-0.9.0.dist-info/METADATA,sha256=wUHVQEfqOyqKM7xdRqUXXsJE4AI1aDZQ3x9q3_9zon4,23389
|
|
28
|
+
continuo_python_runtime-0.9.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
29
|
+
continuo_python_runtime-0.9.0.dist-info/entry_points.txt,sha256=ZvIwSm9f9B9ZmZMf6CyF5Zt4QONn8rqaIk-u-B1CF2Y,70
|
|
30
|
+
continuo_python_runtime-0.9.0.dist-info/licenses/LICENSE,sha256=hfXfFCk-8gnps4YvHA6bjCNaSYqOrq2gAdqkXYycAQc,11345
|
|
31
|
+
continuo_python_runtime-0.9.0.dist-info/licenses/NOTICE,sha256=ykgYyQkAMx3uas90FZHzt0aTca9IgJQCLpZ4d74Pl3U,465
|
|
32
|
+
continuo_python_runtime-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
{continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{continuo_python_runtime-0.7.0.dist-info → continuo_python_runtime-0.9.0.dist-info}/licenses/NOTICE
RENAMED
|
File without changes
|