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 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
+ ]
@@ -0,0 +1,5 @@
1
+ """Version information."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Wherobots"
5
+ __email__ = "support@wherobots.com"
@@ -0,0 +1,7 @@
1
+ """Wherobots API layer — public exports."""
2
+
3
+ from wherobots.api.base import BaseClient
4
+ from wherobots.api.files import FilesAPI
5
+ from wherobots.api.runs import RunsAPI
6
+
7
+ __all__ = ["BaseClient", "FilesAPI", "RunsAPI"]
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