futurehouse-client 0.3.19.dev111__tar.gz → 0.3.19.dev129__tar.gz
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-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/PKG-INFO +1 -1
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/clients/rest_client.py +76 -79
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client.egg-info/PKG-INFO +1 -1
- futurehouse_client-0.3.19.dev129/tests/test_rest.py +680 -0
- futurehouse_client-0.3.19.dev111/tests/test_rest.py +0 -235
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/LICENSE +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/README.md +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/docs/__init__.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/docs/client_notebook.ipynb +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/__init__.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/clients/__init__.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/clients/job_client.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/models/__init__.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/models/app.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/models/client.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/models/rest.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/utils/__init__.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/utils/auth.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/utils/general.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/utils/module_utils.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/utils/monitoring.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client.egg-info/SOURCES.txt +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client.egg-info/dependency_links.txt +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client.egg-info/requires.txt +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client.egg-info/top_level.txt +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/pyproject.toml +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/setup.cfg +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/tests/test_client.py +0 -0
- {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/uv.lock +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: futurehouse-client
|
3
|
-
Version: 0.3.19.
|
3
|
+
Version: 0.3.19.dev129
|
4
4
|
Summary: A client for interacting with endpoints of the FutureHouse service.
|
5
5
|
Author-email: FutureHouse technical staff <hello@futurehouse.org>
|
6
6
|
Classifier: Operating System :: OS Independent
|
@@ -444,37 +444,36 @@ class RestClient:
|
|
444
444
|
self, task_id: str | None = None, history: bool = False, verbose: bool = False
|
445
445
|
) -> "TaskResponse":
|
446
446
|
"""Get details for a specific task."""
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
full_url = f"{self.base_url}{url}"
|
451
|
-
|
452
|
-
with (
|
453
|
-
external_trace(
|
454
|
-
url=full_url,
|
455
|
-
method="GET",
|
456
|
-
library="httpx",
|
457
|
-
custom_params={
|
458
|
-
"operation": "get_job",
|
459
|
-
"job_id": task_id,
|
460
|
-
},
|
461
|
-
),
|
462
|
-
self.client.stream("GET", url, params={"history": history}) as response,
|
463
|
-
):
|
464
|
-
response.raise_for_status()
|
465
|
-
json_data = "".join(response.iter_text(chunk_size=1024))
|
466
|
-
data = json.loads(json_data)
|
467
|
-
if "id" not in data:
|
468
|
-
data["id"] = task_id
|
469
|
-
verbose_response = TaskResponseVerbose(**data)
|
447
|
+
task_id = task_id or self.trajectory_id
|
448
|
+
url = f"/v0.1/trajectories/{task_id}"
|
449
|
+
full_url = f"{self.base_url}{url}"
|
470
450
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
451
|
+
with (
|
452
|
+
external_trace(
|
453
|
+
url=full_url,
|
454
|
+
method="GET",
|
455
|
+
library="httpx",
|
456
|
+
custom_params={
|
457
|
+
"operation": "get_job",
|
458
|
+
"job_id": task_id,
|
459
|
+
},
|
460
|
+
),
|
461
|
+
self.client.stream("GET", url, params={"history": history}) as response,
|
462
|
+
):
|
463
|
+
if response.status_code in {401, 403}:
|
464
|
+
raise PermissionError(
|
465
|
+
f"Error getting task: Permission denied for task {task_id}"
|
466
|
+
)
|
467
|
+
response.raise_for_status()
|
468
|
+
json_data = "".join(response.iter_text(chunk_size=1024))
|
469
|
+
data = json.loads(json_data)
|
470
|
+
if "id" not in data:
|
471
|
+
data["id"] = task_id
|
472
|
+
verbose_response = TaskResponseVerbose(**data)
|
473
|
+
|
474
|
+
if verbose:
|
475
|
+
return verbose_response
|
476
|
+
return JobNames.get_response_object_from_job(verbose_response.job_name)(**data)
|
478
477
|
|
479
478
|
@retry(
|
480
479
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
@@ -485,39 +484,36 @@ class RestClient:
|
|
485
484
|
self, task_id: str | None = None, history: bool = False, verbose: bool = False
|
486
485
|
) -> "TaskResponse":
|
487
486
|
"""Get details for a specific task asynchronously."""
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
487
|
+
task_id = task_id or self.trajectory_id
|
488
|
+
url = f"/v0.1/trajectories/{task_id}"
|
489
|
+
full_url = f"{self.base_url}{url}"
|
490
|
+
|
491
|
+
with external_trace(
|
492
|
+
url=full_url,
|
493
|
+
method="GET",
|
494
|
+
library="httpx",
|
495
|
+
custom_params={
|
496
|
+
"operation": "get_job",
|
497
|
+
"job_id": task_id,
|
498
|
+
},
|
499
|
+
):
|
500
|
+
async with self.async_client.stream(
|
501
|
+
"GET", url, params={"history": history}
|
502
|
+
) as response:
|
503
|
+
if response.status_code in {401, 403}:
|
504
|
+
raise PermissionError(
|
505
|
+
f"Error getting task: Permission denied for task {task_id}"
|
506
|
+
)
|
507
|
+
response.raise_for_status()
|
508
|
+
json_data = "".join([chunk async for chunk in response.aiter_text()])
|
509
|
+
data = json.loads(json_data)
|
510
|
+
if "id" not in data:
|
511
|
+
data["id"] = task_id
|
512
|
+
verbose_response = TaskResponseVerbose(**data)
|
492
513
|
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
library="httpx",
|
497
|
-
custom_params={
|
498
|
-
"operation": "get_job",
|
499
|
-
"job_id": task_id,
|
500
|
-
},
|
501
|
-
):
|
502
|
-
async with self.async_client.stream(
|
503
|
-
"GET", url, params={"history": history}
|
504
|
-
) as response:
|
505
|
-
response.raise_for_status()
|
506
|
-
json_data = "".join([
|
507
|
-
chunk async for chunk in response.aiter_text()
|
508
|
-
])
|
509
|
-
data = json.loads(json_data)
|
510
|
-
if "id" not in data:
|
511
|
-
data["id"] = task_id
|
512
|
-
verbose_response = TaskResponseVerbose(**data)
|
513
|
-
|
514
|
-
if verbose:
|
515
|
-
return verbose_response
|
516
|
-
return JobNames.get_response_object_from_job(verbose_response.job_name)(
|
517
|
-
**data
|
518
|
-
)
|
519
|
-
except Exception as e:
|
520
|
-
raise TaskFetchError(f"Error getting task: {e!s}") from e
|
514
|
+
if verbose:
|
515
|
+
return verbose_response
|
516
|
+
return JobNames.get_response_object_from_job(verbose_response.job_name)(**data)
|
521
517
|
|
522
518
|
@retry(
|
523
519
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
@@ -535,15 +531,16 @@ class RestClient:
|
|
535
531
|
self.stage,
|
536
532
|
)
|
537
533
|
|
538
|
-
|
539
|
-
|
540
|
-
|
534
|
+
response = self.client.post(
|
535
|
+
"/v0.1/crows", json=task_data.model_dump(mode="json")
|
536
|
+
)
|
537
|
+
if response.status_code in {401, 403}:
|
538
|
+
raise PermissionError(
|
539
|
+
f"Error creating task: Permission denied for task {task_data.name}"
|
541
540
|
)
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
except Exception as e:
|
546
|
-
raise TaskFetchError(f"Error creating task: {e!s}") from e
|
541
|
+
response.raise_for_status()
|
542
|
+
trajectory_id = response.json()["trajectory_id"]
|
543
|
+
self.trajectory_id = trajectory_id
|
547
544
|
return trajectory_id
|
548
545
|
|
549
546
|
@retry(
|
@@ -561,16 +558,16 @@ class RestClient:
|
|
561
558
|
task_data.name.name,
|
562
559
|
self.stage,
|
563
560
|
)
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
561
|
+
response = await self.async_client.post(
|
562
|
+
"/v0.1/crows", json=task_data.model_dump(mode="json")
|
563
|
+
)
|
564
|
+
if response.status_code in {401, 403}:
|
565
|
+
raise PermissionError(
|
566
|
+
f"Error creating task: Permission denied for task {task_data.name}"
|
568
567
|
)
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
except Exception as e:
|
573
|
-
raise TaskFetchError(f"Error creating task: {e!s}") from e
|
568
|
+
response.raise_for_status()
|
569
|
+
trajectory_id = response.json()["trajectory_id"]
|
570
|
+
self.trajectory_id = trajectory_id
|
574
571
|
return trajectory_id
|
575
572
|
|
576
573
|
async def arun_tasks_until_done(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: futurehouse-client
|
3
|
-
Version: 0.3.19.
|
3
|
+
Version: 0.3.19.dev129
|
4
4
|
Summary: A client for interacting with endpoints of the FutureHouse service.
|
5
5
|
Author-email: FutureHouse technical staff <hello@futurehouse.org>
|
6
6
|
Classifier: Operating System :: OS Independent
|
@@ -0,0 +1,680 @@
|
|
1
|
+
# ruff: noqa: ARG001
|
2
|
+
# ruff: noqa: SIM117
|
3
|
+
# pylint: disable=too-many-lines,import-error,too-many-public-methods
|
4
|
+
import asyncio
|
5
|
+
import os
|
6
|
+
import tempfile
|
7
|
+
import time
|
8
|
+
import types
|
9
|
+
from pathlib import Path
|
10
|
+
from unittest.mock import MagicMock, mock_open, patch
|
11
|
+
|
12
|
+
import pytest
|
13
|
+
from futurehouse_client.clients import (
|
14
|
+
JobNames,
|
15
|
+
)
|
16
|
+
from futurehouse_client.clients.rest_client import (
|
17
|
+
FileUploadError,
|
18
|
+
RestClient,
|
19
|
+
RestClientError,
|
20
|
+
)
|
21
|
+
from futurehouse_client.models.app import (
|
22
|
+
PhoenixTaskResponse,
|
23
|
+
PQATaskResponse,
|
24
|
+
Stage,
|
25
|
+
TaskRequest,
|
26
|
+
TaskResponseVerbose,
|
27
|
+
)
|
28
|
+
from futurehouse_client.models.rest import ExecutionStatus
|
29
|
+
from pytest_subtests import SubTests
|
30
|
+
|
31
|
+
ADMIN_API_KEY = os.environ["PLAYWRIGHT_ADMIN_API_KEY"]
|
32
|
+
PUBLIC_API_KEY = os.environ["PLAYWRIGHT_PUBLIC_API_KEY"]
|
33
|
+
TEST_MAX_POLLS = 100
|
34
|
+
|
35
|
+
|
36
|
+
@pytest.fixture
|
37
|
+
def admin_client():
|
38
|
+
"""Create a RestClient for testing; using an admin key."""
|
39
|
+
return RestClient(
|
40
|
+
stage=Stage.DEV,
|
41
|
+
api_key=ADMIN_API_KEY,
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
@pytest.fixture
|
46
|
+
def pub_client():
|
47
|
+
"""Create a RestClient for testing; using a public user key with limited access."""
|
48
|
+
return RestClient(
|
49
|
+
stage=Stage.DEV,
|
50
|
+
api_key=PUBLIC_API_KEY,
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
@pytest.fixture
|
55
|
+
def task_req():
|
56
|
+
"""Create a sample task request."""
|
57
|
+
return TaskRequest(
|
58
|
+
name=JobNames.from_string("dummy"),
|
59
|
+
query="How many moons does earth have?",
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
@pytest.fixture
|
64
|
+
def pqa_task_req():
|
65
|
+
return TaskRequest(
|
66
|
+
name=JobNames.from_string("crow"),
|
67
|
+
query="How many moons does earth have?",
|
68
|
+
)
|
69
|
+
|
70
|
+
|
71
|
+
@pytest.fixture
|
72
|
+
def phoenix_task_req():
|
73
|
+
return TaskRequest(
|
74
|
+
name=JobNames.from_string("phoenix"),
|
75
|
+
query="What is the molecular weight of ascorbic acids?",
|
76
|
+
)
|
77
|
+
|
78
|
+
|
79
|
+
@pytest.mark.timeout(300)
|
80
|
+
@pytest.mark.flaky(reruns=3)
|
81
|
+
def test_futurehouse_dummy_env_crow(admin_client: RestClient, task_req: TaskRequest):
|
82
|
+
admin_client.create_task(task_req)
|
83
|
+
while (task_status := admin_client.get_task().status) in {"queued", "in progress"}:
|
84
|
+
time.sleep(5)
|
85
|
+
assert task_status == "success"
|
86
|
+
|
87
|
+
|
88
|
+
def test_insufficient_permissions_request(
|
89
|
+
pub_client: RestClient, task_req: TaskRequest
|
90
|
+
):
|
91
|
+
# Create a new instance so that cached credentials aren't reused
|
92
|
+
with pytest.raises(PermissionError) as exc_info:
|
93
|
+
pub_client.create_task(task_req)
|
94
|
+
|
95
|
+
assert "Error creating task" in str(exc_info.value)
|
96
|
+
|
97
|
+
|
98
|
+
@pytest.mark.timeout(300)
|
99
|
+
@pytest.mark.asyncio
|
100
|
+
async def test_job_response( # noqa: PLR0915
|
101
|
+
subtests: SubTests,
|
102
|
+
admin_client: RestClient,
|
103
|
+
pqa_task_req: TaskRequest,
|
104
|
+
phoenix_task_req: TaskRequest,
|
105
|
+
):
|
106
|
+
task_id = admin_client.create_task(pqa_task_req)
|
107
|
+
atask_id = await admin_client.acreate_task(pqa_task_req)
|
108
|
+
phoenix_task_id = admin_client.create_task(phoenix_task_req)
|
109
|
+
aphoenix_task_id = await admin_client.acreate_task(phoenix_task_req)
|
110
|
+
|
111
|
+
with subtests.test("Test TaskResponse with queued task"):
|
112
|
+
task_response = admin_client.get_task(task_id)
|
113
|
+
assert task_response.status in {"queued", "in progress"}
|
114
|
+
assert task_response.job_name == pqa_task_req.name
|
115
|
+
assert task_response.query == pqa_task_req.query
|
116
|
+
task_response = await admin_client.aget_task(atask_id)
|
117
|
+
assert task_response.status in {"queued", "in progress"}
|
118
|
+
assert task_response.job_name == pqa_task_req.name
|
119
|
+
assert task_response.query == pqa_task_req.query
|
120
|
+
|
121
|
+
for _ in range(TEST_MAX_POLLS):
|
122
|
+
task_response = admin_client.get_task(task_id)
|
123
|
+
if task_response.status in ExecutionStatus.terminal_states():
|
124
|
+
break
|
125
|
+
await asyncio.sleep(5)
|
126
|
+
|
127
|
+
for _ in range(TEST_MAX_POLLS):
|
128
|
+
task_response = await admin_client.aget_task(atask_id)
|
129
|
+
if task_response.status in ExecutionStatus.terminal_states():
|
130
|
+
break
|
131
|
+
await asyncio.sleep(5)
|
132
|
+
|
133
|
+
with subtests.test("Test PQA job response"):
|
134
|
+
task_response = admin_client.get_task(task_id)
|
135
|
+
assert isinstance(task_response, PQATaskResponse)
|
136
|
+
# assert it has general fields
|
137
|
+
assert task_response.status == "success"
|
138
|
+
assert task_response.task_id is not None
|
139
|
+
assert pqa_task_req.name in task_response.job_name
|
140
|
+
assert pqa_task_req.query in task_response.query
|
141
|
+
# assert it has PQA specific fields
|
142
|
+
assert task_response.answer is not None
|
143
|
+
# assert it's not verbose
|
144
|
+
assert not hasattr(task_response, "environment_frame")
|
145
|
+
assert not hasattr(task_response, "agent_state")
|
146
|
+
|
147
|
+
with subtests.test("Test async PQA job response"):
|
148
|
+
task_response = await admin_client.aget_task(atask_id)
|
149
|
+
assert isinstance(task_response, PQATaskResponse)
|
150
|
+
# assert it has general fields
|
151
|
+
assert task_response.status == "success"
|
152
|
+
assert task_response.task_id is not None
|
153
|
+
assert pqa_task_req.name in task_response.job_name
|
154
|
+
assert pqa_task_req.query in task_response.query
|
155
|
+
# assert it has PQA specific fields
|
156
|
+
assert task_response.answer is not None
|
157
|
+
# assert it's not verbose
|
158
|
+
assert not hasattr(task_response, "environment_frame")
|
159
|
+
assert not hasattr(task_response, "agent_state")
|
160
|
+
|
161
|
+
with subtests.test("Test Phoenix job response"):
|
162
|
+
task_response = admin_client.get_task(phoenix_task_id)
|
163
|
+
assert isinstance(task_response, PhoenixTaskResponse)
|
164
|
+
assert task_response.status == "success"
|
165
|
+
assert task_response.task_id is not None
|
166
|
+
assert phoenix_task_req.name in task_response.job_name
|
167
|
+
assert phoenix_task_req.query in task_response.query
|
168
|
+
|
169
|
+
with subtests.test("Test async Phoenix job response"):
|
170
|
+
task_response = await admin_client.aget_task(aphoenix_task_id)
|
171
|
+
assert isinstance(task_response, PhoenixTaskResponse)
|
172
|
+
assert task_response.status == "success"
|
173
|
+
assert task_response.task_id is not None
|
174
|
+
assert phoenix_task_req.name in task_response.job_name
|
175
|
+
assert phoenix_task_req.query in task_response.query
|
176
|
+
|
177
|
+
with subtests.test("Test task response with verbose"):
|
178
|
+
task_response = admin_client.get_task(task_id, verbose=True)
|
179
|
+
assert isinstance(task_response, TaskResponseVerbose)
|
180
|
+
assert task_response.status == "success"
|
181
|
+
assert task_response.environment_frame is not None
|
182
|
+
assert task_response.agent_state is not None
|
183
|
+
|
184
|
+
with subtests.test("Test task async response with verbose"):
|
185
|
+
task_response = await admin_client.aget_task(atask_id, verbose=True)
|
186
|
+
assert isinstance(task_response, TaskResponseVerbose)
|
187
|
+
assert task_response.status == "success"
|
188
|
+
assert task_response.environment_frame is not None
|
189
|
+
assert task_response.agent_state is not None
|
190
|
+
|
191
|
+
|
192
|
+
@pytest.mark.timeout(300)
|
193
|
+
@pytest.mark.flaky(reruns=3)
|
194
|
+
def test_run_until_done_futurehouse_dummy_env_crow(
|
195
|
+
admin_client: RestClient, task_req: TaskRequest
|
196
|
+
):
|
197
|
+
tasks_to_do = [task_req, task_req]
|
198
|
+
|
199
|
+
results = admin_client.run_tasks_until_done(tasks_to_do)
|
200
|
+
|
201
|
+
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
202
|
+
assert all(task.status == "success" for task in results)
|
203
|
+
|
204
|
+
|
205
|
+
@pytest.mark.timeout(300)
|
206
|
+
@pytest.mark.flaky(reruns=3)
|
207
|
+
@pytest.mark.asyncio
|
208
|
+
async def test_arun_until_done_futurehouse_dummy_env_crow(
|
209
|
+
admin_client: RestClient, task_req: TaskRequest
|
210
|
+
):
|
211
|
+
tasks_to_do = [task_req, task_req]
|
212
|
+
|
213
|
+
results = await admin_client.arun_tasks_until_done(tasks_to_do)
|
214
|
+
|
215
|
+
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
216
|
+
assert all(task.status == "success" for task in results)
|
217
|
+
|
218
|
+
|
219
|
+
@pytest.mark.timeout(300)
|
220
|
+
@pytest.mark.flaky(reruns=3)
|
221
|
+
@pytest.mark.asyncio
|
222
|
+
async def test_timeout_run_until_done_futurehouse_dummy_env_crow(
|
223
|
+
admin_client: RestClient, task_req: TaskRequest
|
224
|
+
):
|
225
|
+
tasks_to_do = [task_req, task_req]
|
226
|
+
|
227
|
+
results = await admin_client.arun_tasks_until_done(
|
228
|
+
tasks_to_do, verbose=True, timeout=5, progress_bar=True
|
229
|
+
)
|
230
|
+
|
231
|
+
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
232
|
+
assert all(task.status != "success" for task in results), "Should not be success."
|
233
|
+
assert all(not isinstance(task, PQATaskResponse) for task in results), (
|
234
|
+
"Should be verbose."
|
235
|
+
)
|
236
|
+
|
237
|
+
results = admin_client.run_tasks_until_done(
|
238
|
+
tasks_to_do, verbose=True, timeout=5, progress_bar=True
|
239
|
+
)
|
240
|
+
|
241
|
+
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
242
|
+
assert all(task.status != "success" for task in results), "Should not be success."
|
243
|
+
assert all(not isinstance(task, PQATaskResponse) for task in results), (
|
244
|
+
"Should be verbose."
|
245
|
+
)
|
246
|
+
|
247
|
+
|
248
|
+
class TestParallelChunking:
|
249
|
+
"""Test suite for parallel chunk upload functionality."""
|
250
|
+
|
251
|
+
@pytest.fixture
|
252
|
+
def mock_client(self):
|
253
|
+
"""Create a mock RestClient for testing."""
|
254
|
+
# we don't need a real RestClient auth here
|
255
|
+
client = MagicMock(spec=RestClient)
|
256
|
+
client.CHUNK_SIZE = 16 * 1024 * 1024 # 16MB
|
257
|
+
client.MAX_CONCURRENT_CHUNKS = 12
|
258
|
+
client.multipart_client = MagicMock()
|
259
|
+
|
260
|
+
# Set up the real methods we want to test by properly binding them
|
261
|
+
client._upload_chunks_parallel = types.MethodType(
|
262
|
+
RestClient._upload_chunks_parallel, client
|
263
|
+
)
|
264
|
+
client._upload_single_chunk = types.MethodType(
|
265
|
+
RestClient._upload_single_chunk, client
|
266
|
+
)
|
267
|
+
client._upload_final_chunk = types.MethodType(
|
268
|
+
RestClient._upload_final_chunk, client
|
269
|
+
)
|
270
|
+
client._upload_single_file = types.MethodType(
|
271
|
+
RestClient._upload_single_file, client
|
272
|
+
)
|
273
|
+
client._upload_directory = types.MethodType(
|
274
|
+
RestClient._upload_directory, client
|
275
|
+
)
|
276
|
+
client.upload_file = types.MethodType(RestClient.upload_file, client)
|
277
|
+
client._wait_for_all_assemblies_completion = MagicMock(return_value=True)
|
278
|
+
|
279
|
+
return client
|
280
|
+
|
281
|
+
@pytest.fixture
|
282
|
+
def large_file_content(self):
|
283
|
+
"""Create content for a large file that will be chunked."""
|
284
|
+
# Create content larger than CHUNK_SIZE (16MB)
|
285
|
+
chunk_size = 16 * 1024 * 1024
|
286
|
+
return b"A" * (chunk_size * 2 + 1000) # ~32MB + 1000 bytes
|
287
|
+
|
288
|
+
@pytest.fixture
|
289
|
+
def small_file_content(self):
|
290
|
+
"""Create content for a small file that won't be chunked."""
|
291
|
+
return b"Small file content"
|
292
|
+
|
293
|
+
def test_upload_small_file_no_chunking(self, mock_client):
|
294
|
+
"""Test uploading a small file that doesn't require chunking."""
|
295
|
+
job_name = "test-job"
|
296
|
+
file_content = b"Small file content"
|
297
|
+
|
298
|
+
# Mock successful response
|
299
|
+
mock_response = MagicMock()
|
300
|
+
mock_response.raise_for_status.return_value = None
|
301
|
+
mock_response.json.return_value = {"status_url": "http://test.com/status"}
|
302
|
+
mock_client.multipart_client.post.return_value = mock_response
|
303
|
+
|
304
|
+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
305
|
+
temp_file.write(file_content)
|
306
|
+
temp_file.flush()
|
307
|
+
temp_path = Path(temp_file.name)
|
308
|
+
|
309
|
+
try:
|
310
|
+
# Mock assembly completion
|
311
|
+
with patch.object(
|
312
|
+
mock_client,
|
313
|
+
"_wait_for_all_assemblies_completion",
|
314
|
+
return_value=True,
|
315
|
+
):
|
316
|
+
upload_id = mock_client.upload_file(job_name, temp_path)
|
317
|
+
|
318
|
+
# Verify upload was called once (single chunk)
|
319
|
+
assert mock_client.multipart_client.post.call_count == 1
|
320
|
+
assert upload_id is not None
|
321
|
+
|
322
|
+
# Verify the post call was made with correct endpoint
|
323
|
+
call_args = mock_client.multipart_client.post.call_args
|
324
|
+
assert f"/v0.1/crows/{job_name}/upload-chunk" in call_args[0][0]
|
325
|
+
finally:
|
326
|
+
temp_path.unlink()
|
327
|
+
|
328
|
+
def test_upload_large_file_with_chunking(self, mock_client, large_file_content):
|
329
|
+
"""Test uploading a large file that requires chunking and parallel uploads."""
|
330
|
+
job_name = "test-job"
|
331
|
+
|
332
|
+
# Mock successful responses for all chunks
|
333
|
+
mock_response = MagicMock()
|
334
|
+
mock_response.raise_for_status.return_value = None
|
335
|
+
mock_response.json.return_value = {"status_url": "http://test.com/status"}
|
336
|
+
mock_response.status_code = 200
|
337
|
+
mock_client.multipart_client.post.return_value = mock_response
|
338
|
+
|
339
|
+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
340
|
+
temp_file.write(large_file_content)
|
341
|
+
temp_file.flush()
|
342
|
+
temp_path = Path(temp_file.name)
|
343
|
+
|
344
|
+
try:
|
345
|
+
# Mock assembly completion
|
346
|
+
with patch.object(
|
347
|
+
mock_client,
|
348
|
+
"_wait_for_all_assemblies_completion",
|
349
|
+
return_value=True,
|
350
|
+
):
|
351
|
+
upload_id = mock_client.upload_file(job_name, temp_path)
|
352
|
+
|
353
|
+
# Verify multiple chunks were uploaded
|
354
|
+
# File size: ~32MB + 1000 bytes, chunk size: 16MB
|
355
|
+
# Expected chunks: 3 (16MB + 16MB + 1000 bytes)
|
356
|
+
expected_chunks = 3
|
357
|
+
assert mock_client.multipart_client.post.call_count == expected_chunks
|
358
|
+
assert upload_id is not None
|
359
|
+
|
360
|
+
# Verify all calls were to the upload-chunk endpoint
|
361
|
+
for call in mock_client.multipart_client.post.call_args_list:
|
362
|
+
assert f"/v0.1/crows/{job_name}/upload-chunk" in call[0][0]
|
363
|
+
finally:
|
364
|
+
temp_path.unlink()
|
365
|
+
|
366
|
+
def test_upload_chunks_parallel_batch_processing(self, mock_client):
|
367
|
+
"""Test that chunks are processed in parallel batches."""
|
368
|
+
job_name = "test-job"
|
369
|
+
file_path = Path("test_file.txt")
|
370
|
+
file_name = "test_file.txt"
|
371
|
+
upload_id = "test-upload-id"
|
372
|
+
num_regular_chunks = 5 # Smaller number for easier testing
|
373
|
+
total_chunks = 6
|
374
|
+
|
375
|
+
# Use patch to mock the _upload_single_chunk method
|
376
|
+
with patch.object(mock_client, "_upload_single_chunk") as mock_upload_chunk:
|
377
|
+
# Call the method - it should use ThreadPoolExecutor internally
|
378
|
+
mock_client._upload_chunks_parallel(
|
379
|
+
job_name,
|
380
|
+
file_path,
|
381
|
+
file_name,
|
382
|
+
upload_id,
|
383
|
+
num_regular_chunks,
|
384
|
+
total_chunks,
|
385
|
+
)
|
386
|
+
|
387
|
+
# Verify all chunks were processed by checking the call count
|
388
|
+
assert mock_upload_chunk.call_count == num_regular_chunks
|
389
|
+
|
390
|
+
# Verify the calls were made with correct parameters
|
391
|
+
for call_idx, call in enumerate(mock_upload_chunk.call_args_list):
|
392
|
+
args = call[0]
|
393
|
+
assert args[0] == job_name
|
394
|
+
assert args[1] == file_path
|
395
|
+
assert args[2] == file_name
|
396
|
+
assert args[3] == upload_id
|
397
|
+
assert args[4] == call_idx # chunk_index
|
398
|
+
assert args[5] == total_chunks
|
399
|
+
|
400
|
+
def test_upload_single_chunk_success(self, mock_client):
|
401
|
+
"""Test successful upload of a single chunk."""
|
402
|
+
job_name = "test-job"
|
403
|
+
file_path = Path("test_file.txt")
|
404
|
+
file_name = "test_file.txt"
|
405
|
+
upload_id = "test-upload-id"
|
406
|
+
chunk_index = 0
|
407
|
+
total_chunks = 5
|
408
|
+
|
409
|
+
# Mock file content
|
410
|
+
chunk_content = b"A" * mock_client.CHUNK_SIZE
|
411
|
+
|
412
|
+
# Mock successful response
|
413
|
+
mock_response = MagicMock()
|
414
|
+
mock_response.raise_for_status.return_value = None
|
415
|
+
mock_client.multipart_client.post.return_value = mock_response
|
416
|
+
|
417
|
+
with patch("builtins.open", mock_open(read_data=chunk_content)):
|
418
|
+
with patch("tempfile.NamedTemporaryFile") as mock_temp_file:
|
419
|
+
# Setup mock temporary file
|
420
|
+
mock_temp_file.return_value.__enter__.return_value.name = "temp_chunk"
|
421
|
+
|
422
|
+
mock_client._upload_single_chunk(
|
423
|
+
job_name, file_path, file_name, upload_id, chunk_index, total_chunks
|
424
|
+
)
|
425
|
+
|
426
|
+
# Verify the upload was called with correct parameters
|
427
|
+
mock_client.multipart_client.post.assert_called_once()
|
428
|
+
call_args = mock_client.multipart_client.post.call_args
|
429
|
+
|
430
|
+
# Check endpoint
|
431
|
+
assert f"/v0.1/crows/{job_name}/upload-chunk" in call_args[0][0]
|
432
|
+
|
433
|
+
# Check data parameters
|
434
|
+
data = call_args[1]["data"]
|
435
|
+
assert data["file_name"] == file_name
|
436
|
+
assert data["chunk_index"] == chunk_index
|
437
|
+
assert data["total_chunks"] == total_chunks
|
438
|
+
assert data["upload_id"] == upload_id
|
439
|
+
|
440
|
+
def test_upload_final_chunk_with_retry_on_conflict(self, mock_client):
|
441
|
+
"""Test final chunk upload with retry logic for missing chunks (409 conflict)."""
|
442
|
+
job_name = "test-job"
|
443
|
+
file_path = Path("test_file.txt")
|
444
|
+
file_name = "test_file.txt"
|
445
|
+
upload_id = "test-upload-id"
|
446
|
+
chunk_index = 2
|
447
|
+
total_chunks = 3
|
448
|
+
|
449
|
+
# Mock file content
|
450
|
+
chunk_content = b"A" * 1000
|
451
|
+
|
452
|
+
# Create mock responses: first returns 409 (conflict), second succeeds
|
453
|
+
mock_response_conflict = MagicMock()
|
454
|
+
mock_response_conflict.status_code = 409 # CONFLICT
|
455
|
+
mock_response_conflict.raise_for_status.side_effect = None
|
456
|
+
|
457
|
+
mock_response_success = MagicMock()
|
458
|
+
mock_response_success.status_code = 200
|
459
|
+
mock_response_success.raise_for_status.return_value = None
|
460
|
+
mock_response_success.json.return_value = {
|
461
|
+
"status_url": "http://test.com/status"
|
462
|
+
}
|
463
|
+
|
464
|
+
mock_client.multipart_client.post.side_effect = [
|
465
|
+
mock_response_conflict,
|
466
|
+
mock_response_success,
|
467
|
+
]
|
468
|
+
|
469
|
+
with patch("builtins.open", mock_open(read_data=chunk_content)):
|
470
|
+
with patch("tempfile.NamedTemporaryFile") as mock_temp_file:
|
471
|
+
with patch("time.sleep") as mock_sleep: # Speed up test
|
472
|
+
mock_temp_file.return_value.__enter__.return_value.name = (
|
473
|
+
"temp_chunk"
|
474
|
+
)
|
475
|
+
|
476
|
+
status_url = mock_client._upload_final_chunk(
|
477
|
+
job_name,
|
478
|
+
file_path,
|
479
|
+
file_name,
|
480
|
+
upload_id,
|
481
|
+
chunk_index,
|
482
|
+
total_chunks,
|
483
|
+
)
|
484
|
+
|
485
|
+
# Verify retry was attempted
|
486
|
+
assert mock_client.multipart_client.post.call_count == 2
|
487
|
+
assert status_url == "http://test.com/status"
|
488
|
+
mock_sleep.assert_called_once() # Verify sleep was called for retry
|
489
|
+
|
490
|
+
def test_upload_final_chunk_max_retries_exceeded(self, mock_client):
|
491
|
+
"""Test final chunk upload fails after max retries."""
|
492
|
+
job_name = "test-job"
|
493
|
+
file_path = Path("test_file.txt")
|
494
|
+
file_name = "test_file.txt"
|
495
|
+
upload_id = "test-upload-id"
|
496
|
+
chunk_index = 2
|
497
|
+
total_chunks = 3
|
498
|
+
|
499
|
+
# Mock file content
|
500
|
+
chunk_content = b"A" * 1000
|
501
|
+
|
502
|
+
# Mock response that always returns 409 (conflict) and raises an exception on raise_for_status
|
503
|
+
mock_response = MagicMock()
|
504
|
+
mock_response.status_code = 409
|
505
|
+
# Make raise_for_status raise an exception after the retries are exhausted
|
506
|
+
from httpx import HTTPStatusError, Request, codes
|
507
|
+
|
508
|
+
mock_request = MagicMock(spec=Request)
|
509
|
+
mock_response.raise_for_status.side_effect = HTTPStatusError(
|
510
|
+
"409 Conflict", request=mock_request, response=mock_response
|
511
|
+
)
|
512
|
+
mock_client.multipart_client.post.return_value = mock_response
|
513
|
+
|
514
|
+
with patch("builtins.open", mock_open(read_data=chunk_content)):
|
515
|
+
with patch("tempfile.NamedTemporaryFile") as mock_temp_file:
|
516
|
+
with patch("time.sleep"): # Speed up test
|
517
|
+
# Set up the code constant correctly
|
518
|
+
with patch("futurehouse_client.clients.rest_client.codes", codes):
|
519
|
+
mock_temp_file.return_value.__enter__.return_value.name = (
|
520
|
+
"temp_chunk"
|
521
|
+
)
|
522
|
+
|
523
|
+
with pytest.raises(
|
524
|
+
FileUploadError, match="Error uploading final chunk"
|
525
|
+
):
|
526
|
+
mock_client._upload_final_chunk(
|
527
|
+
job_name,
|
528
|
+
file_path,
|
529
|
+
file_name,
|
530
|
+
upload_id,
|
531
|
+
chunk_index,
|
532
|
+
total_chunks,
|
533
|
+
)
|
534
|
+
|
535
|
+
# Verify that retries were attempted (should be 3 attempts total)
|
536
|
+
assert mock_client.multipart_client.post.call_count == 3
|
537
|
+
|
538
|
+
def test_upload_directory_recursive(self, mock_client):
|
539
|
+
"""Test uploading a directory with nested files."""
|
540
|
+
job_name = "test-job"
|
541
|
+
|
542
|
+
# Mock successful response
|
543
|
+
mock_response = MagicMock()
|
544
|
+
mock_response.raise_for_status.return_value = None
|
545
|
+
mock_response.json.return_value = {"status_url": "http://test.com/status"}
|
546
|
+
mock_client.multipart_client.post.return_value = mock_response
|
547
|
+
|
548
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
549
|
+
temp_path = Path(temp_dir)
|
550
|
+
|
551
|
+
# Create nested directory structure
|
552
|
+
(temp_path / "subdir").mkdir()
|
553
|
+
(temp_path / "file1.txt").write_text("content1")
|
554
|
+
(temp_path / "file2.txt").write_text("content2")
|
555
|
+
(temp_path / "subdir" / "file3.txt").write_text("content3")
|
556
|
+
|
557
|
+
# Mock assembly completion
|
558
|
+
with patch.object(
|
559
|
+
mock_client, "_wait_for_all_assemblies_completion", return_value=True
|
560
|
+
):
|
561
|
+
upload_id_result = mock_client.upload_file(job_name, temp_path)
|
562
|
+
|
563
|
+
# Verify files were uploaded (3 files total)
|
564
|
+
assert mock_client.multipart_client.post.call_count == 3
|
565
|
+
# Just check that we got some upload_id back (it will be a UUID)
|
566
|
+
assert upload_id_result is not None
|
567
|
+
assert len(upload_id_result) > 0
|
568
|
+
|
569
|
+
# Verify calls were made to upload-chunk endpoint
|
570
|
+
for call in mock_client.multipart_client.post.call_args_list:
|
571
|
+
assert f"/v0.1/crows/{job_name}/upload-chunk" in call[0][0]
|
572
|
+
|
573
|
+
def test_upload_file_assembly_failure(self, mock_client):
|
574
|
+
"""Test upload_file raises error when assembly fails."""
|
575
|
+
job_name = "test-job"
|
576
|
+
file_content = b"test content"
|
577
|
+
|
578
|
+
# Mock successful upload but failed assembly
|
579
|
+
mock_response = MagicMock()
|
580
|
+
mock_response.raise_for_status.return_value = None
|
581
|
+
mock_response.json.return_value = {"status_url": "http://test.com/status"}
|
582
|
+
mock_client.multipart_client.post.return_value = mock_response
|
583
|
+
|
584
|
+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
585
|
+
temp_file.write(file_content)
|
586
|
+
temp_file.flush()
|
587
|
+
temp_path = Path(temp_file.name)
|
588
|
+
|
589
|
+
try:
|
590
|
+
# Mock assembly failure
|
591
|
+
with (
|
592
|
+
patch.object(
|
593
|
+
mock_client,
|
594
|
+
"_wait_for_all_assemblies_completion",
|
595
|
+
return_value=False,
|
596
|
+
),
|
597
|
+
pytest.raises(
|
598
|
+
RestClientError, match="Assembly failed or timed out"
|
599
|
+
),
|
600
|
+
):
|
601
|
+
mock_client.upload_file(job_name, temp_path)
|
602
|
+
finally:
|
603
|
+
temp_path.unlink()
|
604
|
+
|
605
|
+
def test_upload_file_skip_assembly_wait(self, mock_client):
|
606
|
+
"""Test upload_file with wait_for_assembly=False."""
|
607
|
+
job_name = "test-job"
|
608
|
+
file_content = b"test content"
|
609
|
+
|
610
|
+
# Mock successful response
|
611
|
+
mock_response = MagicMock()
|
612
|
+
mock_response.raise_for_status.return_value = None
|
613
|
+
mock_response.json.return_value = {"status_url": "http://test.com/status"}
|
614
|
+
mock_client.multipart_client.post.return_value = mock_response
|
615
|
+
|
616
|
+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
617
|
+
temp_file.write(file_content)
|
618
|
+
temp_file.flush()
|
619
|
+
temp_path = Path(temp_file.name)
|
620
|
+
|
621
|
+
try:
|
622
|
+
with patch.object(
|
623
|
+
mock_client, "_wait_for_all_assemblies_completion"
|
624
|
+
) as mock_wait:
|
625
|
+
upload_id = mock_client.upload_file(
|
626
|
+
job_name, temp_path, wait_for_assembly=False
|
627
|
+
)
|
628
|
+
|
629
|
+
# Verify assembly wait was not called
|
630
|
+
mock_wait.assert_not_called()
|
631
|
+
assert upload_id is not None
|
632
|
+
finally:
|
633
|
+
temp_path.unlink()
|
634
|
+
|
635
|
+
def test_max_concurrent_chunks_constant(self, mock_client):
|
636
|
+
"""Test that MAX_CONCURRENT_CHUNKS constant is properly set."""
|
637
|
+
assert mock_client.MAX_CONCURRENT_CHUNKS == 12
|
638
|
+
assert isinstance(mock_client.MAX_CONCURRENT_CHUNKS, int)
|
639
|
+
assert mock_client.MAX_CONCURRENT_CHUNKS > 0
|
640
|
+
|
641
|
+
def test_upload_empty_file_handled(self, mock_client):
|
642
|
+
"""Test that empty files are handled gracefully."""
|
643
|
+
job_name = "test-job"
|
644
|
+
|
645
|
+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
646
|
+
# Create empty file
|
647
|
+
temp_file.flush()
|
648
|
+
temp_path = Path(temp_file.name)
|
649
|
+
|
650
|
+
try:
|
651
|
+
# Mock assembly completion
|
652
|
+
with patch.object(
|
653
|
+
mock_client,
|
654
|
+
"_wait_for_all_assemblies_completion",
|
655
|
+
return_value=True,
|
656
|
+
):
|
657
|
+
upload_id = mock_client.upload_file(job_name, temp_path)
|
658
|
+
|
659
|
+
# Should not call post since empty files are skipped
|
660
|
+
assert mock_client.multipart_client.post.call_count == 0
|
661
|
+
assert upload_id is not None
|
662
|
+
finally:
|
663
|
+
temp_path.unlink()
|
664
|
+
|
665
|
+
def test_chunk_size_calculation(self, mock_client):
|
666
|
+
"""Test that chunk size calculation works correctly."""
|
667
|
+
file_size = 32 * 1024 * 1024 + 1000 # 32MB + 1000 bytes
|
668
|
+
chunk_size = mock_client.CHUNK_SIZE # 16MB
|
669
|
+
|
670
|
+
expected_total_chunks = (file_size + chunk_size - 1) // chunk_size
|
671
|
+
assert expected_total_chunks == 3 # 16MB + 16MB + ~1000 bytes
|
672
|
+
|
673
|
+
# Test edge cases
|
674
|
+
assert (
|
675
|
+
(chunk_size - 1) + chunk_size - 1
|
676
|
+
) // chunk_size == 1 # Just under 1 chunk
|
677
|
+
assert (chunk_size + chunk_size - 1) // chunk_size == 1 # Exactly 1 chunk
|
678
|
+
assert (
|
679
|
+
(chunk_size + 1) + chunk_size - 1
|
680
|
+
) // chunk_size == 2 # Just over 1 chunk
|
@@ -1,235 +0,0 @@
|
|
1
|
-
# ruff: noqa: ARG001
|
2
|
-
import asyncio
|
3
|
-
import os
|
4
|
-
import time
|
5
|
-
|
6
|
-
import pytest
|
7
|
-
from futurehouse_client.clients import (
|
8
|
-
JobNames,
|
9
|
-
)
|
10
|
-
from futurehouse_client.clients.rest_client import RestClient, TaskFetchError
|
11
|
-
from futurehouse_client.models.app import (
|
12
|
-
PhoenixTaskResponse,
|
13
|
-
PQATaskResponse,
|
14
|
-
Stage,
|
15
|
-
TaskRequest,
|
16
|
-
TaskResponseVerbose,
|
17
|
-
)
|
18
|
-
from futurehouse_client.models.rest import ExecutionStatus
|
19
|
-
from pytest_subtests import SubTests
|
20
|
-
|
21
|
-
ADMIN_API_KEY = os.environ["PLAYWRIGHT_ADMIN_API_KEY"]
|
22
|
-
PUBLIC_API_KEY = os.environ["PLAYWRIGHT_PUBLIC_API_KEY"]
|
23
|
-
TEST_MAX_POLLS = 100
|
24
|
-
|
25
|
-
|
26
|
-
@pytest.fixture
|
27
|
-
def admin_client():
|
28
|
-
"""Create a RestClient for testing; using an admin key."""
|
29
|
-
return RestClient(
|
30
|
-
stage=Stage.DEV,
|
31
|
-
api_key=ADMIN_API_KEY,
|
32
|
-
)
|
33
|
-
|
34
|
-
|
35
|
-
@pytest.fixture
|
36
|
-
def pub_client():
|
37
|
-
"""Create a RestClient for testing; using a public user key with limited access."""
|
38
|
-
return RestClient(
|
39
|
-
stage=Stage.DEV,
|
40
|
-
api_key=PUBLIC_API_KEY,
|
41
|
-
)
|
42
|
-
|
43
|
-
|
44
|
-
@pytest.fixture
|
45
|
-
def task_req():
|
46
|
-
"""Create a sample task request."""
|
47
|
-
return TaskRequest(
|
48
|
-
name=JobNames.from_string("dummy"),
|
49
|
-
query="How many moons does earth have?",
|
50
|
-
)
|
51
|
-
|
52
|
-
|
53
|
-
@pytest.fixture
|
54
|
-
def pqa_task_req():
|
55
|
-
return TaskRequest(
|
56
|
-
name=JobNames.from_string("crow"),
|
57
|
-
query="How many moons does earth have?",
|
58
|
-
)
|
59
|
-
|
60
|
-
|
61
|
-
@pytest.fixture
|
62
|
-
def phoenix_task_req():
|
63
|
-
return TaskRequest(
|
64
|
-
name=JobNames.from_string("phoenix"),
|
65
|
-
query="What is the molecular weight of ascorbic acids?",
|
66
|
-
)
|
67
|
-
|
68
|
-
|
69
|
-
@pytest.mark.timeout(300)
|
70
|
-
@pytest.mark.flaky(reruns=3)
|
71
|
-
def test_futurehouse_dummy_env_crow(admin_client: RestClient, task_req: TaskRequest):
|
72
|
-
admin_client.create_task(task_req)
|
73
|
-
while (task_status := admin_client.get_task().status) in {"queued", "in progress"}:
|
74
|
-
time.sleep(5)
|
75
|
-
assert task_status == "success"
|
76
|
-
|
77
|
-
|
78
|
-
def test_insufficient_permissions_request(
|
79
|
-
pub_client: RestClient, task_req: TaskRequest
|
80
|
-
):
|
81
|
-
# Create a new instance so that cached credentials aren't reused
|
82
|
-
with pytest.raises(TaskFetchError) as exc_info:
|
83
|
-
pub_client.create_task(task_req)
|
84
|
-
|
85
|
-
assert "Error creating task" in str(exc_info.value)
|
86
|
-
|
87
|
-
|
88
|
-
@pytest.mark.timeout(300)
|
89
|
-
@pytest.mark.asyncio
|
90
|
-
async def test_job_response( # noqa: PLR0915
|
91
|
-
subtests: SubTests,
|
92
|
-
admin_client: RestClient,
|
93
|
-
pqa_task_req: TaskRequest,
|
94
|
-
phoenix_task_req: TaskRequest,
|
95
|
-
):
|
96
|
-
task_id = admin_client.create_task(pqa_task_req)
|
97
|
-
atask_id = await admin_client.acreate_task(pqa_task_req)
|
98
|
-
phoenix_task_id = admin_client.create_task(phoenix_task_req)
|
99
|
-
aphoenix_task_id = await admin_client.acreate_task(phoenix_task_req)
|
100
|
-
|
101
|
-
with subtests.test("Test TaskResponse with queued task"):
|
102
|
-
task_response = admin_client.get_task(task_id)
|
103
|
-
assert task_response.status in {"queued", "in progress"}
|
104
|
-
assert task_response.job_name == pqa_task_req.name
|
105
|
-
assert task_response.query == pqa_task_req.query
|
106
|
-
task_response = await admin_client.aget_task(atask_id)
|
107
|
-
assert task_response.status in {"queued", "in progress"}
|
108
|
-
assert task_response.job_name == pqa_task_req.name
|
109
|
-
assert task_response.query == pqa_task_req.query
|
110
|
-
|
111
|
-
for _ in range(TEST_MAX_POLLS):
|
112
|
-
task_response = admin_client.get_task(task_id)
|
113
|
-
if task_response.status in ExecutionStatus.terminal_states():
|
114
|
-
break
|
115
|
-
await asyncio.sleep(5)
|
116
|
-
|
117
|
-
for _ in range(TEST_MAX_POLLS):
|
118
|
-
task_response = await admin_client.aget_task(atask_id)
|
119
|
-
if task_response.status in ExecutionStatus.terminal_states():
|
120
|
-
break
|
121
|
-
await asyncio.sleep(5)
|
122
|
-
|
123
|
-
with subtests.test("Test PQA job response"):
|
124
|
-
task_response = admin_client.get_task(task_id)
|
125
|
-
assert isinstance(task_response, PQATaskResponse)
|
126
|
-
# assert it has general fields
|
127
|
-
assert task_response.status == "success"
|
128
|
-
assert task_response.task_id is not None
|
129
|
-
assert pqa_task_req.name in task_response.job_name
|
130
|
-
assert pqa_task_req.query in task_response.query
|
131
|
-
# assert it has PQA specific fields
|
132
|
-
assert task_response.answer is not None
|
133
|
-
# assert it's not verbose
|
134
|
-
assert not hasattr(task_response, "environment_frame")
|
135
|
-
assert not hasattr(task_response, "agent_state")
|
136
|
-
|
137
|
-
with subtests.test("Test async PQA job response"):
|
138
|
-
task_response = await admin_client.aget_task(atask_id)
|
139
|
-
assert isinstance(task_response, PQATaskResponse)
|
140
|
-
# assert it has general fields
|
141
|
-
assert task_response.status == "success"
|
142
|
-
assert task_response.task_id is not None
|
143
|
-
assert pqa_task_req.name in task_response.job_name
|
144
|
-
assert pqa_task_req.query in task_response.query
|
145
|
-
# assert it has PQA specific fields
|
146
|
-
assert task_response.answer is not None
|
147
|
-
# assert it's not verbose
|
148
|
-
assert not hasattr(task_response, "environment_frame")
|
149
|
-
assert not hasattr(task_response, "agent_state")
|
150
|
-
|
151
|
-
with subtests.test("Test Phoenix job response"):
|
152
|
-
task_response = admin_client.get_task(phoenix_task_id)
|
153
|
-
assert isinstance(task_response, PhoenixTaskResponse)
|
154
|
-
assert task_response.status == "success"
|
155
|
-
assert task_response.task_id is not None
|
156
|
-
assert phoenix_task_req.name in task_response.job_name
|
157
|
-
assert phoenix_task_req.query in task_response.query
|
158
|
-
|
159
|
-
with subtests.test("Test async Phoenix job response"):
|
160
|
-
task_response = await admin_client.aget_task(aphoenix_task_id)
|
161
|
-
assert isinstance(task_response, PhoenixTaskResponse)
|
162
|
-
assert task_response.status == "success"
|
163
|
-
assert task_response.task_id is not None
|
164
|
-
assert phoenix_task_req.name in task_response.job_name
|
165
|
-
assert phoenix_task_req.query in task_response.query
|
166
|
-
|
167
|
-
with subtests.test("Test task response with verbose"):
|
168
|
-
task_response = admin_client.get_task(task_id, verbose=True)
|
169
|
-
assert isinstance(task_response, TaskResponseVerbose)
|
170
|
-
assert task_response.status == "success"
|
171
|
-
assert task_response.environment_frame is not None
|
172
|
-
assert task_response.agent_state is not None
|
173
|
-
|
174
|
-
with subtests.test("Test task async response with verbose"):
|
175
|
-
task_response = await admin_client.aget_task(atask_id, verbose=True)
|
176
|
-
assert isinstance(task_response, TaskResponseVerbose)
|
177
|
-
assert task_response.status == "success"
|
178
|
-
assert task_response.environment_frame is not None
|
179
|
-
assert task_response.agent_state is not None
|
180
|
-
|
181
|
-
|
182
|
-
@pytest.mark.timeout(300)
|
183
|
-
@pytest.mark.flaky(reruns=3)
|
184
|
-
def test_run_until_done_futurehouse_dummy_env_crow(
|
185
|
-
admin_client: RestClient, task_req: TaskRequest
|
186
|
-
):
|
187
|
-
tasks_to_do = [task_req, task_req]
|
188
|
-
|
189
|
-
results = admin_client.run_tasks_until_done(tasks_to_do)
|
190
|
-
|
191
|
-
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
192
|
-
assert all(task.status == "success" for task in results)
|
193
|
-
|
194
|
-
|
195
|
-
@pytest.mark.timeout(300)
|
196
|
-
@pytest.mark.flaky(reruns=3)
|
197
|
-
@pytest.mark.asyncio
|
198
|
-
async def test_arun_until_done_futurehouse_dummy_env_crow(
|
199
|
-
admin_client: RestClient, task_req: TaskRequest
|
200
|
-
):
|
201
|
-
tasks_to_do = [task_req, task_req]
|
202
|
-
|
203
|
-
results = await admin_client.arun_tasks_until_done(tasks_to_do)
|
204
|
-
|
205
|
-
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
206
|
-
assert all(task.status == "success" for task in results)
|
207
|
-
|
208
|
-
|
209
|
-
@pytest.mark.timeout(300)
|
210
|
-
@pytest.mark.flaky(reruns=3)
|
211
|
-
@pytest.mark.asyncio
|
212
|
-
async def test_timeout_run_until_done_futurehouse_dummy_env_crow(
|
213
|
-
admin_client: RestClient, task_req: TaskRequest
|
214
|
-
):
|
215
|
-
tasks_to_do = [task_req, task_req]
|
216
|
-
|
217
|
-
results = await admin_client.arun_tasks_until_done(
|
218
|
-
tasks_to_do, verbose=True, timeout=5, progress_bar=True
|
219
|
-
)
|
220
|
-
|
221
|
-
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
222
|
-
assert all(task.status != "success" for task in results), "Should not be success."
|
223
|
-
assert all(not isinstance(task, PQATaskResponse) for task in results), (
|
224
|
-
"Should be verbose."
|
225
|
-
)
|
226
|
-
|
227
|
-
results = admin_client.run_tasks_until_done(
|
228
|
-
tasks_to_do, verbose=True, timeout=5, progress_bar=True
|
229
|
-
)
|
230
|
-
|
231
|
-
assert len(results) == len(tasks_to_do), "Should return 2 tasks."
|
232
|
-
assert all(task.status != "success" for task in results), "Should not be success."
|
233
|
-
assert all(not isinstance(task, PQATaskResponse) for task in results), (
|
234
|
-
"Should be verbose."
|
235
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
{futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/docs/client_notebook.ipynb
RENAMED
File without changes
|
{futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev129}/futurehouse_client/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|