humalab 0.0.1__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.

@@ -0,0 +1,64 @@
1
+ from humalab_sdk.dists.distribution import Distribution
2
+ from typing import Any
3
+ import numpy as np
4
+
5
+
6
+ class TruncatedGaussian(Distribution):
7
+ def __init__(self,
8
+ generator: np.random.Generator,
9
+ loc: float | Any,
10
+ scale: float | Any,
11
+ low: float | Any,
12
+ high: float | Any,
13
+ size: int | tuple[int, ...] | None = None) -> None:
14
+ """
15
+ Initialize the truncated Gaussian (normal) distribution.
16
+
17
+ Args:
18
+ generator (np.random.Generator): The random number generator.
19
+ loc (float | Any): The mean of the distribution.
20
+ scale (float | Any): The standard deviation of the distribution.
21
+ low (float | Any): The lower truncation bound.
22
+ high (float | Any): The upper truncation bound.
23
+ size (int | tuple[int, ...] | None): The size of the output.
24
+ """
25
+ super().__init__(generator=generator)
26
+ self._loc = loc
27
+ self._scale = scale
28
+ self._low = low
29
+ self._high = high
30
+ self._size = size
31
+
32
+ def _sample(self) -> int | float | np.ndarray:
33
+ samples = self._generator.normal(loc=self._loc, scale=self._scale, size=self._size)
34
+ mask = (samples < self._low) | (samples > self._high)
35
+ while np.any(mask):
36
+ samples[mask] = self._generator.normal(loc=self._loc, scale=self._scale, size=np.sum(mask))
37
+ mask = (samples < self._low) | (samples > self._high)
38
+ return samples
39
+
40
+ def __repr__(self) -> str:
41
+ return f"TruncatedGaussian(loc={self._loc}, scale={self._scale}, low={self._low}, high={self._high}, size={self._size})"
42
+
43
+ @staticmethod
44
+ def create(generator: np.random.Generator,
45
+ loc: float | Any,
46
+ scale: float | Any,
47
+ low: float | Any,
48
+ high: float | Any,
49
+ size: int | tuple[int, ...] | None = None) -> 'TruncatedGaussian':
50
+ """
51
+ Create a truncated Gaussian (normal) distribution.
52
+
53
+ Args:
54
+ generator (np.random.Generator): The random number generator.
55
+ loc (float | Any): The mean of the distribution.
56
+ scale (float | Any): The standard deviation of the distribution.
57
+ low (float | Any): The lower truncation bound.
58
+ high (float | Any): The upper truncation bound.
59
+ size (int | tuple[int, ...] | None): The size of the output.
60
+
61
+ Returns:
62
+ TruncatedGaussian: The created truncated Gaussian distribution.
63
+ """
64
+ return TruncatedGaussian(generator=generator, loc=loc, scale=scale, low=low, high=high, size=size)
@@ -0,0 +1,49 @@
1
+ from humalab_sdk.dists.distribution import Distribution
2
+
3
+ from typing import Any
4
+ import numpy as np
5
+
6
+ class Uniform(Distribution):
7
+ def __init__(self,
8
+ generator: np.random.Generator,
9
+ low: float | Any,
10
+ high: float | Any,
11
+ size: int | tuple[int, ...] | None = None, ) -> None:
12
+ """
13
+ Initialize the uniform distribution.
14
+
15
+ Args:
16
+ generator (np.random.Generator): The random number generator.
17
+ low (float | Any): The lower bound (inclusive).
18
+ high (float | Any): The upper bound (exclusive).
19
+ size (int | tuple[int, ...] | None): The size of the output.
20
+ """
21
+ super().__init__(generator=generator)
22
+ self._low = np.array(low)
23
+ self._high = np.array(high)
24
+ self._size = size
25
+
26
+ def _sample(self) -> int | float | np.ndarray:
27
+ return self._generator.uniform(self._low, self._high, size=self._size)
28
+
29
+ def __repr__(self) -> str:
30
+ return f"Uniform(low={self._low}, high={self._high}, size={self._size})"
31
+
32
+ @staticmethod
33
+ def create(generator: np.random.Generator,
34
+ low: float | Any,
35
+ high: float | Any,
36
+ size: int | tuple[int, ...] | None = None) -> 'Uniform':
37
+ """
38
+ Create a uniform distribution.
39
+
40
+ Args:
41
+ generator (np.random.Generator): The random number generator.
42
+ low (float | Any): The lower bound (inclusive).
43
+ high (float | Any): The upper bound (exclusive).
44
+ size (int | tuple[int, ...] | None): The size of the output.
45
+
46
+ Returns:
47
+ Uniform: The created uniform distribution.
48
+ """
49
+ return Uniform(generator=generator, low=low, high=high, size=size)
humalab/humalab.py ADDED
@@ -0,0 +1,149 @@
1
+ from contextlib import contextmanager
2
+ from humalab_sdk.run import Run
3
+ from humalab_sdk.humalab_config import HumalabConfig
4
+ from humalab_sdk.humalab_api_client import HumaLabApiClient
5
+ from humalab_sdk.constants import EpisodeStatus
6
+
7
+ import uuid
8
+ import os
9
+
10
+ from collections.abc import Generator
11
+
12
+ from humalab_sdk.scenario import Scenario
13
+
14
+ _cur_run: Run | None = None
15
+
16
+ def _pull_scenario(client: HumaLabApiClient,
17
+ scenario: str | list | dict | None = None,
18
+ scenario_id: str | None = None,) -> str | list | dict | None:
19
+ if scenario_id is not None:
20
+ scenario_response = client.get_scenario(uuid=scenario_id)
21
+ return scenario_response["yaml_content"]
22
+ return scenario
23
+
24
+ @contextmanager
25
+ def init(entity: str | None = None,
26
+ project: str | None = None,
27
+ name: str | None = None,
28
+ description: str | None = None,
29
+ id: str | None = None,
30
+ tags: list[str] | None = None,
31
+ scenario: str | list | dict | None = None,
32
+ scenario_id: str | None = None,
33
+ base_url: str | None = None,
34
+ api_key: str | None = None,
35
+ seed: int | None=None,
36
+ timeout: float | None = None,
37
+ num_env: int | None = None
38
+ ) -> Generator[Run, None, None]:
39
+ global _cur_run
40
+ run = None
41
+ try:
42
+ humalab_config = HumalabConfig()
43
+ entity = entity or humalab_config.entity
44
+ project = project or "default"
45
+ name = name or ""
46
+ description = description or ""
47
+ id = id or str(uuid.uuid4())
48
+
49
+ base_url = base_url or humalab_config.base_url
50
+ api_key = api_key or humalab_config.api_key
51
+ timeout = timeout or humalab_config.timeout
52
+
53
+ api_client = HumaLabApiClient(base_url=base_url,
54
+ api_key=api_key,
55
+ timeout=timeout)
56
+ final_scenario = _pull_scenario(client=api_client,
57
+ scenario=scenario,
58
+ scenario_id=scenario_id)
59
+ scenario_inst = Scenario()
60
+ scenario_inst.init(run_id=id,
61
+ scenario=final_scenario,
62
+ seed=seed,
63
+ episode_id=str(uuid.uuid4()),
64
+ num_env=num_env)
65
+
66
+ run = Run(
67
+ entity=entity,
68
+ project=project,
69
+ name=name,
70
+ description=description,
71
+ id=id,
72
+ tags=tags,
73
+ scenario=scenario_inst,
74
+ )
75
+ _cur_run = run
76
+ yield run
77
+ finally:
78
+ if run:
79
+ run.finish()
80
+
81
+
82
+ def finish(status: EpisodeStatus = EpisodeStatus.PASS,
83
+ quiet: bool | None = None) -> None:
84
+ global _cur_run
85
+ if _cur_run:
86
+ _cur_run.finish(status=status, quiet=quiet)
87
+
88
+ def login(api_key: str | None = None,
89
+ relogin: bool | None = None,
90
+ host: str | None = None,
91
+ force: bool | None = None,
92
+ timeout: float | None = None) -> bool:
93
+ # TODO: Validate api_key against host given.
94
+ # and retrieve entity information.
95
+ humalab_config = HumalabConfig()
96
+ humalab_config.api_key = api_key or humalab_config.api_key
97
+ humalab_config.base_url = host or humalab_config.base_url
98
+ humalab_config.timeout = timeout or humalab_config.timeout
99
+ return True
100
+
101
+
102
+ if __name__ == "__main__":
103
+ login(api_key="GSdIImnRJs1TQRpkN74PyIVHhX8_PISLOI9NVF6uO94",
104
+ host="http://localhost:8000")
105
+
106
+ with init(entity="default",
107
+ project="test",
108
+ name="my first run",
109
+ description="testing the humalab sdk",
110
+ tags=["tag1", "tag2"],
111
+ scenario_id="cb9668c6-99fe-490c-a97c-e8c1f06b54a6",
112
+ num_env=None) as run:
113
+ print(f"Run ID: {run.id}")
114
+ print(f"Run Name: {run.name}")
115
+ print(f"Run Description: {run.description}")
116
+ print(f"Run Tags: {run.tags}")
117
+ print(f"Run Scenario YAML:\n{run.scenario.yaml}")
118
+
119
+ scenario = run.scenario
120
+ # Simulate some operations
121
+ print("CUP position: ", scenario.scenario.cup.position)
122
+ print("CUP orientation: ", scenario.scenario.cup.orientation)
123
+ print("Asset: ", scenario.scenario.cup.asset)
124
+ print("Friction: ", scenario.scenario.cup.friction)
125
+ print("Num Objects: ", scenario.scenario.num_objects)
126
+ scenario.reset()
127
+ print("======================SCENARIO RESET==========================")
128
+ print("CUP position: ", scenario.scenario.cup.position)
129
+ print("CUP orientation: ", scenario.scenario.cup.orientation)
130
+ print("Asset: ", scenario.scenario.cup.asset)
131
+ print("Friction: ", scenario.scenario.cup.friction)
132
+ print("Num Objects: ", scenario.scenario.num_objects)
133
+
134
+ humalab_config = HumalabConfig()
135
+ base_url = humalab_config.base_url
136
+ api_key = humalab_config.api_key
137
+ timeout = humalab_config.timeout
138
+
139
+ api_client = HumaLabApiClient(base_url=base_url,
140
+ api_key=api_key,
141
+ timeout=timeout)
142
+ resource = api_client.get_resource(name="lerobot", version=1)
143
+ print("Resource metadata: ", resource)
144
+ file_content = api_client.download_resource(name="lerobot")
145
+ filename = os.path.basename(resource['resource_url'])
146
+ filename = os.path.join(humalab_config.workspace_path, filename)
147
+ with open(filename, "wb") as f:
148
+ f.write(file_content)
149
+ print(f"Resource file downloaded: {filename}")
@@ -0,0 +1,273 @@
1
+ """HTTP client for accessing HumaLab service APIs with API key authentication."""
2
+
3
+ import os
4
+ import requests
5
+ from typing import Dict, Any, Optional, List
6
+ from urllib.parse import urljoin
7
+
8
+
9
+ class HumaLabApiClient:
10
+ """HTTP client for making authenticated requests to HumaLab service APIs."""
11
+
12
+ def __init__(
13
+ self,
14
+ base_url: str | None = None,
15
+ api_key: str | None = None,
16
+ timeout: float | None = None
17
+ ):
18
+ """
19
+ Initialize the HumaLab API client.
20
+
21
+ Args:
22
+ base_url: Base URL for the HumaLab service (defaults to localhost:8000)
23
+ api_key: API key for authentication (defaults to HUMALAB_API_KEY env var)
24
+ timeout: Request timeout in seconds
25
+ """
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
29
+
30
+ # Ensure base_url ends without trailing slash
31
+ self.base_url = self.base_url.rstrip('/')
32
+
33
+ if not self.api_key:
34
+ raise ValueError(
35
+ "API key is required. Set HUMALAB_API_KEY environment variable "
36
+ "or pass api_key parameter to HumaLabApiClient constructor."
37
+ )
38
+
39
+ def _get_headers(self) -> Dict[str, str]:
40
+ """Get common headers for API requests."""
41
+ return {
42
+ "Authorization": f"Bearer {self.api_key}",
43
+ "Content-Type": "application/json",
44
+ "User-Agent": "HumaLab-SDK/1.0"
45
+ }
46
+
47
+ def _make_request(
48
+ self,
49
+ method: str,
50
+ endpoint: str,
51
+ data: Optional[Dict[str, Any]] = None,
52
+ params: Optional[Dict[str, Any]] = None,
53
+ files: Optional[Dict[str, Any]] = None,
54
+ **kwargs
55
+ ) -> requests.Response:
56
+ """
57
+ Make an HTTP request to the HumaLab service.
58
+
59
+ Args:
60
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
61
+ endpoint: API endpoint (will be joined with base_url)
62
+ data: JSON data for request body
63
+ params: Query parameters
64
+ files: Files for multipart upload
65
+ **kwargs: Additional arguments passed to requests
66
+
67
+ Returns:
68
+ requests.Response object
69
+
70
+ Raises:
71
+ requests.exceptions.RequestException: For HTTP errors
72
+ """
73
+ url = urljoin(self.base_url + "/", endpoint.lstrip('/'))
74
+ headers = self._get_headers()
75
+
76
+ # If files are being uploaded, don't set Content-Type (let requests handle it)
77
+ if files:
78
+ headers.pop("Content-Type", None)
79
+
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
+ )
90
+
91
+ # Raise an exception for HTTP error responses
92
+ response.raise_for_status()
93
+
94
+ return response
95
+
96
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> requests.Response:
97
+ """Make a GET request."""
98
+ return self._make_request("GET", endpoint, params=params, **kwargs)
99
+
100
+ def post(
101
+ self,
102
+ endpoint: str,
103
+ data: Optional[Dict[str, Any]] = None,
104
+ files: Optional[Dict[str, Any]] = None,
105
+ **kwargs
106
+ ) -> requests.Response:
107
+ """Make a POST request."""
108
+ return self._make_request("POST", endpoint, data=data, files=files, **kwargs)
109
+
110
+ def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> requests.Response:
111
+ """Make a PUT request."""
112
+ return self._make_request("PUT", endpoint, data=data, **kwargs)
113
+
114
+ def delete(self, endpoint: str, **kwargs) -> requests.Response:
115
+ """Make a DELETE request."""
116
+ return self._make_request("DELETE", endpoint, **kwargs)
117
+
118
+ # Convenience methods for common API operations
119
+
120
+ def get_resources(
121
+ self,
122
+ resource_types: Optional[str] = None,
123
+ limit: int = 20,
124
+ offset: int = 0,
125
+ latest_only: bool = False
126
+ ) -> Dict[str, Any]:
127
+ """
128
+ Get list of all resources.
129
+
130
+ Args:
131
+ resource_types: Comma-separated resource types to filter by
132
+ limit: Maximum number of resources to return
133
+ offset: Number of resources to skip
134
+ latest_only: If true, only return latest version of each resource
135
+
136
+ Returns:
137
+ Resource list with pagination info
138
+ """
139
+ params = {
140
+ "limit": limit,
141
+ "offset": offset,
142
+ "latest_only": latest_only
143
+ }
144
+ if resource_types:
145
+ params["resource_types"] = resource_types
146
+
147
+ response = self.get("/resources", params=params)
148
+ return response.json()
149
+
150
+ def get_resource(self, name: str, version: Optional[int] = None) -> Dict[str, Any]:
151
+ """
152
+ Get a specific resource.
153
+
154
+ Args:
155
+ name: Resource name
156
+ version: Specific version (defaults to latest)
157
+
158
+ Returns:
159
+ Resource data
160
+ """
161
+ if version is not None:
162
+ endpoint = f"/resources/{name}/{version}"
163
+ else:
164
+ endpoint = f"/resources/{name}"
165
+
166
+ response = self.get(endpoint)
167
+ return response.json()
168
+
169
+ def download_resource(
170
+ self,
171
+ name: str,
172
+ version: Optional[int] = None
173
+ ) -> bytes:
174
+ """
175
+ Download a resource file.
176
+
177
+ Args:
178
+ name: Resource name
179
+ version: Specific version (defaults to latest)
180
+
181
+ Returns:
182
+ Resource file content as bytes
183
+ """
184
+ if version is not None:
185
+ endpoint = f"/resources/{name}/download?version={version}"
186
+ else:
187
+ endpoint = f"/resources/{name}/download"
188
+
189
+ response = self.get(endpoint)
190
+ return response.content
191
+
192
+ def upload_resource(
193
+ self,
194
+ name: str,
195
+ file_path: str,
196
+ resource_type: str,
197
+ description: Optional[str] = None,
198
+ filename: Optional[str] = None,
199
+ allow_duplicate_name: bool = False
200
+ ) -> Dict[str, Any]:
201
+ """
202
+ Upload a resource file.
203
+
204
+ Args:
205
+ name: Resource name
206
+ file_path: Path to file to upload
207
+ resource_type: Type of resource (urdf, mjcf, etc.)
208
+ description: Optional description
209
+ filename: Optional custom filename
210
+ allow_duplicate_name: Allow creating new version with existing name
211
+
212
+ Returns:
213
+ Created resource data
214
+ """
215
+ with open(file_path, 'rb') as f:
216
+ files = {'file': f}
217
+ data = {}
218
+ if description:
219
+ data['description'] = description
220
+ if filename:
221
+ data['filename'] = filename
222
+
223
+ params = {
224
+ 'resource_type': resource_type,
225
+ 'allow_duplicate_name': allow_duplicate_name
226
+ }
227
+
228
+ response = self.post(f"/resources/{name}/upload", files=files, params=params)
229
+ return response.json()
230
+
231
+ def get_resource_types(self) -> List[str]:
232
+ """Get list of all available resource types."""
233
+ response = self.get("/resources/types")
234
+ return response.json()
235
+
236
+ def delete_resource(self, name: str, version: Optional[int] = None) -> None:
237
+ """
238
+ Delete a resource.
239
+
240
+ Args:
241
+ name: Resource name
242
+ version: Specific version to delete (if None, deletes all versions)
243
+ """
244
+ if version is not None:
245
+ endpoint = f"/resources/{name}/{version}"
246
+ else:
247
+ endpoint = f"/resources/{name}"
248
+
249
+ self.delete(endpoint)
250
+
251
+ def get_scenarios(self) -> List[Dict[str, Any]]:
252
+ """Get list of all scenarios."""
253
+ response = self.get("/scenarios")
254
+ return response.json()
255
+
256
+ def get_scenario(self, uuid: str, version: Optional[int] = None) -> Dict[str, Any]:
257
+ """
258
+ Get a specific scenario.
259
+
260
+ Args:
261
+ uuid: Scenario UUID
262
+ version: Specific version (defaults to latest)
263
+
264
+ Returns:
265
+ Scenario data
266
+ """
267
+ if version is not None:
268
+ endpoint = f"/scenarios/{uuid}/{version}"
269
+ else:
270
+ endpoint = f"/scenarios/{uuid}"
271
+
272
+ response = self.get(endpoint)
273
+ return response.json()
@@ -0,0 +1,86 @@
1
+ from pathlib import Path
2
+ import yaml
3
+ import os
4
+
5
+ class HumalabConfig:
6
+ def __init__(self):
7
+ self._config = {
8
+ "workspace_path": "",
9
+ "entity": "",
10
+ "base_url": "",
11
+ "api_key": "",
12
+ "timeout": 30.0,
13
+ }
14
+ self._workspace_path = ""
15
+ self._entity = ""
16
+ self._base_url = ""
17
+ self._api_key = ""
18
+ self._timeout = 30.0
19
+ self._load_config()
20
+
21
+ def _load_config(self):
22
+ home_path = Path.home()
23
+ config_path = home_path / ".humalab" / "config.yaml"
24
+ if not config_path.exists():
25
+ config_path.parent.mkdir(parents=True, exist_ok=True)
26
+ config_path.touch()
27
+ with open(config_path, "r") as f:
28
+ self._config = yaml.safe_load(f)
29
+ self._workspace_path = os.path.expanduser(self._config["workspace_path"]) if self._config and "workspace_path" in self._config else home_path
30
+ self._entity = self._config["entity"] if self._config and "entity" in self._config else ""
31
+ self._base_url = self._config["base_url"] if self._config and "base_url" in self._config else ""
32
+ self._api_key = self._config["api_key"] if self._config and "api_key" in self._config else ""
33
+ self._timeout = self._config["timeout"] if self._config and "timeout" in self._config else 30.0
34
+
35
+ def _save(self) -> None:
36
+ yaml.dump(self._config, open(Path.home() / ".humalab" / "config.yaml", "w"))
37
+
38
+ @property
39
+ def workspace_path(self) -> str:
40
+ return str(self._workspace_path)
41
+
42
+ @workspace_path.setter
43
+ def workspace_path(self, path: str) -> None:
44
+ self._workspace_path = path
45
+ self._config["workspace_path"] = path
46
+ self._save()
47
+
48
+ @property
49
+ def entity(self) -> str:
50
+ return str(self._entity)
51
+
52
+ @entity.setter
53
+ def entity(self, entity: str) -> None:
54
+ self._entity = entity
55
+ self._config["entity"] = entity
56
+ self._save()
57
+
58
+ @property
59
+ def base_url(self) -> str:
60
+ return str(self._base_url)
61
+
62
+ @base_url.setter
63
+ def base_url(self, base_url: str) -> None:
64
+ self._base_url = base_url
65
+ self._config["base_url"] = base_url
66
+ self._save()
67
+
68
+ @property
69
+ def api_key(self) -> str:
70
+ return str(self._api_key)
71
+
72
+ @api_key.setter
73
+ def api_key(self, api_key: str) -> None:
74
+ self._api_key = api_key
75
+ self._config["api_key"] = api_key
76
+ self._save()
77
+
78
+ @property
79
+ def timeout(self) -> float:
80
+ return self._timeout
81
+
82
+ @timeout.setter
83
+ def timeout(self, timeout: float) -> None:
84
+ self._timeout = timeout
85
+ self._config["timeout"] = timeout
86
+ self._save()