continuo-python-runtime 0.8.0__tar.gz → 0.9.0__tar.gz
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-0.9.0/.python-version +1 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/CHANGELOG.md +84 -0
- continuo_python_runtime-0.9.0/CLAUDE.md +67 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/Dockerfile.duckdb +1 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/Dockerfile.postgres +1 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/Dockerfile.trino +1 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/PKG-INFO +77 -13
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/README.md +75 -11
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/adapter.py +3 -3
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/application/ports.py +15 -3
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/application/warehouse.py +64 -7
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/domain/columns.py +1 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/domain/layout.py +2 -2
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/ddl.py +44 -4
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/extensions.py +2 -2
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/session.py +32 -8
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/pyproject.toml +2 -2
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/conftest.py +8 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_adapter_duckdb_runtime.py +63 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_infra_duckdb_session.py +54 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_infra_duckdb_settings_ddl.py +34 -0
- continuo_python_runtime-0.9.0/adapters/duckdb/tests/test_integration_duckdb_conformance.py +27 -0
- continuo_python_runtime-0.9.0/adapters/duckdb/tests/test_integration_duckdb_nullability_probe.py +35 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_integration_duckdb_runtime.py +78 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/continuo_postgres_adapter/adapter.py +180 -40
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/pyproject.toml +2 -2
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/tests/test_adapter_runtime_postgres.py +86 -0
- continuo_python_runtime-0.9.0/adapters/postgres/tests/test_integration_postgres_conformance.py +103 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/tests/test_integration_runtime_postgres.py +108 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/continuo_trino_adapter/adapter.py +183 -24
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/pyproject.toml +2 -2
- continuo_python_runtime-0.9.0/adapters/trino/tests/test_adapter_trino_writes.py +231 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/tests/test_integration_runtime_trino.py +86 -0
- continuo_python_runtime-0.9.0/adapters/trino/tests/test_integration_trino_conformance.py +77 -0
- continuo_python_runtime-0.9.0/adapters/trino/tests/test_integration_trino_merge_probe.py +98 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/contract/kinds.py +12 -5
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/contract/loader.py +82 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/contract/merge.py +11 -2
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/contract/model.py +7 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/harness.py +100 -4
- continuo_python_runtime-0.9.0/continuo_python_runtime/shape.py +95 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/README.md +10 -2
- continuo_python_runtime-0.9.0/contract/continuo_engine_contract/keys.py +87 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/continuo_engine_contract/port.py +98 -2
- continuo_python_runtime-0.9.0/contract/continuo_engine_contract/testing/__init__.py +4 -0
- continuo_python_runtime-0.9.0/contract/continuo_engine_contract/testing/conformance.py +306 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/pyproject.toml +7 -1
- continuo_python_runtime-0.9.0/contract/tests/test_conformance_suite.py +168 -0
- continuo_python_runtime-0.9.0/contract/tests/test_contract_keys.py +95 -0
- continuo_python_runtime-0.9.0/contract/tests/test_contract_packaging.py +26 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/tests/test_port.py +122 -2
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/docs/boundary-contract.md +45 -13
- continuo_python_runtime-0.9.0/image-requirements-duckdb.txt +3 -0
- continuo_python_runtime-0.9.0/image-requirements-postgres.txt +3 -0
- continuo_python_runtime-0.9.0/image-requirements-trino.txt +3 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/pyproject.toml +2 -2
- continuo_python_runtime-0.9.0/template/.github/workflows/release.yml +188 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/template/Dockerfile +1 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/template/Dockerfile.pip +1 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/template/README.md +30 -11
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/conftest.py +48 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/contract/test_kinds.py +16 -1
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/contract/test_loader.py +151 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/contract/test_merge.py +89 -2
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/contract/test_model.py +32 -1
- continuo_python_runtime-0.9.0/tests/contract/test_wire_contract_write_modes.py +53 -0
- continuo_python_runtime-0.9.0/tests/fixtures/wire_contract_write_modes.yml +53 -0
- continuo_python_runtime-0.9.0/tests/test_harness_write_modes.py +259 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_kind_registries.py +5 -1
- continuo_python_runtime-0.9.0/tests/test_python_version_consistency.py +85 -0
- continuo_python_runtime-0.9.0/tests/test_shape.py +60 -0
- continuo_python_runtime-0.9.0/tests/test_template.py +248 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/uv.lock +17 -6
- continuo_python_runtime-0.8.0/CLAUDE.md +0 -37
- continuo_python_runtime-0.8.0/image-requirements-duckdb.txt +0 -3
- continuo_python_runtime-0.8.0/image-requirements-postgres.txt +0 -3
- continuo_python_runtime-0.8.0/image-requirements-trino.txt +0 -3
- continuo_python_runtime-0.8.0/template/.github/workflows/release.yml +0 -66
- continuo_python_runtime-0.8.0/tests/test_template.py +0 -82
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.claude/agents/ddd-architecture-auditor.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.dockerignore +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/CODEOWNERS +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/dependabot.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/ci.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/dco.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/image-build.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/image-smoke.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/images.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/publish-pypi.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/release.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/security.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.github/workflows/verify-packages.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.gitignore +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/.gitleaks.toml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/CODE_OF_CONDUCT.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/CONTRIBUTING.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/DCO +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/LICENSE +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/NOTICE +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/SECURITY.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/README.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/application/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/domain/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/domain/identifiers.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/passfile.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/settings.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_adapter_duckdb_proxy.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_adapter_duckdb_validation.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_adapter_duckdb_wiring.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_domain_duckdb_columns.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_domain_duckdb_identifiers.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_domain_duckdb_layout.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_integration_duckdb_layout.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_integration_duckdb_stack.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/duckdb/tests/test_integration_duckdb_validation.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/README.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/continuo_postgres_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/tests/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/tests/conftest.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/tests/test_adapter_postgres_validation.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/postgres/tests/test_integration_postgres_validation.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/README.md +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/continuo_trino_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/tests/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/tests/test_adapter_runtime_trino.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/tests/test_adapter_trino_validation.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/adapters/trino/tests/test_integration_trino_validation.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/cli.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/closure.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/conform.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/context.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/contract/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/contract/paths.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/contract/secret_ref.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/csv_loader.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/csv_readers/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/csv_readers/https.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/csv_readers/s3.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/csv_source.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/errors.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/hashing.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/lint.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/types.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/validation/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/validation/runner.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/continuo_python_runtime/validation/s3.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/LICENSE +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/NOTICE +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/continuo_engine_contract/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/continuo_engine_contract/config.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/continuo_engine_contract/result.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/continuo_engine_contract/sql.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/continuo_engine_contract/types.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/tests/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/tests/test_config.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/tests/test_contract_types.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/tests/test_result.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/contract/tests/test_sql.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/scripts/apply_rehearsal_version.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/scripts/bake_duckdb_extensions.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/scripts/check_version_bumps.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/scripts/security-scan.sh +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/scripts/verify_installed_packages.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/template/contracts/example.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/template/contracts/example_csv.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/template/requirements.lock +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/template/scripts/example.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/contract/__init__.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/smoke/duckdb-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/smoke/node_smoke/contracts/smoke.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/smoke/node_smoke/scripts/smoke.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/smoke/postgres-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/smoke/trino-stack/catalog/iceberg.properties +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/smoke/trino-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_adapter_naming.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_check_version_bumps_tags.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_ci_test_coverage.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_cli.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_closure.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_conform.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_context.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_contract_loader.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_contract_pin_consistency.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_csv_loader.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_csv_readers_https.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_csv_readers_integration.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_csv_source.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_errors.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_harness.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_hashing.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_image_requirements_sync.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_image_smoke_validation.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_lint.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_no_legacy_names.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_package.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_rehearsal_version.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_release_pipeline_structure.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_types.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_validation_runner.py +0 -0
- {continuo_python_runtime-0.8.0 → continuo_python_runtime-0.9.0}/tests/test_validation_s3.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14.2
|
|
@@ -5,6 +5,90 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.9.0] - 2026-10-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Contracts may set `mode_type` — `replace` (the default), `append` or
|
|
13
|
+
`upsert` — and, for `upsert` only, `primary_key`: a non-empty list of output
|
|
14
|
+
columns, each spelled exactly as in `output_columns` and declared
|
|
15
|
+
`nullable: false`, names unique case-insensitively, and none a
|
|
16
|
+
floating-point (`DOUBLE PRECISION`) column — the warehouse collapses `0.0`
|
|
17
|
+
and `-0.0`, so a float key is rejected at validation rather than failing at
|
|
18
|
+
write time.
|
|
19
|
+
`python-csv` nodes allow only `replace`. `continuo-runtime validate`, `merge`
|
|
20
|
+
and `run` enforce these rules; the harness's write path for the new modes
|
|
21
|
+
follows in a later entry.
|
|
22
|
+
- The merged wire entry carries `mode_type` when it is not `replace` and
|
|
23
|
+
`primary_key` (authored order) for `upsert`; both are part of
|
|
24
|
+
`config_hash`. Entries of nodes that do not set them, and their hashes, are
|
|
25
|
+
unchanged.
|
|
26
|
+
- `continuo-engine-contract`: opt-in write capabilities on top of
|
|
27
|
+
`WarehouseAdapter` — `TableIntrospection` (`table_columns`, returning
|
|
28
|
+
`LiveColumn(name, nullable)`; `LookupError` for a missing table),
|
|
29
|
+
`AppendCapable` (`append`) and `UpsertCapable` (`upsert`, which runs
|
|
30
|
+
`ensure_unique_key` and then the engine's `_upsert`). `ensure_table` takes an
|
|
31
|
+
optional keyword-only `primary_key`. `continuo_engine_contract.keys` holds
|
|
32
|
+
`ensure_unique_key` and `DuplicateKeyError(ValueError)`.
|
|
33
|
+
- `continuo_engine_contract.testing.conformance`: a pytest conformance suite for the
|
|
34
|
+
write capabilities (`IntrospectionConformance`, `AppendConformance`,
|
|
35
|
+
`UpsertConformance`). Adapter integration suites subclass the reusable
|
|
36
|
+
classes and run them against their real engine, providing `adapter` and
|
|
37
|
+
`schema` fixtures. Install with `continuo-engine-contract[testing]`.
|
|
38
|
+
- `continuo-postgres-adapter`: `append` (batched inserts in one transaction),
|
|
39
|
+
`upsert` (`INSERT ... ON CONFLICT (key) DO UPDATE`, `DO NOTHING` when every
|
|
40
|
+
column is in the key) and `table_columns`; `ensure_table` declares an
|
|
41
|
+
upsert key as the table's `PRIMARY KEY`. `load` now shares the insert path
|
|
42
|
+
(no behavior change).
|
|
43
|
+
- `continuo-duckdb-adapter`: `append` (one transaction), `upsert` (`MERGE`
|
|
44
|
+
from the registered Arrow data, one transaction) and `table_columns`.
|
|
45
|
+
`ensure_table` accepts and ignores an upsert key: DuckLake has no
|
|
46
|
+
constraints, so the key is guaranteed by the write path.
|
|
47
|
+
- `continuo-trino-adapter`: `append` and `upsert` through a stage table
|
|
48
|
+
shaped like the target, then one `INSERT ... SELECT` or one `MERGE` — one
|
|
49
|
+
Iceberg commit per write; the stage is dropped best-effort afterwards.
|
|
50
|
+
`table_columns` reads `information_schema`. `ensure_table` accepts an
|
|
51
|
+
upsert key (no constraint DDL) and rejects it with `format_version: 1`.
|
|
52
|
+
`load` now shares the stage helpers (no behavior change).
|
|
53
|
+
- The harness writes each node with its `mode_type`: `load` for `replace`,
|
|
54
|
+
`append`, or `upsert` with the node's `primary_key` (also passed to
|
|
55
|
+
`ensure_table`). An adapter without the needed capability fails with
|
|
56
|
+
`LoadError: <Adapter> does not support <mode>` before the script runs; an
|
|
57
|
+
`upsert` whose rows repeat a key fails with `ConformError` and writes
|
|
58
|
+
nothing. Before an `append` or `upsert` the live table's columns and
|
|
59
|
+
nullability are compared with `output_columns`; any difference is a
|
|
60
|
+
`LoadError` naming each one. See "Write modes" in the README.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- The Python version is pinned in one place, `.python-version` (`3.14.2`), the
|
|
65
|
+
single source of truth: the engine images' `Dockerfile`s pin the same exact
|
|
66
|
+
patch (`python:3.14.2-slim`), and `requires-python`, mypy `python_version`,
|
|
67
|
+
the trove classifiers and `setup-python` inputs track its major.minor. A new
|
|
68
|
+
consistency guard (`tests/test_python_version_consistency.py`) scans every
|
|
69
|
+
`pyproject.toml`, `Dockerfile*` and workflow and fails on any drift.
|
|
70
|
+
- `continuo-engine-contract` 0.7.3 -> 0.8.0 (the write capabilities above).
|
|
71
|
+
The runtime and the three adapters pin it exactly, as do the engine images'
|
|
72
|
+
`image-requirements-*.txt`.
|
|
73
|
+
- **Breaking for newly copied templates.** The `template/` release workflow
|
|
74
|
+
calls continuo's public release API instead of an unauthenticated webhook:
|
|
75
|
+
- `RELEASE_ENDPOINT` keeps its name but is now the origin of continuo's
|
|
76
|
+
`auth.publicUrl` (`scheme://host[:port]`, no path), which is also the OIDC
|
|
77
|
+
audience.
|
|
78
|
+
- The repository must be bound to the service in continuo's
|
|
79
|
+
`ciAuth.bindings`.
|
|
80
|
+
- The first release of a service is an operator bootstrap; the workflow never
|
|
81
|
+
sends `bootstrap`.
|
|
82
|
+
|
|
83
|
+
The workflow checks the endpoint and does an authenticated read of
|
|
84
|
+
`/api/v1/current-prod` before building, so a wrong URL, audience or missing
|
|
85
|
+
binding fails early. It then submits to `<RELEASE_ENDPOINT>/api/v1/releases`
|
|
86
|
+
with a fresh GitHub Actions OIDC token per call and polls
|
|
87
|
+
`GET /api/v1/releases/{id}` for up to about 50 minutes (`timeout-minutes: 60`),
|
|
88
|
+
failing on `rejected` and `superseded`. A newer push no longer cancels a run
|
|
89
|
+
that is waiting for its release (`cancel-in-progress: false`). Workflows
|
|
90
|
+
already copied from the template are untouched.
|
|
91
|
+
|
|
8
92
|
## [0.8.0] - 2026-10-01
|
|
9
93
|
|
|
10
94
|
Packages in this release: `continuo-python-runtime` 0.8.0 (no runtime code
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# continuo-python-runtime
|
|
2
|
+
|
|
3
|
+
The validation/execution runtime that continuo's executor runs as a Kubernetes
|
|
4
|
+
Job. Python 3.12, uv workspace: the runtime (`continuo_python_runtime`), the
|
|
5
|
+
engine contract (`contract/`), and the engine adapters (`adapters/postgres`,
|
|
6
|
+
`adapters/trino`, `adapters/duckdb`).
|
|
7
|
+
|
|
8
|
+
## CHANGELOG is not optional
|
|
9
|
+
|
|
10
|
+
Every user-facing change keeps `CHANGELOG.md` current. Two steps, both required:
|
|
11
|
+
|
|
12
|
+
- **On every PR** that changes behavior, the CLI/op surface, a package version,
|
|
13
|
+
or a container: add a bullet under `## [Unreleased]` (Keep a Changelog style —
|
|
14
|
+
Added / Changed / Fixed / Removed). No entry, no merge.
|
|
15
|
+
- **On every release**: rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`,
|
|
16
|
+
add a fresh empty `## [Unreleased]` above it, and tag. `release.yml` reads the
|
|
17
|
+
section matching the pushed tag for the GitHub Release notes, so a release with
|
|
18
|
+
no matching section ships with generic auto-notes and the changelog silently
|
|
19
|
+
loses that version.
|
|
20
|
+
|
|
21
|
+
If the changelog is missing a shipped version, backfill it from that tag's
|
|
22
|
+
merged PRs before adding anything new.
|
|
23
|
+
|
|
24
|
+
## Engine adapter conformance
|
|
25
|
+
|
|
26
|
+
`contract/continuo_engine_contract/testing/conformance.py`
|
|
27
|
+
(`continuo_engine_contract.testing.conformance`) is the shared, published
|
|
28
|
+
conformance suite for engine adapter authors. Its dependencies are installed
|
|
29
|
+
with `continuo-engine-contract[testing]`.
|
|
30
|
+
|
|
31
|
+
- Every adapter MUST satisfy the engine contract and pass the shared
|
|
32
|
+
conformance classes for every capability it declares: `IntrospectionConformance`
|
|
33
|
+
for `TableIntrospection`, `AppendConformance` for `AppendCapable`, and
|
|
34
|
+
`UpsertConformance` for `UpsertCapable`. Append and upsert adapters must also
|
|
35
|
+
run the introspection class. An adapter need not declare capabilities its
|
|
36
|
+
engine cannot support.
|
|
37
|
+
- Each adapter's integration suite MUST subclass the applicable classes,
|
|
38
|
+
supply connected `adapter` and temporary `schema` fixtures, and run them
|
|
39
|
+
against its real warehouse in CI. In-memory reference tests alone do not
|
|
40
|
+
establish an adapter's conformance. Do not skip or weaken shared checks to
|
|
41
|
+
accommodate an adapter that violates the contract.
|
|
42
|
+
- Every new or changed engine-independent contract requirement, including
|
|
43
|
+
bug regressions, MUST have coverage in this shared testing module. Extend
|
|
44
|
+
the applicable conformance class, or add a class for a new capability, so
|
|
45
|
+
all adapters declaring it are checked by the same cases. Exercise the
|
|
46
|
+
reusable cases in `contract/tests/test_conformance_suite.py` as well.
|
|
47
|
+
- Engine-specific implementation details, such as SQL generation, driver
|
|
48
|
+
behavior and authentication, belong in the owning adapter's tests. Add a
|
|
49
|
+
shared case when the behavior expresses a requirement of the engine
|
|
50
|
+
contract; unrelated runtime features do not belong in this module.
|
|
51
|
+
- Before completing an adapter or contract change, run `contract/tests` and
|
|
52
|
+
the affected adapter's unit and real-engine integration suites in Docker.
|
|
53
|
+
|
|
54
|
+
## Releasing
|
|
55
|
+
|
|
56
|
+
A release is one `chore(release):` commit that bumps versions and updates the
|
|
57
|
+
changelog, then a `vX.Y.Z` tag on it. Bump the version of every package whose
|
|
58
|
+
source changed since the last tag — `scripts/check_version_bumps.py` fails the
|
|
59
|
+
PyPI publish otherwise. The tag builds and publishes the `-postgres` / `-trino` / `-duckdb`
|
|
60
|
+
images and the PyPI packages. See `CONTRIBUTING.md` for the full steps.
|
|
61
|
+
|
|
62
|
+
## Conventions
|
|
63
|
+
|
|
64
|
+
- Standard `logging` for diagnostics, never `print` — except the CLI's
|
|
65
|
+
sentinel-framed result blocks, which own stdout.
|
|
66
|
+
- In-repo packages are pinned exactly, not with ranges (see `pyproject.toml`).
|
|
67
|
+
- Commits are signed off (`git commit -s`); CI enforces the DCO.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM python:3.14-slim
|
|
1
|
+
FROM python:3.14.2-slim
|
|
2
2
|
# One dual-role image per engine: the python-node runtime harness (default
|
|
3
3
|
# command `run`) and the blue/green validation runner (`validation-op`). It
|
|
4
4
|
# installs the PUBLISHED, versioned libraries, never the repo source:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM python:3.14-slim
|
|
1
|
+
FROM python:3.14.2-slim
|
|
2
2
|
# One dual-role image per engine — the python-node runtime harness (default
|
|
3
3
|
# command `run`) and the blue/green validation runner (`validation-op`). It
|
|
4
4
|
# installs the PUBLISHED, versioned libraries, never the repo source:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM python:3.14-slim
|
|
1
|
+
FROM python:3.14.2-slim
|
|
2
2
|
# One dual-role image per engine — the python-node runtime harness (default
|
|
3
3
|
# command `run`) and the blue/green validation runner (`validation-op`). It
|
|
4
4
|
# installs the PUBLISHED, versioned libraries, never the repo source:
|
|
@@ -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
|
|
@@ -73,7 +73,8 @@ engine adapters, the validation runner, and the node harness. The former
|
|
|
73
73
|
`continuo-validation` repository was merged in — there is no longer a separate
|
|
74
74
|
validation-side port, adapter class, entry-point group, or image. One
|
|
75
75
|
`WarehouseAdapter` per engine serves both the data plane (`fetch` /
|
|
76
|
-
`ensure_table` / `load
|
|
76
|
+
`ensure_table` / `load`, plus the opt-in `table_columns` / `append` / `upsert`
|
|
77
|
+
capabilities) and validation (`ensure_schema` / `drop_schema` /
|
|
77
78
|
`build_empty_from_sql` / `build_empty_from_columns` / `clone_empty_from_prod` /
|
|
78
79
|
`check_binds`), and one image per engine runs both roles.
|
|
79
80
|
|
|
@@ -121,9 +122,13 @@ the Go parser has not been taught is a production outage, not a refactor.
|
|
|
121
122
|
service name (one service name per domain repo).
|
|
122
123
|
3. Configure repository variables in GitHub (Settings → Secrets and
|
|
123
124
|
variables → Actions): `REGISTRY` (your Docker registry), `BUCKET` (your
|
|
124
|
-
S3 bucket for contract artifacts), `RELEASE_ENDPOINT` (the
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
127
132
|
4. Configure repository secrets: `AWS_ACCESS_KEY_ID` and
|
|
128
133
|
`AWS_SECRET_ACCESS_KEY` for the S3 upload. The template workflow pushes
|
|
129
134
|
the built image to GHCR using the workflow's own `GITHUB_TOKEN` (granted
|
|
@@ -191,6 +196,62 @@ produced, never how it is checked or written.
|
|
|
191
196
|
script reads with `os.environ[...]`. The contract never holds a secret value
|
|
192
197
|
or env var name; the Secret itself is created in the cluster by the operator.
|
|
193
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
|
+
|
|
194
255
|
## The script API
|
|
195
256
|
|
|
196
257
|
A node script is a Python file with exactly one required entry point:
|
|
@@ -277,8 +338,8 @@ text.
|
|
|
277
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. |
|
|
278
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. |
|
|
279
340
|
| `ScriptError` | `run()` raised, has no callable `run`, or returned a value that isn't Arrow-convertible. | The node script. |
|
|
280
|
-
| `ConformError` | Structural mismatch (extra/missing/duplicate columns), a strict-cast failure, a not-null violation,
|
|
281
|
-
| `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. |
|
|
282
343
|
|
|
283
344
|
## Engine selection
|
|
284
345
|
|
|
@@ -286,18 +347,21 @@ A domain repo picks its warehouse engine by which base image it builds
|
|
|
286
347
|
`FROM`:
|
|
287
348
|
|
|
288
349
|
```dockerfile
|
|
289
|
-
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
|
|
290
353
|
# or
|
|
291
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-
|
|
354
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.9.0
|
|
292
355
|
```
|
|
293
356
|
|
|
294
357
|
The engine is part of the image **name**; the tag is the bare version, so
|
|
295
358
|
Continuo's Helm chart can pin an image as `<name>:vX.Y.Z@sha256:<digest>`.
|
|
296
359
|
|
|
297
360
|
Each image bakes in exactly one `WarehouseAdapter` for that engine — the
|
|
298
|
-
pinned PyPI version of the `continuo-postgres-adapter
|
|
299
|
-
`continuo-trino-adapter` package built from this
|
|
300
|
-
|
|
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
|
|
301
365
|
`continuo_engine.adapters` entry-point group (entry names `postgres` /
|
|
302
366
|
`trino` / `duckdb`). The runtime discovers it via `discover_adapter()` at run time, so a
|
|
303
367
|
single image serves every node in the service and the release-time
|
|
@@ -53,7 +53,8 @@ engine adapters, the validation runner, and the node harness. The former
|
|
|
53
53
|
`continuo-validation` repository was merged in — there is no longer a separate
|
|
54
54
|
validation-side port, adapter class, entry-point group, or image. One
|
|
55
55
|
`WarehouseAdapter` per engine serves both the data plane (`fetch` /
|
|
56
|
-
`ensure_table` / `load
|
|
56
|
+
`ensure_table` / `load`, plus the opt-in `table_columns` / `append` / `upsert`
|
|
57
|
+
capabilities) and validation (`ensure_schema` / `drop_schema` /
|
|
57
58
|
`build_empty_from_sql` / `build_empty_from_columns` / `clone_empty_from_prod` /
|
|
58
59
|
`check_binds`), and one image per engine runs both roles.
|
|
59
60
|
|
|
@@ -101,9 +102,13 @@ the Go parser has not been taught is a production outage, not a refactor.
|
|
|
101
102
|
service name (one service name per domain repo).
|
|
102
103
|
3. Configure repository variables in GitHub (Settings → Secrets and
|
|
103
104
|
variables → Actions): `REGISTRY` (your Docker registry), `BUCKET` (your
|
|
104
|
-
S3 bucket for contract artifacts), `RELEASE_ENDPOINT` (the
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
S3 bucket for contract artifacts), `RELEASE_ENDPOINT` (the base
|
|
106
|
+
URL of your continuo install, `scheme://host[:port]` with no path, the origin of continuo's
|
|
107
|
+
`auth.publicUrl`). The
|
|
108
|
+
workflow calls `<RELEASE_ENDPOINT>/api/v1/releases` with its GitHub Actions
|
|
109
|
+
OIDC token, so the repository must be bound to your service in continuo's
|
|
110
|
+
`ciAuth.bindings` (see [Releasing from CI](https://github.com/carolsimone/continuo/blob/main/deploy/README.md#releasing-from-ci-github-actions)),
|
|
111
|
+
and an operator bootstraps the service's first release.
|
|
107
112
|
4. Configure repository secrets: `AWS_ACCESS_KEY_ID` and
|
|
108
113
|
`AWS_SECRET_ACCESS_KEY` for the S3 upload. The template workflow pushes
|
|
109
114
|
the built image to GHCR using the workflow's own `GITHUB_TOKEN` (granted
|
|
@@ -171,6 +176,62 @@ produced, never how it is checked or written.
|
|
|
171
176
|
script reads with `os.environ[...]`. The contract never holds a secret value
|
|
172
177
|
or env var name; the Secret itself is created in the cluster by the operator.
|
|
173
178
|
|
|
179
|
+
## Write modes
|
|
180
|
+
|
|
181
|
+
`mode_type:` chooses how the harness writes a node's table on each run:
|
|
182
|
+
|
|
183
|
+
| `mode_type` | Each run | Rows from earlier runs |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `replace` (default) | empties the table, inserts this run's rows | gone |
|
|
186
|
+
| `append` | inserts this run's rows | kept |
|
|
187
|
+
| `upsert` | inserts rows with a new key, overwrites rows whose key exists | kept unless overwritten; never deleted |
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
- schema: fx
|
|
191
|
+
table: fx_rates
|
|
192
|
+
kind: python-api
|
|
193
|
+
mode_type: upsert
|
|
194
|
+
primary_key: [rate_date, base, quote]
|
|
195
|
+
output_columns:
|
|
196
|
+
- {name: rate_date, type: DATE, nullable: false}
|
|
197
|
+
- {name: base, type: VARCHAR(3), nullable: false}
|
|
198
|
+
- {name: quote, type: VARCHAR(3), nullable: false}
|
|
199
|
+
- {name: rate, type: DECIMAL(12,6)}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- `python-csv` nodes only `replace`.
|
|
203
|
+
- `primary_key` is required for `upsert` and rejected otherwise: a non-empty
|
|
204
|
+
list of output columns declared `nullable: false` (a NULL key never
|
|
205
|
+
matches), each spelled exactly as in `output_columns`, names unique
|
|
206
|
+
case-insensitively. A Postgres `replace` table that
|
|
207
|
+
wants uniqueness uses `config.indexes` with `unique: true`.
|
|
208
|
+
- Every write is atomic: all of the run's rows or none. An `append` or
|
|
209
|
+
`upsert` run with zero rows writes nothing (a `replace` run with zero rows
|
|
210
|
+
still empties the table).
|
|
211
|
+
- `append` is a plain insert: a run that committed but was reported failed
|
|
212
|
+
and is retried stores its rows twice. `upsert` is idempotent.
|
|
213
|
+
- An `upsert` run whose own rows repeat a key fails with `ConformError` and
|
|
214
|
+
writes nothing.
|
|
215
|
+
- Before an `append` or `upsert` the harness compares the live table with
|
|
216
|
+
`output_columns` — names case-insensitively, nullability exactly; order and
|
|
217
|
+
types are not compared — and fails with `LoadError` naming every missing
|
|
218
|
+
column, unexpected column and nullability mismatch. Changing an `append` or
|
|
219
|
+
`upsert` table (columns, types, nullability, key or mode) needs a new table
|
|
220
|
+
name (`_v2`) under a new contract; continuo rejects the release otherwise.
|
|
221
|
+
Adding a nullable column is no exception: the existing rows cannot be
|
|
222
|
+
backfilled.
|
|
223
|
+
- The installed engine must support the mode; all three shipped adapters do.
|
|
224
|
+
A bring-your-own adapter declares `AppendCapable` / `UpsertCapable` from
|
|
225
|
+
`continuo-engine-contract` (and can run its conformance suite); without them
|
|
226
|
+
an `append`/`upsert` node fails with `LoadError: <Adapter> does not support
|
|
227
|
+
<mode>` before its script runs.
|
|
228
|
+
- Engines: on Postgres the key is the table's `PRIMARY KEY`, created with the
|
|
229
|
+
table; DuckLake and Trino/Iceberg have no constraints, so the key is
|
|
230
|
+
guaranteed by the write path only. On Trino an `upsert` table needs Iceberg
|
|
231
|
+
`format_version` 2 (the default).
|
|
232
|
+
- `full_refresh` stays refused for every python node: a rebuild would destroy
|
|
233
|
+
an accumulating table.
|
|
234
|
+
|
|
174
235
|
## The script API
|
|
175
236
|
|
|
176
237
|
A node script is a Python file with exactly one required entry point:
|
|
@@ -257,8 +318,8 @@ text.
|
|
|
257
318
|
| `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. |
|
|
258
319
|
| `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. |
|
|
259
320
|
| `ScriptError` | `run()` raised, has no callable `run`, or returned a value that isn't Arrow-convertible. | The node script. |
|
|
260
|
-
| `ConformError` | Structural mismatch (extra/missing/duplicate columns), a strict-cast failure, a not-null violation,
|
|
261
|
-
| `LoadError` | Adapter construction failed, or the DDL/INSERT failed at the warehouse during the write. | Warehouse connectivity/permissions, or the target table. |
|
|
321
|
+
| `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. |
|
|
322
|
+
| `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. |
|
|
262
323
|
|
|
263
324
|
## Engine selection
|
|
264
325
|
|
|
@@ -266,18 +327,21 @@ A domain repo picks its warehouse engine by which base image it builds
|
|
|
266
327
|
`FROM`:
|
|
267
328
|
|
|
268
329
|
```dockerfile
|
|
269
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.
|
|
330
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.9.0
|
|
331
|
+
# or
|
|
332
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.9.0
|
|
270
333
|
# or
|
|
271
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-
|
|
334
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.9.0
|
|
272
335
|
```
|
|
273
336
|
|
|
274
337
|
The engine is part of the image **name**; the tag is the bare version, so
|
|
275
338
|
Continuo's Helm chart can pin an image as `<name>:vX.Y.Z@sha256:<digest>`.
|
|
276
339
|
|
|
277
340
|
Each image bakes in exactly one `WarehouseAdapter` for that engine — the
|
|
278
|
-
pinned PyPI version of the `continuo-postgres-adapter
|
|
279
|
-
`continuo-trino-adapter` package built from this
|
|
280
|
-
|
|
341
|
+
pinned PyPI version of the `continuo-postgres-adapter`,
|
|
342
|
+
`continuo-trino-adapter` or `continuo-duckdb-adapter` package built from this
|
|
343
|
+
repo's `adapters/postgres/`, `adapters/trino/` or `adapters/duckdb/` source
|
|
344
|
+
(see the table above) — registered under the
|
|
281
345
|
`continuo_engine.adapters` entry-point group (entry names `postgres` /
|
|
282
346
|
`trino` / `duckdb`). The runtime discovers it via `discover_adapter()` at run time, so a
|
|
283
347
|
single image serves every node in the service and the release-time
|
|
@@ -6,9 +6,9 @@ port leaves to the concrete engine, ``required_env`` and ``from_env``.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from .application.warehouse import LakeWarehouse
|
|
10
|
-
from .infrastructure.session import DuckLakeSession
|
|
11
|
-
from .infrastructure.settings import REQUIRED_ENV, DuckLakeSettings
|
|
9
|
+
from continuo_duckdb_adapter.application.warehouse import LakeWarehouse
|
|
10
|
+
from continuo_duckdb_adapter.infrastructure.session import DuckLakeSession
|
|
11
|
+
from continuo_duckdb_adapter.infrastructure.settings import REQUIRED_ENV, DuckLakeSettings
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class DuckDBAdapter(LakeWarehouse):
|
|
@@ -6,9 +6,11 @@ from collections.abc import Sequence
|
|
|
6
6
|
from contextlib import AbstractContextManager
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
from
|
|
9
|
+
from continuo_engine_contract.port import LiveColumn # type: ignore[import-untyped]
|
|
10
|
+
|
|
11
|
+
from continuo_duckdb_adapter.domain.columns import ColumnDefinition
|
|
12
|
+
from continuo_duckdb_adapter.domain.identifiers import Identifier, QualifiedTable
|
|
13
|
+
from continuo_duckdb_adapter.domain.layout import TableLayout
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING: # pragma: no cover
|
|
14
16
|
import pyarrow # type: ignore[import-untyped]
|
|
@@ -35,6 +37,10 @@ class LakeGateway(ABC):
|
|
|
35
37
|
@abstractmethod
|
|
36
38
|
def table_exists(self, table: QualifiedTable) -> bool: ...
|
|
37
39
|
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def columns(self, table: QualifiedTable) -> "list[LiveColumn]":
|
|
42
|
+
"""Name and nullability of every column of *table*, in table order; [] when absent."""
|
|
43
|
+
|
|
38
44
|
@abstractmethod
|
|
39
45
|
def drop_table_if_exists(self, table: QualifiedTable) -> None: ...
|
|
40
46
|
|
|
@@ -73,5 +79,11 @@ class LakeGateway(ABC):
|
|
|
73
79
|
def insert_arrow(self, table: QualifiedTable, data: "pyarrow.Table") -> None:
|
|
74
80
|
"""Append *data* in its own column order."""
|
|
75
81
|
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def merge_arrow(
|
|
84
|
+
self, table: QualifiedTable, data: "pyarrow.Table", key: Sequence[str]
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Insert *data*'s rows with a new *key*, overwrite rows whose key exists."""
|
|
87
|
+
|
|
76
88
|
@abstractmethod
|
|
77
89
|
def close(self) -> None: ...
|
|
@@ -10,16 +10,21 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import time
|
|
13
|
-
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
14
|
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
|
-
from continuo_engine_contract.port import
|
|
16
|
+
from continuo_engine_contract.port import ( # type: ignore[import-untyped]
|
|
17
|
+
AppendCapable,
|
|
18
|
+
LiveColumn,
|
|
19
|
+
UpsertCapable,
|
|
20
|
+
WarehouseAdapter,
|
|
21
|
+
)
|
|
17
22
|
from continuo_engine_contract.sql import ensure_single_read # type: ignore[import-untyped]
|
|
18
23
|
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
22
|
-
from .ports import LakeConflictError, LakeGateway
|
|
24
|
+
from continuo_duckdb_adapter.domain.columns import ColumnDefinition, column_types
|
|
25
|
+
from continuo_duckdb_adapter.domain.identifiers import Identifier, QualifiedTable
|
|
26
|
+
from continuo_duckdb_adapter.domain.layout import TableLayout
|
|
27
|
+
from continuo_duckdb_adapter.application.ports import LakeConflictError, LakeGateway
|
|
23
28
|
|
|
24
29
|
if TYPE_CHECKING: # pragma: no cover
|
|
25
30
|
import pyarrow as pa # type: ignore[import-untyped]
|
|
@@ -42,7 +47,7 @@ def _columns(raw: list[dict]) -> list[ColumnDefinition]:
|
|
|
42
47
|
return [ColumnDefinition.from_mapping(entry) for entry in raw]
|
|
43
48
|
|
|
44
49
|
|
|
45
|
-
class LakeWarehouse(WarehouseAdapter):
|
|
50
|
+
class LakeWarehouse(WarehouseAdapter, AppendCapable, UpsertCapable):
|
|
46
51
|
"""WarehouseAdapter implemented over a LakeGateway."""
|
|
47
52
|
|
|
48
53
|
def __init__(
|
|
@@ -159,6 +164,17 @@ class LakeWarehouse(WarehouseAdapter):
|
|
|
159
164
|
"""
|
|
160
165
|
TableLayout.from_config(config, {name: None for name in column_names})
|
|
161
166
|
|
|
167
|
+
def table_columns(self, schema: str, table: str) -> list[LiveColumn]:
|
|
168
|
+
"""Columns of ``schema.table`` as the DuckLake catalog reports them.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
LookupError: If the table does not exist.
|
|
172
|
+
"""
|
|
173
|
+
columns = self._gateway.columns(QualifiedTable.of(schema, table))
|
|
174
|
+
if not columns:
|
|
175
|
+
raise LookupError(f"table {schema}.{table} does not exist")
|
|
176
|
+
return columns
|
|
177
|
+
|
|
162
178
|
def ensure_table(
|
|
163
179
|
self,
|
|
164
180
|
schema: str,
|
|
@@ -166,6 +182,7 @@ class LakeWarehouse(WarehouseAdapter):
|
|
|
166
182
|
columns: list[dict[str, Any]],
|
|
167
183
|
*,
|
|
168
184
|
config: dict[str, Any],
|
|
185
|
+
primary_key: Sequence[str] = (),
|
|
169
186
|
) -> None:
|
|
170
187
|
"""Create the table if absent; layout is applied only when this call creates it.
|
|
171
188
|
|
|
@@ -173,7 +190,12 @@ class LakeWarehouse(WarehouseAdapter):
|
|
|
173
190
|
existence check and the create share one transaction; a concurrent
|
|
174
191
|
creator makes one side fail with a conflict, and the retry then finds
|
|
175
192
|
the table and returns.
|
|
193
|
+
|
|
194
|
+
*primary_key* is accepted and ignored: DuckLake rejects PRIMARY KEY and
|
|
195
|
+
UNIQUE constraints, so an upsert node's key is guaranteed by ``upsert``
|
|
196
|
+
(the port's duplicate check, then MERGE), not by the table.
|
|
176
197
|
"""
|
|
198
|
+
del primary_key
|
|
177
199
|
defs = _columns(columns)
|
|
178
200
|
layout = TableLayout.from_config(config, column_types(defs))
|
|
179
201
|
self.ensure_schema(schema)
|
|
@@ -200,6 +222,41 @@ class LakeWarehouse(WarehouseAdapter):
|
|
|
200
222
|
logger.info("loading %d row(s) into %s.%s", data.num_rows, schema, table)
|
|
201
223
|
self._gateway.insert_arrow(target, data)
|
|
202
224
|
|
|
225
|
+
def append(self, schema: str, table: str, data: "pa.Table") -> None:
|
|
226
|
+
"""Atomically insert every row of *data*; earlier rows stay (one transaction).
|
|
227
|
+
|
|
228
|
+
The insert names *data*'s columns. A 0-row *data* opens no transaction,
|
|
229
|
+
so DuckLake records no snapshot for it.
|
|
230
|
+
"""
|
|
231
|
+
if not data.num_rows:
|
|
232
|
+
logger.info("append of 0 rows into %s.%s: nothing to write", schema, table)
|
|
233
|
+
return
|
|
234
|
+
target = QualifiedTable.of(schema, table)
|
|
235
|
+
with self._gateway.transaction():
|
|
236
|
+
logger.info("appending %d row(s) into %s.%s", data.num_rows, schema, table)
|
|
237
|
+
self._gateway.insert_arrow(target, data)
|
|
238
|
+
|
|
239
|
+
def _upsert(
|
|
240
|
+
self, schema: str, table: str, data: "pa.Table", primary_key: Sequence[str]
|
|
241
|
+
) -> None:
|
|
242
|
+
"""MERGE *data* into ``schema.table`` on *primary_key*, in one transaction.
|
|
243
|
+
|
|
244
|
+
DuckLake's MERGE does not detect a key repeated within *data* (it
|
|
245
|
+
inserts both rows of a new key and keeps the last of an existing one);
|
|
246
|
+
the port's ``upsert`` has already rejected that. A 0-row *data* opens
|
|
247
|
+
no transaction.
|
|
248
|
+
"""
|
|
249
|
+
if not data.num_rows:
|
|
250
|
+
logger.info("upsert of 0 rows into %s.%s: nothing to write", schema, table)
|
|
251
|
+
return
|
|
252
|
+
target = QualifiedTable.of(schema, table)
|
|
253
|
+
with self._gateway.transaction():
|
|
254
|
+
logger.info(
|
|
255
|
+
"upserting %d row(s) into %s.%s on (%s)",
|
|
256
|
+
data.num_rows, schema, table, ", ".join(primary_key),
|
|
257
|
+
)
|
|
258
|
+
self._gateway.merge_arrow(target, data, list(primary_key))
|
|
259
|
+
|
|
203
260
|
def close(self) -> None:
|
|
204
261
|
"""Release the underlying connection."""
|
|
205
262
|
self._gateway.close()
|
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from continuo_engine_contract.types import validate_column_type # type: ignore[import-untyped]
|
|
10
10
|
|
|
11
|
-
from .identifiers import Identifier
|
|
11
|
+
from continuo_duckdb_adapter.domain.identifiers import Identifier
|
|
12
12
|
|
|
13
13
|
_TEMPORAL = re.compile(r"^(TIMESTAMP|DATE)\Z", re.IGNORECASE | re.ASCII)
|
|
14
14
|
|
|
@@ -13,8 +13,8 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from continuo_engine_contract.config import ensure_known_keys # type: ignore[import-untyped]
|
|
15
15
|
|
|
16
|
-
from .columns import is_temporal_type
|
|
17
|
-
from .identifiers import Identifier
|
|
16
|
+
from continuo_duckdb_adapter.domain.columns import is_temporal_type
|
|
17
|
+
from continuo_duckdb_adapter.domain.identifiers import Identifier
|
|
18
18
|
|
|
19
19
|
ENGINE = "duckdb"
|
|
20
20
|
KNOWN_CONFIG_KEYS: tuple[str, ...] = ("partitioned_by", "sorted_by")
|