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 +85 -0
- vaultkit/client.py +441 -0
- vaultkit/core/__init__.py +1 -0
- vaultkit/core/http.py +190 -0
- vaultkit/core/polling.py +140 -0
- vaultkit/errors/__init__.py +36 -0
- vaultkit/errors/base.py +30 -0
- vaultkit/errors/exceptions.py +211 -0
- vaultkit/models/__init__.py +11 -0
- vaultkit/models/dataset_info.py +25 -0
- vaultkit/models/dataset_schema.py +75 -0
- vaultkit/models/fetch_result.py +53 -0
- vaultkit/models/query_result.py +73 -0
- vaultkit/tools/__init__.py +5 -0
- vaultkit/tools/adapters/__init__.py +12 -0
- vaultkit/tools/adapters/anthropic.py +17 -0
- vaultkit/tools/adapters/openai.py +23 -0
- vaultkit/tools/builder.py +128 -0
- vaultkit/tools/definitions.py +177 -0
- vaultkit/tools/executor.py +199 -0
- vaultkit/tools/schemas.py +39 -0
- vaultkit/utils/__init__.py +1 -0
- vaultkit/utils/retry.py +81 -0
- vaultkit/utils/validation.py +87 -0
- vaultkit-0.1.0.dist-info/METADATA +207 -0
- vaultkit-0.1.0.dist-info/RECORD +28 -0
- vaultkit-0.1.0.dist-info/WHEEL +5 -0
- vaultkit-0.1.0.dist-info/top_level.txt +1 -0
vaultkit/core/http.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Optional, Callable
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from vaultkit.errors.base import ErrorContext
|
|
10
|
+
from vaultkit.errors.exceptions import map_http_error, TransportError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class HttpConfig:
|
|
15
|
+
timeout_s: float = 15.0
|
|
16
|
+
user_agent: str = "vaultkit-python/1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HttpClient:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
base_url: str,
|
|
24
|
+
token: str,
|
|
25
|
+
config: Optional[HttpConfig] = None,
|
|
26
|
+
retry_fn: Optional[Callable[[Callable[[], httpx.Response]], httpx.Response]] = None,
|
|
27
|
+
logger: Optional[logging.Logger] = None,
|
|
28
|
+
):
|
|
29
|
+
self.base_url = base_url.rstrip("/")
|
|
30
|
+
self.token = token
|
|
31
|
+
self.config = config or HttpConfig()
|
|
32
|
+
self._client = httpx.Client(timeout=self.config.timeout_s)
|
|
33
|
+
self._retry_fn = retry_fn # optional retry wrapper
|
|
34
|
+
self._logger = logger
|
|
35
|
+
|
|
36
|
+
self._default_headers = {
|
|
37
|
+
"Authorization": f"Bearer {self.token}",
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"Accept": "application/json",
|
|
40
|
+
"User-Agent": self.config.user_agent,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def close(self) -> None:
|
|
44
|
+
self._client.close()
|
|
45
|
+
|
|
46
|
+
# public API
|
|
47
|
+
|
|
48
|
+
def get(
|
|
49
|
+
self,
|
|
50
|
+
path: str,
|
|
51
|
+
*,
|
|
52
|
+
params: Optional[Dict[str, Any]] = None,
|
|
53
|
+
) -> Dict[str, Any]:
|
|
54
|
+
return self._request("GET", path, params=params)
|
|
55
|
+
|
|
56
|
+
def post(
|
|
57
|
+
self,
|
|
58
|
+
path: str,
|
|
59
|
+
*,
|
|
60
|
+
json_body: Dict[str, Any],
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
return self._request("POST", path, json=json_body)
|
|
63
|
+
|
|
64
|
+
# internal
|
|
65
|
+
|
|
66
|
+
def _request(
|
|
67
|
+
self,
|
|
68
|
+
method: str,
|
|
69
|
+
path: str,
|
|
70
|
+
*,
|
|
71
|
+
params: Optional[Dict[str, Any]] = None,
|
|
72
|
+
json: Optional[Dict[str, Any]] = None,
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
url = f"{self.base_url}/{path.lstrip('/')}"
|
|
75
|
+
|
|
76
|
+
if self._logger:
|
|
77
|
+
self._logger.debug(
|
|
78
|
+
"[VaultKit] HTTP request",
|
|
79
|
+
extra={
|
|
80
|
+
"method": method,
|
|
81
|
+
"path": path,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def send() -> httpx.Response:
|
|
86
|
+
return self._client.request(
|
|
87
|
+
method,
|
|
88
|
+
url,
|
|
89
|
+
headers=self._default_headers,
|
|
90
|
+
params=params,
|
|
91
|
+
json=json,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
res = self._retry_fn(send) if self._retry_fn else send()
|
|
96
|
+
return self._handle(res, method=method, path=path)
|
|
97
|
+
except httpx.RequestError as e:
|
|
98
|
+
if self._logger:
|
|
99
|
+
self._logger.error(
|
|
100
|
+
"[VaultKit] Network error",
|
|
101
|
+
extra={
|
|
102
|
+
"method": method,
|
|
103
|
+
"path": path,
|
|
104
|
+
"error": str(e),
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
raise TransportError(
|
|
109
|
+
f"Network error: {e}",
|
|
110
|
+
context=ErrorContext(raw=str(e)),
|
|
111
|
+
) from e
|
|
112
|
+
|
|
113
|
+
def _handle(self, res: httpx.Response, *, method: str, path: str) -> Dict[str, Any]:
|
|
114
|
+
correlation_id = (
|
|
115
|
+
res.headers.get("x-correlation-id")
|
|
116
|
+
or res.headers.get("correlation_id")
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if self._logger:
|
|
120
|
+
self._logger.debug(
|
|
121
|
+
"[VaultKit] HTTP response",
|
|
122
|
+
extra={
|
|
123
|
+
"method": method,
|
|
124
|
+
"path": path,
|
|
125
|
+
"status_code": res.status_code,
|
|
126
|
+
"correlation_id": correlation_id,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# success
|
|
131
|
+
if 200 <= res.status_code < 300:
|
|
132
|
+
if not res.content:
|
|
133
|
+
return {}
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
data = res.json()
|
|
137
|
+
except Exception as e:
|
|
138
|
+
if self._logger:
|
|
139
|
+
self._logger.error(
|
|
140
|
+
"[VaultKit] Invalid JSON response",
|
|
141
|
+
extra={
|
|
142
|
+
"status_code": res.status_code,
|
|
143
|
+
"correlation_id": correlation_id,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
raise TransportError(
|
|
148
|
+
"Invalid JSON response from VaultKit",
|
|
149
|
+
context=ErrorContext(
|
|
150
|
+
status_code=res.status_code,
|
|
151
|
+
correlation_id=correlation_id,
|
|
152
|
+
raw=res.text,
|
|
153
|
+
),
|
|
154
|
+
) from e
|
|
155
|
+
|
|
156
|
+
return data if isinstance(data, dict) else {"data": data}
|
|
157
|
+
|
|
158
|
+
# error
|
|
159
|
+
raw: Any = None
|
|
160
|
+
msg = f"VaultKit API error ({res.status_code})"
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
raw = res.json()
|
|
164
|
+
if isinstance(raw, dict):
|
|
165
|
+
msg = (
|
|
166
|
+
raw.get("error")
|
|
167
|
+
or raw.get("message")
|
|
168
|
+
or raw.get("detail")
|
|
169
|
+
or msg
|
|
170
|
+
)
|
|
171
|
+
except Exception:
|
|
172
|
+
raw = res.text
|
|
173
|
+
|
|
174
|
+
if self._logger:
|
|
175
|
+
self._logger.error(
|
|
176
|
+
"[VaultKit] API error",
|
|
177
|
+
extra={
|
|
178
|
+
"status_code": res.status_code,
|
|
179
|
+
"correlation_id": correlation_id,
|
|
180
|
+
"message": msg,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
ctx = ErrorContext(
|
|
185
|
+
status_code=res.status_code,
|
|
186
|
+
correlation_id=correlation_id,
|
|
187
|
+
raw=raw,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
raise map_http_error(res.status_code, msg, context=ctx)
|
vaultkit/core/polling.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import random
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
from vaultkit.errors.exceptions import QueuedError, PollTimeoutError, NotFoundError
|
|
10
|
+
from vaultkit.models.query_result import QueryResult
|
|
11
|
+
|
|
12
|
+
_TERMINAL_STATUSES = {"granted", "denied", "pending_approval"}
|
|
13
|
+
_APPROVAL_STATUSES = {"queued", "pending_approval"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PollConfig:
|
|
18
|
+
interval_s: float = 2.0
|
|
19
|
+
timeout_s: float = 60.0
|
|
20
|
+
|
|
21
|
+
# Approval-specific backoff (long waits expected)
|
|
22
|
+
approval_backoff_factor: float = 1.5
|
|
23
|
+
approval_max_interval_s: float = 15.0
|
|
24
|
+
|
|
25
|
+
# Jitter percentage (±10% by default)
|
|
26
|
+
jitter_ratio: float = 0.1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def poll_until_done(
|
|
30
|
+
*,
|
|
31
|
+
initial: QueryResult,
|
|
32
|
+
poll_fn: Callable[[str], QueryResult],
|
|
33
|
+
config: PollConfig,
|
|
34
|
+
logger: Optional[logging.Logger] = None,
|
|
35
|
+
) -> QueryResult:
|
|
36
|
+
# If already in a terminal state, return immediately
|
|
37
|
+
if initial.status in _TERMINAL_STATUSES:
|
|
38
|
+
if logger:
|
|
39
|
+
logger.debug(
|
|
40
|
+
"[VaultKit] Poll skipped (already terminal)",
|
|
41
|
+
extra={"status": initial.status, "request_id": initial.request_id},
|
|
42
|
+
)
|
|
43
|
+
return initial
|
|
44
|
+
|
|
45
|
+
if not initial.request_id:
|
|
46
|
+
raise QueuedError(
|
|
47
|
+
"Request is queued but no request_id was provided; cannot poll safely."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
request_id = initial.request_id
|
|
51
|
+
deadline = time.time() + config.timeout_s
|
|
52
|
+
|
|
53
|
+
current = initial
|
|
54
|
+
interval = config.interval_s
|
|
55
|
+
attempt = 0
|
|
56
|
+
|
|
57
|
+
while time.time() < deadline:
|
|
58
|
+
attempt += 1
|
|
59
|
+
|
|
60
|
+
# Apply jitter to avoid thundering herd issues when many requests are queued
|
|
61
|
+
jitter = random.uniform(
|
|
62
|
+
-interval * config.jitter_ratio,
|
|
63
|
+
interval * config.jitter_ratio,
|
|
64
|
+
)
|
|
65
|
+
sleep_time = max(0.0, interval + jitter)
|
|
66
|
+
|
|
67
|
+
# Ensure we don't sleep past the deadline
|
|
68
|
+
remaining = deadline - time.time()
|
|
69
|
+
if remaining <= 0:
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
if logger:
|
|
73
|
+
logger.debug(
|
|
74
|
+
"[VaultKit] Polling request",
|
|
75
|
+
extra={
|
|
76
|
+
"request_id": request_id,
|
|
77
|
+
"attempt": attempt,
|
|
78
|
+
"interval": round(interval, 2),
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
time.sleep(min(sleep_time, remaining))
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
current = poll_fn(request_id)
|
|
86
|
+
except NotFoundError:
|
|
87
|
+
# Request disappeared (expired / deleted / invalid)
|
|
88
|
+
if logger:
|
|
89
|
+
logger.error(
|
|
90
|
+
"[VaultKit] Request not found during polling",
|
|
91
|
+
extra={"request_id": request_id},
|
|
92
|
+
)
|
|
93
|
+
raise PollTimeoutError(
|
|
94
|
+
"Request not found while polling",
|
|
95
|
+
request_id=request_id,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if logger:
|
|
99
|
+
logger.debug(
|
|
100
|
+
"[VaultKit] Poll result",
|
|
101
|
+
extra={
|
|
102
|
+
"request_id": request_id,
|
|
103
|
+
"status": current.status,
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# If we've reached a terminal state, return result
|
|
108
|
+
if current.status in _TERMINAL_STATUSES:
|
|
109
|
+
if logger:
|
|
110
|
+
logger.info(
|
|
111
|
+
"[VaultKit] Polling complete",
|
|
112
|
+
extra={
|
|
113
|
+
"request_id": request_id,
|
|
114
|
+
"status": current.status,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
return current
|
|
118
|
+
|
|
119
|
+
# Adjust interval based on status
|
|
120
|
+
if current.status in _APPROVAL_STATUSES:
|
|
121
|
+
# Slow down polling for approvals
|
|
122
|
+
interval = min(
|
|
123
|
+
interval * config.approval_backoff_factor,
|
|
124
|
+
config.approval_max_interval_s,
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
# Reset to default interval for active processing states
|
|
128
|
+
interval = config.interval_s
|
|
129
|
+
|
|
130
|
+
# Timeout reached
|
|
131
|
+
if logger:
|
|
132
|
+
logger.error(
|
|
133
|
+
"[VaultKit] Polling timed out",
|
|
134
|
+
extra={"request_id": request_id},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
raise PollTimeoutError(
|
|
138
|
+
f"Request still queued after {config.timeout_s:.0f}s",
|
|
139
|
+
request_id=request_id,
|
|
140
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .base import VaultKitError
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
ApprovalRequiredError,
|
|
4
|
+
DeniedError,
|
|
5
|
+
GrantExpiredError,
|
|
6
|
+
GrantRevokedError,
|
|
7
|
+
PolicyBundleRevokedError,
|
|
8
|
+
PollTimeoutError,
|
|
9
|
+
QueuedError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
TransportError,
|
|
12
|
+
ServerError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
# Base
|
|
18
|
+
"VaultKitError",
|
|
19
|
+
|
|
20
|
+
# Domain (core VaultKit behavior)
|
|
21
|
+
"DeniedError",
|
|
22
|
+
"ApprovalRequiredError",
|
|
23
|
+
"QueuedError",
|
|
24
|
+
"GrantExpiredError",
|
|
25
|
+
"GrantRevokedError",
|
|
26
|
+
"PolicyBundleRevokedError",
|
|
27
|
+
"PollTimeoutError",
|
|
28
|
+
|
|
29
|
+
# User / validation
|
|
30
|
+
"ValidationError",
|
|
31
|
+
|
|
32
|
+
# Transport / infra
|
|
33
|
+
"TransportError",
|
|
34
|
+
"ServerError",
|
|
35
|
+
"RateLimitError",
|
|
36
|
+
]
|
vaultkit/errors/base.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ErrorContext:
|
|
9
|
+
status_code: Optional[int] = None
|
|
10
|
+
request_id: Optional[str] = None
|
|
11
|
+
correlation_id: Optional[str] = None
|
|
12
|
+
raw: Optional[Any] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VaultKitError(Exception):
|
|
16
|
+
"""Base exception for VaultKit SDK."""
|
|
17
|
+
|
|
18
|
+
error_code: str = "vaultkit_error"
|
|
19
|
+
retryable: bool = False
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, *, context: Optional[ErrorContext] = None):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.message = message
|
|
24
|
+
self.context = context or ErrorContext()
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
base = self.message
|
|
28
|
+
if self.context and self.context.correlation_id:
|
|
29
|
+
return f"{base} (correlation_id={self.context.correlation_id})"
|
|
30
|
+
return base
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Any
|
|
4
|
+
|
|
5
|
+
from .base import VaultKitError, ErrorContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# generic categories
|
|
9
|
+
|
|
10
|
+
class ValidationError(VaultKitError):
|
|
11
|
+
error_code = "validation_error"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthError(VaultKitError):
|
|
15
|
+
error_code = "auth_error"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ForbiddenError(VaultKitError):
|
|
19
|
+
error_code = "forbidden"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotFoundError(VaultKitError):
|
|
23
|
+
error_code = "not_found"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConflictError(VaultKitError):
|
|
27
|
+
error_code = "conflict"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RateLimitError(VaultKitError):
|
|
31
|
+
error_code = "rate_limited"
|
|
32
|
+
retryable = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TransportError(VaultKitError):
|
|
36
|
+
error_code = "transport_error"
|
|
37
|
+
retryable = True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ServerError(VaultKitError):
|
|
41
|
+
error_code = "server_error"
|
|
42
|
+
retryable = True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ClientError(VaultKitError):
|
|
46
|
+
error_code = "client_error"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# domain-specific (VaultKit)
|
|
50
|
+
|
|
51
|
+
class DeniedError(VaultKitError):
|
|
52
|
+
"""
|
|
53
|
+
Raised when VaultKit policy denies a request.
|
|
54
|
+
Do not retry — denial is deterministic.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
error_code = "policy_denied"
|
|
58
|
+
retryable = False
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
message: str,
|
|
63
|
+
*,
|
|
64
|
+
policy_id: Optional[str] = None,
|
|
65
|
+
context: Optional[ErrorContext] = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
super().__init__(message, context=context)
|
|
68
|
+
self.policy_id = policy_id
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class QueuedError(VaultKitError):
|
|
72
|
+
"""
|
|
73
|
+
Raised when a request is queued and the caller chose not to poll.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
error_code = "queued"
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
message: str,
|
|
81
|
+
*,
|
|
82
|
+
request_id: Optional[str] = None,
|
|
83
|
+
context: Optional[ErrorContext] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
super().__init__(message, context=context)
|
|
86
|
+
self.request_id = request_id
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ApprovalRequiredError(VaultKitError):
|
|
90
|
+
"""
|
|
91
|
+
Raised when a request reaches pending_approval after polling.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
error_code = "approval_required"
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
message: str,
|
|
99
|
+
*,
|
|
100
|
+
request_id: Optional[str] = None,
|
|
101
|
+
context: Optional[ErrorContext] = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
super().__init__(message, context=context)
|
|
104
|
+
self.request_id = request_id
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class GrantExpiredError(VaultKitError):
|
|
108
|
+
error_code = "grant_expired"
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
message: str,
|
|
113
|
+
*,
|
|
114
|
+
grant_ref: Optional[str] = None,
|
|
115
|
+
context: Optional[ErrorContext] = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
super().__init__(message, context=context)
|
|
118
|
+
self.grant_ref = grant_ref
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class GrantRevokedError(VaultKitError):
|
|
122
|
+
error_code = "grant_revoked"
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
message: str,
|
|
127
|
+
*,
|
|
128
|
+
grant_ref: Optional[str] = None,
|
|
129
|
+
context: Optional[ErrorContext] = None,
|
|
130
|
+
) -> None:
|
|
131
|
+
super().__init__(message, context=context)
|
|
132
|
+
self.grant_ref = grant_ref
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PolicyBundleRevokedError(VaultKitError):
|
|
136
|
+
"""
|
|
137
|
+
Unrecoverable — escalate to a VaultKit administrator.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
error_code = "policy_bundle_revoked"
|
|
141
|
+
retryable = False
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
message: str,
|
|
146
|
+
*,
|
|
147
|
+
bundle_checksum: Optional[str] = None,
|
|
148
|
+
context: Optional[ErrorContext] = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
super().__init__(message, context=context)
|
|
151
|
+
self.bundle_checksum = bundle_checksum
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class PollTimeoutError(VaultKitError):
|
|
155
|
+
"""
|
|
156
|
+
Polling exceeded timeout. Request may still be processing.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
error_code = "poll_timeout"
|
|
160
|
+
retryable = True # caller can retry polling
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
message: str,
|
|
165
|
+
*,
|
|
166
|
+
request_id: Optional[str] = None,
|
|
167
|
+
context: Optional[ErrorContext] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
super().__init__(message, context=context)
|
|
170
|
+
self.request_id = request_id
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# HTTP mapping
|
|
174
|
+
|
|
175
|
+
def map_http_error(status_code: int, message: str, *, context: ErrorContext) -> VaultKitError:
|
|
176
|
+
raw = context.raw if isinstance(context.raw, dict) else {}
|
|
177
|
+
|
|
178
|
+
# future-proof: allow backend to send structured error types
|
|
179
|
+
error_type = raw.get("type") if isinstance(raw, dict) else None
|
|
180
|
+
|
|
181
|
+
if status_code == 400:
|
|
182
|
+
return ValidationError(message, context=context)
|
|
183
|
+
|
|
184
|
+
if status_code == 401:
|
|
185
|
+
return AuthError(message, context=context)
|
|
186
|
+
|
|
187
|
+
if status_code == 403:
|
|
188
|
+
if error_type == "policy_denied":
|
|
189
|
+
return DeniedError(
|
|
190
|
+
message,
|
|
191
|
+
policy_id=raw.get("policy_id"),
|
|
192
|
+
context=context,
|
|
193
|
+
)
|
|
194
|
+
return ForbiddenError(message, context=context)
|
|
195
|
+
|
|
196
|
+
if status_code == 404:
|
|
197
|
+
return NotFoundError(message, context=context)
|
|
198
|
+
|
|
199
|
+
if status_code == 409:
|
|
200
|
+
return ConflictError(message, context=context)
|
|
201
|
+
|
|
202
|
+
if status_code == 429:
|
|
203
|
+
return RateLimitError(message, context=context)
|
|
204
|
+
|
|
205
|
+
if 400 <= status_code < 500:
|
|
206
|
+
return ClientError(message, context=context)
|
|
207
|
+
|
|
208
|
+
if 500 <= status_code:
|
|
209
|
+
return ServerError(message, context=context)
|
|
210
|
+
|
|
211
|
+
return VaultKitError(message, context=context)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class DatasetInfo:
|
|
7
|
+
dataset: str
|
|
8
|
+
datasource: str
|
|
9
|
+
visibility: Optional[str] = None
|
|
10
|
+
correlation_id: Optional[str] = None
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_dict(cls, data):
|
|
14
|
+
dataset = data.get("name") or data.get("dataset")
|
|
15
|
+
datasource = data.get("datasource")
|
|
16
|
+
|
|
17
|
+
if not dataset or not datasource:
|
|
18
|
+
raise ValueError("DatasetInfo requires 'name'/'dataset' and 'datasource'")
|
|
19
|
+
|
|
20
|
+
return cls(
|
|
21
|
+
dataset=dataset,
|
|
22
|
+
datasource=datasource,
|
|
23
|
+
visibility=data.get("visibility"),
|
|
24
|
+
correlation_id=data.get("correlation_id"),
|
|
25
|
+
)
|