wherobots-python-sdk 0.3.0__py3-none-any.whl → 0.5.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.
- wherobots/__init__.py +16 -1
- wherobots/__version__.py +1 -1
- wherobots/api/__init__.py +2 -1
- wherobots/api/base.py +98 -34
- wherobots/api/drive.py +481 -0
- wherobots/api/files.py +11 -10
- wherobots/api/runs.py +8 -8
- wherobots/client.py +34 -22
- wherobots/config.py +15 -4
- wherobots/enums.py +10 -0
- wherobots/exceptions.py +37 -3
- wherobots/models.py +152 -20
- wherobots/sdk.py +91 -0
- wherobots/utils/client_header.py +205 -0
- wherobots/utils/validation.py +1 -1
- {wherobots_python_sdk-0.3.0.dist-info → wherobots_python_sdk-0.5.0.dist-info}/METADATA +105 -19
- wherobots_python_sdk-0.5.0.dist-info/RECORD +23 -0
- {wherobots_python_sdk-0.3.0.dist-info → wherobots_python_sdk-0.5.0.dist-info}/WHEEL +1 -1
- wherobots_python_sdk-0.3.0.dist-info/RECORD +0 -20
- {wherobots_python_sdk-0.3.0.dist-info → wherobots_python_sdk-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {wherobots_python_sdk-0.3.0.dist-info → wherobots_python_sdk-0.5.0.dist-info}/top_level.txt +0 -0
wherobots/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"""Wherobots Python SDK (Jobs API
|
|
1
|
+
"""Wherobots Python SDK (Jobs API and Files surfaces)."""
|
|
2
2
|
|
|
3
3
|
from wherobots.__version__ import __version__
|
|
4
|
+
from wherobots.api.drive import Files, FilesDrive
|
|
4
5
|
from wherobots.api.files import FilesAPI
|
|
5
6
|
from wherobots.client import WherobotsJob
|
|
6
7
|
from wherobots.config import WherobotsConfig
|
|
@@ -9,6 +10,7 @@ from wherobots.enums import (
|
|
|
9
10
|
AppType,
|
|
10
11
|
DependencyFileType,
|
|
11
12
|
DependencyType,
|
|
13
|
+
Drive,
|
|
12
14
|
JobStatus,
|
|
13
15
|
Region,
|
|
14
16
|
Runtime,
|
|
@@ -17,7 +19,10 @@ from wherobots.enums import (
|
|
|
17
19
|
from wherobots.exceptions import (
|
|
18
20
|
WherobotsAPIError,
|
|
19
21
|
WherobotsConfigError,
|
|
22
|
+
WherobotsFileNotFoundError,
|
|
23
|
+
WherobotsFilesNotEnabledError,
|
|
20
24
|
WherobotsJobError,
|
|
25
|
+
WherobotsNotAuthenticatedError,
|
|
21
26
|
WherobotsS3Error,
|
|
22
27
|
WherobotsTimeoutError,
|
|
23
28
|
WherobotsValidationError,
|
|
@@ -25,6 +30,7 @@ from wherobots.exceptions import (
|
|
|
25
30
|
from wherobots.models import (
|
|
26
31
|
CreateRunPayload,
|
|
27
32
|
FileDependency,
|
|
33
|
+
FileEntry,
|
|
28
34
|
KubeAppEvent,
|
|
29
35
|
LogItem,
|
|
30
36
|
LogsResponse,
|
|
@@ -43,6 +49,7 @@ from wherobots.models import (
|
|
|
43
49
|
TemporaryAwsCredentials,
|
|
44
50
|
UtilizationStats,
|
|
45
51
|
)
|
|
52
|
+
from wherobots.sdk import Wherobots
|
|
46
53
|
|
|
47
54
|
# Convenience alias
|
|
48
55
|
Job = WherobotsJob
|
|
@@ -50,6 +57,9 @@ Job = WherobotsJob
|
|
|
50
57
|
__all__ = [
|
|
51
58
|
"__version__",
|
|
52
59
|
# Client
|
|
60
|
+
"Wherobots",
|
|
61
|
+
"Files",
|
|
62
|
+
"FilesDrive",
|
|
53
63
|
"WherobotsJob",
|
|
54
64
|
"Job",
|
|
55
65
|
"FilesAPI",
|
|
@@ -57,6 +67,7 @@ __all__ = [
|
|
|
57
67
|
# Enums
|
|
58
68
|
"Runtime",
|
|
59
69
|
"Region",
|
|
70
|
+
"Drive",
|
|
60
71
|
"JobStatus",
|
|
61
72
|
"AppStatus",
|
|
62
73
|
"AppType",
|
|
@@ -70,6 +81,9 @@ __all__ = [
|
|
|
70
81
|
"WherobotsS3Error",
|
|
71
82
|
"WherobotsConfigError",
|
|
72
83
|
"WherobotsTimeoutError",
|
|
84
|
+
"WherobotsNotAuthenticatedError",
|
|
85
|
+
"WherobotsFilesNotEnabledError",
|
|
86
|
+
"WherobotsFileNotFoundError",
|
|
73
87
|
# Models
|
|
74
88
|
"CreateRunPayload",
|
|
75
89
|
"RunView",
|
|
@@ -90,4 +104,5 @@ __all__ = [
|
|
|
90
104
|
"StorageIntegration",
|
|
91
105
|
"TemporaryAwsCredentials",
|
|
92
106
|
"UtilizationStats",
|
|
107
|
+
"FileEntry",
|
|
93
108
|
]
|
wherobots/__version__.py
CHANGED
wherobots/api/__init__.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Wherobots API layer — public exports."""
|
|
2
2
|
|
|
3
3
|
from wherobots.api.base import BaseClient
|
|
4
|
+
from wherobots.api.drive import Files, FilesDrive
|
|
4
5
|
from wherobots.api.files import FilesAPI
|
|
5
6
|
from wherobots.api.runs import RunsAPI
|
|
6
7
|
|
|
7
|
-
__all__ = ["BaseClient", "FilesAPI", "RunsAPI"]
|
|
8
|
+
__all__ = ["BaseClient", "Files", "FilesAPI", "FilesDrive", "RunsAPI"]
|
wherobots/api/base.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
``BaseClient`` wraps a ``requests.Session`` with:
|
|
4
4
|
* Auth header construction (``X-API-Key``).
|
|
5
|
+
* Client attribution via the advisory ``X-Wherobots-Client`` header,
|
|
6
|
+
set once on the session so every outgoing request carries it.
|
|
5
7
|
* Automatic retry with exponential back-off for transient errors
|
|
6
8
|
(429 Too Many Requests, 503 Service Unavailable, connection errors).
|
|
7
9
|
* Unified error handling that maps HTTP failures into
|
|
@@ -11,15 +13,16 @@
|
|
|
11
13
|
from __future__ import annotations
|
|
12
14
|
|
|
13
15
|
import warnings
|
|
14
|
-
from typing import Any
|
|
16
|
+
from typing import Any # noqa: TID251
|
|
15
17
|
|
|
16
18
|
import requests
|
|
17
19
|
from requests.adapters import HTTPAdapter
|
|
18
20
|
from urllib3.util.retry import Retry
|
|
19
21
|
|
|
20
22
|
from wherobots.__version__ import __version__
|
|
21
|
-
from wherobots.config import WherobotsConfig
|
|
23
|
+
from wherobots.config import WherobotsConfig # noqa: TC001
|
|
22
24
|
from wherobots.exceptions import WherobotsAPIError, WherobotsConfigError
|
|
25
|
+
from wherobots.utils.client_header import CLIENT_HEADER, client_header_value
|
|
23
26
|
from wherobots.utils.logger import get_logger
|
|
24
27
|
|
|
25
28
|
logger = get_logger("api")
|
|
@@ -29,6 +32,7 @@ logger = get_logger("api")
|
|
|
29
32
|
_DEFAULT_RETRIES = 3
|
|
30
33
|
_DEFAULT_BACKOFF_FACTOR = 0.5
|
|
31
34
|
_RETRIABLE_STATUS_CODES = (429, 502, 503)
|
|
35
|
+
_REDIRECT_STATUS_CODES = frozenset({301, 302, 303, 307, 308})
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
class BaseClient:
|
|
@@ -45,7 +49,7 @@ class BaseClient:
|
|
|
45
49
|
retries.
|
|
46
50
|
"""
|
|
47
51
|
|
|
48
|
-
def __init__(
|
|
52
|
+
def __init__( # noqa: D107
|
|
49
53
|
self,
|
|
50
54
|
config: WherobotsConfig,
|
|
51
55
|
max_retries: int = _DEFAULT_RETRIES,
|
|
@@ -94,15 +98,24 @@ class BaseClient:
|
|
|
94
98
|
# Intentionally not mounting on http:// — API keys must never
|
|
95
99
|
# be transmitted over plaintext HTTP.
|
|
96
100
|
|
|
97
|
-
# Common headers
|
|
101
|
+
# Common headers. ``X-Wherobots-Client`` is set here, on the
|
|
102
|
+
# session, so it rides along with every request this client makes
|
|
103
|
+
# instead of having to be threaded through each call site. Auth
|
|
104
|
+
# headers are merged last so nothing can displace them.
|
|
98
105
|
self._session.headers.update(
|
|
99
106
|
{
|
|
100
107
|
"Content-Type": "application/json",
|
|
101
108
|
"User-Agent": f"wherobots-python-sdk/{__version__}",
|
|
109
|
+
CLIENT_HEADER: client_header_value(config.client_chain),
|
|
102
110
|
**self._auth_headers(config.api_key),
|
|
103
111
|
}
|
|
104
112
|
)
|
|
105
113
|
|
|
114
|
+
@property
|
|
115
|
+
def timeout(self) -> int:
|
|
116
|
+
"""Per-request timeout in seconds, from ``WherobotsConfig``."""
|
|
117
|
+
return self._timeout
|
|
118
|
+
|
|
106
119
|
# --------------------------------------------------------------------- #
|
|
107
120
|
# Auth
|
|
108
121
|
# --------------------------------------------------------------------- #
|
|
@@ -154,35 +167,86 @@ class BaseClient:
|
|
|
154
167
|
timeout=self._timeout,
|
|
155
168
|
)
|
|
156
169
|
response.raise_for_status()
|
|
157
|
-
return response
|
|
170
|
+
return response # noqa: TRY300
|
|
158
171
|
except requests.HTTPError as exc:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
detail = None
|
|
163
|
-
request_id = None
|
|
164
|
-
if response is not None:
|
|
165
|
-
request_id = response.headers.get("X-Request-Id")
|
|
166
|
-
try:
|
|
167
|
-
error_payload = response.json()
|
|
168
|
-
detail = error_payload.get("message", error_payload)
|
|
169
|
-
except ValueError:
|
|
170
|
-
detail = response.text
|
|
171
|
-
|
|
172
|
-
message = "API request failed"
|
|
173
|
-
if status_code:
|
|
174
|
-
message = f"{message} (status {status_code})"
|
|
175
|
-
if detail:
|
|
176
|
-
message = f"{message}: {detail}"
|
|
172
|
+
raise self._api_error(exc) from exc
|
|
173
|
+
except requests.RequestException as exc:
|
|
174
|
+
raise WherobotsAPIError(f"Request failed: {exc}") from exc
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _api_error(exc: requests.HTTPError) -> WherobotsAPIError:
|
|
178
|
+
"""Map an HTTP error response to ``WherobotsAPIError``.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
exc: The ``HTTPError`` raised by ``raise_for_status``.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
A ``WherobotsAPIError`` carrying the status, parsed body and request id.
|
|
185
|
+
"""
|
|
186
|
+
response = exc.response
|
|
187
|
+
status_code = response.status_code if response is not None else None
|
|
188
|
+
error_payload = None
|
|
189
|
+
detail = None
|
|
190
|
+
request_id = None
|
|
191
|
+
if response is not None:
|
|
192
|
+
request_id = response.headers.get("X-Request-Id")
|
|
193
|
+
try:
|
|
194
|
+
error_payload = response.json()
|
|
195
|
+
detail = error_payload.get("message", error_payload)
|
|
196
|
+
except ValueError:
|
|
197
|
+
detail = response.text
|
|
198
|
+
|
|
199
|
+
message = "API request failed"
|
|
200
|
+
if status_code:
|
|
201
|
+
message = f"{message} (status {status_code})"
|
|
202
|
+
if detail:
|
|
203
|
+
message = f"{message}: {detail}"
|
|
204
|
+
|
|
205
|
+
return WherobotsAPIError(
|
|
206
|
+
message,
|
|
207
|
+
status_code=status_code,
|
|
208
|
+
response=error_payload,
|
|
209
|
+
request_id=request_id,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def get_redirect_target(self, path: str, params: dict[str, str] | None = None) -> str:
|
|
213
|
+
"""Send a GET that must answer with a redirect, and return its target.
|
|
214
|
+
|
|
215
|
+
The redirect is not followed, so the API key never travels to the
|
|
216
|
+
target. Errors are mapped exactly as in ``request``.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
path: API path appended to the base URL.
|
|
220
|
+
params: Optional query-string parameters.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The ``Location`` the API redirected to.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
WherobotsAPIError: On HTTP or connection failures, or when the
|
|
227
|
+
API answers without a redirect.
|
|
228
|
+
"""
|
|
229
|
+
url = f"{self._base_url}{path}"
|
|
230
|
+
try:
|
|
231
|
+
response = self._session.request(
|
|
232
|
+
"GET",
|
|
233
|
+
url,
|
|
234
|
+
params=params,
|
|
235
|
+
timeout=self._timeout,
|
|
236
|
+
allow_redirects=False,
|
|
237
|
+
)
|
|
238
|
+
response.raise_for_status()
|
|
239
|
+
except requests.HTTPError as exc:
|
|
240
|
+
raise self._api_error(exc) from exc
|
|
184
241
|
except requests.RequestException as exc:
|
|
185
242
|
raise WherobotsAPIError(f"Request failed: {exc}") from exc
|
|
243
|
+
location = response.headers.get("Location")
|
|
244
|
+
if response.status_code not in _REDIRECT_STATUS_CODES or not isinstance(location, str):
|
|
245
|
+
raise WherobotsAPIError(
|
|
246
|
+
f"Expected a redirect from GET {path}, got status {response.status_code}",
|
|
247
|
+
status_code=response.status_code,
|
|
248
|
+
)
|
|
249
|
+
return location
|
|
186
250
|
|
|
187
251
|
# Convenience wrappers
|
|
188
252
|
def get(self, path: str, params: dict[str, Any] | None = None) -> requests.Response:
|
|
@@ -224,15 +288,15 @@ class BaseClient:
|
|
|
224
288
|
self._session.close()
|
|
225
289
|
self._closed = True
|
|
226
290
|
|
|
227
|
-
def __enter__(self) -> BaseClient:
|
|
291
|
+
def __enter__(self) -> BaseClient: # noqa: PYI034
|
|
228
292
|
"""Enter the runtime context (context-manager protocol)."""
|
|
229
293
|
return self
|
|
230
294
|
|
|
231
|
-
def __exit__(self, *exc:
|
|
295
|
+
def __exit__(self, *exc: object) -> None:
|
|
232
296
|
"""Exit the runtime context, closing the session."""
|
|
233
297
|
self.close()
|
|
234
298
|
|
|
235
|
-
def __del__(self) -> None:
|
|
299
|
+
def __del__(self) -> None: # noqa: D105
|
|
236
300
|
# Mirror ``open()``/``socket``: warn callers who let the client
|
|
237
301
|
# get GC'd without closing it, so connection pools don't leak
|
|
238
302
|
# silently. Guarded against partial __init__ failure and
|
|
@@ -242,7 +306,7 @@ class BaseClient:
|
|
|
242
306
|
return
|
|
243
307
|
if getattr(self, "_session", None) is None:
|
|
244
308
|
return
|
|
245
|
-
except Exception:
|
|
309
|
+
except Exception: # noqa: BLE001
|
|
246
310
|
return
|
|
247
311
|
try:
|
|
248
312
|
warnings.warn(
|
|
@@ -252,5 +316,5 @@ class BaseClient:
|
|
|
252
316
|
stacklevel=2,
|
|
253
317
|
)
|
|
254
318
|
self._session.close()
|
|
255
|
-
except Exception:
|
|
319
|
+
except Exception: # noqa: BLE001, S110
|
|
256
320
|
pass
|