wherobots-python-sdk 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.
- wherobots/__init__.py +87 -0
- wherobots/__version__.py +5 -0
- wherobots/api/__init__.py +7 -0
- wherobots/api/base.py +256 -0
- wherobots/api/files.py +386 -0
- wherobots/api/runs.py +255 -0
- wherobots/client.py +640 -0
- wherobots/config.py +165 -0
- wherobots/enums.py +140 -0
- wherobots/exceptions.py +47 -0
- wherobots/models.py +1080 -0
- wherobots/py.typed +0 -0
- wherobots/utils/__init__.py +6 -0
- wherobots/utils/logger.py +34 -0
- wherobots/utils/validation.py +31 -0
- wherobots_python_sdk-0.1.0.dist-info/METADATA +580 -0
- wherobots_python_sdk-0.1.0.dist-info/RECORD +20 -0
- wherobots_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- wherobots_python_sdk-0.1.0.dist-info/licenses/LICENSE +191 -0
- wherobots_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
wherobots/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Wherobots Python SDK (Jobs API surface)."""
|
|
2
|
+
|
|
3
|
+
from wherobots.__version__ import __version__
|
|
4
|
+
from wherobots.client import WherobotsJob
|
|
5
|
+
from wherobots.config import WherobotsConfig
|
|
6
|
+
from wherobots.enums import (
|
|
7
|
+
AppStatus,
|
|
8
|
+
AppType,
|
|
9
|
+
DependencyFileType,
|
|
10
|
+
DependencyType,
|
|
11
|
+
JobStatus,
|
|
12
|
+
Region,
|
|
13
|
+
Runtime,
|
|
14
|
+
is_terminal_status,
|
|
15
|
+
)
|
|
16
|
+
from wherobots.exceptions import (
|
|
17
|
+
WherobotsAPIError,
|
|
18
|
+
WherobotsConfigError,
|
|
19
|
+
WherobotsJobError,
|
|
20
|
+
WherobotsS3Error,
|
|
21
|
+
WherobotsTimeoutError,
|
|
22
|
+
WherobotsValidationError,
|
|
23
|
+
)
|
|
24
|
+
from wherobots.models import (
|
|
25
|
+
CreateRunPayload,
|
|
26
|
+
FileDependency,
|
|
27
|
+
KubeAppEvent,
|
|
28
|
+
LogItem,
|
|
29
|
+
LogsResponse,
|
|
30
|
+
OrganizationCustomer,
|
|
31
|
+
PyPiDependency,
|
|
32
|
+
RunAppMeta,
|
|
33
|
+
RunEnvironment,
|
|
34
|
+
RunEventMeta,
|
|
35
|
+
RunJarPayload,
|
|
36
|
+
RunKubeApp,
|
|
37
|
+
RunListPage,
|
|
38
|
+
RunMetricsResponse,
|
|
39
|
+
RunPythonPayload,
|
|
40
|
+
RunView,
|
|
41
|
+
StorageIntegration,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Convenience alias
|
|
45
|
+
Job = WherobotsJob
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"__version__",
|
|
49
|
+
# Client
|
|
50
|
+
"WherobotsJob",
|
|
51
|
+
"Job",
|
|
52
|
+
"WherobotsConfig",
|
|
53
|
+
# Enums
|
|
54
|
+
"Runtime",
|
|
55
|
+
"Region",
|
|
56
|
+
"JobStatus",
|
|
57
|
+
"AppStatus",
|
|
58
|
+
"AppType",
|
|
59
|
+
"DependencyType",
|
|
60
|
+
"DependencyFileType",
|
|
61
|
+
"is_terminal_status",
|
|
62
|
+
# Exceptions
|
|
63
|
+
"WherobotsJobError",
|
|
64
|
+
"WherobotsAPIError",
|
|
65
|
+
"WherobotsValidationError",
|
|
66
|
+
"WherobotsS3Error",
|
|
67
|
+
"WherobotsConfigError",
|
|
68
|
+
"WherobotsTimeoutError",
|
|
69
|
+
# Models
|
|
70
|
+
"CreateRunPayload",
|
|
71
|
+
"RunView",
|
|
72
|
+
"RunAppMeta",
|
|
73
|
+
"RunKubeApp",
|
|
74
|
+
"KubeAppEvent",
|
|
75
|
+
"RunEventMeta",
|
|
76
|
+
"OrganizationCustomer",
|
|
77
|
+
"RunListPage",
|
|
78
|
+
"LogsResponse",
|
|
79
|
+
"LogItem",
|
|
80
|
+
"RunMetricsResponse",
|
|
81
|
+
"RunPythonPayload",
|
|
82
|
+
"RunJarPayload",
|
|
83
|
+
"RunEnvironment",
|
|
84
|
+
"PyPiDependency",
|
|
85
|
+
"FileDependency",
|
|
86
|
+
"StorageIntegration",
|
|
87
|
+
]
|
wherobots/__version__.py
ADDED
wherobots/api/base.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Low-level HTTP client for the Wherobots REST API.
|
|
2
|
+
|
|
3
|
+
``BaseClient`` wraps a ``requests.Session`` with:
|
|
4
|
+
* Auth header construction (``X-API-Key``).
|
|
5
|
+
* Automatic retry with exponential back-off for transient errors
|
|
6
|
+
(429 Too Many Requests, 503 Service Unavailable, connection errors).
|
|
7
|
+
* Unified error handling that maps HTTP failures into
|
|
8
|
+
``WherobotsAPIError``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import warnings
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
from requests.adapters import HTTPAdapter
|
|
18
|
+
from urllib3.util.retry import Retry
|
|
19
|
+
|
|
20
|
+
from wherobots.__version__ import __version__
|
|
21
|
+
from wherobots.config import WherobotsConfig
|
|
22
|
+
from wherobots.exceptions import WherobotsAPIError, WherobotsConfigError
|
|
23
|
+
from wherobots.utils.logger import get_logger
|
|
24
|
+
|
|
25
|
+
logger = get_logger("api")
|
|
26
|
+
|
|
27
|
+
# Default retry policy: 3 retries with back-off on 429 / 502 / 503 and
|
|
28
|
+
# connection-level failures.
|
|
29
|
+
_DEFAULT_RETRIES = 3
|
|
30
|
+
_DEFAULT_BACKOFF_FACTOR = 0.5
|
|
31
|
+
_RETRIABLE_STATUS_CODES = (429, 502, 503)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseClient:
|
|
35
|
+
"""Thin HTTP wrapper around ``requests.Session``.
|
|
36
|
+
|
|
37
|
+
Provides auth header injection, automatic retries with exponential
|
|
38
|
+
back-off, and unified error mapping to ``WherobotsAPIError``.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: A populated ``WherobotsConfig``. Must contain at least
|
|
42
|
+
``api_key`` and ``base_url``.
|
|
43
|
+
max_retries: Maximum number of retries for transient failures.
|
|
44
|
+
backoff_factor: Multiplier for exponential back-off between
|
|
45
|
+
retries.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
config: WherobotsConfig,
|
|
51
|
+
max_retries: int = _DEFAULT_RETRIES,
|
|
52
|
+
backoff_factor: float = _DEFAULT_BACKOFF_FACTOR,
|
|
53
|
+
) -> None:
|
|
54
|
+
# Set early so __del__ never blows up on a partially-initialized
|
|
55
|
+
# instance. ``_session`` is populated below once the request
|
|
56
|
+
# session is built.
|
|
57
|
+
self._closed = False
|
|
58
|
+
|
|
59
|
+
if not config.api_key or not config.api_key.strip():
|
|
60
|
+
raise WherobotsConfigError(
|
|
61
|
+
"API key required. Provide via api_key parameter or "
|
|
62
|
+
"WHEROBOTS_API_KEY environment variable."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self._config = config
|
|
66
|
+
self._base_url = config.base_url.rstrip("/")
|
|
67
|
+
self._timeout = config.request_timeout_seconds
|
|
68
|
+
|
|
69
|
+
# Validate URL scheme
|
|
70
|
+
if not (self._base_url.startswith("https://") or self._base_url.startswith("http://")):
|
|
71
|
+
raise WherobotsConfigError(f"base_url must use https://. Got: {self._base_url}")
|
|
72
|
+
|
|
73
|
+
if self._base_url.startswith("http://"):
|
|
74
|
+
raise WherobotsConfigError(
|
|
75
|
+
"base_url must use HTTPS. Sending API keys over plaintext "
|
|
76
|
+
"HTTP is not allowed. Got: " + self._base_url
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Build a session with retry adapter
|
|
80
|
+
self._session = requests.Session()
|
|
81
|
+
|
|
82
|
+
# Disable redirect following to prevent auth header leaks
|
|
83
|
+
self._session.max_redirects = 0
|
|
84
|
+
|
|
85
|
+
retry = Retry(
|
|
86
|
+
total=max_retries,
|
|
87
|
+
backoff_factor=backoff_factor,
|
|
88
|
+
status_forcelist=list(_RETRIABLE_STATUS_CODES),
|
|
89
|
+
allowed_methods=["GET"],
|
|
90
|
+
raise_on_status=False,
|
|
91
|
+
)
|
|
92
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
93
|
+
self._session.mount("https://", adapter)
|
|
94
|
+
# Intentionally not mounting on http:// — API keys must never
|
|
95
|
+
# be transmitted over plaintext HTTP.
|
|
96
|
+
|
|
97
|
+
# Common headers
|
|
98
|
+
self._session.headers.update(
|
|
99
|
+
{
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"User-Agent": f"wherobots-python-sdk/{__version__}",
|
|
102
|
+
**self._auth_headers(config.api_key),
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# --------------------------------------------------------------------- #
|
|
107
|
+
# Auth
|
|
108
|
+
# --------------------------------------------------------------------- #
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _auth_headers(api_key: str) -> dict[str, str]:
|
|
112
|
+
"""Build the authentication header dictionary.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
api_key: The Wherobots API key.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A single-entry dict with the ``X-API-Key`` header.
|
|
119
|
+
"""
|
|
120
|
+
return {"X-API-Key": api_key.strip()}
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------- #
|
|
123
|
+
# Core request
|
|
124
|
+
# --------------------------------------------------------------------- #
|
|
125
|
+
|
|
126
|
+
def request(
|
|
127
|
+
self,
|
|
128
|
+
method: str,
|
|
129
|
+
path: str,
|
|
130
|
+
params: dict[str, Any] | None = None,
|
|
131
|
+
json_body: dict[str, Any] | None = None,
|
|
132
|
+
) -> requests.Response:
|
|
133
|
+
"""Send an HTTP request and return the response.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
method: HTTP method (e.g. ``"GET"``, ``"POST"``).
|
|
137
|
+
path: API path appended to the base URL (e.g. ``"/runs"``).
|
|
138
|
+
params: Optional query-string parameters.
|
|
139
|
+
json_body: Optional JSON-serializable request body.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The successful ``requests.Response``.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
WherobotsAPIError: On HTTP or connection failures.
|
|
146
|
+
"""
|
|
147
|
+
url = f"{self._base_url}{path}"
|
|
148
|
+
try:
|
|
149
|
+
response = self._session.request(
|
|
150
|
+
method,
|
|
151
|
+
url,
|
|
152
|
+
params=params,
|
|
153
|
+
json=json_body,
|
|
154
|
+
timeout=self._timeout,
|
|
155
|
+
)
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
return response
|
|
158
|
+
except requests.HTTPError as exc:
|
|
159
|
+
response = exc.response
|
|
160
|
+
status_code = response.status_code if response is not None else None
|
|
161
|
+
error_payload = None
|
|
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}"
|
|
177
|
+
|
|
178
|
+
raise WherobotsAPIError(
|
|
179
|
+
message,
|
|
180
|
+
status_code=status_code,
|
|
181
|
+
response=error_payload,
|
|
182
|
+
request_id=request_id,
|
|
183
|
+
) from exc
|
|
184
|
+
except requests.RequestException as exc:
|
|
185
|
+
raise WherobotsAPIError(f"Request failed: {exc}") from exc
|
|
186
|
+
|
|
187
|
+
# Convenience wrappers
|
|
188
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> requests.Response:
|
|
189
|
+
"""Send a GET request.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
path: API path appended to the base URL.
|
|
193
|
+
params: Optional query-string parameters.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The ``requests.Response`` from the API.
|
|
197
|
+
"""
|
|
198
|
+
return self.request("GET", path, params=params)
|
|
199
|
+
|
|
200
|
+
def post(
|
|
201
|
+
self,
|
|
202
|
+
path: str,
|
|
203
|
+
params: dict[str, Any] | None = None,
|
|
204
|
+
json_body: dict[str, Any] | None = None,
|
|
205
|
+
) -> requests.Response:
|
|
206
|
+
"""Send a POST request.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
path: API path appended to the base URL.
|
|
210
|
+
params: Optional query-string parameters.
|
|
211
|
+
json_body: Optional JSON-serializable request body.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
The ``requests.Response`` from the API.
|
|
215
|
+
"""
|
|
216
|
+
return self.request("POST", path, params=params, json_body=json_body)
|
|
217
|
+
|
|
218
|
+
# --------------------------------------------------------------------- #
|
|
219
|
+
# Lifecycle
|
|
220
|
+
# --------------------------------------------------------------------- #
|
|
221
|
+
|
|
222
|
+
def close(self) -> None:
|
|
223
|
+
"""Close the underlying session."""
|
|
224
|
+
self._session.close()
|
|
225
|
+
self._closed = True
|
|
226
|
+
|
|
227
|
+
def __enter__(self) -> BaseClient:
|
|
228
|
+
"""Enter the runtime context (context-manager protocol)."""
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def __exit__(self, *exc: Any) -> None:
|
|
232
|
+
"""Exit the runtime context, closing the session."""
|
|
233
|
+
self.close()
|
|
234
|
+
|
|
235
|
+
def __del__(self) -> None:
|
|
236
|
+
# Mirror ``open()``/``socket``: warn callers who let the client
|
|
237
|
+
# get GC'd without closing it, so connection pools don't leak
|
|
238
|
+
# silently. Guarded against partial __init__ failure and
|
|
239
|
+
# interpreter shutdown quirks.
|
|
240
|
+
try:
|
|
241
|
+
if getattr(self, "_closed", False):
|
|
242
|
+
return
|
|
243
|
+
if getattr(self, "_session", None) is None:
|
|
244
|
+
return
|
|
245
|
+
except Exception:
|
|
246
|
+
return
|
|
247
|
+
try:
|
|
248
|
+
warnings.warn(
|
|
249
|
+
f"Unclosed {type(self).__name__} — call close() or use "
|
|
250
|
+
"the object as a context manager to release the HTTP session.",
|
|
251
|
+
ResourceWarning,
|
|
252
|
+
stacklevel=2,
|
|
253
|
+
)
|
|
254
|
+
self._session.close()
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|