Continuo Python Runtime
Runtime harness, contract tooling, and CI lint for Continuo python nodes.
This repo is what domain data teams (marketing, finance, …) template from to
ship a python node into Continuo: write a contract + a run(ctx) script,
push to main, and CI does the rest — lint, validate, merge, build, publish,
and register the release with Continuo.
What this repo is
Four artifacts come out of this repository:
- The
continuo-python-runtimePyPI package — thecontinuo-runtimeCLI (validate/merge/hash/lint/run/validation-op) and the harness library (conform(),RunContext, the error taxonomy) that domain repos install. - The
continuo-engine-contractPyPI package — theWarehouseAdapterport, the contract schema, the shared SQL/type/config guards, and the sentinel result-block format. Adapter authors outside this repo pin it. - Per-engine base images, one per warehouse engine
(
continuo-python-runtime-postgres,continuo-python-runtime-trino), that domain repos buildFROM. Each image bakes in the runtime and a singleWarehouseAdapterfor that engine, and serves both roles that adapter has: the node harness (ENTRYPOINT ["continuo-runtime"],CMD ["run"]) and the validation runner (continuo-runtime validation-op). template/— a copy-ready domain repo:Dockerfile,contracts/,scripts/, and therelease.ymlCI/CD workflow.
One vX.Y.Z git tag releases all of it: publish-pypi.yml builds both
distributions into a single dist/ and publishes them together, and
images.yml builds and pushes both engine images multi-arch under the same
tag.
What this repo owns
This repository owns the entire python-node surface: the engine contract, both
engine adapters, the validation runner, and the node harness. The former
continuo-validation repository was merged in — there is no longer a separate
validation-side port, adapter class, entry-point group, or image. One
WarehouseAdapter per engine serves both the data plane (fetch /
ensure_table / load) and validation (ensure_schema / drop_schema /
build_empty_from_sql / build_empty_from_columns / clone_empty_from_prod /
check_binds), and one image per engine runs both roles.
| Package (distribution name) | Module | Lives in | Role |
|---|---|---|---|
continuo-python-runtime |
continuo_python_runtime |
this repo (root) | Harness (CLI, conform(), RunContext, error taxonomy) and the validation runner (continuo-runtime validation-op). Published to PyPI. |
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. |
continuo-python-runtime-postgres |
continuo_python_runtime_postgres |
this repo, adapters/postgres/ |
PostgresAdapter — one class, both roles. Not published to PyPI — built from source into the image. |
continuo-python-runtime-trino |
continuo_python_runtime_trino |
this repo, adapters/trino/ |
TrinoAdapter — one class, both roles, for Trino/Iceberg. Not published to PyPI — built from source into the image. |
All four are uv workspace members ([tool.uv.workspace] in the root
pyproject.toml), so uv sync --all-packages --all-groups at the repo root
installs everything for local development.
Only continuo-python-runtime and continuo-engine-contract are published
to PyPI. The two engine adapters are built from source into the engine
images: Dockerfile.postgres and Dockerfile.trino install them out of the
build context, so each image ships exactly one adapter and the runtime
discovers it through the continuo_engine.adapters entry-point group at run
time. Nothing installs them from an index — the harness package does not
depend on them, and domain repos get their adapter by building FROM a
published base image. They are still built, type-checked, and tested by CI on
every change.
The result block is a frozen wire contract
continuo_engine_contract.result writes a sentinel-framed JSON block as the
last line of stdout, and Continuo's Go side parses it byte-for-byte
(pkg/validationresult in the continuo repository). That wire format — the
sentinel markers, the framing, and the field names inside the block — is
frozen. Reuse it; never change it from this side alone. A change here that
the Go parser has not been taught is a production outage, not a refactor.
Quickstart for domain teams
- Copy
template/into a new repository. - Edit
template/.github/workflows/release.ymland setSERVICEto your service name (one service name per domain repo). - Configure repository variables in GitHub (Settings → Secrets and
variables → Actions):
REGISTRY(your Docker registry),BUCKET(your S3 bucket for contract artifacts),RELEASE_ENDPOINT(the release webhook endpoint).RELEASE_ENDPOINTis the base URL of the Continuo API (no/releasessuffix) — the workflow appends/releasesitself. - Configure repository secrets:
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYfor the S3 upload. The template workflow pushes the built image to GHCR using the workflow's ownGITHUB_TOKEN(grantedpackages: write) — no registry secret is needed for that. If you pointREGISTRYat a different or private registry, add your owndocker loginstep torelease.yml. - Write a contract file under
contracts/(seetemplate/contracts/example.yml) and a script underscripts/that implementsrun(ctx)(seetemplate/scripts/example.py). - Push to
main. Therelease.ymlworkflow lints the scripts, validates and merges the contracts, builds and pushes the image, uploads the merged contract to S3, and POSTs the release.
Upgrading an existing domain repo
validate / merge / hash now hand every declared read to a real SQL
parser (sqlglot, via continuo_engine_contract.sql.ensure_single_read)
instead of scanning it for a leading SELECT/WITH. Two things follow for a
repo written before this, on its next release: SQL a driver would accept but
a parser will not — most commonly a driver-specific bind placeholder like
psycopg2's %(name)s, which ctx.read(name) could never have used anyway —
now fails validation, and engine-specific syntax (postgres ~, @>, …)
needs --dialect <engine>, which a repo should be passing regardless since
Continuo bind-checks every read in the install's own warehouse dialect. Run
the pre-flight check once before your next release; it reports every affected
read at once:
continuo-runtime validate contracts/ --dialect postgres # or trino
The runtime image does not re-run this gate, so a read that passes here is
not re-judged under a different grammar in production. See
docs/boundary-contract.md §13.1.
The script API
A node script is a Python file with exactly one required entry point:
def run(ctx):
...
-
ctxis aRunContext(continuo_python_runtime.context.RunContext). Its only method isctx.read(name), wherenameis one of the read names declared under the node'sreads:map in the contract — reading anything else raisesReadError. Each declared read is fetched once and memoized;ctx.read(name)returns apyarrow.Table. -
run(ctx)can return anything Arrow-convertible: apyarrow.Tableas-is, a pandasDataFrame(converted viapa.Table.from_pandas(..., preserve_index=False)), or any object implementing the Arrow C stream protocol (__arrow_c_stream__) — for example a polars DataFrame. Returning anything else raisesScriptError. -
The dataframe library is your choice. No dataframe library is baked into the base image — the package's own runtime dependencies are
pyarrow,PyYAML, andcontinuo-engine-contract. Add whatever you script against (pandas, polars, …) as aRUN pip installline in your ownDockerfile, on top of the base image. The base image runs as non-root (uid 65532), which pip cannot install under, so switch to root for the install and back afterward —USER root, then theRUN pip installline, thenUSER 65532:65532— since the executor's pod spec expects the image to end at uid 65532. Seetemplate/Dockerfilefor the pattern. -
Scripts do not import warehouse drivers, write raw SQL literals, or call data-access methods directly —
continuo-runtime lintrejects those:- forbidden driver imports (
psycopg2/sqlalchemy/trino/etc.), - SQL string literals (in plain strings, f-strings, and
+concatenation), - forbidden data-access calls (
execute/read_sql/etc.), including ones reached via afrom ... importalias (e.g.from pandas import read_sql as rsthen callingrs(...)), - private/protected attribute access (
obj._x) — except onself/cls, so a script's own class-private helpers (self._helper()) aren't flagged.
The SQL-literal rule is best-effort: docstrings are exempt, but other prose may still occasionally match. The hard guarantees enforced by lint are the driver-import and data-access-call rules, together with the fact that
RunContextonly exposesctx.read()— all warehouse access goes through it. - forbidden driver imports (
-
Scripts may import shared in-repo helpers. Before executing a script the harness puts the repo root (
APP_ROOT) and the script's own directory onsys.path, so bothimport helpers(a sibling of the script) andfrom lib.shared import ...(anywhere under the repo root) work, including from insiderun(). Every helper a script reaches transitively is folded intoshared_code_hash, so editing one re-fingerprints the node — but the hash does not put the file in the image:COPYevery directory your scripts import from in yourDockerfile, or the release is valid and the node dies withModuleNotFoundErroron its first run. Because the repo root precedes the standard library onsys.path, avoid naming a top-level module after a stdlib one (types.py,json.py,logging.py, …). -
The harness — not the script — performs the write. It calls
conform()on whateverrun()returned and issues the only INSERT; the script never writes directly.
Conform rules
conform() (continuo_python_runtime/conform.py) enforces the node's
declared output_columns on the table run() returned, in this order:
| Check | Behavior |
|---|---|
| Duplicate columns | Any duplicate column name in the returned table always raises ConformError. |
| 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. |
| Missing columns | Any declared column absent from the returned table always raises ConformError. |
| Column order | The table is reselected into the declared column order. |
| 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. |
| Not-null | A column declared nullable: false that contains any null raises ConformError. |
| VARCHAR/CHAR length | A column declared VARCHAR(n)/CHAR(n) whose longest value exceeds n raises ConformError. |
Error taxonomy
Every runtime failure is one of five HarnessError subclasses
(continuo_python_runtime/errors.py). The sentinel result block's message
is prefixed <ErrorClass>: so failures can be triaged without parsing free
text.
| Class | Meaning | Typical fix target |
|---|---|---|
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. |
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. |
ScriptError |
run() raised, has no callable run, or returned a value that isn't Arrow-convertible. |
The node script. |
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. |
LoadError |
Adapter construction failed, or the DDL/INSERT failed at the warehouse during the write. | Warehouse connectivity/permissions, or the target table. |
Engine selection
A domain repo picks its warehouse engine by which base image it builds
FROM:
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.3.1
# or
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.3.1
The engine is part of the image name; the tag is the bare version, so
Continuo's Helm chart can pin an image as <name>:vX.Y.Z@sha256:<digest>.
Each image bakes in exactly one WarehouseAdapter for that engine — installed
from this repo's adapters/postgres/ or adapters/trino/ package (see the
table above) — registered under the continuo_engine.adapters entry-point
group (entry names postgres / trino). The runtime discovers it via
discover_adapter() at run time, so a single image serves every node in the
service and the release-time validation Job for it. The executor injects the
warehouse connection as environment variables (engine-native, e.g.
POSTGRES_HOST/POSTGRES_DB/POSTGRES_USER) plus the node-selection
environment (NODE_ID, TABLE_NAME, TARGET_SCHEMA, and optionally
CONTRACT_DIR/APP_ROOT) that continuo-runtime run reads to dispatch the
right node's script.
Further reading
docs/superpowers/specs/2026-07-31-python-runtime-design.md— this repo's design.docs/boundary-contract.md— the parent design's boundary contract (§13): the five surfaces (S3 artifact,content_hash, the release call, the runtime image, and the domain repo's CI/CD) that this repo implements.
Metadata
Release files for continuo-python-runtime 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| continuo_python_runtime-0.3.1.tar.gz | 241.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| continuo_python_runtime-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 292.5 kB
Release files / continuo_python_runtime-0.3.1.tar.gz
| Download URL | continuo_python_runtime-0.3.1.tar.gz |
|---|---|
| Size | 241.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a6f7e64147d3ca17aab1371dfed5620d67e7dd964f0ab525bb2f179284a27e2
|
|
BLAKE2b-256 checksum How to use checksums |
a14029668f5c8db5a01ef4cf2ae211a802c8082d54257363765fc8502746042d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency logRelease files / continuo_python_runtime-0.3.1-py3-none-any.whl
| Download URL | continuo_python_runtime-0.3.1-py3-none-any.whl |
|---|---|
| Size | 51.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
81f306e8db0bff620de5065ba3a658ff071cac46ae27508634093900fac25285
|
|
BLAKE2b-256 checksum How to use checksums |
65810e317caac9cdfdbcbfab7faffd4d911cc86feaf3ba099ea143c53381560a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency log