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/models.py
ADDED
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
"""Typed data models for the Wherobots Jobs API.
|
|
2
|
+
|
|
3
|
+
All models use dataclasses with ``from_dict`` / ``to_dict`` helpers
|
|
4
|
+
for JSON round-tripping without requiring pydantic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from wherobots.enums import (
|
|
13
|
+
AppStatus,
|
|
14
|
+
AppType,
|
|
15
|
+
DependencyFileType,
|
|
16
|
+
DependencyType,
|
|
17
|
+
JobStatus,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Dependency models
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PyPiDependency:
|
|
27
|
+
"""A PyPI package dependency."""
|
|
28
|
+
|
|
29
|
+
library_name: str
|
|
30
|
+
library_version: str
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
|
+
"""Serialize to the Wherobots API JSON format.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dict with ``sourceType``, ``libraryName``, and
|
|
37
|
+
``libraryVersion``.
|
|
38
|
+
"""
|
|
39
|
+
return {
|
|
40
|
+
"sourceType": DependencyType.PYPI.value,
|
|
41
|
+
"libraryName": self.library_name,
|
|
42
|
+
"libraryVersion": self.library_version,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_dict(cls, data: dict[str, Any]) -> PyPiDependency:
|
|
47
|
+
"""Deserialize from API JSON.
|
|
48
|
+
|
|
49
|
+
Permissive: missing fields default to empty strings. The API's
|
|
50
|
+
own validation surfaces real errors via ``WherobotsAPIError``.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
data: Dictionary containing ``libraryName`` and
|
|
54
|
+
``libraryVersion``.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A new ``PyPiDependency`` instance.
|
|
58
|
+
"""
|
|
59
|
+
return cls(
|
|
60
|
+
library_name=data.get("libraryName") or "",
|
|
61
|
+
library_version=data.get("libraryVersion") or "",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class FileDependency:
|
|
67
|
+
"""A file-based dependency (JAR, wheel, zip, etc.)."""
|
|
68
|
+
|
|
69
|
+
file_path: str
|
|
70
|
+
file_type: DependencyFileType | None = None
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict[str, Any]:
|
|
73
|
+
"""Serialize to the Wherobots API JSON format.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict with ``sourceType``, ``filePath``, and optionally
|
|
77
|
+
``fileType``.
|
|
78
|
+
"""
|
|
79
|
+
d: dict[str, Any] = {
|
|
80
|
+
"sourceType": DependencyType.FILE.value,
|
|
81
|
+
"filePath": self.file_path,
|
|
82
|
+
}
|
|
83
|
+
if self.file_type is not None:
|
|
84
|
+
d["fileType"] = self.file_type.value
|
|
85
|
+
return d
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, data: dict[str, Any]) -> FileDependency:
|
|
89
|
+
"""Deserialize from API JSON.
|
|
90
|
+
|
|
91
|
+
Permissive: missing ``filePath`` defaults to empty string;
|
|
92
|
+
unknown ``fileType`` values survive as ``None``.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
data: Dictionary containing ``filePath`` and optionally
|
|
96
|
+
``fileType``.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A new ``FileDependency`` instance.
|
|
100
|
+
"""
|
|
101
|
+
file_type = None
|
|
102
|
+
if data.get("fileType") is not None:
|
|
103
|
+
try:
|
|
104
|
+
file_type = DependencyFileType(data["fileType"])
|
|
105
|
+
except ValueError:
|
|
106
|
+
file_type = None
|
|
107
|
+
return cls(
|
|
108
|
+
file_path=data.get("filePath") or "",
|
|
109
|
+
file_type=file_type,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Run environment / payload models
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class RunEnvironment:
|
|
120
|
+
"""Environment configuration for a job run."""
|
|
121
|
+
|
|
122
|
+
spark_configs: dict[str, str] | None = None
|
|
123
|
+
dependencies: list[dict[str, Any]] | None = None
|
|
124
|
+
spark_driver_disk_gb: int | None = None
|
|
125
|
+
spark_executor_disk_gb: int | None = None
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> dict[str, Any]:
|
|
128
|
+
"""Serialize to the Wherobots API JSON format.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dict with Spark configs, dependencies, and disk settings.
|
|
132
|
+
Only non-``None`` fields are included.
|
|
133
|
+
"""
|
|
134
|
+
d: dict[str, Any] = {}
|
|
135
|
+
if self.spark_configs:
|
|
136
|
+
d["sparkConfigs"] = self.spark_configs
|
|
137
|
+
if self.dependencies:
|
|
138
|
+
d["dependencies"] = self.dependencies
|
|
139
|
+
if self.spark_driver_disk_gb is not None:
|
|
140
|
+
d["sparkDriverDiskGB"] = self.spark_driver_disk_gb
|
|
141
|
+
if self.spark_executor_disk_gb is not None:
|
|
142
|
+
d["sparkExecutorDiskGB"] = self.spark_executor_disk_gb
|
|
143
|
+
return d
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_dict(cls, data: dict[str, Any]) -> RunEnvironment:
|
|
147
|
+
"""Deserialize from API JSON.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
data: Dictionary with optional ``sparkConfigs``,
|
|
151
|
+
``dependencies``, ``sparkDriverDiskGB``, and
|
|
152
|
+
``sparkExecutorDiskGB``.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A new ``RunEnvironment`` instance.
|
|
156
|
+
"""
|
|
157
|
+
return cls(
|
|
158
|
+
spark_configs=data.get("sparkConfigs"),
|
|
159
|
+
dependencies=data.get("dependencies"),
|
|
160
|
+
spark_driver_disk_gb=data.get("sparkDriverDiskGB"),
|
|
161
|
+
spark_executor_disk_gb=data.get("sparkExecutorDiskGB"),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class RunPythonPayload:
|
|
167
|
+
"""Python script execution payload."""
|
|
168
|
+
|
|
169
|
+
uri: str
|
|
170
|
+
args: list[str] = field(default_factory=list)
|
|
171
|
+
|
|
172
|
+
def to_dict(self) -> dict[str, Any]:
|
|
173
|
+
"""Serialize to the Wherobots API JSON format.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dict with ``uri`` and ``args``.
|
|
177
|
+
"""
|
|
178
|
+
return {"uri": self.uri, "args": self.args}
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_dict(cls, data: dict[str, Any]) -> RunPythonPayload:
|
|
182
|
+
"""Deserialize from API JSON.
|
|
183
|
+
|
|
184
|
+
Permissive: missing ``uri`` defaults to empty string.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
data: Dictionary containing ``uri`` and optionally ``args``.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
A new ``RunPythonPayload`` instance.
|
|
191
|
+
"""
|
|
192
|
+
return cls(uri=data.get("uri") or "", args=data.get("args") or [])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class RunJarPayload:
|
|
197
|
+
"""JAR execution payload.
|
|
198
|
+
|
|
199
|
+
Note: per the API spec, ``mainClass`` is optional on responses
|
|
200
|
+
(the server may infer it). Kept as a string here with ``""`` as
|
|
201
|
+
the "unset" sentinel so existing callers who read the field
|
|
202
|
+
unconditionally continue to work.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
uri: str
|
|
206
|
+
main_class: str = ""
|
|
207
|
+
args: list[str] = field(default_factory=list)
|
|
208
|
+
|
|
209
|
+
def to_dict(self) -> dict[str, Any]:
|
|
210
|
+
"""Serialize to the Wherobots API JSON format.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dict with ``uri``, ``mainClass``, and ``args``. ``mainClass``
|
|
214
|
+
is omitted when empty so requests don't override server-side
|
|
215
|
+
inference.
|
|
216
|
+
"""
|
|
217
|
+
d: dict[str, Any] = {"uri": self.uri, "args": self.args}
|
|
218
|
+
if self.main_class:
|
|
219
|
+
d["mainClass"] = self.main_class
|
|
220
|
+
return d
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def from_dict(cls, data: dict[str, Any]) -> RunJarPayload:
|
|
224
|
+
"""Deserialize from API JSON.
|
|
225
|
+
|
|
226
|
+
Permissive: all fields default; the API owns field-level
|
|
227
|
+
validation.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
data: Dictionary containing ``uri``, ``mainClass``, and
|
|
231
|
+
optionally ``args``.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
A new ``RunJarPayload`` instance.
|
|
235
|
+
"""
|
|
236
|
+
return cls(
|
|
237
|
+
uri=data.get("uri") or "",
|
|
238
|
+
main_class=data.get("mainClass") or "",
|
|
239
|
+
args=data.get("args") or [],
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class CreateRunPayload:
|
|
245
|
+
"""Full payload for ``POST /runs``."""
|
|
246
|
+
|
|
247
|
+
runtime: str
|
|
248
|
+
name: str
|
|
249
|
+
version: str = "latest"
|
|
250
|
+
timeout_seconds: int = 3600
|
|
251
|
+
run_python: RunPythonPayload | None = None
|
|
252
|
+
run_jar: RunJarPayload | None = None
|
|
253
|
+
environment: RunEnvironment | None = None
|
|
254
|
+
|
|
255
|
+
def to_dict(self) -> dict[str, Any]:
|
|
256
|
+
"""Serialize to the Wherobots API JSON format.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Dict suitable for ``POST /runs`` request body.
|
|
260
|
+
"""
|
|
261
|
+
d: dict[str, Any] = {
|
|
262
|
+
"runtime": self.runtime,
|
|
263
|
+
"name": self.name,
|
|
264
|
+
"version": self.version,
|
|
265
|
+
"timeoutSeconds": self.timeout_seconds,
|
|
266
|
+
}
|
|
267
|
+
if self.run_python is not None:
|
|
268
|
+
d["runPython"] = self.run_python.to_dict()
|
|
269
|
+
if self.run_jar is not None:
|
|
270
|
+
d["runJar"] = self.run_jar.to_dict()
|
|
271
|
+
if self.environment is not None:
|
|
272
|
+
env_dict = self.environment.to_dict()
|
|
273
|
+
if env_dict:
|
|
274
|
+
d["environment"] = env_dict
|
|
275
|
+
return d
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def from_dict(cls, data: dict[str, Any]) -> CreateRunPayload:
|
|
279
|
+
"""Deserialize from API JSON.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
data: Dictionary containing ``runtime``, ``name``, and
|
|
283
|
+
optional nested ``runPython``, ``runJar``, and
|
|
284
|
+
``environment`` objects.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
A new ``CreateRunPayload`` instance.
|
|
288
|
+
"""
|
|
289
|
+
run_python = None
|
|
290
|
+
if "runPython" in data and data["runPython"] is not None:
|
|
291
|
+
run_python = RunPythonPayload.from_dict(data["runPython"])
|
|
292
|
+
run_jar = None
|
|
293
|
+
if "runJar" in data and data["runJar"] is not None:
|
|
294
|
+
run_jar = RunJarPayload.from_dict(data["runJar"])
|
|
295
|
+
environment = None
|
|
296
|
+
if "environment" in data and data["environment"] is not None:
|
|
297
|
+
environment = RunEnvironment.from_dict(data["environment"])
|
|
298
|
+
return cls(
|
|
299
|
+
runtime=data.get("runtime", ""),
|
|
300
|
+
name=data.get("name", ""),
|
|
301
|
+
version=data.get("version", "latest"),
|
|
302
|
+
timeout_seconds=data.get("timeoutSeconds", 3600),
|
|
303
|
+
run_python=run_python,
|
|
304
|
+
run_jar=run_jar,
|
|
305
|
+
environment=environment,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Run view / response models
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass
|
|
315
|
+
class RunAppMeta:
|
|
316
|
+
"""Application metadata attached to a run (OpenAPI ``RunAppMeta``)."""
|
|
317
|
+
|
|
318
|
+
spark_ui_url: str | None = None
|
|
319
|
+
spark_eventlogs_url: str | None = None
|
|
320
|
+
resolved_version: str | None = None
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def from_dict(cls, data: dict[str, Any]) -> RunAppMeta:
|
|
324
|
+
"""Deserialize from API JSON.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
data: Dictionary with optional ``spark_ui_url``,
|
|
328
|
+
``spark_eventlogs_url``, and ``resolved_version``.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
A new ``RunAppMeta`` instance.
|
|
332
|
+
"""
|
|
333
|
+
return cls(
|
|
334
|
+
spark_ui_url=data.get("spark_ui_url"),
|
|
335
|
+
spark_eventlogs_url=data.get("spark_eventlogs_url"),
|
|
336
|
+
resolved_version=data.get("resolved_version"),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def to_dict(self) -> dict[str, Any]:
|
|
340
|
+
"""Serialize to the Wherobots API JSON format.
|
|
341
|
+
|
|
342
|
+
Uses the API's own snake_case keys.
|
|
343
|
+
"""
|
|
344
|
+
d: dict[str, Any] = {}
|
|
345
|
+
if self.spark_ui_url is not None:
|
|
346
|
+
d["spark_ui_url"] = self.spark_ui_url
|
|
347
|
+
if self.spark_eventlogs_url is not None:
|
|
348
|
+
d["spark_eventlogs_url"] = self.spark_eventlogs_url
|
|
349
|
+
if self.resolved_version is not None:
|
|
350
|
+
d["resolved_version"] = self.resolved_version
|
|
351
|
+
return d
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass
|
|
355
|
+
class RunEventMeta:
|
|
356
|
+
"""Timestamp window attached to a ``KubeAppEvent`` (OpenAPI ``RunEventMeta``)."""
|
|
357
|
+
|
|
358
|
+
start_timestamp: int | None = None
|
|
359
|
+
end_timestamp: int | None = None
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def from_dict(cls, data: dict[str, Any]) -> RunEventMeta:
|
|
363
|
+
return cls(
|
|
364
|
+
start_timestamp=data.get("start_timestamp"),
|
|
365
|
+
end_timestamp=data.get("end_timestamp"),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def to_dict(self) -> dict[str, Any]:
|
|
369
|
+
d: dict[str, Any] = {}
|
|
370
|
+
if self.start_timestamp is not None:
|
|
371
|
+
d["start_timestamp"] = self.start_timestamp
|
|
372
|
+
if self.end_timestamp is not None:
|
|
373
|
+
d["end_timestamp"] = self.end_timestamp
|
|
374
|
+
return d
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@dataclass
|
|
378
|
+
class KubeAppEvent:
|
|
379
|
+
"""Kubernetes application event (OpenAPI ``KubeAppEvent``)."""
|
|
380
|
+
|
|
381
|
+
id: str = ""
|
|
382
|
+
create_time: str = ""
|
|
383
|
+
update_time: str = ""
|
|
384
|
+
code: str | None = None
|
|
385
|
+
message: str | None = None
|
|
386
|
+
event_meta: RunEventMeta | None = None
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def from_dict(cls, data: dict[str, Any]) -> KubeAppEvent:
|
|
390
|
+
event_meta = None
|
|
391
|
+
if data.get("eventMeta") is not None:
|
|
392
|
+
event_meta = RunEventMeta.from_dict(data["eventMeta"])
|
|
393
|
+
return cls(
|
|
394
|
+
id=data.get("id") or "",
|
|
395
|
+
create_time=data.get("createTime") or "",
|
|
396
|
+
update_time=data.get("updateTime") or "",
|
|
397
|
+
code=data.get("code"),
|
|
398
|
+
message=data.get("message"),
|
|
399
|
+
event_meta=event_meta,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def to_dict(self) -> dict[str, Any]:
|
|
403
|
+
d: dict[str, Any] = {
|
|
404
|
+
"id": self.id,
|
|
405
|
+
"createTime": self.create_time,
|
|
406
|
+
"updateTime": self.update_time,
|
|
407
|
+
}
|
|
408
|
+
if self.code is not None:
|
|
409
|
+
d["code"] = self.code
|
|
410
|
+
if self.message is not None:
|
|
411
|
+
d["message"] = self.message
|
|
412
|
+
if self.event_meta is not None:
|
|
413
|
+
d["eventMeta"] = self.event_meta.to_dict()
|
|
414
|
+
return d
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@dataclass
|
|
418
|
+
class RunKubeApp:
|
|
419
|
+
"""Kubernetes application details for a run (OpenAPI ``RunKubeApp``).
|
|
420
|
+
|
|
421
|
+
Forward-compat: ``app_type`` and ``status`` round-trip unknown
|
|
422
|
+
server values as raw strings rather than silently dropping them.
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
id: str = ""
|
|
426
|
+
create_time: str = ""
|
|
427
|
+
update_time: str = ""
|
|
428
|
+
instance_id: str = ""
|
|
429
|
+
app_type: AppType | str | None = None
|
|
430
|
+
status: AppStatus | str | None = None
|
|
431
|
+
created_by_id: str | None = None
|
|
432
|
+
api_payload: dict[str, Any] | None = None
|
|
433
|
+
app_meta: RunAppMeta | None = None
|
|
434
|
+
events: list[KubeAppEvent] = field(default_factory=list)
|
|
435
|
+
cpu_usage: int = 0
|
|
436
|
+
region_name: str = ""
|
|
437
|
+
message: str | None = None
|
|
438
|
+
|
|
439
|
+
@classmethod
|
|
440
|
+
def from_dict(cls, data: dict[str, Any]) -> RunKubeApp:
|
|
441
|
+
app_type: AppType | str | None = None
|
|
442
|
+
raw_app_type = data.get("appType")
|
|
443
|
+
if raw_app_type:
|
|
444
|
+
try:
|
|
445
|
+
app_type = AppType(raw_app_type)
|
|
446
|
+
except ValueError:
|
|
447
|
+
app_type = raw_app_type
|
|
448
|
+
|
|
449
|
+
status: AppStatus | str | None = None
|
|
450
|
+
raw_status = data.get("status")
|
|
451
|
+
if raw_status:
|
|
452
|
+
try:
|
|
453
|
+
status = AppStatus(raw_status)
|
|
454
|
+
except ValueError:
|
|
455
|
+
status = raw_status
|
|
456
|
+
|
|
457
|
+
app_meta = None
|
|
458
|
+
if data.get("appMeta") is not None:
|
|
459
|
+
app_meta = RunAppMeta.from_dict(data["appMeta"])
|
|
460
|
+
|
|
461
|
+
events = [KubeAppEvent.from_dict(e) for e in (data.get("events") or [])]
|
|
462
|
+
|
|
463
|
+
return cls(
|
|
464
|
+
id=data.get("id") or "",
|
|
465
|
+
create_time=data.get("createTime") or "",
|
|
466
|
+
update_time=data.get("updateTime") or "",
|
|
467
|
+
instance_id=data.get("instanceId") or "",
|
|
468
|
+
app_type=app_type,
|
|
469
|
+
status=status,
|
|
470
|
+
created_by_id=data.get("createdById"),
|
|
471
|
+
api_payload=data.get("apiPayload"),
|
|
472
|
+
app_meta=app_meta,
|
|
473
|
+
events=events,
|
|
474
|
+
cpu_usage=data.get("cpuUsage") or 0,
|
|
475
|
+
region_name=data.get("regionName") or "",
|
|
476
|
+
message=data.get("message"),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def to_dict(self) -> dict[str, Any]:
|
|
480
|
+
d: dict[str, Any] = {
|
|
481
|
+
"id": self.id,
|
|
482
|
+
"createTime": self.create_time,
|
|
483
|
+
"updateTime": self.update_time,
|
|
484
|
+
"instanceId": self.instance_id,
|
|
485
|
+
"cpuUsage": self.cpu_usage,
|
|
486
|
+
"regionName": self.region_name,
|
|
487
|
+
}
|
|
488
|
+
if isinstance(self.app_type, AppType):
|
|
489
|
+
d["appType"] = self.app_type.value
|
|
490
|
+
elif isinstance(self.app_type, str):
|
|
491
|
+
d["appType"] = self.app_type
|
|
492
|
+
if isinstance(self.status, AppStatus):
|
|
493
|
+
d["status"] = self.status.value
|
|
494
|
+
elif isinstance(self.status, str):
|
|
495
|
+
d["status"] = self.status
|
|
496
|
+
if self.created_by_id is not None:
|
|
497
|
+
d["createdById"] = self.created_by_id
|
|
498
|
+
if self.api_payload is not None:
|
|
499
|
+
d["apiPayload"] = self.api_payload
|
|
500
|
+
if self.app_meta is not None:
|
|
501
|
+
d["appMeta"] = self.app_meta.to_dict()
|
|
502
|
+
if self.events:
|
|
503
|
+
d["events"] = [e.to_dict() for e in self.events]
|
|
504
|
+
if self.message is not None:
|
|
505
|
+
d["message"] = self.message
|
|
506
|
+
return d
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@dataclass
|
|
510
|
+
class OrganizationCustomer:
|
|
511
|
+
"""A user/customer in the organization (OpenAPI ``OrganizationCustomer``).
|
|
512
|
+
|
|
513
|
+
Returned on ``RunView.triggeredBy`` to identify the user who
|
|
514
|
+
submitted a run.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
id: str = ""
|
|
518
|
+
create_time: str = ""
|
|
519
|
+
update_time: str = ""
|
|
520
|
+
email: str | None = None
|
|
521
|
+
name: str | None = None
|
|
522
|
+
roles: list[str] = field(default_factory=list)
|
|
523
|
+
|
|
524
|
+
@classmethod
|
|
525
|
+
def from_dict(cls, data: dict[str, Any]) -> OrganizationCustomer:
|
|
526
|
+
return cls(
|
|
527
|
+
id=data.get("id") or "",
|
|
528
|
+
create_time=data.get("createTime") or "",
|
|
529
|
+
update_time=data.get("updateTime") or "",
|
|
530
|
+
email=data.get("email"),
|
|
531
|
+
name=data.get("name"),
|
|
532
|
+
roles=list(data.get("roles") or []),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def to_dict(self) -> dict[str, Any]:
|
|
536
|
+
d: dict[str, Any] = {
|
|
537
|
+
"id": self.id,
|
|
538
|
+
"createTime": self.create_time,
|
|
539
|
+
"updateTime": self.update_time,
|
|
540
|
+
"roles": list(self.roles),
|
|
541
|
+
}
|
|
542
|
+
if self.email is not None:
|
|
543
|
+
d["email"] = self.email
|
|
544
|
+
if self.name is not None:
|
|
545
|
+
d["name"] = self.name
|
|
546
|
+
return d
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@dataclass
|
|
550
|
+
class RunView:
|
|
551
|
+
"""Complete view of a job run (``GET /runs/{id}`` response).
|
|
552
|
+
|
|
553
|
+
The Wherobots API nests payload fields (``runtime``, ``runPython``,
|
|
554
|
+
``runJar``, ``timeoutSeconds``, ``environment``) under a ``"payload"``
|
|
555
|
+
key. ``from_dict`` transparently flattens these so callers can access
|
|
556
|
+
e.g. ``view.runtime`` directly.
|
|
557
|
+
|
|
558
|
+
Timestamp fields from the API use ``createTime`` / ``startTime`` /
|
|
559
|
+
``completeTime``; for convenience the parser also accepts the legacy
|
|
560
|
+
``createdAt`` / ``startedAt`` / ``completedAt`` aliases.
|
|
561
|
+
|
|
562
|
+
Forward-compat:
|
|
563
|
+
|
|
564
|
+
* ``status`` is typed as ``JobStatus | str | None`` so a status value
|
|
565
|
+
the SDK doesn't know about yet (e.g. a new terminal state added
|
|
566
|
+
server-side) round-trips as the raw string instead of silently
|
|
567
|
+
disappearing. Use :func:`is_terminal_status` to check terminality
|
|
568
|
+
without assuming the enum.
|
|
569
|
+
* ``raw`` holds the full server response dict, so newly added fields
|
|
570
|
+
are always reachable even when the typed surface hasn't caught up.
|
|
571
|
+
"""
|
|
572
|
+
|
|
573
|
+
id: str
|
|
574
|
+
name: str
|
|
575
|
+
status: JobStatus | str | None = None
|
|
576
|
+
runtime: str | None = None
|
|
577
|
+
region: str | None = None
|
|
578
|
+
version: str | None = None
|
|
579
|
+
timeout_seconds: int | None = None
|
|
580
|
+
create_time: str | None = None
|
|
581
|
+
update_time: str | None = None
|
|
582
|
+
start_time: str | None = None
|
|
583
|
+
complete_time: str | None = None
|
|
584
|
+
run_python: RunPythonPayload | None = None
|
|
585
|
+
run_jar: RunJarPayload | None = None
|
|
586
|
+
environment: RunEnvironment | None = None
|
|
587
|
+
app_meta: RunAppMeta | None = None
|
|
588
|
+
kube_app: RunKubeApp | None = None
|
|
589
|
+
triggered_by: OrganizationCustomer | None = None
|
|
590
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
591
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
592
|
+
|
|
593
|
+
# Legacy aliases ---------------------------------------------------
|
|
594
|
+
@property
|
|
595
|
+
def created_at(self) -> str | None:
|
|
596
|
+
"""Alias kept for backward compatibility."""
|
|
597
|
+
return self.create_time
|
|
598
|
+
|
|
599
|
+
@property
|
|
600
|
+
def started_at(self) -> str | None:
|
|
601
|
+
"""Alias kept for backward compatibility."""
|
|
602
|
+
return self.start_time
|
|
603
|
+
|
|
604
|
+
@property
|
|
605
|
+
def completed_at(self) -> str | None:
|
|
606
|
+
"""Alias kept for backward compatibility."""
|
|
607
|
+
return self.complete_time
|
|
608
|
+
|
|
609
|
+
@classmethod
|
|
610
|
+
def from_dict(cls, data: dict[str, Any]) -> RunView:
|
|
611
|
+
"""Deserialize from API JSON, flattening the nested ``payload``.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
data: Full API response dictionary for a single run.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
A new ``RunView`` instance with payload fields promoted to
|
|
618
|
+
top-level attributes.
|
|
619
|
+
"""
|
|
620
|
+
# The API nests payload fields under "payload"; flatten them.
|
|
621
|
+
payload: dict[str, Any] = data.get("payload") or {}
|
|
622
|
+
|
|
623
|
+
status: JobStatus | str | None = None
|
|
624
|
+
raw_status = data.get("status")
|
|
625
|
+
if raw_status:
|
|
626
|
+
try:
|
|
627
|
+
status = JobStatus(raw_status)
|
|
628
|
+
except ValueError:
|
|
629
|
+
# Preserve unknown status strings verbatim so callers
|
|
630
|
+
# can still see the value and react (e.g. log it).
|
|
631
|
+
status = raw_status
|
|
632
|
+
|
|
633
|
+
# Helper: look in payload first, then top-level (for flexibility).
|
|
634
|
+
def _get(key: str, default: Any = None) -> Any:
|
|
635
|
+
if key in payload:
|
|
636
|
+
return payload[key]
|
|
637
|
+
return data.get(key, default)
|
|
638
|
+
|
|
639
|
+
run_python = None
|
|
640
|
+
rp = _get("runPython")
|
|
641
|
+
if rp is not None:
|
|
642
|
+
run_python = RunPythonPayload.from_dict(rp)
|
|
643
|
+
|
|
644
|
+
run_jar = None
|
|
645
|
+
rj = _get("runJar")
|
|
646
|
+
if rj is not None:
|
|
647
|
+
run_jar = RunJarPayload.from_dict(rj)
|
|
648
|
+
|
|
649
|
+
environment = None
|
|
650
|
+
env = _get("environment")
|
|
651
|
+
if env is not None:
|
|
652
|
+
environment = RunEnvironment.from_dict(env)
|
|
653
|
+
|
|
654
|
+
app_meta = None
|
|
655
|
+
# appMeta may live inside kubeApp or at top level
|
|
656
|
+
am = data.get("appMeta")
|
|
657
|
+
if am is not None:
|
|
658
|
+
app_meta = RunAppMeta.from_dict(am)
|
|
659
|
+
|
|
660
|
+
kube_app = None
|
|
661
|
+
ka = data.get("kubeApp")
|
|
662
|
+
if ka is not None:
|
|
663
|
+
kube_app = RunKubeApp.from_dict(ka)
|
|
664
|
+
# appMeta can also be nested in kubeApp
|
|
665
|
+
if app_meta is None and ka.get("appMeta") is not None:
|
|
666
|
+
app_meta = RunAppMeta.from_dict(ka["appMeta"])
|
|
667
|
+
|
|
668
|
+
triggered_by = None
|
|
669
|
+
if data.get("triggeredBy") is not None:
|
|
670
|
+
triggered_by = OrganizationCustomer.from_dict(data["triggeredBy"])
|
|
671
|
+
|
|
672
|
+
known_keys = {
|
|
673
|
+
"id",
|
|
674
|
+
"name",
|
|
675
|
+
"status",
|
|
676
|
+
"runtime",
|
|
677
|
+
"region",
|
|
678
|
+
"version",
|
|
679
|
+
"timeoutSeconds",
|
|
680
|
+
"payload",
|
|
681
|
+
# API timestamp keys
|
|
682
|
+
"createTime",
|
|
683
|
+
"updateTime",
|
|
684
|
+
"startTime",
|
|
685
|
+
"completeTime",
|
|
686
|
+
# Legacy timestamp keys
|
|
687
|
+
"createdAt",
|
|
688
|
+
"startedAt",
|
|
689
|
+
"completedAt",
|
|
690
|
+
"runPython",
|
|
691
|
+
"runJar",
|
|
692
|
+
"environment",
|
|
693
|
+
"appMeta",
|
|
694
|
+
"kubeApp",
|
|
695
|
+
"triggeredBy",
|
|
696
|
+
}
|
|
697
|
+
extra = {k: v for k, v in data.items() if k not in known_keys}
|
|
698
|
+
|
|
699
|
+
return cls(
|
|
700
|
+
id=data.get("id", ""),
|
|
701
|
+
name=data.get("name") or payload.get("name", ""),
|
|
702
|
+
status=status,
|
|
703
|
+
runtime=_get("runtime"),
|
|
704
|
+
region=data.get("region"),
|
|
705
|
+
version=_get("version"),
|
|
706
|
+
timeout_seconds=_get("timeoutSeconds"),
|
|
707
|
+
create_time=data.get("createTime", data.get("createdAt")),
|
|
708
|
+
update_time=data.get("updateTime"),
|
|
709
|
+
start_time=data.get("startTime", data.get("startedAt")),
|
|
710
|
+
complete_time=data.get("completeTime", data.get("completedAt")),
|
|
711
|
+
run_python=run_python,
|
|
712
|
+
run_jar=run_jar,
|
|
713
|
+
environment=environment,
|
|
714
|
+
app_meta=app_meta,
|
|
715
|
+
kube_app=kube_app,
|
|
716
|
+
triggered_by=triggered_by,
|
|
717
|
+
extra=extra,
|
|
718
|
+
raw=dict(data),
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
def to_dict(self) -> dict[str, Any]:
|
|
722
|
+
"""Serialize to the Wherobots API JSON format.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Dict with all run fields. Only non-``None`` fields are
|
|
726
|
+
included. Extra (unknown) fields are merged at the top
|
|
727
|
+
level.
|
|
728
|
+
"""
|
|
729
|
+
d: dict[str, Any] = {"id": self.id, "name": self.name}
|
|
730
|
+
if isinstance(self.status, JobStatus):
|
|
731
|
+
d["status"] = self.status.value
|
|
732
|
+
elif isinstance(self.status, str):
|
|
733
|
+
d["status"] = self.status
|
|
734
|
+
if self.runtime is not None:
|
|
735
|
+
d["runtime"] = self.runtime
|
|
736
|
+
if self.region is not None:
|
|
737
|
+
d["region"] = self.region
|
|
738
|
+
if self.version is not None:
|
|
739
|
+
d["version"] = self.version
|
|
740
|
+
if self.timeout_seconds is not None:
|
|
741
|
+
d["timeoutSeconds"] = self.timeout_seconds
|
|
742
|
+
if self.create_time is not None:
|
|
743
|
+
d["createTime"] = self.create_time
|
|
744
|
+
if self.update_time is not None:
|
|
745
|
+
d["updateTime"] = self.update_time
|
|
746
|
+
if self.start_time is not None:
|
|
747
|
+
d["startTime"] = self.start_time
|
|
748
|
+
if self.complete_time is not None:
|
|
749
|
+
d["completeTime"] = self.complete_time
|
|
750
|
+
if self.run_python is not None:
|
|
751
|
+
d["runPython"] = self.run_python.to_dict()
|
|
752
|
+
if self.run_jar is not None:
|
|
753
|
+
d["runJar"] = self.run_jar.to_dict()
|
|
754
|
+
if self.environment is not None:
|
|
755
|
+
d["environment"] = self.environment.to_dict()
|
|
756
|
+
if self.app_meta is not None:
|
|
757
|
+
d["appMeta"] = self.app_meta.to_dict()
|
|
758
|
+
if self.kube_app is not None:
|
|
759
|
+
d["kubeApp"] = self.kube_app.to_dict()
|
|
760
|
+
if self.triggered_by is not None:
|
|
761
|
+
d["triggeredBy"] = self.triggered_by.to_dict()
|
|
762
|
+
if self.extra:
|
|
763
|
+
d.update(self.extra)
|
|
764
|
+
return d
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# ---------------------------------------------------------------------------
|
|
768
|
+
# Logs models
|
|
769
|
+
# ---------------------------------------------------------------------------
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
@dataclass
|
|
773
|
+
class LogItem:
|
|
774
|
+
"""A single log entry.
|
|
775
|
+
|
|
776
|
+
The ``timestamp`` field is typed as ``Union[int, str, None]`` because
|
|
777
|
+
the Wherobots API returns integer epoch values (e.g. ``0``) while
|
|
778
|
+
other sources may use ISO-8601 strings.
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
raw: str = ""
|
|
782
|
+
timestamp: int | str | None = None
|
|
783
|
+
level: str | None = None
|
|
784
|
+
message: str | None = None
|
|
785
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
786
|
+
|
|
787
|
+
@classmethod
|
|
788
|
+
def from_dict(cls, data: dict[str, Any]) -> LogItem:
|
|
789
|
+
"""Deserialize from API JSON.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
data: Dictionary with ``raw`` and optional ``timestamp``,
|
|
793
|
+
``level``, ``message`` fields. Unknown keys are
|
|
794
|
+
captured in ``extra``.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
A new ``LogItem`` instance.
|
|
798
|
+
"""
|
|
799
|
+
known_keys = {"raw", "timestamp", "level", "message"}
|
|
800
|
+
extra = {k: v for k, v in data.items() if k not in known_keys}
|
|
801
|
+
return cls(
|
|
802
|
+
raw=data.get("raw", ""),
|
|
803
|
+
timestamp=data.get("timestamp"),
|
|
804
|
+
level=data.get("level"),
|
|
805
|
+
message=data.get("message"),
|
|
806
|
+
extra=extra,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
def to_dict(self) -> dict[str, Any]:
|
|
810
|
+
"""Serialize to the Wherobots API JSON format.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Dict with log entry fields. Only non-``None`` fields are
|
|
814
|
+
included. Extra fields are merged at the top level.
|
|
815
|
+
"""
|
|
816
|
+
d: dict[str, Any] = {"raw": self.raw}
|
|
817
|
+
if self.timestamp is not None:
|
|
818
|
+
d["timestamp"] = self.timestamp
|
|
819
|
+
if self.level is not None:
|
|
820
|
+
d["level"] = self.level
|
|
821
|
+
if self.message is not None:
|
|
822
|
+
d["message"] = self.message
|
|
823
|
+
if self.extra:
|
|
824
|
+
d.update(self.extra)
|
|
825
|
+
return d
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@dataclass
|
|
829
|
+
class LogsResponse:
|
|
830
|
+
"""Response from ``GET /runs/{id}/logs``.
|
|
831
|
+
|
|
832
|
+
The API returns ``current_page`` (int) and ``next_page`` (int) for
|
|
833
|
+
cursor-based pagination. ``raw`` holds the original server response
|
|
834
|
+
so callers can reach new fields without a SDK release.
|
|
835
|
+
"""
|
|
836
|
+
|
|
837
|
+
items: list[LogItem] = field(default_factory=list)
|
|
838
|
+
next_page: int | str | None = None
|
|
839
|
+
current_page: int | str | None = None
|
|
840
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
841
|
+
|
|
842
|
+
@classmethod
|
|
843
|
+
def from_dict(cls, data: dict[str, Any]) -> LogsResponse:
|
|
844
|
+
"""Deserialize from API JSON.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
data: Dictionary with ``items`` list and optional
|
|
848
|
+
``next_page`` / ``current_page`` cursors. Accepts
|
|
849
|
+
both snake_case and camelCase key variants.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
A new ``LogsResponse`` instance.
|
|
853
|
+
"""
|
|
854
|
+
raw_items = data.get("items")
|
|
855
|
+
items = [LogItem.from_dict(i) for i in (raw_items or [])]
|
|
856
|
+
next_page = data.get("next_page", data.get("nextPage"))
|
|
857
|
+
current_page = data.get("current_page", data.get("currentPage"))
|
|
858
|
+
return cls(
|
|
859
|
+
items=items,
|
|
860
|
+
next_page=next_page,
|
|
861
|
+
current_page=current_page,
|
|
862
|
+
raw=dict(data),
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
def to_dict(self) -> dict[str, Any]:
|
|
866
|
+
"""Serialize to the Wherobots API JSON format.
|
|
867
|
+
|
|
868
|
+
Returns:
|
|
869
|
+
Dict with serialized ``items`` and pagination cursors.
|
|
870
|
+
"""
|
|
871
|
+
d: dict[str, Any] = {"items": [i.to_dict() for i in self.items]}
|
|
872
|
+
if self.next_page is not None:
|
|
873
|
+
d["next_page"] = self.next_page
|
|
874
|
+
if self.current_page is not None:
|
|
875
|
+
d["current_page"] = self.current_page
|
|
876
|
+
return d
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
# ---------------------------------------------------------------------------
|
|
880
|
+
# Metrics models
|
|
881
|
+
# ---------------------------------------------------------------------------
|
|
882
|
+
#
|
|
883
|
+
# The OpenAPI spec types the metrics blobs as plain ``object`` with
|
|
884
|
+
# no fixed per-metric shape, so the SDK intentionally does not expose
|
|
885
|
+
# typed ``SeriesMetric`` / ``InstantMetric`` / ``MetricDetail`` /
|
|
886
|
+
# ``DataValue`` dataclasses. Use ``RunMetricsResponse.series_metrics`` /
|
|
887
|
+
# ``instant_metrics`` (keyed by metric name) for ergonomic lookup and
|
|
888
|
+
# ``RunMetricsResponse.raw`` to see exactly what the server returned.
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@dataclass
|
|
892
|
+
class RunMetricsResponse:
|
|
893
|
+
"""Response from ``GET /runs/{id}/metrics``.
|
|
894
|
+
|
|
895
|
+
Per the OpenAPI spec the metrics blobs use snake_case keys
|
|
896
|
+
(``series_metrics`` / ``instant_metrics``) and are typed as plain
|
|
897
|
+
``object`` (the server's internal metric shape is not stable enough
|
|
898
|
+
to type against). The SDK exposes them as untyped dicts keyed by
|
|
899
|
+
metric name; callers can inspect ``raw`` for any new fields the
|
|
900
|
+
server adds.
|
|
901
|
+
"""
|
|
902
|
+
|
|
903
|
+
series_metrics: dict[str, Any] = field(default_factory=dict)
|
|
904
|
+
instant_metrics: dict[str, Any] = field(default_factory=dict)
|
|
905
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
906
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
907
|
+
|
|
908
|
+
@classmethod
|
|
909
|
+
def from_dict(cls, data: dict[str, Any]) -> RunMetricsResponse:
|
|
910
|
+
"""Deserialize from API JSON.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
data: Dictionary with ``series_metrics`` / ``instant_metrics``
|
|
914
|
+
(also accepts camelCase variants for robustness).
|
|
915
|
+
Unknown keys are captured in ``extra``.
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
A new ``RunMetricsResponse`` instance.
|
|
919
|
+
"""
|
|
920
|
+
known_keys = {"series_metrics", "seriesMetrics", "instant_metrics", "instantMetrics"}
|
|
921
|
+
extra = {k: v for k, v in data.items() if k not in known_keys}
|
|
922
|
+
raw_series = data.get("series_metrics", data.get("seriesMetrics", {})) or {}
|
|
923
|
+
raw_instant = data.get("instant_metrics", data.get("instantMetrics", {})) or {}
|
|
924
|
+
return cls(
|
|
925
|
+
series_metrics=dict(raw_series) if isinstance(raw_series, dict) else {},
|
|
926
|
+
instant_metrics=dict(raw_instant) if isinstance(raw_instant, dict) else {},
|
|
927
|
+
extra=extra,
|
|
928
|
+
raw=dict(data),
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
def to_dict(self) -> dict[str, Any]:
|
|
932
|
+
"""Serialize to the Wherobots API JSON format.
|
|
933
|
+
|
|
934
|
+
Uses the snake_case keys the API itself uses.
|
|
935
|
+
"""
|
|
936
|
+
d: dict[str, Any] = {
|
|
937
|
+
"series_metrics": dict(self.series_metrics),
|
|
938
|
+
"instant_metrics": dict(self.instant_metrics),
|
|
939
|
+
}
|
|
940
|
+
if self.extra:
|
|
941
|
+
d.update(self.extra)
|
|
942
|
+
return d
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
# ---------------------------------------------------------------------------
|
|
946
|
+
# Pagination
|
|
947
|
+
# ---------------------------------------------------------------------------
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
@dataclass
|
|
951
|
+
class RunListPage:
|
|
952
|
+
"""Paginated list of runs (``GET /runs`` response).
|
|
953
|
+
|
|
954
|
+
The Wherobots API returns string-based pagination cursors:
|
|
955
|
+
``next_page``, ``previous_page``, ``current_page``,
|
|
956
|
+
``current_page_backwards``.
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
items: list[RunView] = field(default_factory=list)
|
|
960
|
+
total: int = 0
|
|
961
|
+
next_page: str | None = None
|
|
962
|
+
previous_page: str | None = None
|
|
963
|
+
current_page: str | None = None
|
|
964
|
+
current_page_backwards: str | None = None
|
|
965
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
966
|
+
|
|
967
|
+
# Legacy alias for backward compatibility
|
|
968
|
+
@property
|
|
969
|
+
def cursor(self) -> str | None:
|
|
970
|
+
"""Alias for ``next_page`` — kept for backward compatibility."""
|
|
971
|
+
return self.next_page
|
|
972
|
+
|
|
973
|
+
@classmethod
|
|
974
|
+
def from_dict(cls, data: dict[str, Any]) -> RunListPage:
|
|
975
|
+
"""Deserialize from API JSON.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
data: Dictionary with ``items`` list, ``total`` count, and
|
|
979
|
+
optional pagination cursors. Also accepts legacy
|
|
980
|
+
``cursor`` key as fallback for ``next_page``.
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
A new ``RunListPage`` instance.
|
|
984
|
+
"""
|
|
985
|
+
raw_items = data.get("items")
|
|
986
|
+
items = [RunView.from_dict(i) for i in (raw_items or [])]
|
|
987
|
+
return cls(
|
|
988
|
+
items=items,
|
|
989
|
+
total=data.get("total", len(items)),
|
|
990
|
+
next_page=data.get("next_page", data.get("cursor")),
|
|
991
|
+
previous_page=data.get("previous_page"),
|
|
992
|
+
current_page=data.get("current_page"),
|
|
993
|
+
current_page_backwards=data.get("current_page_backwards"),
|
|
994
|
+
raw=dict(data),
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
def to_dict(self) -> dict[str, Any]:
|
|
998
|
+
"""Serialize to the Wherobots API JSON format.
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
Dict with serialized ``items``, ``total``, and pagination
|
|
1002
|
+
cursors.
|
|
1003
|
+
"""
|
|
1004
|
+
d: dict[str, Any] = {
|
|
1005
|
+
"items": [i.to_dict() for i in self.items],
|
|
1006
|
+
"total": self.total,
|
|
1007
|
+
}
|
|
1008
|
+
if self.next_page is not None:
|
|
1009
|
+
d["next_page"] = self.next_page
|
|
1010
|
+
if self.previous_page is not None:
|
|
1011
|
+
d["previous_page"] = self.previous_page
|
|
1012
|
+
if self.current_page is not None:
|
|
1013
|
+
d["current_page"] = self.current_page
|
|
1014
|
+
if self.current_page_backwards is not None:
|
|
1015
|
+
d["current_page_backwards"] = self.current_page_backwards
|
|
1016
|
+
return d
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
# =========================================================================== #
|
|
1020
|
+
# Storage Integration Models
|
|
1021
|
+
# =========================================================================== #
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
@dataclass
|
|
1025
|
+
class StorageIntegration:
|
|
1026
|
+
"""A user-configured S3 storage integration.
|
|
1027
|
+
|
|
1028
|
+
Represents a connection between the Wherobots organization and an
|
|
1029
|
+
external S3 bucket, as returned by ``GET /storage``.
|
|
1030
|
+
"""
|
|
1031
|
+
|
|
1032
|
+
id: str
|
|
1033
|
+
name: str
|
|
1034
|
+
path: str
|
|
1035
|
+
region: str
|
|
1036
|
+
create_time: str | None = None
|
|
1037
|
+
update_time: str | None = None
|
|
1038
|
+
role_arn: str | None = None
|
|
1039
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
1040
|
+
|
|
1041
|
+
@classmethod
|
|
1042
|
+
def from_dict(cls, data: dict[str, Any]) -> StorageIntegration:
|
|
1043
|
+
"""Deserialize from the Wherobots API JSON format.
|
|
1044
|
+
|
|
1045
|
+
Args:
|
|
1046
|
+
data: Dictionary from the API response.
|
|
1047
|
+
|
|
1048
|
+
Returns:
|
|
1049
|
+
A new ``StorageIntegration`` instance.
|
|
1050
|
+
"""
|
|
1051
|
+
return cls(
|
|
1052
|
+
id=data.get("id", ""),
|
|
1053
|
+
name=data.get("name", ""),
|
|
1054
|
+
path=data.get("path", ""),
|
|
1055
|
+
region=data.get("region", ""),
|
|
1056
|
+
create_time=data.get("createTime"),
|
|
1057
|
+
update_time=data.get("updateTime"),
|
|
1058
|
+
role_arn=data.get("roleArn"),
|
|
1059
|
+
raw=dict(data),
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1063
|
+
"""Serialize to the Wherobots API JSON format.
|
|
1064
|
+
|
|
1065
|
+
Returns:
|
|
1066
|
+
Dict with camelCase keys matching the API contract.
|
|
1067
|
+
"""
|
|
1068
|
+
d: dict[str, Any] = {
|
|
1069
|
+
"id": self.id,
|
|
1070
|
+
"name": self.name,
|
|
1071
|
+
"path": self.path,
|
|
1072
|
+
"region": self.region,
|
|
1073
|
+
}
|
|
1074
|
+
if self.create_time is not None:
|
|
1075
|
+
d["createTime"] = self.create_time
|
|
1076
|
+
if self.update_time is not None:
|
|
1077
|
+
d["updateTime"] = self.update_time
|
|
1078
|
+
if self.role_arn is not None:
|
|
1079
|
+
d["roleArn"] = self.role_arn
|
|
1080
|
+
return d
|