vgi-python 0.27.0__tar.gz → 0.28.1__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 (406) hide show
  1. {vgi_python-0.27.0 → vgi_python-0.28.1}/PKG-INFO +2 -2
  2. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/column-statistics.md +24 -0
  3. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/function-patterns.md +25 -0
  4. vgi_python-0.28.1/examples/batch_index_worker.py +142 -0
  5. {vgi_python-0.27.0 → vgi_python-0.28.1}/pyproject.toml +1 -1
  6. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_column_statistics.py +86 -0
  7. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_broken_pipe.py +20 -9
  8. vgi_python-0.28.1/tests/client/test_stop_force.py +101 -0
  9. vgi_python-0.28.1/tests/client/test_table_column_statistics.py +73 -0
  10. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_worker_debug.py +15 -13
  11. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/test_client.py +10 -6
  12. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_examples_workers.py +20 -0
  13. {vgi_python-0.27.0 → vgi_python-0.28.1}/uv.lock +525 -336
  14. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/__init__.py +7 -0
  15. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/catalog_interface.py +65 -0
  16. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/catalog_mixin.py +53 -3
  17. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/client.py +82 -26
  18. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/landing.html +61 -13
  19. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/vgi-client.js +9 -9
  20. {vgi_python-0.27.0 → vgi_python-0.28.1}/.gitattributes +0 -0
  21. {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/dependabot.yml +0 -0
  22. {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/styles/config/vocabularies/VGI/accept.txt +0 -0
  23. {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/ci.yml +0 -0
  24. {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/docs.yml +0 -0
  25. {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/integration.yml +0 -0
  26. {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/release.yml +0 -0
  27. {vgi_python-0.27.0 → vgi_python-0.28.1}/.gitignore +0 -0
  28. {vgi_python-0.27.0 → vgi_python-0.28.1}/.python-version +0 -0
  29. {vgi_python-0.27.0 → vgi_python-0.28.1}/.vale.ini +0 -0
  30. {vgi_python-0.27.0 → vgi_python-0.28.1}/CLAUDE.md +0 -0
  31. {vgi_python-0.27.0 → vgi_python-0.28.1}/DOCS_ACCEPTANCE_CRITERIA.md +0 -0
  32. {vgi_python-0.27.0 → vgi_python-0.28.1}/DOCS_REVIEW_RUBRIC.md +0 -0
  33. {vgi_python-0.27.0 → vgi_python-0.28.1}/DOCS_USABILITY_TEST.md +0 -0
  34. {vgi_python-0.27.0 → vgi_python-0.28.1}/LICENSE +0 -0
  35. {vgi_python-0.27.0 → vgi_python-0.28.1}/README.md +0 -0
  36. {vgi_python-0.27.0 → vgi_python-0.28.1}/RELEASE-RUNBOOK.md +0 -0
  37. {vgi_python-0.27.0 → vgi_python-0.28.1}/SECURITY.md +0 -0
  38. {vgi_python-0.27.0 → vgi_python-0.28.1}/ci/README.md +0 -0
  39. {vgi_python-0.27.0 → vgi_python-0.28.1}/ci/preprocess-require.awk +0 -0
  40. {vgi_python-0.27.0 → vgi_python-0.28.1}/ci/run-integration.sh +0 -0
  41. {vgi_python-0.27.0 → vgi_python-0.28.1}/dist-vgi/.gitignore +0 -0
  42. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/aggregate-functions.md +0 -0
  43. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/arguments.md +0 -0
  44. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/auth.md +0 -0
  45. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/catalogs.md +0 -0
  46. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/client.md +0 -0
  47. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/exceptions.md +0 -0
  48. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/filters.md +0 -0
  49. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/functions.md +0 -0
  50. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/http.md +0 -0
  51. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/index.md +0 -0
  52. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/metadata.md +0 -0
  53. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/observability.md +0 -0
  54. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/storage.md +0 -0
  55. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/transactor.md +0 -0
  56. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/worker.md +0 -0
  57. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/argument-serialization.md +0 -0
  58. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/apple-touch-icon.png +0 -0
  59. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/favicon-16x16.png +0 -0
  60. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/favicon-32x32.png +0 -0
  61. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/favicon.ico +0 -0
  62. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/aggregate.svg +0 -0
  63. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/buffering.svg +0 -0
  64. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/scalar.svg +0 -0
  65. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/table-in-out.svg +0 -0
  66. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/table.svg +0 -0
  67. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/logo.png +0 -0
  68. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/social-card.png +0 -0
  69. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/authentication.md +0 -0
  70. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/catalog-interface.md +0 -0
  71. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/cli.md +0 -0
  72. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/concepts/index.md +0 -0
  73. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/contributing-docs.md +0 -0
  74. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/filter-pushdown.md +0 -0
  75. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/generator-api.md +0 -0
  76. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/global-functions.md +0 -0
  77. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/catalogs.md +0 -0
  78. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/http-auth.md +0 -0
  79. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/index.md +0 -0
  80. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/pushdown-and-statistics.md +0 -0
  81. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/state-storage.md +0 -0
  82. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/index.md +0 -0
  83. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/lifecycle.md +0 -0
  84. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/metadata.md +0 -0
  85. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/overrides/main.html +0 -0
  86. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/robots.txt +0 -0
  87. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/shared-storage.md +0 -0
  88. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/stylesheets/extra.css +0 -0
  89. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/tutorial/index.md +0 -0
  90. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/tutorial/scalar.md +0 -0
  91. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/tutorial/table.md +0 -0
  92. {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/vgi-logo.png +0 -0
  93. {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/calc_scalar_worker.py +0 -0
  94. {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/calc_worker.py +0 -0
  95. {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/filter_worker.py +0 -0
  96. {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/greeting_scalar_worker.py +0 -0
  97. {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/row_count_worker.py +0 -0
  98. {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/series_streaming_worker.py +0 -0
  99. {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/sum_worker.py +0 -0
  100. {vgi_python-0.27.0 → vgi_python-0.28.1}/mkdocs.yml +0 -0
  101. {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/LICENSE +0 -0
  102. {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/README.md +0 -0
  103. {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/docker/Dockerfile +0 -0
  104. {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/docker/docker-entrypoint.sh +0 -0
  105. {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/pyproject.toml +0 -0
  106. {vgi_python-0.27.0 → vgi_python-0.28.1}/scripts/measure_startup.py +0 -0
  107. {vgi_python-0.27.0 → vgi_python-0.28.1}/scripts/run_all_tests.sh +0 -0
  108. {vgi_python-0.27.0 → vgi_python-0.28.1}/test-data/generate.sh +0 -0
  109. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/__init__.py +0 -0
  110. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/_http_fixtures.py +0 -0
  111. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/__init__.py +0 -0
  112. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_attach_option.py +0 -0
  113. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_catalog_interface.py +0 -0
  114. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_client_catalog.py +0 -0
  115. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_declarative.py +0 -0
  116. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_example_worker_catalog.py +0 -0
  117. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_integration.py +0 -0
  118. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_required_filters.py +0 -0
  119. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_scan_branches.py +0 -0
  120. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_serialization.py +0 -0
  121. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_setting.py +0 -0
  122. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_storage.py +0 -0
  123. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_time_travel.py +0 -0
  124. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_writable_table.py +0 -0
  125. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/__init__.py +0 -0
  126. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_cli.py +0 -0
  127. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_cli_catalog_functions.py +0 -0
  128. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/__init__.py +0 -0
  129. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/_stub.py +0 -0
  130. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/conftest.py +0 -0
  131. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_accumulate.py +0 -0
  132. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_aggregate.py +0 -0
  133. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_attach.py +0 -0
  134. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_bearer_auth.py +0 -0
  135. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_const_param_enforcement.py +0 -0
  136. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_container.py +0 -0
  137. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_copy_from.py +0 -0
  138. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_copy_to.py +0 -0
  139. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_directory_parity.py +0 -0
  140. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_function_inventory.py +0 -0
  141. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_github.py +0 -0
  142. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_http_client.py +0 -0
  143. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_http_external_location.py +0 -0
  144. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_http_upload_url.py +0 -0
  145. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_macro.py +0 -0
  146. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_overload.py +0 -0
  147. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_protocol_inventory.py +0 -0
  148. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_protocol_version.py +0 -0
  149. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_resumable_scan.py +0 -0
  150. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_scalar.py +0 -0
  151. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_scalar_attach_opaque_data.py +0 -0
  152. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_secret.py +0 -0
  153. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_settings.py +0 -0
  154. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_table.py +0 -0
  155. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_table_in_out.py +0 -0
  156. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_view.py +0 -0
  157. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_writable.py +0 -0
  158. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conftest.py +0 -0
  159. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/__init__.py +0 -0
  160. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_bernoulli_function.py +0 -0
  161. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_binary_packet_function.py +0 -0
  162. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_client.py +0 -0
  163. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_conditional_message_function.py +0 -0
  164. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_hash_seed_function.py +0 -0
  165. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_multiply_by_setting_function.py +0 -0
  166. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_multiply_function.py +0 -0
  167. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_random_bytes_function.py +0 -0
  168. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_return_secret_value_function.py +0 -0
  169. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/__init__.py +0 -0
  170. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/__init__.py +0 -0
  171. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_constant_columns_function.py +0 -0
  172. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_double_sequence_function.py +0 -0
  173. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_exception_function.py +0 -0
  174. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_filter_echo_function.py +0 -0
  175. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_logging_function.py +0 -0
  176. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_nested_sequence_function.py +0 -0
  177. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_partitioned_function.py +0 -0
  178. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_projected_data_function.py +0 -0
  179. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_sequence_function.py +0 -0
  180. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_settings_function.py +0 -0
  181. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_struct_settings_function.py +0 -0
  182. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_ten_thousand_function.py +0 -0
  183. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/test_wide_columns.py +0 -0
  184. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/__init__.py +0 -0
  185. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/__init__.py +0 -0
  186. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_buffer_input_function.py +0 -0
  187. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_echo_function.py +0 -0
  188. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_exception_functions.py +0 -0
  189. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_filter_by_setting_function.py +0 -0
  190. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_repeat_inputs_function.py +0 -0
  191. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_substream_partial_sum_function.py +0 -0
  192. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_sum_all_columns_function.py +0 -0
  193. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/test_blended_metadata.py +0 -0
  194. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_access_log_audit.py +0 -0
  195. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_aggregate_function.py +0 -0
  196. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_argument_spec.py +0 -0
  197. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_auth.py +0 -0
  198. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_bind_exceptions.py +0 -0
  199. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_bind_request_at_clause.py +0 -0
  200. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_bound_storage_conformance.py +0 -0
  201. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_cache_control.py +0 -0
  202. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_call_state_attach.py +0 -0
  203. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_catalog_auth_binding.py +0 -0
  204. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_copy_from_function.py +0 -0
  205. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_copy_to_function.py +0 -0
  206. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_docstrings.py +0 -0
  207. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_documentation_examples.py +0 -0
  208. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_example_function_arg_types.py +0 -0
  209. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_exception_handling.py +0 -0
  210. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_exceptions.py +0 -0
  211. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_filter_pushdown.py +0 -0
  212. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_filter_pushdown_extension.py +0 -0
  213. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage.py +0 -0
  214. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_azure_sql.py +0 -0
  215. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_cf_do.py +0 -0
  216. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_cf_do_integration.py +0 -0
  217. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_conformance.py +0 -0
  218. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_constants.py +0 -0
  219. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_protocol_version.py +0 -0
  220. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_request_builders.py +0 -0
  221. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_schemas.py +0 -0
  222. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_secret.py +0 -0
  223. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_go_schemas.py +0 -0
  224. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_protocol_version.py +0 -0
  225. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_schemas_cross_lang.py +0 -0
  226. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_ts_client.py +0 -0
  227. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_ts_schemas.py +0 -0
  228. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_http_demo_storage.py +0 -0
  229. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_http_s3_offload_input.py +0 -0
  230. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_http_s3_offload_output.py +0 -0
  231. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_metadata.py +0 -0
  232. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_mypy_consolidated.py +0 -0
  233. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_nest_tensor.py +0 -0
  234. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_otel.py +0 -0
  235. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_projection_enforcement.py +0 -0
  236. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_projection_repro.py +0 -0
  237. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_protocol_classes.py +0 -0
  238. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_resolved_secrets.py +0 -0
  239. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_schema_scoped_functions.py +0 -0
  240. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_schema_utils.py +0 -0
  241. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_serve.py +0 -0
  242. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_setting_secret_annotations.py +0 -0
  243. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_signing_key.py +0 -0
  244. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_table_buffering_function.py +0 -0
  245. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_table_function_dynamic_to_string.py +0 -0
  246. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_tcp_transport.py +0 -0
  247. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_type_bounds.py +0 -0
  248. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_union_argument.py +0 -0
  249. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_worker.py +0 -0
  250. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_worker_cli.py +0 -0
  251. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_worker_page.py +0 -0
  252. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/transactor/__init__.py +0 -0
  253. {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/transactor/test_transactor.py +0 -0
  254. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/__init__.py +0 -0
  255. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_duckdb.py +0 -0
  256. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_storage_profile.py +0 -0
  257. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/__init__.py +0 -0
  258. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/accumulate/__init__.py +0 -0
  259. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/accumulate/worker.py +0 -0
  260. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/__init__.py +0 -0
  261. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/_common.py +0 -0
  262. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/basic.py +0 -0
  263. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/dynamic.py +0 -0
  264. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/generic.py +0 -0
  265. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/listagg.py +0 -0
  266. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/percentile.py +0 -0
  267. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/same_name.py +0 -0
  268. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/secret_typed.py +0 -0
  269. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/streaming.py +0 -0
  270. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/varargs.py +0 -0
  271. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/window.py +0 -0
  272. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/attach_options.py +0 -0
  273. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/bad_enum.py +0 -0
  274. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/bad_protocol.py +0 -0
  275. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/cancellable.py +0 -0
  276. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/catalog.py +0 -0
  277. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/companion.py +0 -0
  278. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/copy_from.py +0 -0
  279. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/copy_to.py +0 -0
  280. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/global_functions.py +0 -0
  281. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/http_server.py +0 -0
  282. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/narrow_bind/__init__.py +0 -0
  283. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/narrow_bind/worker.py +0 -0
  284. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/nest_tensor.py +0 -0
  285. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/orchard_catalog.py +0 -0
  286. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/projection_repro/__init__.py +0 -0
  287. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/projection_repro/worker.py +0 -0
  288. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/__init__.py +0 -0
  289. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/_common.py +0 -0
  290. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/arithmetic.py +0 -0
  291. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/bench_ladder.py +0 -0
  292. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/binary.py +0 -0
  293. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/formatting.py +0 -0
  294. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/geo.py +0 -0
  295. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/null_handling.py +0 -0
  296. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/random_demo.py +0 -0
  297. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/same_name.py +0 -0
  298. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/settings_secrets.py +0 -0
  299. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/type_info.py +0 -0
  300. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/schema_reconcile/__init__.py +0 -0
  301. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/schema_reconcile/worker.py +0 -0
  302. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/simple_writable.py +0 -0
  303. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/__init__.py +0 -0
  304. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/_common.py +0 -0
  305. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/batch_index.py +0 -0
  306. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/batch_index_broken.py +0 -0
  307. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/cache.py +0 -0
  308. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/catalog_scans.py +0 -0
  309. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/filters.py +0 -0
  310. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/late_materialization.py +0 -0
  311. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/make_series.py +0 -0
  312. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/misc.py +0 -0
  313. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/order_modes.py +0 -0
  314. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/pairs.py +0 -0
  315. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/partition_columns.py +0 -0
  316. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/partition_columns_broken.py +0 -0
  317. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/profiling_example.py +0 -0
  318. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/required_filters.py +0 -0
  319. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/same_name_cached.py +0 -0
  320. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/sequence.py +0 -0
  321. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/settings.py +0 -0
  322. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/transaction_storage.py +0 -0
  323. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/tt_pushdown.py +0 -0
  324. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/typed_probe.py +0 -0
  325. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/versioned.py +0 -0
  326. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/wide.py +0 -0
  327. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table_in_out.py +0 -0
  328. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table_in_out_same_name.py +0 -0
  329. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/twin_catalogs.py +0 -0
  330. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/versioned.py +0 -0
  331. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/versioned_tables.py +0 -0
  332. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/worker.py +0 -0
  333. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/__init__.py +0 -0
  334. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/generic.py +0 -0
  335. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/table.py +0 -0
  336. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/worker.py +0 -0
  337. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/aggregate_function.py +0 -0
  338. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/argument_spec.py +0 -0
  339. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/arguments.py +0 -0
  340. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/attach_header.py +0 -0
  341. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/auth.py +0 -0
  342. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/cache_control.py +0 -0
  343. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/_descriptor_spec.py +0 -0
  344. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/attach_option.py +0 -0
  345. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/descriptors.py +0 -0
  346. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/duckdb_statistics.py +0 -0
  347. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/secret_type.py +0 -0
  348. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/setting.py +0 -0
  349. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/storage.py +0 -0
  350. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/__init__.py +0 -0
  351. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli.py +0 -0
  352. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_catalog.py +0 -0
  353. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_schema.py +0 -0
  354. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_table.py +0 -0
  355. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_transaction.py +0 -0
  356. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_utils.py +0 -0
  357. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_view.py +0 -0
  358. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/__init__.py +0 -0
  359. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/_common.py +0 -0
  360. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_constants.py +0 -0
  361. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_protocol_version.py +0 -0
  362. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_request_builders.py +0 -0
  363. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_schemas.py +0 -0
  364. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_secret_protocol_version.py +0 -0
  365. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_secret_request_builders.py +0 -0
  366. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_secret_schemas.py +0 -0
  367. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/go_schemas.py +0 -0
  368. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/protocol_version.py +0 -0
  369. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/ts_client.py +0 -0
  370. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/ts_schemas.py +0 -0
  371. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/copy_from_function.py +0 -0
  372. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/copy_to_function.py +0 -0
  373. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/exceptions.py +0 -0
  374. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function.py +0 -0
  375. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function_storage.py +0 -0
  376. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function_storage_azure_sql.py +0 -0
  377. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function_storage_cf_do.py +0 -0
  378. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/__init__.py +0 -0
  379. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/demo_storage.py +0 -0
  380. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/landing_page.py +0 -0
  381. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/worker_page.py +0 -0
  382. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/invocation.py +0 -0
  383. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/logging_config.py +0 -0
  384. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/meta_worker.py +0 -0
  385. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/metadata.py +0 -0
  386. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/otel.py +0 -0
  387. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/profiling.py +0 -0
  388. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/protocol.py +0 -0
  389. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/protocol_version.txt +0 -0
  390. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/py.typed +0 -0
  391. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/scalar_function.py +0 -0
  392. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/schema_utils.py +0 -0
  393. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/secret_protocol.py +0 -0
  394. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/secret_service.py +0 -0
  395. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/serve.py +0 -0
  396. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_buffering_function.py +0 -0
  397. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_filter_pushdown.py +0 -0
  398. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_function.py +0 -0
  399. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_in_out_function.py +0 -0
  400. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/__init__.py +0 -0
  401. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/_duckdb_compat.py +0 -0
  402. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/client.py +0 -0
  403. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/protocol.py +0 -0
  404. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/server.py +0 -0
  405. {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/worker.py +0 -0
  406. {vgi_python-0.27.0 → vgi_python-0.28.1}/wrangler.jsonc +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: vgi-python
3
- Version: 0.27.0
3
+ Version: 0.28.1
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
@@ -180,6 +180,30 @@ Statistics are transmitted via the `catalog_table_column_statistics_get` RPC met
180
180
 
181
181
  Cache TTL is carried as IPC batch `custom_metadata` with key `cache_max_age_seconds`.
182
182
 
183
+ ## Reading Statistics From a Client
184
+
185
+ DuckDB is not the only caller. A pure-Python [`Client`][vgi.client.Client] reads the same statistics with
186
+ `table_column_statistics`, which issues the RPC and decodes the sparse-union batch for you:
187
+
188
+ ```python test="skip"
189
+ stats = client.table_column_statistics(
190
+ attach_opaque_data=attach.attach_opaque_data,
191
+ schema_name="main",
192
+ name="events",
193
+ )
194
+ for column in stats:
195
+ print(column.column_name, column.min, column.max, column.distinct_count)
196
+ ```
197
+
198
+ Workers may also inline statistics on `TableInfo.column_statistics` rather than serving them
199
+ lazily; `TableInfo.supports_column_statistics` advertises which. Prefer the inlined copy when
200
+ it is present and fall back to the call above.
201
+
202
+ To decode bytes you already hold, use `deserialize_column_statistics` — the inverse of
203
+ `serialize_column_statistics`. The `cache_max_age_seconds` TTL travels in the batch's custom
204
+ metadata rather than in the statistics, so read it with `deserialize_record_batch` if you
205
+ need it.
206
+
183
207
  ## Capability Flags
184
208
 
185
209
  Statistics are opt-in at two levels:
@@ -213,6 +213,31 @@ ATTACH 'buffers' (TYPE vgi, LOCATION 'uv run row_count_worker.py');
213
213
  SELECT * FROM buffers.row_count((SELECT * FROM big_table));
214
214
  ```
215
215
 
216
+ ### Getting the input in order
217
+
218
+ The sink runs in parallel across DuckDB threads, so `process` sees batches in no particular order.
219
+ When output depends on input *order* — a running total, a row pattern match — set
220
+ `Meta.requires_input_batch_index = True` and each `process` call receives `params.batch_index`, a
221
+ monotonic ordinal you can sort the buffered batches by in `finalize`.
222
+
223
+ Not every source can supply one (a base table scan can; `range()` and `VALUES` cannot). When it
224
+ cannot, the extension serializes the sink and numbers the batches itself, so a worker always gets a
225
+ valid index and callers never have to wrap their input. This example reports the indices it saw, one
226
+ row per buffered batch:
227
+
228
+ ```python
229
+ --8<-- "examples/batch_index_worker.py"
230
+ ```
231
+
232
+ ```sql
233
+ ATTACH 'bi' (TYPE vgi, LOCATION 'uv run batch_index_worker.py');
234
+ SELECT * FROM bi.batch_indexes((SELECT * FROM range(5000))) ORDER BY batch_index;
235
+ ```
236
+
237
+ The alternative, `Meta.sink_order_dependent = True`, delivers ordered input by forcing a
238
+ single-threaded sink. It works with any source but gives up parallel ingest, which is usually the
239
+ bulk of a buffering query's wall time. The two flags are mutually exclusive.
240
+
216
241
  ??? info "Buffering vs. table-in-out"
217
242
  Both consume a relation, but a **table-in-out** function emits *per input batch* and never
218
243
  holds the whole input — use it for streaming transforms (filter, enrich, reshape). Reach for
@@ -0,0 +1,142 @@
1
+ # /// script
2
+ # requires-python = ">=3.13"
3
+ # dependencies = ["vgi-python"]
4
+ # ///
5
+ """A buffering function that asks for input order, and reports what it got.
6
+
7
+ A buffering sink normally runs in parallel across DuckDB threads, so batches
8
+ arrive in no particular order. Setting ``Meta.requires_input_batch_index`` asks
9
+ for DuckDB's per-chunk index alongside each batch, which is what lets a worker
10
+ put the input back in order — the prerequisite for anything order-sensitive,
11
+ like row pattern matching or a running total.
12
+
13
+ Not every source can supply one: a base table scan can, while ``range()`` and
14
+ ``VALUES`` cannot. When it cannot, the extension serializes the sink and numbers
15
+ the batches itself, so a worker sees a valid monotonic index either way and never
16
+ has to care which route produced it.
17
+
18
+ This function emits one row per buffered batch — its index and its row count — so
19
+ the guarantee is directly observable:
20
+
21
+ ATTACH 'bi' (TYPE vgi, LOCATION 'uv run batch_index_worker.py');
22
+ SELECT * FROM bi.batch_indexes((SELECT * FROM range(5000))) ORDER BY batch_index;
23
+ """
24
+
25
+ from dataclasses import dataclass
26
+ from typing import Annotated
27
+
28
+ import pyarrow as pa
29
+ from vgi_rpc import ArrowSerializableDataclass
30
+
31
+ from vgi import Arg, Worker
32
+ from vgi.arguments import TableInput
33
+ from vgi.catalog import Catalog, Schema
34
+ from vgi.invocation import BindResponse
35
+ from vgi.table_buffering_function import OutputCollector, TableBufferingFunction, TableBufferingParams
36
+ from vgi.table_function import BindParams
37
+
38
+ _RESULT = pa.schema([("batch_index", pa.int64()), ("rows", pa.int64())])
39
+
40
+ # One log entry per buffered batch: the index DuckDB gave us, and the batch size.
41
+ _NS = b"batches"
42
+
43
+
44
+ @dataclass(slots=True, frozen=True, kw_only=True)
45
+ class BatchIndexArgs:
46
+ """Arguments: the input table whose batches should be reported."""
47
+
48
+ data: Annotated[TableInput, Arg(0, doc="Input table")]
49
+
50
+
51
+ @dataclass(kw_only=True)
52
+ class DrainState(ArrowSerializableDataclass):
53
+ """Per-finalize-stream cursor: emit the report once, then finish."""
54
+
55
+ done: bool = False
56
+
57
+
58
+ class BatchIndexes(TableBufferingFunction[BatchIndexArgs, DrainState]):
59
+ """Report the batch index and row count of every buffered input batch."""
60
+
61
+ class Meta:
62
+ """Function metadata."""
63
+
64
+ name = "batch_indexes"
65
+ # Ask for DuckDB's per-chunk index. Mutually exclusive with
66
+ # sink_order_dependent, which orders the input by serializing the sink
67
+ # instead of by numbering it.
68
+ requires_input_batch_index = True
69
+
70
+ @classmethod
71
+ def on_bind(cls, params: BindParams[BatchIndexArgs]) -> BindResponse:
72
+ """Output shape is fixed: one row per input batch."""
73
+ return BindResponse(output_schema=_RESULT)
74
+
75
+ @classmethod
76
+ def process(cls, batch: pa.RecordBatch, params: TableBufferingParams[BatchIndexArgs]) -> bytes:
77
+ """Sink: record this batch's index and size.
78
+
79
+ ``params.batch_index`` is populated because ``Meta`` asked for it; -1
80
+ stands in for the absent case so an older host that does not supply one
81
+ degrades to a visible marker instead of a crash.
82
+ """
83
+ index = params.batch_index if params.batch_index is not None else -1
84
+ payload = index.to_bytes(8, "little", signed=True) + batch.num_rows.to_bytes(8, "little")
85
+ params.storage.state_append(_NS, b"", payload)
86
+ return params.execution_id
87
+
88
+ @classmethod
89
+ def combine(cls, state_ids: list[bytes], params: TableBufferingParams[BatchIndexArgs]) -> list[bytes]:
90
+ """Nothing to reduce: the log already holds one entry per batch."""
91
+ return [params.execution_id]
92
+
93
+ @classmethod
94
+ def initial_finalize_state(
95
+ cls, finalize_state_id: bytes, params: TableBufferingParams[BatchIndexArgs]
96
+ ) -> DrainState:
97
+ """One cursor per finalize stream."""
98
+ return DrainState()
99
+
100
+ @classmethod
101
+ def finalize(
102
+ cls,
103
+ params: TableBufferingParams[BatchIndexArgs],
104
+ finalize_state_id: bytes,
105
+ state: DrainState,
106
+ out: OutputCollector,
107
+ ) -> None:
108
+ """Source: emit the report, ordered by batch index."""
109
+ if state.done:
110
+ out.finish()
111
+ return
112
+ entries = [
113
+ (
114
+ int.from_bytes(value[:8], "little", signed=True),
115
+ int.from_bytes(value[8:16], "little"),
116
+ )
117
+ for _id, value in params.storage.state_log_scan(_NS, b"")
118
+ ]
119
+ entries.sort()
120
+ out.emit(
121
+ pa.RecordBatch.from_pydict(
122
+ {
123
+ "batch_index": [index for index, _rows in entries],
124
+ "rows": [rows for _index, rows in entries],
125
+ },
126
+ schema=params.output_schema,
127
+ )
128
+ )
129
+ state.done = True
130
+
131
+
132
+ class BatchIndexWorker(Worker):
133
+ """A worker exposing the ``bi`` catalog."""
134
+
135
+ catalog = Catalog(
136
+ name="bi",
137
+ schemas=[Schema(name="main", functions=[BatchIndexes])],
138
+ )
139
+
140
+
141
+ if __name__ == "__main__":
142
+ BatchIndexWorker().run()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vgi-python"
3
- version = "0.27.0"
3
+ version = "0.28.1"
4
4
  description = "Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -12,6 +12,7 @@ from vgi_rpc.utils import deserialize_record_batch
12
12
  from vgi.catalog.catalog_interface import (
13
13
  ColumnStatistics,
14
14
  TableColumnStatisticsResult,
15
+ deserialize_column_statistics,
15
16
  serialize_column_statistics,
16
17
  )
17
18
  from vgi.catalog.descriptors import ColumnStatisticsInput, Table
@@ -199,6 +200,91 @@ class TestSerializeColumnStatistics:
199
200
  assert min_type.num_fields == 1
200
201
 
201
202
 
203
+ # ============================================================================
204
+ # deserialize_column_statistics
205
+ # ============================================================================
206
+
207
+
208
+ def _roundtrip(stats: list[ColumnStatistics]) -> list[ColumnStatistics]:
209
+ return deserialize_column_statistics(serialize_column_statistics(stats))
210
+
211
+
212
+ class TestDeserializeColumnStatistics:
213
+ """Tests that deserialization is a faithful inverse of serialization."""
214
+
215
+ def test_empty_batch(self) -> None:
216
+ assert _roundtrip([]) == []
217
+
218
+ def test_int_column_roundtrips(self) -> None:
219
+ stats = [
220
+ ColumnStatistics(
221
+ column_name="id",
222
+ min=pa.scalar(1, pa.int64()),
223
+ max=pa.scalar(100, pa.int64()),
224
+ has_null=False,
225
+ has_not_null=True,
226
+ distinct_count=100,
227
+ )
228
+ ]
229
+ (got,) = _roundtrip(stats)
230
+ assert got.column_name == "id"
231
+ assert got.min.as_py() == 1 # type: ignore[union-attr]
232
+ assert got.max.as_py() == 100 # type: ignore[union-attr]
233
+ assert got.has_null is False
234
+ assert got.has_not_null is True
235
+ assert got.distinct_count == 100
236
+
237
+ def test_string_column_roundtrips_with_string_fields(self) -> None:
238
+ stats = [
239
+ ColumnStatistics(
240
+ column_name="name",
241
+ min=pa.scalar("Alice"),
242
+ max=pa.scalar("Zoe"),
243
+ contains_unicode=True,
244
+ max_string_length=5,
245
+ )
246
+ ]
247
+ (got,) = _roundtrip(stats)
248
+ assert got.min.as_py() == "Alice" # type: ignore[union-attr]
249
+ assert got.max.as_py() == "Zoe" # type: ignore[union-attr]
250
+ assert got.contains_unicode is True
251
+ assert got.max_string_length == 5
252
+
253
+ def test_unknown_min_max_stays_none(self) -> None:
254
+ """A column with no min/max must come back as None, not a null scalar."""
255
+ (got,) = _roundtrip([ColumnStatistics(column_name="opaque")])
256
+ assert got.min is None
257
+ assert got.max is None
258
+ assert got.distinct_count is None
259
+ assert got.contains_unicode is None
260
+ assert got.max_string_length is None
261
+
262
+ def test_mixed_types_keep_their_own_union_arm(self) -> None:
263
+ """Several column types share one sparse union; each must decode to its own type."""
264
+ stats = [
265
+ ColumnStatistics(column_name="i", min=pa.scalar(1, pa.int64()), max=pa.scalar(9, pa.int64())),
266
+ ColumnStatistics(column_name="s", min=pa.scalar("a"), max=pa.scalar("z")),
267
+ ColumnStatistics(column_name="f", min=pa.scalar(0.5, pa.float64()), max=pa.scalar(9.5, pa.float64())),
268
+ ]
269
+ got = _roundtrip(stats)
270
+ assert [s.column_name for s in got] == ["i", "s", "f"]
271
+ assert got[0].min.as_py() == 1 # type: ignore[union-attr]
272
+ assert got[1].min.as_py() == "a" # type: ignore[union-attr]
273
+ assert got[2].max.as_py() == 9.5 # type: ignore[union-attr]
274
+
275
+ def test_order_is_preserved(self) -> None:
276
+ stats = [ColumnStatistics(column_name=f"c{i}", min=pa.scalar(i, pa.int64())) for i in range(6)]
277
+ assert [s.column_name for s in _roundtrip(stats)] == [f"c{i}" for i in range(6)]
278
+
279
+ def test_cache_ttl_rides_in_batch_metadata_not_the_stats(self) -> None:
280
+ """The TTL is out-of-band; deserialization returns only the statistics."""
281
+ data = serialize_column_statistics([ColumnStatistics(column_name="a")], cache_max_age_seconds=60)
282
+ _, metadata = deserialize_record_batch(data)
283
+ assert metadata is not None
284
+ assert metadata[b"cache_max_age_seconds"] == b"60"
285
+ assert len(deserialize_column_statistics(data)) == 1
286
+
287
+
202
288
  # ============================================================================
203
289
  # ColumnStatisticsInput
204
290
  # ============================================================================
@@ -9,6 +9,8 @@ input or producing output).
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import sys
13
+
12
14
  import pyarrow as pa
13
15
  import pytest
14
16
 
@@ -17,6 +19,16 @@ from vgi.client.catalog_mixin import CatalogClientError
17
19
  from vgi.client.client import Client, ClientError
18
20
 
19
21
 
22
+ def _exits_with(code: int) -> list[str]:
23
+ """Argv for a worker that exits immediately with ``code``, reading no input.
24
+
25
+ A real process rather than the shell's ``exit`` builtin: workers are spawned
26
+ without a shell, so a builtin would not resolve. Returned as argv because no
27
+ one string quoting convention survives both POSIX and Windows.
28
+ """
29
+ return [sys.executable, "-c", f"raise SystemExit({code})"]
30
+
31
+
20
32
  def _make_test_batch() -> pa.RecordBatch:
21
33
  """Create a simple test batch for invoking functions."""
22
34
  return pa.RecordBatch.from_pydict({"x": [1, 2, 3]})
@@ -27,9 +39,9 @@ class TestClientWorkerExitHandling:
27
39
 
28
40
  def test_client_raises_error_when_worker_exits_immediately(self) -> None:
29
41
  """Client raises ClientError when worker exits before accepting input."""
30
- # Use 'exit 1' which immediately terminates without reading stdin
31
- # pool=None required because 'exit 1' is a shell command, not a binary
32
- client = Client("exit 1", pool=None)
42
+ # A worker that terminates without ever reading stdin.
43
+ # pool=None: the pool is for real, reusable workers.
44
+ client = Client(_exits_with(1), pool=None)
33
45
 
34
46
  # Worker exits before sending any output - detected either via BrokenPipeError
35
47
  # or via early exit check when reading fails
@@ -48,9 +60,8 @@ class TestClientWorkerExitHandling:
48
60
 
49
61
  def test_client_raises_error_with_useful_message(self) -> None:
50
62
  """Client error message includes useful debugging information."""
51
- # Use a specific exit code
52
- # pool=None required because 'exit 42' is a shell command, not a binary
53
- client = Client("exit 42", pool=None)
63
+ # Use a specific exit code so the message can be checked for it.
64
+ client = Client(_exits_with(42), pool=None)
54
65
 
55
66
  try:
56
67
  client.start()
@@ -84,8 +95,8 @@ class TestCatalogMixinWorkerExitHandling:
84
95
 
85
96
  def test_catalog_invoke_raises_error_when_worker_exits(self) -> None:
86
97
  """Catalog invoke raises CatalogClientError when worker exits."""
87
- # Use 'exit 1' which immediately terminates without reading stdin
88
- client = Client("exit 1")
98
+ # A worker that terminates without ever reading stdin.
99
+ client = Client(_exits_with(1))
89
100
 
90
101
  # Worker exits - detected either via BrokenPipeError (shows "terminated
91
102
  # unexpectedly") or via read failure (shows "Failed to read catalog result")
@@ -94,7 +105,7 @@ class TestCatalogMixinWorkerExitHandling:
94
105
 
95
106
  def test_catalog_invoke_error_includes_useful_info(self) -> None:
96
107
  """Catalog error message includes useful debugging information."""
97
- client = Client("exit 42")
108
+ client = Client(_exits_with(42))
98
109
 
99
110
  try:
100
111
  list(client.catalogs())
@@ -0,0 +1,101 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ # ruff: noqa: S101, D102, D103
4
+ """Tests for ``Client.stop(force=True)``.
5
+
6
+ A graceful ``stop()`` shuts the RPC connection down and waits for the worker, so it cannot
7
+ be used to abandon a call that has overrun its budget — the wait is exactly what the caller
8
+ is trying to escape. ``force=True`` SIGKILLs a direct subprocess worker first, which unblocks
9
+ any thread waiting on its output immediately.
10
+
11
+ Only *direct* subprocess workers own their process; pooled workers are returned to the pool
12
+ and HTTP/TCP connections are already prompt to close, so those paths must accept ``force``
13
+ without changing behaviour.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ import time
20
+ from typing import Any
21
+
22
+ import pyarrow as pa
23
+ import pytest
24
+
25
+ from vgi.arguments import Arguments
26
+ from vgi.client.client import Client, _default_pool
27
+
28
+ # Long enough that a graceful teardown would have to wait on it if the worker were stuck.
29
+ _SLEEP_MS = 5_000
30
+
31
+
32
+ def _started_client(**kwargs: Any) -> Client:
33
+ client = Client("vgi-fixture-worker", **kwargs)
34
+ client.start()
35
+ return client
36
+
37
+
38
+ def test_force_kills_a_direct_subprocess(tmp_path: Any) -> None:
39
+ """The worker process is signalled, not asked to exit."""
40
+ client = _started_client(pool=None)
41
+ assert client._primary is not None # noqa: SLF001 - a started client always has one
42
+ proc = client._primary.proc # noqa: SLF001 - asserting the process really died
43
+ assert proc is not None
44
+ assert proc.poll() is None, "worker should be running"
45
+
46
+ returncode = client.stop(force=True)
47
+ assert returncode is not None
48
+ if sys.platform == "win32":
49
+ # Windows has no signals: TerminateProcess sets an ordinary non-zero code.
50
+ assert returncode != 0, "a killed process should not report a clean exit"
51
+ else:
52
+ assert returncode < 0, "a killed process reports a negative (signal) exit code"
53
+ assert proc.poll() is not None, "worker process should be reaped"
54
+
55
+
56
+ def test_force_returns_promptly_mid_stream(tmp_path: Any) -> None:
57
+ """Killing while a slow scan is in flight must not wait for the scan."""
58
+ probe = tmp_path / "probe.txt"
59
+ client = _started_client(pool=None)
60
+ gen = client.table_function(
61
+ function_name="slow_cancellable",
62
+ schema_name="main",
63
+ arguments=Arguments(positional=(pa.scalar(str(probe)),), named={"sleep_ms": pa.scalar(_SLEEP_MS)}),
64
+ )
65
+ next(gen) # worker is now mid-stream, sleeping between batches
66
+
67
+ started = time.monotonic()
68
+ client.stop(force=True)
69
+ elapsed = time.monotonic() - started
70
+ assert elapsed < _SLEEP_MS / 1000.0, f"forced stop waited {elapsed:.2f}s on a sleeping worker"
71
+
72
+
73
+ def test_graceful_stop_is_unchanged() -> None:
74
+ """Force defaults to False and still reports a clean exit."""
75
+ client = _started_client(pool=None)
76
+ assert client.stop() == 0
77
+
78
+
79
+ def test_force_is_accepted_by_pooled_workers() -> None:
80
+ """Pooled workers have no proc to kill, so force must be inert rather than an error."""
81
+ client = _started_client(pool=_default_pool)
82
+ assert client._primary is not None # noqa: SLF001 - a started client always has one
83
+ assert client._primary.proc is None # noqa: SLF001 - documents why force is inert here
84
+ assert client.stop(force=True) == 0
85
+
86
+
87
+ def test_stop_still_requires_a_started_client() -> None:
88
+ client = Client("vgi-fixture-worker", pool=None)
89
+ with pytest.raises(Exception, match="not started"):
90
+ client.stop(force=True)
91
+
92
+
93
+ def test_client_can_restart_after_a_forced_stop() -> None:
94
+ """Force must leave the client in the same reusable state as a graceful stop."""
95
+ client = _started_client(pool=None)
96
+ client.stop(force=True)
97
+ client.start()
98
+ try:
99
+ assert client._primary is not None # noqa: SLF001 - restart sanity check
100
+ finally:
101
+ client.stop()
@@ -0,0 +1,73 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ # ruff: noqa: S101, D102, D103
4
+ """Tests for ``Client.table_column_statistics``.
5
+
6
+ Workers may inline statistics on ``TableInfo`` or serve them lazily through the per-table
7
+ ``catalog_table_column_statistics_get`` RPC. This wrapper fetches the lazy form and decodes
8
+ it, so a client never has to reach past the public API to reimplement the sparse-union wire
9
+ layout. Driven against the real ``vgi-fixture-worker`` over every client transport.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ import pytest
17
+
18
+ from vgi.catalog import ColumnStatistics, deserialize_column_statistics
19
+
20
+
21
+ @pytest.fixture
22
+ def attached(client_transport: Any) -> Any:
23
+ """Yield ``(client, attach_opaque_data)`` for the example catalog."""
24
+ with client_transport() as client:
25
+ res = client.catalog_attach(name="example", data_version_spec=None, implementation_version=None)
26
+ yield client, res.attach_opaque_data
27
+
28
+
29
+ def test_returns_typed_statistics(attached: Any) -> None:
30
+ client, aod = attached
31
+ stats = client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="numbers")
32
+ assert stats
33
+ assert all(isinstance(s, ColumnStatistics) for s in stats)
34
+
35
+
36
+ def test_decodes_min_max_for_a_known_table(attached: Any) -> None:
37
+ """`numbers` holds integers 0-99, so its bounds are known exactly."""
38
+ client, aod = attached
39
+ stats = client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="numbers")
40
+ value = next(s for s in stats if s.column_name == "value")
41
+ assert value.min is not None
42
+ assert value.max is not None
43
+ assert value.min.as_py() == 0
44
+ assert value.max.as_py() == 99
45
+
46
+
47
+ def test_matches_the_inlined_statistics(attached: Any) -> None:
48
+ """The lazy RPC and the copy inlined on TableInfo must agree.
49
+
50
+ ``TableInfo.column_statistics`` arrives client-side as the raw wire bytes, so this is
51
+ also the case that motivates a public deserializer: without one, a caller holding an
52
+ inlined blob has no supported way to read it.
53
+ """
54
+ client, aod = attached
55
+ info = client.table_get(attach_opaque_data=aod, schema_name="data", name="numbers")
56
+ assert info is not None
57
+ if not info.column_statistics:
58
+ pytest.skip("worker does not inline statistics for this table")
59
+ fetched = client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="numbers")
60
+ inlined = {s.column_name: s for s in deserialize_column_statistics(info.column_statistics)}
61
+ assert {s.column_name for s in fetched} == set(inlined)
62
+ for s in fetched:
63
+ other = inlined[s.column_name]
64
+ assert (s.min is None) == (other.min is None)
65
+ if s.min is not None and other.min is not None:
66
+ assert s.min.as_py() == other.min.as_py()
67
+ assert s.has_null == other.has_null
68
+
69
+
70
+ def test_unknown_table_returns_empty_list(attached: Any) -> None:
71
+ """A missing table yields no statistics rather than raising."""
72
+ client, aod = attached
73
+ assert client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="no_such_table") == []
@@ -5,6 +5,7 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  import os
8
+ import sys
8
9
  from unittest.mock import patch
9
10
 
10
11
  import pyarrow as pa
@@ -14,6 +15,16 @@ from vgi.arguments import Arguments
14
15
  from vgi.client.client import Client, ClientError
15
16
 
16
17
 
18
+ def _stderr_worker(*lines: str) -> list[str]:
19
+ """Argv for a worker that writes ``lines`` to stderr and exits non-zero.
20
+
21
+ An argv list rather than a command string: the ``-c`` body carries spaces and
22
+ quotes, which no single quoting convention survives on both POSIX and Windows.
23
+ """
24
+ body = "import sys; " + "".join(f"sys.stderr.write({line!r} + chr(10)); " for line in lines)
25
+ return [sys.executable, "-c", body + "sys.stderr.flush(); sys.exit(1)"]
26
+
27
+
17
28
  class TestWorkerDebugEnvVar:
18
29
  """Tests for VGI_WORKER_DEBUG environment variable behavior."""
19
30
 
@@ -53,14 +64,7 @@ class TestStderrInErrorMessages:
53
64
 
54
65
  def test_error_includes_stderr_on_worker_failure(self) -> None:
55
66
  """Error messages should include stderr when worker fails (non-pooled)."""
56
- worker_script = (
57
- 'python -c "'
58
- "import sys; "
59
- "sys.stderr.write('Debug: bind starting\\n'); "
60
- "sys.stderr.write('Error: function not found\\n'); "
61
- "sys.stderr.flush(); "
62
- 'sys.exit(1)"'
63
- )
67
+ worker_script = _stderr_worker("Debug: bind starting", "Error: function not found")
64
68
 
65
69
  client = Client(worker_script, pool=None)
66
70
  with pytest.raises(ClientError) as exc_info:
@@ -80,7 +84,7 @@ class TestStderrInErrorMessages:
80
84
 
81
85
  def test_error_no_stderr_section_when_passthrough(self) -> None:
82
86
  """Error messages should NOT include 'Worker stderr:' when passthrough is enabled."""
83
- worker_script = "python -c \"import sys; sys.stderr.write('Debug info\\n'); sys.stderr.flush(); sys.exit(1)\""
87
+ worker_script = _stderr_worker("Debug info")
84
88
 
85
89
  client = Client(worker_script, passthrough_stderr=True, pool=None)
86
90
  with pytest.raises(ClientError) as exc_info:
@@ -99,9 +103,7 @@ class TestStderrInErrorMessages:
99
103
 
100
104
  def test_stderr_enrichment_on_table_in_out_function(self) -> None:
101
105
  """table_in_out_function errors should include stderr."""
102
- worker_script = (
103
- "python -c \"import sys; sys.stderr.write('worker log line\\n'); sys.stderr.flush(); sys.exit(1)\""
104
- )
106
+ worker_script = _stderr_worker("worker log line")
105
107
 
106
108
  batch = pa.RecordBatch.from_pydict({"x": [1, 2, 3]})
107
109
  client = Client(worker_script, pool=None)
@@ -120,7 +122,7 @@ class TestStderrInErrorMessages:
120
122
 
121
123
  def test_stderr_enrichment_on_scalar_function(self) -> None:
122
124
  """scalar_function errors should include stderr."""
123
- worker_script = "python -c \"import sys; sys.stderr.write('scalar debug\\n'); sys.stderr.flush(); sys.exit(1)\""
125
+ worker_script = _stderr_worker("scalar debug")
124
126
 
125
127
  batch = pa.RecordBatch.from_pydict({"x": [1, 2, 3]})
126
128
  client = Client(worker_script, pool=None)
@@ -4,6 +4,7 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import sys
7
8
  import time
8
9
 
9
10
  import pyarrow as pa
@@ -249,14 +250,17 @@ class TestWorkerStderrCapture:
249
250
  def test_stderr_available_on_error(self) -> None:
250
251
  """Should be able to access stderr after an error occurs."""
251
252
  # Use a worker script that writes to stderr then fails
252
- worker_script = (
253
- 'python -c "'
253
+ # Argv, not a command string: the -c body carries spaces and quotes, which
254
+ # no single quoting convention survives on both POSIX and Windows.
255
+ worker_script = [
256
+ sys.executable,
257
+ "-c",
254
258
  "import sys; "
255
- "sys.stderr.write('Debug: worker starting\\n'); "
256
- "sys.stderr.write('Error: something went wrong\\n'); "
259
+ "sys.stderr.write('Debug: worker starting' + chr(10)); "
260
+ "sys.stderr.write('Error: something went wrong' + chr(10)); "
257
261
  "sys.stderr.flush(); "
258
- 'sys.exit(1)"'
259
- )
262
+ "sys.exit(1)",
263
+ ]
260
264
 
261
265
  client = Client(worker_script, pool=None)
262
266
  client.start()