continuo-python-runtime 0.10.0__tar.gz → 0.11.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.11.0/.python-version +1 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/CHANGELOG.md +71 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/CLAUDE.md +10 -8
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/Dockerfile.duckdb +1 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/Dockerfile.postgres +1 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/Dockerfile.trino +1 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/PKG-INFO +12 -9
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/README.md +7 -4
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/README.md +12 -3
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/session.py +47 -2
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/pyproject.toml +7 -7
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_infra_duckdb_session.py +66 -3
- continuo_python_runtime-0.11.0/adapters/duckdb/tests/test_integration_duckdb_conformance.py +51 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_stack.py +18 -6
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/README.md +7 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/continuo_postgres_adapter/adapter.py +68 -11
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/pyproject.toml +6 -6
- continuo_python_runtime-0.11.0/adapters/postgres/tests/test_adapter_postgres_connect.py +209 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_integration_postgres_conformance.py +23 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_integration_runtime_postgres.py +29 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/README.md +11 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/continuo_trino_adapter/adapter.py +84 -2
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/pyproject.toml +6 -6
- continuo_python_runtime-0.11.0/adapters/trino/tests/test_adapter_trino_connect.py +217 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_trino_conformance.py +24 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/harness.py +8 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/README.md +1 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/port.py +9 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/testing/conformance.py +130 -5
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/pyproject.toml +3 -3
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_contract_packaging.py +1 -1
- continuo_python_runtime-0.11.0/contract/tests/test_startup_conformance.py +167 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/docs/boundary-contract.md +5 -2
- continuo_python_runtime-0.11.0/image-requirements-duckdb.txt +3 -0
- continuo_python_runtime-0.11.0/image-requirements-postgres.txt +3 -0
- continuo_python_runtime-0.11.0/image-requirements-trino.txt +3 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/pyproject.toml +7 -7
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/Dockerfile +1 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/Dockerfile.pip +1 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/README.md +1 -1
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_harness.py +99 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_rehearsal_version.py +1 -1
- continuo_python_runtime-0.11.0/uv.lock +920 -0
- continuo_python_runtime-0.10.0/.python-version +0 -1
- continuo_python_runtime-0.10.0/adapters/duckdb/tests/test_integration_duckdb_conformance.py +0 -27
- continuo_python_runtime-0.10.0/image-requirements-duckdb.txt +0 -3
- continuo_python_runtime-0.10.0/image-requirements-postgres.txt +0 -3
- continuo_python_runtime-0.10.0/image-requirements-trino.txt +0 -3
- continuo_python_runtime-0.10.0/uv.lock +0 -895
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.claude/agents/ddd-architecture-auditor.md +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.dockerignore +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/CODEOWNERS +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/dependabot.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/ci.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/dco.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/image-build.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/image-smoke.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/images.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/publish-pypi.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/release.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/security.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.github/workflows/verify-packages.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.gitignore +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/.gitleaks.toml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/CODE_OF_CONDUCT.md +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/CONTRIBUTING.md +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/DCO +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/LICENSE +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/NOTICE +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/SECURITY.md +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/adapter.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/application/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/application/ports.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/application/warehouse.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/columns.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/identifiers.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/layout.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/ddl.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/extensions.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/passfile.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/settings.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/conftest.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_proxy.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_runtime.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_validation.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_wiring.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_domain_duckdb_columns.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_domain_duckdb_identifiers.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_domain_duckdb_layout.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_infra_duckdb_settings_ddl.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_layout.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_nullability_probe.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_runtime.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_validation.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/continuo_postgres_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/conftest.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_adapter_postgres_validation.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_adapter_runtime_postgres.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_integration_postgres_validation.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/continuo_trino_adapter/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_adapter_runtime_trino.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_adapter_trino_validation.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_adapter_trino_writes.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_runtime_trino.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_trino_merge_probe.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_trino_validation.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/cli.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/closure.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/conform.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/context.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/kinds.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/loader.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/merge.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/model.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/paths.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/secret_ref.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_loader.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_readers/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_readers/https.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_readers/s3.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_source.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/errors.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/hashing.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/lint.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/shape.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/types.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/validation/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/validation/runner.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/validation/s3.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/LICENSE +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/NOTICE +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/config.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/keys.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/result.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/sql.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/testing/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/types.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_config.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_conformance_suite.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_contract_keys.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_contract_types.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_port.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_result.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/contract/tests/test_sql.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/scripts/apply_rehearsal_version.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/scripts/bake_duckdb_extensions.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/scripts/check_version_bumps.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/scripts/security-scan.sh +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/scripts/verify_installed_packages.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/.github/workflows/release.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/contracts/example.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/contracts/example_csv.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/requirements.lock +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/template/scripts/example.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/conftest.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/contract/__init__.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/contract/test_kinds.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/contract/test_loader.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/contract/test_merge.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/contract/test_model.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/contract/test_wire_contract_write_modes.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/fixtures/wire_contract_write_modes.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/smoke/duckdb-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/smoke/node_smoke/contracts/smoke.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/smoke/node_smoke/scripts/smoke.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/smoke/postgres-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/smoke/trino-stack/catalog/iceberg.properties +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/smoke/trino-stack/docker-compose.yml +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_adapter_naming.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_check_version_bumps_tags.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_ci_test_coverage.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_cli.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_closure.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_conform.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_context.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_contract_loader.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_contract_pin_consistency.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_csv_loader.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_csv_readers_https.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_csv_readers_integration.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_csv_source.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_errors.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_harness_write_modes.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_hashing.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_image_requirements_sync.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_image_smoke_validation.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_kind_registries.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_lint.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_no_legacy_names.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_package.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_python_version_consistency.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_release_pipeline_structure.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_shape.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_template.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_types.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_validation_runner.py +0 -0
- {continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/tests/test_validation_s3.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14.8
|
|
@@ -5,6 +5,77 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.11.0] - 2026-11-04
|
|
9
|
+
|
|
10
|
+
**TL;DR**
|
|
11
|
+
|
|
12
|
+
- A failed python node's log now says why it failed.
|
|
13
|
+
- Every engine adapter waits up to about 10 s for a warehouse that refuses
|
|
14
|
+
the first connections, as a new Kubernetes pod sees until the cluster's
|
|
15
|
+
network policy admits it. That used to fail the attempt at once.
|
|
16
|
+
- The trino adapter now connects when it is created, so a wrong Trino
|
|
17
|
+
password fails at start-up instead of at the first read.
|
|
18
|
+
- Adapter authors: `StartupConformance` is a new conformance class every
|
|
19
|
+
adapter's integration suite runs.
|
|
20
|
+
|
|
21
|
+
**Upgrade**
|
|
22
|
+
|
|
23
|
+
1. Build your domain image `FROM` the `v0.11.0` engine image (or pin
|
|
24
|
+
`continuo-python-runtime==0.11.0` and your adapter's new version).
|
|
25
|
+
2. Bring-your-own adapter: subclass `StartupConformance` in your integration
|
|
26
|
+
suite and provide the `warehouse_address` and `adapter_from_env` fixtures.
|
|
27
|
+
|
|
28
|
+
Versions: runtime 0.11.0, engine contract 0.9.0, postgres adapter 0.3.2,
|
|
29
|
+
trino adapter 0.4.0, duckdb adapter 0.2.2.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- A failed python node's stored log names its failure. The harness logs every
|
|
34
|
+
runtime error at ERROR on stderr, with the traceback of the exception it
|
|
35
|
+
wraps, as well as printing it in the result block. Continuo cuts the result
|
|
36
|
+
block out of the pod log before storing it, so a failure only printed there,
|
|
37
|
+
such as `LoadError: adapter construction failed: … Connection refused`, was
|
|
38
|
+
missing from the stored log.
|
|
39
|
+
- Every adapter's `from_env` waits out a warehouse that refuses connections
|
|
40
|
+
for the first few seconds. A Job pod's first connection can be refused until
|
|
41
|
+
the cluster's NetworkPolicy engine (k3s's kube-router rejects) has admitted
|
|
42
|
+
the pod; that cost a task retry and a failed Job. A connection that no server
|
|
43
|
+
answered (every address refused, had no route or sat on an unreachable
|
|
44
|
+
network) is tried up to 5 times, 2 s apart, each retry logged. An
|
|
45
|
+
authentication failure, a timeout on any address and every other failure are
|
|
46
|
+
raised at once.
|
|
47
|
+
- postgres: around `psycopg2.connect`.
|
|
48
|
+
- duckdb: around the catalog attach.
|
|
49
|
+
- trino: `from_env` now sends `SELECT 1`, since the trino DBAPI opens no
|
|
50
|
+
connection until the first request. A refusal is retried only if it failed
|
|
51
|
+
within 5 s: urllib3 reports only the last address it tried, so a slower
|
|
52
|
+
one hides an address that timed out. A failed authentication now fails
|
|
53
|
+
`from_env` (a harness `LoadError`) rather than the first read or write.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `StartupConformance` in `continuo_engine_contract.testing.conformance`: every
|
|
58
|
+
adapter's integration suite runs it. It puts the real engine behind a
|
|
59
|
+
loopback port that refuses connections for 3 s (`LateOpeningPort`), then
|
|
60
|
+
checks that `from_env` returns an adapter that answers `SELECT 1`. The new
|
|
61
|
+
fixtures are `warehouse_address` and `adapter_from_env`.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- The python-node env contract in `docs/boundary-contract.md` lists
|
|
66
|
+
`TASK_ATTEMPT`, the 0-based number of the task's attempt, which the executor
|
|
67
|
+
sets on every production Job. The harness does not read it; a node script
|
|
68
|
+
may. Documentation only: no package changes.
|
|
69
|
+
- The engine images build on `python:3.14.8-slim` (was `3.14.2`), and
|
|
70
|
+
`.python-version` and the `template/` Dockerfile follow.
|
|
71
|
+
- `pyarrow` is pinned at `26.0.0` (was `25.0.1`) in the runtime, the engine
|
|
72
|
+
contract's `testing` extra and the three adapters, so they keep resolving
|
|
73
|
+
together.
|
|
74
|
+
- Dependency pins move together across the workspace: `ruff` 0.17.1, `mypy`
|
|
75
|
+
2.4.0, `boto3` 1.43.114, `sqlglot` 30.23.0 (runtime and engine contract),
|
|
76
|
+
`trino` 0.340.0 and `psycopg2-binary` 2.9.13. `urllib3` moves to 2.8.0 in
|
|
77
|
+
`uv.lock` (CVE-2026-97687, CVE-2026-97689).
|
|
78
|
+
|
|
8
79
|
## [0.10.0] - 2026-10-22
|
|
9
80
|
|
|
10
81
|
### Added
|
|
@@ -28,15 +28,17 @@ merged PRs before adding anything new.
|
|
|
28
28
|
conformance suite for engine adapter authors. Its dependencies are installed
|
|
29
29
|
with `continuo-engine-contract[testing]`.
|
|
30
30
|
|
|
31
|
-
- Every adapter MUST satisfy the engine contract and pass
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
- Every adapter MUST satisfy the engine contract and pass `StartupConformance`
|
|
32
|
+
(`from_env` waits out a warehouse that refuses connections at first) and the
|
|
33
|
+
shared conformance classes for every capability it declares:
|
|
34
|
+
`IntrospectionConformance` for `TableIntrospection`, `AppendConformance` for
|
|
35
|
+
`AppendCapable`, and `UpsertConformance` for `UpsertCapable`. Append and
|
|
36
|
+
upsert adapters must also run the introspection class. An adapter need not
|
|
37
|
+
declare capabilities its engine cannot support.
|
|
37
38
|
- Each adapter's integration suite MUST subclass the applicable classes,
|
|
38
|
-
supply connected `adapter` and temporary `schema` fixtures
|
|
39
|
-
|
|
39
|
+
supply connected `adapter` and temporary `schema` fixtures (and, for
|
|
40
|
+
`StartupConformance`, `warehouse_address` and `adapter_from_env`), and run
|
|
41
|
+
them against its real warehouse in CI. In-memory reference tests alone do not
|
|
40
42
|
establish an adapter's conformance. Do not skip or weaken shared checks to
|
|
41
43
|
accommodate an adapter that violates the contract.
|
|
42
44
|
- Every new or changed engine-independent contract requirement, including
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM python:3.14.
|
|
1
|
+
FROM python:3.14.8-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.
|
|
1
|
+
FROM python:3.14.8-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.
|
|
1
|
+
FROM python:3.14.8-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.11.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
|
|
@@ -11,11 +11,11 @@ Classifier: Development Status :: 4 - Beta
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Python: >=3.14
|
|
14
|
-
Requires-Dist: boto3==1.43.
|
|
15
|
-
Requires-Dist: continuo-engine-contract==0.
|
|
16
|
-
Requires-Dist: pyarrow==
|
|
14
|
+
Requires-Dist: boto3==1.43.114
|
|
15
|
+
Requires-Dist: continuo-engine-contract==0.9.0
|
|
16
|
+
Requires-Dist: pyarrow==26.0.0
|
|
17
17
|
Requires-Dist: pyyaml==6.0.3
|
|
18
|
-
Requires-Dist: sqlglot==30.
|
|
18
|
+
Requires-Dist: sqlglot==30.23.0
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# Continuo Python Runtime
|
|
@@ -340,7 +340,10 @@ declared `output_columns` on the table `run()` returned, in this order:
|
|
|
340
340
|
Every runtime failure is one of five `HarnessError` subclasses
|
|
341
341
|
(`continuo_python_runtime/errors.py`). The sentinel result block's message
|
|
342
342
|
is prefixed `<ErrorClass>: ` so failures can be triaged without parsing free
|
|
343
|
-
text.
|
|
343
|
+
text. The harness also logs the same message at ERROR on stderr, with the
|
|
344
|
+
traceback of the exception it wraps (a driver error, the script's own
|
|
345
|
+
exception), so the stored log of a failed run names its failure: Continuo cuts
|
|
346
|
+
the result block out of the log before storing it.
|
|
344
347
|
|
|
345
348
|
| Class | Meaning | Typical fix target |
|
|
346
349
|
| --- | --- | --- |
|
|
@@ -356,11 +359,11 @@ A domain repo picks its warehouse engine by which base image it builds
|
|
|
356
359
|
`FROM`:
|
|
357
360
|
|
|
358
361
|
```dockerfile
|
|
359
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.
|
|
362
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.11.0
|
|
360
363
|
# or
|
|
361
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.
|
|
364
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.11.0
|
|
362
365
|
# or
|
|
363
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.
|
|
366
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.11.0
|
|
364
367
|
```
|
|
365
368
|
|
|
366
369
|
The engine is part of the image **name**; the tag is the bare version, so
|
|
@@ -320,7 +320,10 @@ declared `output_columns` on the table `run()` returned, in this order:
|
|
|
320
320
|
Every runtime failure is one of five `HarnessError` subclasses
|
|
321
321
|
(`continuo_python_runtime/errors.py`). The sentinel result block's message
|
|
322
322
|
is prefixed `<ErrorClass>: ` so failures can be triaged without parsing free
|
|
323
|
-
text.
|
|
323
|
+
text. The harness also logs the same message at ERROR on stderr, with the
|
|
324
|
+
traceback of the exception it wraps (a driver error, the script's own
|
|
325
|
+
exception), so the stored log of a failed run names its failure: Continuo cuts
|
|
326
|
+
the result block out of the log before storing it.
|
|
324
327
|
|
|
325
328
|
| Class | Meaning | Typical fix target |
|
|
326
329
|
| --- | --- | --- |
|
|
@@ -336,11 +339,11 @@ A domain repo picks its warehouse engine by which base image it builds
|
|
|
336
339
|
`FROM`:
|
|
337
340
|
|
|
338
341
|
```dockerfile
|
|
339
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.
|
|
342
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.11.0
|
|
340
343
|
# or
|
|
341
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.
|
|
344
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.11.0
|
|
342
345
|
# or
|
|
343
|
-
FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.
|
|
346
|
+
FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.11.0
|
|
344
347
|
```
|
|
345
348
|
|
|
346
349
|
The engine is part of the image **name**; the tag is the bare version, so
|
|
@@ -47,9 +47,18 @@ contains a line break, when `PGPASSWORD` is set in the environment (libpq would
|
|
|
47
47
|
prefer it to any passfile), or when no temp file can be created. The `aws`
|
|
48
48
|
extension is only loaded for the AWS credential-chain S3 path.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
The attach is retried, up to 5 attempts in all, for two failures only:
|
|
51
|
+
|
|
52
|
+
- a first attach to a brand-new catalog from several Jobs at once can race on
|
|
53
|
+
DuckLake's metadata creation;
|
|
54
|
+
- a catalog connection that no server answered (every address of the host
|
|
55
|
+
refused, had no route or sat on an unreachable network) is tried again 2 s
|
|
56
|
+
later, each retry logged: a Job pod's first connection can be refused for a
|
|
57
|
+
few seconds after the pod starts, until the cluster's NetworkPolicy engine
|
|
58
|
+
has admitted the pod.
|
|
59
|
+
|
|
60
|
+
Every other connection error, an authentication failure or a connect timeout
|
|
61
|
+
on any address among them, surfaces immediately.
|
|
53
62
|
|
|
54
63
|
## Physical layout (`config`)
|
|
55
64
|
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import contextlib
|
|
15
15
|
import logging
|
|
16
|
+
import re
|
|
16
17
|
import time
|
|
17
18
|
import uuid
|
|
18
19
|
from collections.abc import Callable, Iterator, Sequence
|
|
@@ -52,13 +53,30 @@ logger = logging.getLogger("continuo_duckdb_adapter")
|
|
|
52
53
|
|
|
53
54
|
CATALOG_ALIAS = "lake"
|
|
54
55
|
|
|
56
|
+
# An ATTACH is retried on a fresh connection, up to _ATTACH_ATTEMPTS in all, for
|
|
57
|
+
# two failures only; every other failure is raised at once.
|
|
58
|
+
#
|
|
55
59
|
# DuckLake creates its metadata tables on the first ATTACH of a fresh catalog
|
|
56
60
|
# and does not guard that against a concurrent first ATTACH (several Jobs of one
|
|
57
61
|
# run starting together): all but one fail on the duplicate CREATE. The loser
|
|
58
|
-
# retries
|
|
59
|
-
#
|
|
62
|
+
# retries and then finds the catalog initialised.
|
|
63
|
+
#
|
|
64
|
+
# A Job pod's first connection to the catalog can be refused for a few seconds
|
|
65
|
+
# after the pod starts, until the cluster's NetworkPolicy engine has admitted
|
|
66
|
+
# the pod: an engine that rejects (k3s's kube-router) refuses at once instead
|
|
67
|
+
# of dropping the packets. So a connection no server answered, refused or
|
|
68
|
+
# unroutable, is retried. A connect timeout is not: an engine that drops
|
|
69
|
+
# packets only delays the first connection, which TCP's own retransmissions
|
|
70
|
+
# carry through, so a timeout means the catalog stayed silent for the whole
|
|
71
|
+
# timeout and every retry would wait that long again.
|
|
60
72
|
_ATTACH_ATTEMPTS = 5
|
|
61
73
|
_ATTACH_BACKOFF_SECONDS = 0.2
|
|
74
|
+
_UNREACHABLE_RETRY_SECONDS = 2.0
|
|
75
|
+
# The ATTACH error carries libpq's text for the catalog connection: one line per
|
|
76
|
+
# address it tried, the reason after "failed: " (libpq 14 and later) or after
|
|
77
|
+
# "could not connect to server: " (libpq 13 and earlier).
|
|
78
|
+
_ADDRESS_FAILURE = re.compile(r"(?:failed|could not connect to server): ([^\n]*)")
|
|
79
|
+
_UNREACHABLE_REASONS = frozenset({"Connection refused", "No route to host", "Network is unreachable"})
|
|
62
80
|
|
|
63
81
|
|
|
64
82
|
def _is_first_attach_race(exc: duckdb.Error) -> bool:
|
|
@@ -68,6 +86,25 @@ def _is_first_attach_race(exc: duckdb.Error) -> bool:
|
|
|
68
86
|
)
|
|
69
87
|
|
|
70
88
|
|
|
89
|
+
def _is_catalog_unreachable(exc: duckdb.Error) -> bool:
|
|
90
|
+
"""Whether *exc* says that every address of the catalog host refused or could not be routed to.
|
|
91
|
+
|
|
92
|
+
A host name can have several addresses, and libpq reports each one it
|
|
93
|
+
tried. One that timed out means a retry would wait out its timeout again;
|
|
94
|
+
a FATAL line came from a server that answered (a failed authentication, an
|
|
95
|
+
unknown database). libpq 13 and earlier print a timeout as a bare
|
|
96
|
+
"timeout expired" line, with no prefix.
|
|
97
|
+
"""
|
|
98
|
+
message = str(exc)
|
|
99
|
+
reasons = [reason.strip() for reason in _ADDRESS_FAILURE.findall(message)]
|
|
100
|
+
return (
|
|
101
|
+
bool(reasons)
|
|
102
|
+
and all(reason in _UNREACHABLE_REASONS for reason in reasons)
|
|
103
|
+
and "FATAL:" not in message
|
|
104
|
+
and "timeout expired" not in message
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
71
108
|
def _load_extension(con: "duckdb.DuckDBPyConnection", name: str) -> None:
|
|
72
109
|
try:
|
|
73
110
|
con.execute(load_extension(name))
|
|
@@ -135,6 +172,14 @@ class DuckLakeSession(LakeGateway):
|
|
|
135
172
|
logger.info("first attach of a fresh catalog raced (attempt %d); retrying", attempt)
|
|
136
173
|
sleep(_ATTACH_BACKOFF_SECONDS * attempt)
|
|
137
174
|
continue
|
|
175
|
+
if attempt < _ATTACH_ATTEMPTS and _is_catalog_unreachable(exc):
|
|
176
|
+
logger.warning(
|
|
177
|
+
"catalog not reachable yet (attempt %d of %d), retrying in %.0f s: %s",
|
|
178
|
+
attempt, _ATTACH_ATTEMPTS, _UNREACHABLE_RETRY_SECONDS,
|
|
179
|
+
_redact(exc, secrets).strip(),
|
|
180
|
+
)
|
|
181
|
+
sleep(_UNREACHABLE_RETRY_SECONDS)
|
|
182
|
+
continue
|
|
138
183
|
raise _translate(exc, secrets) from None
|
|
139
184
|
return cls(con, DdlRenderer(CATALOG_ALIAS), secrets=secrets, passfile=passfile)
|
|
140
185
|
except BaseException:
|
{continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/pyproject.toml
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[project]
|
|
2
|
-
version = "0.2.
|
|
2
|
+
version = "0.2.2"
|
|
3
3
|
name = "continuo-duckdb-adapter"
|
|
4
4
|
description = "DuckDB (DuckLake) warehouse adapter for Continuo: validation and python-node runtime."
|
|
5
5
|
authors = [{ name = "Simone Carolini" }]
|
|
@@ -10,9 +10,9 @@ dependencies = [
|
|
|
10
10
|
# Pinned exactly for the same reason as the postgres adapter: the
|
|
11
11
|
# [tool.uv.sources] redirect below is uv-only and never reaches built
|
|
12
12
|
# metadata, and this adapter subclasses the contract's port directly.
|
|
13
|
-
"continuo-engine-contract==0.
|
|
13
|
+
"continuo-engine-contract==0.9.0",
|
|
14
14
|
"duckdb==1.5.6",
|
|
15
|
-
"pyarrow==
|
|
15
|
+
"pyarrow==26.0.0",
|
|
16
16
|
]
|
|
17
17
|
classifiers = [
|
|
18
18
|
"Development Status :: 4 - Beta",
|
|
@@ -25,16 +25,16 @@ duckdb = "continuo_duckdb_adapter.adapter:DuckDBAdapter"
|
|
|
25
25
|
|
|
26
26
|
[dependency-groups]
|
|
27
27
|
dev = [
|
|
28
|
-
"ruff==0.
|
|
29
|
-
"mypy==2.
|
|
28
|
+
"ruff==0.17.1",
|
|
29
|
+
"mypy==2.4.0",
|
|
30
30
|
]
|
|
31
31
|
test = [
|
|
32
32
|
"pytest==9.1.1",
|
|
33
33
|
"pytest-cov==7.1.0",
|
|
34
34
|
# Integration tests read the DuckLake catalog (postgres) and the data
|
|
35
35
|
# files (MinIO) directly to prove layout was really applied.
|
|
36
|
-
"psycopg2-binary==2.9.
|
|
37
|
-
"boto3==1.43.
|
|
36
|
+
"psycopg2-binary==2.9.13",
|
|
37
|
+
"boto3==1.43.114",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
40
|
[build-system]
|
|
@@ -298,12 +298,75 @@ def test_a_persistent_init_race_surfaces_after_bounded_attempts(connect_with):
|
|
|
298
298
|
assert all(c.closed for c in script.connections)
|
|
299
299
|
|
|
300
300
|
|
|
301
|
+
# --- a catalog the pod cannot reach yet --------------------------------------
|
|
302
|
+
#
|
|
303
|
+
# A Job pod's first connection can be refused for a few seconds after the pod
|
|
304
|
+
# starts, while the cluster's NetworkPolicy engine has not yet admitted it. The
|
|
305
|
+
# ATTACH reports libpq's text for the catalog connection; these are DuckDB's
|
|
306
|
+
# messages as it prints them.
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _catalog_unreachable(reason: str) -> duckdb.IOException:
|
|
310
|
+
conninfo = "host=catalog port=5432 dbname=catalog user=continuo"
|
|
311
|
+
return duckdb.IOException(
|
|
312
|
+
f'IO Error: Failed to attach DuckLake MetaData "__ducklake_metadata_lake" at path + '
|
|
313
|
+
f'"postgres:{conninfo}"Unable to connect to Postgres at "{conninfo}": connection to '
|
|
314
|
+
f'server at "catalog" (10.43.0.7), port 5432 failed: {reason}\n'
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
REFUSED = "Connection refused\n\tIs the server running on that host and accepting TCP/IP connections?"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@pytest.mark.parametrize("reason", [
|
|
322
|
+
REFUSED, "No route to host", "Network is unreachable",
|
|
323
|
+
f'{REFUSED}\nconnection to server at "catalog" (10.43.0.8), port 5432 failed: {REFUSED}',
|
|
324
|
+
], ids=["refused", "no-route", "net-unreachable", "refused-twice"])
|
|
325
|
+
def test_an_unreachable_catalog_is_retried_on_a_fresh_connection(connect_with, reason):
|
|
326
|
+
script, sleeps, connect = connect_with([_catalog_unreachable(reason)] * 2 + [None])
|
|
327
|
+
session = connect()
|
|
328
|
+
assert len(script.connections) == 3
|
|
329
|
+
assert [c.closed for c in script.connections] == [True, True, False]
|
|
330
|
+
assert sleeps == [2.0, 2.0]
|
|
331
|
+
session.close()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_a_catalog_that_stays_unreachable_fails_after_bounded_attempts(connect_with):
|
|
335
|
+
from continuo_duckdb_adapter.infrastructure import session as session_module
|
|
336
|
+
|
|
337
|
+
script, sleeps, connect = connect_with([_catalog_unreachable(REFUSED)] * 50)
|
|
338
|
+
with pytest.raises(duckdb.IOException, match="Connection refused"):
|
|
339
|
+
connect()
|
|
340
|
+
assert len(script.connections) == session_module._ATTACH_ATTEMPTS == 5
|
|
341
|
+
assert sleeps == [2.0, 2.0, 2.0, 2.0]
|
|
342
|
+
assert all(c.closed for c in script.connections)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_each_catalog_retry_is_logged_redacted(connect_with, caplog):
|
|
346
|
+
leaking = duckdb.IOException(f"{_catalog_unreachable(REFUSED)} password={PASSWORD}")
|
|
347
|
+
_, _, connect = connect_with([leaking, leaking, None])
|
|
348
|
+
caplog.set_level(logging.INFO, logger="continuo_duckdb_adapter")
|
|
349
|
+
connect().close()
|
|
350
|
+
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
|
351
|
+
assert len(warnings) == 2
|
|
352
|
+
assert all("port 5432 failed: Connection refused" in r.getMessage() for r in warnings)
|
|
353
|
+
assert PASSWORD not in caplog.text
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# A catalog host with two addresses, one refused and one silent: the ATTACH
|
|
357
|
+
# error carries both of libpq's lines.
|
|
358
|
+
REFUSED_THEN_TIMED_OUT = (
|
|
359
|
+
f'{REFUSED}\nconnection to server at "catalog" (10.43.0.8), port 5432 failed: timeout expired'
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
301
363
|
@pytest.mark.parametrize("error", [
|
|
302
|
-
|
|
303
|
-
|
|
364
|
+
_catalog_unreachable('FATAL: password authentication failed for user "continuo"'),
|
|
365
|
+
_catalog_unreachable("timeout expired"),
|
|
366
|
+
_catalog_unreachable(REFUSED_THEN_TIMED_OUT),
|
|
304
367
|
duckdb.Error("Failed to initialize DuckLake: disk full"),
|
|
305
368
|
duckdb.CatalogException("something else"),
|
|
306
|
-
])
|
|
369
|
+
], ids=["auth-failed", "timed-out", "refused-then-timed-out", "init-failed", "catalog-error"])
|
|
307
370
|
def test_every_other_attach_error_is_not_retried(connect_with, error):
|
|
308
371
|
script, sleeps, connect = connect_with([error])
|
|
309
372
|
with pytest.raises(type(error)):
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The engine-contract conformance suite against a real DuckLake (tests/smoke/duckdb-stack).
|
|
2
|
+
|
|
3
|
+
The `adapter`, `schema` and `lake_env` fixtures come from this directory's
|
|
4
|
+
conftest.py. The first connection ``from_env`` opens is the catalog's, so the
|
|
5
|
+
startup case puts the catalog behind the refusing port.
|
|
6
|
+
Run the stack first:
|
|
7
|
+
docker compose -f tests/smoke/duckdb-stack/docker-compose.yml up -d --wait
|
|
8
|
+
"""
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from continuo_engine_contract.testing.conformance import (
|
|
12
|
+
AppendConformance,
|
|
13
|
+
IntrospectionConformance,
|
|
14
|
+
StartupConformance,
|
|
15
|
+
UpsertConformance,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
pytestmark = pytest.mark.integration
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def warehouse_address(lake_env):
|
|
23
|
+
return "127.0.0.1", int(lake_env["DUCKDB_CATALOG_PORT"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def adapter_from_env(lake_env, monkeypatch):
|
|
28
|
+
from continuo_duckdb_adapter.adapter import DuckDBAdapter
|
|
29
|
+
|
|
30
|
+
def build(host, port):
|
|
31
|
+
for key, value in {**lake_env, "DUCKDB_CATALOG_HOST": host, "DUCKDB_CATALOG_PORT": str(port)}.items():
|
|
32
|
+
monkeypatch.setenv(key, value)
|
|
33
|
+
return DuckDBAdapter.from_env()
|
|
34
|
+
|
|
35
|
+
return build
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestDuckDBStartup(StartupConformance):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestDuckDBIntrospection(IntrospectionConformance):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestDuckDBAppend(AppendConformance):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestDuckDBUpsert(UpsertConformance):
|
|
51
|
+
pass
|
|
@@ -20,14 +20,26 @@ def test_ensure_schema_creates_a_schema_visible_to_another_connection(adapter, a
|
|
|
20
20
|
assert other.fetch(f'SELECT count(*) AS n FROM "{schema}"."t"').to_pylist() == [{"n": 0}]
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def
|
|
24
|
-
with
|
|
25
|
-
|
|
23
|
+
def _connect(lake_env, sleeps, **overrides):
|
|
24
|
+
"""DuckDBAdapter.from_env's session, with its retry waits recorded instead of slept."""
|
|
25
|
+
from continuo_duckdb_adapter.infrastructure.session import DuckLakeSession
|
|
26
|
+
from continuo_duckdb_adapter.infrastructure.settings import DuckLakeSettings
|
|
27
|
+
|
|
28
|
+
return DuckLakeSession.connect(DuckLakeSettings.from_env({**lake_env, **overrides}), sleep=sleeps.append)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_unreachable_catalog_is_retried_then_raises_a_clear_error(lake_env):
|
|
32
|
+
sleeps: list[float] = []
|
|
33
|
+
with pytest.raises(duckdb.Error, match="Connection refused"):
|
|
34
|
+
_connect(lake_env, sleeps, DUCKDB_CATALOG_PORT="1")
|
|
35
|
+
assert sleeps == [2.0, 2.0, 2.0, 2.0]
|
|
26
36
|
|
|
27
37
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
def test_wrong_catalog_password_raises_a_clear_error_at_once(lake_env):
|
|
39
|
+
sleeps: list[float] = []
|
|
40
|
+
with pytest.raises(duckdb.Error, match="password authentication failed"):
|
|
41
|
+
_connect(lake_env, sleeps, DUCKDB_CATALOG_PASSWORD="definitely-wrong")
|
|
42
|
+
assert sleeps == []
|
|
31
43
|
|
|
32
44
|
|
|
33
45
|
def test_failed_attach_does_not_echo_the_password(adapter_factory):
|
{continuo_python_runtime-0.10.0 → continuo_python_runtime-0.11.0}/adapters/postgres/README.md
RENAMED
|
@@ -12,6 +12,13 @@ registers itself under entry-point group `continuo_engine.adapters` as
|
|
|
12
12
|
Connection env: `POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_USER` (required);
|
|
13
13
|
`POSTGRES_PORT` (default 5432), `POSTGRES_PASSWORD` (default empty).
|
|
14
14
|
|
|
15
|
+
`from_env` tries a connection that no server answered (every address of the
|
|
16
|
+
host refused, had no route or sat on an unreachable network) up to 5 times,
|
|
17
|
+
2 s apart, logging each retry: a Job pod's first connection can be refused
|
|
18
|
+
for a few seconds after the pod starts, until the cluster's NetworkPolicy
|
|
19
|
+
engine has admitted the pod. Every other failure, an authentication failure or
|
|
20
|
+
a connect timeout on any address among them, is raised at once.
|
|
21
|
+
|
|
15
22
|
The connection runs with `autocommit = True`; every method owns its
|
|
16
23
|
transaction explicitly. `load()` and `ensure_table()` open an explicit
|
|
17
24
|
`BEGIN` around their statements and `COMMIT`/`ROLLBACK` themselves, so a
|
|
@@ -28,8 +28,10 @@ import contextlib
|
|
|
28
28
|
import hashlib
|
|
29
29
|
import logging
|
|
30
30
|
import os
|
|
31
|
+
import re
|
|
32
|
+
import time
|
|
31
33
|
|
|
32
|
-
from collections.abc import Iterator, Sequence
|
|
34
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
33
35
|
from typing import Any
|
|
34
36
|
|
|
35
37
|
import psycopg2 # type: ignore[import-untyped]
|
|
@@ -79,6 +81,42 @@ _DEFAULT_INDEX_TYPE = "btree"
|
|
|
79
81
|
# postgres actually stores.
|
|
80
82
|
_MAX_IDENTIFIER_BYTES = 63
|
|
81
83
|
|
|
84
|
+
# A Job pod's first connection can be refused for a few seconds after the pod
|
|
85
|
+
# starts, until the cluster's NetworkPolicy engine has admitted the pod: an
|
|
86
|
+
# engine that rejects (k3s's kube-router) refuses at once instead of dropping
|
|
87
|
+
# the packets. So a connection the server never answered, refused or
|
|
88
|
+
# unroutable, is tried again. A connect timeout is not: an engine that drops
|
|
89
|
+
# packets only delays the first connection, which TCP's own retransmissions
|
|
90
|
+
# carry through, so a timeout means the server stayed silent for the whole
|
|
91
|
+
# timeout and every retry would wait that long again.
|
|
92
|
+
_CONNECT_ATTEMPTS = 5
|
|
93
|
+
_CONNECT_RETRY_SECONDS = 2.0
|
|
94
|
+
# libpq writes one line per address it tried, the reason after "failed: "
|
|
95
|
+
# (libpq 14 and later) or after "could not connect to server: " (libpq 13 and
|
|
96
|
+
# earlier); pgcode is None for every connection failure, so the text is all
|
|
97
|
+
# there is.
|
|
98
|
+
_ADDRESS_FAILURE = re.compile(r"(?:failed|could not connect to server): ([^\n]*)")
|
|
99
|
+
_UNREACHABLE_REASONS = frozenset({"Connection refused", "No route to host", "Network is unreachable"})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_unreachable(exc: "psycopg2.OperationalError") -> bool:
|
|
103
|
+
"""Whether *exc* says that every address of the host refused or could not be routed to.
|
|
104
|
+
|
|
105
|
+
A host name can have several addresses, and libpq reports each one it
|
|
106
|
+
tried. One that timed out means a retry would wait out its timeout again;
|
|
107
|
+
a FATAL line came from a server that answered (a failed authentication, an
|
|
108
|
+
unknown database). libpq 13 and earlier print a timeout as a bare
|
|
109
|
+
"timeout expired" line, with no prefix.
|
|
110
|
+
"""
|
|
111
|
+
message = str(exc)
|
|
112
|
+
reasons = [reason.strip() for reason in _ADDRESS_FAILURE.findall(message)]
|
|
113
|
+
return (
|
|
114
|
+
bool(reasons)
|
|
115
|
+
and all(reason in _UNREACHABLE_REASONS for reason in reasons)
|
|
116
|
+
and "FATAL:" not in message
|
|
117
|
+
and "timeout expired" not in message
|
|
118
|
+
)
|
|
119
|
+
|
|
82
120
|
|
|
83
121
|
def _truncate_explicit_identifier(name: str) -> str:
|
|
84
122
|
"""Truncate an explicit, author-given identifier to at most 63 bytes.
|
|
@@ -418,16 +456,35 @@ class PostgresAdapter(WarehouseAdapter, AppendCapable, UpsertCapable):
|
|
|
418
456
|
return ["POSTGRES_HOST", "POSTGRES_DB", "POSTGRES_USER"]
|
|
419
457
|
|
|
420
458
|
@classmethod
|
|
421
|
-
def from_env(cls) -> "PostgresAdapter":
|
|
422
|
-
"""Connect from POSTGRES_* env (port defaults 5432, password empty).
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
459
|
+
def from_env(cls, *, sleep: Callable[[float], None] = time.sleep) -> "PostgresAdapter":
|
|
460
|
+
"""Connect from POSTGRES_* env (port defaults 5432, password empty).
|
|
461
|
+
|
|
462
|
+
A connection no server answered (refused, no route, network
|
|
463
|
+
unreachable) is tried again, up to ``_CONNECT_ATTEMPTS`` times in all,
|
|
464
|
+
``_CONNECT_RETRY_SECONDS`` apart, each retry logged; the last failure
|
|
465
|
+
is raised. Every other failure is raised at once.
|
|
466
|
+
"""
|
|
467
|
+
params = {
|
|
468
|
+
"host": os.environ["POSTGRES_HOST"],
|
|
469
|
+
"port": os.environ.get("POSTGRES_PORT", "5432"),
|
|
470
|
+
"dbname": os.environ["POSTGRES_DB"],
|
|
471
|
+
"user": os.environ["POSTGRES_USER"],
|
|
472
|
+
"password": os.environ.get("POSTGRES_PASSWORD", ""),
|
|
473
|
+
}
|
|
474
|
+
for attempt in range(1, _CONNECT_ATTEMPTS + 1):
|
|
475
|
+
try:
|
|
476
|
+
conn = psycopg2.connect(**params)
|
|
477
|
+
except psycopg2.OperationalError as exc:
|
|
478
|
+
if attempt == _CONNECT_ATTEMPTS or not _is_unreachable(exc):
|
|
479
|
+
raise
|
|
480
|
+
logger.warning(
|
|
481
|
+
"postgres not reachable yet (attempt %d of %d), retrying in %.0f s: %s",
|
|
482
|
+
attempt, _CONNECT_ATTEMPTS, _CONNECT_RETRY_SECONDS, str(exc).strip(),
|
|
483
|
+
)
|
|
484
|
+
sleep(_CONNECT_RETRY_SECONDS)
|
|
485
|
+
continue
|
|
486
|
+
return cls(conn)
|
|
487
|
+
raise AssertionError("unreachable") # pragma: no cover
|
|
431
488
|
|
|
432
489
|
# --- Schema lifecycle ---------------------------------------------------
|
|
433
490
|
|