vgi-python 0.40.0__py3-none-any.whl → 0.42.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/_duckdb.py +3 -3
- vgi/_test_fixtures/catalog_contents.py +3 -1
- vgi/_test_fixtures/http_server.py +2 -1
- vgi/_test_fixtures/identity_fixture.py +7 -1
- vgi/client/__init__.py +2 -1
- vgi/client/catalog_mixin.py +165 -6
- vgi/client/cli_catalog.py +47 -0
- vgi/client/cli_utils.py +1 -0
- vgi/client/client.py +1 -0
- vgi/rpc_server.py +56 -3
- vgi/serve.py +73 -2
- {vgi_python-0.40.0.dist-info → vgi_python-0.42.0.dist-info}/METADATA +2 -2
- {vgi_python-0.40.0.dist-info → vgi_python-0.42.0.dist-info}/RECORD +16 -16
- {vgi_python-0.40.0.dist-info → vgi_python-0.42.0.dist-info}/WHEEL +0 -0
- {vgi_python-0.40.0.dist-info → vgi_python-0.42.0.dist-info}/entry_points.txt +0 -0
- {vgi_python-0.40.0.dist-info → vgi_python-0.42.0.dist-info}/licenses/LICENSE +0 -0
vgi/_duckdb.py
CHANGED
|
@@ -7,8 +7,8 @@ engine (expression-filter evaluation, the transactor, the statistics
|
|
|
7
7
|
fixtures) resolves it through this module, which prefers Query Farm's
|
|
8
8
|
``haybarn`` distribution when installed and falls back to stock ``duckdb``.
|
|
9
9
|
|
|
10
|
-
Install one via the extras: ``pip install vgi[haybarn]`` (preferred) or
|
|
11
|
-
``pip install vgi[duckdb]``.
|
|
10
|
+
Install one via the extras: ``pip install 'vgi-python[haybarn]'`` (preferred) or
|
|
11
|
+
``pip install 'vgi-python[duckdb]'``.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
@@ -50,7 +50,7 @@ def engine_module() -> ModuleType:
|
|
|
50
50
|
else:
|
|
51
51
|
raise ImportError(
|
|
52
52
|
"No DuckDB-compatible engine is installed. Install 'haybarn' (preferred) "
|
|
53
|
-
"or 'duckdb', e.g. `pip install vgi[haybarn]` or `pip install vgi[duckdb]`."
|
|
53
|
+
"or 'duckdb', e.g. `pip install 'vgi-python[haybarn]'` or `pip install 'vgi-python[duckdb]'`."
|
|
54
54
|
)
|
|
55
55
|
return _engine
|
|
56
56
|
|
|
@@ -42,7 +42,9 @@ at least once (tables, a view, scalar / aggregate / table functions, scalar and
|
|
|
42
42
|
table macros), split over two schemas (``main`` and ``extra``), so RPC
|
|
43
43
|
accounting in ``vgi/test/sql/integration/catalog/catalog_contents*.test`` is
|
|
44
44
|
deterministic. Served by ``vgi-fixture-worker`` and ``vgi-fixture-http``
|
|
45
|
-
(MetaWorker), so the same tests run over every transport.
|
|
45
|
+
(MetaWorker), so the same tests run over every transport. Every other SDK's
|
|
46
|
+
fixture worker serves these six catalogs too, and the same ``.test`` files run
|
|
47
|
+
against each of them, so the semantics here are the cross-SDK contract. Their functions
|
|
46
48
|
re-use the example catalog's classes (``main.double`` ...), so every worker
|
|
47
49
|
here sets ``route_unattached_calls = False``: they are reachable only through
|
|
48
50
|
their own attach, and an unattached ``main.double`` still routes to the
|
|
@@ -372,7 +372,7 @@ def main() -> None:
|
|
|
372
372
|
from vgi._test_fixtures.catalog_contents import CONTENTS_WORKERS
|
|
373
373
|
from vgi._test_fixtures.narrow_bind.worker import NarrowBindWorker
|
|
374
374
|
from vgi._test_fixtures.twin_catalogs import TwinAWorker, TwinBWorker
|
|
375
|
-
from vgi.rpc_server import build_rpc_server
|
|
375
|
+
from vgi.rpc_server import build_rpc_server, resolve_grant_keys
|
|
376
376
|
|
|
377
377
|
worker_classes: list[type] = [
|
|
378
378
|
ExampleWorker,
|
|
@@ -408,6 +408,7 @@ def main() -> None:
|
|
|
408
408
|
describe=describe,
|
|
409
409
|
external_location=external_location,
|
|
410
410
|
introspect_principals=introspect_principals,
|
|
411
|
+
grant_keys=resolve_grant_keys(),
|
|
411
412
|
)
|
|
412
413
|
wsgi_app = make_wsgi_app(
|
|
413
414
|
server,
|
|
@@ -61,12 +61,18 @@ class IdentityExampleWorker(ExampleWorker):
|
|
|
61
61
|
def conformance_authenticate(req: Any) -> AuthContext:
|
|
62
62
|
"""Derive the caller from ``X-Conformance-Principal`` / ``X-Conformance-Auth-Time``.
|
|
63
63
|
|
|
64
|
-
An absent principal header means *unauthenticated
|
|
64
|
+
An absent principal header means *unauthenticated*, unless an
|
|
65
|
+
``Authorization`` header is present: that request falls through to the
|
|
66
|
+
sealed-grant and ``resolve_token`` bearers. The auth-time header is
|
|
65
67
|
placed in the claims verbatim and unparsed, because the guard does the
|
|
66
68
|
parsing and the fixture only carries the value.
|
|
67
69
|
"""
|
|
68
70
|
principal = req.get_header(PRINCIPAL_HEADER)
|
|
69
71
|
if not principal:
|
|
72
|
+
if req.get_header("Authorization"):
|
|
73
|
+
# A bearer is not ours: fall through to the identity bearer
|
|
74
|
+
# authenticators vgi-rpc appends (sealed grants, resolve_token).
|
|
75
|
+
raise ValueError("no conformance principal header")
|
|
70
76
|
return AuthContext(domain=None, authenticated=False, principal=None)
|
|
71
77
|
claims: dict[str, object] = {}
|
|
72
78
|
auth_time = req.get_header(AUTH_TIME_HEADER)
|
vgi/client/__init__.py
CHANGED
|
@@ -45,7 +45,7 @@ from vgi.client.aggregate import (
|
|
|
45
45
|
AggregateSession,
|
|
46
46
|
AggregateStreamingSession,
|
|
47
47
|
)
|
|
48
|
-
from vgi.client.catalog_mixin import CatalogClientMixin, CatalogContents
|
|
48
|
+
from vgi.client.catalog_mixin import CatalogClientMixin, CatalogContents, CatalogSnapshot
|
|
49
49
|
from vgi.client.client import Client, ClientError, ResumableTableScan, ResumeUnsupported
|
|
50
50
|
|
|
51
51
|
if TYPE_CHECKING:
|
|
@@ -57,6 +57,7 @@ __all__ = [
|
|
|
57
57
|
"AggregateStreamingSession",
|
|
58
58
|
"CatalogClientMixin",
|
|
59
59
|
"CatalogContents",
|
|
60
|
+
"CatalogSnapshot",
|
|
60
61
|
"Client",
|
|
61
62
|
"ClientError",
|
|
62
63
|
"OutputWriter",
|
vgi/client/catalog_mixin.py
CHANGED
|
@@ -42,7 +42,7 @@ import shlex
|
|
|
42
42
|
import sys
|
|
43
43
|
from collections.abc import Iterator, Sequence
|
|
44
44
|
from contextlib import contextmanager
|
|
45
|
-
from dataclasses import dataclass
|
|
45
|
+
from dataclasses import dataclass, replace
|
|
46
46
|
from typing import Any, Literal, overload
|
|
47
47
|
|
|
48
48
|
import pyarrow as pa
|
|
@@ -129,6 +129,55 @@ class CatalogContents:
|
|
|
129
129
|
schemas: list[SchemaContentsInfo]
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class CatalogSnapshot:
|
|
134
|
+
"""A whole catalog — every schema and everything in it — from [`CatalogClientMixin.load_catalog`][].
|
|
135
|
+
|
|
136
|
+
Hold on to it and pass it back as ``previous`` to revalidate: when it came
|
|
137
|
+
from ``catalog_contents`` with an etag, the next load sends
|
|
138
|
+
``if_none_match`` and a ``not_modified`` answer returns this same content
|
|
139
|
+
without re-downloading it.
|
|
140
|
+
|
|
141
|
+
Attributes:
|
|
142
|
+
schemas: One [`SchemaContentsInfo`][] per schema, parents first.
|
|
143
|
+
catalog_version: The version the snapshot was taken at, or ``None``
|
|
144
|
+
when it was assembled from the per-schema RPCs (which carry none).
|
|
145
|
+
etag: The ``catalog_contents`` validator, or ``None`` when the worker
|
|
146
|
+
does not revalidate or the snapshot did not come from
|
|
147
|
+
``catalog_contents``.
|
|
148
|
+
source: ``"catalog_contents"`` (one RPC) or ``"per_schema"``
|
|
149
|
+
(``catalog_schemas`` plus the per-schema ``catalog_schema_contents_*``
|
|
150
|
+
RPCs — the worker did not advertise ``catalog_contents``, the load
|
|
151
|
+
ran inside a transaction, or ``catalog_contents`` failed).
|
|
152
|
+
not_modified: ``True`` when this is ``previous`` confirmed current by a
|
|
153
|
+
``not_modified`` answer.
|
|
154
|
+
fallback_reason: Why ``catalog_contents`` was advertised but not used
|
|
155
|
+
(its error message, or a protocol violation), else ``None``.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
schemas: list[SchemaContentsInfo]
|
|
159
|
+
catalog_version: int | None
|
|
160
|
+
etag: str | None
|
|
161
|
+
source: Literal["catalog_contents", "per_schema"]
|
|
162
|
+
not_modified: bool = False
|
|
163
|
+
fallback_reason: str | None = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ``SchemaInfo.estimated_object_count`` key for each per-schema kind; a count of
|
|
167
|
+
# exactly 0 is a hard "none of this kind" guarantee, so the RPC is skipped (the
|
|
168
|
+
# same rule the DuckDB extension and ``CatalogInterface.catalog_contents`` apply).
|
|
169
|
+
_PER_SCHEMA_KINDS: tuple[tuple[str, SchemaObjectType, str], ...] = (
|
|
170
|
+
("tables", SchemaObjectType.TABLE, "table"),
|
|
171
|
+
("views", SchemaObjectType.VIEW, "view"),
|
|
172
|
+
("scalar_functions", SchemaObjectType.SCALAR_FUNCTION, "scalar_function"),
|
|
173
|
+
("aggregate_functions", SchemaObjectType.AGGREGATE_FUNCTION, "aggregate_function"),
|
|
174
|
+
("table_functions", SchemaObjectType.TABLE_FUNCTION, "table_function"),
|
|
175
|
+
("scalar_macros", SchemaObjectType.SCALAR_MACRO, "macro"),
|
|
176
|
+
("table_macros", SchemaObjectType.TABLE_MACRO, "macro"),
|
|
177
|
+
("indexes", SchemaObjectType.INDEX, "index"),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
132
181
|
class CatalogClientError(Exception):
|
|
133
182
|
"""Error raised by catalog operations."""
|
|
134
183
|
|
|
@@ -492,13 +541,123 @@ class CatalogClientMixin:
|
|
|
492
541
|
one [`SchemaContentsInfo`][] per schema, parents first.
|
|
493
542
|
|
|
494
543
|
"""
|
|
544
|
+
# Decode inside the connection block so a malformed item surfaces as
|
|
545
|
+
# CatalogClientError (which load_catalog falls back on), not a bare
|
|
546
|
+
# decode error.
|
|
495
547
|
with self._catalog_connect() as proxy:
|
|
496
548
|
response = proxy.catalog_contents(attach_opaque_data=attach_opaque_data, if_none_match=if_none_match)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
549
|
+
return CatalogContents(
|
|
550
|
+
catalog_version=response.catalog_version,
|
|
551
|
+
etag=response.etag,
|
|
552
|
+
not_modified=response.not_modified,
|
|
553
|
+
schemas=[_decode_schema_contents(entry) for entry in response.schemas],
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
def load_catalog(
|
|
557
|
+
self,
|
|
558
|
+
*,
|
|
559
|
+
attach: CatalogAttachResult,
|
|
560
|
+
previous: CatalogSnapshot | None = None,
|
|
561
|
+
transaction_opaque_data: TransactionOpaqueData | None = None,
|
|
562
|
+
) -> CatalogSnapshot:
|
|
563
|
+
"""Load every schema and all of its contents, in as few RPCs as the worker allows.
|
|
564
|
+
|
|
565
|
+
This is the client's whole-catalog enumeration. When the attach result
|
|
566
|
+
advertises ``supports_catalog_contents`` it is one ``catalog_contents``
|
|
567
|
+
call; otherwise — or inside a transaction, or when that call fails — it
|
|
568
|
+
is ``catalog_schemas`` plus the per-schema ``catalog_schema_contents_*``
|
|
569
|
+
RPCs (skipping a kind whose ``estimated_object_count`` is exactly 0).
|
|
570
|
+
``catalog_contents`` is never sent to a worker that did not advertise it.
|
|
571
|
+
|
|
572
|
+
Revalidation: pass the snapshot from a previous load as ``previous``.
|
|
573
|
+
If it came from ``catalog_contents`` with an etag, the call sends
|
|
574
|
+
``if_none_match``; a ``not_modified`` answer returns ``previous``'s
|
|
575
|
+
content (with ``not_modified=True`` and the current version), and a full
|
|
576
|
+
answer replaces it. A snapshot older than ``previous`` (a lagging
|
|
577
|
+
replica) is retried once, then the per-schema RPCs are used.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
attach: The result of [`catalog_attach`][vgi.client.CatalogClientMixin.catalog_attach]
|
|
581
|
+
for this catalog.
|
|
582
|
+
previous: A snapshot already held, to revalidate instead of reload.
|
|
583
|
+
transaction_opaque_data: Load inside this transaction. ``catalog_contents``
|
|
584
|
+
returns only the committed catalog, so a transactional load always
|
|
585
|
+
uses the (transaction-aware) per-schema RPCs.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
A [`CatalogSnapshot`][]. ``source`` says which path served it and
|
|
589
|
+
``fallback_reason`` why an advertised ``catalog_contents`` was not used.
|
|
590
|
+
"""
|
|
591
|
+
attach_opaque_data = attach.attach_opaque_data
|
|
592
|
+
if not attach.supports_catalog_contents or transaction_opaque_data is not None:
|
|
593
|
+
return self._load_catalog_per_schema(attach_opaque_data, transaction_opaque_data, None)
|
|
594
|
+
|
|
595
|
+
if_none_match = previous.etag if previous is not None and previous.source == "catalog_contents" else None
|
|
596
|
+
known_version = previous.catalog_version if previous is not None else None
|
|
597
|
+
reason: str
|
|
598
|
+
for _attempt in range(2):
|
|
599
|
+
try:
|
|
600
|
+
contents = self.contents(attach_opaque_data=attach_opaque_data, if_none_match=if_none_match)
|
|
601
|
+
except CatalogClientError as e:
|
|
602
|
+
reason = str(e)
|
|
603
|
+
break
|
|
604
|
+
if contents.not_modified:
|
|
605
|
+
if previous is None or if_none_match is None or contents.schemas:
|
|
606
|
+
reason = "catalog_contents answered not_modified to a request it could not match"
|
|
607
|
+
break
|
|
608
|
+
return replace(
|
|
609
|
+
previous,
|
|
610
|
+
catalog_version=contents.catalog_version,
|
|
611
|
+
etag=contents.etag if contents.etag is not None else previous.etag,
|
|
612
|
+
not_modified=True,
|
|
613
|
+
)
|
|
614
|
+
# Versions are monotonic within a session; 0 means "unknown".
|
|
615
|
+
if known_version and contents.catalog_version and contents.catalog_version < known_version:
|
|
616
|
+
reason = (
|
|
617
|
+
f"catalog_contents returned version {contents.catalog_version}, "
|
|
618
|
+
f"older than the known version {known_version}"
|
|
619
|
+
)
|
|
620
|
+
continue
|
|
621
|
+
return CatalogSnapshot(
|
|
622
|
+
schemas=contents.schemas,
|
|
623
|
+
catalog_version=contents.catalog_version,
|
|
624
|
+
etag=contents.etag,
|
|
625
|
+
source="catalog_contents",
|
|
626
|
+
)
|
|
627
|
+
return self._load_catalog_per_schema(attach_opaque_data, None, reason)
|
|
628
|
+
|
|
629
|
+
def _load_catalog_per_schema(
|
|
630
|
+
self,
|
|
631
|
+
attach_opaque_data: AttachOpaqueData,
|
|
632
|
+
transaction_opaque_data: TransactionOpaqueData | None,
|
|
633
|
+
fallback_reason: str | None,
|
|
634
|
+
) -> CatalogSnapshot:
|
|
635
|
+
"""Assemble a snapshot from ``catalog_schemas`` and the per-schema RPCs."""
|
|
636
|
+
result: list[SchemaContentsInfo] = []
|
|
637
|
+
for schema in self.schemas(
|
|
638
|
+
attach_opaque_data=attach_opaque_data, transaction_opaque_data=transaction_opaque_data
|
|
639
|
+
):
|
|
640
|
+
counts = schema.estimated_object_count or {}
|
|
641
|
+
kinds: dict[str, list[Any]] = {}
|
|
642
|
+
for attr, object_type, count_key in _PER_SCHEMA_KINDS:
|
|
643
|
+
if counts.get(count_key, 1) == 0:
|
|
644
|
+
kinds[attr] = []
|
|
645
|
+
continue
|
|
646
|
+
kinds[attr] = list(
|
|
647
|
+
self.schema_contents(
|
|
648
|
+
attach_opaque_data=attach_opaque_data,
|
|
649
|
+
transaction_opaque_data=transaction_opaque_data,
|
|
650
|
+
path=list(schema.path),
|
|
651
|
+
type=object_type,
|
|
652
|
+
)
|
|
653
|
+
)
|
|
654
|
+
result.append(SchemaContentsInfo(schema=schema, **kinds))
|
|
655
|
+
return CatalogSnapshot(
|
|
656
|
+
schemas=result,
|
|
657
|
+
catalog_version=None,
|
|
658
|
+
etag=None,
|
|
659
|
+
source="per_schema",
|
|
660
|
+
fallback_reason=fallback_reason,
|
|
502
661
|
)
|
|
503
662
|
|
|
504
663
|
def schema_get(
|
vgi/client/cli_catalog.py
CHANGED
|
@@ -11,6 +11,7 @@ catalog
|
|
|
11
11
|
├── create <name> # Create a new catalog
|
|
12
12
|
├── drop <name> # Drop a catalog
|
|
13
13
|
├── version # Get catalog version (--attach-opaque-data or --catalog)
|
|
14
|
+
├── contents # Every schema and its contents (catalog_contents when advertised)
|
|
14
15
|
├── schema # Schema operations
|
|
15
16
|
│ ├── list/get/create/drop/contents
|
|
16
17
|
├── table # Table operations
|
|
@@ -163,6 +164,52 @@ def catalog_version(
|
|
|
163
164
|
output_json({"version": version, "attach_opaque_data": bytes_to_hex(resolved_attach_opaque_data)})
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
@catalog.command("contents")
|
|
168
|
+
@click.option("--catalog", "catalog_name", required=True, help="Catalog name to attach")
|
|
169
|
+
@click.option("--attach-options", default="{}", help="Attach options as JSON")
|
|
170
|
+
@click.option("--worker", "-w", required=True, help="VGI worker command")
|
|
171
|
+
def catalog_contents(catalog_name: str, attach_options: str, worker: str) -> None:
|
|
172
|
+
"""List every schema and the names of everything in it.
|
|
173
|
+
|
|
174
|
+
Uses one ``catalog_contents`` call when the catalog advertises it, else
|
|
175
|
+
``catalog_schemas`` plus the per-schema RPCs (see ``Client.load_catalog``).
|
|
176
|
+
"""
|
|
177
|
+
client = Client(worker)
|
|
178
|
+
attach = client.catalog_attach(
|
|
179
|
+
name=catalog_name,
|
|
180
|
+
options=parse_json_option(attach_options, "--attach-options"),
|
|
181
|
+
data_version_spec=None,
|
|
182
|
+
implementation_version=None,
|
|
183
|
+
)
|
|
184
|
+
snapshot = client.load_catalog(attach=attach)
|
|
185
|
+
kinds = (
|
|
186
|
+
"tables",
|
|
187
|
+
"views",
|
|
188
|
+
"scalar_functions",
|
|
189
|
+
"aggregate_functions",
|
|
190
|
+
"table_functions",
|
|
191
|
+
"scalar_macros",
|
|
192
|
+
"table_macros",
|
|
193
|
+
"indexes",
|
|
194
|
+
)
|
|
195
|
+
output_json(
|
|
196
|
+
{
|
|
197
|
+
"source": snapshot.source,
|
|
198
|
+
"catalog_version": snapshot.catalog_version,
|
|
199
|
+
"etag": snapshot.etag,
|
|
200
|
+
"fallback_reason": snapshot.fallback_reason,
|
|
201
|
+
"schemas": [
|
|
202
|
+
{
|
|
203
|
+
"path": list(entry.schema.path),
|
|
204
|
+
"comment": entry.schema.comment,
|
|
205
|
+
**{kind: [item.name for item in getattr(entry, kind)] for kind in kinds},
|
|
206
|
+
}
|
|
207
|
+
for entry in snapshot.schemas
|
|
208
|
+
],
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
166
213
|
# Add nested subcommand groups
|
|
167
214
|
catalog.add_command(schema)
|
|
168
215
|
catalog.add_command(table)
|
vgi/client/cli_utils.py
CHANGED
|
@@ -389,6 +389,7 @@ def catalog_attach_result_to_dict(result: CatalogAttachResult) -> dict[str, Any]
|
|
|
389
389
|
"catalog_version": result.catalog_version,
|
|
390
390
|
"attach_opaque_data_required": result.attach_opaque_data_required,
|
|
391
391
|
"default_schema": result.default_schema,
|
|
392
|
+
"supports_catalog_contents": result.supports_catalog_contents,
|
|
392
393
|
"settings": [bytes_to_hex(s) for s in result.settings],
|
|
393
394
|
"resolved_data_version": result.resolved_data_version,
|
|
394
395
|
"resolved_implementation_version": result.resolved_implementation_version,
|
vgi/client/client.py
CHANGED
|
@@ -54,6 +54,7 @@ Key methods
|
|
|
54
54
|
client.catalog_attach(...) — open a catalog
|
|
55
55
|
client.schemas(...) — list schemas
|
|
56
56
|
client.schema_contents(...) — list tables/views/functions/macros
|
|
57
|
+
client.load_catalog(...) — whole catalog (catalog_contents when advertised)
|
|
57
58
|
client.scalar_function(...) — invoke a scalar
|
|
58
59
|
client.table_function(...) — invoke a table function
|
|
59
60
|
client.table_function_plan(...) — plan a table function into named, redeemable splits
|
vgi/rpc_server.py
CHANGED
|
@@ -34,13 +34,14 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
|
34
34
|
|
|
35
35
|
if TYPE_CHECKING:
|
|
36
36
|
from vgi_rpc.external import ExternalLocationConfig
|
|
37
|
+
from vgi_rpc.grants import GrantKeys
|
|
37
38
|
from vgi_rpc.rpc import RpcServer
|
|
38
39
|
from vgi_rpc.rpc._token_identity import IdentityImpl
|
|
39
40
|
|
|
40
41
|
from vgi.meta_worker import MetaWorker
|
|
41
42
|
from vgi.worker import Worker
|
|
42
43
|
|
|
43
|
-
__all__ = ["Transport", "build_rpc_server"]
|
|
44
|
+
__all__ = ["Transport", "build_rpc_server", "resolve_grant_keys"]
|
|
44
45
|
|
|
45
46
|
Transport = Literal["pipe", "unix", "tcp", "iroh", "http"]
|
|
46
47
|
"""The transport a server is being built for.
|
|
@@ -65,6 +66,7 @@ def build_rpc_server(
|
|
|
65
66
|
transport: Transport,
|
|
66
67
|
describe: bool = True,
|
|
67
68
|
introspect_principals: Iterable[str] | None = None,
|
|
69
|
+
grant_keys: GrantKeys | None = None,
|
|
68
70
|
external_location: ExternalLocationConfig | None = None,
|
|
69
71
|
otel_config: Any = None,
|
|
70
72
|
) -> RpcServer:
|
|
@@ -83,6 +85,11 @@ def build_rpc_server(
|
|
|
83
85
|
introspect_principals: HTTP only. Principals permitted to call
|
|
84
86
|
``introspect_token``; ``None`` reads ``VGI_INTROSPECT_PRINCIPALS``.
|
|
85
87
|
Only consulted when the worker overrides ``resolve_token``.
|
|
88
|
+
grant_keys: HTTP only. Sealed-grant configuration (see
|
|
89
|
+
[`resolve_grant_keys`][vgi.rpc_server.resolve_grant_keys]). When
|
|
90
|
+
given, Identity is hosted even without hooks: the framework mints
|
|
91
|
+
sealed grants unless the worker overrides ``mint_grant``, and the
|
|
92
|
+
HTTP server accepts them back as bearer credentials.
|
|
86
93
|
external_location: Optional external-storage configuration for large
|
|
87
94
|
batches (HTTP).
|
|
88
95
|
otel_config: Optional ``OtelConfig``. When given, the server is
|
|
@@ -116,13 +123,18 @@ def build_rpc_server(
|
|
|
116
123
|
if transport in _IDENTITY_TRANSPORTS:
|
|
117
124
|
# A Worker's hooks are classmethods; a MetaWorker answers for the one
|
|
118
125
|
# child that implements each hook.
|
|
119
|
-
identity = _build_identity(
|
|
126
|
+
identity = _build_identity(
|
|
127
|
+
type(worker) if isinstance(worker, Worker) else worker, introspect_principals, grant_keys
|
|
128
|
+
)
|
|
120
129
|
|
|
121
130
|
server = RpcServer(
|
|
122
131
|
primary,
|
|
123
132
|
worker,
|
|
124
133
|
extra_protocols=extra,
|
|
125
134
|
identity=identity,
|
|
135
|
+
# Explicit, never vgi-rpc's "env" default: grants are an HTTP-only
|
|
136
|
+
# identity feature here, and the caller resolved them already.
|
|
137
|
+
grant_keys=grant_keys if transport in _IDENTITY_TRANSPORTS else None,
|
|
126
138
|
external_location=external_location,
|
|
127
139
|
enable_describe=describe,
|
|
128
140
|
server_version=_get_vgi_version(),
|
|
@@ -191,6 +203,7 @@ def _validated_hosted_protocols(worker: Worker | MetaWorker, primary: type) -> t
|
|
|
191
203
|
def _build_identity(
|
|
192
204
|
worker_cls: type[Worker] | MetaWorker,
|
|
193
205
|
introspect_principals: Iterable[str] | None,
|
|
206
|
+
grant_keys: GrantKeys | None = None,
|
|
194
207
|
) -> IdentityImpl | None:
|
|
195
208
|
"""Build the ``vgi_rpc.Identity.v1`` implementation, or ``None``.
|
|
196
209
|
|
|
@@ -217,6 +230,9 @@ def _build_identity(
|
|
|
217
230
|
the one child class that overrides each.
|
|
218
231
|
introspect_principals: Principals permitted to introspect, or ``None``
|
|
219
232
|
to read the environment.
|
|
233
|
+
grant_keys: Sealed-grant configuration. When given, Identity is built
|
|
234
|
+
even with no hook, and mints sealed grants unless the worker
|
|
235
|
+
overrides ``mint_grant``.
|
|
220
236
|
|
|
221
237
|
Returns:
|
|
222
238
|
The implementation, or ``None`` when this worker does not resolve
|
|
@@ -225,7 +241,7 @@ def _build_identity(
|
|
|
225
241
|
"""
|
|
226
242
|
resolver = worker_cls._introspect_resolver()
|
|
227
243
|
minter = worker_cls._grant_minter()
|
|
228
|
-
if resolver is None and minter is None:
|
|
244
|
+
if resolver is None and minter is None and grant_keys is None:
|
|
229
245
|
return None
|
|
230
246
|
|
|
231
247
|
# Not re-exported by ``vgi_rpc.rpc``, so the private module is the only
|
|
@@ -239,6 +255,9 @@ def _build_identity(
|
|
|
239
255
|
# validates it only when a resolver is supplied — a worker that mints
|
|
240
256
|
# grants but resolves nothing is not an oracle and needs no allowlist.
|
|
241
257
|
introspect_principals=(_resolve_introspect_principals(introspect_principals) if resolver is not None else None),
|
|
258
|
+
# With keys and no mint_grant override, IdentityImpl mints sealed
|
|
259
|
+
# grants itself; the HTTP server then accepts them as bearers.
|
|
260
|
+
grant_keys=grant_keys,
|
|
242
261
|
)
|
|
243
262
|
|
|
244
263
|
|
|
@@ -285,3 +304,37 @@ def _resolve_introspect_principals(explicit: Iterable[str] | None) -> list[str]:
|
|
|
285
304
|
)
|
|
286
305
|
sys.exit(1)
|
|
287
306
|
return principals
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def resolve_grant_keys(explicit: Sequence[str] | None = None) -> GrantKeys | None:
|
|
310
|
+
"""Resolve the sealed-grant configuration, or exit with an actionable message.
|
|
311
|
+
|
|
312
|
+
Grants are opt-in: with no key configured this returns ``None`` and
|
|
313
|
+
nothing changes. Keys come from *explicit* (``--grant-key``, repeatable)
|
|
314
|
+
or else ``VGI_RPC_GRANT_KEYS`` (comma-separated). Either way
|
|
315
|
+
``VGI_RPC_GRANT_AUDIENCE`` and ``VGI_RPC_GRANT_MAX_TTL_SECONDS`` (default
|
|
316
|
+
7 days) apply. Each key is standard base64 of exactly 32 bytes; the first
|
|
317
|
+
mints and every key verifies, which is how keys rotate.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
explicit: Keys given on the command line, or ``None`` to read the
|
|
321
|
+
environment.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
The configuration, or ``None`` when no key is configured.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
SystemExit: A key or lifetime is malformed. A worker refuses to start
|
|
328
|
+
rather than run with a key it misread.
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
from vgi_rpc.grants import GRANT_KEYS_ENV, GrantKeys
|
|
332
|
+
|
|
333
|
+
env: dict[str, str] = dict(os.environ)
|
|
334
|
+
if explicit:
|
|
335
|
+
env[GRANT_KEYS_ENV] = ",".join(explicit)
|
|
336
|
+
try:
|
|
337
|
+
return GrantKeys.from_env(env)
|
|
338
|
+
except ValueError as exc:
|
|
339
|
+
sys.stderr.write(f"Error: invalid sealed-grant configuration ({GRANT_KEYS_ENV} / --grant-key): {exc}\n")
|
|
340
|
+
sys.exit(1)
|
vgi/serve.py
CHANGED
|
@@ -41,6 +41,7 @@ from vgi.profiling import maybe_start_profile
|
|
|
41
41
|
|
|
42
42
|
if TYPE_CHECKING:
|
|
43
43
|
import falcon
|
|
44
|
+
from vgi_rpc.grants import GrantKeys
|
|
44
45
|
from vgi_rpc.otel import OtelConfig
|
|
45
46
|
from vgi_rpc.rpc import AuthContext, PeerAuthenticationPolicy, PeerIdentityProvider
|
|
46
47
|
|
|
@@ -216,6 +217,7 @@ def create_app(
|
|
|
216
217
|
max_stream_response_bytes: int | None = None,
|
|
217
218
|
max_externalized_response_bytes: int | None = None,
|
|
218
219
|
introspect_principals: Iterable[str] | None = None,
|
|
220
|
+
grant_keys: GrantKeys | None = None,
|
|
219
221
|
iroh_bridge_issuer: str | None = None,
|
|
220
222
|
iroh_trusted_proxy_addresses: Iterable[str] = (),
|
|
221
223
|
iroh_authenticate: bool = True,
|
|
@@ -277,6 +279,16 @@ def create_app(
|
|
|
277
279
|
``vgi_rpc.Identity.v1``'s ``introspect_token``. Only consulted
|
|
278
280
|
when the worker class overrides ``resolve_token``. ``None`` reads
|
|
279
281
|
``VGI_INTROSPECT_PRINCIPALS``.
|
|
282
|
+
grant_keys: Sealed-grant configuration. ``None`` reads
|
|
283
|
+
``VGI_RPC_GRANT_KEYS`` (see
|
|
284
|
+
[`resolve_grant_keys`][vgi.rpc_server.resolve_grant_keys]); no key
|
|
285
|
+
configured means grants are off. With keys, ``issue_grant`` mints
|
|
286
|
+
sealed grants (unless the worker overrides ``mint_grant``) and
|
|
287
|
+
requests may present them as ``Authorization: Bearer <grant>``.
|
|
288
|
+
Independently, a worker that overrides ``resolve_token`` has it
|
|
289
|
+
consulted for bearer credentials. Both come after *authenticate*
|
|
290
|
+
in the chain, so the deployment's own authenticator answers first
|
|
291
|
+
and must raise ``ValueError`` for a credential it does not know.
|
|
280
292
|
iroh_bridge_issuer: Operator-controlled identity namespace for an
|
|
281
293
|
``iroh-http/2`` bridge. ``None`` disables Iroh header trust.
|
|
282
294
|
iroh_trusted_proxy_addresses: Exact immediate bridge addresses.
|
|
@@ -297,7 +309,7 @@ def create_app(
|
|
|
297
309
|
sys.exit(1)
|
|
298
310
|
|
|
299
311
|
from vgi.otel import VgiTracer
|
|
300
|
-
from vgi.rpc_server import build_rpc_server
|
|
312
|
+
from vgi.rpc_server import build_rpc_server, resolve_grant_keys
|
|
301
313
|
|
|
302
314
|
# Resolve the signing key once, here, so the worker (which seals catalog
|
|
303
315
|
# opaque-data envelopes) and the HTTP state-token machinery share the same
|
|
@@ -319,7 +331,9 @@ def create_app(
|
|
|
319
331
|
transport="http",
|
|
320
332
|
describe=describe,
|
|
321
333
|
introspect_principals=introspect_principals,
|
|
334
|
+
grant_keys=grant_keys if grant_keys is not None else resolve_grant_keys(),
|
|
322
335
|
)
|
|
336
|
+
authenticate, identity_bearer = _compose_identity_bearer(server, authenticate)
|
|
323
337
|
|
|
324
338
|
effective_peer_identity_providers = tuple(peer_identity_providers)
|
|
325
339
|
effective_peer_authentication_policy = peer_authentication_policy
|
|
@@ -356,6 +370,7 @@ def create_app(
|
|
|
356
370
|
max_stream_response_bytes=max_stream_response_bytes,
|
|
357
371
|
max_externalized_response_bytes=max_externalized_response_bytes,
|
|
358
372
|
enable_landing_page=False,
|
|
373
|
+
identity_bearer=identity_bearer,
|
|
359
374
|
)
|
|
360
375
|
|
|
361
376
|
# Frontend: either redirect to external CDN or serve pre-rendered worker page
|
|
@@ -490,6 +505,16 @@ def main() -> None:
|
|
|
490
505
|
"that case, with no permissive default. Env: VGI_INTROSPECT_PRINCIPALS."
|
|
491
506
|
),
|
|
492
507
|
),
|
|
508
|
+
grant_key: list[str] | None = typer.Option( # noqa: B008
|
|
509
|
+
None,
|
|
510
|
+
"--grant-key",
|
|
511
|
+
help=(
|
|
512
|
+
"Sealed-grant key: standard base64 of 32 bytes; repeat to rotate (the first "
|
|
513
|
+
"mints, all verify). Enables issue_grant without a mint_grant override and "
|
|
514
|
+
"accepts the grants as HTTP bearer credentials. Env: VGI_RPC_GRANT_KEYS "
|
|
515
|
+
"(comma-separated), VGI_RPC_GRANT_AUDIENCE, VGI_RPC_GRANT_MAX_TTL_SECONDS."
|
|
516
|
+
),
|
|
517
|
+
),
|
|
493
518
|
access_log_sample: float | None = typer.Option( # noqa: B008
|
|
494
519
|
None,
|
|
495
520
|
"--access-log-sample",
|
|
@@ -573,6 +598,7 @@ def main() -> None:
|
|
|
573
598
|
max_stream_response_bytes=max_stream_response_bytes,
|
|
574
599
|
max_externalized_response_bytes=max_externalized_response_bytes,
|
|
575
600
|
introspect_principals=introspect_principals,
|
|
601
|
+
grant_keys=grant_key,
|
|
576
602
|
server=server,
|
|
577
603
|
worker_ref=worker_ref,
|
|
578
604
|
http_workers=http_workers,
|
|
@@ -770,7 +796,44 @@ def _resolve_authenticate() -> Callable[..., Any] | None:
|
|
|
770
796
|
# this worker", with user identity handled upstream.
|
|
771
797
|
from vgi_rpc.http import require_all
|
|
772
798
|
|
|
773
|
-
|
|
799
|
+
gated = require_all(gate, inner)
|
|
800
|
+
# Kept so create_app can put the identity bearers (sealed grants,
|
|
801
|
+
# resolve_token) inside the gate rather than beside it.
|
|
802
|
+
setattr(gated, _PROXY_GATE_PARTS_ATTR, (gate, inner))
|
|
803
|
+
return gated
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
#: Attribute on a proxy-gated authenticator holding its ``(gate, inner)``.
|
|
807
|
+
_PROXY_GATE_PARTS_ATTR = "_vgi_proxy_gate_parts"
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _compose_identity_bearer(
|
|
811
|
+
server: Any,
|
|
812
|
+
authenticate: Callable[..., Any] | None,
|
|
813
|
+
) -> tuple[Callable[..., Any] | None, bool]:
|
|
814
|
+
"""Return ``(authenticate, identity_bearer)`` for ``make_wsgi_app``.
|
|
815
|
+
|
|
816
|
+
vgi-rpc appends the identity bearers (sealed grants, then the worker's
|
|
817
|
+
``resolve_token``) after *authenticate* itself. It refuses when
|
|
818
|
+
*authenticate* depends on proxy-injected evidence, because an OR beside a
|
|
819
|
+
proxy-proof gate would let a grant bypass it. VGI's own gate
|
|
820
|
+
(``VGI_PROXY_PROOF_MODE``) is built by :func:`_resolve_authenticate`, so
|
|
821
|
+
here the bearers go *inside* it: ``require_all(gate, inner, then grants,
|
|
822
|
+
then resolve_token)``. Any other authenticator is left to vgi-rpc.
|
|
823
|
+
"""
|
|
824
|
+
identity = getattr(server, "identity", None)
|
|
825
|
+
parts = getattr(authenticate, _PROXY_GATE_PARTS_ATTR, None)
|
|
826
|
+
if identity is None or parts is None:
|
|
827
|
+
return authenticate, True
|
|
828
|
+
if identity.grant_keys is None and identity.resolve_token_hook is None:
|
|
829
|
+
return authenticate, True
|
|
830
|
+
from vgi_rpc.http import compose_identity_authenticate, require_all
|
|
831
|
+
|
|
832
|
+
gate, inner = parts
|
|
833
|
+
composed = compose_identity_authenticate(
|
|
834
|
+
inner, grant_keys=identity.grant_keys, resolve_token=identity.resolve_token_hook
|
|
835
|
+
)
|
|
836
|
+
return require_all(gate, composed), False
|
|
774
837
|
|
|
775
838
|
|
|
776
839
|
def _resolve_proxy_proof_gate() -> Any | None:
|
|
@@ -1352,6 +1415,7 @@ def _serve_http(
|
|
|
1352
1415
|
max_stream_response_bytes: int | None = None,
|
|
1353
1416
|
max_externalized_response_bytes: int | None = None,
|
|
1354
1417
|
introspect_principals: str | None = None,
|
|
1418
|
+
grant_keys: Sequence[str] | None = None,
|
|
1355
1419
|
server: str = "waitress",
|
|
1356
1420
|
worker_ref: str | None = None,
|
|
1357
1421
|
http_workers: int | None = None,
|
|
@@ -1369,6 +1433,13 @@ def _serve_http(
|
|
|
1369
1433
|
# export_serve_config for why the environment is the channel.
|
|
1370
1434
|
if introspect_principals is not None:
|
|
1371
1435
|
os.environ["VGI_INTROSPECT_PRINCIPALS"] = introspect_principals
|
|
1436
|
+
if grant_keys:
|
|
1437
|
+
from vgi_rpc.grants import GRANT_KEYS_ENV
|
|
1438
|
+
|
|
1439
|
+
from vgi.rpc_server import resolve_grant_keys
|
|
1440
|
+
|
|
1441
|
+
resolve_grant_keys(grant_keys) # a malformed key stops startup here
|
|
1442
|
+
os.environ[GRANT_KEYS_ENV] = ",".join(grant_keys)
|
|
1372
1443
|
|
|
1373
1444
|
if server == "granian":
|
|
1374
1445
|
_serve_http_granian(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vgi-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.42.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.12.0
|
|
|
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.49.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'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
vgi/__init__.py,sha256=8uLMNQcNzWjp8VzYtj2u98_Wp8WpB8c_dKlMv67TH0g,5384
|
|
2
|
-
vgi/_duckdb.py,sha256=
|
|
2
|
+
vgi/_duckdb.py,sha256=Tu8Yb4vABRv4yOMo7qul8LxG_wH5ZqZuOCSSjyiMgbY,2198
|
|
3
3
|
vgi/_storage_profile.py,sha256=VkTsXojuE0tHEzurmteQSAiL1vI3CZSgYkL6D_h8GvE,5061
|
|
4
4
|
vgi/aggregate_function.py,sha256=NYxudbvQ9ZLkP2w5hRdd4YgsGUZ_0cVfqqXnlmJPfls,27389
|
|
5
5
|
vgi/argument_spec.py,sha256=SADH97VlFu3asI8CWZ_I7XTuSxwqFciFgdupdAf-tkY,34231
|
|
@@ -25,13 +25,13 @@ vgi/profiling.py,sha256=rK7vij3oHhXC3JHI2BsMySsXh7_hrXi0aXpCW8iFSIc,3671
|
|
|
25
25
|
vgi/protocol.py,sha256=J9vaxstGvxe4Vmlf_Aij9HkabGbdFx_nEsQI_m-V7ak,175438
|
|
26
26
|
vgi/protocol_version.txt,sha256=yJuHwmLp1f_SDnOqx0oXcq3Mn1ohaCzDOtz1CG5f71w,6
|
|
27
27
|
vgi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
vgi/rpc_server.py,sha256=
|
|
28
|
+
vgi/rpc_server.py,sha256=Qdqi6rHkX78SYXUZqG5D-a9NR0ZtGoxW22he0J9xS-0,14914
|
|
29
29
|
vgi/scalar_function.py,sha256=YBy5s4eEBYTMjACGPE-8-82BObyJoM9qt9cnm24b_RQ,53485
|
|
30
30
|
vgi/schema_path.py,sha256=GVlkZlCUj7GbVhgiocSMC6Ff5iTJhKqiYE62hMIEKiI,2403
|
|
31
31
|
vgi/schema_utils.py,sha256=zQCliUgIpO2u77Mhtgg3OFvOEDNgJopu6kj2fw8YP1I,7945
|
|
32
32
|
vgi/secret_protocol.py,sha256=iCDl7nF7Nb4cJV4vpfbldunTDqDJWzCzS7uCmOACf7U,7151
|
|
33
33
|
vgi/secret_service.py,sha256=r7qCzfnOMirf9wSCUe-mgFe3z6v9_cDRzixuSAZG_bs,8973
|
|
34
|
-
vgi/serve.py,sha256=
|
|
34
|
+
vgi/serve.py,sha256=V56NcO9VzWaMlkUCXumLBsfMEZ8oZeckT_EJ9sumsQc,63442
|
|
35
35
|
vgi/split_token.py,sha256=tutcksj-hgWrLY7UW3sSqkyoaD-cMqNTOeff40MGl_g,10850
|
|
36
36
|
vgi/table_buffering_function.py,sha256=SI6yLAcs8WBHqS6TeaZ92s-Q6GXn0ReROyQWDujw2iA,19554
|
|
37
37
|
vgi/table_filter_pushdown.py,sha256=4Qo4OAOLjTYQ5pinNTOjLCDnb96OHqpcxJlbSJo3cJU,67144
|
|
@@ -45,13 +45,13 @@ vgi/_test_fixtures/bad_enum.py,sha256=b8l-afmXgGR6iqgIUiVeqHu0C0F8Of8ahEmdQGtqd1
|
|
|
45
45
|
vgi/_test_fixtures/bad_protocol.py,sha256=fPU7rpXEgBxRtPHfuCa0O2AHNRRbZDllLfydHsAKPEg,2992
|
|
46
46
|
vgi/_test_fixtures/cancellable.py,sha256=hetTRLyT4kkazPNcxAdfzj0pYzEmegMksamJbR0w3Ho,12151
|
|
47
47
|
vgi/_test_fixtures/catalog.py,sha256=zjBKBgg4TKoDQ9SYIFj7sx6bOL3_aXHMJHSzTgk1fdA,28526
|
|
48
|
-
vgi/_test_fixtures/catalog_contents.py,sha256=
|
|
48
|
+
vgi/_test_fixtures/catalog_contents.py,sha256=C9vxHil2kmsqoELiTq2jZINBOwONJ2KIIYrreNMEN6c,11934
|
|
49
49
|
vgi/_test_fixtures/companion.py,sha256=Ya_rg2J4tXOYC7zBFOjTTh-4AYuRhJN5BT4tVZULQ-Q,6037
|
|
50
50
|
vgi/_test_fixtures/copy_from.py,sha256=YAWametIhrKE_K_kRwx37QpKMY0yBWAordE5-cNVn4M,6134
|
|
51
51
|
vgi/_test_fixtures/copy_to.py,sha256=fFHxPYVd0wfAfMEvpeBRijqUasBeoAGREY3kHmm_oK8,9667
|
|
52
52
|
vgi/_test_fixtures/global_functions.py,sha256=G1h0w5o6bHMULDFLZuNjrZVrBbO6Cre6kTqTnQwTFmE,11251
|
|
53
|
-
vgi/_test_fixtures/http_server.py,sha256=
|
|
54
|
-
vgi/_test_fixtures/identity_fixture.py,sha256=
|
|
53
|
+
vgi/_test_fixtures/http_server.py,sha256=hS6yKEE6I0UzwfIk_q7ORKee4jn4vD2xSCYItj8VkWE,20552
|
|
54
|
+
vgi/_test_fixtures/identity_fixture.py,sha256=wNTON3wpOGwH7HS9oCPb4Iav7GMYW0NlhtlZF1TL7aY,3318
|
|
55
55
|
vgi/_test_fixtures/nest_tensor.py,sha256=UjjW9UwipYG3ehQpZOm4hdzsafMyvVc0ZKJCrqiZTPU,24471
|
|
56
56
|
vgi/_test_fixtures/orchard_catalog.py,sha256=8Uv-mKTIgl-OYU9JLgG23owlHGmgniyF2chPoKXGtuI,1643
|
|
57
57
|
vgi/_test_fixtures/secret_cache.py,sha256=su4a90I-YHTfsmVPoJGyycTkEzT7GItreYZEfbXMp2o,9552
|
|
@@ -133,17 +133,17 @@ vgi/catalog/duckdb_statistics.py,sha256=Bzw4UGO-zdKrontaCcRV2palO8G3XLCWh-AhZozv
|
|
|
133
133
|
vgi/catalog/secret_type.py,sha256=1DoEMyQun9K5z53817rUuhG1YpHSEAsazEmubkRLVL0,3745
|
|
134
134
|
vgi/catalog/setting.py,sha256=06QfgaAR-0BKflIJ0du6PGqA5BPMdrkwr6u7f4nddII,1846
|
|
135
135
|
vgi/catalog/storage.py,sha256=DhngzywbhQUfLF7NjFyiPp4Sw2ovOhcUXAuW3rkBQD8,11935
|
|
136
|
-
vgi/client/__init__.py,sha256=
|
|
136
|
+
vgi/client/__init__.py,sha256=QOZGVkJWOfB8r4jjiwfU2q8Q97eWkLTnT6BK1jPZ2RE,2547
|
|
137
137
|
vgi/client/aggregate.py,sha256=EAaI-4d1mnNjGORZvLf3wfVuu8RXBTFGVbMek0DeGDg,38482
|
|
138
|
-
vgi/client/catalog_mixin.py,sha256=
|
|
138
|
+
vgi/client/catalog_mixin.py,sha256=CMUl4AUDCFDerRLc6mieZau1JCx9CIAbG_hIRHsZRJs,69620
|
|
139
139
|
vgi/client/cli.py,sha256=nahuRCjday0z74Gk9zxOxIOJX9n6fHrP7-CGwJoyTzU,22754
|
|
140
|
-
vgi/client/cli_catalog.py,sha256=
|
|
140
|
+
vgi/client/cli_catalog.py,sha256=RVhSJLFKSBfVwLfEb_GLT8vnuliOwU6FzlsVUF7Rpsw,7308
|
|
141
141
|
vgi/client/cli_schema.py,sha256=9G2vZiaAyqINItcDCoARdM04qNlDyJ3IXw3SuA6jgU8,7818
|
|
142
142
|
vgi/client/cli_table.py,sha256=H1xrKZF5yYmSe55YSrLWXGIXPmuGuzQcncEBvqL43zs,26498
|
|
143
143
|
vgi/client/cli_transaction.py,sha256=TDW0ZrBy8XM_eYSkYk5ewEsnozfzDJOlopd66Fm7OOE,3142
|
|
144
|
-
vgi/client/cli_utils.py,sha256=
|
|
144
|
+
vgi/client/cli_utils.py,sha256=ykh7XK-yzcW6WGED2_DhPLIVPSyVG3KUdllV5ztBicw,17458
|
|
145
145
|
vgi/client/cli_view.py,sha256=0qhxoS91-zFMjq8V_Fb0PNW-YDxERu8-zPpxSFEE-G8,8490
|
|
146
|
-
vgi/client/client.py,sha256=
|
|
146
|
+
vgi/client/client.py,sha256=GSkcVXQzI0VCmeUf7WY4kR0ZqFtYmeb6EkenarBvX5c,167399
|
|
147
147
|
vgi/client/errors.py,sha256=cSN65cyQMk3crwZCz1IdPErK3mtlEkTNpu-nuQUDOQE,1633
|
|
148
148
|
vgi/http/__init__.py,sha256=hlOwOVcoZqmEKVGk7ganOdr5ryRSpEhLBF9sRD7BkYc,608
|
|
149
149
|
vgi/http/demo_storage.py,sha256=830s-H61thozC3eEuqdnwCpYFfvoJRq9vjyuXa5OURo,8769
|
|
@@ -156,8 +156,8 @@ vgi/transactor/_duckdb_compat.py,sha256=sXVZ9JLKAQyGR1BjWczSwdQEavtr-TcZPoVZZnTr
|
|
|
156
156
|
vgi/transactor/client.py,sha256=7DTeMksogsw6ANjQjGOPpKYrV76rg4_kGjktMJf54jg,4486
|
|
157
157
|
vgi/transactor/protocol.py,sha256=v59IvrKnuvwOXvn_HEcBAbCzDVcx0akgKN0R1mChXg0,5034
|
|
158
158
|
vgi/transactor/server.py,sha256=nzsZQxJZdgappdYX8okrFIrjFtUbdM_Hhskl1fZ2nDY,32553
|
|
159
|
-
vgi_python-0.
|
|
160
|
-
vgi_python-0.
|
|
161
|
-
vgi_python-0.
|
|
162
|
-
vgi_python-0.
|
|
163
|
-
vgi_python-0.
|
|
159
|
+
vgi_python-0.42.0.dist-info/METADATA,sha256=1c3-T4txCgcbPeUyxS-fOuMogZtSRPvbjNDOeau4pSg,25953
|
|
160
|
+
vgi_python-0.42.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
161
|
+
vgi_python-0.42.0.dist-info/entry_points.txt,sha256=3Kz1vgodw3pOL_xjtSyDB55-ZRy-U2X-X_Bdr582x0Q,165
|
|
162
|
+
vgi_python-0.42.0.dist-info/licenses/LICENSE,sha256=pbJb4zZasP6n5ifEV81wFu017TarjydaYVmGbHcehtY,6103
|
|
163
|
+
vgi_python-0.42.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|