humalab 0.0.5__py3-none-any.whl → 0.0.7__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.

Files changed (42) hide show
  1. humalab/__init__.py +25 -0
  2. humalab/assets/__init__.py +8 -2
  3. humalab/assets/files/resource_file.py +96 -6
  4. humalab/assets/files/urdf_file.py +49 -11
  5. humalab/assets/resource_operator.py +139 -0
  6. humalab/constants.py +48 -5
  7. humalab/dists/__init__.py +7 -0
  8. humalab/dists/bernoulli.py +26 -1
  9. humalab/dists/categorical.py +25 -0
  10. humalab/dists/discrete.py +27 -2
  11. humalab/dists/distribution.py +11 -0
  12. humalab/dists/gaussian.py +27 -2
  13. humalab/dists/log_uniform.py +29 -3
  14. humalab/dists/truncated_gaussian.py +33 -4
  15. humalab/dists/uniform.py +24 -0
  16. humalab/episode.py +291 -11
  17. humalab/humalab.py +93 -38
  18. humalab/humalab_api_client.py +297 -95
  19. humalab/humalab_config.py +49 -0
  20. humalab/humalab_test.py +46 -17
  21. humalab/metrics/__init__.py +11 -5
  22. humalab/metrics/code.py +59 -0
  23. humalab/metrics/metric.py +69 -102
  24. humalab/metrics/scenario_stats.py +163 -0
  25. humalab/metrics/summary.py +45 -24
  26. humalab/run.py +224 -101
  27. humalab/scenarios/__init__.py +11 -0
  28. humalab/{scenario.py → scenarios/scenario.py} +130 -136
  29. humalab/scenarios/scenario_operator.py +114 -0
  30. humalab/{scenario_test.py → scenarios/scenario_test.py} +150 -269
  31. humalab/utils.py +37 -0
  32. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/METADATA +1 -1
  33. humalab-0.0.7.dist-info/RECORD +39 -0
  34. humalab/assets/resource_manager.py +0 -58
  35. humalab/evaluators/__init__.py +0 -16
  36. humalab/humalab_main.py +0 -119
  37. humalab/metrics/dist_metric.py +0 -22
  38. humalab-0.0.5.dist-info/RECORD +0 -37
  39. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/WHEEL +0 -0
  40. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/entry_points.txt +0 -0
  41. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/licenses/LICENSE +0 -0
  42. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,28 @@
1
1
  """HTTP client for accessing HumaLab service APIs with API key authentication."""
2
2
 
3
+ from enum import Enum
3
4
  import os
4
5
  import requests
5
6
  from typing import Dict, Any, Optional, List
6
7
  from urllib.parse import urljoin
8
+ from humalab.humalab_config import HumalabConfig
9
+
10
+
11
+ class RunStatus(Enum):
12
+ """Status of runs"""
13
+ RUNNING = "running"
14
+ CANCELED = "canceled"
15
+ ERRORED = "errored"
16
+ FINISHED = "finished"
17
+
18
+
19
+ class EpisodeStatus(Enum):
20
+ """Status of validation episodes"""
21
+ RUNNING = "running"
22
+ CANCELED = "canceled"
23
+ ERRORED = "errored"
24
+ SUCCESS = "success"
25
+ FAILED = "failed"
7
26
 
8
27
 
9
28
  class HumaLabApiClient:
@@ -19,13 +38,14 @@ class HumaLabApiClient:
19
38
  Initialize the HumaLab API client.
20
39
 
21
40
  Args:
22
- base_url: Base URL for the HumaLab service (defaults to localhost:8000)
41
+ base_url: Base URL for the HumaLab service (defaults to https://api.humalab.ai)
23
42
  api_key: API key for authentication (defaults to HUMALAB_API_KEY env var)
24
43
  timeout: Request timeout in seconds
25
44
  """
26
- self.base_url = base_url or os.getenv("HUMALAB_SERVICE_URL", "http://localhost:8000")
27
- self.api_key = api_key or os.getenv("HUMALAB_API_KEY")
28
- self.timeout = timeout or 30.0 # Default timeout of 30 seconds
45
+ humalab_config = HumalabConfig()
46
+ self.base_url = base_url or humalab_config.base_url or os.getenv("HUMALAB_SERVICE_URL", "https://api.humalab.ai")
47
+ self.api_key = api_key or humalab_config.api_key or os.getenv("HUMALAB_API_KEY")
48
+ self.timeout = timeout or humalab_config.timeout or 30.0 # Default timeout of 30 seconds
29
49
 
30
50
  # Ensure base_url ends without trailing slash
31
51
  self.base_url = self.base_url.rstrip('/')
@@ -77,16 +97,35 @@ class HumaLabApiClient:
77
97
  if files:
78
98
  headers.pop("Content-Type", None)
79
99
 
80
- response = requests.request(
81
- method=method,
82
- url=url,
83
- json=data,
84
- params=params,
85
- files=files,
86
- headers=headers,
87
- timeout=self.timeout,
88
- **kwargs
89
- )
100
+ # Determine if we should send form data or JSON
101
+ # Form data endpoints: /artifacts/code, /artifacts/blob/upload, /artifacts/python
102
+ is_form_endpoint = any(form_path in endpoint for form_path in ['/artifacts/code', '/artifacts/blob', '/artifacts/python'])
103
+
104
+ if is_form_endpoint or files:
105
+ # Send as form data
106
+ headers.pop("Content-Type", None) # Let requests set multipart/form-data
107
+ response = requests.request(
108
+ method=method,
109
+ url=url,
110
+ data=data,
111
+ params=params,
112
+ files=files,
113
+ headers=headers,
114
+ timeout=self.timeout,
115
+ **kwargs
116
+ )
117
+ else:
118
+ # Send as JSON (default behavior)
119
+ response = requests.request(
120
+ method=method,
121
+ url=url,
122
+ json=data,
123
+ params=params,
124
+ files=files,
125
+ headers=headers,
126
+ timeout=self.timeout,
127
+ **kwargs
128
+ )
90
129
 
91
130
  # Raise an exception for HTTP error responses
92
131
  response.raise_for_status()
@@ -123,7 +162,7 @@ class HumaLabApiClient:
123
162
  Returns:
124
163
  User information from the validated token
125
164
  """
126
- response = self.get("/api-key/validate")
165
+ response = self.get("/auth/validate")
127
166
  return response.json()
128
167
 
129
168
  # Convenience methods for common API operations
@@ -134,7 +173,7 @@ class HumaLabApiClient:
134
173
  resource_types: Optional[str] = None,
135
174
  limit: int = 20,
136
175
  offset: int = 0,
137
- latest_only: bool = False
176
+ latest_only: bool = True
138
177
  ) -> Dict[str, Any]:
139
178
  """
140
179
  Get list of all resources.
@@ -258,8 +297,8 @@ class HumaLabApiClient:
258
297
  def get_scenarios(
259
298
  self,
260
299
  project_name: str,
261
- skip: int = 0,
262
- limit: int = 10,
300
+ limit: int = 20,
301
+ offset: int = 0,
263
302
  include_inactive: bool = False,
264
303
  search: Optional[str] = None,
265
304
  status_filter: Optional[str] = None
@@ -269,8 +308,8 @@ class HumaLabApiClient:
269
308
 
270
309
  Args:
271
310
  project_name: Project name (required)
272
- skip: Number of scenarios to skip for pagination
273
311
  limit: Maximum number of scenarios to return (1-100)
312
+ offset: Number of scenarios to skip
274
313
  include_inactive: Include inactive scenarios in results
275
314
  search: Search term to filter by name, description, or UUID
276
315
  status_filter: Filter by specific status
@@ -280,7 +319,7 @@ class HumaLabApiClient:
280
319
  """
281
320
  params = {
282
321
  "project_name": project_name,
283
- "skip": skip,
322
+ "skip": offset,
284
323
  "limit": limit,
285
324
  "include_inactive": include_inactive
286
325
  }
@@ -441,7 +480,8 @@ class HumaLabApiClient:
441
480
  "name": name,
442
481
  "project_name": project_name,
443
482
  "arguments": arguments or [],
444
- "tags": tags or []
483
+ "tags": tags or [],
484
+ "status": RunStatus.RUNNING.value
445
485
  }
446
486
  if description:
447
487
  data["description"] = description
@@ -452,7 +492,7 @@ class HumaLabApiClient:
452
492
  def get_runs(
453
493
  self,
454
494
  project_name: Optional[str],
455
- status: Optional[str] = None,
495
+ status: Optional[RunStatus] = None,
456
496
  tags: Optional[List[str]] = None,
457
497
  limit: int = 20,
458
498
  offset: int = 0
@@ -475,7 +515,7 @@ class HumaLabApiClient:
475
515
  raise ValueError("project_name is required to get runs.")
476
516
  params["project_name"] = project_name
477
517
  if status:
478
- params["status"] = status
518
+ params["status"] = status.value
479
519
  if tags:
480
520
  params["tags"] = ",".join(tags)
481
521
 
@@ -500,7 +540,8 @@ class HumaLabApiClient:
500
540
  run_id: str,
501
541
  name: Optional[str] = None,
502
542
  description: Optional[str] = None,
503
- status: Optional[str] = None,
543
+ status: Optional[RunStatus] = None,
544
+ err_msg: Optional[str] = None,
504
545
  arguments: Optional[List[Dict[str, str]]] = None,
505
546
  tags: Optional[List[str]] = None
506
547
  ) -> Dict[str, Any]:
@@ -512,6 +553,7 @@ class HumaLabApiClient:
512
553
  name: Optional new name
513
554
  description: Optional new description
514
555
  status: Optional new status
556
+ err_msg: Optional error message
515
557
  arguments: Optional new arguments
516
558
  tags: Optional new tags
517
559
 
@@ -524,7 +566,9 @@ class HumaLabApiClient:
524
566
  if description is not None:
525
567
  data["description"] = description
526
568
  if status is not None:
527
- data["status"] = status
569
+ data["status"] = status.value
570
+ if err_msg is not None:
571
+ data["err_msg"] = err_msg
528
572
  if arguments is not None:
529
573
  data["arguments"] = arguments
530
574
  if tags is not None:
@@ -536,26 +580,26 @@ class HumaLabApiClient:
536
580
  def create_episode(
537
581
  self,
538
582
  run_id: str,
539
- episode_name: str,
540
- status: Optional[str] = None
583
+ episode_id: str,
584
+ status: Optional[EpisodeStatus] = None
541
585
  ) -> Dict[str, Any]:
542
586
  """
543
587
  Create a new episode.
544
588
 
545
589
  Args:
546
590
  run_id: Run ID
547
- episode_name: Episode name
591
+ episode_id: Episode name
548
592
  status: Optional episode status
549
593
 
550
594
  Returns:
551
595
  Created episode data
552
596
  """
553
597
  data = {
554
- "episode_name": episode_name,
598
+ "episode_id": episode_id,
555
599
  "run_id": run_id
556
600
  }
557
601
  if status:
558
- data["status"] = status
602
+ data["status"] = status.value
559
603
 
560
604
  response = self.post("/episodes", data=data)
561
605
  return response.json()
@@ -563,7 +607,7 @@ class HumaLabApiClient:
563
607
  def get_episodes(
564
608
  self,
565
609
  run_id: Optional[str] = None,
566
- status: Optional[str] = None,
610
+ status: Optional[EpisodeStatus] = None,
567
611
  limit: int = 20,
568
612
  offset: int = 0
569
613
  ) -> Dict[str, Any]:
@@ -583,182 +627,340 @@ class HumaLabApiClient:
583
627
  if run_id:
584
628
  params["run_id"] = run_id
585
629
  if status:
586
- params["status"] = status
630
+ params["status"] = status.value
587
631
 
588
632
  response = self.get("/episodes", params=params)
589
633
  return response.json()
590
634
 
591
- def get_episode(self, run_id: str, episode_name: str) -> Dict[str, Any]:
635
+ def get_episode(self, run_id: str, episode_id: str) -> Dict[str, Any]:
592
636
  """
593
637
  Get a specific episode.
594
638
 
595
639
  Args:
596
640
  run_id: Run ID
597
- episode_name: Episode name
641
+ episode_id: Episode name
598
642
 
599
643
  Returns:
600
644
  Episode data
601
645
  """
602
- response = self.get(f"/episodes/{run_id}/{episode_name}")
646
+ response = self.get(f"/episodes/{run_id}/{episode_id}")
603
647
  return response.json()
604
648
 
605
649
  def update_episode(
606
650
  self,
607
651
  run_id: str,
608
- episode_name: str,
609
- status: Optional[str] = None
652
+ episode_id: str,
653
+ status: Optional[EpisodeStatus] = None,
654
+ err_msg: Optional[str] = None
610
655
  ) -> Dict[str, Any]:
611
656
  """
612
657
  Update an episode.
613
658
 
614
659
  Args:
615
660
  run_id: Run ID
616
- episode_name: Episode name
661
+ episode_id: Episode name
617
662
  status: Optional new status
618
-
663
+ err_msg: Optional error message
664
+
619
665
  Returns:
620
666
  Updated episode data
621
667
  """
622
668
  data = {}
623
669
  if status is not None:
624
- data["status"] = status
625
-
626
- response = self.put(f"/episodes/{run_id}/{episode_name}", data=data)
670
+ data["status"] = status.value
671
+ if err_msg is not None:
672
+ data["err_msg"] = err_msg
673
+ response = self.put(f"/episodes/{run_id}/{episode_id}", data=data)
627
674
  return response.json()
628
675
 
629
- def delete_episode(self, run_id: str, episode_name: str) -> None:
676
+ def delete_episode(self, run_id: str, episode_id: str) -> None:
630
677
  """
631
678
  Delete an episode.
632
679
 
633
680
  Args:
634
681
  run_id: Run ID
635
- episode_name: Episode name
682
+ episode_id: Episode name
636
683
  """
637
- self.delete(f"/episodes/{run_id}/{episode_name}")
684
+ self.delete(f"/episodes/{run_id}/{episode_id}")
638
685
 
639
686
  def upload_blob(
640
687
  self,
641
688
  artifact_key: str,
642
689
  run_id: str,
643
- file_path: str,
644
690
  artifact_type: str,
645
- episode_name: Optional[str] = None,
646
- description: Optional[str] = None
691
+ file_content: bytes | None = None,
692
+ file_path: str | None = None,
693
+ episode_id: Optional[str] = None,
694
+ filename: Optional[str] = None,
695
+ content_type: Optional[str] = None
647
696
  ) -> Dict[str, Any]:
648
697
  """
649
698
  Upload a blob artifact (image/video).
650
-
699
+
651
700
  Args:
652
701
  artifact_key: Artifact key identifier
653
702
  run_id: Run ID
654
- file_path: Path to file to upload
655
703
  artifact_type: Type of artifact ('image' or 'video')
656
- episode_name: Optional episode name (None for run-level artifacts)
657
- description: Optional description
658
-
704
+ file_content: File content as bytes
705
+ file_path: Path to file to upload
706
+ episode_id: Optional episode ID (None for run-level artifacts)
707
+ filename: Optional filename to use for the uploaded file
708
+ content_type: Optional content type (e.g., 'image/png', 'video/mp4')
709
+
659
710
  Returns:
660
711
  Created artifact data
661
712
  """
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
-
713
+ form_data = {
714
+ 'artifact_key': artifact_key,
715
+ 'run_id': run_id,
716
+ 'artifact_type': artifact_type
717
+ }
718
+ if episode_id:
719
+ form_data['episode_id'] = episode_id
720
+ if filename:
721
+ form_data['filename'] = filename
722
+ if content_type:
723
+ form_data['content_type'] = content_type
724
+
725
+ if file_path:
726
+ with open(file_path, 'rb') as f:
727
+ files = {'file': f}
728
+ response = self.post("/artifacts/blob/upload", files=files, data=form_data)
729
+ elif file_content:
730
+ files = {'file': ('blob', file_content)}
674
731
  response = self.post("/artifacts/blob/upload", files=files, data=form_data)
675
- return response.json()
732
+ else:
733
+ raise ValueError("Either file_path or file_content must be provided for blob upload.")
734
+ return response.json()
676
735
 
677
736
  def upsert_metrics(
678
737
  self,
679
738
  artifact_key: str,
680
739
  run_id: str,
681
740
  metric_type: str,
682
- metric_data: List[Dict[str, Any]],
683
- episode_name: Optional[str] = None,
684
- description: Optional[str] = None
741
+ metric_data: Optional[List[Dict[str, Any]]] = None,
742
+ episode_id: Optional[str] = None
685
743
  ) -> Dict[str, Any]:
686
744
  """
687
745
  Upsert metrics artifact (create or append).
688
-
746
+
689
747
  Args:
690
748
  artifact_key: Artifact key identifier
691
749
  run_id: Run ID
692
750
  metric_type: Type of metric display ('line', 'bar', 'scatter', 'gauge', 'counter')
693
751
  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
-
752
+ episode_id: Optional episode ID (None for run-level artifacts)
753
+
697
754
  Returns:
698
755
  Created/updated artifact data
699
756
  """
700
757
  data = {
701
758
  "artifact_key": artifact_key,
702
759
  "run_id": run_id,
703
- "metric_type": metric_type,
704
- "metric_data": metric_data
760
+ "metric_type": metric_type
705
761
  }
706
- if episode_name:
707
- data["episode_name"] = episode_name
708
- if description:
709
- data["description"] = description
710
-
762
+ if episode_id:
763
+ data["episode_id"] = episode_id
764
+ if metric_data:
765
+ data["metric_data"] = metric_data
766
+
711
767
  response = self.post("/artifacts/metrics", data=data)
712
768
  return response.json()
713
769
 
714
770
  def get_artifacts(
715
771
  self,
716
772
  run_id: Optional[str] = None,
717
- episode_name: Optional[str] = None,
773
+ episode_id: Optional[str] = None,
718
774
  artifact_type: Optional[str] = None,
719
775
  limit: int = 20,
720
776
  offset: int = 0
721
777
  ) -> Dict[str, Any]:
722
778
  """
723
779
  Get list of artifacts.
724
-
780
+
725
781
  Args:
726
782
  run_id: Filter by run ID
727
- episode_name: Filter by episode name
783
+ episode_id: Filter by episode ID
728
784
  artifact_type: Filter by artifact type
729
- limit: Maximum number of artifacts to return
785
+ limit: Maximum number of artifacts to return (0 for no limit)
730
786
  offset: Number of artifacts to skip
731
-
787
+
732
788
  Returns:
733
789
  Artifact list with pagination info
734
790
  """
735
791
  params = {"limit": limit, "offset": offset}
736
792
  if run_id:
737
793
  params["run_id"] = run_id
738
- if episode_name:
739
- params["episode_name"] = episode_name
794
+ if episode_id:
795
+ params["episode_id"] = episode_id
740
796
  if artifact_type:
741
797
  params["artifact_type"] = artifact_type
742
-
798
+
743
799
  response = self.get("/artifacts", params=params)
744
800
  return response.json()
745
801
 
746
802
  def get_artifact(
747
- self,
748
- run_id: str,
749
- episode_name: str,
803
+ self,
804
+ run_id: str,
805
+ episode_id: str,
750
806
  artifact_key: str
751
807
  ) -> Dict[str, Any]:
752
808
  """
753
809
  Get a specific artifact.
754
-
810
+
755
811
  Args:
756
812
  run_id: Run ID
757
- episode_name: Episode name
813
+ episode_id: Episode ID
758
814
  artifact_key: Artifact key
759
-
815
+
760
816
  Returns:
761
817
  Artifact data
762
818
  """
763
- response = self.get(f"/artifacts/{run_id}/{episode_name}/{artifact_key}")
819
+ response = self.get(f"/artifacts/{run_id}/{episode_id}/{artifact_key}")
820
+ return response.json()
821
+
822
+ def upload_code(
823
+ self,
824
+ artifact_key: str,
825
+ run_id: str,
826
+ code_content: str,
827
+ episode_id: Optional[str] = None
828
+ ) -> Dict[str, Any]:
829
+ """
830
+ Upload code artifact (YAML/string content).
831
+
832
+ Args:
833
+ artifact_key: Artifact key identifier
834
+ run_id: Run ID
835
+ code_content: Code/text content to upload
836
+ episode_id: Optional episode ID (None for run-level artifacts)
837
+
838
+ Returns:
839
+ Created artifact data
840
+ """
841
+ data = {
842
+ 'artifact_key': artifact_key,
843
+ 'run_id': run_id,
844
+ 'code_content': code_content
845
+ }
846
+ if episode_id:
847
+ data['episode_id'] = episode_id
848
+
849
+ response = self.post("/artifacts/code", data=data)
850
+ return response.json()
851
+
852
+ def upload_python(
853
+ self,
854
+ artifact_key: str,
855
+ run_id: str,
856
+ pickled_bytes: bytes,
857
+ episode_id: Optional[str] = None
858
+ ) -> Dict[str, Any]:
859
+ """
860
+ Upload pickled Python object as artifact.
861
+
862
+ Args:
863
+ artifact_key: Artifact key identifier
864
+ run_id: Run ID
865
+ pickled_bytes: Pickled Python object as bytes
866
+ episode_id: Optional episode ID (None for run-level artifacts)
867
+
868
+ Returns:
869
+ Created artifact data
870
+ """
871
+ data = {
872
+ 'artifact_key': artifact_key,
873
+ 'run_id': run_id
874
+ }
875
+ if episode_id:
876
+ data['episode_id'] = episode_id
877
+
878
+ files = {'file': pickled_bytes}
879
+ response = self.post("/artifacts/python", files=files, data=data)
880
+ return response.json()
881
+
882
+ def upload_scenario_stats_artifact(
883
+ self,
884
+ artifact_key: str,
885
+ run_id: str,
886
+ pickled_bytes: bytes,
887
+ graph_type: str,
888
+ ) -> Dict[str, Any]:
889
+ """
890
+ Upload scenario stats artifact (pickled Python dict data).
891
+ This is an upsert operation - creates if doesn't exist, appends if it does.
892
+ Run-level only (no episode_id support).
893
+
894
+ Args:
895
+ artifact_key: Artifact key identifier
896
+ run_id: Run ID
897
+ pickled_bytes: Pickled Python dict as bytes containing scenario stats
898
+ graph_type: Graph display type - one of: 'line', 'bar', 'scatter',
899
+ 'histogram', 'gaussian', 'heatmap', '3d_map'
900
+
901
+ Returns:
902
+ Created/updated artifact data
903
+ """
904
+ data = {
905
+ 'artifact_key': artifact_key,
906
+ 'run_id': run_id,
907
+ 'graph_type': graph_type
908
+ }
909
+
910
+ files = {'file': pickled_bytes}
911
+ response = self.post("/artifacts/scenario_stats", files=files, data=data)
912
+ return response.json()
913
+
914
+ def download_artifact(
915
+ self,
916
+ run_id: str,
917
+ episode_id: str,
918
+ artifact_key: str
919
+ ) -> bytes:
920
+ """
921
+ Download a blob artifact file.
922
+
923
+ Args:
924
+ run_id: Run ID
925
+ episode_id: Episode ID
926
+ artifact_key: Artifact key
927
+
928
+ Returns:
929
+ Artifact file content as bytes
930
+ """
931
+ endpoint = f"/artifacts/{run_id}/{episode_id}/{artifact_key}/download"
932
+ response = self.get(endpoint)
933
+ return response.content
934
+
935
+ def upload_metrics(
936
+ self,
937
+ run_id: str,
938
+ artifact_key: str,
939
+ pickled_bytes: bytes,
940
+ graph_type: str,
941
+ episode_id: str | None = None,
942
+ ) -> Dict[str, Any]:
943
+ """
944
+ Upload metrics artifact.
945
+
946
+ Args:
947
+ run_id: Run ID
948
+ artifact_key: Artifact key
949
+ pickled_bytes: Pickled metrics data as bytes
950
+ graph_type: Optional new graph type
951
+ episode_id: Optional new episode ID
952
+
953
+ Returns:
954
+ Updated artifact data
955
+ """
956
+ data = {
957
+ "run_id": run_id,
958
+ "artifact_key": artifact_key,
959
+ 'graph_type': graph_type
960
+ }
961
+ files = {'file': pickled_bytes}
962
+ if episode_id:
963
+ data["episode_id"] = episode_id
964
+
965
+ response = self.post("/artifacts/metrics", files=files, data=data)
764
966
  return response.json()