vgi-python 0.8.6__tar.gz → 0.8.8__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 (369) hide show
  1. {vgi_python-0.8.6 → vgi_python-0.8.8}/PKG-INFO +2 -2
  2. {vgi_python-0.8.6 → vgi_python-0.8.8}/pyproject.toml +2 -2
  3. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_protocol_inventory.py +9 -0
  4. vgi_python-0.8.8/tests/test_copy_from_function.py +147 -0
  5. vgi_python-0.8.8/tests/test_copy_to_function.py +120 -0
  6. vgi_python-0.8.8/tests/test_resolved_secrets.py +61 -0
  7. vgi_python-0.8.8/tests/test_tcp_transport.py +122 -0
  8. {vgi_python-0.8.6 → vgi_python-0.8.8}/uv.lock +5 -5
  9. vgi_python-0.8.8/vgi/_test_fixtures/copy_from.py +99 -0
  10. vgi_python-0.8.8/vgi/_test_fixtures/copy_to.py +160 -0
  11. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/__init__.py +4 -0
  12. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/pairs.py +107 -1
  13. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/settings.py +57 -2
  14. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/worker.py +11 -0
  15. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/catalog_interface.py +131 -0
  16. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/catalog_mixin.py +23 -7
  17. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/client.py +92 -6
  18. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/_common.py +2 -0
  19. vgi_python-0.8.8/vgi/copy_from_function.py +157 -0
  20. vgi_python-0.8.8/vgi/copy_to_function.py +179 -0
  21. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/meta_worker.py +1 -0
  22. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/protocol.py +71 -0
  23. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/scalar_function.py +22 -4
  24. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/table_function.py +91 -10
  25. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/worker.py +73 -3
  26. {vgi_python-0.8.6 → vgi_python-0.8.8}/.gitattributes +0 -0
  27. {vgi_python-0.8.6 → vgi_python-0.8.8}/.github/dependabot.yml +0 -0
  28. {vgi_python-0.8.6 → vgi_python-0.8.8}/.github/styles/config/vocabularies/VGI/accept.txt +0 -0
  29. {vgi_python-0.8.6 → vgi_python-0.8.8}/.github/workflows/ci.yml +0 -0
  30. {vgi_python-0.8.6 → vgi_python-0.8.8}/.github/workflows/docs.yml +0 -0
  31. {vgi_python-0.8.6 → vgi_python-0.8.8}/.github/workflows/integration.yml +0 -0
  32. {vgi_python-0.8.6 → vgi_python-0.8.8}/.github/workflows/release.yml +0 -0
  33. {vgi_python-0.8.6 → vgi_python-0.8.8}/.gitignore +0 -0
  34. {vgi_python-0.8.6 → vgi_python-0.8.8}/.python-version +0 -0
  35. {vgi_python-0.8.6 → vgi_python-0.8.8}/.vale.ini +0 -0
  36. {vgi_python-0.8.6 → vgi_python-0.8.8}/CLAUDE.md +0 -0
  37. {vgi_python-0.8.6 → vgi_python-0.8.8}/DOCS_ACCEPTANCE_CRITERIA.md +0 -0
  38. {vgi_python-0.8.6 → vgi_python-0.8.8}/DOCS_REVIEW_RUBRIC.md +0 -0
  39. {vgi_python-0.8.6 → vgi_python-0.8.8}/DOCS_USABILITY_TEST.md +0 -0
  40. {vgi_python-0.8.6 → vgi_python-0.8.8}/LICENSE +0 -0
  41. {vgi_python-0.8.6 → vgi_python-0.8.8}/README.md +0 -0
  42. {vgi_python-0.8.6 → vgi_python-0.8.8}/SECURITY.md +0 -0
  43. {vgi_python-0.8.6 → vgi_python-0.8.8}/ci/README.md +0 -0
  44. {vgi_python-0.8.6 → vgi_python-0.8.8}/ci/preprocess-require.awk +0 -0
  45. {vgi_python-0.8.6 → vgi_python-0.8.8}/ci/run-integration.sh +0 -0
  46. {vgi_python-0.8.6 → vgi_python-0.8.8}/dist-vgi/.gitignore +0 -0
  47. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/aggregate-functions.md +0 -0
  48. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/arguments.md +0 -0
  49. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/auth.md +0 -0
  50. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/catalogs.md +0 -0
  51. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/client.md +0 -0
  52. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/exceptions.md +0 -0
  53. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/filters.md +0 -0
  54. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/functions.md +0 -0
  55. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/http.md +0 -0
  56. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/index.md +0 -0
  57. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/metadata.md +0 -0
  58. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/observability.md +0 -0
  59. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/storage.md +0 -0
  60. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/transactor.md +0 -0
  61. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/api/worker.md +0 -0
  62. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/argument-serialization.md +0 -0
  63. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/apple-touch-icon.png +0 -0
  64. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/favicon-16x16.png +0 -0
  65. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/favicon-32x32.png +0 -0
  66. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/favicon.ico +0 -0
  67. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/kinds/aggregate.svg +0 -0
  68. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/kinds/buffering.svg +0 -0
  69. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/kinds/scalar.svg +0 -0
  70. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/kinds/table-in-out.svg +0 -0
  71. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/kinds/table.svg +0 -0
  72. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/logo.png +0 -0
  73. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/assets/social-card.png +0 -0
  74. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/authentication.md +0 -0
  75. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/catalog-interface.md +0 -0
  76. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/cli.md +0 -0
  77. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/column-statistics.md +0 -0
  78. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/concepts/index.md +0 -0
  79. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/contributing-docs.md +0 -0
  80. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/filter-pushdown.md +0 -0
  81. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/generator-api.md +0 -0
  82. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/how-to/catalogs.md +0 -0
  83. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/how-to/function-patterns.md +0 -0
  84. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/how-to/http-auth.md +0 -0
  85. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/how-to/index.md +0 -0
  86. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/how-to/pushdown-and-statistics.md +0 -0
  87. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/how-to/state-storage.md +0 -0
  88. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/index.md +0 -0
  89. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/lifecycle.md +0 -0
  90. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/metadata.md +0 -0
  91. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/overrides/main.html +0 -0
  92. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/robots.txt +0 -0
  93. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/shared-storage.md +0 -0
  94. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/stylesheets/extra.css +0 -0
  95. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/tutorial/index.md +0 -0
  96. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/tutorial/scalar.md +0 -0
  97. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/tutorial/table.md +0 -0
  98. {vgi_python-0.8.6 → vgi_python-0.8.8}/docs/vgi-logo.png +0 -0
  99. {vgi_python-0.8.6 → vgi_python-0.8.8}/examples/calc_scalar_worker.py +0 -0
  100. {vgi_python-0.8.6 → vgi_python-0.8.8}/examples/calc_worker.py +0 -0
  101. {vgi_python-0.8.6 → vgi_python-0.8.8}/examples/filter_worker.py +0 -0
  102. {vgi_python-0.8.6 → vgi_python-0.8.8}/examples/greeting_scalar_worker.py +0 -0
  103. {vgi_python-0.8.6 → vgi_python-0.8.8}/examples/row_count_worker.py +0 -0
  104. {vgi_python-0.8.6 → vgi_python-0.8.8}/examples/series_streaming_worker.py +0 -0
  105. {vgi_python-0.8.6 → vgi_python-0.8.8}/examples/sum_worker.py +0 -0
  106. {vgi_python-0.8.6 → vgi_python-0.8.8}/mkdocs.yml +0 -0
  107. {vgi_python-0.8.6 → vgi_python-0.8.8}/packages/vgi-fixtures/LICENSE +0 -0
  108. {vgi_python-0.8.6 → vgi_python-0.8.8}/packages/vgi-fixtures/README.md +0 -0
  109. {vgi_python-0.8.6 → vgi_python-0.8.8}/packages/vgi-fixtures/pyproject.toml +0 -0
  110. {vgi_python-0.8.6 → vgi_python-0.8.8}/scripts/measure_startup.py +0 -0
  111. {vgi_python-0.8.6 → vgi_python-0.8.8}/scripts/run_all_tests.sh +0 -0
  112. {vgi_python-0.8.6 → vgi_python-0.8.8}/test-data/generate.sh +0 -0
  113. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/__init__.py +0 -0
  114. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/_http_fixtures.py +0 -0
  115. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/__init__.py +0 -0
  116. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_catalog_interface.py +0 -0
  117. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_client_catalog.py +0 -0
  118. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_column_statistics.py +0 -0
  119. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_declarative.py +0 -0
  120. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_example_worker_catalog.py +0 -0
  121. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_integration.py +0 -0
  122. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_required_field_filter_paths.py +0 -0
  123. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_scan_branches.py +0 -0
  124. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_serialization.py +0 -0
  125. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_setting.py +0 -0
  126. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_storage.py +0 -0
  127. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_time_travel.py +0 -0
  128. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/catalog/test_writable_table.py +0 -0
  129. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/client/__init__.py +0 -0
  130. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/client/test_broken_pipe.py +0 -0
  131. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/client/test_cli.py +0 -0
  132. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/client/test_cli_catalog_functions.py +0 -0
  133. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/client/test_worker_debug.py +0 -0
  134. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/__init__.py +0 -0
  135. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/_stub.py +0 -0
  136. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/conftest.py +0 -0
  137. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_accumulate.py +0 -0
  138. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_aggregate.py +0 -0
  139. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_attach.py +0 -0
  140. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_bearer_auth.py +0 -0
  141. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_directory_parity.py +0 -0
  142. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_function_inventory.py +0 -0
  143. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_http_client.py +0 -0
  144. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_http_external_location.py +0 -0
  145. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_http_upload_url.py +0 -0
  146. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_macro.py +0 -0
  147. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_overload.py +0 -0
  148. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_protocol_version.py +0 -0
  149. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_resumable_scan.py +0 -0
  150. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_scalar.py +0 -0
  151. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_scalar_attach_opaque_data.py +0 -0
  152. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_secret.py +0 -0
  153. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_settings.py +0 -0
  154. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_table.py +0 -0
  155. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_table_in_out.py +0 -0
  156. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_view.py +0 -0
  157. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conformance/test_writable.py +0 -0
  158. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/conftest.py +0 -0
  159. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/__init__.py +0 -0
  160. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_bernoulli_function.py +0 -0
  161. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_binary_packet_function.py +0 -0
  162. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_client.py +0 -0
  163. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_conditional_message_function.py +0 -0
  164. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_hash_seed_function.py +0 -0
  165. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_multiply_by_setting_function.py +0 -0
  166. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_multiply_function.py +0 -0
  167. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_random_bytes_function.py +0 -0
  168. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/scalar/test_return_secret_value_function.py +0 -0
  169. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/__init__.py +0 -0
  170. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/__init__.py +0 -0
  171. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_constant_columns_function.py +0 -0
  172. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_double_sequence_function.py +0 -0
  173. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_exception_function.py +0 -0
  174. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_filter_echo_function.py +0 -0
  175. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_logging_function.py +0 -0
  176. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_nested_sequence_function.py +0 -0
  177. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_partitioned_function.py +0 -0
  178. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_projected_data_function.py +0 -0
  179. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_sequence_function.py +0 -0
  180. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_settings_function.py +0 -0
  181. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_struct_settings_function.py +0 -0
  182. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table/generator/test_ten_thousand_function.py +0 -0
  183. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/__init__.py +0 -0
  184. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/generator/__init__.py +0 -0
  185. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/generator/test_buffer_input_function.py +0 -0
  186. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/generator/test_echo_function.py +0 -0
  187. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/generator/test_exception_functions.py +0 -0
  188. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/generator/test_filter_by_setting_function.py +0 -0
  189. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/generator/test_repeat_inputs_function.py +0 -0
  190. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/generator/test_sum_all_columns_function.py +0 -0
  191. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/table_in_out/test_client.py +0 -0
  192. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_access_log_audit.py +0 -0
  193. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_aggregate_function.py +0 -0
  194. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_argument_spec.py +0 -0
  195. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_auth.py +0 -0
  196. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_bind_exceptions.py +0 -0
  197. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_bind_request_at_clause.py +0 -0
  198. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_bound_storage_conformance.py +0 -0
  199. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_catalog_auth_binding.py +0 -0
  200. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_docstrings.py +0 -0
  201. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_documentation_examples.py +0 -0
  202. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_example_function_arg_types.py +0 -0
  203. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_examples_workers.py +0 -0
  204. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_exception_handling.py +0 -0
  205. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_exceptions.py +0 -0
  206. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_filter_pushdown.py +0 -0
  207. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_filter_pushdown_extension.py +0 -0
  208. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_function_storage.py +0 -0
  209. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_function_storage_azure_sql.py +0 -0
  210. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_function_storage_cf_do.py +0 -0
  211. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_function_storage_cf_do_integration.py +0 -0
  212. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_function_storage_conformance.py +0 -0
  213. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_cpp_constants.py +0 -0
  214. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_cpp_protocol_version.py +0 -0
  215. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_cpp_request_builders.py +0 -0
  216. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_cpp_schemas.py +0 -0
  217. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_cpp_secret.py +0 -0
  218. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_go_schemas.py +0 -0
  219. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_protocol_version.py +0 -0
  220. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_schemas_cross_lang.py +0 -0
  221. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_ts_client.py +0 -0
  222. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_generated_ts_schemas.py +0 -0
  223. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_http_demo_storage.py +0 -0
  224. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_http_s3_offload_input.py +0 -0
  225. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_http_s3_offload_output.py +0 -0
  226. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_metadata.py +0 -0
  227. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_mypy_consolidated.py +0 -0
  228. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_nest_tensor.py +0 -0
  229. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_otel.py +0 -0
  230. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_projection_enforcement.py +0 -0
  231. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_projection_repro.py +0 -0
  232. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_protocol_classes.py +0 -0
  233. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_schema_utils.py +0 -0
  234. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_serve.py +0 -0
  235. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_setting_secret_annotations.py +0 -0
  236. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_table_buffering_function.py +0 -0
  237. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_table_function_dynamic_to_string.py +0 -0
  238. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_type_bounds.py +0 -0
  239. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_union_argument.py +0 -0
  240. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_worker.py +0 -0
  241. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_worker_cli.py +0 -0
  242. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/test_worker_page.py +0 -0
  243. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/transactor/__init__.py +0 -0
  244. {vgi_python-0.8.6 → vgi_python-0.8.8}/tests/transactor/test_transactor.py +0 -0
  245. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/__init__.py +0 -0
  246. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_duckdb.py +0 -0
  247. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_storage_profile.py +0 -0
  248. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/__init__.py +0 -0
  249. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/accumulate/__init__.py +0 -0
  250. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/accumulate/worker.py +0 -0
  251. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/__init__.py +0 -0
  252. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/_common.py +0 -0
  253. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/basic.py +0 -0
  254. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/dynamic.py +0 -0
  255. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/generic.py +0 -0
  256. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/listagg.py +0 -0
  257. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/percentile.py +0 -0
  258. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/streaming.py +0 -0
  259. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/varargs.py +0 -0
  260. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/aggregate/window.py +0 -0
  261. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/attach_options.py +0 -0
  262. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/bad_enum.py +0 -0
  263. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/bad_protocol.py +0 -0
  264. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/cancellable.py +0 -0
  265. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/catalog.py +0 -0
  266. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/http_server.py +0 -0
  267. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/narrow_bind/__init__.py +0 -0
  268. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/narrow_bind/worker.py +0 -0
  269. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/nest_tensor.py +0 -0
  270. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/orchard_catalog.py +0 -0
  271. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/projection_repro/__init__.py +0 -0
  272. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/projection_repro/worker.py +0 -0
  273. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/__init__.py +0 -0
  274. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/_common.py +0 -0
  275. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/arithmetic.py +0 -0
  276. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/binary.py +0 -0
  277. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/formatting.py +0 -0
  278. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/geo.py +0 -0
  279. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/null_handling.py +0 -0
  280. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/random_demo.py +0 -0
  281. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/settings_secrets.py +0 -0
  282. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/scalar/type_info.py +0 -0
  283. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/schema_reconcile/__init__.py +0 -0
  284. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/schema_reconcile/worker.py +0 -0
  285. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/simple_writable.py +0 -0
  286. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/_common.py +0 -0
  287. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/batch_index.py +0 -0
  288. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/batch_index_broken.py +0 -0
  289. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/catalog_scans.py +0 -0
  290. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/filters.py +0 -0
  291. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/late_materialization.py +0 -0
  292. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/make_series.py +0 -0
  293. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/misc.py +0 -0
  294. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/order_modes.py +0 -0
  295. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/partition_columns.py +0 -0
  296. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/partition_columns_broken.py +0 -0
  297. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/profiling_example.py +0 -0
  298. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/required_filters.py +0 -0
  299. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/sequence.py +0 -0
  300. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/transaction_storage.py +0 -0
  301. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/tt_pushdown.py +0 -0
  302. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/typed_probe.py +0 -0
  303. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table/versioned.py +0 -0
  304. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/table_in_out.py +0 -0
  305. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/versioned.py +0 -0
  306. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/versioned_tables.py +0 -0
  307. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/writable/__init__.py +0 -0
  308. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/writable/generic.py +0 -0
  309. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/writable/table.py +0 -0
  310. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/_test_fixtures/writable/worker.py +0 -0
  311. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/aggregate_function.py +0 -0
  312. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/argument_spec.py +0 -0
  313. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/arguments.py +0 -0
  314. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/auth.py +0 -0
  315. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/__init__.py +0 -0
  316. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/_descriptor_spec.py +0 -0
  317. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/attach_option.py +0 -0
  318. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/descriptors.py +0 -0
  319. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/duckdb_statistics.py +0 -0
  320. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/secret_type.py +0 -0
  321. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/setting.py +0 -0
  322. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/catalog/storage.py +0 -0
  323. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/__init__.py +0 -0
  324. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/cli.py +0 -0
  325. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/cli_catalog.py +0 -0
  326. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/cli_schema.py +0 -0
  327. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/cli_table.py +0 -0
  328. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/cli_transaction.py +0 -0
  329. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/cli_utils.py +0 -0
  330. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/client/cli_view.py +0 -0
  331. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/__init__.py +0 -0
  332. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/cpp_constants.py +0 -0
  333. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/cpp_protocol_version.py +0 -0
  334. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/cpp_request_builders.py +0 -0
  335. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/cpp_schemas.py +0 -0
  336. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/cpp_secret_protocol_version.py +0 -0
  337. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/cpp_secret_request_builders.py +0 -0
  338. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/cpp_secret_schemas.py +0 -0
  339. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/go_schemas.py +0 -0
  340. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/protocol_version.py +0 -0
  341. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/ts_client.py +0 -0
  342. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/codegen/ts_schemas.py +0 -0
  343. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/exceptions.py +0 -0
  344. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/function.py +0 -0
  345. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/function_storage.py +0 -0
  346. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/function_storage_azure_sql.py +0 -0
  347. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/function_storage_cf_do.py +0 -0
  348. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/http/__init__.py +0 -0
  349. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/http/demo_storage.py +0 -0
  350. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/http/worker_page.py +0 -0
  351. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/invocation.py +0 -0
  352. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/logging_config.py +0 -0
  353. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/metadata.py +0 -0
  354. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/otel.py +0 -0
  355. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/protocol_version.txt +0 -0
  356. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/py.typed +0 -0
  357. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/schema_utils.py +0 -0
  358. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/secret_protocol.py +0 -0
  359. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/secret_service.py +0 -0
  360. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/serve.py +0 -0
  361. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/table_buffering_function.py +0 -0
  362. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/table_filter_pushdown.py +0 -0
  363. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/table_in_out_function.py +0 -0
  364. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/transactor/__init__.py +0 -0
  365. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/transactor/_duckdb_compat.py +0 -0
  366. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/transactor/client.py +0 -0
  367. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/transactor/protocol.py +0 -0
  368. {vgi_python-0.8.6 → vgi_python-0.8.8}/vgi/transactor/server.py +0 -0
  369. {vgi_python-0.8.6 → vgi_python-0.8.8}/wrangler.jsonc +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vgi-python
3
- Version: 0.8.6
3
+ Version: 0.8.8
4
4
  Summary: Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow
5
5
  Project-URL: Homepage, https://query.farm
6
6
  Project-URL: Repository, https://github.com/Query-farm/vgi-python
@@ -162,7 +162,7 @@ Requires-Dist: httpx>=0.24
162
162
  Requires-Dist: platformdirs
163
163
  Requires-Dist: pyarrow
164
164
  Requires-Dist: typer>=0.9
165
- Requires-Dist: vgi-rpc>=0.20.5
165
+ Requires-Dist: vgi-rpc>=0.21.0
166
166
  Provides-Extra: azure
167
167
  Requires-Dist: azure-identity>=1.16.0; extra == 'azure'
168
168
  Requires-Dist: pymssql>=2.3.0; extra == 'azure'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vgi-python"
3
- version = "0.8.6"
3
+ version = "0.8.8"
4
4
  description = "Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -40,7 +40,7 @@ dependencies = [
40
40
  "pyarrow",
41
41
  "typer>=0.9",
42
42
  "platformdirs",
43
- "vgi-rpc>=0.20.5",
43
+ "vgi-rpc>=0.21.0",
44
44
  "httpx>=0.24",
45
45
  ]
46
46
 
@@ -180,6 +180,15 @@ _RPC_ALLOWLIST: dict[str, tuple[str, ...] | NotExposed] = {
180
180
  "catalog_schema_contents_views": ("schema_contents",),
181
181
  "catalog_schema_contents_functions": ("schema_contents",),
182
182
  "catalog_schema_contents_macros": ("schema_contents",),
183
+ "catalog_copy_from_formats": NotExposed(
184
+ reason=(
185
+ "COPY ... FROM format discovery. Consumed today by the C++ extension, "
186
+ "which calls this at ATTACH to register a DuckDB CopyFunction per "
187
+ "advertised format; covered by C++ integration/copy_from/*.test. Client "
188
+ "wrappers for the other VGI languages are planned to follow the "
189
+ "Python/C++ implementation."
190
+ )
191
+ ),
183
192
  "catalog_schema_contents_indexes": NotExposed(
184
193
  reason=(
185
194
  "DuckDB-only metadata path. Indexes are catalog-planner territory; "
@@ -0,0 +1,147 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Worker-side unit tests for CopyFromFunction and the example_lines format."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import tempfile
8
+
9
+ import pyarrow as pa
10
+ import pytest
11
+
12
+ from vgi._test_fixtures.copy_from import ExampleLinesCopyFromArgs, ExampleLinesCopyFromFunction
13
+ from vgi._test_fixtures.worker import ExampleCatalog
14
+ from vgi.arguments import Arguments
15
+ from vgi.invocation import FunctionType
16
+ from vgi.protocol import BindRequest, CopyFromContext
17
+
18
+
19
+ class _CollectOut:
20
+ """Minimal OutputCollector stand-in for read()."""
21
+
22
+ def __init__(self) -> None:
23
+ self.batches: list[pa.RecordBatch] = []
24
+
25
+ def emit(self, batch: pa.RecordBatch, **_kwargs: object) -> None:
26
+ self.batches.append(batch)
27
+
28
+ def finish(self) -> None: # pragma: no cover - read() never calls finish itself
29
+ pass
30
+
31
+
32
+ def _write(text: str) -> str:
33
+ """Write ``text`` to a throwaway file and return its path."""
34
+ with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as fp:
35
+ fp.write(text)
36
+ return fp.name
37
+
38
+
39
+ EXPECTED = pa.schema([("a", pa.int64()), ("b", pa.string())])
40
+
41
+
42
+ def test_on_bind_binds_to_expected_schema() -> None:
43
+ """on_bind binds output to the COPY target schema."""
44
+ cf = CopyFromContext(format="example_lines", file_path="/x", expected_schema=EXPECTED)
45
+ br = BindRequest(
46
+ function_name="example_lines_copy_reader",
47
+ arguments=Arguments(named={"null_string": pa.scalar("NA")}),
48
+ function_type=FunctionType.TABLE,
49
+ copy_from=cf,
50
+ )
51
+ resp = ExampleLinesCopyFromFunction.bind(br)
52
+ assert resp.output_schema.equals(EXPECTED)
53
+
54
+
55
+ def test_on_bind_without_copy_from_context_raises() -> None:
56
+ """on_bind rejects a non-COPY invocation."""
57
+ br = BindRequest(
58
+ function_name="example_lines_copy_reader",
59
+ arguments=Arguments(named={"null_string": pa.scalar("NA")}),
60
+ function_type=FunctionType.TABLE,
61
+ )
62
+ with pytest.raises(ValueError, match="COPY FROM format reader"):
63
+ ExampleLinesCopyFromFunction.bind(br)
64
+
65
+
66
+ def test_read_parses_and_coerces_with_null_string() -> None:
67
+ """read() parses, null-maps, and casts to the target schema."""
68
+ path = _write("1,foo\n2,NA\n3,baz\n")
69
+ out = _CollectOut()
70
+ ExampleLinesCopyFromFunction.read(
71
+ path=path,
72
+ options=ExampleLinesCopyFromArgs(null_string="NA"),
73
+ expected_schema=EXPECTED,
74
+ params=None,
75
+ out=out,
76
+ )
77
+ table = pa.Table.from_batches(out.batches)
78
+ assert table.schema.equals(EXPECTED)
79
+ assert table.to_pydict() == {"a": [1, 2, 3], "b": ["foo", None, "baz"]}
80
+
81
+
82
+ def test_read_custom_delimiter_and_skip_rows() -> None:
83
+ """read() honors delimiter and skip_rows options."""
84
+ path = _write("# header\n1|a\n2|b\n")
85
+ out = _CollectOut()
86
+ ExampleLinesCopyFromFunction.read(
87
+ path=path,
88
+ options=ExampleLinesCopyFromArgs(null_string="NA", delimiter="|", skip_rows=1),
89
+ expected_schema=EXPECTED,
90
+ params=None,
91
+ out=out,
92
+ )
93
+ assert pa.Table.from_batches(out.batches).to_pydict() == {"a": [1, 2], "b": ["a", "b"]}
94
+
95
+
96
+ def test_read_on_error_fail_vs_skip() -> None:
97
+ """on_error 'fail' raises; 'skip' drops the bad row."""
98
+ path = _write("1,a\nBADROW\n3,c\n")
99
+ with pytest.raises(ValueError, match="example_lines: row has"):
100
+ ExampleLinesCopyFromFunction.read(
101
+ path=path,
102
+ options=ExampleLinesCopyFromArgs(null_string="NA"), # on_error defaults to "fail"
103
+ expected_schema=EXPECTED,
104
+ params=None,
105
+ out=_CollectOut(),
106
+ )
107
+
108
+ out = _CollectOut()
109
+ ExampleLinesCopyFromFunction.read(
110
+ path=path,
111
+ options=ExampleLinesCopyFromArgs(null_string="NA", on_error="skip"),
112
+ expected_schema=EXPECTED,
113
+ params=None,
114
+ out=out,
115
+ )
116
+ assert pa.Table.from_batches(out.batches).num_rows == 2
117
+
118
+
119
+ def test_catalog_advertises_copy_format() -> None:
120
+ """The example catalog advertises the example_lines format."""
121
+ formats = ExampleCatalog().copy_from_formats(attach_opaque_data=b"", transaction_opaque_data=None)
122
+ by_name = {f.format_name: f for f in formats}
123
+ assert "example_lines" in by_name
124
+ fmt = by_name["example_lines"]
125
+ assert fmt.handler == "example_lines_copy_reader"
126
+ assert fmt.direction == "from"
127
+ assert fmt.comment == "Toy delimited-text reader for tests"
128
+ assert fmt.tags.get("category") == "copy_from"
129
+ opt_schema = pa.ipc.read_schema(pa.py_buffer(fmt.options))
130
+ assert set(opt_schema.names) == {"delimiter", "null_string", "skip_rows", "on_error"}
131
+ assert opt_schema.field("null_string").metadata[b"vgi_doc"] == b"Token parsed as SQL NULL"
132
+
133
+
134
+ def test_bind_request_copy_from_wire_roundtrip() -> None:
135
+ """copy_from survives a BindRequest wire round-trip."""
136
+ cf = CopyFromContext(format="example_lines", file_path="/p", expected_schema=EXPECTED)
137
+ br = BindRequest(
138
+ function_name="h",
139
+ arguments=Arguments(named={"null_string": pa.scalar("NA")}),
140
+ function_type=FunctionType.TABLE,
141
+ copy_from=cf,
142
+ )
143
+ restored = BindRequest.deserialize_from_bytes(br.serialize_to_bytes())
144
+ assert restored.copy_from is not None
145
+ assert restored.copy_from.format == "example_lines"
146
+ assert restored.copy_from.file_path == "/p"
147
+ assert restored.copy_from.expected_schema.equals(EXPECTED)
@@ -0,0 +1,120 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Worker-side unit tests for CopyToFunction and the example_lines_out format."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import tempfile
8
+ import types
9
+
10
+ import pyarrow as pa
11
+
12
+ from vgi._test_fixtures.copy_to import ExampleLinesCopyToArgs, ExampleLinesCopyToFunction
13
+ from vgi._test_fixtures.worker import ExampleCatalog
14
+
15
+ SCHEMA = pa.schema([("a", pa.int64()), ("b", pa.string())])
16
+
17
+
18
+ class _Store:
19
+ """Minimal in-memory BoundStorage stub (append + ordered log scan)."""
20
+
21
+ def __init__(self) -> None:
22
+ self.log: list[tuple[int, bytes]] = []
23
+
24
+ def state_append(self, ns: bytes, key: bytes, val: bytes) -> None:
25
+ self.log.append((len(self.log), val))
26
+
27
+ def state_log_scan(self, ns: bytes, key: bytes, after_id: int = -1, limit: int | None = None) -> list:
28
+ rows = [(i, v) for (i, v) in self.log if i > after_id]
29
+ return rows if limit is None else rows[:limit]
30
+
31
+
32
+ def _tmp_path() -> str:
33
+ with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as fh:
34
+ return fh.name
35
+
36
+
37
+ def _read(path: str) -> str:
38
+ with open(path, encoding="utf-8") as fh:
39
+ return fh.read()
40
+
41
+
42
+ def _params(store: _Store) -> types.SimpleNamespace:
43
+ bind_call = types.SimpleNamespace(input_schema=SCHEMA)
44
+ init_call = types.SimpleNamespace(bind_call=bind_call)
45
+ return types.SimpleNamespace(storage=store, init_call=init_call, execution_id=b"x", args=None)
46
+
47
+
48
+ def test_write_then_close_round_trips_with_null_string() -> None:
49
+ """write() buffers shards; close() concatenates them to a delimited file."""
50
+ store = _Store()
51
+ params = _params(store)
52
+ opts = ExampleLinesCopyToArgs(null_string="NA")
53
+ out_name = _tmp_path()
54
+
55
+ ExampleLinesCopyToFunction.write(
56
+ batch=pa.record_batch({"a": [1, 2], "b": ["foo", None]}, schema=SCHEMA),
57
+ options=opts,
58
+ file_path=out_name,
59
+ params=params,
60
+ )
61
+ ExampleLinesCopyToFunction.write(
62
+ batch=pa.record_batch({"a": [3], "b": ["baz"]}, schema=SCHEMA),
63
+ options=opts,
64
+ file_path=out_name,
65
+ params=params,
66
+ )
67
+ n = ExampleLinesCopyToFunction.close(options=opts, file_path=out_name, params=params)
68
+ assert n == 3
69
+ assert _read(out_name) == "1,foo\n2,NA\n3,baz\n"
70
+
71
+
72
+ def test_close_honors_delimiter_and_header() -> None:
73
+ """Non-default delimiter + header row are applied."""
74
+ store = _Store()
75
+ params = _params(store)
76
+ opts = ExampleLinesCopyToArgs(null_string="NA", delimiter="|", header=True)
77
+ out_name = _tmp_path()
78
+ ExampleLinesCopyToFunction.write(
79
+ batch=pa.record_batch({"a": [1], "b": ["x"]}, schema=SCHEMA),
80
+ options=opts,
81
+ file_path=out_name,
82
+ params=params,
83
+ )
84
+ n = ExampleLinesCopyToFunction.close(options=opts, file_path=out_name, params=params)
85
+ assert n == 1
86
+ assert _read(out_name) == "a|b\n1|x\n"
87
+
88
+
89
+ def test_close_empty_input_with_header_writes_header_only() -> None:
90
+ """An empty COPY with header=true still emits the header row (0 data rows)."""
91
+ store = _Store()
92
+ params = _params(store)
93
+ out_name = _tmp_path()
94
+ n = ExampleLinesCopyToFunction.close(
95
+ options=ExampleLinesCopyToArgs(null_string="NA", header=True),
96
+ file_path=out_name,
97
+ params=params,
98
+ )
99
+ assert n == 0
100
+ assert _read(out_name) == "a,b\n"
101
+
102
+
103
+ def test_catalog_advertises_copy_to_format() -> None:
104
+ """The example catalog advertises example_lines_out with direction='to'."""
105
+ formats = ExampleCatalog().copy_from_formats(attach_opaque_data=b"", transaction_opaque_data=None)
106
+ by = {(f.direction, f.format_name): f for f in formats}
107
+ assert ("to", "example_lines_out") in by
108
+ fmt = by[("to", "example_lines_out")]
109
+ assert fmt.handler == "example_lines_writer"
110
+ assert fmt.comment == "Toy delimited-text writer for tests"
111
+ assert fmt.tags.get("category") == "copy_to"
112
+ opt_schema = pa.ipc.read_schema(pa.py_buffer(fmt.options))
113
+ assert set(opt_schema.names) == {
114
+ "delimiter",
115
+ "null_string",
116
+ "header",
117
+ "header_repeat",
118
+ "on_exists",
119
+ "fail_on_value",
120
+ }
@@ -0,0 +1,61 @@
1
+ """Unit tests for ResolvedSecrets type- and scope-aware selection."""
2
+
3
+ from vgi.table_function import ResolvedSecrets
4
+
5
+
6
+ def _secrets() -> ResolvedSecrets:
7
+ # Values are plain strings here; ResolvedSecrets also accepts pyarrow Scalars
8
+ # (it calls .as_py() when present).
9
+ return ResolvedSecrets(
10
+ {
11
+ "my_s3": {"type": "s3", "key_id": "AAA", "scope": "s3://bucket-a"},
12
+ "my_s3_b": {
13
+ "type": "s3",
14
+ "key_id": "BBB",
15
+ "scope": "s3://bucket-b\ns3://bucket-b2",
16
+ },
17
+ "my_gcs": {"type": "gcs", "key_id": "G"},
18
+ }
19
+ )
20
+
21
+
22
+ def test_type_aware() -> None:
23
+ """Type-aware accessors find/identify secrets by type."""
24
+ s = _secrets()
25
+ assert s.secret_type("my_s3") == "s3"
26
+ assert s.secret_type("my_gcs") == "gcs"
27
+ assert len(s.of_type("s3")) == 2
28
+ assert len(s.of_type("gcs")) == 1
29
+ assert s.of_type("azure") == []
30
+
31
+
32
+ def test_for_scope_of_type_per_bucket() -> None:
33
+ """Per-bucket scope selection picks the right s3 secret."""
34
+ s = _secrets()
35
+ assert s.for_scope_of_type("s3://bucket-a/x.dat", "s3")["key_id"] == "AAA"
36
+ assert s.for_scope_of_type("s3://bucket-b2/y.dat", "s3")["key_id"] == "BBB"
37
+ assert s.field_for("s3://bucket-a/x.dat", "key_id") == "AAA"
38
+
39
+
40
+ def test_longest_prefix_and_fallback() -> None:
41
+ """Longest scope prefix wins; unscoped is the fallback."""
42
+ s = ResolvedSecrets(
43
+ {
44
+ "broad": {"type": "s3", "key_id": "broad", "scope": "s3://bucket"},
45
+ "narrow": {"type": "s3", "key_id": "narrow", "scope": "s3://bucket/data"},
46
+ }
47
+ )
48
+ assert s.for_scope("s3://bucket/data/x.dat")["key_id"] == "narrow"
49
+ assert s.for_scope("s3://bucket/other/x.dat")["key_id"] == "broad"
50
+
51
+ unscoped = ResolvedSecrets({"only": {"type": "s3", "key_id": "only"}})
52
+ assert unscoped.for_scope("s3://any/x")["key_id"] == "only"
53
+
54
+ assert s.for_scope("s3://nope/x") is None
55
+
56
+
57
+ def test_dict_access_still_works() -> None:
58
+ """ResolvedSecrets keeps plain dict access."""
59
+ s = _secrets()
60
+ assert s["my_s3"]["key_id"] == "AAA"
61
+ assert s.get("missing") is None
@@ -0,0 +1,122 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Round-trip tests for the TCP transport.
4
+
5
+ Spawns ``vgi-fixture-worker --tcp 127.0.0.1:0`` (raw Arrow-IPC framing over a
6
+ TCP socket, served by ``vgi_rpc.rpc.run_server``), parses the ``TCP:host:port``
7
+ discovery line it prints on stdout, then drives it through
8
+ ``Client.from_tcp(...)`` — the TCP analog of the HTTP round-trip in
9
+ ``tests/_http_fixtures.py``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import queue
15
+ import subprocess
16
+ import sys
17
+ import threading
18
+ from collections.abc import Iterator
19
+ from contextlib import contextmanager
20
+
21
+ import pyarrow as pa
22
+ import pytest
23
+
24
+ from vgi.arguments import Arguments
25
+ from vgi.client import Client
26
+
27
+
28
+ @contextmanager
29
+ def run_tcp_worker(*, bind: str = "127.0.0.1:0") -> Iterator[tuple[str, int]]:
30
+ """Run ``vgi-fixture-worker --tcp`` and yield the bound ``(host, port)``.
31
+
32
+ The worker prints ``TCP:<host>:<port>`` once bound and then must not write
33
+ further to stdout (the cross-language launcher discovery contract), so we
34
+ read exactly one line to learn the port. stderr is drained in the
35
+ background to keep the worker from blocking on a full pipe buffer.
36
+ """
37
+ proc = subprocess.Popen(
38
+ [sys.executable, "-m", "vgi._test_fixtures.worker", "--tcp", bind],
39
+ stdout=subprocess.PIPE,
40
+ stderr=subprocess.PIPE,
41
+ text=True,
42
+ bufsize=1,
43
+ )
44
+
45
+ def _drain(pipe: object) -> None:
46
+ for _ in pipe: # type: ignore[attr-defined]
47
+ pass
48
+
49
+ stderr_thread = threading.Thread(target=_drain, args=(proc.stderr,), daemon=True)
50
+ stderr_thread.start()
51
+
52
+ # Read the discovery line off stdout with a timeout so a worker that never
53
+ # binds fails the test instead of hanging it.
54
+ line_q: queue.Queue[str] = queue.Queue(maxsize=1)
55
+
56
+ def _read_line() -> None:
57
+ assert proc.stdout is not None
58
+ for line in proc.stdout:
59
+ if line.startswith("TCP:"):
60
+ line_q.put(line.strip())
61
+ return
62
+
63
+ reader = threading.Thread(target=_read_line, daemon=True)
64
+ reader.start()
65
+ try:
66
+ discovery = line_q.get(timeout=30)
67
+ except queue.Empty:
68
+ proc.terminate()
69
+ raise TimeoutError("worker did not emit a TCP: discovery line within 30s") from None
70
+
71
+ _, host, port_str = discovery.split(":", 2)
72
+ try:
73
+ yield host, int(port_str)
74
+ finally:
75
+ proc.terminate()
76
+ try:
77
+ proc.wait(timeout=10)
78
+ except subprocess.TimeoutExpired:
79
+ proc.kill()
80
+ proc.wait(timeout=5)
81
+ stderr_thread.join(timeout=5)
82
+
83
+
84
+ def test_tcp_round_trip_table_function() -> None:
85
+ """A table function streams rows correctly over the TCP transport."""
86
+ with run_tcp_worker() as (host, port), Client.from_tcp(host, port) as client:
87
+ batches = list(
88
+ client.table_function(
89
+ function_name="sequence",
90
+ arguments=Arguments(positional=(pa.scalar(5),)),
91
+ )
92
+ )
93
+
94
+ table = pa.Table.from_batches(batches)
95
+ assert table.column("n").to_pylist() == [0, 1, 2, 3, 4]
96
+
97
+
98
+ def test_tcp_round_trip_catalog_listing() -> None:
99
+ """Catalog discovery works over the TCP transport (catalog_mixin path)."""
100
+ with run_tcp_worker() as (host, port), Client.from_tcp(host, port) as client:
101
+ catalogs = client.catalogs()
102
+
103
+ assert any(c.name == "example" for c in catalogs)
104
+
105
+
106
+ class TestTcpConstructorValidation:
107
+ """``transport='tcp'`` argument validation."""
108
+
109
+ def test_requires_host_and_port(self) -> None:
110
+ """Tcp transport without host/port is rejected."""
111
+ with pytest.raises(ValueError, match="requires tcp_host and tcp_port"):
112
+ Client(transport="tcp", pool=None)
113
+
114
+ def test_rejects_server_path(self) -> None:
115
+ """server_path is subprocess-only."""
116
+ with pytest.raises(ValueError, match="server_path is only meaningful"):
117
+ Client("some-worker", transport="tcp", tcp_host="127.0.0.1", tcp_port=1, pool=None)
118
+
119
+ def test_rejects_base_url(self) -> None:
120
+ """base_url is http-only."""
121
+ with pytest.raises(ValueError, match="base_url is only meaningful"):
122
+ Client(transport="tcp", tcp_host="127.0.0.1", tcp_port=1, base_url="http://x", pool=None)
@@ -2250,7 +2250,7 @@ requires-dist = [
2250
2250
 
2251
2251
  [[package]]
2252
2252
  name = "vgi-python"
2253
- version = "0.8.5"
2253
+ version = "0.8.8"
2254
2254
  source = { editable = "." }
2255
2255
  dependencies = [
2256
2256
  { name = "click" },
@@ -2360,7 +2360,7 @@ requires-dist = [
2360
2360
  { name = "vgi-python", extras = ["test-fixtures"], marker = "extra == 'test-fixtures-writable'" },
2361
2361
  { name = "vgi-python", extras = ["test-fixtures", "test-fixtures-writable", "http", "oauth", "otel", "sentry", "azure"], marker = "extra == 'dev'" },
2362
2362
  { name = "vgi-python", extras = ["transactor"], marker = "extra == 'test-fixtures-writable'" },
2363
- { name = "vgi-rpc", specifier = ">=0.20.5" },
2363
+ { name = "vgi-rpc", specifier = ">=0.21.0" },
2364
2364
  { name = "vgi-rpc", extras = ["conformance", "external", "http", "oauth", "otel", "sentry"], marker = "extra == 'dev'" },
2365
2365
  { name = "vgi-rpc", extras = ["http"], marker = "extra == 'http'" },
2366
2366
  { name = "vgi-rpc", extras = ["oauth"], marker = "extra == 'oauth'" },
@@ -2389,7 +2389,7 @@ docs = [
2389
2389
 
2390
2390
  [[package]]
2391
2391
  name = "vgi-rpc"
2392
- version = "0.20.6"
2392
+ version = "0.21.0"
2393
2393
  source = { registry = "https://pypi.org/simple" }
2394
2394
  dependencies = [
2395
2395
  { name = "docstring-parser" },
@@ -2397,9 +2397,9 @@ dependencies = [
2397
2397
  { name = "pywin32", marker = "sys_platform == 'win32'" },
2398
2398
  { name = "tzdata", marker = "sys_platform == 'win32'" },
2399
2399
  ]
2400
- sdist = { url = "https://files.pythonhosted.org/packages/40/76/4a2c35af34928c6ab17fcb9b1b50639241bf9d015d45b23ce004d0d70d51/vgi_rpc-0.20.6.tar.gz", hash = "sha256:3172d5041b901b0d1a32c9b45da32363195bbca18adb7e305456f19c241cf8fe", size = 847271, upload-time = "2026-06-22T18:24:18.969Z" }
2400
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/7b/0de17f5f638414188829f8bb4952e9a6116145ad402512ead8d0f035b705/vgi_rpc-0.21.0.tar.gz", hash = "sha256:c82c17149c0977080184d4728bdebe38aebcd1374984f6c81a3ce39be0aca367", size = 857916, upload-time = "2026-06-26T00:05:14.045Z" }
2401
2401
  wheels = [
2402
- { url = "https://files.pythonhosted.org/packages/d3/3c/9ccc888e725ad9d7091e8ec6fd7504eec15dc2ccfabedabd7d072123b0e8/vgi_rpc-0.20.6-py3-none-any.whl", hash = "sha256:74f8b7c0685f1c28a3ff754d76f8c0179c782e33aa5915ad0343bb9955d7509d", size = 371735, upload-time = "2026-06-22T18:24:17.458Z" },
2402
+ { url = "https://files.pythonhosted.org/packages/40/71/1607a7b093e2477d64a0193d21ec9b9fdfe8bc09f0165ce378fc214ef0a9/vgi_rpc-0.21.0-py3-none-any.whl", hash = "sha256:5e5b1ef8585f459eb0761c9232a6e4eeedef69be7b61ead61e0993f4a15b8698", size = 378442, upload-time = "2026-06-26T00:05:11.821Z" },
2403
2403
  ]
2404
2404
 
2405
2405
  [package.optional-dependencies]
@@ -0,0 +1,99 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Fixture ``COPY ... FROM`` format reader for VGI integration tests.
4
+
5
+ ``ExampleLinesCopyFromFunction`` registers the SQL format ``example_lines`` — a
6
+ toy delimited-text reader. It exercises the full COPY-FROM path plus the option
7
+ machinery: a defaulted option (``delimiter``), an ``INTEGER`` option with a range
8
+ constraint (``skip_rows``), a required option (``null_string``), and an
9
+ enum/``choices`` option (``on_error``).
10
+
11
+ Usage::
12
+
13
+ CREATE TABLE t (a INTEGER, b VARCHAR);
14
+ COPY t FROM '/path/data.txt' (FORMAT example_lines, null_string 'NA');
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING, Annotated, ClassVar
21
+
22
+ import pyarrow as pa
23
+
24
+ from vgi.arguments import Arg
25
+ from vgi.copy_from_function import CopyFromFunction
26
+
27
+ if TYPE_CHECKING:
28
+ from vgi_rpc.rpc import OutputCollector
29
+
30
+ from vgi.table_function import ProcessParams
31
+
32
+ __all__ = ["ExampleLinesCopyFromFunction"]
33
+
34
+
35
+ @dataclass(slots=True, frozen=True, kw_only=True)
36
+ class ExampleLinesCopyFromArgs:
37
+ """Options for the ``example_lines`` COPY format."""
38
+
39
+ null_string: Annotated[str, Arg("null_string", doc="Token parsed as SQL NULL")]
40
+ delimiter: Annotated[str, Arg("delimiter", default=",", doc="Field separator")] = ","
41
+ skip_rows: Annotated[int, Arg("skip_rows", default=0, ge=0, doc="Leading lines to skip before data")] = 0
42
+ on_error: Annotated[
43
+ str,
44
+ Arg(
45
+ "on_error",
46
+ default="fail",
47
+ choices=["fail", "skip"],
48
+ doc="Behavior on a row whose column count does not match the target",
49
+ ),
50
+ ] = "fail"
51
+
52
+
53
+ class ExampleLinesCopyFromFunction(CopyFromFunction[ExampleLinesCopyFromArgs]):
54
+ """Toy delimited-text ``COPY ... FROM`` reader (test fixture)."""
55
+
56
+ COPY_FROM_FORMAT: ClassVar[str] = "example_lines"
57
+ COPY_FROM_COMMENT: ClassVar[str | None] = "Toy delimited-text reader for tests"
58
+
59
+ class Meta:
60
+ name = "example_lines_copy_reader"
61
+ description = "Read a delimited text file into the COPY target table"
62
+ categories = ["copy", "test"]
63
+ tags = {"category": "copy_from", "stability": "test"}
64
+
65
+ @classmethod
66
+ def read(
67
+ cls,
68
+ *,
69
+ path: str,
70
+ options: ExampleLinesCopyFromArgs,
71
+ expected_schema: pa.Schema,
72
+ params: ProcessParams[ExampleLinesCopyFromArgs],
73
+ out: OutputCollector,
74
+ ) -> None:
75
+ """Parse ``path`` line-by-line and emit one batch matching ``expected_schema``."""
76
+ with open(path, encoding="utf-8") as fh:
77
+ lines = fh.read().splitlines()
78
+ lines = lines[options.skip_rows :]
79
+
80
+ ncols = len(expected_schema)
81
+ rows: list[list[str]] = []
82
+ for line in lines:
83
+ if line == "":
84
+ continue
85
+ cells = line.split(options.delimiter)
86
+ if len(cells) != ncols:
87
+ if options.on_error == "skip":
88
+ continue
89
+ raise ValueError(f"example_lines: row has {len(cells)} fields, expected {ncols}: {line!r}")
90
+ rows.append(cells)
91
+
92
+ # Column-major string arrays, NULL where the cell equals null_string,
93
+ # then cast each column to the target type (DuckDB inserts no cast).
94
+ columns = list(zip(*rows)) if rows else [() for _ in range(ncols)]
95
+ arrays = []
96
+ for idx, field in enumerate(expected_schema):
97
+ raw = [None if v == options.null_string else v for v in columns[idx]]
98
+ arrays.append(pa.array(raw, type=pa.string()).cast(field.type))
99
+ out.emit(pa.RecordBatch.from_arrays(arrays, schema=expected_schema))