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.
Files changed (41) hide show
  1. {vaultkit-0.2.0 → vaultkit-0.3.1}/PKG-INFO +32 -1
  2. {vaultkit-0.2.0 → vaultkit-0.3.1}/README.md +31 -0
  3. {vaultkit-0.2.0 → vaultkit-0.3.1}/pyproject.toml +1 -1
  4. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/__init__.py +1 -1
  5. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/client.py +43 -0
  6. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/core/http.py +1 -0
  7. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/errors/exceptions.py +6 -1
  8. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/fetch_result.py +12 -1
  9. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/definitions.py +67 -0
  10. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/executor.py +15 -6
  11. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/utils/validation.py +74 -0
  12. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/PKG-INFO +32 -1
  13. {vaultkit-0.2.0 → vaultkit-0.3.1}/setup.cfg +0 -0
  14. {vaultkit-0.2.0 → vaultkit-0.3.1}/tests/test_fetch.py +0 -0
  15. {vaultkit-0.2.0 → vaultkit-0.3.1}/tests/test_query.py +0 -0
  16. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/core/__init__.py +0 -0
  17. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/core/polling.py +0 -0
  18. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/errors/__init__.py +0 -0
  19. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/errors/base.py +0 -0
  20. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/__init__.py +0 -0
  21. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/__main__.py +0 -0
  22. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/identity.py +0 -0
  23. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/mcp/server.py +0 -0
  24. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/__init__.py +0 -0
  25. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/dataset_info.py +0 -0
  26. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/dataset_schema.py +0 -0
  27. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/models/query_result.py +0 -0
  28. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/__init__.py +0 -0
  29. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/__init__.py +0 -0
  30. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/anthropic.py +0 -0
  31. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/mcp.py +0 -0
  32. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/adapters/openai.py +0 -0
  33. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/builder.py +0 -0
  34. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/tools/schemas.py +0 -0
  35. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/utils/__init__.py +0 -0
  36. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit/utils/retry.py +0 -0
  37. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/SOURCES.txt +0 -0
  38. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/dependency_links.txt +0 -0
  39. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/entry_points.txt +0 -0
  40. {vaultkit-0.2.0 → vaultkit-0.3.1}/vaultkit.egg-info/requires.txt +0 -0
  41. {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.2.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.2.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" }
@@ -53,7 +53,7 @@ from .models import (
53
53
  # Optional (nice DX improvement)
54
54
  from .tools import ToolBuilder, ToolExecutor
55
55
 
56
- __version__ = "0.1.0"
56
+ __version__ = "0.3.1"
57
57
 
58
58
  __all__ = [
59
59
  "VaultKitClient",
@@ -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(
@@ -166,6 +166,7 @@ class HttpClient:
166
166
  raw.get("error")
167
167
  or raw.get("message")
168
168
  or raw.get("detail")
169
+ or raw.get("reason")
169
170
  or msg
170
171
  )
171
172
  except Exception:
@@ -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
- if error_type == "policy_denied":
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, if your API returns them there.
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 self._error(
159
- f"Access denied by VaultKit policy{extra}. Reason: {msg}. "
160
- "Do not retry — this denial is deterministic.",
161
- code="denied",
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.2.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