vaultkit 0.3.0__tar.gz → 0.4.0__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.3.0 → vaultkit-0.4.0}/PKG-INFO +19 -1
- {vaultkit-0.3.0 → vaultkit-0.4.0}/README.md +18 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/pyproject.toml +1 -1
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/__init__.py +1 -1
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/client.py +10 -3
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/core/http.py +1 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/errors/exceptions.py +6 -1
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/models/fetch_result.py +12 -1
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/definitions.py +34 -4
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/executor.py +11 -6
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/utils/validation.py +47 -2
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit.egg-info/PKG-INFO +19 -1
- {vaultkit-0.3.0 → vaultkit-0.4.0}/setup.cfg +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/tests/test_fetch.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/tests/test_query.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/core/__init__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/core/polling.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/errors/__init__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/errors/base.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/mcp/__init__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/mcp/__main__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/mcp/identity.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/mcp/server.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/models/__init__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/models/dataset_info.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/models/dataset_schema.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/models/query_result.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/__init__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/adapters/__init__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/adapters/anthropic.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/adapters/mcp.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/adapters/openai.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/builder.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/tools/schemas.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/utils/__init__.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit/utils/retry.py +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit.egg-info/SOURCES.txt +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit.egg-info/dependency_links.txt +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit.egg-info/entry_points.txt +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/vaultkit.egg-info/requires.txt +0 -0
- {vaultkit-0.3.0 → vaultkit-0.4.0}/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.4.0
|
|
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
|
|
@@ -93,6 +93,24 @@ result = client.execute(
|
|
|
93
93
|
|
|
94
94
|
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
95
95
|
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
96
|
+
An entry can also be a time bucket of a timestamp or date field, which groups by period
|
|
97
|
+
(`day`, `week`, `month`, `quarter` or `year`; `alias` and `timezone` are optional, and
|
|
98
|
+
the time zone defaults to UTC). Use one bucketed query for a trend instead of one query
|
|
99
|
+
per period:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
client.execute(
|
|
103
|
+
dataset="payments",
|
|
104
|
+
aggregates=[{"func": "count", "field": "*", "alias": "failures"}],
|
|
105
|
+
filters=[{"field": "status", "operator": "eq", "value": "failed"}],
|
|
106
|
+
group_by=[{"field": "created_at", "bucket": "quarter", "alias": "quarter"}],
|
|
107
|
+
order_by={"column": "quarter"},
|
|
108
|
+
purpose="Failed payments per quarter",
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Bucketing a fully or partially masked field is denied, like grouping by it. Needs a
|
|
113
|
+
control plane with time buckets (vaultkit#106); older ones reject the request.
|
|
96
114
|
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
97
115
|
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
98
116
|
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
@@ -67,6 +67,24 @@ result = client.execute(
|
|
|
67
67
|
|
|
68
68
|
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
69
69
|
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
70
|
+
An entry can also be a time bucket of a timestamp or date field, which groups by period
|
|
71
|
+
(`day`, `week`, `month`, `quarter` or `year`; `alias` and `timezone` are optional, and
|
|
72
|
+
the time zone defaults to UTC). Use one bucketed query for a trend instead of one query
|
|
73
|
+
per period:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
client.execute(
|
|
77
|
+
dataset="payments",
|
|
78
|
+
aggregates=[{"func": "count", "field": "*", "alias": "failures"}],
|
|
79
|
+
filters=[{"field": "status", "operator": "eq", "value": "failed"}],
|
|
80
|
+
group_by=[{"field": "created_at", "bucket": "quarter", "alias": "quarter"}],
|
|
81
|
+
order_by={"column": "quarter"},
|
|
82
|
+
purpose="Failed payments per quarter",
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Bucketing a fully or partially masked field is denied, like grouping by it. Needs a
|
|
87
|
+
control plane with time buckets (vaultkit#106); older ones reject the request.
|
|
70
88
|
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
71
89
|
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
72
90
|
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vaultkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "VaultKit Python SDK for policy-driven, runtime governed data access"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "VaultKit", email = "founders@vaultkit.io" }
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
from vaultkit.core.http import HttpClient, HttpConfig
|
|
8
8
|
from vaultkit.core.polling import PollConfig, poll_until_done
|
|
@@ -103,7 +103,7 @@ class VaultKitClient:
|
|
|
103
103
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|
104
104
|
limit: Optional[int] = None,
|
|
105
105
|
aggregates: Optional[List[Dict[str, Any]]] = None,
|
|
106
|
-
group_by: Optional[List[str]] = None,
|
|
106
|
+
group_by: Optional[List[Union[str, Dict[str, str]]]] = None,
|
|
107
107
|
having: Optional[List[Dict[str, Any]]] = None,
|
|
108
108
|
order_by: Optional[Dict[str, Any]] = None,
|
|
109
109
|
purpose: Optional[str] = None,
|
|
@@ -118,6 +118,11 @@ class VaultKitClient:
|
|
|
118
118
|
aggregates [{"func": "sum", "field": "amount", "alias": "total"}]
|
|
119
119
|
func is one of count, sum, avg, min, max; "*" only with count.
|
|
120
120
|
group_by ["region"] — every field in `fields` must be listed when given.
|
|
121
|
+
A time bucket of a timestamp or date field groups by period:
|
|
122
|
+
{"field": "created_at", "bucket": "quarter", "alias": "quarter",
|
|
123
|
+
"timezone": "America/Toronto"}; bucket is one of day, week,
|
|
124
|
+
month, quarter, year; alias and timezone (default UTC) are
|
|
125
|
+
optional. One bucketed query replaces one query per period.
|
|
121
126
|
having [{"field": "SUM(amount)", "operator": "gt", "value": 1000}]
|
|
122
127
|
field must be an aggregate expression, not an alias.
|
|
123
128
|
order_by {"column": "total", "direction": "desc"}
|
|
@@ -190,6 +195,7 @@ class VaultKitClient:
|
|
|
190
195
|
raise DeniedError(
|
|
191
196
|
result.reason or "Request denied by policy",
|
|
192
197
|
policy_id=result.policy_id,
|
|
198
|
+
reason=result.reason,
|
|
193
199
|
)
|
|
194
200
|
|
|
195
201
|
# Case 1: data already returned (reuse path)
|
|
@@ -239,7 +245,7 @@ class VaultKitClient:
|
|
|
239
245
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|
240
246
|
limit: Optional[int] = None,
|
|
241
247
|
aggregates: Optional[List[Dict[str, Any]]] = None,
|
|
242
|
-
group_by: Optional[List[str]] = None,
|
|
248
|
+
group_by: Optional[List[Union[str, Dict[str, str]]]] = None,
|
|
243
249
|
having: Optional[List[Dict[str, Any]]] = None,
|
|
244
250
|
order_by: Optional[Dict[str, Any]] = None,
|
|
245
251
|
purpose: Optional[str] = None,
|
|
@@ -315,6 +321,7 @@ class VaultKitClient:
|
|
|
315
321
|
raise DeniedError(
|
|
316
322
|
result.reason or "Request denied",
|
|
317
323
|
policy_id=result.policy_id,
|
|
324
|
+
reason=result.reason,
|
|
318
325
|
)
|
|
319
326
|
if result.is_pending and not poll:
|
|
320
327
|
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
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, List, Optional
|
|
4
4
|
|
|
5
|
+
from vaultkit.utils.validation import TIME_BUCKETS
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def discover_tool_def(
|
|
7
9
|
*,
|
|
@@ -110,6 +112,25 @@ def query_tool_def(
|
|
|
110
112
|
"required": ["logic", "conditions"],
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
time_bucket = {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"field": {"type": "string", "description": "A timestamp or date field of the dataset."},
|
|
119
|
+
"bucket": {"type": "string", "enum": list(TIME_BUCKETS)},
|
|
120
|
+
"alias": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
|
|
123
|
+
"description": "Name of the result column (default <field>_<bucket>). Use it in order_by.",
|
|
124
|
+
},
|
|
125
|
+
"timezone": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "IANA time zone the periods start in, e.g. 'America/Toronto'. Default UTC.",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
"required": ["field", "bucket"],
|
|
131
|
+
"additionalProperties": False,
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
return {
|
|
114
135
|
"name": "vaultkit_query",
|
|
115
136
|
"description": (
|
|
@@ -117,6 +138,8 @@ def query_tool_def(
|
|
|
117
138
|
"sensitive fields may be masked and some datasets require approval before data is returned. "
|
|
118
139
|
"Supports aggregation (count/sum/avg/min/max), grouping, having and sorting on a single dataset; "
|
|
119
140
|
"joins are not supported. "
|
|
141
|
+
"For a trend over time (per day, week, month, quarter or year), send ONE query that groups by a "
|
|
142
|
+
"time bucket of a timestamp field, not one query per period. "
|
|
120
143
|
"Returns data or a pending status if approval is required."
|
|
121
144
|
),
|
|
122
145
|
"input_schema": {
|
|
@@ -173,10 +196,17 @@ def query_tool_def(
|
|
|
173
196
|
},
|
|
174
197
|
"group_by": {
|
|
175
198
|
"type": "array",
|
|
176
|
-
"items": {"type": "string"},
|
|
199
|
+
"items": {"oneOf": [{"type": "string"}, time_bucket]},
|
|
177
200
|
"description": (
|
|
178
|
-
"Fields to group by
|
|
179
|
-
"every entry in 'fields'
|
|
201
|
+
"Fields to group by, or time buckets of a timestamp or date field. Optional: "
|
|
202
|
+
"defaults to 'fields'. If given, it must include every entry in 'fields'; a "
|
|
203
|
+
"bucketed field is not listed in 'fields'.\n\n"
|
|
204
|
+
"Example, failed payments per quarter in one query: "
|
|
205
|
+
"fields: [], aggregates: [{func: 'count', field: '*', alias: 'failures'}], "
|
|
206
|
+
"filters: [{field: 'status', operator: 'eq', value: 'failed'}], "
|
|
207
|
+
"group_by: [{field: 'created_at', bucket: 'quarter', alias: 'quarter'}], "
|
|
208
|
+
"order_by: {column: 'quarter'}. Each row has the quarter's start date and its count. "
|
|
209
|
+
"Mix with plain fields: group_by: ['status', {field: 'created_at', bucket: 'month'}]."
|
|
180
210
|
),
|
|
181
211
|
},
|
|
182
212
|
"having": {
|
|
@@ -203,7 +233,7 @@ def query_tool_def(
|
|
|
203
233
|
"properties": {
|
|
204
234
|
"column": {
|
|
205
235
|
"type": "string",
|
|
206
|
-
"description": "A field, an aggregate alias, or an aggregate expression.",
|
|
236
|
+
"description": "A field, an aggregate or time bucket alias, or an aggregate expression.",
|
|
207
237
|
},
|
|
208
238
|
"direction": {"type": "string", "enum": ["asc", "desc"], "default": "asc"},
|
|
209
239
|
},
|
|
@@ -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
|
],
|
|
@@ -121,6 +120,7 @@ class ToolExecutor:
|
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
if result.masked_fields:
|
|
123
|
+
response["masked_fields"] = result.masked_fields
|
|
124
124
|
response["note"] = f"Fields {result.masked_fields} were masked per data policy."
|
|
125
125
|
|
|
126
126
|
return response
|
|
@@ -158,12 +158,17 @@ class ToolExecutor:
|
|
|
158
158
|
|
|
159
159
|
if isinstance(exc, DeniedError):
|
|
160
160
|
policy_id = getattr(exc, "policy_id", None)
|
|
161
|
+
reason = getattr(exc, "reason", None) or exc.message
|
|
161
162
|
extra = f" (policy_id={policy_id})" if policy_id else ""
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
"
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
172
|
|
|
168
173
|
if isinstance(exc, ApprovalRequiredError):
|
|
169
174
|
return {
|
|
@@ -131,19 +131,64 @@ def validate_aggregates(value: Optional[Any]) -> Optional[list]:
|
|
|
131
131
|
return value
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
# Units a group_by time bucket can use. The control plane checks the field is
|
|
135
|
+
# a timestamp or date column and that the time zone exists.
|
|
136
|
+
TIME_BUCKETS = ("day", "week", "month", "quarter", "year")
|
|
137
|
+
TIME_BUCKET_KEYS = {"field", "bucket", "alias", "timezone"}
|
|
138
|
+
|
|
139
|
+
|
|
134
140
|
def validate_group_by(value: Optional[Any]) -> Optional[list]:
|
|
135
141
|
if value is None:
|
|
136
142
|
return None
|
|
137
143
|
|
|
138
|
-
if not isinstance(value, list)
|
|
144
|
+
if not isinstance(value, list):
|
|
139
145
|
raise ValidationError(
|
|
140
|
-
"group_by must be a list of field names",
|
|
146
|
+
"group_by must be a list of field names or time buckets",
|
|
141
147
|
context=ErrorContext(raw=value),
|
|
142
148
|
)
|
|
143
149
|
|
|
150
|
+
for entry in value:
|
|
151
|
+
if isinstance(entry, dict):
|
|
152
|
+
_validate_time_bucket(entry)
|
|
153
|
+
elif not (isinstance(entry, str) and entry.strip()):
|
|
154
|
+
raise ValidationError(
|
|
155
|
+
"group_by must be a list of field names or time buckets",
|
|
156
|
+
context=ErrorContext(raw=value),
|
|
157
|
+
)
|
|
158
|
+
|
|
144
159
|
return value
|
|
145
160
|
|
|
146
161
|
|
|
162
|
+
def _validate_time_bucket(entry: Dict[str, Any]) -> None:
|
|
163
|
+
unknown = set(entry) - TIME_BUCKET_KEYS
|
|
164
|
+
if unknown:
|
|
165
|
+
raise ValidationError(
|
|
166
|
+
f"group_by time bucket has unknown keys: {', '.join(sorted(unknown))}",
|
|
167
|
+
context=ErrorContext(raw=entry),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
field = entry.get("field")
|
|
171
|
+
if not isinstance(field, str) or not field.strip():
|
|
172
|
+
raise ValidationError(
|
|
173
|
+
"group_by time bucket needs a 'field'",
|
|
174
|
+
context=ErrorContext(raw=entry),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
bucket = entry.get("bucket")
|
|
178
|
+
if not isinstance(bucket, str) or bucket.lower() not in TIME_BUCKETS:
|
|
179
|
+
raise ValidationError(
|
|
180
|
+
f"group_by time bucket must be one of {', '.join(TIME_BUCKETS)}",
|
|
181
|
+
context=ErrorContext(raw=entry),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
for key in ("alias", "timezone"):
|
|
185
|
+
if key in entry and not (isinstance(entry[key], str) and entry[key].strip()):
|
|
186
|
+
raise ValidationError(
|
|
187
|
+
f"group_by time bucket '{key}' must be a non-empty string",
|
|
188
|
+
context=ErrorContext(raw=entry),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
147
192
|
def validate_having(value: Optional[Any]) -> Optional[list]:
|
|
148
193
|
return _require_list_of_dicts("having", value)
|
|
149
194
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vaultkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -93,6 +93,24 @@ result = client.execute(
|
|
|
93
93
|
|
|
94
94
|
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
95
95
|
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
96
|
+
An entry can also be a time bucket of a timestamp or date field, which groups by period
|
|
97
|
+
(`day`, `week`, `month`, `quarter` or `year`; `alias` and `timezone` are optional, and
|
|
98
|
+
the time zone defaults to UTC). Use one bucketed query for a trend instead of one query
|
|
99
|
+
per period:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
client.execute(
|
|
103
|
+
dataset="payments",
|
|
104
|
+
aggregates=[{"func": "count", "field": "*", "alias": "failures"}],
|
|
105
|
+
filters=[{"field": "status", "operator": "eq", "value": "failed"}],
|
|
106
|
+
group_by=[{"field": "created_at", "bucket": "quarter", "alias": "quarter"}],
|
|
107
|
+
order_by={"column": "quarter"},
|
|
108
|
+
purpose="Failed payments per quarter",
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Bucketing a fully or partially masked field is denied, like grouping by it. Needs a
|
|
113
|
+
control plane with time buckets (vaultkit#106); older ones reject the request.
|
|
96
114
|
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
97
115
|
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
98
116
|
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
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
|