continuo-python-runtime 0.9.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.
Files changed (212) hide show
  1. continuo_python_runtime-0.11.0/.python-version +1 -0
  2. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/CHANGELOG.md +93 -0
  3. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/CLAUDE.md +10 -8
  4. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/Dockerfile.duckdb +1 -1
  5. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/Dockerfile.postgres +1 -1
  6. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/Dockerfile.trino +1 -1
  7. continuo_python_runtime-0.9.0/README.md → continuo_python_runtime-0.11.0/PKG-INFO +36 -4
  8. continuo_python_runtime-0.9.0/PKG-INFO → continuo_python_runtime-0.11.0/README.md +16 -24
  9. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/README.md +12 -3
  10. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/session.py +47 -2
  11. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/pyproject.toml +7 -7
  12. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_infra_duckdb_session.py +66 -3
  13. continuo_python_runtime-0.11.0/adapters/duckdb/tests/test_integration_duckdb_conformance.py +51 -0
  14. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_stack.py +18 -6
  15. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/README.md +7 -0
  16. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/continuo_postgres_adapter/adapter.py +69 -12
  17. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/pyproject.toml +6 -6
  18. continuo_python_runtime-0.11.0/adapters/postgres/tests/test_adapter_postgres_connect.py +209 -0
  19. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_integration_postgres_conformance.py +23 -0
  20. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_integration_runtime_postgres.py +29 -0
  21. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/README.md +11 -0
  22. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/continuo_trino_adapter/adapter.py +84 -2
  23. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/pyproject.toml +6 -6
  24. continuo_python_runtime-0.11.0/adapters/trino/tests/test_adapter_trino_connect.py +217 -0
  25. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_trino_conformance.py +24 -0
  26. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/loader.py +17 -1
  27. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/merge.py +4 -0
  28. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/model.py +1 -0
  29. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/harness.py +8 -0
  30. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/hashing.py +8 -3
  31. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/README.md +1 -1
  32. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/keys.py +3 -3
  33. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/port.py +9 -1
  34. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/testing/conformance.py +130 -5
  35. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/pyproject.toml +3 -3
  36. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_contract_packaging.py +1 -1
  37. continuo_python_runtime-0.11.0/contract/tests/test_startup_conformance.py +167 -0
  38. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/docs/boundary-contract.md +17 -3
  39. continuo_python_runtime-0.11.0/image-requirements-duckdb.txt +3 -0
  40. continuo_python_runtime-0.11.0/image-requirements-postgres.txt +3 -0
  41. continuo_python_runtime-0.11.0/image-requirements-trino.txt +3 -0
  42. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/pyproject.toml +7 -7
  43. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/Dockerfile +1 -1
  44. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/Dockerfile.pip +1 -1
  45. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/README.md +1 -1
  46. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/contract/test_loader.py +20 -1
  47. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/contract/test_merge.py +22 -0
  48. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_harness.py +99 -0
  49. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_hashing.py +7 -0
  50. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_rehearsal_version.py +1 -1
  51. continuo_python_runtime-0.11.0/uv.lock +920 -0
  52. continuo_python_runtime-0.9.0/.python-version +0 -1
  53. continuo_python_runtime-0.9.0/adapters/duckdb/tests/test_integration_duckdb_conformance.py +0 -27
  54. continuo_python_runtime-0.9.0/image-requirements-duckdb.txt +0 -3
  55. continuo_python_runtime-0.9.0/image-requirements-postgres.txt +0 -3
  56. continuo_python_runtime-0.9.0/image-requirements-trino.txt +0 -3
  57. continuo_python_runtime-0.9.0/uv.lock +0 -895
  58. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.claude/agents/ddd-architecture-auditor.md +0 -0
  59. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.dockerignore +0 -0
  60. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/CODEOWNERS +0 -0
  61. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  62. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  63. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  64. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  65. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/dependabot.yml +0 -0
  66. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/ci.yml +0 -0
  67. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/dco.yml +0 -0
  68. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/image-build.yml +0 -0
  69. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/image-smoke.yml +0 -0
  70. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/images.yml +0 -0
  71. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/publish-pypi.yml +0 -0
  72. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/release.yml +0 -0
  73. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/security.yml +0 -0
  74. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.github/workflows/verify-packages.yml +0 -0
  75. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.gitignore +0 -0
  76. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/.gitleaks.toml +0 -0
  77. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/CODE_OF_CONDUCT.md +0 -0
  78. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/CONTRIBUTING.md +0 -0
  79. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/DCO +0 -0
  80. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/LICENSE +0 -0
  81. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/NOTICE +0 -0
  82. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/SECURITY.md +0 -0
  83. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/__init__.py +0 -0
  84. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/adapter.py +0 -0
  85. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/application/__init__.py +0 -0
  86. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/application/ports.py +0 -0
  87. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/application/warehouse.py +0 -0
  88. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/__init__.py +0 -0
  89. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/columns.py +0 -0
  90. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/identifiers.py +0 -0
  91. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/domain/layout.py +0 -0
  92. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/__init__.py +0 -0
  93. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/ddl.py +0 -0
  94. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/extensions.py +0 -0
  95. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/passfile.py +0 -0
  96. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/continuo_duckdb_adapter/infrastructure/settings.py +0 -0
  97. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/__init__.py +0 -0
  98. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/conftest.py +0 -0
  99. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_proxy.py +0 -0
  100. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_runtime.py +0 -0
  101. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_validation.py +0 -0
  102. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_adapter_duckdb_wiring.py +0 -0
  103. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_domain_duckdb_columns.py +0 -0
  104. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_domain_duckdb_identifiers.py +0 -0
  105. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_domain_duckdb_layout.py +0 -0
  106. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_infra_duckdb_settings_ddl.py +0 -0
  107. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_layout.py +0 -0
  108. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_nullability_probe.py +0 -0
  109. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_runtime.py +0 -0
  110. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/duckdb/tests/test_integration_duckdb_validation.py +0 -0
  111. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/continuo_postgres_adapter/__init__.py +0 -0
  112. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/__init__.py +0 -0
  113. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/conftest.py +0 -0
  114. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_adapter_postgres_validation.py +0 -0
  115. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_adapter_runtime_postgres.py +0 -0
  116. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/postgres/tests/test_integration_postgres_validation.py +0 -0
  117. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/continuo_trino_adapter/__init__.py +0 -0
  118. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/__init__.py +0 -0
  119. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_adapter_runtime_trino.py +0 -0
  120. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_adapter_trino_validation.py +0 -0
  121. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_adapter_trino_writes.py +0 -0
  122. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_runtime_trino.py +0 -0
  123. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_trino_merge_probe.py +0 -0
  124. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/adapters/trino/tests/test_integration_trino_validation.py +0 -0
  125. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/__init__.py +0 -0
  126. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/cli.py +0 -0
  127. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/closure.py +0 -0
  128. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/conform.py +0 -0
  129. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/context.py +0 -0
  130. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/__init__.py +0 -0
  131. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/kinds.py +0 -0
  132. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/paths.py +0 -0
  133. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/contract/secret_ref.py +0 -0
  134. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_loader.py +0 -0
  135. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_readers/__init__.py +0 -0
  136. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_readers/https.py +0 -0
  137. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_readers/s3.py +0 -0
  138. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/csv_source.py +0 -0
  139. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/errors.py +0 -0
  140. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/lint.py +0 -0
  141. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/shape.py +0 -0
  142. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/types.py +0 -0
  143. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/validation/__init__.py +0 -0
  144. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/validation/runner.py +0 -0
  145. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/continuo_python_runtime/validation/s3.py +0 -0
  146. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/LICENSE +0 -0
  147. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/NOTICE +0 -0
  148. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/__init__.py +0 -0
  149. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/config.py +0 -0
  150. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/result.py +0 -0
  151. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/sql.py +0 -0
  152. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/testing/__init__.py +0 -0
  153. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/continuo_engine_contract/types.py +0 -0
  154. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/__init__.py +0 -0
  155. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_config.py +0 -0
  156. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_conformance_suite.py +0 -0
  157. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_contract_keys.py +0 -0
  158. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_contract_types.py +0 -0
  159. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_port.py +0 -0
  160. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_result.py +0 -0
  161. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/contract/tests/test_sql.py +0 -0
  162. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/scripts/apply_rehearsal_version.py +0 -0
  163. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/scripts/bake_duckdb_extensions.py +0 -0
  164. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/scripts/check_version_bumps.py +0 -0
  165. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/scripts/security-scan.sh +0 -0
  166. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/scripts/verify_installed_packages.py +0 -0
  167. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/.github/workflows/release.yml +0 -0
  168. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/contracts/example.yml +0 -0
  169. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/contracts/example_csv.yml +0 -0
  170. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/requirements.lock +0 -0
  171. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/template/scripts/example.py +0 -0
  172. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/__init__.py +0 -0
  173. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/conftest.py +0 -0
  174. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/contract/__init__.py +0 -0
  175. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/contract/test_kinds.py +0 -0
  176. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/contract/test_model.py +0 -0
  177. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/contract/test_wire_contract_write_modes.py +0 -0
  178. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/fixtures/wire_contract_write_modes.yml +0 -0
  179. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/smoke/duckdb-stack/docker-compose.yml +0 -0
  180. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/smoke/node_smoke/contracts/smoke.yml +0 -0
  181. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/smoke/node_smoke/scripts/smoke.py +0 -0
  182. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/smoke/postgres-stack/docker-compose.yml +0 -0
  183. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/smoke/trino-stack/catalog/iceberg.properties +0 -0
  184. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/smoke/trino-stack/docker-compose.yml +0 -0
  185. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_adapter_naming.py +0 -0
  186. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_check_version_bumps_tags.py +0 -0
  187. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_ci_test_coverage.py +0 -0
  188. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_cli.py +0 -0
  189. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_closure.py +0 -0
  190. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_conform.py +0 -0
  191. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_context.py +0 -0
  192. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_contract_loader.py +0 -0
  193. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_contract_pin_consistency.py +0 -0
  194. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_csv_loader.py +0 -0
  195. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_csv_readers_https.py +0 -0
  196. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_csv_readers_integration.py +0 -0
  197. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_csv_source.py +0 -0
  198. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_errors.py +0 -0
  199. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_harness_write_modes.py +0 -0
  200. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_image_requirements_sync.py +0 -0
  201. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_image_smoke_validation.py +0 -0
  202. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_kind_registries.py +0 -0
  203. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_lint.py +0 -0
  204. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_no_legacy_names.py +0 -0
  205. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_package.py +0 -0
  206. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_python_version_consistency.py +0 -0
  207. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_release_pipeline_structure.py +0 -0
  208. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_shape.py +0 -0
  209. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_template.py +0 -0
  210. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_types.py +0 -0
  211. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_validation_runner.py +0 -0
  212. {continuo_python_runtime-0.9.0 → continuo_python_runtime-0.11.0}/tests/test_validation_s3.py +0 -0
@@ -0,0 +1 @@
1
+ 3.14.8
@@ -5,6 +5,99 @@ 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
+
79
+ ## [0.10.0] - 2026-10-22
80
+
81
+ ### Added
82
+
83
+ - Contracts may set `timeout_minutes` on any node: an integer from 1 to 10,080
84
+ that is the node's task timeout. Other values are refused as a contract
85
+ error naming the node. The merged wire entry carries the key only when set,
86
+ and it is the one wire key outside `config_hash`: changing it alone leaves
87
+ `config_hash` and `content_hash` unchanged. A node that does not set it has
88
+ the wire entry it always had.
89
+
90
+ ### Changed
91
+
92
+ - The "mode_type not allowed for kind" contract error now quotes the offending
93
+ `mode_type` and `kind` (`repr`), matching the sibling "must be one of" error,
94
+ so an empty or whitespace value is legible. Docstring fixes only alongside:
95
+ the postgres `ensure_table` port-signature reference (0.6.0 → 0.8.0) and the
96
+ `ensure_unique_key` message-order claim (pyarrow grouping order is not
97
+ contractual).
98
+ - The trino adapter (0.3.1) and duckdb adapter (0.2.1) release with their
99
+ `continuo-engine-contract` pin moved to 0.8.1; no behavior change.
100
+
8
101
  ## [0.9.0] - 2026-10-11
9
102
 
10
103
  ### 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 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.
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, and run them
39
- against its real warehouse in CI. In-memory reference tests alone do not
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.2-slim
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.2-slim
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.2-slim
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,3 +1,23 @@
1
+ Metadata-Version: 2.5
2
+ Name: continuo-python-runtime
3
+ Version: 0.11.0
4
+ Summary: Runtime harness, contract tooling, and CI lint for Continuo python nodes.
5
+ Author: Simone Carolini
6
+ Maintainer: Simone Carolini
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Python: >=3.14
14
+ Requires-Dist: boto3==1.43.114
15
+ Requires-Dist: continuo-engine-contract==0.9.0
16
+ Requires-Dist: pyarrow==26.0.0
17
+ Requires-Dist: pyyaml==6.0.3
18
+ Requires-Dist: sqlglot==30.23.0
19
+ Description-Content-Type: text/markdown
20
+
1
21
  # Continuo Python Runtime
2
22
 
3
23
  [![CI](https://github.com/carolsimone/continuo-python-runtime/actions/workflows/ci.yml/badge.svg)](https://github.com/carolsimone/continuo-python-runtime/actions/workflows/ci.yml)
@@ -176,6 +196,15 @@ produced, never how it is checked or written.
176
196
  script reads with `os.environ[...]`. The contract never holds a secret value
177
197
  or env var name; the Secret itself is created in the cluster by the operator.
178
198
 
199
+ ### Task timeout
200
+
201
+ Any node may set `timeout_minutes:` — an integer from 1 to 10,080 (one week) —
202
+ to declare how long its task may run before continuo gives up on it. A value
203
+ outside that range, or one that is not an integer, fails
204
+ `continuo-runtime validate`/`merge`/`lint`. It is the one key that is not part
205
+ of `config_hash`, so changing only the timeout never changes the node's
206
+ `content_hash`.
207
+
179
208
  ## Write modes
180
209
 
181
210
  `mode_type:` chooses how the harness writes a node's table on each run:
@@ -311,7 +340,10 @@ declared `output_columns` on the table `run()` returned, in this order:
311
340
  Every runtime failure is one of five `HarnessError` subclasses
312
341
  (`continuo_python_runtime/errors.py`). The sentinel result block's message
313
342
  is prefixed `<ErrorClass>: ` so failures can be triaged without parsing free
314
- 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.
315
347
 
316
348
  | Class | Meaning | Typical fix target |
317
349
  | --- | --- | --- |
@@ -327,11 +359,11 @@ A domain repo picks its warehouse engine by which base image it builds
327
359
  `FROM`:
328
360
 
329
361
  ```dockerfile
330
- FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.9.0
362
+ FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.11.0
331
363
  # or
332
- FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.9.0
364
+ FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.11.0
333
365
  # or
334
- FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.9.0
366
+ FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.11.0
335
367
  ```
336
368
 
337
369
  The engine is part of the image **name**; the tag is the bare version, so
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: continuo-python-runtime
3
- Version: 0.9.0
4
- Summary: Runtime harness, contract tooling, and CI lint for Continuo python nodes.
5
- Author: Simone Carolini
6
- Maintainer: Simone Carolini
7
- License-Expression: Apache-2.0
8
- License-File: LICENSE
9
- License-File: NOTICE
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Programming Language :: Python :: 3.14
13
- Requires-Python: >=3.14
14
- Requires-Dist: boto3==1.43.85
15
- Requires-Dist: continuo-engine-contract==0.8.0
16
- Requires-Dist: pyarrow==25.0.1
17
- Requires-Dist: pyyaml==6.0.3
18
- Requires-Dist: sqlglot==30.17.0
19
- Description-Content-Type: text/markdown
20
-
21
1
  # Continuo Python Runtime
22
2
 
23
3
  [![CI](https://github.com/carolsimone/continuo-python-runtime/actions/workflows/ci.yml/badge.svg)](https://github.com/carolsimone/continuo-python-runtime/actions/workflows/ci.yml)
@@ -196,6 +176,15 @@ produced, never how it is checked or written.
196
176
  script reads with `os.environ[...]`. The contract never holds a secret value
197
177
  or env var name; the Secret itself is created in the cluster by the operator.
198
178
 
179
+ ### Task timeout
180
+
181
+ Any node may set `timeout_minutes:` — an integer from 1 to 10,080 (one week) —
182
+ to declare how long its task may run before continuo gives up on it. A value
183
+ outside that range, or one that is not an integer, fails
184
+ `continuo-runtime validate`/`merge`/`lint`. It is the one key that is not part
185
+ of `config_hash`, so changing only the timeout never changes the node's
186
+ `content_hash`.
187
+
199
188
  ## Write modes
200
189
 
201
190
  `mode_type:` chooses how the harness writes a node's table on each run:
@@ -331,7 +320,10 @@ declared `output_columns` on the table `run()` returned, in this order:
331
320
  Every runtime failure is one of five `HarnessError` subclasses
332
321
  (`continuo_python_runtime/errors.py`). The sentinel result block's message
333
322
  is prefixed `<ErrorClass>: ` so failures can be triaged without parsing free
334
- 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.
335
327
 
336
328
  | Class | Meaning | Typical fix target |
337
329
  | --- | --- | --- |
@@ -347,11 +339,11 @@ A domain repo picks its warehouse engine by which base image it builds
347
339
  `FROM`:
348
340
 
349
341
  ```dockerfile
350
- FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.9.0
342
+ FROM ghcr.io/carolsimone/continuo-python-runtime-postgres:v0.11.0
351
343
  # or
352
- FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.9.0
344
+ FROM ghcr.io/carolsimone/continuo-python-runtime-trino:v0.11.0
353
345
  # or
354
- FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.9.0
346
+ FROM ghcr.io/carolsimone/continuo-python-runtime-duckdb:v0.11.0
355
347
  ```
356
348
 
357
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
- A first attach to a brand-new catalog from several Jobs at once can race on
51
- DuckLake's metadata creation; the adapter retries exactly that failure a few
52
- times, and surfaces every other connection error immediately.
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 on a fresh connection and then finds the catalog initialised. Bounded,
59
- # and only this failure class is retried.
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:
@@ -1,5 +1,5 @@
1
1
  [project]
2
- version = "0.2.0"
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.8.0",
13
+ "continuo-engine-contract==0.9.0",
14
14
  "duckdb==1.5.6",
15
- "pyarrow==25.0.1",
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.16.5",
29
- "mypy==2.3.1",
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.12",
37
- "boto3==1.43.85",
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
- duckdb.IOException('Unable to connect to Postgres at "host=h": Connection refused'),
303
- duckdb.IOException('Failed to attach DuckLake: FATAL: password authentication failed'),
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 test_unreachable_catalog_raises_a_clear_error(adapter_factory):
24
- with pytest.raises(duckdb.Error):
25
- adapter_factory(DUCKDB_CATALOG_PORT="1")
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 test_wrong_catalog_password_raises_a_clear_error(adapter_factory):
29
- with pytest.raises(duckdb.Error):
30
- adapter_factory(DUCKDB_CATALOG_PASSWORD="definitely-wrong")
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):
@@ -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