vgi-python 0.42.0__py3-none-any.whl → 0.42.1__py3-none-any.whl

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.
vgi/client/client.py CHANGED
@@ -2757,7 +2757,10 @@ class Client(CatalogClientMixin, AggregateClientMixin):
2757
2757
  arguments: Arguments | None = None,
2758
2758
  function_type: FunctionType = FunctionType.TABLE,
2759
2759
  settings: dict[str, Any] | None = None,
2760
+ secrets: dict[str, Any] | None = None,
2760
2761
  transaction_opaque_data: bytes | None = None,
2762
+ at_unit: str | None = None,
2763
+ at_value: str | None = None,
2761
2764
  ) -> BindResponse:
2762
2765
  """Resolve a function's bind response without running init()/process().
2763
2766
 
@@ -2789,8 +2792,16 @@ class Client(CatalogClientMixin, AggregateClientMixin):
2789
2792
  settings: Optional dictionary of settings/pragmas — some
2790
2793
  functions' output schema depends on setting values (see
2791
2794
  `Meta.required_settings`).
2795
+ secrets: Optional dictionary of secret name to value pairs, as for
2796
+ `table_function` — a function whose bind resolves secrets
2797
+ (e.g. a secret-backed source) needs them here too.
2792
2798
  transaction_opaque_data: Optional unique identifier for the
2793
2799
  DuckDB transaction.
2800
+ at_unit: Optional time travel unit (e.g. 'timestamp', 'version'):
2801
+ bind the function as of a past point, as `table_function`'s
2802
+ `at_unit` does, so a versioned table's historical schema can be
2803
+ discovered. `None` binds live.
2804
+ at_value: Optional time travel value, paired with `at_unit`.
2794
2805
 
2795
2806
  Returns:
2796
2807
  [`BindResponse`][] with `output_schema` and any opaque bind data —
@@ -2814,8 +2825,10 @@ class Client(CatalogClientMixin, AggregateClientMixin):
2814
2825
  arguments=arguments,
2815
2826
  function_type=function_type,
2816
2827
  settings=settings,
2817
- secrets=None,
2828
+ secrets=secrets,
2818
2829
  transaction_opaque_data=transaction_opaque_data,
2830
+ at_unit=at_unit,
2831
+ at_value=at_value,
2819
2832
  )
2820
2833
  return self._do_bind(self._primary.proxy, bind_request, None)
2821
2834
  except ClientError as e:
@@ -3273,15 +3286,23 @@ class Client(CatalogClientMixin, AggregateClientMixin):
3273
3286
  # AnnotatedBatch (not just .batch) so custom_metadata survives the
3274
3287
  # cross-thread handoff for batch_metadata_callback below.
3275
3288
  output_queue: Queue[AnnotatedBatch | BaseException | None] = Queue()
3289
+ # Set when the consumer stops before every worker finished (the
3290
+ # generator was closed early, or a worker failed). Each reader thread is
3291
+ # its stream's only reader, so it is also the one that ends the stream:
3292
+ # a producer stream advances one tick per batch, so a reader notices the
3293
+ # flag within one batch and cancels from its own thread. Ending it from
3294
+ # the consumer's thread instead would put two readers on one pipe.
3295
+ stop = threading.Event()
3276
3296
 
3277
3297
  def read_worker_output(worker: WorkerConnection) -> None:
3278
3298
  """Thread function that reads all output from a single worker."""
3279
3299
  try:
3280
- if worker.stream is None:
3300
+ stream = worker.stream
3301
+ if stream is None:
3281
3302
  output_queue.put(None)
3282
3303
  return
3283
3304
 
3284
- for output in worker.stream:
3305
+ for output in stream:
3285
3306
  _logger.debug(
3286
3307
  "received_output_from_worker worker_index=%s num_rows=%s",
3287
3308
  worker.worker_index,
@@ -3289,6 +3310,10 @@ class Client(CatalogClientMixin, AggregateClientMixin):
3289
3310
  )
3290
3311
  if output.batch.num_rows > 0:
3291
3312
  output_queue.put(output)
3313
+ if stop.is_set():
3314
+ _logger.debug("table_function_worker_cancelled worker_index=%s", worker.worker_index)
3315
+ stream.cancel()
3316
+ break
3292
3317
 
3293
3318
  output_queue.put(None) # Signal completion
3294
3319
  except StopIteration:
@@ -3308,41 +3333,59 @@ class Client(CatalogClientMixin, AggregateClientMixin):
3308
3333
  thread.start()
3309
3334
  threads.append(thread)
3310
3335
 
3311
- # Collect outputs from all workers until all are done
3312
- workers_finished = 0
3313
- while workers_finished < num_workers:
3314
- result = output_queue.get()
3315
-
3316
- # Check for exceptions from worker threads
3317
- if isinstance(result, BaseException):
3318
- if isinstance(result, RpcError):
3319
- raise ClientError.from_rpc_error(result) from result
3320
- raise ClientError(f"Worker thread failed: {result}") from result
3321
-
3322
- # None signals a worker finished
3323
- if result is None:
3324
- workers_finished += 1
3325
- _logger.debug(
3326
- "worker_finished workers_finished=%s total_workers=%s",
3327
- workers_finished,
3328
- num_workers,
3329
- )
3330
- continue
3331
-
3332
- if batch_metadata_callback is not None:
3333
- batch_metadata_callback(result.custom_metadata)
3334
- yield result.batch
3335
-
3336
- self._join_threads(threads)
3337
- _logger.debug("all_table_function_workers_complete")
3336
+ # Collect outputs from all workers until all are done. The finally
3337
+ # block also runs when the consumer closes the generator early
3338
+ # (GeneratorExit at the yield) or a worker fails: the reader threads are
3339
+ # told to stop and joined before any stream is touched here.
3340
+ completed = False
3341
+ try:
3342
+ workers_finished = 0
3343
+ while workers_finished < num_workers:
3344
+ result = output_queue.get()
3345
+
3346
+ # Check for exceptions from worker threads
3347
+ if isinstance(result, BaseException):
3348
+ if isinstance(result, RpcError):
3349
+ raise ClientError.from_rpc_error(result) from result
3350
+ raise ClientError(f"Worker thread failed: {result}") from result
3351
+
3352
+ # None signals a worker finished
3353
+ if result is None:
3354
+ workers_finished += 1
3355
+ _logger.debug(
3356
+ "worker_finished workers_finished=%s total_workers=%s",
3357
+ workers_finished,
3358
+ num_workers,
3359
+ )
3360
+ continue
3338
3361
 
3339
- # Close streams and secondary workers
3340
- for worker in all_workers:
3341
- if worker.stream is not None:
3342
- worker.stream.close()
3343
- worker.stream = None
3344
- self._close_secondary_workers()
3345
- _logger.debug("parallel_table_function_complete")
3362
+ if batch_metadata_callback is not None:
3363
+ batch_metadata_callback(result.custom_metadata)
3364
+ yield result.batch
3365
+ completed = True
3366
+ finally:
3367
+ if not completed:
3368
+ stop.set()
3369
+ self._join_threads(threads)
3370
+ _logger.debug("all_table_function_workers_complete completed=%s", completed)
3371
+
3372
+ # Close streams and secondary workers. A stream a reader cancelled is
3373
+ # already closed, so close() is a no-op for it.
3374
+ for worker, thread in zip(all_workers, threads, strict=True):
3375
+ if thread.is_alive():
3376
+ # Still reading after the join timeout: the stream's state is
3377
+ # unknown and its reader still owns it. Kill the worker so
3378
+ # neither that reader nor a later stop() blocks on the pipe.
3379
+ _logger.warning("table_function_worker_killed worker_index=%s", worker.worker_index)
3380
+ if worker.proc is not None:
3381
+ worker.proc.kill()
3382
+ worker.stream = None
3383
+ continue
3384
+ if worker.stream is not None:
3385
+ worker.stream.close()
3386
+ worker.stream = None
3387
+ self._close_secondary_workers()
3388
+ _logger.debug("parallel_table_function_complete")
3346
3389
 
3347
3390
  # ==================================================================
3348
3391
  # Custom COPY formats
vgi/filter_v2.py CHANGED
@@ -117,6 +117,34 @@ class FilterV2Error(ValueError):
117
117
  """A Filter Encoding v2 document is malformed or cannot be evaluated."""
118
118
 
119
119
 
120
+ def _payload_nbytes(batch: pa.RecordBatch) -> int:
121
+ """Size of a filter payload batch, for the payload limit.
122
+
123
+ ``RecordBatch.nbytes`` is not implemented for view types (``string_view``,
124
+ ``binary_view``, ``list_view``) before pyarrow 19 and raises a raw
125
+ ``ArrowTypeError``. Fall back to the total buffer size there, which walks
126
+ every buffer generically and can only overcount (shared buffers), never
127
+ undercount, so the limit still holds.
128
+
129
+ Args:
130
+ batch: The filter payload batch.
131
+
132
+ Returns:
133
+ Its size in bytes, for comparison with ``MAX_PAYLOAD_BYTES``.
134
+
135
+ Raises:
136
+ FilterV2Error: If neither measurement is available for the batch.
137
+ """
138
+ try:
139
+ return int(batch.nbytes)
140
+ except (pa.ArrowTypeError, pa.ArrowNotImplementedError):
141
+ pass
142
+ try:
143
+ return int(batch.get_total_buffer_size())
144
+ except (pa.ArrowTypeError, pa.ArrowNotImplementedError) as exc:
145
+ raise FilterV2Error(f"cannot measure filter payload size: {exc}") from exc
146
+
147
+
120
148
  class PredicateMode(StrEnum):
121
149
  """Whether a predicate must be applied exactly or is only a pruning hint."""
122
150
 
@@ -592,7 +620,7 @@ class _Parser:
592
620
  raise FilterV2Error(f"noncanonical payload field name {name!r}")
593
621
  if name.startswith("type_") and self.batch.column(i)[0].is_valid:
594
622
  raise FilterV2Error(f"{name} must contain a NULL value")
595
- if self.batch.nbytes > MAX_PAYLOAD_BYTES:
623
+ if _payload_nbytes(self.batch) > MAX_PAYLOAD_BYTES:
596
624
  raise FilterV2Error("filter payload exceeds 16 MiB")
597
625
  return parse_evaluation_context(self.batch.schema)
598
626
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vgi-python
3
- Version: 0.42.0
3
+ Version: 0.42.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
@@ -10,7 +10,7 @@ vgi/cache_control.py,sha256=IyBp9gZTCc4Y-4V7Rs05_o5lSEjbR2jt6cddXPU0dKw,7812
10
10
  vgi/copy_from_function.py,sha256=iSkR48b3W9IVQ-HY2I8-5W1QjTtWF7k1GkogHNE7ZMM,7114
11
11
  vgi/copy_to_function.py,sha256=8l0v_DyEF1EnFwinStrZDQ-yNFfGJsv3Mr5o2jG30f0,9279
12
12
  vgi/exceptions.py,sha256=oX_sZc9xGWi7Xf8cJQf89fX19i3ocDEj_V_76GINgBQ,7294
13
- vgi/filter_v2.py,sha256=RnuZAPTlAOhUYiY-D3lYlaMvr8OdaR3MhdjVufI-0Dw,57860
13
+ vgi/filter_v2.py,sha256=ezQ1rDweMJJkow-5mQPG5BYVnN93426ObYPD2KzUNSs,58889
14
14
  vgi/filter_v2_builder.py,sha256=8OmyzDI6GDdH4pYm2sJFMidzKusi0I8IGNipolPpjX0,5535
15
15
  vgi/function.py,sha256=KIfvKk1nzBvRDPXViHNX89tv6gvIMiDj4e8fl6ANqQg,11626
16
16
  vgi/function_storage.py,sha256=FjVwxc1BIjWo-82j8YE5qcU8w66sGjwyGsTmCUhn-Kw,67576
@@ -143,7 +143,7 @@ vgi/client/cli_table.py,sha256=H1xrKZF5yYmSe55YSrLWXGIXPmuGuzQcncEBvqL43zs,26498
143
143
  vgi/client/cli_transaction.py,sha256=TDW0ZrBy8XM_eYSkYk5ewEsnozfzDJOlopd66Fm7OOE,3142
144
144
  vgi/client/cli_utils.py,sha256=ykh7XK-yzcW6WGED2_DhPLIVPSyVG3KUdllV5ztBicw,17458
145
145
  vgi/client/cli_view.py,sha256=0qhxoS91-zFMjq8V_Fb0PNW-YDxERu8-zPpxSFEE-G8,8490
146
- vgi/client/client.py,sha256=GSkcVXQzI0VCmeUf7WY4kR0ZqFtYmeb6EkenarBvX5c,167399
146
+ vgi/client/client.py,sha256=OtrpTHVeM4nbXzJkk77ab_WC5hONuMLoYAD4MlLD5qM,170099
147
147
  vgi/client/errors.py,sha256=cSN65cyQMk3crwZCz1IdPErK3mtlEkTNpu-nuQUDOQE,1633
148
148
  vgi/http/__init__.py,sha256=hlOwOVcoZqmEKVGk7ganOdr5ryRSpEhLBF9sRD7BkYc,608
149
149
  vgi/http/demo_storage.py,sha256=830s-H61thozC3eEuqdnwCpYFfvoJRq9vjyuXa5OURo,8769
@@ -156,8 +156,8 @@ vgi/transactor/_duckdb_compat.py,sha256=sXVZ9JLKAQyGR1BjWczSwdQEavtr-TcZPoVZZnTr
156
156
  vgi/transactor/client.py,sha256=7DTeMksogsw6ANjQjGOPpKYrV76rg4_kGjktMJf54jg,4486
157
157
  vgi/transactor/protocol.py,sha256=v59IvrKnuvwOXvn_HEcBAbCzDVcx0akgKN0R1mChXg0,5034
158
158
  vgi/transactor/server.py,sha256=nzsZQxJZdgappdYX8okrFIrjFtUbdM_Hhskl1fZ2nDY,32553
159
- vgi_python-0.42.0.dist-info/METADATA,sha256=1c3-T4txCgcbPeUyxS-fOuMogZtSRPvbjNDOeau4pSg,25953
160
- vgi_python-0.42.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
161
- vgi_python-0.42.0.dist-info/entry_points.txt,sha256=3Kz1vgodw3pOL_xjtSyDB55-ZRy-U2X-X_Bdr582x0Q,165
162
- vgi_python-0.42.0.dist-info/licenses/LICENSE,sha256=pbJb4zZasP6n5ifEV81wFu017TarjydaYVmGbHcehtY,6103
163
- vgi_python-0.42.0.dist-info/RECORD,,
159
+ vgi_python-0.42.1.dist-info/METADATA,sha256=8JvivAw5wSBb73Xpal2NdQIafuA4enfCQnEQAqs5RL4,25953
160
+ vgi_python-0.42.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
161
+ vgi_python-0.42.1.dist-info/entry_points.txt,sha256=3Kz1vgodw3pOL_xjtSyDB55-ZRy-U2X-X_Bdr582x0Q,165
162
+ vgi_python-0.42.1.dist-info/licenses/LICENSE,sha256=pbJb4zZasP6n5ifEV81wFu017TarjydaYVmGbHcehtY,6103
163
+ vgi_python-0.42.1.dist-info/RECORD,,