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.
@@ -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, RpcServer
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.worker import _get_vgi_version
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 = RpcServer(
384
- VgiProtocol,
405
+ server = build_rpc_server(
385
406
  worker,
407
+ transport="http",
408
+ describe=describe,
386
409
  external_location=external_location,
387
- enable_describe=describe,
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)
@@ -106,12 +106,12 @@ class GeoDistanceListFunction(ScalarFunction):
106
106
  @classmethod
107
107
  def compute(
108
108
  cls,
109
- p1: Annotated[ # type: ignore[type-arg]
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[ # type: ignore[type-arg]
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[ # type: ignore[type-arg]
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[ # type: ignore[type-arg]
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[ # type: ignore[type-arg]
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[ # type: ignore[type-arg]
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),
@@ -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[ # type: ignore[valid-type]
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 ``twin_b`` fixture catalogs (all
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",