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.
- vgi/_test_fixtures/table/filters.py +4 -1
- vgi/_test_fixtures/table/sequence.py +4 -1
- vgi/_test_fixtures/table/splits.py +10 -5
- vgi/_test_fixtures/table_in_out.py +6 -5
- vgi/auth.py +86 -30
- vgi/catalog/catalog_interface.py +40 -20
- vgi/client/client.py +197 -96
- vgi/filter_v2.py +62 -2
- vgi/function.py +18 -4
- vgi/function_storage.py +41 -29
- vgi/protocol.py +140 -25
- vgi/scalar_function.py +3 -2
- vgi/serve.py +1 -58
- vgi/table_filter_pushdown.py +36 -0
- vgi/table_in_out_function.py +12 -5
- {vgi_python-0.34.1.dist-info → vgi_python-0.36.0.dist-info}/METADATA +2 -2
- {vgi_python-0.34.1.dist-info → vgi_python-0.36.0.dist-info}/RECORD +20 -20
- {vgi_python-0.34.1.dist-info → vgi_python-0.36.0.dist-info}/WHEEL +0 -0
- {vgi_python-0.34.1.dist-info → vgi_python-0.36.0.dist-info}/entry_points.txt +0 -0
- {vgi_python-0.34.1.dist-info → vgi_python-0.36.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
|
142
|
-
``params.substream_id``
|
|
143
|
-
|
|
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
|
|
195
|
-
#
|
|
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
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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})
|
vgi/catalog/catalog_interface.py
CHANGED
|
@@ -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
|
|
3324
|
-
|
|
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
|
*,
|