vgi-python 0.34.1__py3-none-any.whl → 0.36.0__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.
@@ -1003,7 +1003,10 @@ class FilterEchoPartitionedFunction(TableFunctionGenerator[_FilterEchoPartitione
1003
1003
  end_idx = min(start_idx + chunk, params.args.count)
1004
1004
  work_items.append(struct.pack(">QQ", start_idx, end_idx))
1005
1005
  params.storage.queue_push(work_items)
1006
- return GlobalInitResponse()
1006
+ # No more readers than work items. Left at the default (unbounded) the
1007
+ # client opens one stream per DuckDB thread -- 48 on a 48-core host for at
1008
+ # most MAX_PARTITIONS items, every extra one an init plus an empty drain.
1009
+ return GlobalInitResponse(max_workers=max(1, len(work_items)))
1007
1010
 
1008
1011
  @classmethod
1009
1012
  def initial_state(cls, params: ProcessParams[_FilterEchoPartitionedArgs]) -> _FilterEchoPartitionedState:
@@ -516,7 +516,10 @@ class PartitionedSequenceFunction(
516
516
 
517
517
  # Always enqueue (even if empty) to register the invocation
518
518
  params.storage.queue_push(work_items)
519
- return GlobalInitResponse()
519
+ # No more readers than work items. Left at the default (unbounded) the
520
+ # client opens one stream per DuckDB thread -- 48 on a 48-core host for at
521
+ # most MAX_PARTITIONS items, every extra one an init plus an empty drain.
522
+ return GlobalInitResponse(max_workers=max(1, len(work_items)))
520
523
 
521
524
  @classmethod
522
525
  def initial_state(cls, params: ProcessParams[PartitionedSequenceFunctionArguments]) -> PartitionedSequenceState:
@@ -35,6 +35,7 @@ from vgi_rpc.rpc import OutputCollector
35
35
 
36
36
  from vgi.arguments import Arg
37
37
  from vgi.cache_control import CacheControl
38
+ from vgi.filter_v2 import replaying_accepted_state
38
39
  from vgi.metadata import FunctionExample, PartitionKind
39
40
  from vgi.protocol import PlanResponse, ScanSplit, VgiOutputCollector
40
41
  from vgi.schema_utils import partition_field, schema
@@ -1055,11 +1056,15 @@ class SplitDynamicFilterFunction(TableFunctionGenerator[SplitSequenceArgs, Split
1055
1056
  init = params.init_call
1056
1057
  merged = None
1057
1058
  if init is not None and init.pushdown_filters is not None:
1058
- merged = cls.pushdown_filters(
1059
- init.pushdown_filters,
1060
- join_keys=init.join_keys,
1061
- output_schema=init.output_schema,
1062
- )
1059
+ # The framework validated these at init; re-binding every
1060
+ # predicate through the evaluator on every tick is what made
1061
+ # this fixture cost ~10 ms a batch.
1062
+ with replaying_accepted_state():
1063
+ merged = cls.pushdown_filters(
1064
+ init.pushdown_filters,
1065
+ join_keys=init.join_keys,
1066
+ output_schema=init.output_schema,
1067
+ )
1063
1068
  if merged is None:
1064
1069
  merged = params.current_pushdown_filters
1065
1070
  filter_str = _render_filters_canonical(merged)
@@ -138,9 +138,9 @@ class SubstreamPartialSumFunction(TableInOutFunction[SingleTableArguments, Subst
138
138
  their finalize outputs, so the caller re-aggregates with an outer
139
139
  ``SELECT sum(...)`` to get the global total — correct no matter how the rows
140
140
  were partitioned across substreams. Each substream's ``finish()`` reads only
141
- its OWN worker's accumulated state (keyed by the substream's execution_id;
142
- ``params.substream_id`` is the stable client-owned key available for workers
143
- that manage cross-backend state themselves). This is the per-substream
141
+ its OWN execution's accumulated state — the framework keeps one row per
142
+ ``params.substream_id`` in storage scoped to that execution, so it is found
143
+ even when a finalize lands on a different backend. This is the per-substream
144
144
  finalize contract A4 enables — it is NOT a global cross-substream combine
145
145
  (that is a ``TableBufferingFunction``; see ``SumAllColumnsSimpleDistributed``).
146
146
 
@@ -191,8 +191,9 @@ class SubstreamPartialSumFunction(TableInOutFunction[SingleTableArguments, Subst
191
191
  params: ProcessParams[SingleTableArguments],
192
192
  states: list[SubstreamPartialSumState],
193
193
  ) -> list[pa.RecordBatch]:
194
- # `states` are THIS substream's accumulated states (one per worker pid that
195
- # handled this substream's batches); their sum is this substream's partial.
194
+ # `states` are this execution's accumulated states, one per substream that
195
+ # saw input (the Python client fans one execution across several); their
196
+ # sum is this finalize's partial.
196
197
  total = sum(st.total for st in states)
197
198
  name = params.output_schema.names[0]
198
199
  return [pa.RecordBatch.from_pydict({name: [total]}, schema=params.output_schema)]
vgi/auth.py CHANGED
@@ -18,8 +18,9 @@ JWT auth (requires ``vgi[oauth]``):
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- import contextlib
21
+ import importlib
22
22
  from collections.abc import Callable
23
+ from typing import TYPE_CHECKING, Any
23
24
 
24
25
  from vgi_rpc.rpc import AuthContext, CallContext
25
26
 
@@ -43,34 +44,89 @@ __all__ = [
43
44
  "TokenResolver",
44
45
  ]
45
46
 
46
- # HTTP auth helpers — available when vgi[http] is installed.
47
- with contextlib.suppress(ImportError):
48
- from vgi_rpc.http import ( # noqa: F401
49
- AuthUnavailableError,
50
- OAuthResourceMetadata,
51
- bearer_authenticate,
52
- bearer_authenticate_static,
53
- chain_authenticate,
54
- parse_client_id,
55
- parse_client_secret,
56
- parse_device_code_client_id,
57
- parse_device_code_client_secret,
47
+ # HTTP auth helpers (``vgi[http]``) and JWT auth (``vgi[oauth]``), resolved on
48
+ # first access rather than at import. ``vgi/__init__`` imports this module, and
49
+ # ``vgi_rpc.http`` brings in the HTTP server and client stack (falcon, httpx2,
50
+ # cryptography, joserfc): importing it here put about 180 ms on every
51
+ # ``import vgi``, and so on the start of every subprocess worker, which never
52
+ # uses it. ``from vgi.auth import bearer_authenticate`` works as before; when
53
+ # the extra is missing it raises ImportError, as it did.
54
+ _LAZY_EXPORTS = {
55
+ "AuthUnavailableError": "vgi_rpc.http",
56
+ "OAuthResourceMetadata": "vgi_rpc.http",
57
+ "bearer_authenticate": "vgi_rpc.http",
58
+ "bearer_authenticate_static": "vgi_rpc.http",
59
+ "chain_authenticate": "vgi_rpc.http",
60
+ "parse_client_id": "vgi_rpc.http",
61
+ "parse_client_secret": "vgi_rpc.http",
62
+ "parse_device_code_client_id": "vgi_rpc.http",
63
+ "parse_device_code_client_secret": "vgi_rpc.http",
64
+ "jwt_authenticate": "vgi_rpc.http._oauth_jwt",
65
+ }
66
+
67
+ if TYPE_CHECKING:
68
+ # ``X as X``: explicit re-exports, so type checkers see these as part of
69
+ # ``vgi.auth`` exactly as they were before they became lazy.
70
+ from vgi_rpc.http import (
71
+ AuthUnavailableError as AuthUnavailableError,
72
+ )
73
+ from vgi_rpc.http import (
74
+ OAuthResourceMetadata as OAuthResourceMetadata,
75
+ )
76
+ from vgi_rpc.http import (
77
+ bearer_authenticate as bearer_authenticate,
78
+ )
79
+ from vgi_rpc.http import (
80
+ bearer_authenticate_static as bearer_authenticate_static,
81
+ )
82
+ from vgi_rpc.http import (
83
+ chain_authenticate as chain_authenticate,
84
+ )
85
+ from vgi_rpc.http import (
86
+ parse_client_id as parse_client_id,
87
+ )
88
+ from vgi_rpc.http import (
89
+ parse_client_secret as parse_client_secret,
90
+ )
91
+ from vgi_rpc.http import (
92
+ parse_device_code_client_id as parse_device_code_client_id,
58
93
  )
94
+ from vgi_rpc.http import (
95
+ parse_device_code_client_secret as parse_device_code_client_secret,
96
+ )
97
+ from vgi_rpc.http._oauth_jwt import jwt_authenticate as jwt_authenticate
98
+
99
+
100
+ def __getattr__(name: str) -> Any:
101
+ """Import an HTTP or JWT auth helper on first access (PEP 562).
102
+
103
+ Args:
104
+ name: The attribute being looked up.
105
+
106
+ Returns:
107
+ The helper, which is then cached in this module's namespace.
108
+
109
+ Raises:
110
+ AttributeError: If ``name`` is not a helper this module re-exports, or
111
+ its extra is not installed.
112
+ """
113
+ module_name = _LAZY_EXPORTS.get(name)
114
+ if module_name is None:
115
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
116
+ try:
117
+ value = getattr(importlib.import_module(module_name), name)
118
+ except ImportError as exc:
119
+ raise AttributeError(
120
+ f"module {__name__!r} has no attribute {name!r}: it needs {module_name}, which failed to import ({exc})"
121
+ ) from exc
122
+ globals()[name] = value
123
+ return value
124
+
125
+
126
+ def __dir__() -> list[str]:
127
+ """List this module's names, the lazily imported helpers included.
59
128
 
60
- __all__ += [
61
- "AuthUnavailableError",
62
- "OAuthResourceMetadata",
63
- "bearer_authenticate",
64
- "bearer_authenticate_static",
65
- "chain_authenticate",
66
- "parse_client_id",
67
- "parse_client_secret",
68
- "parse_device_code_client_id",
69
- "parse_device_code_client_secret",
70
- ]
71
-
72
- # JWT auth — available when vgi[oauth] is installed (requires authlib).
73
- with contextlib.suppress(ImportError):
74
- from vgi_rpc.http._oauth_jwt import jwt_authenticate # noqa: F401
75
-
76
- __all__ += ["jwt_authenticate"]
129
+ Returns:
130
+ The module's attribute names.
131
+ """
132
+ return sorted({*globals(), *_LAZY_EXPORTS})
@@ -2698,6 +2698,8 @@ class ReadOnlyCatalogInterface(CatalogInterface):
2698
2698
  _function_registry: "dict[tuple[SchemaKey, str], list[type]] | None" = None
2699
2699
  _macro_registry: "dict[tuple[SchemaKey, str], Macro] | None" = None
2700
2700
  _index_registry: "dict[tuple[SchemaKey, str], Index] | None" = None
2701
+ # Function listings by (schema, listing type), filled in by ``_function_infos``.
2702
+ _function_info_cache: "dict[tuple[SchemaKey, SchemaObjectType], tuple[FunctionInfo, ...]] | None" = None
2701
2703
  # Lazy registry build is one-time but the fixture HTTP server is
2702
2704
  # multi-threaded and shares one catalog instance, so concurrent
2703
2705
  # first-requests can race the build. Serialize it under a lock and flip
@@ -2738,6 +2740,7 @@ class ReadOnlyCatalogInterface(CatalogInterface):
2738
2740
  self._function_registry = {}
2739
2741
  self._macro_registry = {}
2740
2742
  self._index_registry = {}
2743
+ self._function_info_cache = {}
2741
2744
 
2742
2745
  def _register_table(schema_key: SchemaKey, table: "Table") -> None:
2743
2746
  key = (schema_key, table.name.lower())
@@ -3320,29 +3323,46 @@ class ReadOnlyCatalogInterface(CatalogInterface):
3320
3323
  if sn == path_key and macro.macro_type == target_macro_type:
3321
3324
  results.append(macro.to_macro_info(schema_path))
3322
3325
  else:
3323
- # SCALAR_FUNCTION or TABLE_FUNCTION
3324
- for (sn, _), func_classes in self._function_registry.items():
3325
- if sn != path_key:
3326
- continue
3327
- for func_cls in func_classes:
3328
- func_info = self._function_to_info(func_cls, schema_path)
3329
- # Filter by function type
3330
- if type_enum == SchemaObjectType.SCALAR_FUNCTION and func_info.function_type != FunctionType.SCALAR:
3331
- continue
3332
- if type_enum == SchemaObjectType.TABLE_FUNCTION and func_info.function_type not in (
3333
- FunctionType.TABLE,
3334
- FunctionType.TABLE_BUFFERING,
3335
- ):
3336
- continue
3337
- if (
3338
- type_enum == SchemaObjectType.AGGREGATE_FUNCTION
3339
- and func_info.function_type != FunctionType.AGGREGATE
3340
- ):
3341
- continue
3342
- results.append(func_info)
3326
+ # SCALAR_FUNCTION, TABLE_FUNCTION or AGGREGATE_FUNCTION
3327
+ results.extend(self._function_infos(path_key, schema_path, type_enum))
3343
3328
 
3344
3329
  return results
3345
3330
 
3331
+ def _function_infos(
3332
+ self, path_key: SchemaKey, schema_path: SchemaPath, type_enum: SchemaObjectType
3333
+ ) -> tuple[FunctionInfo, ...]:
3334
+ """Return the schema's functions of one listing type, built on the first request.
3335
+
3336
+ A listing depends only on the registered classes' static metadata, so
3337
+ every request for it returns the same `FunctionInfo` instances. Rebuilding
3338
+ it on each request re-derived every function in the schema to keep the
3339
+ requested type's. It also made the worker encode each one again, at
3340
+ about 0.8 ms per function, because ``FunctionsResponse.from_infos``
3341
+ stores an instance's encoding on the instance.
3342
+ The instances are shared between requests, so they must not be mutated.
3343
+ """
3344
+ assert self._function_registry is not None
3345
+ assert self._function_info_cache is not None
3346
+ key = (path_key, type_enum)
3347
+ cached = self._function_info_cache.get(key)
3348
+ if cached is not None:
3349
+ return cached
3350
+ wanted = {
3351
+ SchemaObjectType.SCALAR_FUNCTION: (FunctionType.SCALAR,),
3352
+ SchemaObjectType.TABLE_FUNCTION: (FunctionType.TABLE, FunctionType.TABLE_BUFFERING),
3353
+ SchemaObjectType.AGGREGATE_FUNCTION: (FunctionType.AGGREGATE,),
3354
+ }.get(type_enum, ())
3355
+ infos: list[FunctionInfo] = []
3356
+ for (sn, _), func_classes in self._function_registry.items():
3357
+ if sn != path_key:
3358
+ continue
3359
+ for func_cls in func_classes:
3360
+ func_info = self._function_to_info(func_cls, schema_path)
3361
+ if func_info.function_type in wanted:
3362
+ infos.append(func_info)
3363
+ # Concurrent first requests may both build it; keep whichever landed first.
3364
+ return self._function_info_cache.setdefault(key, tuple(infos))
3365
+
3346
3366
  def copy_from_formats(
3347
3367
  self,
3348
3368
  *,