vgi-python 0.36.1__py3-none-any.whl → 0.37.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.
@@ -65,6 +65,7 @@ from vgi._test_fixtures.table.catalog_scans import (
65
65
  ProjectsScanFunction,
66
66
  )
67
67
  from vgi._test_fixtures.table.filters import (
68
+ BoolFilterEchoFunction,
68
69
  DictFilterEchoFunction,
69
70
  DynamicFilterEchoFunction,
70
71
  ExpressionFilterTestFunction,
@@ -216,6 +217,7 @@ __all__ = [
216
217
  "ConstantColumnsFunction",
217
218
  "CountryPartitionedSalesFunction",
218
219
  "DepartmentsScanFunction",
220
+ "BoolFilterEchoFunction",
219
221
  "DictFilterEchoFunction",
220
222
  "DisjointRangePartitionedFunction",
221
223
  "DoubleSequenceFunction",
@@ -191,6 +191,129 @@ class FilterEchoFunction(TableFunctionGenerator[FilterEchoFunctionArgs, FilterEc
191
191
  state.remaining -= size
192
192
 
193
193
 
194
+ # ============================================================================
195
+ # BoolFilterEchoFunction — a table function with a BOOLEAN column, so the
196
+ # bare-column predicate shapes (`WHERE flag`, `WHERE NOT flag`) can be driven
197
+ # end to end.
198
+ #
199
+ # filter_echo cannot cover these: its schema has no boolean column, so the
200
+ # predicate cannot even be written. The table-in-out `echo` path cannot either
201
+ # — DuckDB does not hand it a bare boolean column as a pushed predicate, so a
202
+ # test written there passes whether or not the worker can decode one. It takes
203
+ # a TABLE FUNCTION with a boolean column to see the shape at all, which is why
204
+ # a decoder that refused every one of them ("predicate root must resolve to
205
+ # BOOLEAN", vgi-python < 0.36.2) went unnoticed by a filter-pushdown suite of
206
+ # 348 files including one dedicated to booleans.
207
+ #
208
+ # `pushed_filters` echoes the SQL rendering, so this covers both halves: the
209
+ # decode (the rows are auto-applied and must be right) and `to_sql()` (a shape
210
+ # that parses but renders nothing would show up here as "(none)").
211
+ # ============================================================================
212
+
213
+
214
+ @dataclass(slots=True, frozen=True)
215
+ class _BoolFilterEchoArgs:
216
+ """Arguments for BoolFilterEchoFunction."""
217
+
218
+ count: Annotated[int, Arg(0, doc="Number of rows to generate", ge=0)]
219
+
220
+
221
+ @dataclass(kw_only=True)
222
+ class _BoolFilterEchoState(ArrowSerializableDataclass):
223
+ """Remaining rows plus the echoed SQL rendering of the pushed filters."""
224
+
225
+ remaining: int
226
+ current_index: int = 0
227
+ filter_str: str = "(none)"
228
+
229
+
230
+ @init_single_worker
231
+ @bind_fixed_schema
232
+ @_cardinality_from_count
233
+ class BoolFilterEchoFunction(TableFunctionGenerator[_BoolFilterEchoArgs, _BoolFilterEchoState]):
234
+ """Rows with a nullable BOOLEAN column, echoing the pushed-down filters.
235
+
236
+ SCHEMA
237
+ ------
238
+ Output: {"n": int64, "flag": bool, "pushed_filters": string}
239
+
240
+ ``flag`` cycles TRUE, FALSE, NULL so every predicate is exercised against
241
+ all three truth values -- the NULL row is what distinguishes ``WHERE flag``
242
+ from ``WHERE flag IS NOT FALSE``, and ``WHERE NOT flag`` from
243
+ ``WHERE flag IS NOT TRUE``.
244
+
245
+ Attributes:
246
+ FIXED_SCHEMA: The fixed Arrow output schema this function always produces.
247
+
248
+ """
249
+
250
+ class Meta:
251
+ """Metadata for BoolFilterEchoFunction."""
252
+
253
+ name = "bool_filter_echo"
254
+ description = "Rows with a nullable BOOLEAN column, echoing pushed-down filters"
255
+ categories = ["generator", "diagnostic"]
256
+ filter_pushdown = True
257
+ auto_apply_filters = True
258
+ projection_pushdown = True
259
+ examples = [
260
+ FunctionExample(
261
+ sql="SELECT * FROM bool_filter_echo(6) WHERE flag",
262
+ description="A boolean column is a predicate on its own",
263
+ ),
264
+ ]
265
+
266
+ FIXED_SCHEMA: ClassVar[pa.Schema] = schema({"n": pa.int64(), "flag": pa.bool_(), "pushed_filters": pa.utf8()})
267
+
268
+ @classmethod
269
+ def initial_state(cls, params: ProcessParams[_BoolFilterEchoArgs]) -> _BoolFilterEchoState:
270
+ """Create initial state, caching the SQL rendering of the pushed filters."""
271
+ assert params.init_call is not None
272
+ pf = params.init_call.pushdown_filters
273
+ filters = (
274
+ cls.pushdown_filters(
275
+ pf,
276
+ join_keys=params.init_call.join_keys,
277
+ output_schema=params.init_call.output_schema,
278
+ )
279
+ if pf is not None
280
+ else None
281
+ )
282
+ return _BoolFilterEchoState(
283
+ remaining=params.args.count,
284
+ filter_str=_format_pushed_filters(filters),
285
+ )
286
+
287
+ @classmethod
288
+ def process(
289
+ cls,
290
+ params: ProcessParams[_BoolFilterEchoArgs],
291
+ state: _BoolFilterEchoState,
292
+ out: OutputCollector,
293
+ ) -> None:
294
+ """Emit rows whose ``flag`` cycles TRUE, FALSE, NULL."""
295
+ if state.remaining <= 0:
296
+ out.finish()
297
+ return
298
+
299
+ n_values = list(range(state.current_index, state.current_index + state.remaining))
300
+ flags: list[bool | None] = [(True, False, None)[i % 3] for i in n_values]
301
+
302
+ out.emit(
303
+ pa.RecordBatch.from_pydict(
304
+ {
305
+ "n": n_values,
306
+ "flag": flags,
307
+ "pushed_filters": [state.filter_str] * len(n_values),
308
+ },
309
+ schema=params.output_schema,
310
+ )
311
+ )
312
+
313
+ state.current_index += state.remaining
314
+ state.remaining = 0
315
+
316
+
194
317
  # ============================================================================
195
318
  # ValuePruneFunction — exercises PushdownFilters.get_column_values('n'), the
196
319
  # partition-pruning idiom (resolve the discrete value set up front, fetch only
@@ -140,6 +140,7 @@ from vgi._test_fixtures.table import (
140
140
  RFF_SIMPLE_COLUMNS,
141
141
  RFF_STRUCT_COLUMNS,
142
142
  BatchIndexOverflowFunction,
143
+ BoolFilterEchoFunction,
143
144
  BrokenMissingPartitionValuesFunction,
144
145
  BrokenPartitionColumnAbsentFromBatchFunction,
145
146
  BrokenPartitionMinNeqMaxFunction,
@@ -486,6 +487,7 @@ _EXAMPLE_CATALOG = Catalog(
486
487
  # TableFunctionGenerator - generate output without input
487
488
  ConstantColumnsFunction,
488
489
  SlowCancellableFunction,
490
+ BoolFilterEchoFunction,
489
491
  FilterEchoFunction,
490
492
  FilterEchoPartitionedFunction,
491
493
  FilterEchoTableScanFunction,
vgi/auth.py CHANGED
@@ -9,8 +9,8 @@ HTTP auth factories (require ``vgi[http]``):
9
9
  bearer_authenticate, bearer_authenticate_static, chain_authenticate,
10
10
  OAuthResourceMetadata, AuthUnavailableError
11
11
 
12
- Token introspection (always available):
13
- TokenIdentity, TokenResolver
12
+ Token introspection and delegation (always available):
13
+ TokenIdentity, TokenResolver, IssuedGrant, GrantMinter
14
14
 
15
15
  JWT auth (requires ``vgi[oauth]``):
16
16
  jwt_authenticate
@@ -30,16 +30,25 @@ from vgi_rpc.rpc import AuthContext, CallContext
30
30
  # on the ``http`` extra. Still not re-exported by ``vgi_rpc.rpc`` itself, so
31
31
  # the private module remains the only import path; re-exported here so a worker
32
32
  # that implements ``resolve_token`` never has to name one.
33
- from vgi_rpc.rpc._token_identity import TokenIdentity
33
+ from vgi_rpc.rpc._token_identity import IssuedGrant, TokenIdentity
34
34
 
35
35
  #: What :meth:`vgi.worker.Worker.resolve_token` is. Upstream dropped its own
36
36
  #: alias when identity moved to the RPC layer; it is spelled out here so the
37
37
  #: name ``vgi.auth.TokenResolver`` keeps working.
38
38
  TokenResolver = Callable[[str], "TokenIdentity | None"]
39
39
 
40
+ #: What :meth:`vgi.worker.Worker.mint_grant` is: ``(principal, purpose, scopes,
41
+ #: ttl_seconds) -> IssuedGrant``. The subject is a *parameter of the call the
42
+ #: framework makes*, never of the wire method — ``issue_grant`` has no subject
43
+ #: field, so a worker is handed the caller's own principal and cannot be asked
44
+ #: to mint for anybody else.
45
+ GrantMinter = Callable[[str, str, list[str], int], "IssuedGrant"]
46
+
40
47
  __all__ = [
41
48
  "AuthContext",
42
49
  "CallContext",
50
+ "GrantMinter",
51
+ "IssuedGrant",
43
52
  "TokenIdentity",
44
53
  "TokenResolver",
45
54
  ]
vgi/filter_v2.py CHANGED
@@ -740,11 +740,24 @@ class _Parser:
740
740
 
741
741
  @staticmethod
742
742
  def _is_boolean(expression: FilterExpression) -> bool:
743
- if isinstance(expression, Literal):
744
- return pa.types.is_boolean(expression.field.type)
745
- if isinstance(expression, (Comparison, BooleanExpression, Not, IsNull, In, RuntimeFilter)):
746
- return True
747
- return isinstance(expression, Call)
743
+ """Whether *expression* resolves to BOOLEAN.
744
+
745
+ Ask the type, never the node kind. The two agree for every node whose
746
+ type is structurally fixed -- a comparison is BOOLEAN whatever its
747
+ operands -- and that is exactly why enumerating kinds looked sufficient.
748
+ It is not: ``column_ref``, ``field_ref``, ``cast`` and ``arithmetic``
749
+ are BOOLEAN or not depending on a type resolved from elsewhere (the
750
+ bind output schema, or a parent expression), and the schema admits all
751
+ four as a predicate root -- ``coreExpression`` lists ``columnRef``
752
+ first. Enumerating kinds rejected the lot, so the idiomatic
753
+ ``WHERE flag`` / ``WHERE NOT flag`` -- which DuckDB pushes down as a
754
+ bare column reference rather than rewriting to ``flag = true`` -- was
755
+ refused as "predicate root must resolve to BOOLEAN" even though the
756
+ parser had already resolved the column to BOOLEAN and stored it on the
757
+ node.
758
+ """
759
+ data_type = expression_type(expression)
760
+ return data_type is not None and pa.types.is_boolean(data_type)
748
761
 
749
762
  def _payload(self, prefix: str, ref: object) -> tuple[pa.Field[Any], pa.Scalar[Any]]:
750
763
  index = _uint(ref, f"{prefix}_ref")
vgi/serve.py CHANGED
@@ -791,9 +791,12 @@ def _build_identity(
791
791
  ) -> IdentityImpl | None:
792
792
  """Build the ``vgi_rpc.Identity.v1`` implementation, or ``None``.
793
793
 
794
- ``None`` unless the worker class actually implements the lookup, and that
795
- is the point: ``RpcServer`` then does not host the protocol at all, rather
796
- than hosting it and refusing every call. Absent beats routed-and-refusing —
794
+ ``None`` unless the worker class implements at least one of the two
795
+ methods, and that is the point: ``RpcServer`` then does not host the
796
+ protocol at all, rather than hosting it and refusing every call. The two
797
+ are independent — a worker may resolve credentials without minting them,
798
+ mint without resolving, or do both, and ``offered_methods()`` reports
799
+ exactly what it wrote. Absent beats routed-and-refusing —
797
800
  it is what keeps a dependency upgrade from growing a
798
801
  credential-to-identity oracle on every existing worker.
799
802
 
@@ -804,8 +807,8 @@ def _build_identity(
804
807
  calling and reading an error.
805
808
 
806
809
  Args:
807
- worker_cls: The worker class, consulted for a ``resolve_token``
808
- override.
810
+ worker_cls: The worker class, consulted for ``resolve_token`` and
811
+ ``mint_grant`` overrides.
809
812
  introspect_principals: Principals permitted to introspect, or ``None``
810
813
  to read the environment.
811
814
 
@@ -815,7 +818,8 @@ def _build_identity(
815
818
 
816
819
  """
817
820
  resolver = worker_cls._introspect_resolver()
818
- if resolver is None:
821
+ minter = worker_cls._grant_minter()
822
+ if resolver is None and minter is None:
819
823
  return None
820
824
 
821
825
  # Not re-exported by ``vgi_rpc.rpc``, so the private module is the only
@@ -824,7 +828,11 @@ def _build_identity(
824
828
 
825
829
  return IdentityImpl(
826
830
  resolve_token=resolver,
827
- introspect_principals=_resolve_introspect_principals(introspect_principals),
831
+ mint_grant=minter,
832
+ # Only meaningful for ``introspect_token``, and ``IdentityImpl``
833
+ # validates it only when a resolver is supplied — a worker that mints
834
+ # grants but resolves nothing is not an oracle and needs no allowlist.
835
+ introspect_principals=(_resolve_introspect_principals(introspect_principals) if resolver is not None else None),
828
836
  )
829
837
 
830
838
 
@@ -63,6 +63,9 @@ from vgi.filter_v2 import (
63
63
  from vgi.filter_v2 import (
64
64
  Literal as V2Literal,
65
65
  )
66
+ from vgi.filter_v2 import (
67
+ Not as V2Not,
68
+ )
66
69
  from vgi.filter_v2 import (
67
70
  RuntimeFilter as V2RuntimeFilter,
68
71
  )
@@ -72,6 +75,9 @@ from vgi.filter_v2 import (
72
75
  from vgi.filter_v2 import (
73
76
  expression_columns as v2_expression_columns,
74
77
  )
78
+ from vgi.filter_v2 import (
79
+ expression_type as v2_expression_type,
80
+ )
75
81
 
76
82
  if TYPE_CHECKING:
77
83
  from collections.abc import Callable, Iterator
@@ -207,6 +213,7 @@ __all__ = [
207
213
  "OrFilter",
208
214
  "StructFilter",
209
215
  "ExpressionFilter",
216
+ "FilterSQLUnsupportedError",
210
217
  "V2ExpressionFilter",
211
218
  # Expression node classes
212
219
  "ExpressionNode",
@@ -1638,6 +1645,22 @@ class PushdownFilters:
1638
1645
  # =============================================================================
1639
1646
 
1640
1647
 
1648
+ class FilterSQLUnsupportedError(ValueError):
1649
+ """A filter that `to_sql()` cannot render as a SQL fragment.
1650
+
1651
+ Raised rather than skipped, and that is the whole point: DuckDB does **not**
1652
+ re-apply a predicate it pushed into a table function, so a WHERE clause that
1653
+ silently omits one does not return the same rows more slowly -- it returns
1654
+ the wrong rows. Degrading quietly here would turn an unrenderable filter
1655
+ into a wrong answer with no error anywhere.
1656
+
1657
+ A worker that catches this has two honest options: evaluate the filters in
1658
+ memory instead (`PushdownFilters.apply()` / `evaluate()` handle every v2
1659
+ expression), or decline the scan. Subclasses `ValueError` so existing
1660
+ callers that catch the old error keep working.
1661
+ """
1662
+
1663
+
1641
1664
  def _filter_to_sql(
1642
1665
  f: Filter,
1643
1666
  quote: Callable[[str], str],
@@ -1713,8 +1736,19 @@ def _filter_to_sql(
1713
1736
  # Constants are embedded directly in the SQL string.
1714
1737
  return expr.to_sql(f.column_name), []
1715
1738
 
1739
+ case V2ExpressionFilter(expression=expression):
1740
+ # A v2 expression the legacy projection could not reduce to a leaf
1741
+ # shape (arithmetic, casts, extension calls, ...). It evaluates
1742
+ # correctly -- `V2ExpressionFilter.evaluate()` runs the v2 reference
1743
+ # evaluator -- there is simply no SQL rendering for it yet.
1744
+ raise FilterSQLUnsupportedError(
1745
+ f"filter on {f.column_name!r} is a v2 expression with no SQL rendering "
1746
+ f"({type(expression).__name__}); evaluate it with PushdownFilters.apply() "
1747
+ f"instead, or the scan will return rows the predicate excludes"
1748
+ )
1749
+
1716
1750
  case _:
1717
- raise ValueError(f"Unknown filter type: {type(f)}")
1751
+ raise FilterSQLUnsupportedError(f"Unknown filter type: {type(f)}")
1718
1752
 
1719
1753
 
1720
1754
  # =============================================================================
@@ -1787,6 +1821,48 @@ def _pushdown_from_v2_state(state: V2FilterState) -> PushdownFilters:
1787
1821
  )
1788
1822
 
1789
1823
 
1824
+ def _v2_boolean_column_view(expression: V2FilterExpression) -> Filter | None:
1825
+ """Project ``WHERE flag`` / ``WHERE NOT flag`` onto the legacy equality leaf.
1826
+
1827
+ DuckDB pushes a predicate that *is* a boolean column down as a bare
1828
+ ``column_ref``, not as ``flag = true``. Left as a generic v2 expression it
1829
+ becomes a `V2ExpressionFilter`, which evaluates fine but renders no SQL --
1830
+ so every SQL-backed worker loses pushdown on the most ordinary boolean
1831
+ predicate there is, and on the commonest shape of all, a conjunction with
1832
+ one, it loses pushdown for the whole conjunction.
1833
+
1834
+ The rewrite is exact rather than approximate, including under NULLs:
1835
+ ``WHERE flag`` keeps only TRUE (a NULL predicate is not satisfied), and so
1836
+ does ``flag = true``; ``WHERE NOT flag`` keeps only FALSE, and so does
1837
+ ``flag = false``. Three-valued logic makes the two pairs agree on every
1838
+ input, which is what lets this be a projection and not a semantic change.
1839
+
1840
+ Args:
1841
+ expression: A v2 expression that may be a bare boolean column or its
1842
+ negation.
1843
+
1844
+ Returns:
1845
+ The equivalent `ConstantFilter`, or None when *expression* is neither.
1846
+
1847
+ """
1848
+
1849
+ def boolean_column(node: V2FilterExpression) -> V2ColumnRef | None:
1850
+ if not isinstance(node, V2ColumnRef):
1851
+ return None
1852
+ node_type = v2_expression_type(node)
1853
+ # An unresolved column (no bind schema) is not known to be boolean, and
1854
+ # guessing here would rewrite a non-boolean predicate into `= true`.
1855
+ if node_type is None or not pa.types.is_boolean(node_type):
1856
+ return None
1857
+ return node
1858
+
1859
+ if ref := boolean_column(expression):
1860
+ return ConstantFilter(ref.column_name, ref.column_index, ComparisonOp.EQ, pa.scalar(True))
1861
+ if isinstance(expression, V2Not) and (ref := boolean_column(expression.expression)):
1862
+ return ConstantFilter(ref.column_name, ref.column_index, ComparisonOp.EQ, pa.scalar(False))
1863
+ return None
1864
+
1865
+
1790
1866
  def _v2_filter_view(expression: V2FilterExpression, context: EvaluationContext) -> Filter | None:
1791
1867
  """Project common v2 shapes into the longstanding convenience classes."""
1792
1868
  if isinstance(expression, V2Comparison):
@@ -1802,6 +1878,8 @@ def _v2_filter_view(expression: V2FilterExpression, context: EvaluationContext)
1802
1878
  }
1803
1879
  if op := mapping.get(expression.op):
1804
1880
  return ConstantFilter(operands[0].column_name, operands[0].column_index, op, operands[1].value)
1881
+ if boolean_leaf := _v2_boolean_column_view(expression):
1882
+ return boolean_leaf
1805
1883
  if isinstance(expression, V2IsNull) and isinstance(expression.expression, V2ColumnRef):
1806
1884
  ref = expression.expression
1807
1885
  if expression.negated:
vgi/worker.py CHANGED
@@ -173,7 +173,7 @@ if TYPE_CHECKING:
173
173
  from ssl import SSLContext
174
174
 
175
175
  from vgi_rpc.rpc import PeerAuthenticationPolicy, PeerIdentityProvider
176
- from vgi_rpc.rpc._token_identity import TokenIdentity
176
+ from vgi_rpc.rpc._token_identity import IssuedGrant, TokenIdentity
177
177
 
178
178
  from vgi.catalog.descriptors import Catalog
179
179
  from vgi.protocol import (
@@ -1478,6 +1478,80 @@ class Worker:
1478
1478
  return None
1479
1479
  return cls.resolve_token
1480
1480
 
1481
+ @classmethod
1482
+ def mint_grant(cls, principal: str, purpose: str, scopes: list[str], ttl_seconds: int) -> IssuedGrant:
1483
+ """Mint a standing delegation credential for *principal*.
1484
+
1485
+ Override to host ``vgi_rpc.Identity.v1``'s ``issue_grant``. Like
1486
+ ``resolve_token``, the method is absent until it is overridden rather
1487
+ than hosted-and-refusing, so no worker grows a credential *issuer* by
1488
+ upgrading a dependency.
1489
+
1490
+ OAuth cannot express durable delegation: it fuses the grant, the
1491
+ credential and the session into one refresh token, so an IdP shortening
1492
+ session lifetime shortens the grant. This mints the durable record —
1493
+ created while the user is present, presented later by unattended
1494
+ automation as an ordinary bearer.
1495
+
1496
+ **There is no subject on the wire.** ``issue_grant`` takes no subject
1497
+ field; the framework passes the *caller's own* authenticated principal,
1498
+ so cross-subject minting is closed by construction rather than by a
1499
+ check that could be forgotten. That is also why this needs no allowlist
1500
+ while ``introspect_token`` has one — it is not an oracle about anybody
1501
+ else. The framework additionally refuses a caller whose authentication
1502
+ is stale (``max_auth_age``, 15 minutes by default), because "minted
1503
+ while the user is present" is the property that makes a grant
1504
+ accountable to them.
1505
+
1506
+ ``IssuedGrant.token`` is **opaque to the framework** — a sealed
1507
+ envelope, a database row, or a credential brokered from the IdP are all
1508
+ equally valid and equally invisible here. It is never parsed and never
1509
+ logged. Note that a self-contained sealed token cannot be revoked
1510
+ individually: with no server-side record there is nothing to delete,
1511
+ and rotating the sealing key invalidates every grant at once. If
1512
+ per-grant revocation matters, the token has to name something the
1513
+ worker can look up and remove.
1514
+
1515
+ Args:
1516
+ principal: The caller's authenticated principal, in the form this
1517
+ worker derives itself. Supplied by the framework, never by the
1518
+ caller.
1519
+ purpose: Free-text reason recorded with the grant, for the audit
1520
+ trail.
1521
+ scopes: Scopes the grant is limited to. A worker that ignores this
1522
+ is issuing a credential broader than the caller asked for.
1523
+ ttl_seconds: Lifetime the caller requested. A worker may issue a
1524
+ shorter one — and should say so in ``expires_at`` — but must
1525
+ never issue a longer one.
1526
+
1527
+ Returns:
1528
+ The minted ``IssuedGrant``. ``expires_at`` is a declaration rather
1529
+ than an enforcement: the real lifetime lives inside the opaque
1530
+ token, so a worker that states one has thought about one.
1531
+
1532
+ Raises:
1533
+ GrantRefusedError: When this caller may not mint this grant —
1534
+ an unsupported scope, a ttl beyond policy, a principal the
1535
+ worker declines to delegate for.
1536
+
1537
+ """
1538
+ from vgi_rpc.rpc._token_identity import GrantRefusedError
1539
+
1540
+ raise GrantRefusedError("this worker does not mint grants")
1541
+
1542
+ @classmethod
1543
+ def _grant_minter(cls) -> Callable[[str, str, list[str], int], IssuedGrant] | None:
1544
+ """Return this worker's grant minter, or ``None`` when it has none.
1545
+
1546
+ Identity comparison against the base implementation, for the same
1547
+ reason ``_introspect_resolver`` uses one: the method must be absent
1548
+ unless a worker actually wrote the minting, and a "supports grants"
1549
+ flag can be set without one.
1550
+ """
1551
+ if cls.mint_grant.__func__ is Worker.mint_grant.__func__: # type: ignore[attr-defined]
1552
+ return None
1553
+ return cls.mint_grant
1554
+
1481
1555
  @final
1482
1556
  @classmethod
1483
1557
  def main(cls) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vgi-python
3
- Version: 0.36.1
3
+ Version: 0.37.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
@@ -5,12 +5,12 @@ vgi/aggregate_function.py,sha256=NYxudbvQ9ZLkP2w5hRdd4YgsGUZ_0cVfqqXnlmJPfls,273
5
5
  vgi/argument_spec.py,sha256=SADH97VlFu3asI8CWZ_I7XTuSxwqFciFgdupdAf-tkY,34231
6
6
  vgi/arguments.py,sha256=ncnC1CS7KN-2rDjSw9vdf1uHqV5ozebet_eZtcq_ujw,72300
7
7
  vgi/attach_header.py,sha256=sxpzCOVa5fl_-kQSCZgeUxS5I9Kuv8VCUjAOF5s1pr0,3782
8
- vgi/auth.py,sha256=m97hDDR8bXIQRBvqhHQZesBrNkAnKgSH6jhuXtGubjI,4688
8
+ vgi/auth.py,sha256=qMhaLlQfn6F_ovMg4vwD8fl55THalWrSQ9A_V8arGDU,5195
9
9
  vgi/cache_control.py,sha256=IyBp9gZTCc4Y-4V7Rs05_o5lSEjbR2jt6cddXPU0dKw,7812
10
10
  vgi/copy_from_function.py,sha256=oroQe5qYzZTJ-ppzQTQ-OruEF1ZlwTBNLBoXYfN-P2o,7133
11
11
  vgi/copy_to_function.py,sha256=8l0v_DyEF1EnFwinStrZDQ-yNFfGJsv3Mr5o2jG30f0,9279
12
12
  vgi/exceptions.py,sha256=oX_sZc9xGWi7Xf8cJQf89fX19i3ocDEj_V_76GINgBQ,7294
13
- vgi/filter_v2.py,sha256=pSdmZF4HgSr9ZuM_mU3NMwUuAw0X4sZ7KonTMeGnjUw,56243
13
+ vgi/filter_v2.py,sha256=FXuYOrm7uA-c0hYBXd5ScCRfjJmR5I9KfTT78e3o0vM,57089
14
14
  vgi/filter_v2_builder.py,sha256=8OmyzDI6GDdH4pYm2sJFMidzKusi0I8IGNipolPpjX0,5535
15
15
  vgi/function.py,sha256=KIfvKk1nzBvRDPXViHNX89tv6gvIMiDj4e8fl6ANqQg,11626
16
16
  vgi/function_storage.py,sha256=FjVwxc1BIjWo-82j8YE5qcU8w66sGjwyGsTmCUhn-Kw,67576
@@ -30,13 +30,13 @@ vgi/schema_path.py,sha256=GVlkZlCUj7GbVhgiocSMC6Ff5iTJhKqiYE62hMIEKiI,2403
30
30
  vgi/schema_utils.py,sha256=zQCliUgIpO2u77Mhtgg3OFvOEDNgJopu6kj2fw8YP1I,7945
31
31
  vgi/secret_protocol.py,sha256=iCDl7nF7Nb4cJV4vpfbldunTDqDJWzCzS7uCmOACf7U,7151
32
32
  vgi/secret_service.py,sha256=r7qCzfnOMirf9wSCUe-mgFe3z6v9_cDRzixuSAZG_bs,8973
33
- vgi/serve.py,sha256=5UmvRAGcHj5RKCOUQMXG9RFVYAbLZsHwwk1qVI9fipY,63844
33
+ vgi/serve.py,sha256=DfHSp6B30v_EgWoswsqtmfQ4Fwz1PrnJqvUo1GwM2hA,64416
34
34
  vgi/split_token.py,sha256=tutcksj-hgWrLY7UW3sSqkyoaD-cMqNTOeff40MGl_g,10850
35
35
  vgi/table_buffering_function.py,sha256=O5TcIdqk8-YViCQCkb8bOevO-zhe3IoyzLgwXDO3Cjc,19531
36
- vgi/table_filter_pushdown.py,sha256=agzbDOz_-64E2uNZdDOQs8woP1CBAE_SU5rv2N4fqWk,63435
36
+ vgi/table_filter_pushdown.py,sha256=4Qo4OAOLjTYQ5pinNTOjLCDnb96OHqpcxJlbSJo3cJU,67144
37
37
  vgi/table_function.py,sha256=8Pti3uFnsRaPCVG-CGYl9r3GHJF146Fp_62oXXwHKKw,73502
38
38
  vgi/table_in_out_function.py,sha256=F2QEtsbGEYHDVyQ4Cf9OCW7HlWXeqekAyx607foB-bs,19393
39
- vgi/worker.py,sha256=MGMQmy6PPKCqo5q_9fBS3z3zf4y6-UKdJAJiHGfnk50,249553
39
+ vgi/worker.py,sha256=0ez46DE5tc1-78ll29lBP7KRKdDJnkOv89gdr4RjLrE,253453
40
40
  vgi/write_results.py,sha256=-OrUcz6E9ahhC_pnKYAwIMR2LpapwQafLC4IOnPTmec,2851
41
41
  vgi/_test_fixtures/__init__.py,sha256=xQq-QQLk8USQ6TZHsPiPcZldNNUdcJ2gToXMPGzScLI,444
42
42
  vgi/_test_fixtures/attach_options.py,sha256=ZNgNTNF1RymSfnU-uVIDTLvCpc283h6O8tZ1UuEa3Lc,12516
@@ -57,7 +57,7 @@ vgi/_test_fixtures/table_in_out_same_name.py,sha256=jdsQNdKn64T54W_TcLB-rhydI69Y
57
57
  vgi/_test_fixtures/twin_catalogs.py,sha256=UFsFuZ_nFpe62HatGnOMru36vk35Xt3Ta1Hkr1aWN9w,4450
58
58
  vgi/_test_fixtures/versioned.py,sha256=gnxNejc4cNGPKl_DxJk1SU8Ielj-rwmQYGkiShXzxK4,5786
59
59
  vgi/_test_fixtures/versioned_tables.py,sha256=_JHPe6LaiO0x2We28Ion8hh6pSATmHZYvAczB4uwBsA,22272
60
- vgi/_test_fixtures/worker.py,sha256=tAdCcL9x3LwgtY0OG56bapmBCwGZJ-M7paQY9hXjgB8,96440
60
+ vgi/_test_fixtures/worker.py,sha256=ybrkhnYchWhbdMbUYKHHsnB6W0K5y9-QplyJSx2Bs7Y,96508
61
61
  vgi/_test_fixtures/accumulate/__init__.py,sha256=4hYT8jqRoVHSjV9TB7v0Z1CMJtdLuPaDWSz4J2fvMDs,868
62
62
  vgi/_test_fixtures/accumulate/worker.py,sha256=4a2VaCoOIXPEoJpkNume7FoBlQzs9kRl8b-_ZJMHvnQ,30165
63
63
  vgi/_test_fixtures/aggregate/__init__.py,sha256=2GCp80FNrsWFOA2SV3lCffzLjoN5AKwrf57t2YRg8O0,2393
@@ -90,13 +90,13 @@ vgi/_test_fixtures/scalar/settings_secrets.py,sha256=ZOGkGY0CoD3ous_ij0--ss5byB4
90
90
  vgi/_test_fixtures/scalar/type_info.py,sha256=2WeTxakT-_tcWybPfkCrAHVAMOadFN3tb8E3_EmqIyw,6304
91
91
  vgi/_test_fixtures/schema_reconcile/__init__.py,sha256=rCCtM5bd67-PTPeIYg9SCJaKUSglA6YeXsedQBEUlmA,1324
92
92
  vgi/_test_fixtures/schema_reconcile/worker.py,sha256=_r-cCQC3r48F0yTEdZC5zQYN_vuCx1HIkoP8_idBJHk,23680
93
- vgi/_test_fixtures/table/__init__.py,sha256=qE3gpMgrbeersJ8Lw-tX-ivYR1nYeqm4gZSrEjnDzF4,9887
93
+ vgi/_test_fixtures/table/__init__.py,sha256=qwTsNJLlsWgD4wEJav8e6pU1MQl8jV8Gvw6wR0v9crM,9945
94
94
  vgi/_test_fixtures/table/_common.py,sha256=m_99LRrDlULKGg8W8gJQQ29-_RxFINFlCMi3JtM0Y08,7753
95
95
  vgi/_test_fixtures/table/batch_index.py,sha256=P5ds0xgikuEQanSEWVWKMLbdvIzUeJraI-GuSoPdb6U,11641
96
96
  vgi/_test_fixtures/table/batch_index_broken.py,sha256=kZOGrLL7ZW1rmwPmNEYRmiF_vqIfHsfXioq5vKPWHk0,7314
97
97
  vgi/_test_fixtures/table/cache.py,sha256=7oohKpFJ_si7LoKE7QINkz5oIyj1sx8Kiobpwo4Sn7w,72322
98
98
  vgi/_test_fixtures/table/catalog_scans.py,sha256=5j1Sx02-HWK7bFurDu4e9HiS3Q9BmBukA3sAErH4GHE,5080
99
- vgi/_test_fixtures/table/filters.py,sha256=pOvuBFr0Rj7PXUWILUdfGf735sJda-iphJ_aQyiw_VY,42197
99
+ vgi/_test_fixtures/table/filters.py,sha256=1FCD_rTs8ssen7QZ_J3p0izKO09jzWEJ3j5L37sqkc4,46725
100
100
  vgi/_test_fixtures/table/late_materialization.py,sha256=BT1FOpQNeP4IXBS3hUvAmO-5jscl5iiQ11fvxUfN6DM,10355
101
101
  vgi/_test_fixtures/table/make_series.py,sha256=K-G_YNq25Kb7I5bp6XK4rCZzwMYNTxgKH3M_O-AQAlA,8812
102
102
  vgi/_test_fixtures/table/misc.py,sha256=71WOIFqk5ntnEIqsG-57rZ9DY7ShQqMKHi7yluNAlM4,16250
@@ -152,8 +152,8 @@ vgi/transactor/_duckdb_compat.py,sha256=sXVZ9JLKAQyGR1BjWczSwdQEavtr-TcZPoVZZnTr
152
152
  vgi/transactor/client.py,sha256=7DTeMksogsw6ANjQjGOPpKYrV76rg4_kGjktMJf54jg,4486
153
153
  vgi/transactor/protocol.py,sha256=v59IvrKnuvwOXvn_HEcBAbCzDVcx0akgKN0R1mChXg0,5034
154
154
  vgi/transactor/server.py,sha256=nzsZQxJZdgappdYX8okrFIrjFtUbdM_Hhskl1fZ2nDY,32553
155
- vgi_python-0.36.1.dist-info/METADATA,sha256=KiNZHizy9p1n8zOX0mFjOPot5qz_HCfD-qmipkpSBdc,25617
156
- vgi_python-0.36.1.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
157
- vgi_python-0.36.1.dist-info/entry_points.txt,sha256=3Kz1vgodw3pOL_xjtSyDB55-ZRy-U2X-X_Bdr582x0Q,165
158
- vgi_python-0.36.1.dist-info/licenses/LICENSE,sha256=pbJb4zZasP6n5ifEV81wFu017TarjydaYVmGbHcehtY,6103
159
- vgi_python-0.36.1.dist-info/RECORD,,
155
+ vgi_python-0.37.0.dist-info/METADATA,sha256=aYu1DnoElkICwDeYXEoJ_qTIR962ftJnNxkqMvYet5Y,25617
156
+ vgi_python-0.37.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
157
+ vgi_python-0.37.0.dist-info/entry_points.txt,sha256=3Kz1vgodw3pOL_xjtSyDB55-ZRy-U2X-X_Bdr582x0Q,165
158
+ vgi_python-0.37.0.dist-info/licenses/LICENSE,sha256=pbJb4zZasP6n5ifEV81wFu017TarjydaYVmGbHcehtY,6103
159
+ vgi_python-0.37.0.dist-info/RECORD,,