humalab 0.0.4__py3-none-any.whl → 0.0.5__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.
Potentially problematic release.
This version of humalab might be problematic. Click here for more details.
- humalab/assets/__init__.py +1 -1
- humalab/assets/files/urdf_file.py +1 -1
- humalab/assets/resource_manager.py +3 -2
- humalab/dists/bernoulli.py +15 -0
- humalab/dists/categorical.py +4 -0
- humalab/dists/discrete.py +22 -0
- humalab/dists/gaussian.py +22 -0
- humalab/dists/log_uniform.py +22 -0
- humalab/dists/truncated_gaussian.py +36 -0
- humalab/dists/uniform.py +22 -0
- humalab/episode.py +26 -0
- humalab/evaluators/__init__.py +16 -0
- humalab/humalab.py +79 -132
- humalab/humalab_api_client.py +540 -49
- humalab/humalab_config.py +0 -13
- humalab/humalab_main.py +119 -0
- humalab/humalab_test.py +0 -12
- humalab/run.py +0 -12
- humalab/scenario.py +172 -16
- {humalab-0.0.4.dist-info → humalab-0.0.5.dist-info}/METADATA +1 -1
- humalab-0.0.5.dist-info/RECORD +37 -0
- humalab-0.0.4.dist-info/RECORD +0 -34
- {humalab-0.0.4.dist-info → humalab-0.0.5.dist-info}/WHEEL +0 -0
- {humalab-0.0.4.dist-info → humalab-0.0.5.dist-info}/entry_points.txt +0 -0
- {humalab-0.0.4.dist-info → humalab-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {humalab-0.0.4.dist-info → humalab-0.0.5.dist-info}/top_level.txt +0 -0
humalab/humalab_api_client.py
CHANGED
|
@@ -114,11 +114,23 @@ class HumaLabApiClient:
|
|
|
114
114
|
def delete(self, endpoint: str, **kwargs) -> requests.Response:
|
|
115
115
|
"""Make a DELETE request."""
|
|
116
116
|
return self._make_request("DELETE", endpoint, **kwargs)
|
|
117
|
-
|
|
117
|
+
|
|
118
|
+
# User Authentication API methods
|
|
119
|
+
def validate_token(self) -> Dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Validate JWT token and return user info.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
User information from the validated token
|
|
125
|
+
"""
|
|
126
|
+
response = self.get("/api-key/validate")
|
|
127
|
+
return response.json()
|
|
128
|
+
|
|
118
129
|
# Convenience methods for common API operations
|
|
119
130
|
|
|
120
131
|
def get_resources(
|
|
121
|
-
self,
|
|
132
|
+
self,
|
|
133
|
+
project_name: str,
|
|
122
134
|
resource_types: Optional[str] = None,
|
|
123
135
|
limit: int = 20,
|
|
124
136
|
offset: int = 0,
|
|
@@ -126,89 +138,98 @@ class HumaLabApiClient:
|
|
|
126
138
|
) -> Dict[str, Any]:
|
|
127
139
|
"""
|
|
128
140
|
Get list of all resources.
|
|
129
|
-
|
|
141
|
+
|
|
130
142
|
Args:
|
|
143
|
+
project_name: Project name (required)
|
|
131
144
|
resource_types: Comma-separated resource types to filter by
|
|
132
145
|
limit: Maximum number of resources to return
|
|
133
146
|
offset: Number of resources to skip
|
|
134
147
|
latest_only: If true, only return latest version of each resource
|
|
135
|
-
|
|
148
|
+
|
|
136
149
|
Returns:
|
|
137
150
|
Resource list with pagination info
|
|
138
151
|
"""
|
|
139
152
|
params = {
|
|
153
|
+
"project_name": project_name,
|
|
140
154
|
"limit": limit,
|
|
141
155
|
"offset": offset,
|
|
142
156
|
"latest_only": latest_only
|
|
143
157
|
}
|
|
144
158
|
if resource_types:
|
|
145
159
|
params["resource_types"] = resource_types
|
|
146
|
-
|
|
160
|
+
|
|
147
161
|
response = self.get("/resources", params=params)
|
|
148
162
|
return response.json()
|
|
149
163
|
|
|
150
|
-
def get_resource(self, name: str, version: Optional[int] = None) -> Dict[str, Any]:
|
|
164
|
+
def get_resource(self, name: str, project_name: str, version: Optional[int] = None) -> Dict[str, Any]:
|
|
151
165
|
"""
|
|
152
166
|
Get a specific resource.
|
|
153
|
-
|
|
167
|
+
|
|
154
168
|
Args:
|
|
155
169
|
name: Resource name
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
project_name: Project name (required)
|
|
171
|
+
version: Optional specific version (defaults to latest)
|
|
172
|
+
|
|
158
173
|
Returns:
|
|
159
174
|
Resource data
|
|
160
175
|
"""
|
|
161
176
|
if version is not None:
|
|
162
177
|
endpoint = f"/resources/{name}/{version}"
|
|
178
|
+
params = {"project_name": project_name}
|
|
163
179
|
else:
|
|
164
180
|
endpoint = f"/resources/{name}"
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
params = {"project_name": project_name}
|
|
182
|
+
|
|
183
|
+
response = self.get(endpoint, params=params)
|
|
167
184
|
return response.json()
|
|
168
185
|
|
|
169
186
|
def download_resource(
|
|
170
|
-
self,
|
|
171
|
-
name: str,
|
|
187
|
+
self,
|
|
188
|
+
name: str,
|
|
189
|
+
project_name: str,
|
|
172
190
|
version: Optional[int] = None
|
|
173
191
|
) -> bytes:
|
|
174
192
|
"""
|
|
175
193
|
Download a resource file.
|
|
176
|
-
|
|
194
|
+
|
|
177
195
|
Args:
|
|
178
196
|
name: Resource name
|
|
179
|
-
|
|
180
|
-
|
|
197
|
+
project_name: Project name (required)
|
|
198
|
+
version: Optional specific version (defaults to latest)
|
|
199
|
+
|
|
181
200
|
Returns:
|
|
182
201
|
Resource file content as bytes
|
|
183
202
|
"""
|
|
203
|
+
endpoint = f"/resources/{name}/download"
|
|
204
|
+
params = {"project_name": project_name}
|
|
184
205
|
if version is not None:
|
|
185
|
-
|
|
186
|
-
else:
|
|
187
|
-
endpoint = f"/resources/{name}/download"
|
|
206
|
+
params["version"] = str(version)
|
|
188
207
|
|
|
189
|
-
response = self.get(endpoint)
|
|
208
|
+
response = self.get(endpoint, params=params)
|
|
190
209
|
return response.content
|
|
191
210
|
|
|
192
211
|
def upload_resource(
|
|
193
|
-
self,
|
|
194
|
-
name: str,
|
|
195
|
-
file_path: str,
|
|
212
|
+
self,
|
|
213
|
+
name: str,
|
|
214
|
+
file_path: str,
|
|
196
215
|
resource_type: str,
|
|
216
|
+
project_name: str,
|
|
197
217
|
description: Optional[str] = None,
|
|
198
218
|
filename: Optional[str] = None,
|
|
199
219
|
allow_duplicate_name: bool = False
|
|
200
220
|
) -> Dict[str, Any]:
|
|
201
221
|
"""
|
|
202
222
|
Upload a resource file.
|
|
203
|
-
|
|
223
|
+
|
|
204
224
|
Args:
|
|
205
225
|
name: Resource name
|
|
206
226
|
file_path: Path to file to upload
|
|
207
227
|
resource_type: Type of resource (urdf, mjcf, etc.)
|
|
228
|
+
project_name: Project name (required)
|
|
208
229
|
description: Optional description
|
|
209
230
|
filename: Optional custom filename
|
|
210
231
|
allow_duplicate_name: Allow creating new version with existing name
|
|
211
|
-
|
|
232
|
+
|
|
212
233
|
Returns:
|
|
213
234
|
Created resource data
|
|
214
235
|
"""
|
|
@@ -219,12 +240,13 @@ class HumaLabApiClient:
|
|
|
219
240
|
data['description'] = description
|
|
220
241
|
if filename:
|
|
221
242
|
data['filename'] = filename
|
|
222
|
-
|
|
243
|
+
|
|
223
244
|
params = {
|
|
224
245
|
'resource_type': resource_type,
|
|
246
|
+
'project_name': project_name,
|
|
225
247
|
'allow_duplicate_name': allow_duplicate_name
|
|
226
248
|
}
|
|
227
|
-
|
|
249
|
+
|
|
228
250
|
response = self.post(f"/resources/{name}/upload", files=files, params=params)
|
|
229
251
|
return response.json()
|
|
230
252
|
|
|
@@ -232,42 +254,511 @@ class HumaLabApiClient:
|
|
|
232
254
|
"""Get list of all available resource types."""
|
|
233
255
|
response = self.get("/resources/types")
|
|
234
256
|
return response.json()
|
|
257
|
+
|
|
258
|
+
def get_scenarios(
|
|
259
|
+
self,
|
|
260
|
+
project_name: str,
|
|
261
|
+
skip: int = 0,
|
|
262
|
+
limit: int = 10,
|
|
263
|
+
include_inactive: bool = False,
|
|
264
|
+
search: Optional[str] = None,
|
|
265
|
+
status_filter: Optional[str] = None
|
|
266
|
+
) -> Dict[str, Any]:
|
|
267
|
+
"""
|
|
268
|
+
Get list of scenarios with pagination and filtering.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
project_name: Project name (required)
|
|
272
|
+
skip: Number of scenarios to skip for pagination
|
|
273
|
+
limit: Maximum number of scenarios to return (1-100)
|
|
274
|
+
include_inactive: Include inactive scenarios in results
|
|
275
|
+
search: Search term to filter by name, description, or UUID
|
|
276
|
+
status_filter: Filter by specific status
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Paginated list of scenarios
|
|
280
|
+
"""
|
|
281
|
+
params = {
|
|
282
|
+
"project_name": project_name,
|
|
283
|
+
"skip": skip,
|
|
284
|
+
"limit": limit,
|
|
285
|
+
"include_inactive": include_inactive
|
|
286
|
+
}
|
|
287
|
+
if search:
|
|
288
|
+
params["search"] = search
|
|
289
|
+
if status_filter:
|
|
290
|
+
params["status_filter"] = status_filter
|
|
291
|
+
|
|
292
|
+
response = self.get("/scenarios", params=params)
|
|
293
|
+
return response.json()
|
|
235
294
|
|
|
236
|
-
def
|
|
295
|
+
def get_scenario(self, uuid: str, project_name: str, version: Optional[int] = None) -> Dict[str, Any]:
|
|
237
296
|
"""
|
|
238
|
-
|
|
239
|
-
|
|
297
|
+
Get a specific scenario.
|
|
298
|
+
|
|
240
299
|
Args:
|
|
241
|
-
|
|
242
|
-
|
|
300
|
+
uuid: Scenario UUID
|
|
301
|
+
project_name: Project name (required)
|
|
302
|
+
version: Optional specific version (defaults to latest)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Scenario data
|
|
243
306
|
"""
|
|
307
|
+
endpoint = f"/scenarios/{uuid}"
|
|
308
|
+
params = {"project_name": project_name}
|
|
244
309
|
if version is not None:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
310
|
+
params["scenario_version"] = str(version)
|
|
311
|
+
|
|
312
|
+
response = self.get(endpoint, params=params)
|
|
313
|
+
return response.json()
|
|
314
|
+
|
|
315
|
+
def create_scenario(
|
|
316
|
+
self,
|
|
317
|
+
name: str,
|
|
318
|
+
project_name: str,
|
|
319
|
+
description: Optional[str] = None,
|
|
320
|
+
yaml_content: Optional[str] = None
|
|
321
|
+
) -> Dict[str, Any]:
|
|
322
|
+
"""
|
|
323
|
+
Create a new scenario.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
name: Scenario name
|
|
327
|
+
project_name: Project name to organize the scenario (required)
|
|
328
|
+
description: Optional scenario description
|
|
329
|
+
yaml_content: Optional YAML content defining the scenario
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Created scenario data with UUID and version
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
HTTPException: If scenario name already exists for the project
|
|
336
|
+
"""
|
|
337
|
+
data = {
|
|
338
|
+
"name": name,
|
|
339
|
+
"project_name": project_name
|
|
340
|
+
}
|
|
341
|
+
if description:
|
|
342
|
+
data["description"] = description
|
|
343
|
+
if yaml_content:
|
|
344
|
+
data["yaml_content"] = yaml_content
|
|
345
|
+
|
|
346
|
+
response = self.post("/scenarios", data=data)
|
|
347
|
+
return response.json()
|
|
348
|
+
|
|
349
|
+
# Run CI API methods
|
|
350
|
+
|
|
351
|
+
def create_project(self, name: str, description: Optional[str] = None) -> Dict[str, Any]:
|
|
352
|
+
"""
|
|
353
|
+
Create a new project.
|
|
248
354
|
|
|
249
|
-
|
|
355
|
+
Args:
|
|
356
|
+
name: Project name
|
|
357
|
+
description: Optional project description
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Created project data
|
|
361
|
+
"""
|
|
362
|
+
data = {"name": name}
|
|
363
|
+
if description:
|
|
364
|
+
data["description"] = description
|
|
365
|
+
|
|
366
|
+
response = self.post("/projects", data=data)
|
|
367
|
+
return response.json()
|
|
250
368
|
|
|
251
|
-
def
|
|
252
|
-
|
|
253
|
-
|
|
369
|
+
def get_projects(
|
|
370
|
+
self,
|
|
371
|
+
limit: int = 20,
|
|
372
|
+
offset: int = 0
|
|
373
|
+
) -> Dict[str, Any]:
|
|
374
|
+
"""
|
|
375
|
+
Get list of projects.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
limit: Maximum number of projects to return
|
|
379
|
+
offset: Number of projects to skip
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Project list with pagination info
|
|
383
|
+
"""
|
|
384
|
+
params = {"limit": limit, "offset": offset}
|
|
385
|
+
response = self.get("/projects", params=params)
|
|
254
386
|
return response.json()
|
|
255
387
|
|
|
256
|
-
def
|
|
388
|
+
def get_project(self, name: str) -> Dict[str, Any]:
|
|
257
389
|
"""
|
|
258
|
-
Get a specific
|
|
390
|
+
Get a specific project.
|
|
259
391
|
|
|
260
392
|
Args:
|
|
261
|
-
|
|
262
|
-
version: Specific version (defaults to latest)
|
|
393
|
+
name: Project name
|
|
263
394
|
|
|
264
395
|
Returns:
|
|
265
|
-
|
|
396
|
+
Project data
|
|
266
397
|
"""
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
398
|
+
response = self.get(f"/projects/{name}")
|
|
399
|
+
return response.json()
|
|
400
|
+
|
|
401
|
+
def update_project(self, name: str, description: Optional[str] = None) -> Dict[str, Any]:
|
|
402
|
+
"""
|
|
403
|
+
Update a project.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
name: Project name
|
|
407
|
+
description: Optional new description
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Updated project data
|
|
411
|
+
"""
|
|
412
|
+
data = {}
|
|
413
|
+
if description is not None:
|
|
414
|
+
data["description"] = description
|
|
415
|
+
|
|
416
|
+
response = self.put(f"/projects/{name}", data=data)
|
|
417
|
+
return response.json()
|
|
418
|
+
|
|
419
|
+
def create_run(
|
|
420
|
+
self,
|
|
421
|
+
name: str,
|
|
422
|
+
project_name: str,
|
|
423
|
+
description: Optional[str] = None,
|
|
424
|
+
arguments: Optional[List[Dict[str, str]]] = None,
|
|
425
|
+
tags: Optional[List[str]] = None
|
|
426
|
+
) -> Dict[str, Any]:
|
|
427
|
+
"""
|
|
428
|
+
Create a new run.
|
|
271
429
|
|
|
272
|
-
|
|
430
|
+
Args:
|
|
431
|
+
name: Run name
|
|
432
|
+
project_name: Project name
|
|
433
|
+
description: Optional run description
|
|
434
|
+
arguments: Optional list of key-value arguments
|
|
435
|
+
tags: Optional list of tags
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Created run data with runId
|
|
439
|
+
"""
|
|
440
|
+
data = {
|
|
441
|
+
"name": name,
|
|
442
|
+
"project_name": project_name,
|
|
443
|
+
"arguments": arguments or [],
|
|
444
|
+
"tags": tags or []
|
|
445
|
+
}
|
|
446
|
+
if description:
|
|
447
|
+
data["description"] = description
|
|
448
|
+
|
|
449
|
+
response = self.post("/runs", data=data)
|
|
450
|
+
return response.json()
|
|
451
|
+
|
|
452
|
+
def get_runs(
|
|
453
|
+
self,
|
|
454
|
+
project_name: Optional[str],
|
|
455
|
+
status: Optional[str] = None,
|
|
456
|
+
tags: Optional[List[str]] = None,
|
|
457
|
+
limit: int = 20,
|
|
458
|
+
offset: int = 0
|
|
459
|
+
) -> Dict[str, Any]:
|
|
460
|
+
"""
|
|
461
|
+
Get list of runs.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
project_name: Filter by project name
|
|
465
|
+
status: Filter by status (running, finished, failed, killed)
|
|
466
|
+
tags: Filter by tags
|
|
467
|
+
limit: Maximum number of runs to return
|
|
468
|
+
offset: Number of runs to skip
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Run list with pagination info
|
|
472
|
+
"""
|
|
473
|
+
params = {"limit": limit, "offset": offset}
|
|
474
|
+
if not project_name:
|
|
475
|
+
raise ValueError("project_name is required to get runs.")
|
|
476
|
+
params["project_name"] = project_name
|
|
477
|
+
if status:
|
|
478
|
+
params["status"] = status
|
|
479
|
+
if tags:
|
|
480
|
+
params["tags"] = ",".join(tags)
|
|
481
|
+
|
|
482
|
+
response = self.get("/runs", params=params)
|
|
483
|
+
return response.json()
|
|
484
|
+
|
|
485
|
+
def get_run(self, run_id: str) -> Dict[str, Any]:
|
|
486
|
+
"""
|
|
487
|
+
Get a specific run.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
run_id: Run ID
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Run data
|
|
494
|
+
"""
|
|
495
|
+
response = self.get(f"/runs/{run_id}")
|
|
496
|
+
return response.json()
|
|
497
|
+
|
|
498
|
+
def update_run(
|
|
499
|
+
self,
|
|
500
|
+
run_id: str,
|
|
501
|
+
name: Optional[str] = None,
|
|
502
|
+
description: Optional[str] = None,
|
|
503
|
+
status: Optional[str] = None,
|
|
504
|
+
arguments: Optional[List[Dict[str, str]]] = None,
|
|
505
|
+
tags: Optional[List[str]] = None
|
|
506
|
+
) -> Dict[str, Any]:
|
|
507
|
+
"""
|
|
508
|
+
Update a run.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
run_id: Run ID
|
|
512
|
+
name: Optional new name
|
|
513
|
+
description: Optional new description
|
|
514
|
+
status: Optional new status
|
|
515
|
+
arguments: Optional new arguments
|
|
516
|
+
tags: Optional new tags
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Updated run data
|
|
520
|
+
"""
|
|
521
|
+
data = {}
|
|
522
|
+
if name is not None:
|
|
523
|
+
data["name"] = name
|
|
524
|
+
if description is not None:
|
|
525
|
+
data["description"] = description
|
|
526
|
+
if status is not None:
|
|
527
|
+
data["status"] = status
|
|
528
|
+
if arguments is not None:
|
|
529
|
+
data["arguments"] = arguments
|
|
530
|
+
if tags is not None:
|
|
531
|
+
data["tags"] = tags
|
|
532
|
+
|
|
533
|
+
response = self.put(f"/runs/{run_id}", data=data)
|
|
534
|
+
return response.json()
|
|
535
|
+
|
|
536
|
+
def create_episode(
|
|
537
|
+
self,
|
|
538
|
+
run_id: str,
|
|
539
|
+
episode_name: str,
|
|
540
|
+
status: Optional[str] = None
|
|
541
|
+
) -> Dict[str, Any]:
|
|
542
|
+
"""
|
|
543
|
+
Create a new episode.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
run_id: Run ID
|
|
547
|
+
episode_name: Episode name
|
|
548
|
+
status: Optional episode status
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Created episode data
|
|
552
|
+
"""
|
|
553
|
+
data = {
|
|
554
|
+
"episode_name": episode_name,
|
|
555
|
+
"run_id": run_id
|
|
556
|
+
}
|
|
557
|
+
if status:
|
|
558
|
+
data["status"] = status
|
|
559
|
+
|
|
560
|
+
response = self.post("/episodes", data=data)
|
|
561
|
+
return response.json()
|
|
562
|
+
|
|
563
|
+
def get_episodes(
|
|
564
|
+
self,
|
|
565
|
+
run_id: Optional[str] = None,
|
|
566
|
+
status: Optional[str] = None,
|
|
567
|
+
limit: int = 20,
|
|
568
|
+
offset: int = 0
|
|
569
|
+
) -> Dict[str, Any]:
|
|
570
|
+
"""
|
|
571
|
+
Get list of episodes.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
run_id: Filter by run ID
|
|
575
|
+
status: Filter by status
|
|
576
|
+
limit: Maximum number of episodes to return
|
|
577
|
+
offset: Number of episodes to skip
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Episode list with pagination info
|
|
581
|
+
"""
|
|
582
|
+
params = {"limit": limit, "offset": offset}
|
|
583
|
+
if run_id:
|
|
584
|
+
params["run_id"] = run_id
|
|
585
|
+
if status:
|
|
586
|
+
params["status"] = status
|
|
587
|
+
|
|
588
|
+
response = self.get("/episodes", params=params)
|
|
589
|
+
return response.json()
|
|
590
|
+
|
|
591
|
+
def get_episode(self, run_id: str, episode_name: str) -> Dict[str, Any]:
|
|
592
|
+
"""
|
|
593
|
+
Get a specific episode.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
run_id: Run ID
|
|
597
|
+
episode_name: Episode name
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Episode data
|
|
601
|
+
"""
|
|
602
|
+
response = self.get(f"/episodes/{run_id}/{episode_name}")
|
|
603
|
+
return response.json()
|
|
604
|
+
|
|
605
|
+
def update_episode(
|
|
606
|
+
self,
|
|
607
|
+
run_id: str,
|
|
608
|
+
episode_name: str,
|
|
609
|
+
status: Optional[str] = None
|
|
610
|
+
) -> Dict[str, Any]:
|
|
611
|
+
"""
|
|
612
|
+
Update an episode.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
run_id: Run ID
|
|
616
|
+
episode_name: Episode name
|
|
617
|
+
status: Optional new status
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Updated episode data
|
|
621
|
+
"""
|
|
622
|
+
data = {}
|
|
623
|
+
if status is not None:
|
|
624
|
+
data["status"] = status
|
|
625
|
+
|
|
626
|
+
response = self.put(f"/episodes/{run_id}/{episode_name}", data=data)
|
|
627
|
+
return response.json()
|
|
628
|
+
|
|
629
|
+
def delete_episode(self, run_id: str, episode_name: str) -> None:
|
|
630
|
+
"""
|
|
631
|
+
Delete an episode.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
run_id: Run ID
|
|
635
|
+
episode_name: Episode name
|
|
636
|
+
"""
|
|
637
|
+
self.delete(f"/episodes/{run_id}/{episode_name}")
|
|
638
|
+
|
|
639
|
+
def upload_blob(
|
|
640
|
+
self,
|
|
641
|
+
artifact_key: str,
|
|
642
|
+
run_id: str,
|
|
643
|
+
file_path: str,
|
|
644
|
+
artifact_type: str,
|
|
645
|
+
episode_name: Optional[str] = None,
|
|
646
|
+
description: Optional[str] = None
|
|
647
|
+
) -> Dict[str, Any]:
|
|
648
|
+
"""
|
|
649
|
+
Upload a blob artifact (image/video).
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
artifact_key: Artifact key identifier
|
|
653
|
+
run_id: Run ID
|
|
654
|
+
file_path: Path to file to upload
|
|
655
|
+
artifact_type: Type of artifact ('image' or 'video')
|
|
656
|
+
episode_name: Optional episode name (None for run-level artifacts)
|
|
657
|
+
description: Optional description
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Created artifact data
|
|
661
|
+
"""
|
|
662
|
+
with open(file_path, 'rb') as f:
|
|
663
|
+
files = {'file': f}
|
|
664
|
+
form_data = {
|
|
665
|
+
'artifact_key': artifact_key,
|
|
666
|
+
'run_id': run_id,
|
|
667
|
+
'artifact_type': artifact_type
|
|
668
|
+
}
|
|
669
|
+
if episode_name:
|
|
670
|
+
form_data['episode_name'] = episode_name
|
|
671
|
+
if description:
|
|
672
|
+
form_data['description'] = description
|
|
673
|
+
|
|
674
|
+
response = self.post("/artifacts/blob/upload", files=files, data=form_data)
|
|
675
|
+
return response.json()
|
|
676
|
+
|
|
677
|
+
def upsert_metrics(
|
|
678
|
+
self,
|
|
679
|
+
artifact_key: str,
|
|
680
|
+
run_id: str,
|
|
681
|
+
metric_type: str,
|
|
682
|
+
metric_data: List[Dict[str, Any]],
|
|
683
|
+
episode_name: Optional[str] = None,
|
|
684
|
+
description: Optional[str] = None
|
|
685
|
+
) -> Dict[str, Any]:
|
|
686
|
+
"""
|
|
687
|
+
Upsert metrics artifact (create or append).
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
artifact_key: Artifact key identifier
|
|
691
|
+
run_id: Run ID
|
|
692
|
+
metric_type: Type of metric display ('line', 'bar', 'scatter', 'gauge', 'counter')
|
|
693
|
+
metric_data: List of metric data points with 'key', 'values', 'timestamp'
|
|
694
|
+
episode_name: Optional episode name (None for run-level artifacts)
|
|
695
|
+
description: Optional description
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
Created/updated artifact data
|
|
699
|
+
"""
|
|
700
|
+
data = {
|
|
701
|
+
"artifact_key": artifact_key,
|
|
702
|
+
"run_id": run_id,
|
|
703
|
+
"metric_type": metric_type,
|
|
704
|
+
"metric_data": metric_data
|
|
705
|
+
}
|
|
706
|
+
if episode_name:
|
|
707
|
+
data["episode_name"] = episode_name
|
|
708
|
+
if description:
|
|
709
|
+
data["description"] = description
|
|
710
|
+
|
|
711
|
+
response = self.post("/artifacts/metrics", data=data)
|
|
712
|
+
return response.json()
|
|
713
|
+
|
|
714
|
+
def get_artifacts(
|
|
715
|
+
self,
|
|
716
|
+
run_id: Optional[str] = None,
|
|
717
|
+
episode_name: Optional[str] = None,
|
|
718
|
+
artifact_type: Optional[str] = None,
|
|
719
|
+
limit: int = 20,
|
|
720
|
+
offset: int = 0
|
|
721
|
+
) -> Dict[str, Any]:
|
|
722
|
+
"""
|
|
723
|
+
Get list of artifacts.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
run_id: Filter by run ID
|
|
727
|
+
episode_name: Filter by episode name
|
|
728
|
+
artifact_type: Filter by artifact type
|
|
729
|
+
limit: Maximum number of artifacts to return
|
|
730
|
+
offset: Number of artifacts to skip
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Artifact list with pagination info
|
|
734
|
+
"""
|
|
735
|
+
params = {"limit": limit, "offset": offset}
|
|
736
|
+
if run_id:
|
|
737
|
+
params["run_id"] = run_id
|
|
738
|
+
if episode_name:
|
|
739
|
+
params["episode_name"] = episode_name
|
|
740
|
+
if artifact_type:
|
|
741
|
+
params["artifact_type"] = artifact_type
|
|
742
|
+
|
|
743
|
+
response = self.get("/artifacts", params=params)
|
|
744
|
+
return response.json()
|
|
745
|
+
|
|
746
|
+
def get_artifact(
|
|
747
|
+
self,
|
|
748
|
+
run_id: str,
|
|
749
|
+
episode_name: str,
|
|
750
|
+
artifact_key: str
|
|
751
|
+
) -> Dict[str, Any]:
|
|
752
|
+
"""
|
|
753
|
+
Get a specific artifact.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
run_id: Run ID
|
|
757
|
+
episode_name: Episode name
|
|
758
|
+
artifact_key: Artifact key
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
Artifact data
|
|
762
|
+
"""
|
|
763
|
+
response = self.get(f"/artifacts/{run_id}/{episode_name}/{artifact_key}")
|
|
273
764
|
return response.json()
|