continuo-python-runtime 0.1.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.
@@ -0,0 +1,208 @@
1
+ """SQL type parsing and Arrow type mapping.
2
+
3
+ Parses SQL type strings into a canonical representation and provides mapping
4
+ to PyArrow types for schema generation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+
12
+ import pyarrow as pa # type: ignore[import-untyped]
13
+
14
+ from continuo_python_runtime.errors import ContractError
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class SqlType:
19
+ """Canonical SQL type representation.
20
+
21
+ Attributes:
22
+ base: Canonicalized base type name (one of BIGINT, INTEGER,
23
+ DOUBLE_PRECISION, NUMERIC, VARCHAR, CHAR, TEXT, TIMESTAMP,
24
+ DATE, BOOLEAN).
25
+ precision: For NUMERIC types, the total number of digits.
26
+ scale: For NUMERIC types, the number of digits after the decimal.
27
+ length: For VARCHAR and CHAR types, the maximum length in characters.
28
+ """
29
+
30
+ base: str
31
+ precision: int | None = None
32
+ scale: int | None = None
33
+ length: int | None = None
34
+
35
+
36
+ def parse_sql_type(raw: str) -> SqlType:
37
+ """Parse a SQL type string into a canonical SqlType.
38
+
39
+ Args:
40
+ raw: A SQL type string (case-insensitive).
41
+
42
+ Returns:
43
+ A SqlType with canonicalized base and parsed parameters.
44
+
45
+ Raises:
46
+ ContractError: If the type is unsupported or malformed.
47
+ """
48
+ raw = raw.strip()
49
+ if not raw:
50
+ raise ContractError("type: empty string is not a valid SQL type")
51
+
52
+ # Normalize to uppercase for parsing
53
+ normalized = raw.upper()
54
+
55
+ # Handle DOUBLE PRECISION specially (has a space). Only the authored
56
+ # spelling with a space is part of the contract grammar; the internal
57
+ # canonical spelling (with an underscore) is not accepted as input even
58
+ # though it is the SqlType.base value produced above.
59
+ if normalized == "DOUBLE PRECISION":
60
+ return SqlType("DOUBLE_PRECISION")
61
+
62
+ # Try to match parametrized types: TYPE(args)
63
+ match = re.match(r"^([A-Z_]+)\s*\((.*)\)$", normalized)
64
+ if match:
65
+ base_name = match.group(1).strip()
66
+ params_str = match.group(2).strip()
67
+
68
+ # Alias handling
69
+ if base_name == "INT":
70
+ base_name = "INTEGER"
71
+ elif base_name == "DECIMAL":
72
+ base_name = "NUMERIC"
73
+
74
+ if base_name == "NUMERIC":
75
+ # Parse NUMERIC(precision, scale) - strict format: \d+,\s*\d+ (spaces only after comma)
76
+ numeric_match = re.match(r"^(\d+),\s*(\d+)$", params_str)
77
+ if not numeric_match:
78
+ raise ContractError(
79
+ f"type: NUMERIC requires precision and scale as unsigned integers, "
80
+ f"got ({params_str})"
81
+ )
82
+ precision = int(numeric_match.group(1))
83
+ scale = int(numeric_match.group(2))
84
+ if not (1 <= precision <= 38):
85
+ raise ContractError(
86
+ f"type: NUMERIC precision must be between 1 and 38, got {precision}"
87
+ )
88
+ if not (0 <= scale <= precision):
89
+ raise ContractError(
90
+ f"type: NUMERIC scale must be between 0 and precision ({precision}), "
91
+ f"got {scale}"
92
+ )
93
+ return SqlType("NUMERIC", precision=precision, scale=scale)
94
+
95
+ elif base_name == "VARCHAR":
96
+ # Parse VARCHAR(length) - strict format: \d+ only
97
+ if "," in params_str:
98
+ raise ContractError(
99
+ f"type: VARCHAR takes a single parameter (length), "
100
+ f"got {params_str!r}"
101
+ )
102
+ varchar_match = re.match(r"^(\d+)$", params_str)
103
+ if not varchar_match:
104
+ raise ContractError(
105
+ f"type: VARCHAR length must be an unsigned integer, "
106
+ f"got {params_str!r}"
107
+ )
108
+ length = int(varchar_match.group(1))
109
+ return SqlType("VARCHAR", length=length)
110
+
111
+ elif base_name == "CHAR":
112
+ # Parse CHAR(length) - strict format: \d+ only
113
+ if "," in params_str:
114
+ raise ContractError(
115
+ f"type: CHAR takes a single parameter (length), "
116
+ f"got {params_str!r}"
117
+ )
118
+ char_match = re.match(r"^(\d+)$", params_str)
119
+ if not char_match:
120
+ raise ContractError(
121
+ f"type: CHAR length must be an unsigned integer, "
122
+ f"got {params_str!r}"
123
+ )
124
+ length = int(char_match.group(1))
125
+ return SqlType("CHAR", length=length)
126
+
127
+ else:
128
+ # No other types accept parameters
129
+ raise ContractError(
130
+ f"type: {base_name} does not accept parameters, got ({params_str})"
131
+ )
132
+
133
+ # No parameters - must be a bare base type
134
+ normalized_base = normalized
135
+
136
+ # Alias handling
137
+ if normalized_base == "INT":
138
+ normalized_base = "INTEGER"
139
+ elif normalized_base == "DECIMAL":
140
+ normalized_base = "NUMERIC"
141
+
142
+ # List of supported bare types
143
+ # Note: "DOUBLE_PRECISION" (underscored) is deliberately excluded here.
144
+ # It is the canonical SqlType.base value, but only the authored spelling
145
+ # "DOUBLE PRECISION" (with a space) is part of the contract grammar and
146
+ # is handled above.
147
+ supported_bare = {
148
+ "BIGINT",
149
+ "INTEGER",
150
+ "TEXT",
151
+ "TIMESTAMP",
152
+ "DATE",
153
+ "BOOLEAN",
154
+ }
155
+
156
+ if normalized_base in supported_bare:
157
+ return SqlType(normalized_base)
158
+
159
+ # Bare VARCHAR and CHAR require a length parameter
160
+ if normalized_base == "VARCHAR":
161
+ raise ContractError(f"type: VARCHAR requires a length parameter, got {raw!r}")
162
+ if normalized_base == "CHAR":
163
+ raise ContractError(f"type: CHAR requires a length parameter, got {raw!r}")
164
+
165
+ # Bare NUMERIC and DECIMAL require parameters
166
+ if normalized_base == "NUMERIC":
167
+ raise ContractError(f"type: NUMERIC requires precision and scale parameters, got {raw!r}")
168
+
169
+ # Unknown type
170
+ raise ContractError(f"type: unknown SQL type {raw!r}")
171
+
172
+
173
+ def arrow_type(t: SqlType) -> pa.DataType:
174
+ """Map a SqlType to a PyArrow DataType.
175
+
176
+ Args:
177
+ t: A SqlType instance.
178
+
179
+ Returns:
180
+ A PyArrow DataType corresponding to the SQL type.
181
+
182
+ Raises:
183
+ ValueError: If the SqlType is invalid or incomplete.
184
+ """
185
+ if t.base == "BIGINT":
186
+ return pa.int64()
187
+ elif t.base == "INTEGER":
188
+ return pa.int32()
189
+ elif t.base == "DOUBLE_PRECISION":
190
+ return pa.float64()
191
+ elif t.base == "NUMERIC":
192
+ if t.precision is None or t.scale is None:
193
+ raise ValueError(f"NUMERIC requires precision and scale, got {t}")
194
+ return pa.decimal128(t.precision, t.scale)
195
+ elif t.base == "VARCHAR":
196
+ return pa.string()
197
+ elif t.base == "CHAR":
198
+ return pa.string()
199
+ elif t.base == "TEXT":
200
+ return pa.string()
201
+ elif t.base == "TIMESTAMP":
202
+ return pa.timestamp("us")
203
+ elif t.base == "DATE":
204
+ return pa.date32()
205
+ elif t.base == "BOOLEAN":
206
+ return pa.bool_()
207
+ else:
208
+ raise ValueError(f"unknown SQL type base: {t.base}")
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: continuo-python-runtime
3
+ Version: 0.1.0
4
+ Summary: Runtime harness, contract tooling, and CI lint for Continuo python nodes.
5
+ Author: Simone Carolini
6
+ Maintainer: Simone Carolini
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Requires-Python: >=3.14
11
+ Requires-Dist: continuo-validation-contract==0.3.0
12
+ Requires-Dist: pyarrow==25.0.0
13
+ Requires-Dist: pyyaml==6.0.3
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Continuo Python Runtime
17
+
18
+ Runtime harness, contract tooling, and CI lint for Continuo python nodes.
19
+ This repo is what domain data teams (marketing, finance, …) template from to
20
+ ship a python node into Continuo: write a contract + a `run(ctx)` script,
21
+ push to `main`, and CI does the rest — lint, validate, merge, build, publish,
22
+ and register the release with Continuo.
23
+
24
+ ## What this repo is
25
+
26
+ Three artifacts come out of this repository:
27
+
28
+ - **The `continuo-python-runtime` PyPI package** — the `continuo-runtime` CLI
29
+ (`validate` / `merge` / `hash` / `lint` / `run`) and the harness library
30
+ (`conform()`, `RunContext`, the error taxonomy) that domain repos install.
31
+ - **Per-engine base images**, one per warehouse engine (`...-postgres`,
32
+ `...-trino`), that domain repos build `FROM`. Each image bakes in the
33
+ runtime, a single `RuntimeAdapter` for that engine, and the
34
+ `continuo-runtime run` entrypoint.
35
+ - **`template/`** — a copy-ready domain repo: `Dockerfile`, `contracts/`,
36
+ `scripts/`, and the `release.yml` CI/CD workflow.
37
+
38
+ Base images and the PyPI publication of this package land with this repo's
39
+ PR 9 (the image pipeline); until then, install the runtime from git as noted
40
+ in `template/.github/workflows/release.yml`.
41
+
42
+ ### Package map
43
+
44
+ Each per-engine base image bakes together three PyPI packages: the harness,
45
+ the engine adapter, and the published contract. The harness and both engine
46
+ adapters live in *this* repo (`continuo-python-runtime`) as a uv workspace;
47
+ `continuo-validation-runners` owns the *validation-side* (offline lint/merge)
48
+ counterparts of the same engines, which is a separate concern from the
49
+ data-plane adapters here.
50
+
51
+ | Package (distribution name) | Module | Lives in | Role |
52
+ | --- | --- | --- | --- |
53
+ | `continuo-python-runtime` | `continuo_python_runtime` | this repo (root) | Harness: CLI, `conform()`, `RunContext`, error taxonomy. |
54
+ | `continuo-python-runtime-postgres` | `continuo_python_runtime_postgres` | this repo, `python-runtime-postgres/` | Data-plane `RuntimeAdapter` for Postgres (`fetch`/`ensure_table`/`load`). |
55
+ | `continuo-python-runtime-trino` | `continuo_python_runtime_trino` | this repo, `python-runtime-trino/` | Data-plane `RuntimeAdapter` for Trino/Iceberg. |
56
+ | `continuo-validation-contract` | `continuo_validation_contract` | `continuo-validation-runners` | The published contract (schema, `RuntimeAdapter` port, result-block format) both sides depend on. |
57
+ | `continuo-validation-postgres` / `continuo-validation-trino` | `continuo_validation_postgres` / `continuo_validation_trino` | `continuo-validation-runners` | Validation-side (lint/merge, no live warehouse I/O) adapters — not to be confused with the data-plane adapters above. |
58
+
59
+ All three packages built in this repo resolve `continuo-validation-contract`
60
+ from PyPI (`==0.3.0`); the two adapter packages are uv workspace members
61
+ (`[tool.uv.workspace]` in the root `pyproject.toml`), so `uv sync
62
+ --all-packages --all-groups` at the repo root installs everything for local
63
+ development.
64
+
65
+ ## Quickstart for domain teams
66
+
67
+ 1. Copy `template/` into a new repository.
68
+ 2. Edit `template/.github/workflows/release.yml` and set `SERVICE` to your
69
+ service name (one service name per domain repo).
70
+ 3. Configure repository variables in GitHub (Settings → Secrets and
71
+ variables → Actions): `REGISTRY` (your Docker registry), `BUCKET` (your
72
+ S3 bucket for contract artifacts), `RELEASE_ENDPOINT` (the release
73
+ webhook endpoint). `RELEASE_ENDPOINT` is the **base URL** of the Continuo
74
+ API (no `/releases` suffix) — the workflow appends `/releases` itself.
75
+ 4. Configure repository secrets: `AWS_ACCESS_KEY_ID` and
76
+ `AWS_SECRET_ACCESS_KEY` for the S3 upload. The template workflow pushes
77
+ the built image to GHCR using the workflow's own `GITHUB_TOKEN` (granted
78
+ `packages: write`) — no registry secret is needed for that. If you point
79
+ `REGISTRY` at a different or private registry, add your own `docker
80
+ login` step to `release.yml`.
81
+ 5. Write a contract file under `contracts/` (see
82
+ `template/contracts/example.yml`) and a script under `scripts/` that
83
+ implements `run(ctx)` (see `template/scripts/example.py`).
84
+ 6. Push to `main`. The `release.yml` workflow lints the scripts, validates
85
+ and merges the contracts, builds and pushes the image, uploads the merged
86
+ contract to S3, and POSTs the release.
87
+
88
+ ## The script API
89
+
90
+ A node script is a Python file with exactly one required entry point:
91
+
92
+ ```python
93
+ def run(ctx):
94
+ ...
95
+ ```
96
+
97
+ - `ctx` is a `RunContext` (`continuo_python_runtime.context.RunContext`).
98
+ Its only method is `ctx.read(name)`, where `name` is one of the read
99
+ names declared under the node's `reads:` map in the contract — reading
100
+ anything else raises `ReadError`. Each declared read is fetched once and
101
+ memoized; `ctx.read(name)` returns a `pyarrow.Table`.
102
+ - `run(ctx)` can return anything Arrow-convertible: a `pyarrow.Table`
103
+ as-is, a pandas `DataFrame` (converted via
104
+ `pa.Table.from_pandas(..., preserve_index=False)`), or any object
105
+ implementing the Arrow C stream protocol (`__arrow_c_stream__`) — for
106
+ example a polars DataFrame. Returning anything else raises `ScriptError`.
107
+ - The dataframe library is your choice. No dataframe library is baked into
108
+ the base image — the package's own runtime dependencies are `pyarrow`,
109
+ `PyYAML`, and `continuo-validation-contract`. Add whatever you script
110
+ against (pandas, polars, …) as a `RUN pip install` line in your own
111
+ `Dockerfile`, on top of the base image.
112
+ - Scripts do not import warehouse drivers, write raw SQL literals, or call
113
+ data-access methods directly — `continuo-runtime lint` rejects those:
114
+ - forbidden driver imports (`psycopg2`/`sqlalchemy`/`trino`/etc.),
115
+ - SQL string literals (in plain strings, f-strings, and `+` concatenation),
116
+ - forbidden data-access calls (`execute`/`read_sql`/etc.), including ones
117
+ reached via a `from ... import` alias (e.g. `from pandas import
118
+ read_sql as rs` then calling `rs(...)`),
119
+ - private/protected attribute access (`obj._x`) — except on `self`/`cls`,
120
+ so a script's own class-private helpers (`self._helper()`) aren't
121
+ flagged.
122
+
123
+ The SQL-literal rule is best-effort: docstrings are exempt, but other
124
+ prose may still occasionally match. The hard guarantees enforced by lint
125
+ are the driver-import and data-access-call rules, together with the fact
126
+ that `RunContext` only exposes `ctx.read()` — all warehouse access goes
127
+ through it.
128
+ - The harness — not the script — performs the write. It calls `conform()`
129
+ on whatever `run()` returned and issues the only INSERT; the script never
130
+ writes directly.
131
+
132
+ ## Conform rules
133
+
134
+ `conform()` (`continuo_python_runtime/conform.py`) enforces the node's
135
+ declared `output_columns` on the table `run()` returned, in this order:
136
+
137
+ | Check | Behavior |
138
+ | --- | --- |
139
+ | Duplicate columns | Any duplicate column name in the returned table always raises `ConformError`. |
140
+ | Extra columns | Governed by the node's `extra_columns` policy: `raise` (default) fails the run; `warn` drops the undeclared column(s) and logs a warning. |
141
+ | Missing columns | Any declared column absent from the returned table always raises `ConformError`. |
142
+ | Column order | The table is reselected into the declared column order. |
143
+ | Strict cast | Each column is cast to its declared Arrow type with `safe=True`. Casts pyarrow's `safe=True` would silently accept but that are not value-lossless are rejected before the cast even runs: floating → decimal (rounds to scale), non-boolean → boolean (coerces truthiness), timestamp → date (drops time-of-day). Any other cast failure also raises `ConformError`. |
144
+ | Not-null | A column declared `nullable: false` that contains any null raises `ConformError`. |
145
+ | VARCHAR/CHAR length | A column declared `VARCHAR(n)`/`CHAR(n)` whose longest value exceeds `n` raises `ConformError`. |
146
+
147
+ ## Error taxonomy
148
+
149
+ Every runtime failure is one of five `HarnessError` subclasses
150
+ (`continuo_python_runtime/errors.py`). The sentinel result block's message
151
+ is prefixed `<ErrorClass>: ` so failures can be triaged without parsing free
152
+ text.
153
+
154
+ | Class | Meaning | Typical fix target |
155
+ | --- | --- | --- |
156
+ | `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. |
157
+ | `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. |
158
+ | `ScriptError` | `run()` raised, has no callable `run`, or returned a value that isn't Arrow-convertible. | The node script. |
159
+ | `ConformError` | Structural mismatch (extra/missing/duplicate columns), a strict-cast failure, a not-null violation, or a VARCHAR/CHAR overflow. | The script's output shape, or the `output_columns` declaration. |
160
+ | `LoadError` | Adapter construction failed, or the DDL/INSERT failed at the warehouse during the write. | Warehouse connectivity/permissions, or the target table. |
161
+
162
+ ## Engine selection
163
+
164
+ A domain repo picks its warehouse engine by which base image it builds
165
+ `FROM`:
166
+
167
+ ```dockerfile
168
+ FROM ghcr.io/carolsimone/continuo-python-runtime:v0.1.0-postgres
169
+ # or
170
+ FROM ghcr.io/carolsimone/continuo-python-runtime:v0.1.0-trino
171
+ ```
172
+
173
+ Each image bakes in exactly one `RuntimeAdapter` for that engine — installed
174
+ from this repo's `python-runtime-postgres/` or `python-runtime-trino/`
175
+ package (see the package map above; both adapters live in this repo, not
176
+ `continuo-validation-runners`) — registered under the
177
+ `continuo_runtime.adapters` entry-point group (entry names `postgres` /
178
+ `trino`). The harness discovers it via `discover_runtime_adapter()` at run
179
+ time, so a single image serves every node in the service. The executor
180
+ injects the
181
+ warehouse connection as environment variables (engine-native, e.g.
182
+ `POSTGRES_HOST`/`POSTGRES_DB`/`POSTGRES_USER`) plus the node-selection
183
+ environment (`NODE_ID`, `TABLE_NAME`, `TARGET_SCHEMA`, and optionally
184
+ `CONTRACT_DIR`/`APP_ROOT`) that `continuo-runtime run` reads to dispatch the
185
+ right node's script.
186
+
187
+ ## Further reading
188
+
189
+ - `docs/superpowers/specs/2026-07-31-python-runtime-design.md` — this
190
+ repo's design.
191
+ - `docs/boundary-contract.md` — the parent design's boundary contract (§13):
192
+ the five surfaces (S3 artifact, `content_hash`, the release call, the
193
+ runtime image, and the domain repo's CI/CD) that this repo implements.
@@ -0,0 +1,18 @@
1
+ continuo_python_runtime/__init__.py,sha256=otNaXEPsTtRXJV5FLtDUsFO8VD4OC-MLAzwhtimHfOE,70
2
+ continuo_python_runtime/cli.py,sha256=Qki4Mfoab74D0DvguZd5yzSfI-FRQDmtvwC0OacxrAc,4860
3
+ continuo_python_runtime/conform.py,sha256=a9CKCA-gT9Zerzr6c5H8aByTFOUd6uBMo02sNKSDnXg,9199
4
+ continuo_python_runtime/context.py,sha256=ZyeN_dA2DihQbegBiiqHj1_EKk79GsxS-tATPVymUPg,1856
5
+ continuo_python_runtime/errors.py,sha256=nfZzfBhQ2ajm7USAWKKaGY2uUlh8eNjNPNYYuaOJgZE,1032
6
+ continuo_python_runtime/harness.py,sha256=YcWy4ummKY3HQYQ4JQ3-UI5jxDgYwJDQlWT88_GRPH4,7504
7
+ continuo_python_runtime/hashing.py,sha256=6D4MntcjaHTEC55IQBBX2Pq9K59D0TWlxTSXGJ5Jsro,1159
8
+ continuo_python_runtime/lint.py,sha256=HgB0BAuiQuS3DgT6BsOoB2M5A-_OKcFsGbaTWhuM7XU,11897
9
+ continuo_python_runtime/types.py,sha256=z1qnBbbaP6nFN2mmRySXM4ZAesv5N6_4E9vZ4kv74HU,7138
10
+ continuo_python_runtime/contract/__init__.py,sha256=S79x6j_P0CmaSuCGzeWoK1U0507KvD2MZXwQl9DFWMM,37
11
+ continuo_python_runtime/contract/loader.py,sha256=wlxI6XZIoUZ4z9rDe1G8s26jbL0tGXnAHPGajTAM0ag,12828
12
+ continuo_python_runtime/contract/merge.py,sha256=WFde9lzrojlMNjuEU7YW8FWOs6VPSrWPY2_Sri0ljZE,2567
13
+ continuo_python_runtime/contract/model.py,sha256=6luq3SJcgshKoG0QsQhJPZ5fzLa8AcGAev9D4jiGgxA,849
14
+ continuo_python_runtime/contract/paths.py,sha256=SRn70pJkDAIpXw0IcMU2iNKPKvct8w_SZP2ec-lV91M,1458
15
+ continuo_python_runtime-0.1.0.dist-info/METADATA,sha256=YQWMZaYiS3gpRnIutzgKhT3KeB5BtUpeMrwi0qKLRUU,10953
16
+ continuo_python_runtime-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
17
+ continuo_python_runtime-0.1.0.dist-info/entry_points.txt,sha256=ZvIwSm9f9B9ZmZMf6CyF5Zt4QONn8rqaIk-u-B1CF2Y,70
18
+ continuo_python_runtime-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ continuo-runtime = continuo_python_runtime.cli:main