vgi-python 0.38.1__py3-none-any.whl → 0.40.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/catalog_contents.py +336 -0
- vgi/_test_fixtures/http_server.py +30 -8
- vgi/_test_fixtures/identity_fixture.py +75 -0
- vgi/_test_fixtures/scalar/geo.py +12 -12
- vgi/_test_fixtures/worker.py +21 -3
- vgi/catalog/__init__.py +4 -0
- vgi/catalog/catalog_interface.py +148 -0
- vgi/client/__init__.py +2 -1
- vgi/client/catalog_mixin.py +67 -0
- vgi/copy_from_function.py +3 -5
- vgi/filter_v2.py +16 -5
- vgi/meta_worker.py +86 -6
- vgi/protocol.py +106 -3
- vgi/protocol_version.txt +1 -1
- vgi/rpc_server.py +287 -0
- vgi/serve.py +11 -119
- vgi/table_buffering_function.py +1 -0
- vgi/table_in_out_function.py +1 -0
- vgi/worker.py +255 -22
- {vgi_python-0.38.1.dist-info → vgi_python-0.40.0.dist-info}/METADATA +5 -5
- {vgi_python-0.38.1.dist-info → vgi_python-0.40.0.dist-info}/RECORD +24 -21
- {vgi_python-0.38.1.dist-info → vgi_python-0.40.0.dist-info}/WHEEL +0 -0
- {vgi_python-0.38.1.dist-info → vgi_python-0.40.0.dist-info}/entry_points.txt +0 -0
- {vgi_python-0.38.1.dist-info → vgi_python-0.40.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
|
|
3
|
+
"""Small catalogs for the ``catalog_contents`` RPC.
|
|
4
|
+
|
|
5
|
+
The same static two-schema catalog is served under three names, differing only
|
|
6
|
+
in how they answer ``catalog_contents`` (see ``vgi/docs/catalog_contents.md``):
|
|
7
|
+
|
|
8
|
+
``contents_probe``
|
|
9
|
+
Advertises ``supports_catalog_contents`` and serves it normally (the
|
|
10
|
+
``ReadOnlyCatalogInterface`` default: version-frozen, no etag, and cached
|
|
11
|
+
by the worker). A client loads it in one RPC.
|
|
12
|
+
``contents_broken``
|
|
13
|
+
Advertises ``supports_catalog_contents`` but its ``catalog_contents``
|
|
14
|
+
raises. A client must fall back to ``catalog_schemas`` plus the per-schema
|
|
15
|
+
``catalog_schema_contents_*`` RPCs and still see the whole catalog.
|
|
16
|
+
``contents_legacy``
|
|
17
|
+
Does not advertise it, like an older worker. A client must never call
|
|
18
|
+
``catalog_contents`` and must use the per-schema RPCs.
|
|
19
|
+
|
|
20
|
+
Three DDL-capable ``InMemoryCatalog`` catalogs (version not frozen) advertise
|
|
21
|
+
it too. Each ATTACH gets its own empty catalog (one ``main`` schema), so tests
|
|
22
|
+
sharing a warm worker never see each other's objects:
|
|
23
|
+
|
|
24
|
+
``contents_memory``
|
|
25
|
+
Reports ``catalog_version`` 0 ("unknown") and no etag. Exercises the
|
|
26
|
+
client's version-0 rule: the first load of an attach uses
|
|
27
|
+
``catalog_contents``, but reloads (the client clears a version-0 catalog
|
|
28
|
+
at every transaction start) use the lazy per-schema RPCs instead.
|
|
29
|
+
``contents_reval``
|
|
30
|
+
A revalidating catalog with a cheap validator: the etag is
|
|
31
|
+
``"gen-<n>"``, where ``n`` is the catalog version, bumped by every DDL.
|
|
32
|
+
A matching ``if_none_match`` answers ``not_modified`` without building
|
|
33
|
+
anything. The client revalidates with it at transaction start instead of
|
|
34
|
+
polling ``catalog_version``.
|
|
35
|
+
``contents_hash``
|
|
36
|
+
Returns no etag of its own but sets ``catalog_contents_etag =
|
|
37
|
+
"content-hash"``: the worker builds the snapshot on every call and uses
|
|
38
|
+
its SHA-256 as the etag, answering ``not_modified`` when it matches.
|
|
39
|
+
|
|
40
|
+
The static catalog holds every kind the client seeds from ``catalog_contents``
|
|
41
|
+
at least once (tables, a view, scalar / aggregate / table functions, scalar and
|
|
42
|
+
table macros), split over two schemas (``main`` and ``extra``), so RPC
|
|
43
|
+
accounting in ``vgi/test/sql/integration/catalog/catalog_contents*.test`` is
|
|
44
|
+
deterministic. Served by ``vgi-fixture-worker`` and ``vgi-fixture-http``
|
|
45
|
+
(MetaWorker), so the same tests run over every transport. Their functions
|
|
46
|
+
re-use the example catalog's classes (``main.double`` ...), so every worker
|
|
47
|
+
here sets ``route_unattached_calls = False``: they are reachable only through
|
|
48
|
+
their own attach, and an unattached ``main.double`` still routes to the
|
|
49
|
+
example worker.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import uuid
|
|
55
|
+
from dataclasses import replace
|
|
56
|
+
from typing import Any
|
|
57
|
+
|
|
58
|
+
import pyarrow as pa
|
|
59
|
+
|
|
60
|
+
from vgi._test_fixtures.aggregate.basic import SumFunction
|
|
61
|
+
from vgi._test_fixtures.catalog import CatalogData, InMemoryCatalog, SchemaData
|
|
62
|
+
from vgi._test_fixtures.scalar.arithmetic import DoubleFunction
|
|
63
|
+
from vgi._test_fixtures.table.sequence import SequenceFunction
|
|
64
|
+
from vgi.arguments import Arguments
|
|
65
|
+
from vgi.catalog import (
|
|
66
|
+
AttachOpaqueData,
|
|
67
|
+
Catalog,
|
|
68
|
+
CatalogAttachResult,
|
|
69
|
+
CatalogContentsResult,
|
|
70
|
+
CatalogInfo,
|
|
71
|
+
Macro,
|
|
72
|
+
MacroType,
|
|
73
|
+
ReadOnlyCatalogInterface,
|
|
74
|
+
Schema,
|
|
75
|
+
SchemaInfo,
|
|
76
|
+
Table,
|
|
77
|
+
View,
|
|
78
|
+
)
|
|
79
|
+
from vgi.worker import Worker
|
|
80
|
+
|
|
81
|
+
CATALOG_PROBE = "contents_probe"
|
|
82
|
+
CATALOG_BROKEN = "contents_broken"
|
|
83
|
+
CATALOG_LEGACY = "contents_legacy"
|
|
84
|
+
CATALOG_MEMORY = "contents_memory"
|
|
85
|
+
CATALOG_REVAL = "contents_reval"
|
|
86
|
+
CATALOG_HASH = "contents_hash"
|
|
87
|
+
|
|
88
|
+
BROKEN_MESSAGE = "contents_broken: catalog_contents deliberately fails"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _catalog(name: str) -> Catalog:
|
|
92
|
+
return Catalog(
|
|
93
|
+
name=name,
|
|
94
|
+
default_schema="main",
|
|
95
|
+
comment=f"catalog_contents test catalog ({name})",
|
|
96
|
+
schemas=[
|
|
97
|
+
Schema(
|
|
98
|
+
path=["main"],
|
|
99
|
+
comment="Every object kind",
|
|
100
|
+
tables=[
|
|
101
|
+
Table(
|
|
102
|
+
name="ten",
|
|
103
|
+
function=SequenceFunction,
|
|
104
|
+
arguments=Arguments(positional=(pa.scalar(10),)),
|
|
105
|
+
comment="Integers 0..9",
|
|
106
|
+
),
|
|
107
|
+
],
|
|
108
|
+
views=[View(name="answer", definition="SELECT 42 AS answer", comment="One row")],
|
|
109
|
+
functions=[DoubleFunction, SumFunction, SequenceFunction],
|
|
110
|
+
macros=[
|
|
111
|
+
Macro(
|
|
112
|
+
name="contents_triple",
|
|
113
|
+
macro_type=MacroType.SCALAR,
|
|
114
|
+
parameters=["x"],
|
|
115
|
+
definition="x * 3",
|
|
116
|
+
comment="Triple a value",
|
|
117
|
+
),
|
|
118
|
+
Macro(
|
|
119
|
+
name="contents_range",
|
|
120
|
+
macro_type=MacroType.TABLE,
|
|
121
|
+
parameters=["n"],
|
|
122
|
+
definition="SELECT * FROM range(n)",
|
|
123
|
+
comment="Table macro over range(n)",
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
),
|
|
127
|
+
Schema(
|
|
128
|
+
path=["extra"],
|
|
129
|
+
comment="A second schema, tables only",
|
|
130
|
+
tables=[
|
|
131
|
+
Table(
|
|
132
|
+
name="five",
|
|
133
|
+
function=SequenceFunction,
|
|
134
|
+
arguments=Arguments(positional=(pa.scalar(5),)),
|
|
135
|
+
comment="Integers 0..4",
|
|
136
|
+
),
|
|
137
|
+
],
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_CATALOG_PROBE = _catalog(CATALOG_PROBE)
|
|
144
|
+
_CATALOG_BROKEN = _catalog(CATALOG_BROKEN)
|
|
145
|
+
_CATALOG_LEGACY = _catalog(CATALOG_LEGACY)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ContentsProbeCatalog(ReadOnlyCatalogInterface):
|
|
149
|
+
"""Advertises and serves ``catalog_contents``."""
|
|
150
|
+
|
|
151
|
+
catalog = _CATALOG_PROBE
|
|
152
|
+
catalog_name = CATALOG_PROBE
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ContentsBrokenCatalog(ReadOnlyCatalogInterface):
|
|
156
|
+
"""Advertises ``catalog_contents`` but fails to serve it."""
|
|
157
|
+
|
|
158
|
+
catalog = _CATALOG_BROKEN
|
|
159
|
+
catalog_name = CATALOG_BROKEN
|
|
160
|
+
|
|
161
|
+
def catalog_contents(self, *, attach_opaque_data: Any, if_none_match: str | None = None) -> CatalogContentsResult:
|
|
162
|
+
"""Always fail, to drive the client's per-schema fallback."""
|
|
163
|
+
raise RuntimeError(BROKEN_MESSAGE)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ContentsLegacyCatalog(ReadOnlyCatalogInterface):
|
|
167
|
+
"""Does not advertise ``catalog_contents`` (an older worker)."""
|
|
168
|
+
|
|
169
|
+
catalog = _CATALOG_LEGACY
|
|
170
|
+
catalog_name = CATALOG_LEGACY
|
|
171
|
+
|
|
172
|
+
def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult:
|
|
173
|
+
"""Attach as the base class does, without the capability flag."""
|
|
174
|
+
return replace(super().catalog_attach(**kwargs), supports_catalog_contents=False)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ContentsProbeWorker(Worker):
|
|
178
|
+
"""Serves ``contents_probe``."""
|
|
179
|
+
|
|
180
|
+
route_unattached_calls = False
|
|
181
|
+
|
|
182
|
+
catalog_interface = ContentsProbeCatalog
|
|
183
|
+
catalog_name = CATALOG_PROBE
|
|
184
|
+
catalog = _CATALOG_PROBE
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ContentsBrokenWorker(Worker):
|
|
188
|
+
"""Serves ``contents_broken``."""
|
|
189
|
+
|
|
190
|
+
route_unattached_calls = False
|
|
191
|
+
|
|
192
|
+
catalog_interface = ContentsBrokenCatalog
|
|
193
|
+
catalog_name = CATALOG_BROKEN
|
|
194
|
+
catalog = _CATALOG_BROKEN
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ContentsLegacyWorker(Worker):
|
|
198
|
+
"""Serves ``contents_legacy``."""
|
|
199
|
+
|
|
200
|
+
route_unattached_calls = False
|
|
201
|
+
|
|
202
|
+
catalog_interface = ContentsLegacyCatalog
|
|
203
|
+
catalog_name = CATALOG_LEGACY
|
|
204
|
+
catalog = _CATALOG_LEGACY
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class _PrivateMemoryCatalog(InMemoryCatalog):
|
|
208
|
+
"""DDL-capable in-memory catalog that advertises ``catalog_contents``.
|
|
209
|
+
|
|
210
|
+
Every ATTACH of `public_name` gets a private, empty catalog (one ``main``
|
|
211
|
+
schema) registered under an internal name, so state never leaks between
|
|
212
|
+
attaches that share one worker process.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
public_name: str = ""
|
|
216
|
+
|
|
217
|
+
def __init__(self) -> None:
|
|
218
|
+
"""Start with no catalogs; each attach creates its own."""
|
|
219
|
+
super().__init__()
|
|
220
|
+
self._catalogs.clear()
|
|
221
|
+
|
|
222
|
+
def catalogs(self) -> list[CatalogInfo]:
|
|
223
|
+
"""Advertise only the public name (MetaWorker routes on it)."""
|
|
224
|
+
return [CatalogInfo(name=self.public_name, implementation_version=None, data_version_spec=None)]
|
|
225
|
+
|
|
226
|
+
def catalog_attach(
|
|
227
|
+
self,
|
|
228
|
+
*,
|
|
229
|
+
name: str,
|
|
230
|
+
options: dict[str, Any],
|
|
231
|
+
data_version_spec: str | None,
|
|
232
|
+
implementation_version: str | None,
|
|
233
|
+
ctx: Any = None,
|
|
234
|
+
) -> CatalogAttachResult:
|
|
235
|
+
"""Attach a fresh private catalog, advertising ``catalog_contents``."""
|
|
236
|
+
if name != self.public_name:
|
|
237
|
+
raise ValueError(f"Unknown catalog: {name!r}. Available: {self.public_name}")
|
|
238
|
+
private = f"{self.public_name}/{uuid.uuid4().hex}"
|
|
239
|
+
catalog = CatalogData(name=private)
|
|
240
|
+
catalog.schemas[("main",)] = SchemaData(
|
|
241
|
+
info=SchemaInfo(attach_opaque_data=AttachOpaqueData(b"\x00" * 16), path=["main"], comment=None, tags={})
|
|
242
|
+
)
|
|
243
|
+
self._catalogs[private] = catalog
|
|
244
|
+
result = super().catalog_attach(
|
|
245
|
+
name=private,
|
|
246
|
+
options=options,
|
|
247
|
+
data_version_spec=data_version_spec,
|
|
248
|
+
implementation_version=implementation_version,
|
|
249
|
+
ctx=ctx,
|
|
250
|
+
)
|
|
251
|
+
return replace(
|
|
252
|
+
result,
|
|
253
|
+
supports_catalog_contents=True,
|
|
254
|
+
catalog_version=self.catalog_version(
|
|
255
|
+
attach_opaque_data=result.attach_opaque_data, transaction_opaque_data=None
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def catalog_detach(self, *, attach_opaque_data: AttachOpaqueData) -> None:
|
|
260
|
+
"""Detach and drop the private catalog."""
|
|
261
|
+
private = self._attachments.pop(attach_opaque_data, None)
|
|
262
|
+
if private is not None:
|
|
263
|
+
self._catalogs.pop(private, None)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class ContentsMemoryCatalog(_PrivateMemoryCatalog):
|
|
267
|
+
"""Private in-memory catalog reporting version 0 and no etag (the version-0 rule)."""
|
|
268
|
+
|
|
269
|
+
public_name = CATALOG_MEMORY
|
|
270
|
+
|
|
271
|
+
def catalog_version(
|
|
272
|
+
self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: Any, ctx: Any = None
|
|
273
|
+
) -> int:
|
|
274
|
+
"""Always 0: the catalog does not track its version."""
|
|
275
|
+
del attach_opaque_data, transaction_opaque_data, ctx
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class ContentsRevalCatalog(_PrivateMemoryCatalog):
|
|
280
|
+
"""Private in-memory catalog that revalidates with a generation-counter etag."""
|
|
281
|
+
|
|
282
|
+
public_name = CATALOG_REVAL
|
|
283
|
+
|
|
284
|
+
def catalog_contents(
|
|
285
|
+
self, *, attach_opaque_data: AttachOpaqueData, if_none_match: str | None = None
|
|
286
|
+
) -> CatalogContentsResult:
|
|
287
|
+
"""Answer ``not_modified`` from the generation counter, building only on a miss."""
|
|
288
|
+
etag = f"gen-{self._get_catalog(attach_opaque_data).version}"
|
|
289
|
+
if if_none_match == etag:
|
|
290
|
+
return CatalogContentsResult(etag=etag, not_modified=True)
|
|
291
|
+
built = super().catalog_contents(attach_opaque_data=attach_opaque_data)
|
|
292
|
+
return CatalogContentsResult(schemas=built.schemas, etag=etag)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class ContentsHashCatalog(_PrivateMemoryCatalog):
|
|
296
|
+
"""Private in-memory catalog revalidated by the framework's content hash."""
|
|
297
|
+
|
|
298
|
+
public_name = CATALOG_HASH
|
|
299
|
+
catalog_contents_etag = "content-hash"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ContentsMemoryWorker(Worker):
|
|
303
|
+
"""Serves ``contents_memory``."""
|
|
304
|
+
|
|
305
|
+
route_unattached_calls = False
|
|
306
|
+
|
|
307
|
+
catalog_interface = ContentsMemoryCatalog
|
|
308
|
+
catalog_name = CATALOG_MEMORY
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class ContentsRevalWorker(Worker):
|
|
312
|
+
"""Serves ``contents_reval``."""
|
|
313
|
+
|
|
314
|
+
route_unattached_calls = False
|
|
315
|
+
|
|
316
|
+
catalog_interface = ContentsRevalCatalog
|
|
317
|
+
catalog_name = CATALOG_REVAL
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class ContentsHashWorker(Worker):
|
|
321
|
+
"""Serves ``contents_hash``."""
|
|
322
|
+
|
|
323
|
+
route_unattached_calls = False
|
|
324
|
+
|
|
325
|
+
catalog_interface = ContentsHashCatalog
|
|
326
|
+
catalog_name = CATALOG_HASH
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
CONTENTS_WORKERS: list[type[Worker]] = [
|
|
330
|
+
ContentsProbeWorker,
|
|
331
|
+
ContentsBrokenWorker,
|
|
332
|
+
ContentsLegacyWorker,
|
|
333
|
+
ContentsMemoryWorker,
|
|
334
|
+
ContentsRevalWorker,
|
|
335
|
+
ContentsHashWorker,
|
|
336
|
+
]
|
|
@@ -187,6 +187,16 @@ def main() -> None:
|
|
|
187
187
|
"with no continuation escape. Default: no cap."
|
|
188
188
|
),
|
|
189
189
|
),
|
|
190
|
+
identity: bool = typer.Option(
|
|
191
|
+
False,
|
|
192
|
+
"--identity",
|
|
193
|
+
envvar="VGI_FIXTURE_IDENTITY",
|
|
194
|
+
help=(
|
|
195
|
+
"Host vgi_rpc.Identity.v1 with the vgi-rpc conformance fixture policy and its "
|
|
196
|
+
"spoofable X-Conformance-Principal header authentication. Test-only; for "
|
|
197
|
+
"vgi-rpc-test-hosted --identity."
|
|
198
|
+
),
|
|
199
|
+
),
|
|
190
200
|
port_file: str | None = typer.Option(
|
|
191
201
|
None,
|
|
192
202
|
"--port-file",
|
|
@@ -198,7 +208,7 @@ def main() -> None:
|
|
|
198
208
|
),
|
|
199
209
|
) -> None:
|
|
200
210
|
try:
|
|
201
|
-
from vgi_rpc import Compression, ExternalLocationConfig
|
|
211
|
+
from vgi_rpc import Compression, ExternalLocationConfig
|
|
202
212
|
from vgi_rpc.http import make_wsgi_app
|
|
203
213
|
except ImportError:
|
|
204
214
|
sys.stderr.write(
|
|
@@ -301,8 +311,6 @@ def main() -> None:
|
|
|
301
311
|
|
|
302
312
|
import socket
|
|
303
313
|
|
|
304
|
-
from vgi.protocol import VgiProtocol
|
|
305
|
-
|
|
306
314
|
if port == 0:
|
|
307
315
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
308
316
|
s.bind((host, 0))
|
|
@@ -316,6 +324,13 @@ def main() -> None:
|
|
|
316
324
|
authenticate = _resolve_authenticate()
|
|
317
325
|
oauth_metadata = _resolve_oauth_resource_metadata()
|
|
318
326
|
|
|
327
|
+
introspect_principals: list[str] | None = None
|
|
328
|
+
if identity:
|
|
329
|
+
from vgi._test_fixtures.identity_fixture import INTROSPECT_PRINCIPALS, conformance_authenticate
|
|
330
|
+
|
|
331
|
+
authenticate = conformance_authenticate
|
|
332
|
+
introspect_principals = INTROSPECT_PRINCIPALS
|
|
333
|
+
|
|
319
334
|
if authenticate is None:
|
|
320
335
|
# Test-only OPTIONAL bearer auth. Lets the cache identity-isolation
|
|
321
336
|
# test attach the same worker under different principals (alice/bob)
|
|
@@ -354,9 +369,10 @@ def main() -> None:
|
|
|
354
369
|
# and not the other fails the file on that transport alone — which reads
|
|
355
370
|
# as a transport bug and is not one. narrow_bind drifted exactly that
|
|
356
371
|
# way, and it guards a client SIGSEGV, so it was the worst one to lose.
|
|
372
|
+
from vgi._test_fixtures.catalog_contents import CONTENTS_WORKERS
|
|
357
373
|
from vgi._test_fixtures.narrow_bind.worker import NarrowBindWorker
|
|
358
374
|
from vgi._test_fixtures.twin_catalogs import TwinAWorker, TwinBWorker
|
|
359
|
-
from vgi.
|
|
375
|
+
from vgi.rpc_server import build_rpc_server
|
|
360
376
|
|
|
361
377
|
worker_classes: list[type] = [
|
|
362
378
|
ExampleWorker,
|
|
@@ -366,6 +382,7 @@ def main() -> None:
|
|
|
366
382
|
NarrowBindWorker,
|
|
367
383
|
TwinAWorker,
|
|
368
384
|
TwinBWorker,
|
|
385
|
+
*CONTENTS_WORKERS,
|
|
369
386
|
]
|
|
370
387
|
try:
|
|
371
388
|
from vgi._test_fixtures.writable.worker import WritableWorker
|
|
@@ -373,6 +390,11 @@ def main() -> None:
|
|
|
373
390
|
pass
|
|
374
391
|
else:
|
|
375
392
|
worker_classes.append(WritableWorker)
|
|
393
|
+
if identity:
|
|
394
|
+
# Same "example" catalog, plus the conformance identity hooks.
|
|
395
|
+
from vgi._test_fixtures.identity_fixture import IdentityExampleWorker
|
|
396
|
+
|
|
397
|
+
worker_classes[worker_classes.index(ExampleWorker)] = IdentityExampleWorker
|
|
376
398
|
workers = [wc(quiet=True, log_level=effective_level) for wc in worker_classes]
|
|
377
399
|
# One signing key shared by every sub-worker (which seal catalog
|
|
378
400
|
# opaque-data envelopes) and the HTTP state-token machinery.
|
|
@@ -380,12 +402,12 @@ def main() -> None:
|
|
|
380
402
|
for w in workers:
|
|
381
403
|
w._signing_key = signing_key
|
|
382
404
|
worker: Any = workers[0] if len(workers) == 1 else MetaWorker(workers)
|
|
383
|
-
server =
|
|
384
|
-
VgiProtocol,
|
|
405
|
+
server = build_rpc_server(
|
|
385
406
|
worker,
|
|
407
|
+
transport="http",
|
|
408
|
+
describe=describe,
|
|
386
409
|
external_location=external_location,
|
|
387
|
-
|
|
388
|
-
server_version=_get_vgi_version(),
|
|
410
|
+
introspect_principals=introspect_principals,
|
|
389
411
|
)
|
|
390
412
|
wsgi_app = make_wsgi_app(
|
|
391
413
|
server,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
|
|
3
|
+
"""Opt the HTTP fixture into ``vgi_rpc.Identity.v1`` with the conformance policy.
|
|
4
|
+
|
|
5
|
+
Enabled by ``vgi-fixture-http --identity`` (or ``VGI_FIXTURE_IDENTITY=1``) so
|
|
6
|
+
the cross-SDK hosted-protocols group can run its identity cases with
|
|
7
|
+
``vgi-rpc-test-hosted --url ... --identity``. Off by default, so the DuckDB
|
|
8
|
+
integration runs see the same worker as before.
|
|
9
|
+
|
|
10
|
+
The policy is pinned by vgi-rpc's ``IDENTITY_CONFORMANCE_FIXTURE.md``. The
|
|
11
|
+
hooks and constants come straight from the reference
|
|
12
|
+
(``vgi_rpc.conformance.identity_fixture``) rather than being transcribed.
|
|
13
|
+
Copying them would let this fixture drift from the contract it claims to meet.
|
|
14
|
+
|
|
15
|
+
.. warning::
|
|
16
|
+
|
|
17
|
+
The header authenticator below is trivially spoofable by anyone who can
|
|
18
|
+
reach the port. It is a test fixture and must never be deployed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from vgi_rpc.conformance.identity_fixture import (
|
|
26
|
+
AUTH_TIME_HEADER,
|
|
27
|
+
INTROSPECTOR_PRINCIPAL,
|
|
28
|
+
PRINCIPAL_HEADER,
|
|
29
|
+
conformance_mint_grant,
|
|
30
|
+
conformance_resolve_token,
|
|
31
|
+
)
|
|
32
|
+
from vgi_rpc.rpc import AuthContext
|
|
33
|
+
|
|
34
|
+
from vgi._test_fixtures.worker import ExampleWorker
|
|
35
|
+
from vgi.auth import IssuedGrant, TokenIdentity
|
|
36
|
+
|
|
37
|
+
__all__ = ["INTROSPECT_PRINCIPALS", "IdentityExampleWorker", "conformance_authenticate"]
|
|
38
|
+
|
|
39
|
+
#: The introspector allowlist the conformance policy pins: exactly one principal.
|
|
40
|
+
INTROSPECT_PRINCIPALS = [INTROSPECTOR_PRINCIPAL]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IdentityExampleWorker(ExampleWorker):
|
|
44
|
+
"""``ExampleWorker`` whose identity hooks follow the conformance policy.
|
|
45
|
+
|
|
46
|
+
It replaces ``ExampleWorker`` in the fixture's ``MetaWorker``, so it is the
|
|
47
|
+
only child that implements the hooks.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def resolve_token(cls, token: str) -> TokenIdentity | None:
|
|
52
|
+
"""Resolve per the conformance policy (§3.3)."""
|
|
53
|
+
return conformance_resolve_token(token)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def mint_grant(cls, principal: str, purpose: str, scopes: list[str], ttl_seconds: int) -> IssuedGrant:
|
|
57
|
+
"""Mint per the conformance policy (§3.4)."""
|
|
58
|
+
return conformance_mint_grant(principal, purpose, scopes, ttl_seconds)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def conformance_authenticate(req: Any) -> AuthContext:
|
|
62
|
+
"""Derive the caller from ``X-Conformance-Principal`` / ``X-Conformance-Auth-Time``.
|
|
63
|
+
|
|
64
|
+
An absent principal header means *unauthenticated*. The auth-time header is
|
|
65
|
+
placed in the claims verbatim and unparsed, because the guard does the
|
|
66
|
+
parsing and the fixture only carries the value.
|
|
67
|
+
"""
|
|
68
|
+
principal = req.get_header(PRINCIPAL_HEADER)
|
|
69
|
+
if not principal:
|
|
70
|
+
return AuthContext(domain=None, authenticated=False, principal=None)
|
|
71
|
+
claims: dict[str, object] = {}
|
|
72
|
+
auth_time = req.get_header(AUTH_TIME_HEADER)
|
|
73
|
+
if auth_time is not None:
|
|
74
|
+
claims["auth_time"] = auth_time
|
|
75
|
+
return AuthContext(domain="conformance", authenticated=True, principal=principal, claims=claims)
|
vgi/_test_fixtures/scalar/geo.py
CHANGED
|
@@ -106,12 +106,12 @@ class GeoDistanceListFunction(ScalarFunction):
|
|
|
106
106
|
@classmethod
|
|
107
107
|
def compute(
|
|
108
108
|
cls,
|
|
109
|
-
p1: Annotated[
|
|
110
|
-
pa.ListArray,
|
|
109
|
+
p1: Annotated[
|
|
110
|
+
pa.ListArray, # type: ignore[type-arg]
|
|
111
111
|
Param(doc="First point [lat, lon]", arrow_type=pa.list_(pa.float64())),
|
|
112
112
|
],
|
|
113
|
-
p2: Annotated[
|
|
114
|
-
pa.ListArray,
|
|
113
|
+
p2: Annotated[
|
|
114
|
+
pa.ListArray, # type: ignore[type-arg]
|
|
115
115
|
Param(doc="Second point [lat, lon]", arrow_type=pa.list_(pa.float64())),
|
|
116
116
|
],
|
|
117
117
|
) -> Annotated[pa.DoubleArray, Returns()]:
|
|
@@ -151,12 +151,12 @@ class GeoDistanceFixedFunction(ScalarFunction):
|
|
|
151
151
|
@classmethod
|
|
152
152
|
def compute(
|
|
153
153
|
cls,
|
|
154
|
-
p1: Annotated[
|
|
155
|
-
pa.FixedSizeListArray,
|
|
154
|
+
p1: Annotated[
|
|
155
|
+
pa.FixedSizeListArray, # type: ignore[type-arg]
|
|
156
156
|
Param(doc="First point [lat, lon]", arrow_type=pa.list_(pa.float64(), 2)),
|
|
157
157
|
],
|
|
158
|
-
p2: Annotated[
|
|
159
|
-
pa.FixedSizeListArray,
|
|
158
|
+
p2: Annotated[
|
|
159
|
+
pa.FixedSizeListArray, # type: ignore[type-arg]
|
|
160
160
|
Param(doc="Second point [lat, lon]", arrow_type=pa.list_(pa.float64(), 2)),
|
|
161
161
|
],
|
|
162
162
|
) -> Annotated[pa.DoubleArray, Returns()]:
|
|
@@ -240,8 +240,8 @@ class GeoCentroidListFunction(ScalarFunction):
|
|
|
240
240
|
@classmethod
|
|
241
241
|
def compute(
|
|
242
242
|
cls,
|
|
243
|
-
points: Annotated[
|
|
244
|
-
list[pa.ListArray],
|
|
243
|
+
points: Annotated[
|
|
244
|
+
list[pa.ListArray], # type: ignore[type-arg]
|
|
245
245
|
Param(
|
|
246
246
|
doc="Point columns [lat, lon]",
|
|
247
247
|
arrow_type=pa.list_(pa.float64()),
|
|
@@ -284,8 +284,8 @@ class GeoCentroidFixedFunction(ScalarFunction):
|
|
|
284
284
|
@classmethod
|
|
285
285
|
def compute(
|
|
286
286
|
cls,
|
|
287
|
-
points: Annotated[
|
|
288
|
-
list[pa.FixedSizeListArray],
|
|
287
|
+
points: Annotated[
|
|
288
|
+
list[pa.FixedSizeListArray], # type: ignore[type-arg]
|
|
289
289
|
Param(
|
|
290
290
|
doc="Point columns [lat, lon]",
|
|
291
291
|
arrow_type=pa.list_(pa.float64(), 2),
|
vgi/_test_fixtures/worker.py
CHANGED
|
@@ -35,6 +35,7 @@ except ImportError:
|
|
|
35
35
|
import os
|
|
36
36
|
import tempfile
|
|
37
37
|
import uuid
|
|
38
|
+
from collections.abc import Sequence
|
|
38
39
|
from typing import Annotated, Any
|
|
39
40
|
|
|
40
41
|
import pyarrow as pa
|
|
@@ -2143,6 +2144,19 @@ class ExampleWorker(Worker):
|
|
|
2143
2144
|
# operations go through catalog_interface.
|
|
2144
2145
|
catalog = _EXAMPLE_CATALOG
|
|
2145
2146
|
|
|
2147
|
+
@classmethod
|
|
2148
|
+
def hosted_protocols(cls) -> Sequence[tuple[type, object]]:
|
|
2149
|
+
"""Host ``conformance.Secondary.v1`` beside ``vgi.v2``, on every transport.
|
|
2150
|
+
|
|
2151
|
+
The cross-SDK hosted-protocols group (``vgi-rpc-test-hosted``) expects
|
|
2152
|
+
every SDK fixture worker to host the vgi-rpc reference secondary. Only
|
|
2153
|
+
this child of the fixture's ``MetaWorker`` returns it: one process
|
|
2154
|
+
hosts a protocol name once.
|
|
2155
|
+
"""
|
|
2156
|
+
from vgi_rpc.conformance.secondary import conformance_extra_protocols
|
|
2157
|
+
|
|
2158
|
+
return conformance_extra_protocols()
|
|
2159
|
+
|
|
2146
2160
|
class Settings:
|
|
2147
2161
|
"""Settings exposed via catalog_attach."""
|
|
2148
2162
|
|
|
@@ -2151,8 +2165,8 @@ class ExampleWorker(Worker):
|
|
|
2151
2165
|
multiplier: Annotated[int, Setting(desc="Value multiplier")] = 1
|
|
2152
2166
|
threshold: Annotated[int, Setting(desc="Filter threshold")] = 0
|
|
2153
2167
|
scale_factor: Annotated[float, Setting(desc="Float scale factor")] = 1.0
|
|
2154
|
-
config: Annotated[
|
|
2155
|
-
pa.struct([("start", pa.int64()), ("step", pa.int64()), ("label", pa.string())]),
|
|
2168
|
+
config: Annotated[
|
|
2169
|
+
pa.struct([("start", pa.int64()), ("step", pa.int64()), ("label", pa.string())]), # type: ignore[valid-type]
|
|
2156
2170
|
Setting(desc="Sequence configuration struct"),
|
|
2157
2171
|
] = None
|
|
2158
2172
|
|
|
@@ -2178,12 +2192,13 @@ def main() -> None:
|
|
|
2178
2192
|
|
|
2179
2193
|
Always serves the base ExampleWorker catalog plus the
|
|
2180
2194
|
``projection_repro``, ``schema_reconcile``, ``accumulate``,
|
|
2181
|
-
``narrow_bind``, ``twin_a`` and ``
|
|
2195
|
+
``narrow_bind``, ``twin_a``, ``twin_b`` and ``contents_*`` fixture catalogs (all
|
|
2182
2196
|
depend on the ``vgi[test-fixtures]`` extra). Adds the writable
|
|
2183
2197
|
catalog when the ``vgi[test-fixtures-writable]`` extra is also
|
|
2184
2198
|
installed.
|
|
2185
2199
|
"""
|
|
2186
2200
|
from vgi._test_fixtures.accumulate.worker import AccumulateWorker
|
|
2201
|
+
from vgi._test_fixtures.catalog_contents import CONTENTS_WORKERS
|
|
2187
2202
|
from vgi._test_fixtures.narrow_bind.worker import NarrowBindWorker
|
|
2188
2203
|
from vgi._test_fixtures.projection_repro.worker import ProjReproWorker
|
|
2189
2204
|
from vgi._test_fixtures.schema_reconcile.worker import SchemaReconcileWorker
|
|
@@ -2200,6 +2215,9 @@ def main() -> None:
|
|
|
2200
2215
|
# `test_same_name_catalog` — only the attached catalog tells them apart.
|
|
2201
2216
|
TwinAWorker,
|
|
2202
2217
|
TwinBWorker,
|
|
2218
|
+
# contents_*: catalog_contents served / failing / not advertised, and
|
|
2219
|
+
# the version-0, revalidating and content-hash DDL catalogs.
|
|
2220
|
+
*CONTENTS_WORKERS,
|
|
2203
2221
|
]
|
|
2204
2222
|
try:
|
|
2205
2223
|
from vgi._test_fixtures.writable.worker import WritableWorker
|
vgi/catalog/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from vgi.catalog.catalog_interface import (
|
|
|
10
10
|
AttachCatalogInfo,
|
|
11
11
|
AttachOpaqueData,
|
|
12
12
|
CatalogAttachResult,
|
|
13
|
+
CatalogContentsResult,
|
|
13
14
|
CatalogDataVersionRelease,
|
|
14
15
|
CatalogExample,
|
|
15
16
|
CatalogInfo,
|
|
@@ -30,6 +31,7 @@ from vgi.catalog.catalog_interface import (
|
|
|
30
31
|
ScanBranch,
|
|
31
32
|
ScanBranchesResult,
|
|
32
33
|
ScanFunctionResult,
|
|
34
|
+
SchemaContentsInfo,
|
|
33
35
|
SchemaInfo,
|
|
34
36
|
SchemaObjectType,
|
|
35
37
|
SerializedSchema,
|
|
@@ -69,6 +71,8 @@ __all__ = [
|
|
|
69
71
|
"ForeignKeyInfo",
|
|
70
72
|
"IndexInfo",
|
|
71
73
|
"MacroInfo",
|
|
74
|
+
"CatalogContentsResult",
|
|
75
|
+
"SchemaContentsInfo",
|
|
72
76
|
"SchemaInfo",
|
|
73
77
|
"SecretTypeSpec",
|
|
74
78
|
"Setting",
|