futurehouse-client 0.3.20.dev266__py3-none-any.whl → 0.3.20.dev411__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.
- futurehouse_client/__init__.py +8 -0
- futurehouse_client/clients/data_storage_methods.py +1867 -0
- futurehouse_client/clients/rest_client.py +247 -33
- futurehouse_client/models/data_storage_methods.py +333 -0
- futurehouse_client/models/rest.py +15 -0
- futurehouse_client/utils/general.py +34 -0
- futurehouse_client/utils/world_model_tools.py +69 -0
- futurehouse_client/version.py +16 -3
- {futurehouse_client-0.3.20.dev266.dist-info → futurehouse_client-0.3.20.dev411.dist-info}/METADATA +6 -1
- futurehouse_client-0.3.20.dev411.dist-info/RECORD +23 -0
- futurehouse_client-0.3.20.dev266.dist-info/RECORD +0 -20
- {futurehouse_client-0.3.20.dev266.dist-info → futurehouse_client-0.3.20.dev411.dist-info}/WHEEL +0 -0
- {futurehouse_client-0.3.20.dev266.dist-info → futurehouse_client-0.3.20.dev411.dist-info}/licenses/LICENSE +0 -0
- {futurehouse_client-0.3.20.dev266.dist-info → futurehouse_client-0.3.20.dev411.dist-info}/top_level.txt +0 -0
@@ -26,21 +26,14 @@ from httpx import (
|
|
26
26
|
AsyncClient,
|
27
27
|
Client,
|
28
28
|
CloseError,
|
29
|
-
ConnectError,
|
30
|
-
ConnectTimeout,
|
31
29
|
HTTPStatusError,
|
32
|
-
NetworkError,
|
33
|
-
ReadError,
|
34
|
-
ReadTimeout,
|
35
30
|
RemoteProtocolError,
|
36
31
|
codes,
|
37
32
|
)
|
38
33
|
from ldp.agent import AgentConfig
|
39
|
-
from requests.exceptions import RequestException, Timeout
|
40
34
|
from tenacity import (
|
41
35
|
before_sleep_log,
|
42
36
|
retry,
|
43
|
-
retry_if_exception_type,
|
44
37
|
stop_after_attempt,
|
45
38
|
wait_exponential,
|
46
39
|
)
|
@@ -48,6 +41,7 @@ from tqdm import tqdm as sync_tqdm
|
|
48
41
|
from tqdm.asyncio import tqdm
|
49
42
|
|
50
43
|
from futurehouse_client.clients import JobNames
|
44
|
+
from futurehouse_client.clients.data_storage_methods import DataStorageMethods
|
51
45
|
from futurehouse_client.models.app import (
|
52
46
|
AuthType,
|
53
47
|
JobDeploymentConfig,
|
@@ -58,6 +52,7 @@ from futurehouse_client.models.app import (
|
|
58
52
|
TrajectoryQueryParams,
|
59
53
|
)
|
60
54
|
from futurehouse_client.models.rest import (
|
55
|
+
DiscoveryResponse,
|
61
56
|
ExecutionStatus,
|
62
57
|
UserAgentRequest,
|
63
58
|
UserAgentRequestPostPayload,
|
@@ -67,7 +62,10 @@ from futurehouse_client.models.rest import (
|
|
67
62
|
WorldModelResponse,
|
68
63
|
)
|
69
64
|
from futurehouse_client.utils.auth import RefreshingJWT
|
70
|
-
from futurehouse_client.utils.general import
|
65
|
+
from futurehouse_client.utils.general import (
|
66
|
+
create_retry_if_connection_error,
|
67
|
+
gather_with_concurrency,
|
68
|
+
)
|
71
69
|
from futurehouse_client.utils.module_utils import (
|
72
70
|
OrganizationSelector,
|
73
71
|
fetch_environment_function_docstring,
|
@@ -135,10 +133,22 @@ class WorldModelCreationError(RestClientError):
|
|
135
133
|
"""Raised when there's an error creating a world model."""
|
136
134
|
|
137
135
|
|
136
|
+
class WorldModelDeletionError(RestClientError):
|
137
|
+
"""Raised when there's an error deleting a world model."""
|
138
|
+
|
139
|
+
|
138
140
|
class ProjectError(RestClientError):
|
139
141
|
"""Raised when there's an error with trajectory group operations."""
|
140
142
|
|
141
143
|
|
144
|
+
class DiscoveryCreationError(RestClientError):
|
145
|
+
"""Raised when there's an error creating a discovery."""
|
146
|
+
|
147
|
+
|
148
|
+
class DiscoveryFetchError(RestClientError):
|
149
|
+
"""Raised when there's an error fetching a discovery."""
|
150
|
+
|
151
|
+
|
142
152
|
class InvalidTaskDescriptionError(Exception):
|
143
153
|
"""Raised when the task description is invalid or empty."""
|
144
154
|
|
@@ -147,28 +157,15 @@ class FileUploadError(RestClientError):
|
|
147
157
|
"""Raised when there's an error uploading a file."""
|
148
158
|
|
149
159
|
|
150
|
-
retry_if_connection_error =
|
151
|
-
# From requests
|
152
|
-
Timeout,
|
153
|
-
ConnectionError,
|
154
|
-
RequestException,
|
155
|
-
# From httpx
|
156
|
-
ConnectError,
|
157
|
-
ConnectTimeout,
|
158
|
-
ReadTimeout,
|
159
|
-
ReadError,
|
160
|
-
NetworkError,
|
161
|
-
RemoteProtocolError,
|
162
|
-
CloseError,
|
163
|
-
FileUploadError,
|
164
|
-
))
|
160
|
+
retry_if_connection_error = create_retry_if_connection_error(FileUploadError)
|
165
161
|
|
166
162
|
DEFAULT_AGENT_TIMEOUT: int = 2400 # seconds
|
167
163
|
|
168
164
|
|
169
165
|
# pylint: disable=too-many-public-methods
|
170
|
-
class RestClient:
|
171
|
-
REQUEST_TIMEOUT: ClassVar[float] = 30.0 # sec
|
166
|
+
class RestClient(DataStorageMethods):
|
167
|
+
REQUEST_TIMEOUT: ClassVar[float] = 30.0 # sec - for general API calls
|
168
|
+
FILE_UPLOAD_TIMEOUT: ClassVar[float] = 600.0 # 10 minutes - for file uploads
|
172
169
|
MAX_RETRY_ATTEMPTS: ClassVar[int] = 3
|
173
170
|
RETRY_MULTIPLIER: ClassVar[int] = 1
|
174
171
|
MAX_RETRY_WAIT: ClassVar[int] = 10
|
@@ -226,11 +223,35 @@ class RestClient:
|
|
226
223
|
"""Authenticated HTTP client for multipart uploads."""
|
227
224
|
return cast(Client, self.get_client(None, authenticated=True))
|
228
225
|
|
226
|
+
@property
|
227
|
+
def file_upload_client(self) -> Client:
|
228
|
+
"""Authenticated HTTP client with extended timeout for file uploads."""
|
229
|
+
return cast(
|
230
|
+
Client,
|
231
|
+
self.get_client(
|
232
|
+
"application/json", authenticated=True, timeout=self.FILE_UPLOAD_TIMEOUT
|
233
|
+
),
|
234
|
+
)
|
235
|
+
|
236
|
+
@property
|
237
|
+
def async_file_upload_client(self) -> AsyncClient:
|
238
|
+
"""Authenticated async HTTP client with extended timeout for file uploads."""
|
239
|
+
return cast(
|
240
|
+
AsyncClient,
|
241
|
+
self.get_client(
|
242
|
+
"application/json",
|
243
|
+
authenticated=True,
|
244
|
+
async_client=True,
|
245
|
+
timeout=self.FILE_UPLOAD_TIMEOUT,
|
246
|
+
),
|
247
|
+
)
|
248
|
+
|
229
249
|
def get_client(
|
230
250
|
self,
|
231
251
|
content_type: str | None = "application/json",
|
232
252
|
authenticated: bool = True,
|
233
253
|
async_client: bool = False,
|
254
|
+
timeout: float | None = None,
|
234
255
|
) -> Client | AsyncClient:
|
235
256
|
"""Return a cached HTTP client or create one if needed.
|
236
257
|
|
@@ -238,12 +259,13 @@ class RestClient:
|
|
238
259
|
content_type: The desired content type header. Use None for multipart uploads.
|
239
260
|
authenticated: Whether the client should include authentication.
|
240
261
|
async_client: Whether to use an async client.
|
262
|
+
timeout: Custom timeout in seconds. Uses REQUEST_TIMEOUT if not provided.
|
241
263
|
|
242
264
|
Returns:
|
243
265
|
An HTTP client configured with the appropriate headers.
|
244
266
|
"""
|
245
|
-
|
246
|
-
key = f"{content_type or 'multipart'}_{authenticated}_{async_client}"
|
267
|
+
client_timeout = timeout or self.REQUEST_TIMEOUT
|
268
|
+
key = f"{content_type or 'multipart'}_{authenticated}_{async_client}_{client_timeout}"
|
247
269
|
|
248
270
|
if key not in self._clients:
|
249
271
|
headers = copy.deepcopy(self.headers)
|
@@ -269,14 +291,14 @@ class RestClient:
|
|
269
291
|
AsyncClient(
|
270
292
|
base_url=self.base_url,
|
271
293
|
headers=headers,
|
272
|
-
timeout=
|
294
|
+
timeout=client_timeout,
|
273
295
|
auth=auth,
|
274
296
|
)
|
275
297
|
if async_client
|
276
298
|
else Client(
|
277
299
|
base_url=self.base_url,
|
278
300
|
headers=headers,
|
279
|
-
timeout=
|
301
|
+
timeout=client_timeout,
|
280
302
|
auth=auth,
|
281
303
|
)
|
282
304
|
)
|
@@ -1584,6 +1606,48 @@ class RestClient:
|
|
1584
1606
|
except Exception as e:
|
1585
1607
|
raise WorldModelFetchError(f"An unexpected error occurred: {e!r}.") from e
|
1586
1608
|
|
1609
|
+
@retry(
|
1610
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1611
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1612
|
+
retry=retry_if_connection_error,
|
1613
|
+
)
|
1614
|
+
def search_world_models(
|
1615
|
+
self,
|
1616
|
+
query: str,
|
1617
|
+
size: int = 10,
|
1618
|
+
total_search_size: int = 50,
|
1619
|
+
search_all_versions: bool = False,
|
1620
|
+
) -> list[str]:
|
1621
|
+
"""Search for world models.
|
1622
|
+
|
1623
|
+
Args:
|
1624
|
+
query: The search query.
|
1625
|
+
size: The number of results to return.
|
1626
|
+
total_search_size: The number of results to search for.
|
1627
|
+
search_all_versions: Whether to search all versions of the world model or just the latest one.
|
1628
|
+
|
1629
|
+
Returns:
|
1630
|
+
A list of world model names.
|
1631
|
+
"""
|
1632
|
+
try:
|
1633
|
+
response = self.client.get(
|
1634
|
+
"/v0.1/world-models/search/",
|
1635
|
+
params={
|
1636
|
+
"query": query,
|
1637
|
+
"size": size,
|
1638
|
+
"total_search_size": total_search_size,
|
1639
|
+
"search_all_versions": search_all_versions,
|
1640
|
+
},
|
1641
|
+
)
|
1642
|
+
response.raise_for_status()
|
1643
|
+
return response.json()
|
1644
|
+
except HTTPStatusError as e:
|
1645
|
+
raise WorldModelFetchError(
|
1646
|
+
f"Error searching world models: {e.response.status_code} - {e.response.text}"
|
1647
|
+
) from e
|
1648
|
+
except Exception as e:
|
1649
|
+
raise WorldModelFetchError(f"An unexpected error occurred: {e!r}.") from e
|
1650
|
+
|
1587
1651
|
@retry(
|
1588
1652
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1589
1653
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
@@ -1659,6 +1723,32 @@ class RestClient:
|
|
1659
1723
|
f"An unexpected error occurred during world model creation: {e!r}."
|
1660
1724
|
) from e
|
1661
1725
|
|
1726
|
+
@retry(
|
1727
|
+
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1728
|
+
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1729
|
+
retry=retry_if_connection_error,
|
1730
|
+
)
|
1731
|
+
async def delete_world_model(self, world_model_id: UUID) -> None:
|
1732
|
+
"""Delete a world model snapshot by its ID.
|
1733
|
+
|
1734
|
+
Args:
|
1735
|
+
world_model_id: The unique ID of the world model snapshot to delete.
|
1736
|
+
|
1737
|
+
Raises:
|
1738
|
+
WorldModelDeletionError: If the API call fails.
|
1739
|
+
"""
|
1740
|
+
try:
|
1741
|
+
response = await self.async_client.delete(
|
1742
|
+
f"/v0.1/world-models/{world_model_id}"
|
1743
|
+
)
|
1744
|
+
response.raise_for_status()
|
1745
|
+
except HTTPStatusError as e:
|
1746
|
+
raise WorldModelDeletionError(
|
1747
|
+
f"Error deleting world model: {e.response.status_code} - {e.response.text}"
|
1748
|
+
) from e
|
1749
|
+
except Exception as e:
|
1750
|
+
raise WorldModelDeletionError(f"An unexpected error occurred: {e}") from e
|
1751
|
+
|
1662
1752
|
@retry(
|
1663
1753
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
1664
1754
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
@@ -1737,11 +1827,18 @@ class RestClient:
|
|
1737
1827
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1738
1828
|
retry=retry_if_connection_error,
|
1739
1829
|
)
|
1740
|
-
def create_project(
|
1830
|
+
def create_project(
|
1831
|
+
self,
|
1832
|
+
name: str,
|
1833
|
+
shared_with_organizations: list[int] | None = None,
|
1834
|
+
shared_with_users: list[str] | None = None,
|
1835
|
+
) -> str:
|
1741
1836
|
"""Create a new project.
|
1742
1837
|
|
1743
1838
|
Args:
|
1744
1839
|
name: The name for the project
|
1840
|
+
shared_with_organizations: List of organization IDs to share the project with
|
1841
|
+
shared_with_users: List of user emails to share the project with
|
1745
1842
|
|
1746
1843
|
Returns:
|
1747
1844
|
UUID of the created project
|
@@ -1750,7 +1847,11 @@ class RestClient:
|
|
1750
1847
|
ProjectError: If there's an error creating the project
|
1751
1848
|
"""
|
1752
1849
|
try:
|
1753
|
-
data = {
|
1850
|
+
data = {
|
1851
|
+
"name": name,
|
1852
|
+
"shared_with_organizations": shared_with_organizations,
|
1853
|
+
"shared_with_users": shared_with_users,
|
1854
|
+
}
|
1754
1855
|
response = self.client.post("/v0.1/projects", json=data)
|
1755
1856
|
response.raise_for_status()
|
1756
1857
|
return response.json()
|
@@ -1802,11 +1903,18 @@ class RestClient:
|
|
1802
1903
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
1803
1904
|
retry=retry_if_connection_error,
|
1804
1905
|
)
|
1805
|
-
async def acreate_project(
|
1906
|
+
async def acreate_project(
|
1907
|
+
self,
|
1908
|
+
name: str,
|
1909
|
+
shared_with_organizations: list[int] | None = None,
|
1910
|
+
shared_with_users: list[str] | None = None,
|
1911
|
+
) -> str:
|
1806
1912
|
"""Asynchronously create a new project.
|
1807
1913
|
|
1808
1914
|
Args:
|
1809
1915
|
name: The name for the project
|
1916
|
+
shared_with_organizations: List of organization IDs to share the project with
|
1917
|
+
shared_with_users: List of user emails to share the project with
|
1810
1918
|
|
1811
1919
|
Returns:
|
1812
1920
|
UUID of the created project
|
@@ -1815,7 +1923,11 @@ class RestClient:
|
|
1815
1923
|
ProjectError: If there's an error creating the project
|
1816
1924
|
"""
|
1817
1925
|
try:
|
1818
|
-
data = {
|
1926
|
+
data = {
|
1927
|
+
"name": name,
|
1928
|
+
"shared_with_organizations": shared_with_organizations,
|
1929
|
+
"shared_with_users": shared_with_users,
|
1930
|
+
}
|
1819
1931
|
response = await self.async_client.post("/v0.1/projects", json=data)
|
1820
1932
|
response.raise_for_status()
|
1821
1933
|
return response.json()
|
@@ -2326,6 +2438,108 @@ class RestClient:
|
|
2326
2438
|
f"An unexpected error occurred while responding to the request: {e!r}."
|
2327
2439
|
) from e
|
2328
2440
|
|
2441
|
+
def create_discovery(
|
2442
|
+
self,
|
2443
|
+
project_id: UUID,
|
2444
|
+
world_model_id: UUID,
|
2445
|
+
dataset_id: UUID,
|
2446
|
+
description: str,
|
2447
|
+
associated_trajectories: list[UUID],
|
2448
|
+
validation_level: int,
|
2449
|
+
) -> UUID:
|
2450
|
+
"""Create a new discovery.
|
2451
|
+
|
2452
|
+
Args:
|
2453
|
+
project_id: The ID of the project the discovery is associated with.
|
2454
|
+
world_model_id: The ID of the world model the discovery is associated with.
|
2455
|
+
dataset_id: The ID of the dataset the discovery is associated with.
|
2456
|
+
description: The description of the discovery.
|
2457
|
+
associated_trajectories: The IDs of the trajectories to associate with the discovery.
|
2458
|
+
validation_level: The validation level of the discovery.
|
2459
|
+
|
2460
|
+
Returns:
|
2461
|
+
The ID of the created discovery.
|
2462
|
+
|
2463
|
+
Raises:
|
2464
|
+
DiscoveryCreationError: If there's an error creating the discovery.
|
2465
|
+
"""
|
2466
|
+
try:
|
2467
|
+
data = {
|
2468
|
+
"project_id": str(project_id),
|
2469
|
+
"world_model_id": str(world_model_id),
|
2470
|
+
"dataset_id": str(dataset_id),
|
2471
|
+
"description": description,
|
2472
|
+
"associated_trajectories": [
|
2473
|
+
str(trajectory) for trajectory in associated_trajectories
|
2474
|
+
],
|
2475
|
+
"validation_level": validation_level,
|
2476
|
+
}
|
2477
|
+
response = self.client.post("/v0.1/discoveries", json=data)
|
2478
|
+
response.raise_for_status()
|
2479
|
+
return UUID(response.json())
|
2480
|
+
except HTTPStatusError as e:
|
2481
|
+
raise DiscoveryCreationError(
|
2482
|
+
f"Error creating discovery: {e.response.status_code} - {e.response.text}"
|
2483
|
+
) from e
|
2484
|
+
except Exception as e:
|
2485
|
+
raise DiscoveryCreationError(f"Error creating discovery: {e!r}") from e
|
2486
|
+
|
2487
|
+
def get_discovery(self, discovery_id: UUID) -> DiscoveryResponse:
|
2488
|
+
"""Get a discovery by its ID.
|
2489
|
+
|
2490
|
+
Args:
|
2491
|
+
discovery_id: The ID of the discovery to get.
|
2492
|
+
|
2493
|
+
Returns:
|
2494
|
+
The discovery.
|
2495
|
+
|
2496
|
+
Raises:
|
2497
|
+
DiscoveryFetchError: If there's an error fetching the discovery.
|
2498
|
+
"""
|
2499
|
+
try:
|
2500
|
+
response = self.client.get(f"/v0.1/discoveries/{discovery_id}")
|
2501
|
+
response.raise_for_status()
|
2502
|
+
return DiscoveryResponse.model_validate(response.json())
|
2503
|
+
except HTTPStatusError as e:
|
2504
|
+
raise DiscoveryFetchError(
|
2505
|
+
f"Error fetching discovery: {e.response.status_code} - {e.response.text}"
|
2506
|
+
) from e
|
2507
|
+
except Exception as e:
|
2508
|
+
raise DiscoveryFetchError(f"Error fetching discovery: {e!r}") from e
|
2509
|
+
|
2510
|
+
def list_discoveries_for_project(
|
2511
|
+
self, project_id: UUID, limit: int = 50, offset: int = 0
|
2512
|
+
) -> list[DiscoveryResponse]:
|
2513
|
+
"""List discoveries for a specific project.
|
2514
|
+
|
2515
|
+
Args:
|
2516
|
+
project_id: The ID of the project to get discoveries for.
|
2517
|
+
limit: The maximum number of discoveries to return.
|
2518
|
+
offset: The number of discoveries to skip.
|
2519
|
+
|
2520
|
+
Returns:
|
2521
|
+
A list of discoveries for the specified project.
|
2522
|
+
|
2523
|
+
Raises:
|
2524
|
+
DiscoveryFetchError: If there's an error fetching the discoveries.
|
2525
|
+
"""
|
2526
|
+
try:
|
2527
|
+
response = self.client.get(
|
2528
|
+
f"/v0.1/projects/{project_id}/discoveries",
|
2529
|
+
params={"limit": limit, "offset": offset},
|
2530
|
+
)
|
2531
|
+
response.raise_for_status()
|
2532
|
+
data = response.json()
|
2533
|
+
return [DiscoveryResponse.model_validate(d) for d in data]
|
2534
|
+
except HTTPStatusError as e:
|
2535
|
+
raise DiscoveryFetchError(
|
2536
|
+
f"Error fetching discoveries for project: {e.response.status_code} - {e.response.text}"
|
2537
|
+
) from e
|
2538
|
+
except Exception as e:
|
2539
|
+
raise DiscoveryFetchError(
|
2540
|
+
f"Error fetching discoveries for project: {e!r}"
|
2541
|
+
) from e
|
2542
|
+
|
2329
2543
|
|
2330
2544
|
def get_installed_packages() -> dict[str, str]:
|
2331
2545
|
"""Returns a dictionary of installed packages and their versions."""
|