vaultkit 0.3.1__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.
Files changed (41) hide show
  1. {vaultkit-0.3.1 → vaultkit-0.4.0}/PKG-INFO +19 -1
  2. {vaultkit-0.3.1 → vaultkit-0.4.0}/README.md +18 -0
  3. {vaultkit-0.3.1 → vaultkit-0.4.0}/pyproject.toml +1 -1
  4. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/__init__.py +1 -1
  5. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/client.py +8 -3
  6. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/definitions.py +34 -4
  7. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/utils/validation.py +47 -2
  8. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit.egg-info/PKG-INFO +19 -1
  9. {vaultkit-0.3.1 → vaultkit-0.4.0}/setup.cfg +0 -0
  10. {vaultkit-0.3.1 → vaultkit-0.4.0}/tests/test_fetch.py +0 -0
  11. {vaultkit-0.3.1 → vaultkit-0.4.0}/tests/test_query.py +0 -0
  12. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/core/__init__.py +0 -0
  13. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/core/http.py +0 -0
  14. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/core/polling.py +0 -0
  15. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/errors/__init__.py +0 -0
  16. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/errors/base.py +0 -0
  17. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/errors/exceptions.py +0 -0
  18. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/mcp/__init__.py +0 -0
  19. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/mcp/__main__.py +0 -0
  20. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/mcp/identity.py +0 -0
  21. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/mcp/server.py +0 -0
  22. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/models/__init__.py +0 -0
  23. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/models/dataset_info.py +0 -0
  24. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/models/dataset_schema.py +0 -0
  25. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/models/fetch_result.py +0 -0
  26. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/models/query_result.py +0 -0
  27. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/__init__.py +0 -0
  28. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/adapters/__init__.py +0 -0
  29. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/adapters/anthropic.py +0 -0
  30. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/adapters/mcp.py +0 -0
  31. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/adapters/openai.py +0 -0
  32. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/builder.py +0 -0
  33. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/executor.py +0 -0
  34. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/tools/schemas.py +0 -0
  35. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/utils/__init__.py +0 -0
  36. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit/utils/retry.py +0 -0
  37. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit.egg-info/SOURCES.txt +0 -0
  38. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit.egg-info/dependency_links.txt +0 -0
  39. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit.egg-info/entry_points.txt +0 -0
  40. {vaultkit-0.3.1 → vaultkit-0.4.0}/vaultkit.egg-info/requires.txt +0 -0
  41. {vaultkit-0.3.1 → 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.1
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.3.1"
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" }
@@ -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.3.1"
56
+ __version__ = "0.4.0"
57
57
 
58
58
  __all__ = [
59
59
  "VaultKitClient",
@@ -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"}
@@ -240,7 +245,7 @@ class VaultKitClient:
240
245
  filters: Optional[List[Dict[str, Any]]] = None,
241
246
  limit: Optional[int] = None,
242
247
  aggregates: Optional[List[Dict[str, Any]]] = None,
243
- group_by: Optional[List[str]] = None,
248
+ group_by: Optional[List[Union[str, Dict[str, str]]]] = None,
244
249
  having: Optional[List[Dict[str, Any]]] = None,
245
250
  order_by: Optional[Dict[str, Any]] = None,
246
251
  purpose: Optional[str] = None,
@@ -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. Optional: defaults to 'fields'. If given, it must include "
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
  },
@@ -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) or not all(isinstance(v, str) and v.strip() for v in value):
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.1
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