vaultkit 0.2.0__tar.gz → 0.3.1__tar.gz
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.
- {vaultkit-0.2.0 → vaultkit-0.3.1}/PKG-INFO +32 -1
- {vaultkit-0.2.0 → vaultkit-0.3.1}/README.md +31 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/pyproject.toml +1 -1
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/__init__.py +1 -1
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/client.py +43 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/core/http.py +1 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/errors/exceptions.py +6 -1
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/fetch_result.py +12 -1
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/definitions.py +67 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/executor.py +15 -6
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/utils/validation.py +74 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/PKG-INFO +32 -1
- {vaultkit-0.2.0 → vaultkit-0.3.1}/setup.cfg +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/tests/test_fetch.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/tests/test_query.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/core/__init__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/core/polling.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/errors/__init__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/errors/base.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/__init__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/__main__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/identity.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/server.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/__init__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/dataset_info.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/dataset_schema.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/query_result.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/__init__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/__init__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/anthropic.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/mcp.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/openai.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/builder.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/schemas.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/utils/__init__.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/utils/retry.py +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/SOURCES.txt +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/dependency_links.txt +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/entry_points.txt +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/requires.txt +0 -0
- {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vaultkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: VaultKit Python SDK for policy-driven, runtime governed data access
|
|
5
5
|
Author-email: VaultKit <founders@vaultkit.io>
|
|
6
6
|
License: MIT
|
|
@@ -74,6 +74,37 @@ print(result.rows)
|
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
|
+
## Analytical Queries
|
|
78
|
+
|
|
79
|
+
Aggregate, group, filter on aggregates and sort within a single dataset:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
result = client.execute(
|
|
83
|
+
dataset="orders",
|
|
84
|
+
fields=["region"],
|
|
85
|
+
aggregates=[{"func": "sum", "field": "amount", "alias": "total"}],
|
|
86
|
+
having=[{"field": "SUM(amount)", "operator": "gt", "value": 1000}],
|
|
87
|
+
order_by={"column": "total", "direction": "desc"},
|
|
88
|
+
limit=10,
|
|
89
|
+
purpose="Quarterly revenue by region",
|
|
90
|
+
)
|
|
91
|
+
# [{"region": "US", "total": 18250}, {"region": "CA", "total": 4100}, ...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
95
|
+
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
96
|
+
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
97
|
+
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
98
|
+
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
99
|
+
|
|
100
|
+
Policies apply to every field these clauses use. `min`/`max`/`sum`/`avg` over a masked
|
|
101
|
+
field are denied; `count` is allowed. Joins are not supported.
|
|
102
|
+
|
|
103
|
+
> Requires a VaultKit control plane with intent analytics support. Older control
|
|
104
|
+
> planes ignore these parameters and return ungrouped rows.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
77
108
|
## AI Agent Usage
|
|
78
109
|
|
|
79
110
|
VaultKit provides built-in tools for LLM agents.
|
|
@@ -48,6 +48,37 @@ print(result.rows)
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
+
## Analytical Queries
|
|
52
|
+
|
|
53
|
+
Aggregate, group, filter on aggregates and sort within a single dataset:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
result = client.execute(
|
|
57
|
+
dataset="orders",
|
|
58
|
+
fields=["region"],
|
|
59
|
+
aggregates=[{"func": "sum", "field": "amount", "alias": "total"}],
|
|
60
|
+
having=[{"field": "SUM(amount)", "operator": "gt", "value": 1000}],
|
|
61
|
+
order_by={"column": "total", "direction": "desc"},
|
|
62
|
+
limit=10,
|
|
63
|
+
purpose="Quarterly revenue by region",
|
|
64
|
+
)
|
|
65
|
+
# [{"region": "US", "total": 18250}, {"region": "CA", "total": 4100}, ...]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
69
|
+
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
70
|
+
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
71
|
+
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
72
|
+
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
73
|
+
|
|
74
|
+
Policies apply to every field these clauses use. `min`/`max`/`sum`/`avg` over a masked
|
|
75
|
+
field are denied; `count` is allowed. Joins are not supported.
|
|
76
|
+
|
|
77
|
+
> Requires a VaultKit control plane with intent analytics support. Older control
|
|
78
|
+
> planes ignore these parameters and return ungrouped rows.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
51
82
|
## AI Agent Usage
|
|
52
83
|
|
|
53
84
|
VaultKit provides built-in tools for LLM agents.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vaultkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "VaultKit Python SDK for policy-driven, runtime governed data access"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "VaultKit", email = "founders@vaultkit.io" }
|
|
@@ -19,7 +19,11 @@ from vaultkit.models.query_result import QueryResult
|
|
|
19
19
|
from vaultkit.utils.retry import RetryConfig, with_retries
|
|
20
20
|
from vaultkit.utils.validation import (
|
|
21
21
|
require_str,
|
|
22
|
+
validate_aggregates,
|
|
22
23
|
validate_filters,
|
|
24
|
+
validate_group_by,
|
|
25
|
+
validate_having,
|
|
26
|
+
validate_order_by,
|
|
23
27
|
validate_limit,
|
|
24
28
|
)
|
|
25
29
|
|
|
@@ -98,6 +102,10 @@ class VaultKitClient:
|
|
|
98
102
|
fields: Optional[List[str]] = None,
|
|
99
103
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|
100
104
|
limit: Optional[int] = None,
|
|
105
|
+
aggregates: Optional[List[Dict[str, Any]]] = None,
|
|
106
|
+
group_by: Optional[List[str]] = None,
|
|
107
|
+
having: Optional[List[Dict[str, Any]]] = None,
|
|
108
|
+
order_by: Optional[Dict[str, Any]] = None,
|
|
101
109
|
purpose: Optional[str] = None,
|
|
102
110
|
requester_region: Optional[str] = None,
|
|
103
111
|
requester_clearance: Optional[str] = None,
|
|
@@ -106,6 +114,18 @@ class VaultKitClient:
|
|
|
106
114
|
"""
|
|
107
115
|
Submit an intent request and return data — full grant lifecycle internal.
|
|
108
116
|
|
|
117
|
+
Analytical queries (single dataset, no joins):
|
|
118
|
+
aggregates [{"func": "sum", "field": "amount", "alias": "total"}]
|
|
119
|
+
func is one of count, sum, avg, min, max; "*" only with count.
|
|
120
|
+
group_by ["region"] — every field in `fields` must be listed when given.
|
|
121
|
+
having [{"field": "SUM(amount)", "operator": "gt", "value": 1000}]
|
|
122
|
+
field must be an aggregate expression, not an alias.
|
|
123
|
+
order_by {"column": "total", "direction": "desc"}
|
|
124
|
+
|
|
125
|
+
`fields` may be omitted when `aggregates` is given (e.g. a single COUNT(*)).
|
|
126
|
+
Requires a control plane that supports intent analytics; older ones
|
|
127
|
+
ignore these parameters.
|
|
128
|
+
|
|
109
129
|
Raises:
|
|
110
130
|
DeniedError Policy rejected. Do not retry.
|
|
111
131
|
ApprovalRequiredError Needs human approval (request_id attached).
|
|
@@ -126,6 +146,10 @@ class VaultKitClient:
|
|
|
126
146
|
fields=fields,
|
|
127
147
|
filters=filters,
|
|
128
148
|
limit=limit,
|
|
149
|
+
aggregates=aggregates,
|
|
150
|
+
group_by=group_by,
|
|
151
|
+
having=having,
|
|
152
|
+
order_by=order_by,
|
|
129
153
|
purpose=purpose,
|
|
130
154
|
requester_region=requester_region,
|
|
131
155
|
requester_clearance=requester_clearance,
|
|
@@ -166,6 +190,7 @@ class VaultKitClient:
|
|
|
166
190
|
raise DeniedError(
|
|
167
191
|
result.reason or "Request denied by policy",
|
|
168
192
|
policy_id=result.policy_id,
|
|
193
|
+
reason=result.reason,
|
|
169
194
|
)
|
|
170
195
|
|
|
171
196
|
# Case 1: data already returned (reuse path)
|
|
@@ -214,6 +239,10 @@ class VaultKitClient:
|
|
|
214
239
|
fields: Optional[List[str]] = None,
|
|
215
240
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|
216
241
|
limit: Optional[int] = None,
|
|
242
|
+
aggregates: Optional[List[Dict[str, Any]]] = None,
|
|
243
|
+
group_by: Optional[List[str]] = None,
|
|
244
|
+
having: Optional[List[Dict[str, Any]]] = None,
|
|
245
|
+
order_by: Optional[Dict[str, Any]] = None,
|
|
217
246
|
purpose: Optional[str] = None,
|
|
218
247
|
requester_region: Optional[str] = None,
|
|
219
248
|
requester_clearance: Optional[str] = None,
|
|
@@ -223,6 +252,10 @@ class VaultKitClient:
|
|
|
223
252
|
dataset = require_str("dataset", dataset)
|
|
224
253
|
limit = validate_limit(limit)
|
|
225
254
|
filters = validate_filters(filters)
|
|
255
|
+
aggregates = validate_aggregates(aggregates)
|
|
256
|
+
group_by = validate_group_by(group_by)
|
|
257
|
+
having = validate_having(having)
|
|
258
|
+
order_by = validate_order_by(order_by)
|
|
226
259
|
|
|
227
260
|
req: Dict[str, Any] = {"dataset": dataset}
|
|
228
261
|
body: Dict[str, Any] = {"request": req}
|
|
@@ -235,6 +268,14 @@ class VaultKitClient:
|
|
|
235
268
|
req["purpose"] = str(purpose)
|
|
236
269
|
if filters is not None:
|
|
237
270
|
req["filters"] = filters
|
|
271
|
+
if aggregates is not None:
|
|
272
|
+
req["aggregates"] = aggregates
|
|
273
|
+
if group_by is not None:
|
|
274
|
+
req["group_by"] = group_by
|
|
275
|
+
if having is not None:
|
|
276
|
+
req["having"] = having
|
|
277
|
+
if order_by is not None:
|
|
278
|
+
req["order_by"] = order_by
|
|
238
279
|
if requester_region is not None:
|
|
239
280
|
body["requester_region"] = requester_region
|
|
240
281
|
if requester_clearance is not None:
|
|
@@ -248,6 +289,7 @@ class VaultKitClient:
|
|
|
248
289
|
"poll": poll,
|
|
249
290
|
"has_fields": fields is not None,
|
|
250
291
|
"has_filters": filters is not None,
|
|
292
|
+
"has_aggregates": aggregates is not None,
|
|
251
293
|
"limit": limit,
|
|
252
294
|
},
|
|
253
295
|
)
|
|
@@ -274,6 +316,7 @@ class VaultKitClient:
|
|
|
274
316
|
raise DeniedError(
|
|
275
317
|
result.reason or "Request denied",
|
|
276
318
|
policy_id=result.policy_id,
|
|
319
|
+
reason=result.reason,
|
|
277
320
|
)
|
|
278
321
|
if result.is_pending and not poll:
|
|
279
322
|
raise QueuedError(
|
|
@@ -62,10 +62,12 @@ class DeniedError(VaultKitError):
|
|
|
62
62
|
message: str,
|
|
63
63
|
*,
|
|
64
64
|
policy_id: Optional[str] = None,
|
|
65
|
+
reason: Optional[str] = None,
|
|
65
66
|
context: Optional[ErrorContext] = None,
|
|
66
67
|
) -> None:
|
|
67
68
|
super().__init__(message, context=context)
|
|
68
69
|
self.policy_id = policy_id
|
|
70
|
+
self.reason = reason or message
|
|
69
71
|
|
|
70
72
|
|
|
71
73
|
class QueuedError(VaultKitError):
|
|
@@ -185,10 +187,13 @@ def map_http_error(status_code: int, message: str, *, context: ErrorContext) ->
|
|
|
185
187
|
return AuthError(message, context=context)
|
|
186
188
|
|
|
187
189
|
if status_code == 403:
|
|
188
|
-
|
|
190
|
+
# The intent API answers a denial with {"status": "denied", "policy_id", "reason"};
|
|
191
|
+
# older backends sent {"type": "policy_denied"}.
|
|
192
|
+
if error_type == "policy_denied" or raw.get("status") == "denied":
|
|
189
193
|
return DeniedError(
|
|
190
194
|
message,
|
|
191
195
|
policy_id=raw.get("policy_id"),
|
|
196
|
+
reason=raw.get("reason"),
|
|
192
197
|
context=context,
|
|
193
198
|
)
|
|
194
199
|
return ForbiddenError(message, context=context)
|
|
@@ -42,12 +42,23 @@ class FetchResult:
|
|
|
42
42
|
@property
|
|
43
43
|
def masked_fields(self) -> List[str]:
|
|
44
44
|
"""
|
|
45
|
-
Masked field names from meta
|
|
45
|
+
Masked field names from meta.
|
|
46
|
+
|
|
47
|
+
funl sends meta.masked_fields as a map of field -> mask method
|
|
48
|
+
({"email": "partial"}); a plain list of names is also accepted.
|
|
46
49
|
Returns [] safely if meta is absent or has no masked_fields key.
|
|
47
50
|
"""
|
|
48
51
|
value = (self.meta or {}).get("masked_fields")
|
|
52
|
+
if isinstance(value, dict):
|
|
53
|
+
return list(value.keys())
|
|
49
54
|
return value if isinstance(value, list) else []
|
|
50
55
|
|
|
56
|
+
@property
|
|
57
|
+
def masking_methods(self) -> Dict[str, str]:
|
|
58
|
+
"""Mask method per masked field, e.g. {"email": "partial"}. Empty if the API sent a list."""
|
|
59
|
+
value = (self.meta or {}).get("masked_fields")
|
|
60
|
+
return dict(value) if isinstance(value, dict) else {}
|
|
61
|
+
|
|
51
62
|
@property
|
|
52
63
|
def is_empty(self) -> bool:
|
|
53
64
|
return not self.rows
|
|
@@ -115,6 +115,8 @@ def query_tool_def(
|
|
|
115
115
|
"description": (
|
|
116
116
|
"Query a governed dataset through VaultKit. Policies are enforced automatically: "
|
|
117
117
|
"sensitive fields may be masked and some datasets require approval before data is returned. "
|
|
118
|
+
"Supports aggregation (count/sum/avg/min/max), grouping, having and sorting on a single dataset; "
|
|
119
|
+
"joins are not supported. "
|
|
118
120
|
"Returns data or a pending status if approval is required."
|
|
119
121
|
),
|
|
120
122
|
"input_schema": {
|
|
@@ -143,6 +145,71 @@ def query_tool_def(
|
|
|
143
145
|
"maximum": 10000,
|
|
144
146
|
"description": "Maximum number of rows to return.",
|
|
145
147
|
},
|
|
148
|
+
"aggregates": {
|
|
149
|
+
"type": "array",
|
|
150
|
+
"minItems": 1,
|
|
151
|
+
"items": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"func": {"type": "string", "enum": ["count", "sum", "avg", "min", "max"]},
|
|
155
|
+
"field": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "A field of the selected dataset, or '*' with count.",
|
|
158
|
+
},
|
|
159
|
+
"alias": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
|
|
162
|
+
"description": "Name of the result column. Use it in order_by.",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
"required": ["func", "field"],
|
|
166
|
+
},
|
|
167
|
+
"description": (
|
|
168
|
+
"Aggregate functions over the selected dataset. When aggregating, every entry in "
|
|
169
|
+
"'fields' is a grouping column. 'fields' may be omitted for a single total.\n\n"
|
|
170
|
+
"Example: [{func: 'sum', field: 'amount', alias: 'total'}]\n"
|
|
171
|
+
"min/max/sum/avg over masked fields are denied by policy; count is allowed."
|
|
172
|
+
),
|
|
173
|
+
},
|
|
174
|
+
"group_by": {
|
|
175
|
+
"type": "array",
|
|
176
|
+
"items": {"type": "string"},
|
|
177
|
+
"description": (
|
|
178
|
+
"Fields to group by. Optional: defaults to 'fields'. If given, it must include "
|
|
179
|
+
"every entry in 'fields'."
|
|
180
|
+
),
|
|
181
|
+
},
|
|
182
|
+
"having": {
|
|
183
|
+
"type": "array",
|
|
184
|
+
"items": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {
|
|
187
|
+
"field": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "An aggregate expression such as 'SUM(amount)'. Aliases are not allowed here.",
|
|
190
|
+
},
|
|
191
|
+
"operator": {"type": "string", "enum": ["gt", "lt", "eq", "gte", "lte"]},
|
|
192
|
+
"value": {"type": "number"},
|
|
193
|
+
},
|
|
194
|
+
"required": ["field", "operator", "value"],
|
|
195
|
+
},
|
|
196
|
+
"description": (
|
|
197
|
+
"Filters applied after aggregation. Requires 'aggregates'.\n\n"
|
|
198
|
+
"Example: [{field: 'SUM(amount)', operator: 'gt', value: 1000}]"
|
|
199
|
+
),
|
|
200
|
+
},
|
|
201
|
+
"order_by": {
|
|
202
|
+
"type": "object",
|
|
203
|
+
"properties": {
|
|
204
|
+
"column": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"description": "A field, an aggregate alias, or an aggregate expression.",
|
|
207
|
+
},
|
|
208
|
+
"direction": {"type": "string", "enum": ["asc", "desc"], "default": "asc"},
|
|
209
|
+
},
|
|
210
|
+
"required": ["column"],
|
|
211
|
+
"description": "Sort order of the results.",
|
|
212
|
+
},
|
|
146
213
|
"purpose": {
|
|
147
214
|
"type": "string",
|
|
148
215
|
"description": (
|
|
@@ -86,7 +86,6 @@ class ToolExecutor:
|
|
|
86
86
|
"name": d.dataset,
|
|
87
87
|
"datasource": d.datasource,
|
|
88
88
|
"visibility": visibility_map.get(getattr(d, "visibility", None), getattr(d, "visibility", None)),
|
|
89
|
-
"field_count": len(getattr(d, "fields", []) or []),
|
|
90
89
|
}
|
|
91
90
|
for d in datasets
|
|
92
91
|
],
|
|
@@ -105,6 +104,10 @@ class ToolExecutor:
|
|
|
105
104
|
fields=args.get("fields"),
|
|
106
105
|
filters=args.get("filters"),
|
|
107
106
|
limit=args.get("limit"),
|
|
107
|
+
aggregates=args.get("aggregates"),
|
|
108
|
+
group_by=args.get("group_by"),
|
|
109
|
+
having=args.get("having"),
|
|
110
|
+
order_by=args.get("order_by"),
|
|
108
111
|
purpose=purpose,
|
|
109
112
|
requester_region=args.get("requester_region") or self._default_requester_region,
|
|
110
113
|
)
|
|
@@ -117,6 +120,7 @@ class ToolExecutor:
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
if result.masked_fields:
|
|
123
|
+
response["masked_fields"] = result.masked_fields
|
|
120
124
|
response["note"] = f"Fields {result.masked_fields} were masked per data policy."
|
|
121
125
|
|
|
122
126
|
return response
|
|
@@ -154,12 +158,17 @@ class ToolExecutor:
|
|
|
154
158
|
|
|
155
159
|
if isinstance(exc, DeniedError):
|
|
156
160
|
policy_id = getattr(exc, "policy_id", None)
|
|
161
|
+
reason = getattr(exc, "reason", None) or exc.message
|
|
157
162
|
extra = f" (policy_id={policy_id})" if policy_id else ""
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
return {
|
|
164
|
+
"status": "denied",
|
|
165
|
+
"reason": reason,
|
|
166
|
+
"policy_id": policy_id,
|
|
167
|
+
"error": (
|
|
168
|
+
f"Access denied by VaultKit policy{extra}. Reason: {reason}. "
|
|
169
|
+
"Do not retry — this denial is deterministic."
|
|
170
|
+
),
|
|
171
|
+
}
|
|
163
172
|
|
|
164
173
|
if isinstance(exc, ApprovalRequiredError):
|
|
165
174
|
return {
|
|
@@ -85,3 +85,77 @@ def validate_filters(value: Optional[Any]) -> Optional[list]:
|
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Analytical clauses. Only the shape is checked here; the control plane
|
|
91
|
+
# validates functions, fields and operators and returns a clear error.
|
|
92
|
+
|
|
93
|
+
def _require_list_of_dicts(name: str, value: Optional[Any]) -> Optional[list]:
|
|
94
|
+
if value is None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
if not isinstance(value, list):
|
|
98
|
+
raise ValidationError(
|
|
99
|
+
f"{name} must be a list",
|
|
100
|
+
context=ErrorContext(raw=value),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for item in value:
|
|
104
|
+
if not isinstance(item, dict):
|
|
105
|
+
raise ValidationError(
|
|
106
|
+
f"each {name} entry must be an object",
|
|
107
|
+
context=ErrorContext(raw=item),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_aggregates(value: Optional[Any]) -> Optional[list]:
|
|
114
|
+
value = _require_list_of_dicts("aggregates", value)
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
if not value:
|
|
119
|
+
raise ValidationError(
|
|
120
|
+
"aggregates cannot be empty",
|
|
121
|
+
context=ErrorContext(raw=value),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
for agg in value:
|
|
125
|
+
if not agg.get("func") or not agg.get("field"):
|
|
126
|
+
raise ValidationError(
|
|
127
|
+
"each aggregate needs 'func' and 'field'",
|
|
128
|
+
context=ErrorContext(raw=agg),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_group_by(value: Optional[Any]) -> Optional[list]:
|
|
135
|
+
if value is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
if not isinstance(value, list) or not all(isinstance(v, str) and v.strip() for v in value):
|
|
139
|
+
raise ValidationError(
|
|
140
|
+
"group_by must be a list of field names",
|
|
141
|
+
context=ErrorContext(raw=value),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def validate_having(value: Optional[Any]) -> Optional[list]:
|
|
148
|
+
return _require_list_of_dicts("having", value)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def validate_order_by(value: Optional[Any]) -> Optional[Dict[str, Any]]:
|
|
152
|
+
if value is None:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
if not isinstance(value, dict) or not value.get("column"):
|
|
156
|
+
raise ValidationError(
|
|
157
|
+
"order_by must be an object with a 'column'",
|
|
158
|
+
context=ErrorContext(raw=value),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vaultkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: VaultKit Python SDK for policy-driven, runtime governed data access
|
|
5
5
|
Author-email: VaultKit <founders@vaultkit.io>
|
|
6
6
|
License: MIT
|
|
@@ -74,6 +74,37 @@ print(result.rows)
|
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
|
+
## Analytical Queries
|
|
78
|
+
|
|
79
|
+
Aggregate, group, filter on aggregates and sort within a single dataset:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
result = client.execute(
|
|
83
|
+
dataset="orders",
|
|
84
|
+
fields=["region"],
|
|
85
|
+
aggregates=[{"func": "sum", "field": "amount", "alias": "total"}],
|
|
86
|
+
having=[{"field": "SUM(amount)", "operator": "gt", "value": 1000}],
|
|
87
|
+
order_by={"column": "total", "direction": "desc"},
|
|
88
|
+
limit=10,
|
|
89
|
+
purpose="Quarterly revenue by region",
|
|
90
|
+
)
|
|
91
|
+
# [{"region": "US", "total": 18250}, {"region": "CA", "total": 4100}, ...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
95
|
+
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
96
|
+
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
97
|
+
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
98
|
+
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
99
|
+
|
|
100
|
+
Policies apply to every field these clauses use. `min`/`max`/`sum`/`avg` over a masked
|
|
101
|
+
field are denied; `count` is allowed. Joins are not supported.
|
|
102
|
+
|
|
103
|
+
> Requires a VaultKit control plane with intent analytics support. Older control
|
|
104
|
+
> planes ignore these parameters and return ungrouped rows.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
77
108
|
## AI Agent Usage
|
|
78
109
|
|
|
79
110
|
VaultKit provides built-in tools for LLM agents.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|