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 CHANGED
@@ -1,6 +1,7 @@
1
- """Wherobots Python SDK (Jobs API surface)."""
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
@@ -1,5 +1,5 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.5.0"
4
4
  __author__ = "Wherobots"
5
5
  __email__ = "support@wherobots.com"
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
- 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}"
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
- raise WherobotsAPIError(
179
- message,
180
- status_code=status_code,
181
- response=error_payload,
182
- request_id=request_id,
183
- ) from exc
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: Any) -> None:
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