continuo-python-runtime 0.8.0__tar.gz → 0.9.0__tar.gz

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