vgi-python 0.32.0__py3-none-any.whl → 0.34.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/bad_protocol.py +8 -0
- vgi/_test_fixtures/table_in_out.py +84 -1
- vgi/_test_fixtures/worker.py +4 -0
- vgi/argument_spec.py +42 -8
- vgi/auth.py +17 -8
- vgi/protocol.py +16 -0
- vgi/secret_protocol.py +7 -0
- vgi/serve.py +63 -22
- vgi/worker.py +11 -7
- {vgi_python-0.32.0.dist-info → vgi_python-0.34.0.dist-info}/METADATA +2 -2
- {vgi_python-0.32.0.dist-info → vgi_python-0.34.0.dist-info}/RECORD +14 -14
- {vgi_python-0.32.0.dist-info → vgi_python-0.34.0.dist-info}/WHEEL +1 -1
- {vgi_python-0.32.0.dist-info → vgi_python-0.34.0.dist-info}/entry_points.txt +0 -0
- {vgi_python-0.32.0.dist-info → vgi_python-0.34.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -44,6 +44,14 @@ BAD_PROTOCOL_VERSION = "99.0.0"
|
|
|
44
44
|
class BadProtocol(VgiProtocol):
|
|
45
45
|
"""VgiProtocol surface with a deliberately incompatible version."""
|
|
46
46
|
|
|
47
|
+
# The *only* thing this fixture varies is the version. As of vgi-rpc
|
|
48
|
+
# 0.46.0 the protocol name is the dispatch routing key, and it defaults to
|
|
49
|
+
# the class name -- so without this line subclassing alone renames the
|
|
50
|
+
# protocol to ``BadProtocol``, the client's request for ``VgiProtocol``
|
|
51
|
+
# finds no such protocol hosted, and the caller gets
|
|
52
|
+
# ``ProtocolNotSupportedError`` before the version gate it came to test
|
|
53
|
+
# ever runs.
|
|
54
|
+
protocol_name: ClassVar[str] = "vgi.v2"
|
|
47
55
|
protocol_version: ClassVar[str] = BAD_PROTOCOL_VERSION
|
|
48
56
|
|
|
49
57
|
|
|
@@ -51,7 +51,7 @@ from vgi_rpc import ArrowSerializableDataclass, ArrowType
|
|
|
51
51
|
from vgi_rpc.log import Level
|
|
52
52
|
from vgi_rpc.rpc import OutputCollector
|
|
53
53
|
|
|
54
|
-
from vgi.arguments import Arg, Setting, TableInput
|
|
54
|
+
from vgi.arguments import AnyArrow, Arg, Setting, TableInput
|
|
55
55
|
from vgi.cache_control import CacheControl
|
|
56
56
|
from vgi.invocation import BindResponse
|
|
57
57
|
from vgi.metadata import FunctionExample
|
|
@@ -94,6 +94,8 @@ __all__ = [
|
|
|
94
94
|
"GeoEncode3Function",
|
|
95
95
|
"RowSumFunction",
|
|
96
96
|
"BlendedDropFunction",
|
|
97
|
+
"BlendedAnyFunction",
|
|
98
|
+
"BlendedAnyVarargsFunction",
|
|
97
99
|
"SubstreamPartialSumFunction",
|
|
98
100
|
"MultiBatchFinishFunction",
|
|
99
101
|
"SumAllColumnsFunction",
|
|
@@ -1880,6 +1882,87 @@ class BlendedExplodeFunction(RowTransformFunction[_ExplodeArgs]):
|
|
|
1880
1882
|
cast("VgiOutputCollector", out).emit(out_batch, parent_rows=parent_rows)
|
|
1881
1883
|
|
|
1882
1884
|
|
|
1885
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
1886
|
+
class _AnyArgs:
|
|
1887
|
+
"""One positional ANY-typed input column."""
|
|
1888
|
+
|
|
1889
|
+
value: Annotated[AnyArrow, Arg(0, doc="Input column of any type (echoed back unchanged)")]
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
class BlendedAnyFunction(RowTransformFunction[_AnyArgs]):
|
|
1893
|
+
"""Blended 1->1 echo of a single ANY-typed input column.
|
|
1894
|
+
|
|
1895
|
+
Proves a blended positional arg may be declared ANY: the declared type names
|
|
1896
|
+
no concrete Arrow type, so the client must build the worker's input schema
|
|
1897
|
+
from the type DuckDB actually resolved for the call (a STRUCT built per row,
|
|
1898
|
+
a whole-row struct, a LIST, a plain VARCHAR ...) rather than from the
|
|
1899
|
+
declaration. The output column ``value`` is bound to that resolved input
|
|
1900
|
+
type at bind time and the column is passed through untouched, so a test
|
|
1901
|
+
asserts on DuckDB's own ``typeof(value)`` and on the round-tripped values —
|
|
1902
|
+
nothing here depends on how an SDK happens to render a type name.
|
|
1903
|
+
"""
|
|
1904
|
+
|
|
1905
|
+
class Meta:
|
|
1906
|
+
name = "blended_any"
|
|
1907
|
+
description = "Blended 1->1 echo of one ANY-typed input column (output typed from the input)"
|
|
1908
|
+
categories = ["blended", "test"]
|
|
1909
|
+
|
|
1910
|
+
@classmethod
|
|
1911
|
+
def on_bind(cls, params: BindParams[_AnyArgs]) -> BindResponse:
|
|
1912
|
+
input_schema = params.bind_call.input_schema
|
|
1913
|
+
assert input_schema is not None
|
|
1914
|
+
return BindResponse(output_schema=pa.schema([pa.field("value", input_schema.field(0).type)]))
|
|
1915
|
+
|
|
1916
|
+
@classmethod
|
|
1917
|
+
def process(
|
|
1918
|
+
cls,
|
|
1919
|
+
params: ProcessParams[_AnyArgs],
|
|
1920
|
+
state: None,
|
|
1921
|
+
batch: pa.RecordBatch,
|
|
1922
|
+
out: OutputCollector,
|
|
1923
|
+
) -> None:
|
|
1924
|
+
out.emit(pa.record_batch([batch.column(0)], schema=params.output_schema))
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
1928
|
+
class _AnyVarargsArgs:
|
|
1929
|
+
"""N positional ANY-typed input columns, each free to be a different type."""
|
|
1930
|
+
|
|
1931
|
+
values: Annotated[list[AnyArrow], Arg(0, varargs=True, doc="Input columns of any types (echoed back)")]
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
class BlendedAnyVarargsFunction(RowTransformFunction[_AnyVarargsArgs]):
|
|
1935
|
+
"""Blended 1->1 echo of N ANY-typed VARARGS input columns.
|
|
1936
|
+
|
|
1937
|
+
The varargs counterpart of ``blended_any``: every runtime column may resolve
|
|
1938
|
+
to a different concrete type, so the client must take each column's type
|
|
1939
|
+
from the call rather than from the (ANY) vararg element type. Output columns
|
|
1940
|
+
are ``col0..colN-1`` — the names the client generates for varargs blended
|
|
1941
|
+
input — each bound to its own resolved input type.
|
|
1942
|
+
"""
|
|
1943
|
+
|
|
1944
|
+
class Meta:
|
|
1945
|
+
name = "blended_any_varargs"
|
|
1946
|
+
description = "Blended 1->1 echo of N ANY-typed varargs input columns (col0..colN-1)"
|
|
1947
|
+
categories = ["blended", "test"]
|
|
1948
|
+
|
|
1949
|
+
@classmethod
|
|
1950
|
+
def on_bind(cls, params: BindParams[_AnyVarargsArgs]) -> BindResponse:
|
|
1951
|
+
input_schema = params.bind_call.input_schema
|
|
1952
|
+
assert input_schema is not None
|
|
1953
|
+
return BindResponse(output_schema=pa.schema([pa.field(f"col{i}", f.type) for i, f in enumerate(input_schema)]))
|
|
1954
|
+
|
|
1955
|
+
@classmethod
|
|
1956
|
+
def process(
|
|
1957
|
+
cls,
|
|
1958
|
+
params: ProcessParams[_AnyVarargsArgs],
|
|
1959
|
+
state: None,
|
|
1960
|
+
batch: pa.RecordBatch,
|
|
1961
|
+
out: OutputCollector,
|
|
1962
|
+
) -> None:
|
|
1963
|
+
out.emit(pa.record_batch(cls.input_columns(batch), schema=params.output_schema))
|
|
1964
|
+
|
|
1965
|
+
|
|
1883
1966
|
class CachedExplodeFunction(RowTransformFunction[_ExplodeArgs]):
|
|
1884
1967
|
"""Cacheable blended 1->N fan-out advertising vgi.cache.per_value.
|
|
1885
1968
|
|
vgi/_test_fixtures/worker.py
CHANGED
|
@@ -267,6 +267,8 @@ from vgi._test_fixtures.table.tt_pushdown import (
|
|
|
267
267
|
)
|
|
268
268
|
from vgi._test_fixtures.table_in_out import (
|
|
269
269
|
BatchIndexBufferInputFunction,
|
|
270
|
+
BlendedAnyFunction,
|
|
271
|
+
BlendedAnyVarargsFunction,
|
|
270
272
|
BlendedDropFunction,
|
|
271
273
|
BlendedExplodeFunction,
|
|
272
274
|
BufferEmitWideFunction,
|
|
@@ -432,6 +434,8 @@ _EXAMPLE_CATALOG = Catalog(
|
|
|
432
434
|
GeoEncode3Function,
|
|
433
435
|
RowSumFunction,
|
|
434
436
|
BlendedDropFunction,
|
|
437
|
+
BlendedAnyFunction,
|
|
438
|
+
BlendedAnyVarargsFunction,
|
|
435
439
|
BlendedExplodeFunction,
|
|
436
440
|
CachedExplodeFunction,
|
|
437
441
|
ProjectableBlendedFunction,
|
vgi/argument_spec.py
CHANGED
|
@@ -19,7 +19,8 @@ import warnings
|
|
|
19
19
|
from collections.abc import Sequence
|
|
20
20
|
from dataclasses import dataclass
|
|
21
21
|
from functools import cache
|
|
22
|
-
from
|
|
22
|
+
from types import UnionType
|
|
23
|
+
from typing import Annotated, Any, Union, get_args, get_origin, get_type_hints
|
|
23
24
|
|
|
24
25
|
import pyarrow as pa
|
|
25
26
|
|
|
@@ -395,14 +396,22 @@ def parameter_default_values_from_specs(
|
|
|
395
396
|
arrays: list[pa.Array[Any]] = []
|
|
396
397
|
names: list[str] = []
|
|
397
398
|
for spec in default_specs:
|
|
398
|
-
if pa.types.is_null(spec.arrow_type):
|
|
399
|
-
raise ValueError(
|
|
400
|
-
f"Parameter {spec.name!r} has a default but no concrete Arrow type; "
|
|
401
|
-
"typed defaults require an explicit parameter type"
|
|
402
|
-
)
|
|
403
399
|
value = spec.default_value
|
|
404
400
|
if isinstance(value, pa.Scalar):
|
|
405
401
|
value = value.as_py()
|
|
402
|
+
# An ANY-typed parameter (AnyArrow) has no concrete Arrow type, so a
|
|
403
|
+
# VALUE default cannot be represented. `None` still can: a null-typed
|
|
404
|
+
# array holds nulls, and it is column PRESENCE, not the value, that
|
|
405
|
+
# marks a parameter as having a default. Allowing it lets a required
|
|
406
|
+
# ANY parameter be declared `default=None` so that omitting it reaches
|
|
407
|
+
# the function's own on_bind() validation — which can name the argument
|
|
408
|
+
# and show its shape — instead of raising a bare KeyError during
|
|
409
|
+
# argument parsing, before any user code runs.
|
|
410
|
+
if pa.types.is_null(spec.arrow_type) and value is not None:
|
|
411
|
+
raise ValueError(
|
|
412
|
+
f"Parameter {spec.name!r} has a non-null default but no concrete Arrow type; "
|
|
413
|
+
"a typed default requires an explicit parameter type (None is allowed)"
|
|
414
|
+
)
|
|
406
415
|
try:
|
|
407
416
|
arrays.append(pa.array([value], type=spec.arrow_type))
|
|
408
417
|
except (pa.ArrowException, TypeError, ValueError) as exc:
|
|
@@ -538,6 +547,30 @@ def _format_range(
|
|
|
538
547
|
return f"{low}, {high}"
|
|
539
548
|
|
|
540
549
|
|
|
550
|
+
def _unwrap_optional(annotation: Any) -> Any:
|
|
551
|
+
"""``T | None`` -> ``T``; anything else unchanged.
|
|
552
|
+
|
|
553
|
+
A special marker type (``AnyArrow``, ``TableInput``) is recognised by
|
|
554
|
+
identity, so an Optional wrapper would hide it — an ``AnyArrow | None``
|
|
555
|
+
parameter would silently become a NULL-typed one, and DuckDB then cannot
|
|
556
|
+
cast anything into it. Unwrapping first lets a marker be declared nullable,
|
|
557
|
+
which is how a REQUIRED ANY argument opts into reaching its function's own
|
|
558
|
+
``on_bind()`` validation (with ``default=None``) instead of being rejected
|
|
559
|
+
during parsing. Mirrors what ``_accepts_none`` already looks for.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
annotation: The declared type of an ``Annotated[T, Arg(...)]`` field.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
The single non-``None`` member of an optional union, else ``annotation``.
|
|
566
|
+
"""
|
|
567
|
+
origin = get_origin(annotation)
|
|
568
|
+
if origin is not Union and origin is not UnionType:
|
|
569
|
+
return annotation
|
|
570
|
+
members = [a for a in get_args(annotation) if a is not type(None)]
|
|
571
|
+
return members[0] if len(members) == 1 else annotation
|
|
572
|
+
|
|
573
|
+
|
|
541
574
|
def _constraint_kwargs(arg: Arg[Any]) -> dict[str, Any]:
|
|
542
575
|
"""Extract discovery-facing constraint metadata from an [`Arg`][] descriptor.
|
|
543
576
|
|
|
@@ -744,6 +777,7 @@ def _extract_argument_specs(
|
|
|
744
777
|
if type_args:
|
|
745
778
|
infer_type = type_args[0]
|
|
746
779
|
|
|
780
|
+
infer_type = _unwrap_optional(infer_type)
|
|
747
781
|
is_table_input = infer_type is TableInput
|
|
748
782
|
is_any_type = infer_type is AnyArrow or infer_type is AnyArrowValue
|
|
749
783
|
|
|
@@ -790,8 +824,8 @@ def _extract_argument_specs(
|
|
|
790
824
|
# Check for special types (AnyArrow, TableInput)
|
|
791
825
|
# Priority: Arg subscript type (Arg[AnyArrow]) > class type hint
|
|
792
826
|
# Also check _returns_any_arrow_value for Annotated[AnyArrowValue, ...]
|
|
793
|
-
hint = hints.get(attr_name)
|
|
794
|
-
type_param = getattr(arg_legacy, "_type_param", None)
|
|
827
|
+
hint = _unwrap_optional(hints.get(attr_name))
|
|
828
|
+
type_param = _unwrap_optional(getattr(arg_legacy, "_type_param", None))
|
|
795
829
|
is_table_input = type_param is TableInput or hint is TableInput
|
|
796
830
|
is_any_type = (
|
|
797
831
|
type_param is AnyArrow
|
vgi/auth.py
CHANGED
|
@@ -9,7 +9,7 @@ HTTP auth factories (require ``vgi[http]``):
|
|
|
9
9
|
bearer_authenticate, bearer_authenticate_static, chain_authenticate,
|
|
10
10
|
OAuthResourceMetadata, AuthUnavailableError
|
|
11
11
|
|
|
12
|
-
Token introspection (
|
|
12
|
+
Token introspection (always available):
|
|
13
13
|
TokenIdentity, TokenResolver
|
|
14
14
|
|
|
15
15
|
JWT auth (requires ``vgi[oauth]``):
|
|
@@ -19,12 +19,28 @@ JWT auth (requires ``vgi[oauth]``):
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import contextlib
|
|
22
|
+
from collections.abc import Callable
|
|
22
23
|
|
|
23
24
|
from vgi_rpc.rpc import AuthContext, CallContext
|
|
24
25
|
|
|
26
|
+
# ``vgi_rpc.Identity.v1`` lives at the RPC layer as of vgi-rpc 0.46.0 -- it was
|
|
27
|
+
# an HTTP JSON route (``POST {prefix}/__introspect_token__``) through 0.45.x,
|
|
28
|
+
# which meant it existed on exactly one transport. So this is no longer gated
|
|
29
|
+
# on the ``http`` extra. Still not re-exported by ``vgi_rpc.rpc`` itself, so
|
|
30
|
+
# the private module remains the only import path; re-exported here so a worker
|
|
31
|
+
# that implements ``resolve_token`` never has to name one.
|
|
32
|
+
from vgi_rpc.rpc._token_identity import TokenIdentity
|
|
33
|
+
|
|
34
|
+
#: What :meth:`vgi.worker.Worker.resolve_token` is. Upstream dropped its own
|
|
35
|
+
#: alias when identity moved to the RPC layer; it is spelled out here so the
|
|
36
|
+
#: name ``vgi.auth.TokenResolver`` keeps working.
|
|
37
|
+
TokenResolver = Callable[[str], "TokenIdentity | None"]
|
|
38
|
+
|
|
25
39
|
__all__ = [
|
|
26
40
|
"AuthContext",
|
|
27
41
|
"CallContext",
|
|
42
|
+
"TokenIdentity",
|
|
43
|
+
"TokenResolver",
|
|
28
44
|
]
|
|
29
45
|
|
|
30
46
|
# HTTP auth helpers — available when vgi[http] is installed.
|
|
@@ -41,16 +57,9 @@ with contextlib.suppress(ImportError):
|
|
|
41
57
|
parse_device_code_client_secret,
|
|
42
58
|
)
|
|
43
59
|
|
|
44
|
-
# Not re-exported by ``vgi_rpc.http`` itself, so the private module is the
|
|
45
|
-
# only import path. Re-exported here so a worker that implements
|
|
46
|
-
# ``resolve_token`` never has to name a private module.
|
|
47
|
-
from vgi_rpc.http.server._introspect import TokenIdentity, TokenResolver # noqa: F401
|
|
48
|
-
|
|
49
60
|
__all__ += [
|
|
50
61
|
"AuthUnavailableError",
|
|
51
62
|
"OAuthResourceMetadata",
|
|
52
|
-
"TokenIdentity",
|
|
53
|
-
"TokenResolver",
|
|
54
63
|
"bearer_authenticate",
|
|
55
64
|
"bearer_authenticate_static",
|
|
56
65
|
"chain_authenticate",
|
vgi/protocol.py
CHANGED
|
@@ -3077,10 +3077,26 @@ class VgiProtocol(Protocol):
|
|
|
3077
3077
|
a dedicated generator so this version doesn't pollute the byte-key constants).
|
|
3078
3078
|
|
|
3079
3079
|
Attributes:
|
|
3080
|
+
protocol_name: Wire name used as the ``vgi_rpc.protocol`` routing key
|
|
3081
|
+
and as the ``{protocol}`` HTTP path segment. Carries the major
|
|
3082
|
+
version, so an incompatible major is a different name and therefore
|
|
3083
|
+
a 404 rather than a confusing dispatch.
|
|
3080
3084
|
protocol_version: The VGI protocol version integer, declared as the
|
|
3081
3085
|
canonical semver (MAJOR.MINOR.PATCH) of the method-and-schema contract.
|
|
3082
3086
|
"""
|
|
3083
3087
|
|
|
3088
|
+
# The wire name carries the major version, so an incompatible major is a
|
|
3089
|
+
# *different* protocol and therefore a 404 -- an answer every proxy, WAF and
|
|
3090
|
+
# load balancer understands without an Arrow parser, and one that lets
|
|
3091
|
+
# ``vgi.v2`` and a future ``vgi.v3`` be served side by side while clients
|
|
3092
|
+
# migrate. That matters here specifically: the DuckDB extension ships to
|
|
3093
|
+
# users and cannot be flag-dayed.
|
|
3094
|
+
#
|
|
3095
|
+
# Declared rather than derived. Without it the name defaults to the class
|
|
3096
|
+
# name, which left the six implementations disagreeing four ways -- Python
|
|
3097
|
+
# ``VgiProtocol``, Java and C# ``VgiService``, Go the framework default
|
|
3098
|
+
# ``Service``, TypeScript ``vgi`` -- so no client could address them all.
|
|
3099
|
+
protocol_name: ClassVar[str] = "vgi.v2"
|
|
3084
3100
|
protocol_version: ClassVar[str] = "2.0.0"
|
|
3085
3101
|
|
|
3086
3102
|
def bind(self, request: BindRequest) -> BindResponse:
|
vgi/secret_protocol.py
CHANGED
|
@@ -126,10 +126,17 @@ class VgiSecretProtocol(Protocol):
|
|
|
126
126
|
backwards-incompatible change, minor for additive, patch for worker-side fixes.
|
|
127
127
|
|
|
128
128
|
Attributes:
|
|
129
|
+
protocol_name: Wire name used as the ``vgi_rpc.protocol`` routing key.
|
|
130
|
+
Independent of :class:`vgi.protocol.VgiProtocol`'s, so this service
|
|
131
|
+
can be co-hosted beside it or deployed on its own.
|
|
129
132
|
protocol_version: Canonical semver (MAJOR.MINOR.PATCH) of this contract,
|
|
130
133
|
enforced as an exact major+minor match at the dispatch boundary.
|
|
131
134
|
"""
|
|
132
135
|
|
|
136
|
+
# Its own name and its own major, independent of ``vgi.v2``: this contract
|
|
137
|
+
# versions on a separate cadence, which is why ``protocol_version_override``
|
|
138
|
+
# exists on the client side at all.
|
|
139
|
+
protocol_name: ClassVar[str] = "vgi.secret.v1"
|
|
133
140
|
protocol_version: ClassVar[str] = "1.0.0"
|
|
134
141
|
|
|
135
142
|
def secret_lookup(self, path: str, type: str) -> SecretLookupResponse: # noqa: A002
|
vgi/serve.py
CHANGED
|
@@ -43,6 +43,7 @@ if TYPE_CHECKING:
|
|
|
43
43
|
import falcon
|
|
44
44
|
from vgi_rpc.otel import OtelConfig
|
|
45
45
|
from vgi_rpc.rpc import AuthContext, PeerAuthenticationPolicy, PeerIdentityProvider
|
|
46
|
+
from vgi_rpc.rpc._token_identity import IdentityImpl
|
|
46
47
|
|
|
47
48
|
from vgi.worker import Worker
|
|
48
49
|
|
|
@@ -275,8 +276,8 @@ def create_app(
|
|
|
275
276
|
escape, because bytes already uploaded cannot be un-uploaded.
|
|
276
277
|
``None`` (the default) means no cap.
|
|
277
278
|
introspect_principals: Principals permitted to call
|
|
278
|
-
``
|
|
279
|
-
overrides ``resolve_token``. ``None`` reads
|
|
279
|
+
``vgi_rpc.Identity.v1``'s ``introspect_token``. Only consulted
|
|
280
|
+
when the worker class overrides ``resolve_token``. ``None`` reads
|
|
280
281
|
``VGI_INTROSPECT_PRINCIPALS``.
|
|
281
282
|
introspect_rate_limit: Introspection requests allowed per caller per
|
|
282
283
|
second. ``None`` reads ``VGI_INTROSPECT_RATE_LIMIT``, defaulting
|
|
@@ -321,19 +322,13 @@ def create_app(
|
|
|
321
322
|
worker._signing_key = signing_key
|
|
322
323
|
from vgi.worker import _get_vgi_version
|
|
323
324
|
|
|
324
|
-
server = RpcServer(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if introspect_resolver is not None:
|
|
332
|
-
introspect_kwargs = {
|
|
333
|
-
"introspect_resolver": introspect_resolver,
|
|
334
|
-
"introspect_principals": _resolve_introspect_principals(introspect_principals),
|
|
335
|
-
"introspect_rate_limit": _resolve_introspect_rate_limit(introspect_rate_limit),
|
|
336
|
-
}
|
|
325
|
+
server = RpcServer(
|
|
326
|
+
VgiProtocol,
|
|
327
|
+
worker,
|
|
328
|
+
enable_describe=describe,
|
|
329
|
+
server_version=_get_vgi_version(),
|
|
330
|
+
identity=_build_identity(worker_cls, introspect_principals, introspect_rate_limit),
|
|
331
|
+
)
|
|
337
332
|
|
|
338
333
|
effective_peer_identity_providers = tuple(peer_identity_providers)
|
|
339
334
|
effective_peer_authentication_policy = peer_authentication_policy
|
|
@@ -370,7 +365,6 @@ def create_app(
|
|
|
370
365
|
max_stream_response_bytes=max_stream_response_bytes,
|
|
371
366
|
max_externalized_response_bytes=max_externalized_response_bytes,
|
|
372
367
|
enable_landing_page=False,
|
|
373
|
-
**introspect_kwargs,
|
|
374
368
|
)
|
|
375
369
|
|
|
376
370
|
# Frontend: either redirect to external CDN or serve pre-rendered worker page
|
|
@@ -500,7 +494,7 @@ def main() -> None:
|
|
|
500
494
|
None,
|
|
501
495
|
"--introspect-principals",
|
|
502
496
|
help=(
|
|
503
|
-
"Comma-separated principals permitted to call
|
|
497
|
+
"Comma-separated principals permitted to call introspect_token. "
|
|
504
498
|
"Only used when the worker implements resolve_token(); required in "
|
|
505
499
|
"that case, with no permissive default. Env: VGI_INTROSPECT_PRINCIPALS."
|
|
506
500
|
),
|
|
@@ -805,6 +799,53 @@ def _resolve_authenticate() -> Callable[..., Any] | None:
|
|
|
805
799
|
return require_all(gate, inner)
|
|
806
800
|
|
|
807
801
|
|
|
802
|
+
def _build_identity(
|
|
803
|
+
worker_cls: type[Worker],
|
|
804
|
+
introspect_principals: Iterable[str] | None,
|
|
805
|
+
introspect_rate_limit: int | None,
|
|
806
|
+
) -> IdentityImpl | None:
|
|
807
|
+
"""Build the ``vgi_rpc.Identity.v1`` implementation, or ``None``.
|
|
808
|
+
|
|
809
|
+
``None`` unless the worker class actually implements the lookup, and that
|
|
810
|
+
is the point: ``RpcServer`` then does not host the protocol at all, rather
|
|
811
|
+
than hosting it and refusing every call. Absent beats routed-and-refusing —
|
|
812
|
+
it is what keeps a dependency upgrade from growing a
|
|
813
|
+
credential-to-identity oracle on every existing worker.
|
|
814
|
+
|
|
815
|
+
As of vgi-rpc 0.46.0 identity is an RPC-layer protocol hosted on the
|
|
816
|
+
server, not the HTTP JSON route (``POST {prefix}/__introspect_token__``) it
|
|
817
|
+
was through 0.45.x. It therefore reaches every transport rather than only
|
|
818
|
+
HTTP, and a client discovers it through ordinary reflection rather than by
|
|
819
|
+
calling and reading an error.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
worker_cls: The worker class, consulted for a ``resolve_token``
|
|
823
|
+
override.
|
|
824
|
+
introspect_principals: Principals permitted to introspect, or ``None``
|
|
825
|
+
to read the environment.
|
|
826
|
+
introspect_rate_limit: Per-caller, per-second ceiling, or ``None`` to
|
|
827
|
+
read the environment.
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
The implementation, or ``None`` when this worker does not resolve
|
|
831
|
+
credentials.
|
|
832
|
+
|
|
833
|
+
"""
|
|
834
|
+
resolver = worker_cls._introspect_resolver()
|
|
835
|
+
if resolver is None:
|
|
836
|
+
return None
|
|
837
|
+
|
|
838
|
+
# Not re-exported by ``vgi_rpc.rpc``, so the private module is the only
|
|
839
|
+
# import path for the ``vgi_rpc.Identity.v1`` implementation helper.
|
|
840
|
+
from vgi_rpc.rpc._token_identity import IdentityImpl
|
|
841
|
+
|
|
842
|
+
return IdentityImpl(
|
|
843
|
+
resolve_token=resolver,
|
|
844
|
+
introspect_principals=_resolve_introspect_principals(introspect_principals),
|
|
845
|
+
introspect_rate_limit=_resolve_introspect_rate_limit(introspect_rate_limit),
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
|
|
808
849
|
def _resolve_introspect_principals(explicit: Iterable[str] | None) -> list[str]:
|
|
809
850
|
"""Resolve the introspector allowlist, or exit with an actionable message.
|
|
810
851
|
|
|
@@ -835,16 +876,16 @@ def _resolve_introspect_principals(explicit: Iterable[str] | None) -> list[str]:
|
|
|
835
876
|
|
|
836
877
|
if not principals:
|
|
837
878
|
sys.stderr.write(
|
|
838
|
-
"Error: this worker implements resolve_token(), which
|
|
839
|
-
"
|
|
840
|
-
"
|
|
879
|
+
"Error: this worker implements resolve_token(), which hosts the\n"
|
|
880
|
+
" vgi_rpc.Identity.v1 protocol, but no introspector allowlist was\n"
|
|
881
|
+
" configured. Set VGI_INTROSPECT_PRINCIPALS (comma-separated) or\n"
|
|
841
882
|
" pass --introspect-principals.\n"
|
|
842
883
|
"\n"
|
|
843
884
|
" There is no permissive default on purpose: introspection is a\n"
|
|
844
885
|
" separate capability from authentication, and allowing every\n"
|
|
845
886
|
" authenticated caller lets any user resolve any other user's\n"
|
|
846
|
-
" credential to its owner. Remove resolve_token() to
|
|
847
|
-
"
|
|
887
|
+
" credential to its owner. Remove resolve_token() to leave the\n"
|
|
888
|
+
" protocol unhosted entirely.\n"
|
|
848
889
|
)
|
|
849
890
|
sys.exit(1)
|
|
850
891
|
return principals
|
vgi/worker.py
CHANGED
|
@@ -172,8 +172,8 @@ if TYPE_CHECKING:
|
|
|
172
172
|
from collections.abc import Callable
|
|
173
173
|
from ssl import SSLContext
|
|
174
174
|
|
|
175
|
-
from vgi_rpc.http.server._introspect import TokenIdentity
|
|
176
175
|
from vgi_rpc.rpc import PeerAuthenticationPolicy, PeerIdentityProvider
|
|
176
|
+
from vgi_rpc.rpc._token_identity import TokenIdentity
|
|
177
177
|
|
|
178
178
|
from vgi.catalog.descriptors import Catalog
|
|
179
179
|
from vgi.protocol import (
|
|
@@ -1420,13 +1420,17 @@ class Worker:
|
|
|
1420
1420
|
def resolve_token(cls, token: str) -> TokenIdentity | None:
|
|
1421
1421
|
"""Resolve an opaque bearer credential to the identity it authenticates as.
|
|
1422
1422
|
|
|
1423
|
-
Override to
|
|
1424
|
-
reverse proxy calls when it terminates the only public
|
|
1425
|
-
must know *which principal* a credential is before it can
|
|
1426
|
-
anything. Until it is overridden the
|
|
1427
|
-
not "
|
|
1423
|
+
Override to host ``vgi_rpc.Identity.v1``'s ``introspect_token``,
|
|
1424
|
+
which a reverse proxy calls when it terminates the only public
|
|
1425
|
+
listener and must know *which principal* a credential is before it can
|
|
1426
|
+
authorize anything. Until it is overridden the protocol is not hosted
|
|
1427
|
+
at all — not "hosted and refusing", absent — so no worker grows a
|
|
1428
1428
|
credential-to-identity oracle by upgrading a dependency.
|
|
1429
1429
|
|
|
1430
|
+
Through vgi-rpc 0.45.x this was an HTTP JSON route, ``POST
|
|
1431
|
+
{prefix}/__introspect_token__``. As of 0.46.0 identity lives at the RPC
|
|
1432
|
+
layer, so it reaches every transport rather than only HTTP.
|
|
1433
|
+
|
|
1430
1434
|
Enabling it also requires an allowlist of principals permitted to ask
|
|
1431
1435
|
(``--introspect-principals`` / ``VGI_INTROSPECT_PRINCIPALS``). There is
|
|
1432
1436
|
no permissive default: authenticating and introspecting are different
|
|
@@ -1437,7 +1441,7 @@ class Worker:
|
|
|
1437
1441
|
|
|
1438
1442
|
This is deliberately *not* "run the credential back through the
|
|
1439
1443
|
worker's own authenticate chain" — see
|
|
1440
|
-
``vgi_rpc.
|
|
1444
|
+
``vgi_rpc.rpc._token_identity`` for the four ways that breaks.
|
|
1441
1445
|
Write a narrow lookup against whatever store issued the credential.
|
|
1442
1446
|
|
|
1443
1447
|
Args:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vgi-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.34.0
|
|
4
4
|
Summary: Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow
|
|
5
5
|
Project-URL: Homepage, https://query.farm
|
|
6
6
|
Project-URL: Repository, https://github.com/Query-farm/vgi-python
|
|
@@ -162,7 +162,7 @@ Requires-Dist: httpx2>=2.9.1
|
|
|
162
162
|
Requires-Dist: platformdirs
|
|
163
163
|
Requires-Dist: pyarrow
|
|
164
164
|
Requires-Dist: typer>=0.9
|
|
165
|
-
Requires-Dist: vgi-rpc>=0.
|
|
165
|
+
Requires-Dist: vgi-rpc>=0.46.0
|
|
166
166
|
Provides-Extra: azure
|
|
167
167
|
Requires-Dist: azure-identity>=1.16.0; extra == 'azure'
|
|
168
168
|
Requires-Dist: pymssql>=2.3.0; extra == 'azure'
|
|
@@ -2,10 +2,10 @@ vgi/__init__.py,sha256=8uLMNQcNzWjp8VzYtj2u98_Wp8WpB8c_dKlMv67TH0g,5384
|
|
|
2
2
|
vgi/_duckdb.py,sha256=YB5D7N3Bwg_xP6X8a5QlumtlAovSej1A1Go5XlNGVko,2162
|
|
3
3
|
vgi/_storage_profile.py,sha256=VkTsXojuE0tHEzurmteQSAiL1vI3CZSgYkL6D_h8GvE,5061
|
|
4
4
|
vgi/aggregate_function.py,sha256=v03wBSy2Rrl9F2pMYvu8GnGrTCQrAVm01HXIFNdjnm4,26886
|
|
5
|
-
vgi/argument_spec.py,sha256=
|
|
5
|
+
vgi/argument_spec.py,sha256=SADH97VlFu3asI8CWZ_I7XTuSxwqFciFgdupdAf-tkY,34231
|
|
6
6
|
vgi/arguments.py,sha256=ncnC1CS7KN-2rDjSw9vdf1uHqV5ozebet_eZtcq_ujw,72300
|
|
7
7
|
vgi/attach_header.py,sha256=sxpzCOVa5fl_-kQSCZgeUxS5I9Kuv8VCUjAOF5s1pr0,3782
|
|
8
|
-
vgi/auth.py,sha256=
|
|
8
|
+
vgi/auth.py,sha256=TlD5bhdyXnFuAoCnoHMU9ffjewVEnpwO5auIvp9GsCA,2486
|
|
9
9
|
vgi/cache_control.py,sha256=IyBp9gZTCc4Y-4V7Rs05_o5lSEjbR2jt6cddXPU0dKw,7812
|
|
10
10
|
vgi/copy_from_function.py,sha256=oroQe5qYzZTJ-ppzQTQ-OruEF1ZlwTBNLBoXYfN-P2o,7133
|
|
11
11
|
vgi/copy_to_function.py,sha256=8l0v_DyEF1EnFwinStrZDQ-yNFfGJsv3Mr5o2jG30f0,9279
|
|
@@ -22,26 +22,26 @@ vgi/meta_worker.py,sha256=WggIKvDAlahSrBPaVM7ZqVWk3Xc3JNE70EgeoXdy-3w,31725
|
|
|
22
22
|
vgi/metadata.py,sha256=M8KZObX1wwCr2lPjgDL0-79gPbeM_NSOIqYCIAAck5M,74719
|
|
23
23
|
vgi/otel.py,sha256=JlUDnk2c0UXSo_j1PJdS67Y--t55sXPg054ADuvHANc,15452
|
|
24
24
|
vgi/profiling.py,sha256=rK7vij3oHhXC3JHI2BsMySsXh7_hrXi0aXpCW8iFSIc,3671
|
|
25
|
-
vgi/protocol.py,sha256=
|
|
25
|
+
vgi/protocol.py,sha256=m9W00W-c9jv2kqISp_wb8E8Rtity_60_12nzE3_d9Xg,165016
|
|
26
26
|
vgi/protocol_version.txt,sha256=wo_MpTY3vIjhJK8XJd8Ty5jGne3v1i-zzb4c22t2BiQ,6
|
|
27
27
|
vgi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
28
|
vgi/scalar_function.py,sha256=zj6U1e5mXm4hk_ZB_CAjYmeO1LZBJcEMzNDzhwpXPmQ,53415
|
|
29
29
|
vgi/schema_path.py,sha256=GVlkZlCUj7GbVhgiocSMC6Ff5iTJhKqiYE62hMIEKiI,2403
|
|
30
30
|
vgi/schema_utils.py,sha256=zQCliUgIpO2u77Mhtgg3OFvOEDNgJopu6kj2fw8YP1I,7945
|
|
31
|
-
vgi/secret_protocol.py,sha256=
|
|
31
|
+
vgi/secret_protocol.py,sha256=iCDl7nF7Nb4cJV4vpfbldunTDqDJWzCzS7uCmOACf7U,7151
|
|
32
32
|
vgi/secret_service.py,sha256=r7qCzfnOMirf9wSCUe-mgFe3z6v9_cDRzixuSAZG_bs,8973
|
|
33
|
-
vgi/serve.py,sha256=
|
|
33
|
+
vgi/serve.py,sha256=dyLHzMFdu1AUBtte2mwwYjtQeSF0fOLha1v9MBZbuD0,66163
|
|
34
34
|
vgi/split_token.py,sha256=tutcksj-hgWrLY7UW3sSqkyoaD-cMqNTOeff40MGl_g,10850
|
|
35
35
|
vgi/table_buffering_function.py,sha256=O5TcIdqk8-YViCQCkb8bOevO-zhe3IoyzLgwXDO3Cjc,19531
|
|
36
36
|
vgi/table_filter_pushdown.py,sha256=2wJK09OkZZm-WYFsB-um4lbIMZV6gU1fcx8EsHglnA4,61861
|
|
37
37
|
vgi/table_function.py,sha256=lAxWnjBcZBpLoR87Pe-QTK7c8WK_JQh1r_OJ9amI4kk,72746
|
|
38
38
|
vgi/table_in_out_function.py,sha256=UGEeXsGkcx0BLt9RyKP6jQEbK6eFTUG6QIGEhxo_VxA,18865
|
|
39
|
-
vgi/worker.py,sha256=
|
|
39
|
+
vgi/worker.py,sha256=MGMQmy6PPKCqo5q_9fBS3z3zf4y6-UKdJAJiHGfnk50,249553
|
|
40
40
|
vgi/write_results.py,sha256=-OrUcz6E9ahhC_pnKYAwIMR2LpapwQafLC4IOnPTmec,2851
|
|
41
41
|
vgi/_test_fixtures/__init__.py,sha256=xQq-QQLk8USQ6TZHsPiPcZldNNUdcJ2gToXMPGzScLI,444
|
|
42
42
|
vgi/_test_fixtures/attach_options.py,sha256=ZNgNTNF1RymSfnU-uVIDTLvCpc283h6O8tZ1UuEa3Lc,12516
|
|
43
43
|
vgi/_test_fixtures/bad_enum.py,sha256=b8l-afmXgGR6iqgIUiVeqHu0C0F8Of8ahEmdQGtqd1g,2916
|
|
44
|
-
vgi/_test_fixtures/bad_protocol.py,sha256=
|
|
44
|
+
vgi/_test_fixtures/bad_protocol.py,sha256=fPU7rpXEgBxRtPHfuCa0O2AHNRRbZDllLfydHsAKPEg,2992
|
|
45
45
|
vgi/_test_fixtures/cancellable.py,sha256=hetTRLyT4kkazPNcxAdfzj0pYzEmegMksamJbR0w3Ho,12151
|
|
46
46
|
vgi/_test_fixtures/catalog.py,sha256=zjBKBgg4TKoDQ9SYIFj7sx6bOL3_aXHMJHSzTgk1fdA,28526
|
|
47
47
|
vgi/_test_fixtures/companion.py,sha256=Ya_rg2J4tXOYC7zBFOjTTh-4AYuRhJN5BT4tVZULQ-Q,6037
|
|
@@ -52,12 +52,12 @@ vgi/_test_fixtures/http_server.py,sha256=AviomIEClCEuA9WPgQOzrMtQ484N04rQupK8dWO
|
|
|
52
52
|
vgi/_test_fixtures/nest_tensor.py,sha256=UjjW9UwipYG3ehQpZOm4hdzsafMyvVc0ZKJCrqiZTPU,24471
|
|
53
53
|
vgi/_test_fixtures/orchard_catalog.py,sha256=8Uv-mKTIgl-OYU9JLgG23owlHGmgniyF2chPoKXGtuI,1643
|
|
54
54
|
vgi/_test_fixtures/simple_writable.py,sha256=OmyENVnsgJmYH0_5sNWMt_Xgj9JoPaOu7uI5_ROZDKg,32020
|
|
55
|
-
vgi/_test_fixtures/table_in_out.py,sha256=
|
|
55
|
+
vgi/_test_fixtures/table_in_out.py,sha256=sWTSXwH-BUaSNOQsDmDqlFZxZ5GGtD-_xmysD2g7aV8,93148
|
|
56
56
|
vgi/_test_fixtures/table_in_out_same_name.py,sha256=jdsQNdKn64T54W_TcLB-rhydI69Y0Uzr5n6FU7v9rIU,8649
|
|
57
57
|
vgi/_test_fixtures/twin_catalogs.py,sha256=UFsFuZ_nFpe62HatGnOMru36vk35Xt3Ta1Hkr1aWN9w,4450
|
|
58
58
|
vgi/_test_fixtures/versioned.py,sha256=gnxNejc4cNGPKl_DxJk1SU8Ielj-rwmQYGkiShXzxK4,5786
|
|
59
59
|
vgi/_test_fixtures/versioned_tables.py,sha256=_JHPe6LaiO0x2We28Ion8hh6pSATmHZYvAczB4uwBsA,22272
|
|
60
|
-
vgi/_test_fixtures/worker.py,sha256=
|
|
60
|
+
vgi/_test_fixtures/worker.py,sha256=tAdCcL9x3LwgtY0OG56bapmBCwGZJ-M7paQY9hXjgB8,96440
|
|
61
61
|
vgi/_test_fixtures/accumulate/__init__.py,sha256=4hYT8jqRoVHSjV9TB7v0Z1CMJtdLuPaDWSz4J2fvMDs,868
|
|
62
62
|
vgi/_test_fixtures/accumulate/worker.py,sha256=4a2VaCoOIXPEoJpkNume7FoBlQzs9kRl8b-_ZJMHvnQ,30165
|
|
63
63
|
vgi/_test_fixtures/aggregate/__init__.py,sha256=2GCp80FNrsWFOA2SV3lCffzLjoN5AKwrf57t2YRg8O0,2393
|
|
@@ -152,8 +152,8 @@ vgi/transactor/_duckdb_compat.py,sha256=sXVZ9JLKAQyGR1BjWczSwdQEavtr-TcZPoVZZnTr
|
|
|
152
152
|
vgi/transactor/client.py,sha256=7DTeMksogsw6ANjQjGOPpKYrV76rg4_kGjktMJf54jg,4486
|
|
153
153
|
vgi/transactor/protocol.py,sha256=v59IvrKnuvwOXvn_HEcBAbCzDVcx0akgKN0R1mChXg0,5034
|
|
154
154
|
vgi/transactor/server.py,sha256=nzsZQxJZdgappdYX8okrFIrjFtUbdM_Hhskl1fZ2nDY,32553
|
|
155
|
-
vgi_python-0.
|
|
156
|
-
vgi_python-0.
|
|
157
|
-
vgi_python-0.
|
|
158
|
-
vgi_python-0.
|
|
159
|
-
vgi_python-0.
|
|
155
|
+
vgi_python-0.34.0.dist-info/METADATA,sha256=4-WKG1yNlJmW1IRUWKY1kssQ_QLYhOrSlZqQs_7dz28,25617
|
|
156
|
+
vgi_python-0.34.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
157
|
+
vgi_python-0.34.0.dist-info/entry_points.txt,sha256=3Kz1vgodw3pOL_xjtSyDB55-ZRy-U2X-X_Bdr582x0Q,165
|
|
158
|
+
vgi_python-0.34.0.dist-info/licenses/LICENSE,sha256=pbJb4zZasP6n5ifEV81wFu017TarjydaYVmGbHcehtY,6103
|
|
159
|
+
vgi_python-0.34.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|