vaultkit 0.1.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.
vaultkit/__init__.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ VaultKit Python SDK
3
+
4
+ Quick start:
5
+
6
+ from vaultkit import VaultKitClient
7
+
8
+ with VaultKitClient(
9
+ base_url="https://vaultkit.yourorg.com",
10
+ token="your-jwt-token",
11
+ org="your-org-id",
12
+ ) as client:
13
+
14
+ # High-level: full lifecycle, grants invisible
15
+ result = client.execute(
16
+ dataset="customers",
17
+ fields=["id", "email", "revenue"],
18
+ filters=[{"field": "revenue", "operator": "gt", "value": 10000}],
19
+ purpose="Q4 revenue analysis",
20
+ )
21
+
22
+ # For AI agents — scoped tool schemas from the live registry:
23
+ from vaultkit.tools import ToolBuilder, ToolExecutor
24
+
25
+ tools = ToolBuilder(client).build()
26
+ executor = ToolExecutor(client)
27
+ """
28
+
29
+ from .client import ClientConfig, VaultKitClient
30
+
31
+ from .errors import (
32
+ ApprovalRequiredError,
33
+ DeniedError,
34
+ GrantExpiredError,
35
+ GrantRevokedError,
36
+ PolicyBundleRevokedError,
37
+ PollTimeoutError,
38
+ QueuedError,
39
+ ValidationError,
40
+ VaultKitError,
41
+ TransportError,
42
+ ServerError,
43
+ RateLimitError,
44
+ )
45
+
46
+ from .models import (
47
+ DatasetInfo,
48
+ DatasetSchema,
49
+ FetchResult,
50
+ QueryResult,
51
+ )
52
+
53
+ # Optional (nice DX improvement)
54
+ from .tools import ToolBuilder, ToolExecutor
55
+
56
+ __version__ = "0.1.0"
57
+
58
+ __all__ = [
59
+ "VaultKitClient",
60
+ "ClientConfig",
61
+
62
+ # Models
63
+ "QueryResult",
64
+ "FetchResult",
65
+ "DatasetInfo",
66
+ "DatasetSchema",
67
+
68
+ # Errors
69
+ "VaultKitError",
70
+ "DeniedError",
71
+ "ApprovalRequiredError",
72
+ "QueuedError",
73
+ "GrantExpiredError",
74
+ "GrantRevokedError",
75
+ "PolicyBundleRevokedError",
76
+ "ValidationError",
77
+ "PollTimeoutError",
78
+ "TransportError",
79
+ "ServerError",
80
+ "RateLimitError",
81
+
82
+ # Tools
83
+ "ToolBuilder",
84
+ "ToolExecutor",
85
+ ]
vaultkit/client.py ADDED
@@ -0,0 +1,441 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from vaultkit.core.http import HttpClient, HttpConfig
8
+ from vaultkit.core.polling import PollConfig, poll_until_done
9
+ from vaultkit.errors.exceptions import (
10
+ ApprovalRequiredError,
11
+ DeniedError,
12
+ QueuedError,
13
+ ValidationError,
14
+ )
15
+ from vaultkit.models.dataset_info import DatasetInfo
16
+ from vaultkit.models.dataset_schema import DatasetSchema
17
+ from vaultkit.models.fetch_result import FetchResult
18
+ from vaultkit.models.query_result import QueryResult
19
+ from vaultkit.utils.retry import RetryConfig, with_retries
20
+ from vaultkit.utils.validation import (
21
+ require_str,
22
+ validate_filters,
23
+ validate_limit,
24
+ )
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ClientConfig:
29
+ # Use field(default_factory=...) to avoid Python mutable-default error
30
+ http: HttpConfig = field(default_factory=HttpConfig)
31
+ retries: RetryConfig = field(default_factory=RetryConfig)
32
+ polling: PollConfig = field(default_factory=PollConfig)
33
+
34
+
35
+ class VaultKitClient:
36
+ """
37
+ VaultKit Python SDK.
38
+
39
+ Two levels of abstraction:
40
+
41
+ High-level (recommended for agents):
42
+ execute() Full lifecycle: intent → poll → fetch → data.
43
+ Grants are invisible. Agent gets data or a typed exception.
44
+
45
+ Low-level (for custom orchestration):
46
+ query() Submit intent, get QueryResult (granted/queued/denied).
47
+ fetch() Redeem a grant_ref for data.
48
+ poll() Block until queued QueryResult reaches terminal state.
49
+ poll_request() Poll by request_id (used by check_approval flows).
50
+
51
+ Discovery:
52
+ datasets() List authorized datasets from the registry.
53
+ schema() Get field-level schema for a dataset.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ base_url: str,
60
+ token: str,
61
+ org: str,
62
+ config: Optional[ClientConfig] = None,
63
+ logger: Optional[logging.Logger] = None,
64
+ ) -> None:
65
+ self.base_url = require_str("base_url", base_url)
66
+ self.token = require_str("token", token)
67
+ self.org = require_str("org", org)
68
+ self.config = config or ClientConfig()
69
+ self._logger = logger
70
+
71
+ self._http = HttpClient(
72
+ base_url=self.base_url,
73
+ token=self.token,
74
+ config=self.config.http,
75
+ logger=self._logger,
76
+ )
77
+
78
+ def close(self) -> None:
79
+ self._http.close()
80
+
81
+ def __enter__(self) -> "VaultKitClient":
82
+ return self
83
+
84
+ def __exit__(self, *args: Any) -> None:
85
+ self.close()
86
+
87
+ # Internal helpers
88
+
89
+ def _path(self, suffix: str) -> str:
90
+ return f"/api/v1/orgs/{self.org}{suffix}"
91
+
92
+ # High-level
93
+
94
+ def execute(
95
+ self,
96
+ *,
97
+ dataset: str,
98
+ fields: Optional[List[str]] = None,
99
+ filters: Optional[List[Dict[str, Any]]] = None,
100
+ limit: Optional[int] = None,
101
+ purpose: Optional[str] = None,
102
+ requester_region: Optional[str] = None,
103
+ requester_clearance: Optional[str] = None,
104
+ poll_config: Optional[PollConfig] = None,
105
+ ) -> FetchResult:
106
+ """
107
+ Submit an intent request and return data — full grant lifecycle internal.
108
+
109
+ Raises:
110
+ DeniedError Policy rejected. Do not retry.
111
+ ApprovalRequiredError Needs human approval (request_id attached).
112
+ PollTimeoutError Approval polling exceeded configured timeout.
113
+ GrantExpiredError / GrantRevokedError Transient; re-submit.
114
+ PolicyBundleRevokedError Unrecoverable; escalate.
115
+ """
116
+ dataset = require_str("dataset", dataset)
117
+
118
+ if self._logger:
119
+ self._logger.info(
120
+ "[VaultKit] Execute",
121
+ extra={"dataset": dataset},
122
+ )
123
+
124
+ result = self.query(
125
+ dataset=dataset,
126
+ fields=fields,
127
+ filters=filters,
128
+ limit=limit,
129
+ purpose=purpose,
130
+ requester_region=requester_region,
131
+ requester_clearance=requester_clearance,
132
+ poll=True,
133
+ poll_config=poll_config,
134
+ )
135
+
136
+ if result.needs_approval:
137
+ if self._logger:
138
+ self._logger.info(
139
+ "[VaultKit] Approval required",
140
+ extra={
141
+ "dataset": dataset,
142
+ "request_id": result.request_id,
143
+ "approver_role": result.approver_role,
144
+ },
145
+ )
146
+
147
+ raise ApprovalRequiredError(
148
+ f"Dataset '{dataset}' requires human approval. "
149
+ f"Approver role: {result.approver_role or 'unknown'}. "
150
+ f"Use poll_request('{result.request_id}') to check status.",
151
+ request_id=result.request_id,
152
+ )
153
+
154
+ if result.is_denied:
155
+ if self._logger:
156
+ self._logger.info(
157
+ "[VaultKit] Request denied",
158
+ extra={
159
+ "dataset": dataset,
160
+ "request_id": result.request_id,
161
+ "request_id": result.request_id or "none",
162
+ "policy_id": result.policy_id,
163
+ },
164
+ )
165
+
166
+ raise DeniedError(
167
+ result.reason or "Request denied by policy",
168
+ policy_id=result.policy_id,
169
+ )
170
+
171
+ # Case 1: data already returned (reuse path)
172
+ if result.has_data and not result.grant_ref:
173
+ if self._logger:
174
+ self._logger.debug(
175
+ "[VaultKit] Reused request — returning data directly",
176
+ extra={
177
+ "dataset": dataset,
178
+ "request_id": result.request_id,
179
+ },
180
+ )
181
+
182
+ return FetchResult(
183
+ rows=result.rows,
184
+ meta=result.meta,
185
+ correlation_id=result.correlation_id,
186
+ )
187
+
188
+ # Case 2: normal grant flow
189
+ if result.grant_ref:
190
+ return self.fetch(grant_ref=result.grant_ref)
191
+
192
+ raise ValidationError(
193
+ f"Unexpected state after polling: status={result.status}, no grant_ref or data."
194
+ )
195
+
196
+ if self._logger:
197
+ self._logger.debug(
198
+ "[VaultKit] Fetch after grant",
199
+ extra={
200
+ "dataset": dataset,
201
+ "grant_ref": result.grant_ref,
202
+ "request_id": result.request_id,
203
+ },
204
+ )
205
+
206
+ return self.fetch(grant_ref=result.grant_ref)
207
+
208
+ # Low-level primitives
209
+
210
+ def query(
211
+ self,
212
+ *,
213
+ dataset: str,
214
+ fields: Optional[List[str]] = None,
215
+ filters: Optional[List[Dict[str, Any]]] = None,
216
+ limit: Optional[int] = None,
217
+ purpose: Optional[str] = None,
218
+ requester_region: Optional[str] = None,
219
+ requester_clearance: Optional[str] = None,
220
+ poll: bool = False,
221
+ poll_config: Optional[PollConfig] = None,
222
+ ) -> QueryResult:
223
+ dataset = require_str("dataset", dataset)
224
+ limit = validate_limit(limit)
225
+ filters = validate_filters(filters)
226
+
227
+ req: Dict[str, Any] = {"dataset": dataset}
228
+ body: Dict[str, Any] = {"request": req}
229
+
230
+ if fields is not None:
231
+ req["fields"] = list(fields)
232
+ if limit is not None:
233
+ req["limit"] = limit
234
+ if purpose is not None:
235
+ req["purpose"] = str(purpose)
236
+ if filters is not None:
237
+ req["filters"] = filters
238
+ if requester_region is not None:
239
+ body["requester_region"] = requester_region
240
+ if requester_clearance is not None:
241
+ body["requester_clearance"] = requester_clearance
242
+
243
+ if self._logger:
244
+ self._logger.debug(
245
+ "[VaultKit] Submit intent",
246
+ extra={
247
+ "dataset": dataset,
248
+ "poll": poll,
249
+ "has_fields": fields is not None,
250
+ "has_filters": filters is not None,
251
+ "limit": limit,
252
+ },
253
+ )
254
+
255
+ def _do() -> QueryResult:
256
+ data = self._http.post(
257
+ self._path("/intent/requests"),
258
+ json_body=body,
259
+ )
260
+ result = QueryResult.from_dict(data)
261
+
262
+ if self._logger:
263
+ self._logger.debug(
264
+ "[VaultKit] Intent response",
265
+ extra={
266
+ "dataset": dataset,
267
+ "status": result.status,
268
+ "request_id": result.request_id,
269
+ "policy_id": result.policy_id,
270
+ },
271
+ )
272
+
273
+ if result.is_denied:
274
+ raise DeniedError(
275
+ result.reason or "Request denied",
276
+ policy_id=result.policy_id,
277
+ )
278
+ if result.is_pending and not poll:
279
+ raise QueuedError(
280
+ "Request is queued. Set poll=True or call client.poll(result).",
281
+ request_id=result.request_id,
282
+ )
283
+ return result
284
+
285
+ result = with_retries(_do, config=self.config.retries)
286
+
287
+ if poll and result.is_pending:
288
+ result = self.poll(result, config=poll_config)
289
+
290
+ return result
291
+
292
+ def fetch(self, *, grant_ref: str) -> FetchResult:
293
+ grant_ref = require_str("grant_ref", grant_ref)
294
+
295
+ if self._logger:
296
+ self._logger.debug(
297
+ "[VaultKit] Fetch",
298
+ extra={"grant_ref": grant_ref},
299
+ )
300
+
301
+ def _do() -> FetchResult:
302
+ data = self._http.post(
303
+ self._path(f"/grants/{grant_ref}/fetch"),
304
+ json_body={},
305
+ )
306
+
307
+ result = FetchResult.from_dict(data)
308
+
309
+ if self._logger:
310
+ self._logger.info(
311
+ "[VaultKit] Fetch complete",
312
+ extra={"grant_ref": grant_ref, "row_count": result.row_count},
313
+ )
314
+
315
+ return result
316
+
317
+ return with_retries(_do, config=self.config.retries)
318
+
319
+ def poll(
320
+ self,
321
+ result: QueryResult,
322
+ *,
323
+ config: Optional[PollConfig] = None,
324
+ ) -> QueryResult:
325
+ if not result.is_pending:
326
+ return result
327
+
328
+ cfg = config or self.config.polling
329
+
330
+ if self._logger:
331
+ self._logger.debug(
332
+ "[VaultKit] Poll start",
333
+ extra={
334
+ "request_id": result.request_id,
335
+ "status": result.status,
336
+ "timeout_s": cfg.timeout_s,
337
+ },
338
+ )
339
+
340
+ def poll_fn(request_id: str) -> QueryResult:
341
+ def _do() -> QueryResult:
342
+ data = self._http.get(self._path(f"/requests/{request_id}"))
343
+ return QueryResult.from_dict(data)
344
+
345
+ return with_retries(_do, config=self.config.retries)
346
+
347
+ return poll_until_done(
348
+ initial=result,
349
+ poll_fn=poll_fn,
350
+ config=cfg,
351
+ logger=self._logger,
352
+ )
353
+
354
+ def poll_request(self, *, request_id: str) -> QueryResult:
355
+ """Poll by request_id directly — used by the check_approval tool flow."""
356
+ request_id = require_str("request_id", request_id)
357
+
358
+ if self._logger:
359
+ self._logger.debug(
360
+ "[VaultKit] Poll request",
361
+ extra={"request_id": request_id},
362
+ )
363
+
364
+ def _do() -> QueryResult:
365
+ data = self._http.get(self._path(f"/requests/{request_id}"))
366
+ return QueryResult.from_dict(data)
367
+
368
+ return with_retries(_do, config=self.config.retries)
369
+
370
+ # Discovery
371
+
372
+ def datasets(
373
+ self,
374
+ *,
375
+ environment: str = "production",
376
+ requester_region: Optional[str] = None,
377
+ dataset_region: Optional[str] = None,
378
+ ) -> List[DatasetInfo]:
379
+ environment = require_str("environment", environment)
380
+ params: Dict[str, Any] = {"environment": environment}
381
+ if requester_region is not None:
382
+ params["requester_region"] = requester_region
383
+ if dataset_region is not None:
384
+ params["dataset_region"] = dataset_region
385
+
386
+ if self._logger:
387
+ self._logger.debug(
388
+ "[VaultKit] List datasets",
389
+ extra={
390
+ "environment": environment,
391
+ "requester_region": requester_region,
392
+ "dataset_region": dataset_region,
393
+ },
394
+ )
395
+
396
+ def _do() -> List[DatasetInfo]:
397
+ data = self._http.get(self._path("/aql/datasets"), params=params)
398
+ raw = data.get("datasets")
399
+ if not isinstance(raw, list):
400
+ raise ValidationError("Invalid datasets response: expected list")
401
+ return [DatasetInfo.from_dict(d) for d in raw]
402
+
403
+ return with_retries(_do, config=self.config.retries)
404
+
405
+ def schema(
406
+ self,
407
+ dataset: str,
408
+ *,
409
+ environment: str = "production",
410
+ requester_region: Optional[str] = None,
411
+ dataset_region: Optional[str] = None,
412
+ ) -> DatasetSchema:
413
+ dataset = require_str("dataset", dataset)
414
+ environment = require_str("environment", environment)
415
+ params: Dict[str, Any] = {"environment": environment}
416
+ if requester_region is not None:
417
+ params["requester_region"] = requester_region
418
+ if dataset_region is not None:
419
+ params["dataset_region"] = dataset_region
420
+
421
+ if self._logger:
422
+ self._logger.debug(
423
+ "[VaultKit] Get schema",
424
+ extra={
425
+ "dataset": dataset,
426
+ "environment": environment,
427
+ "requester_region": requester_region,
428
+ "dataset_region": dataset_region,
429
+ },
430
+ )
431
+
432
+ def _do() -> DatasetSchema:
433
+ data = self._http.get(
434
+ self._path(f"/aql/datasets/{dataset}/schema"),
435
+ params=params,
436
+ )
437
+ if not isinstance(data, dict):
438
+ raise ValidationError("Invalid schema response")
439
+ return DatasetSchema.from_dict(data)
440
+
441
+ return with_retries(_do, config=self.config.retries)
@@ -0,0 +1 @@
1
+ __all__ = []