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.
@@ -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
 
@@ -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 typing import Annotated, Any, get_args, get_origin, get_type_hints
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 (requires ``vgi[http]``):
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
- ``__introspect_token__``. Only consulted when the worker class
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(VgiProtocol, worker, enable_describe=describe, server_version=_get_vgi_version())
325
-
326
- # Absent unless the worker class actually implements the lookup. Passing
327
- # ``None`` leaves the route unrouted rather than routed-and-refusing, which
328
- # is what keeps a dependency upgrade from growing a credential oracle.
329
- introspect_resolver = worker_cls._introspect_resolver()
330
- introspect_kwargs: dict[str, Any] = {}
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 __introspect_token__. "
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 enables the\n"
839
- " POST /__introspect_token__ endpoint, but no introspector allowlist\n"
840
- " was configured. Set VGI_INTROSPECT_PRINCIPALS (comma-separated) or\n"
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 disable the\n"
847
- " endpoint entirely.\n"
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 enable ``POST {prefix}/__introspect_token__``, which a
1424
- reverse proxy calls when it terminates the only public listener and
1425
- must know *which principal* a credential is before it can authorize
1426
- anything. Until it is overridden the route does not exist at all —
1427
- not "exists and refuses", absent — so no worker grows a
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.http.server._introspect`` for the four ways that breaks.
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.32.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.45.1
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=_aO3r6LrRrM7ziOQ27i_ouaofXD_2XMHiwhihNVfj0o,32374
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=bZq6yWdCI4tP1XglzO-p6t3Dt0b93Am-d57s1aGB_38,1993
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=3ln4b0a20is6nPLyZW-Goiexqg6PpeeuzNfKcWoCJHU,163944
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=53L01b2KeKGxslniDBOYo299GQmAHm7JaDAC3RNLl6Q,6678
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=xfiGIlTaLvalFxh233u-BKGdWOBHgza3yFtVGKb5Qjk,64719
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=NdwS5EqVtZ4ZdeDpEr7kinCJ_AX8jMM8SjJ8nd6JwCU,249336
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=L8kxafWZ-4Sd73F6EpCRo1kWVC-09qzof6OYRXazS_8,2495
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=Ug-n9wvqT5StT7Sm0OQ1MAmOCQr2jzdcpfneiQCpmJM,89813
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=5ocHOoa4mksNCTA39LOdvnhDhusam9hVpEb2MQOFSjU,96306
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.32.0.dist-info/METADATA,sha256=5w0sPJG--crdBQDtvcjDQdPDCr4f89IOXqdPh_PKj5c,25617
156
- vgi_python-0.32.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
157
- vgi_python-0.32.0.dist-info/entry_points.txt,sha256=3Kz1vgodw3pOL_xjtSyDB55-ZRy-U2X-X_Bdr582x0Q,165
158
- vgi_python-0.32.0.dist-info/licenses/LICENSE,sha256=pbJb4zZasP6n5ifEV81wFu017TarjydaYVmGbHcehtY,6103
159
- vgi_python-0.32.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any