diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da887d4..a1ad70b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,9 @@ jobs: - name: Types (contract) run: uv run mypy contract/continuo_engine_contract - name: Types (postgres adapter) - run: uv run --package continuo-python-runtime-postgres mypy adapters/postgres/continuo_python_runtime_postgres + run: uv run --package continuo-postgres-adapter mypy adapters/postgres/continuo_postgres_adapter - name: Types (trino adapter) - run: uv run --package continuo-python-runtime-trino mypy adapters/trino/continuo_python_runtime_trino + run: uv run --package continuo-trino-adapter mypy adapters/trino/continuo_trino_adapter # `-m "not image"` deselects tests/test_image_smoke_validation.py, which # needs a built engine image and the env naming it. Those tests run in # images.yml's smoke jobs, where an image actually exists. `and not diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 7be2ebe..9ab0792 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -21,8 +21,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v7 + - name: Build wheels into a wheelhouse + run: uv build --all-packages -o wheelhouse - name: Build ${{ matrix.engine }} image - run: docker build -f Dockerfile.${{ matrix.engine }} -t cpr-smoke-${{ matrix.engine }} . + run: docker build -f Dockerfile.${{ matrix.engine }} --build-arg WHEEL_SOURCE=wheelhouse -t cpr-smoke-${{ matrix.engine }} . - name: Save image run: docker save cpr-smoke-${{ matrix.engine }} -o /tmp/cpr-smoke-${{ matrix.engine }}.tar - uses: actions/upload-artifact@v7 @@ -153,21 +156,31 @@ jobs: if: always() run: docker compose -f tests/smoke/trino-stack/docker-compose.yml down -v - # On tag push: build both engine images for linux/amd64 and linux/arm64 and - # push them to ghcr.io//continuo-python-runtime-:. The - # engine belongs in the image NAME, not the tag: Continuo's Helm chart pins an - # image as `:vX.Y.Z@sha256:`, so the tag must be the bare + # Runs on this same tag push, after the smoke jobs above pass. It builds both + # engine images for linux/amd64 and linux/arm64 installing the pinned + # versions FROM PyPI (WHEEL_SOURCE=pypi, the Dockerfile default), then + # pushes them to ghcr.io//continuo-python-runtime-:. The + # engine belongs in the image NAME, not the tag: Continuo's Helm chart pins + # an image as `:vX.Y.Z@sha256:`, so the tag must be the bare # version. Both platforms are required: domain repos build their own images # FROM these, and Continuo's e2e runs them in a kind cluster on aarch64 - # developer machines as well as on amd64 CI runners, so a single-arch publish - # is unusable off amd64. The smoke jobs above stay single-arch — they load a - # `docker save` tarball, which has no multi-platform equivalent. - # continuo-engine-contract and the two engine adapters are installed from the - # build context (see Dockerfile.postgres / Dockerfile.trino) — nothing pending - # here. + # developer machines as well as on amd64 CI runners, so a single-arch + # publish is unusable off amd64. The smoke jobs above stay single-arch — + # they load a `docker save` tarball, which has no multi-platform + # equivalent. + # + # publish-pypi.yml runs in parallel on this same tag push, not before this + # job: there is no cross-workflow `needs:`, so this job cannot wait on that + # workflow directly. Instead the wait-until-installable step below polls + # PyPI until the pinned versions this tag just published are actually + # installable, which is what orders this build after publish-pypi.yml + # completes without a cross-workflow dependency. + # -test tags publish to TestPyPI only (see publish-pypi.yml), so this job + # skips them: there is no real-PyPI installable version to build an image + # from for those. publish: - if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-test') needs: [smoke-postgres, smoke-trino] + if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-test') strategy: matrix: engine: [postgres, trino] @@ -185,6 +198,26 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # The pinned distributions declare Requires-Python >= 3.14, so the + # availability probe must resolve them under a 3.14 interpreter — the + # runner's default python is older and pip would reject them even after + # they are published, spinning out every retry. (The image build itself + # installs inside python:3.14-slim, so it is unaffected; this is only for + # the host-side probe below.) + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + - name: Wait until the pinned versions are installable from PyPI + run: | + set -eu + req="image-requirements-${{ matrix.engine }}.txt" + for i in $(seq 1 30); do + if python -m pip download --dest /tmp/probe -r "$req" >/tmp/probe.log 2>&1; then + echo "installable"; exit 0 + fi + echo "attempt $i: not yet on index; sleeping"; sleep 20 + done + echo "pinned versions never became installable"; cat /tmp/probe.log; exit 1 - name: Build and push ${{ matrix.engine }} image uses: docker/build-push-action@v7 with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 9ea43a1..0c753b1 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,18 +1,16 @@ name: publish-pypi -# Publishes both PyPI distributions this repo owns — continuo-python-runtime -# (the harness) and continuo-engine-contract (the port, result-block format, -# and shared guards) — via PyPI Trusted Publishing (OIDC, no stored token). -# -# The two engine adapters (continuo-python-runtime-postgres / -# continuo-python-runtime-trino) are deliberately NOT published here. Nothing -# installs them from an index: the engine images build them from the build -# context (see Dockerfile.postgres / Dockerfile.trino and images.yml), and the -# harness package does not depend on them — adapters are found at run time -# through the `continuo_engine.adapters` entry-point group, inside the image -# that installed exactly one. Publishing them would only add releases with no -# consumer. They remain uv workspace members and are still built, typed, and -# tested by ci.yml. +# Publishes all four PyPI distributions this repo owns — continuo-python-runtime +# (the harness), continuo-engine-contract (the port, result-block format, and +# shared guards), and the two engine adapters (continuo-postgres-adapter, +# continuo-trino-adapter) — via PyPI Trusted Publishing (OIDC, no stored +# token). They publish together on a single v* tag, each at its own +# pyproject version, so a tag that only bumps the runtime still carries an +# unchanged adapter version along for the ride; skip-existing (below) makes +# that a no-op rather than a duplicate-upload failure. A release-time guard +# (scripts/check_version_bumps.py) refuses a tag that changed a package's +# source without bumping its version, so skip-existing can never silently +# ship stale adapter code under an unchanged version number. # # Tag glob note: `v*` is the repository's single release pattern — the same tag # that images.yml builds and pushes both engine images from, so one tag ships @@ -20,9 +18,9 @@ name: publish-pypi # claims every tag beginning with "v"; no other pattern may be introduced. # Tag `v-test` publishes to TestPyPI; `v` publishes to real PyPI. # The GitHub environment name is what the PyPI "pending publisher" is -# registered against — both project names need one on each index. +# registered against — all four project names need one on each index. # -# Both distributions are built into a single `dist/` and uploaded in one +# All four distributions are built into a single `dist/` and uploaded in one # publish call, so there is no ordering constraint between them. The runtime # wheel declares continuo-engine-contract as a dependency and resolves it from # the index at install time ([tool.uv.sources] is dev-only and is not embedded @@ -44,17 +42,42 @@ jobs: contents: read # actions/checkout needs read access to the repo steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 - uses: astral-sh/setup-uv@v7 + - name: Refuse a tag that changed a package without bumping its version + run: python scripts/check_version_bumps.py - name: Test before publishing - # `-m "not image"` deselects the image smoke tests; images.yml runs - # those against a built engine image. + # Gate all four published projects, including both adapter suites — an + # adapter regression must block its own immutable PyPI upload, and + # images.yml runs in parallel so its smoke failure cannot. Each suite is + # a SEPARATE pytest invocation: every workspace member's tests/ is its + # own `tests` package (with __init__.py + conftest.py), so running two + # of them in one process collides on the `tests.conftest` module name + # (see the [tool.pytest.ini_options] note in pyproject.toml). `not image + # and not integration` deselects the tests needing a built engine image + # or a live warehouse (images.yml / the smoke jobs cover those). The + # step's default `set -e` fails the gate if any invocation fails. run: | uv sync --all-packages --all-groups - uv run pytest tests contract/tests -m "not image" -q + uv run pytest tests contract/tests -m "not image and not integration" -q + uv run pytest adapters/postgres/tests -m "not image and not integration" -q + uv run pytest adapters/trino/tests -m "not image and not integration" -q - name: Build the contract sdist + wheel run: uv build --package continuo-engine-contract -o dist - name: Build the runtime sdist + wheel run: uv build -o dist + - name: Build the postgres adapter sdist + wheel + run: uv build --package continuo-postgres-adapter -o dist + - name: Build the trino adapter sdist + wheel + run: uv build --package continuo-trino-adapter -o dist + # Adapters change rarely, so most v* tags carry an unchanged adapter + # version alongside a bumped runtime/contract version. Without + # skip-existing, re-uploading that unchanged version 400s and fails the + # whole publish. Trade-off: a changed package published without a + # version bump is then silently skipped too — the + # check_version_bumps.py guard above is what catches that case, before + # this job's build/publish steps ever run. - name: Publish to TestPyPI if: contains(github.ref_name, '-test') # Pinned to a commit SHA. release/v1 is a moving branch, and this job @@ -64,6 +87,7 @@ jobs: with: repository-url: https://test.pypi.org/legacy/ packages-dir: dist + skip-existing: true - name: Publish to PyPI if: ${{ !contains(github.ref_name, '-test') }} # Pinned to a commit SHA. release/v1 is a moving branch, and this job @@ -72,3 +96,4 @@ jobs: uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 with: packages-dir: dist + skip-existing: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fa000a..12171f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,8 +57,8 @@ uv run ruff check . uv run ruff check contract uv run mypy continuo_python_runtime uv run mypy contract/continuo_engine_contract -uv run --package continuo-python-runtime-postgres mypy adapters/postgres/continuo_python_runtime_postgres -uv run --package continuo-python-runtime-trino mypy adapters/trino/continuo_python_runtime_trino +uv run --package continuo-postgres-adapter mypy adapters/postgres/continuo_postgres_adapter +uv run --package continuo-trino-adapter mypy adapters/trino/continuo_trino_adapter uv run pytest --cov=continuo_python_runtime -m "not image and not integration" -v uv run pytest tests/test_csv_readers_integration.py tests/test_validation_runner.py -m integration -v uv run pytest contract/tests -v diff --git a/Dockerfile.postgres b/Dockerfile.postgres index 68b64d2..850f7ed 100644 --- a/Dockerfile.postgres +++ b/Dockerfile.postgres @@ -1,16 +1,50 @@ FROM python:3.14-slim -# One dual-role image per engine: the python-node runtime harness (default -# command `run`, domain repos build FROM this) and the blue/green validation -# runner (`validation-op`, continuo's executor sets it as the container -# command). Everything installs from this repo's own workspace paths in one -# pip transaction — no PyPI propagation lag; images always build from repo head. -# docker build -f Dockerfile.postgres . -COPY pyproject.toml README.md /src/ -COPY contract /src/contract -COPY continuo_python_runtime /src/continuo_python_runtime -COPY adapters/postgres /src/adapters/postgres -RUN pip install --no-cache-dir /src/contract /src/adapters/postgres /src \ - && rm -rf /src +# One dual-role image per engine — the python-node runtime harness (default +# command `run`) and the blue/green validation runner (`validation-op`). It +# installs the PUBLISHED, versioned libraries, never the repo source: +# WHEEL_SOURCE=pypi (release) installs the pins from PyPI. PyPI versions +# are immutable and the image is consumed +# digest-pinned, so no hash lock is needed here. +# WHEEL_SOURCE=wheelhouse (CI/PR) installs the same pins from ./wheelhouse +# (built by CI) so an unreleased change is testable. +# The wheelhouse holds only this repo's own first-party wheels (built via +# `uv build --all-packages`); third-party dependencies always resolve from +# PyPI in both branches, so the wheelhouse branch never passes --no-index to +# the requirements install below. +# The pinned versions live in image-requirements-postgres.txt, kept equal to the +# repo's pyproject versions by tests/test_image_requirements_sync.py. +# +# The wheelhouse branch installs its three first-party wheels (contract, +# runtime, adapter) by exact local file first, with --no-index --no-deps: a +# plain `--find-links=/tmp/wheelhouse -r req.txt` is not enough, because pip +# does not prefer a find-links wheel over an index match at the same +# name+version, and continuo-python-runtime / continuo-engine-contract are +# already published on PyPI under the version this repo's HEAD currently +# carries (a routine dependency bump lands between releases without a version +# bump). Left unpinned-by-file, pip silently installs the last PyPI release's +# transitive pins instead of the wheel this build just produced, defeating +# the point of testing an unreleased change. Installing the exact local files +# first (no index contacted at all for this step) is unambiguous; the second +# install then only has PyPI-only third-party deps left to resolve, and finds +# the three first-party packages already satisfied. +ARG WHEEL_SOURCE=pypi +COPY image-requirements-postgres.txt /tmp/req.txt +# Optional COPY: the bracket glob matches ./wheelhouse when it exists (the CI/PR +# WHEEL_SOURCE=wheelhouse path builds it) and no-ops when it is absent, so a +# release build or a plain `docker build -f Dockerfile.postgres .` needs no +# pre-created directory. Only the wheelhouse branch below reads its contents. +COPY wheelhous[e] /tmp/wheelhouse +RUN set -eu; \ + if [ "$WHEEL_SOURCE" = "wheelhouse" ]; then \ + pip install --no-cache-dir --no-index --no-deps \ + /tmp/wheelhouse/continuo_engine_contract-*.whl \ + /tmp/wheelhouse/continuo_python_runtime-*.whl \ + /tmp/wheelhouse/continuo_postgres_adapter-*.whl; \ + pip install --no-cache-dir -r /tmp/req.txt; \ + else \ + pip install --no-cache-dir -r /tmp/req.txt; \ + fi; \ + rm -rf /tmp/req.txt /tmp/wheelhouse # PYTHONPATH is belt-and-braces: the harness also inserts APP_ROOT and the # node script's own directory at the front of sys.path before executing it. ENV CONTRACT_DIR=/app/contracts APP_ROOT=/app PYTHONPATH=/app diff --git a/Dockerfile.trino b/Dockerfile.trino index 0102259..1d90623 100644 --- a/Dockerfile.trino +++ b/Dockerfile.trino @@ -1,16 +1,50 @@ FROM python:3.14-slim -# One dual-role image per engine: the python-node runtime harness (default -# command `run`, domain repos build FROM this) and the blue/green validation -# runner (`validation-op`, continuo's executor sets it as the container -# command). Everything installs from this repo's own workspace paths in one -# pip transaction — no PyPI propagation lag; images always build from repo head. -# docker build -f Dockerfile.trino . -COPY pyproject.toml README.md /src/ -COPY contract /src/contract -COPY continuo_python_runtime /src/continuo_python_runtime -COPY adapters/trino /src/adapters/trino -RUN pip install --no-cache-dir /src/contract /src/adapters/trino /src \ - && rm -rf /src +# One dual-role image per engine — the python-node runtime harness (default +# command `run`) and the blue/green validation runner (`validation-op`). It +# installs the PUBLISHED, versioned libraries, never the repo source: +# WHEEL_SOURCE=pypi (release) installs the pins from PyPI. PyPI versions +# are immutable and the image is consumed +# digest-pinned, so no hash lock is needed here. +# WHEEL_SOURCE=wheelhouse (CI/PR) installs the same pins from ./wheelhouse +# (built by CI) so an unreleased change is testable. +# The wheelhouse holds only this repo's own first-party wheels (built via +# `uv build --all-packages`); third-party dependencies always resolve from +# PyPI in both branches, so the wheelhouse branch never passes --no-index to +# the requirements install below. +# The pinned versions live in image-requirements-trino.txt, kept equal to the +# repo's pyproject versions by tests/test_image_requirements_sync.py. +# +# The wheelhouse branch installs its three first-party wheels (contract, +# runtime, adapter) by exact local file first, with --no-index --no-deps: a +# plain `--find-links=/tmp/wheelhouse -r req.txt` is not enough, because pip +# does not prefer a find-links wheel over an index match at the same +# name+version, and continuo-python-runtime / continuo-engine-contract are +# already published on PyPI under the version this repo's HEAD currently +# carries (a routine dependency bump lands between releases without a version +# bump). Left unpinned-by-file, pip silently installs the last PyPI release's +# transitive pins instead of the wheel this build just produced, defeating +# the point of testing an unreleased change. Installing the exact local files +# first (no index contacted at all for this step) is unambiguous; the second +# install then only has PyPI-only third-party deps left to resolve, and finds +# the three first-party packages already satisfied. +ARG WHEEL_SOURCE=pypi +COPY image-requirements-trino.txt /tmp/req.txt +# Optional COPY: the bracket glob matches ./wheelhouse when it exists (the CI/PR +# WHEEL_SOURCE=wheelhouse path builds it) and no-ops when it is absent, so a +# release build or a plain `docker build -f Dockerfile.trino .` needs no +# pre-created directory. Only the wheelhouse branch below reads its contents. +COPY wheelhous[e] /tmp/wheelhouse +RUN set -eu; \ + if [ "$WHEEL_SOURCE" = "wheelhouse" ]; then \ + pip install --no-cache-dir --no-index --no-deps \ + /tmp/wheelhouse/continuo_engine_contract-*.whl \ + /tmp/wheelhouse/continuo_python_runtime-*.whl \ + /tmp/wheelhouse/continuo_trino_adapter-*.whl; \ + pip install --no-cache-dir -r /tmp/req.txt; \ + else \ + pip install --no-cache-dir -r /tmp/req.txt; \ + fi; \ + rm -rf /tmp/req.txt /tmp/wheelhouse # PYTHONPATH is belt-and-braces: the harness also inserts APP_ROOT and the # node script's own directory at the front of sys.path before executing it. ENV CONTRACT_DIR=/app/contracts APP_ROOT=/app PYTHONPATH=/app diff --git a/README.md b/README.md index 68e3698..a198c55 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ and register the release with Continuo. ## What this repo is -Four artifacts come out of this repository: +Five artifacts come out of this repository: - **The `continuo-python-runtime` PyPI package** — the `continuo-runtime` CLI (`validate` / `merge` / `hash` / `lint` / `run` / `validation-op`) and the @@ -21,6 +21,13 @@ Four artifacts come out of this repository: - **The `continuo-engine-contract` PyPI package** — the `WarehouseAdapter` port, the contract schema, the shared SQL/type/config guards, and the sentinel result-block format. Adapter authors outside this repo pin it. +- **The two engine-adapter PyPI packages** (`continuo-postgres-adapter`, + `continuo-trino-adapter`) — one `WarehouseAdapter` implementation per + warehouse engine, each published independently under the same tag. A + domain repo normally never installs these directly (the engine image + already has the matching one baked in); they exist as standalone PyPI + packages for the "build your own container" shape (see below) and for + third-party adapter authors to reference. - **Per-engine base images**, one per warehouse engine (`continuo-python-runtime-postgres`, `continuo-python-runtime-trino`), that domain repos build `FROM`. Each image bakes in the runtime and a single @@ -30,10 +37,11 @@ Four artifacts come out of this repository: - **`template/`** — a copy-ready domain repo: `Dockerfile`, `contracts/`, `scripts/`, and the `release.yml` CI/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. +One `vX.Y.Z` git tag releases all of it: `publish-pypi.yml` builds all four +PyPI distributions into a single `dist/` and publishes them together, and +`images.yml` builds and pushes both engine images — each installing its +matching pinned adapter version from that same release — multi-arch under +the same tag. ### What this repo owns @@ -50,22 +58,27 @@ validation-side port, adapter class, entry-point group, or image. One | --- | --- | --- | --- | | `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. | +| `continuo-postgres-adapter` | `continuo_postgres_adapter` | this repo, `adapters/postgres/` | `PostgresAdapter` — one class, both roles. Published to PyPI. | +| `continuo-trino-adapter` | `continuo_trino_adapter` | this repo, `adapters/trino/` | `TrinoAdapter` — one class, both roles, for Trino/Iceberg. Published to PyPI. | 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. +**All four packages in the table above are published to PyPI**, under the +same `vX.Y.Z` tag. The two engine images then **install the matching pinned +adapter version from PyPI** — `Dockerfile.postgres` installs +`continuo-postgres-adapter==X.Y.Z`, `Dockerfile.trino` installs +`continuo-trino-adapter==X.Y.Z` — rather than building it from this repo's +source tree, so each image still ships exactly one adapter and the runtime +still discovers it through the `continuo_engine.adapters` entry-point group +at run time. The image **name** (`continuo-python-runtime-`) and the +adapter's pip **distribution** name (`continuo--adapter`) are two +different artifacts of the same adapter — same engine, same version, same +runtime behavior, different packaging; see "Build your own container" below +for a build shape that installs the pip package directly instead of `FROM` +the image. All four packages are still built, type-checked, and tested by CI +on every change. ### The result block is a frozen wire contract @@ -250,18 +263,38 @@ FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.4.0 The engine is part of the image **name**; the tag is the bare version, so Continuo's Helm chart can pin an image as `:vX.Y.Z@sha256:`. -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. +Each image bakes in exactly one `WarehouseAdapter` for that engine — the +pinned PyPI version of the `continuo-postgres-adapter` or +`continuo-trino-adapter` package built from this repo's `adapters/postgres/` +or `adapters/trino/` source (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. +### Build your own container + +A domain repo does not have to build `FROM` the published engine image. +`template/` ships two Dockerfiles for the two build shapes (see +`template/README.md` § "Choosing a base" for the full comparison): + +- **Shape 1 — `template/Dockerfile`** — `FROM` the published engine image + (`continuo-python-runtime-`), as shown above. Simplest; the image + already has the runtime and adapter installed and pinned. +- **Shape 2 — `template/Dockerfile.pip`** — your own base image, installing + `continuo-python-runtime` and one `continuo--adapter` from PyPI + via a hash-locked `requirements.lock` (`template/requirements.lock`). Use + this when you must control the base image yourself. + +Both shapes end up running the same runtime and the same adapter version; +which one you pick only changes who controls the base OS layer underneath +them. + ## Further reading - `docs/superpowers/specs/2026-07-31-python-runtime-design.md` — this diff --git a/adapters/postgres/README.md b/adapters/postgres/README.md index 673019b..dbfc11d 100644 --- a/adapters/postgres/README.md +++ b/adapters/postgres/README.md @@ -1,4 +1,4 @@ -# continuo-python-runtime-postgres +# continuo-postgres-adapter Postgres engine-adapter library for Continuo python nodes. `PostgresAdapter` implements `continuo_engine_contract.port.WarehouseAdapter` (from the diff --git a/adapters/postgres/continuo_python_runtime_postgres/__init__.py b/adapters/postgres/continuo_postgres_adapter/__init__.py similarity index 100% rename from adapters/postgres/continuo_python_runtime_postgres/__init__.py rename to adapters/postgres/continuo_postgres_adapter/__init__.py diff --git a/adapters/postgres/continuo_python_runtime_postgres/adapter.py b/adapters/postgres/continuo_postgres_adapter/adapter.py similarity index 99% rename from adapters/postgres/continuo_python_runtime_postgres/adapter.py rename to adapters/postgres/continuo_postgres_adapter/adapter.py index a595539..6e27e55 100644 --- a/adapters/postgres/continuo_python_runtime_postgres/adapter.py +++ b/adapters/postgres/continuo_postgres_adapter/adapter.py @@ -39,7 +39,7 @@ from psycopg2 import sql as pg_sql # type: ignore[import-untyped] from psycopg2.extras import execute_values # type: ignore[import-untyped] -logger = logging.getLogger("continuo_python_runtime_postgres") +logger = logging.getLogger("continuo_postgres_adapter") # The postgres physical-layout vocabulary, mirroring dbt-postgres's own `indexes` # config so the graph reads a python node's layout the way it reads a dbt model's. diff --git a/adapters/postgres/pyproject.toml b/adapters/postgres/pyproject.toml index b0c8a5d..480ce57 100644 --- a/adapters/postgres/pyproject.toml +++ b/adapters/postgres/pyproject.toml @@ -1,6 +1,6 @@ [project] version = "0.2.0" -name = "continuo-python-runtime-postgres" +name = "continuo-postgres-adapter" description = "Postgres warehouse adapter for Continuo: validation and python-node runtime." authors = [{ name = "Simone Carolini" }] maintainers = [{ name = "Simone Carolini" }] @@ -23,7 +23,7 @@ classifiers = [ ] [project.entry-points."continuo_engine.adapters"] -postgres = "continuo_python_runtime_postgres.adapter:PostgresAdapter" +postgres = "continuo_postgres_adapter.adapter:PostgresAdapter" [dependency-groups] dev = [ @@ -40,7 +40,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["continuo_python_runtime_postgres"] +packages = ["continuo_postgres_adapter"] [tool.ruff] target-version = "py312" diff --git a/adapters/postgres/tests/test_adapter_postgres_validation.py b/adapters/postgres/tests/test_adapter_postgres_validation.py index dd2a804..828f415 100644 --- a/adapters/postgres/tests/test_adapter_postgres_validation.py +++ b/adapters/postgres/tests/test_adapter_postgres_validation.py @@ -7,7 +7,7 @@ """ import pytest -from continuo_python_runtime_postgres.adapter import PostgresAdapter, _index_name, _validated_indexes +from continuo_postgres_adapter.adapter import PostgresAdapter, _index_name, _validated_indexes def test_required_env_names_connection_vars(): diff --git a/adapters/postgres/tests/test_adapter_runtime_postgres.py b/adapters/postgres/tests/test_adapter_runtime_postgres.py index fa5e883..7cf72a9 100644 --- a/adapters/postgres/tests/test_adapter_runtime_postgres.py +++ b/adapters/postgres/tests/test_adapter_runtime_postgres.py @@ -25,9 +25,9 @@ import pytest from continuo_engine_contract.types import validate_column_type # type: ignore[import-untyped] -import continuo_python_runtime_postgres.adapter as adapter_module +import continuo_postgres_adapter.adapter as adapter_module -from continuo_python_runtime_postgres.adapter import ( +from continuo_postgres_adapter.adapter import ( PostgresAdapter, _arrow_table_from_rows, _index_name, diff --git a/adapters/postgres/tests/test_integration_postgres_validation.py b/adapters/postgres/tests/test_integration_postgres_validation.py index a0affb9..054ace2 100644 --- a/adapters/postgres/tests/test_integration_postgres_validation.py +++ b/adapters/postgres/tests/test_integration_postgres_validation.py @@ -6,7 +6,7 @@ import psycopg2 import pytest -from continuo_python_runtime_postgres.adapter import PostgresAdapter +from continuo_postgres_adapter.adapter import PostgresAdapter PG = dict( host="localhost", diff --git a/adapters/postgres/tests/test_integration_runtime_postgres.py b/adapters/postgres/tests/test_integration_runtime_postgres.py index 26fd03e..1557456 100644 --- a/adapters/postgres/tests/test_integration_runtime_postgres.py +++ b/adapters/postgres/tests/test_integration_runtime_postgres.py @@ -14,7 +14,7 @@ import yaml from continuo_python_runtime.harness import run_node -from continuo_python_runtime_postgres.adapter import PostgresAdapter +from continuo_postgres_adapter.adapter import PostgresAdapter PG = dict( host="localhost", diff --git a/adapters/trino/README.md b/adapters/trino/README.md index eccdb76..2d5d8a3 100644 --- a/adapters/trino/README.md +++ b/adapters/trino/README.md @@ -1,4 +1,4 @@ -# continuo-python-runtime-trino +# continuo-trino-adapter Trino (Iceberg connector) engine-adapter library for Continuo python nodes. `TrinoAdapter` implements `continuo_engine_contract.port.WarehouseAdapter` @@ -24,7 +24,7 @@ the adapter module docstring. Unlike the postgres runtime adapter, Trino/Iceberg has no multi-statement transactions, so `load()` cannot be a single atomic TRUNCATE+INSERT. Two atomic-replace primitives were verified live (Trino 483 + Iceberg REST catalog) -before choosing one — see the full writeup in `continuo_python_runtime_trino/adapter.py`'s +before choosing one — see the full writeup in `continuo_trino_adapter/adapter.py`'s module docstring: - `CREATE OR REPLACE TABLE t AS SELECT * FROM stage` is a single Iceberg diff --git a/adapters/trino/continuo_python_runtime_trino/__init__.py b/adapters/trino/continuo_trino_adapter/__init__.py similarity index 100% rename from adapters/trino/continuo_python_runtime_trino/__init__.py rename to adapters/trino/continuo_trino_adapter/__init__.py diff --git a/adapters/trino/continuo_python_runtime_trino/adapter.py b/adapters/trino/continuo_trino_adapter/adapter.py similarity index 99% rename from adapters/trino/continuo_python_runtime_trino/adapter.py rename to adapters/trino/continuo_trino_adapter/adapter.py index 029f749..ca983d6 100644 --- a/adapters/trino/continuo_python_runtime_trino/adapter.py +++ b/adapters/trino/continuo_trino_adapter/adapter.py @@ -79,7 +79,7 @@ from trino.auth import BasicAuthentication -logger = logging.getLogger("continuo_python_runtime_trino") +logger = logging.getLogger("continuo_trino_adapter") # Grammar spellings that are not valid Trino type names, mapped to the Trino # spelling with equivalent semantics. Matching is case-insensitive; lookup keys diff --git a/adapters/trino/pyproject.toml b/adapters/trino/pyproject.toml index c40c8b7..9d2ba07 100644 --- a/adapters/trino/pyproject.toml +++ b/adapters/trino/pyproject.toml @@ -1,6 +1,6 @@ [project] version = "0.2.0" -name = "continuo-python-runtime-trino" +name = "continuo-trino-adapter" description = "Trino warehouse adapter for Continuo: validation and python-node runtime." authors = [{ name = "Simone Carolini" }] maintainers = [{ name = "Simone Carolini" }] @@ -23,7 +23,7 @@ classifiers = [ ] [project.entry-points."continuo_engine.adapters"] -trino = "continuo_python_runtime_trino.adapter:TrinoAdapter" +trino = "continuo_trino_adapter.adapter:TrinoAdapter" [dependency-groups] dev = [ @@ -40,7 +40,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["continuo_python_runtime_trino"] +packages = ["continuo_trino_adapter"] [tool.ruff] target-version = "py312" diff --git a/adapters/trino/tests/test_adapter_runtime_trino.py b/adapters/trino/tests/test_adapter_runtime_trino.py index 3c6256b..b5d2bf7 100644 --- a/adapters/trino/tests/test_adapter_runtime_trino.py +++ b/adapters/trino/tests/test_adapter_runtime_trino.py @@ -21,9 +21,9 @@ import pytest from continuo_engine_contract.types import validate_column_type # type: ignore[import-untyped] -import continuo_python_runtime_trino.adapter as adapter_module +import continuo_trino_adapter.adapter as adapter_module -from continuo_python_runtime_trino.adapter import ( +from continuo_trino_adapter.adapter import ( TrinoAdapter, _arrow_table_from_rows, _quote, diff --git a/adapters/trino/tests/test_adapter_trino_validation.py b/adapters/trino/tests/test_adapter_trino_validation.py index 65db8df..0302835 100644 --- a/adapters/trino/tests/test_adapter_trino_validation.py +++ b/adapters/trino/tests/test_adapter_trino_validation.py @@ -6,7 +6,7 @@ """ import pytest -from continuo_python_runtime_trino.adapter import ( +from continuo_trino_adapter.adapter import ( TrinoAdapter, _quote, _sql_string, diff --git a/adapters/trino/tests/test_integration_runtime_trino.py b/adapters/trino/tests/test_integration_runtime_trino.py index 2fa7dc0..1473994 100644 --- a/adapters/trino/tests/test_integration_runtime_trino.py +++ b/adapters/trino/tests/test_integration_runtime_trino.py @@ -21,7 +21,7 @@ import trino -from continuo_python_runtime_trino.adapter import TrinoAdapter +from continuo_trino_adapter.adapter import TrinoAdapter TRINO_ENV = { "TRINO_HOST": "localhost", @@ -359,7 +359,7 @@ def test_entry_point_resolves_to_this_adapter(): """The `trino` engine entry point is registered and loads TrinoAdapter. Does NOT call discover_adapter(): see the module docstring — with - continuo-python-runtime-postgres also installed in this dev venv, two entry points + continuo-postgres-adapter also installed in this dev venv, two entry points are registered under continuo_engine.adapters, and discover_adapter() deliberately raises when more than one is installed. Only a runner image (which installs exactly one engine package) can rely on discovery choosing diff --git a/adapters/trino/tests/test_integration_trino_validation.py b/adapters/trino/tests/test_integration_trino_validation.py index 9f39727..a594cb7 100644 --- a/adapters/trino/tests/test_integration_trino_validation.py +++ b/adapters/trino/tests/test_integration_trino_validation.py @@ -10,7 +10,7 @@ import pytest -from continuo_python_runtime_trino.adapter import TrinoAdapter +from continuo_trino_adapter.adapter import TrinoAdapter TRINO_ENV = { "TRINO_HOST": "localhost", diff --git a/image-requirements-postgres.txt b/image-requirements-postgres.txt new file mode 100644 index 0000000..41cd509 --- /dev/null +++ b/image-requirements-postgres.txt @@ -0,0 +1,2 @@ +continuo-python-runtime==0.4.0 +continuo-postgres-adapter==0.2.0 diff --git a/image-requirements-trino.txt b/image-requirements-trino.txt new file mode 100644 index 0000000..24f84db --- /dev/null +++ b/image-requirements-trino.txt @@ -0,0 +1,2 @@ +continuo-python-runtime==0.4.0 +continuo-trino-adapter==0.2.0 diff --git a/scripts/check_version_bumps.py b/scripts/check_version_bumps.py new file mode 100644 index 0000000..ee73b18 --- /dev/null +++ b/scripts/check_version_bumps.py @@ -0,0 +1,65 @@ +"""Fail if a package's source changed since the previous release tag but its +version was not bumped. skip-existing then safely carries the unchanged +packages. First release (no previous v* tag) is a no-op. + +A package is only an offender when its dist NAME is also unchanged since the +previous tag: if the name changed (e.g. a rename to a new PyPI project), the +same version number is a first publish under that name, not a stale +re-publish, so no bump is required. +""" +import subprocess +import sys +import tomllib + +# dist name -> (pyproject path, paths whose change requires a version bump) +PACKAGES = { + "continuo-python-runtime": ("pyproject.toml", ["pyproject.toml", "continuo_python_runtime"]), + "continuo-engine-contract": ("contract/pyproject.toml", ["contract"]), + "continuo-postgres-adapter": ("adapters/postgres/pyproject.toml", ["adapters/postgres"]), + "continuo-trino-adapter": ("adapters/trino/pyproject.toml", ["adapters/trino"]), +} + + +def _run(*args): + return subprocess.run(args, capture_output=True, text=True) + + +def _prev_tag(): + r = _run("git", "describe", "--tags", "--match", "v*", "--abbrev=0", "HEAD^") + return r.stdout.strip() if r.returncode == 0 else "" + + +def _name_version(ref, path): + if ref: + r = _run("git", "show", f"{ref}:{path}") + if r.returncode != 0: + return None # package did not exist at prev tag -> treat as new + data = tomllib.loads(r.stdout) + else: + with open(path, "rb") as fh: + data = tomllib.load(fh) + return data["project"]["name"], data["project"]["version"] + + +def main() -> int: + prev = _prev_tag() + if not prev: + print("no previous v* tag; first release, nothing to check") + return 0 + offenders = [] + for dist, (pyproj, paths) in PACKAGES.items(): + changed = _run("git", "diff", "--quiet", f"{prev}..HEAD", "--", *paths).returncode != 0 + old = _name_version(prev, pyproj) + new = _name_version("", pyproj) + if changed and old is not None and old[0] == new[0] and old[1] == new[1]: + offenders.append(f"{dist} ({', '.join(paths)} changed since {prev} but version stayed {new[1]})") + if offenders: + print("Bump the version of changed packages before tagging:\n " + "\n ".join(offenders), + file=sys.stderr) + return 1 + print(f"All packages changed since {prev} were version-bumped.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/template/Dockerfile.pip b/template/Dockerfile.pip new file mode 100644 index 0000000..adc249e --- /dev/null +++ b/template/Dockerfile.pip @@ -0,0 +1,23 @@ +# Shape 2 — build a python-node image on YOUR OWN base, installing the Continuo +# runtime + one engine adapter from PyPI. Deterministic via a hash-locked +# requirements file (regenerate with the command in template/README.md). +# Shape 1 (FROM the engine image, see template/Dockerfile) is simpler; use this +# only when you must control the base image. +FROM python:3.14-slim +COPY requirements.lock /tmp/requirements.lock +RUN pip install --no-cache-dir --require-hashes -r /tmp/requirements.lock \ + && rm -f /tmp/requirements.lock +ENV CONTRACT_DIR=/app/contracts APP_ROOT=/app PYTHONPATH=/app +WORKDIR /app +COPY contracts/ /app/contracts/ +COPY scripts/ /app/scripts/ +# COPY every OTHER directory your scripts import from, too. CI folds a script's +# transitive in-repo import closure into shared_code_hash, so a helper module in +# e.g. lib/ or common/ is hashed whether or not it is in the image — a helper +# left out here produces a valid release artifact and a node that dies with +# ModuleNotFoundError on its first run. Add one line per such directory, e.g.: +# COPY lib/ /app/lib/ +RUN useradd --uid 65532 --no-create-home --shell /usr/sbin/nologin nonroot +USER 65532:65532 +ENTRYPOINT ["continuo-runtime"] +CMD ["run"] diff --git a/template/README.md b/template/README.md index 2e03e3a..1151b9e 100644 --- a/template/README.md +++ b/template/README.md @@ -20,6 +20,52 @@ This is a copy-ready template for implementing a [Continuo Python domain repo](h 5. **Write your contracts** in `contracts/` and **implement scripts** in `scripts/` 6. **Push to main** to trigger the release pipeline +## Choosing a base + +This template ships two Dockerfiles that produce the same kind of image +(`ENTRYPOINT ["continuo-runtime"]`, `CMD ["run"]`, contracts + scripts baked +in, running as uid 65532) via two different build shapes. Pick one; you only +need one Dockerfile in your repo. + +**Shape 1 — `Dockerfile`, `FROM` the engine image (simplest).** Builds +`FROM ghcr.io/carolsimone/continuo-python-runtime-:vX.Y.Z`, an image +that already has the Continuo runtime and one engine adapter installed and +pinned by the publisher. You only add your `contracts/` and `scripts/` (and +any extra dependency your script needs). Pin by tag or digest +(`:vX.Y.Z@sha256:`) for reproducibility. Use this unless you have a +specific reason not to. + +**Shape 2 — `Dockerfile.pip`, your own base (hash-locked).** Builds +`FROM python:3.14-slim` (or another base you control) and installs +`continuo-python-runtime` plus one `continuo--adapter` from PyPI via +`pip install --require-hashes -r requirements.lock`. Use this when you must +control the base image yourself — e.g. your org mandates a specific base, +you need OS packages the engine image doesn't carry, or you're building on a +platform the published engine images don't target. `requirements.lock` is +the one place in this template where `--require-hashes` and a committed +hash-lock are used (the engine images themselves pin by version only); this +is what makes the Shape-2 build deterministic without depending on the +publisher's image layers. + +Regenerate `requirements.lock` for your engine and versions with: + +```bash +uv pip compile --generate-hashes --python-version 3.14 - -o requirements.lock <<'EOF' +continuo-python-runtime==0.4.0 +continuo--adapter==X.Y.Z +EOF +``` + +The committed `requirements.lock` in this template is a **placeholder**: the +adapters were not yet published to PyPI when it was written, so it has no +`--hash` entries and `pip install --require-hashes` will refuse to install it +as-is. Regenerate it once your chosen adapter version is actually on PyPI. + +The image name (`continuo-python-runtime-`, what Shape 1 pulls) and +the pip distribution name (`continuo--adapter`, what Shape 2 +installs) are two artifacts published from the same adapter source — same +engine, same version, same runtime behavior, different packaging. + ## Pipeline Overview The CI/CD pipeline (`release.yml`) performs the six-step orchestration: diff --git a/template/requirements.lock b/template/requirements.lock new file mode 100644 index 0000000..9dc015c --- /dev/null +++ b/template/requirements.lock @@ -0,0 +1,22 @@ +# PLACEHOLDER — regenerate with --generate-hashes once the versions are on PyPI. +# +# This file is what template/Dockerfile.pip (Shape 2) installs with +# `pip install --require-hashes`, which refuses to install any requirement +# that lacks a --hash entry. The two lines below are illustrative pins only +# (postgres is the engine used throughout this template, matching Shape 1's +# Dockerfile) — they carry no hashes yet, so installing from this file AS-IS +# WILL FAIL. Do not ship it unmodified. +# +# Once continuo-python-runtime==0.4.0 and continuo-postgres-adapter==0.2.0 (or +# whichever versions you depend on) are published on PyPI, regenerate a real +# hash-locked file for your engine and versions with: +# +# uv pip compile --generate-hashes --python-version 3.14 - -o template/requirements.lock <<'EOF' +# continuo-python-runtime==0.4.0 +# continuo--adapter==X.Y.Z +# EOF +# +# Swap /X.Y.Z for the adapter you actually use (e.g. trino==0.2.0), +# then commit the regenerated file with its --hash entries. +continuo-python-runtime==0.4.0 +continuo-postgres-adapter==0.2.0 diff --git a/tests/test_adapter_naming.py b/tests/test_adapter_naming.py new file mode 100644 index 0000000..5a11b2c --- /dev/null +++ b/tests/test_adapter_naming.py @@ -0,0 +1,41 @@ +"""Guard: for every installed engine adapter, the distribution name, the import +module, and the entry-point target agree — so `pip install continuo--adapter` +always yields `import continuo__adapter`. Regressing this reintroduces the +pip-name != import-name trap and breaks the deterministic BYO template. +""" +import importlib +import importlib.metadata as md + +import pytest + +from continuo_engine_contract.port import ENTRY_POINT_GROUP + + +def _entry_points(): + return list(md.entry_points(group=ENTRY_POINT_GROUP)) + + +def test_at_least_the_two_known_engines_are_installed(): + names = {ep.name for ep in _entry_points()} + assert {"postgres", "trino"} <= names, f"missing engines; found {sorted(names)}" + + +@pytest.mark.parametrize("ep", _entry_points(), ids=lambda ep: ep.name) +def test_adapter_names_follow_the_convention(ep): + engine = ep.name + expected_dist = f"continuo-{engine}-adapter" + expected_module = f"continuo_{engine}_adapter" + # entry-point target lives in the expected top-level import package (the + # target itself is a submodule, e.g. "continuo_postgres_adapter.adapter") + top_level = ep.module.split(".")[0] + assert top_level == expected_module, ( + f"entry point {engine!r} targets {ep.value!r}, expected top-level module " + f"{expected_module!r}" + ) + # the target module imports + importlib.import_module(expected_module) + # the distribution providing it carries the expected pip name + assert ep.dist is not None and ep.dist.name == expected_dist, ( + f"entry point {engine!r} is provided by {getattr(ep.dist, 'name', None)!r}, " + f"expected {expected_dist!r}" + ) diff --git a/tests/test_image_requirements_sync.py b/tests/test_image_requirements_sync.py new file mode 100644 index 0000000..6a57203 --- /dev/null +++ b/tests/test_image_requirements_sync.py @@ -0,0 +1,36 @@ +"""The engine image installs pinned versions from image-requirements-.txt. +Guard that those pins equal the repo's own pyproject versions, so the image can +never ship a version different from what this tag publishes. +""" +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def _v(rel: str) -> str: + with open(ROOT / rel, "rb") as fh: + return tomllib.load(fh)["project"]["version"] + + +def _pins(rel: str) -> dict[str, str]: + pins = {} + for line in (ROOT / rel).read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + name, _, ver = line.partition("==") + pins[name.strip()] = ver.strip() + return pins + + +def test_postgres_image_requirements_match_pyproject(): + pins = _pins("image-requirements-postgres.txt") + assert pins["continuo-python-runtime"] == _v("pyproject.toml") + assert pins["continuo-postgres-adapter"] == _v("adapters/postgres/pyproject.toml") + + +def test_trino_image_requirements_match_pyproject(): + pins = _pins("image-requirements-trino.txt") + assert pins["continuo-python-runtime"] == _v("pyproject.toml") + assert pins["continuo-trino-adapter"] == _v("adapters/trino/pyproject.toml") diff --git a/uv.lock b/uv.lock index f636cbd..6703041 100644 --- a/uv.lock +++ b/uv.lock @@ -9,9 +9,9 @@ resolution-markers = [ [manifest] members = [ "continuo-engine-contract", + "continuo-postgres-adapter", "continuo-python-runtime", - "continuo-python-runtime-postgres", - "continuo-python-runtime-trino", + "continuo-trino-adapter", ] [[package]] @@ -171,22 +171,19 @@ dependencies = [ requires-dist = [{ name = "sqlglot", specifier = "==30.17.0" }] [[package]] -name = "continuo-python-runtime" -version = "0.4.0" -source = { editable = "." } +name = "continuo-postgres-adapter" +version = "0.2.0" +source = { editable = "adapters/postgres" } dependencies = [ - { name = "boto3" }, { name = "continuo-engine-contract" }, + { name = "psycopg2-binary" }, { name = "pyarrow" }, - { name = "pyyaml" }, - { name = "sqlglot" }, ] [package.dev-dependencies] dev = [ { name = "mypy" }, { name = "ruff" }, - { name = "types-pyyaml" }, ] test = [ { name = "pytest" }, @@ -195,18 +192,15 @@ test = [ [package.metadata] requires-dist = [ - { name = "boto3", specifier = "==1.43.74" }, { name = "continuo-engine-contract", editable = "contract" }, + { name = "psycopg2-binary", specifier = "==2.9.12" }, { name = "pyarrow", specifier = "==25.0.1" }, - { name = "pyyaml", specifier = "==6.0.3" }, - { name = "sqlglot", specifier = "==30.17.0" }, ] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = "==2.3.1" }, { name = "ruff", specifier = "==0.16.3" }, - { name = "types-pyyaml", specifier = "==6.0.12.20250915" }, ] test = [ { name = "pytest", specifier = "==9.1.1" }, @@ -214,19 +208,22 @@ test = [ ] [[package]] -name = "continuo-python-runtime-postgres" -version = "0.2.0" -source = { editable = "adapters/postgres" } +name = "continuo-python-runtime" +version = "0.4.0" +source = { editable = "." } dependencies = [ + { name = "boto3" }, { name = "continuo-engine-contract" }, - { name = "psycopg2-binary" }, { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "sqlglot" }, ] [package.dev-dependencies] dev = [ { name = "mypy" }, { name = "ruff" }, + { name = "types-pyyaml" }, ] test = [ { name = "pytest" }, @@ -235,15 +232,18 @@ test = [ [package.metadata] requires-dist = [ + { name = "boto3", specifier = "==1.43.74" }, { name = "continuo-engine-contract", editable = "contract" }, - { name = "psycopg2-binary", specifier = "==2.9.12" }, { name = "pyarrow", specifier = "==25.0.1" }, + { name = "pyyaml", specifier = "==6.0.3" }, + { name = "sqlglot", specifier = "==30.17.0" }, ] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = "==2.3.1" }, { name = "ruff", specifier = "==0.16.3" }, + { name = "types-pyyaml", specifier = "==6.0.12.20250915" }, ] test = [ { name = "pytest", specifier = "==9.1.1" }, @@ -251,7 +251,7 @@ test = [ ] [[package]] -name = "continuo-python-runtime-trino" +name = "continuo-trino-adapter" version = "0.2.0" source = { editable = "adapters/trino" } dependencies = [