futurehouse-client 0.3.19.dev111__tar.gz → 0.3.19.dev133__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.
Files changed (30) hide show
  1. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/PKG-INFO +1 -1
  2. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/clients/rest_client.py +179 -214
  3. futurehouse_client-0.3.19.dev133/futurehouse_client/utils/__init__.py +0 -0
  4. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client.egg-info/PKG-INFO +1 -1
  5. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client.egg-info/SOURCES.txt +1 -0
  6. futurehouse_client-0.3.19.dev133/tests/test_rest.py +684 -0
  7. futurehouse_client-0.3.19.dev111/tests/test_rest.py +0 -235
  8. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/LICENSE +0 -0
  9. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/README.md +0 -0
  10. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/docs/__init__.py +0 -0
  11. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/docs/client_notebook.ipynb +0 -0
  12. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/__init__.py +0 -0
  13. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/clients/__init__.py +0 -0
  14. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/clients/job_client.py +0 -0
  15. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/models/__init__.py +0 -0
  16. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/models/app.py +0 -0
  17. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/models/client.py +0 -0
  18. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/models/rest.py +0 -0
  19. /futurehouse_client-0.3.19.dev111/futurehouse_client/utils/__init__.py → /futurehouse_client-0.3.19.dev133/futurehouse_client/py.typed +0 -0
  20. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/utils/auth.py +0 -0
  21. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/utils/general.py +0 -0
  22. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/utils/module_utils.py +0 -0
  23. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client/utils/monitoring.py +0 -0
  24. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client.egg-info/dependency_links.txt +0 -0
  25. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client.egg-info/requires.txt +0 -0
  26. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/futurehouse_client.egg-info/top_level.txt +0 -0
  27. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/pyproject.toml +0 -0
  28. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/setup.cfg +0 -0
  29. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/tests/test_client.py +0 -0
  30. {futurehouse_client-0.3.19.dev111 → futurehouse_client-0.3.19.dev133}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: futurehouse-client
3
- Version: 0.3.19.dev111
3
+ Version: 0.3.19.dev133
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
- try:
448
- task_id = task_id or self.trajectory_id
449
- url = f"/v0.1/trajectories/{task_id}"
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
- if verbose:
472
- return verbose_response
473
- return JobNames.get_response_object_from_job(verbose_response.job_name)(
474
- **data
475
- )
476
- except Exception as e:
477
- raise TaskFetchError(f"Error getting task: {e!s}") from e
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
- try:
489
- task_id = task_id or self.trajectory_id
490
- url = f"/v0.1/trajectories/{task_id}"
491
- full_url = f"{self.base_url}{url}"
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
- with external_trace(
494
- url=full_url,
495
- method="GET",
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
- try:
539
- response = self.client.post(
540
- "/v0.1/crows", json=task_data.model_dump(mode="json")
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
- response.raise_for_status()
543
- trajectory_id = response.json()["trajectory_id"]
544
- self.trajectory_id = trajectory_id
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
- try:
566
- response = await self.async_client.post(
567
- "/v0.1/crows", json=task_data.model_dump(mode="json")
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
- response.raise_for_status()
570
- trajectory_id = response.json()["trajectory_id"]
571
- self.trajectory_id = trajectory_id
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(
@@ -1056,24 +1053,11 @@ class RestClient:
1056
1053
  status_url = None
1057
1054
 
1058
1055
  try:
1059
- # Upload all chunks except the last one in parallel
1060
- if total_chunks > 1:
1061
- self._upload_chunks_parallel(
1062
- job_name,
1063
- file_path,
1064
- file_name,
1065
- upload_id,
1066
- total_chunks - 1,
1067
- total_chunks,
1068
- )
1069
-
1070
- # Upload the last chunk separately (handles assembly)
1071
- status_url = self._upload_final_chunk(
1056
+ status_url = self._upload_chunks_parallel(
1072
1057
  job_name,
1073
1058
  file_path,
1074
1059
  file_name,
1075
1060
  upload_id,
1076
- total_chunks - 1,
1077
1061
  total_chunks,
1078
1062
  )
1079
1063
 
@@ -1089,149 +1073,74 @@ class RestClient:
1089
1073
  file_path: Path,
1090
1074
  file_name: str,
1091
1075
  upload_id: str,
1092
- num_regular_chunks: int,
1093
1076
  total_chunks: int,
1094
- ) -> None:
1095
- """Upload chunks in parallel batches.
1077
+ ) -> str | None:
1078
+ """Upload all chunks in parallel batches, including the final chunk.
1096
1079
 
1097
1080
  Args:
1098
1081
  job_name: The key of the crow to upload to.
1099
1082
  file_path: The path to the file to upload.
1100
1083
  file_name: The name to use for the file.
1101
1084
  upload_id: The upload ID to use.
1102
- num_regular_chunks: Number of regular chunks (excluding final chunk).
1103
1085
  total_chunks: Total number of chunks.
1104
1086
 
1105
- Raises:
1106
- FileUploadError: If there's an error uploading any chunk.
1107
- """
1108
- if num_regular_chunks <= 0:
1109
- return
1110
-
1111
- # Process chunks in batches
1112
- for batch_start in range(0, num_regular_chunks, self.MAX_CONCURRENT_CHUNKS):
1113
- batch_end = min(
1114
- batch_start + self.MAX_CONCURRENT_CHUNKS, num_regular_chunks
1115
- )
1116
-
1117
- # Upload chunks in this batch concurrently
1118
- with ThreadPoolExecutor(max_workers=self.MAX_CONCURRENT_CHUNKS) as executor:
1119
- futures = {
1120
- executor.submit(
1121
- self._upload_single_chunk,
1122
- job_name,
1123
- file_path,
1124
- file_name,
1125
- upload_id,
1126
- chunk_index,
1127
- total_chunks,
1128
- ): chunk_index
1129
- for chunk_index in range(batch_start, batch_end)
1130
- }
1131
-
1132
- for future in as_completed(futures):
1133
- chunk_index = futures[future]
1134
- try:
1135
- future.result()
1136
- logger.debug(
1137
- f"Uploaded chunk {chunk_index + 1}/{total_chunks} of {file_name}"
1138
- )
1139
- except Exception as e:
1140
- logger.error(f"Error uploading chunk {chunk_index}: {e}")
1141
- raise FileUploadError(
1142
- f"Error uploading chunk {chunk_index} of {file_name}: {e}"
1143
- ) from e
1144
-
1145
- def _upload_single_chunk(
1146
- self,
1147
- job_name: str,
1148
- file_path: Path,
1149
- file_name: str,
1150
- upload_id: str,
1151
- chunk_index: int,
1152
- total_chunks: int,
1153
- ) -> None:
1154
- """Upload a single chunk.
1155
-
1156
- Args:
1157
- job_name: The key of the crow to upload to.
1158
- file_path: The path to the file to upload.
1159
- file_name: The name to use for the file.
1160
- upload_id: The upload ID to use.
1161
- chunk_index: The index of this chunk.
1162
- total_chunks: Total number of chunks.
1087
+ Returns:
1088
+ The status URL from the final chunk response, or None if no chunks.
1163
1089
 
1164
1090
  Raises:
1165
- Exception: If there's an error uploading the chunk.
1091
+ FileUploadError: If there's an error uploading any chunk.
1166
1092
  """
1167
- with open(file_path, "rb") as f:
1168
- # Read the chunk from the file
1169
- f.seek(chunk_index * self.CHUNK_SIZE)
1170
- chunk_data = f.read(self.CHUNK_SIZE)
1093
+ if total_chunks <= 0:
1094
+ return None
1171
1095
 
1172
- # Prepare and send the chunk
1173
- with tempfile.NamedTemporaryFile() as temp_file:
1174
- temp_file.write(chunk_data)
1175
- temp_file.flush()
1096
+ if total_chunks > 1:
1097
+ num_regular_chunks = total_chunks - 1
1098
+ for batch_start in range(0, num_regular_chunks, self.MAX_CONCURRENT_CHUNKS):
1099
+ batch_end = min(
1100
+ batch_start + self.MAX_CONCURRENT_CHUNKS, num_regular_chunks
1101
+ )
1176
1102
 
1177
- # Create form data
1178
- with open(temp_file.name, "rb") as chunk_file_obj:
1179
- files = {
1180
- "chunk": (
1103
+ # Upload chunks in this batch concurrently
1104
+ with ThreadPoolExecutor(
1105
+ max_workers=self.MAX_CONCURRENT_CHUNKS
1106
+ ) as executor:
1107
+ futures = {
1108
+ executor.submit(
1109
+ self._upload_single_chunk,
1110
+ job_name,
1111
+ file_path,
1181
1112
  file_name,
1182
- chunk_file_obj,
1183
- "application/octet-stream",
1184
- )
1185
- }
1186
- data = {
1187
- "file_name": file_name,
1188
- "chunk_index": chunk_index,
1189
- "total_chunks": total_chunks,
1190
- "upload_id": upload_id,
1113
+ upload_id,
1114
+ chunk_index,
1115
+ total_chunks,
1116
+ ): chunk_index
1117
+ for chunk_index in range(batch_start, batch_end)
1191
1118
  }
1192
1119
 
1193
- # Send the chunk
1194
- response = self.multipart_client.post(
1195
- f"/v0.1/crows/{job_name}/upload-chunk",
1196
- files=files,
1197
- data=data,
1198
- )
1199
- response.raise_for_status()
1200
-
1201
- def _upload_final_chunk(
1202
- self,
1203
- job_name: str,
1204
- file_path: Path,
1205
- file_name: str,
1206
- upload_id: str,
1207
- chunk_index: int,
1208
- total_chunks: int,
1209
- ) -> str | None:
1210
- """Upload the final chunk with retry logic for missing chunks.
1211
-
1212
- Args:
1213
- job_name: The key of the crow to upload to.
1214
- file_path: The path to the file to upload.
1215
- file_name: The name to use for the file.
1216
- upload_id: The upload ID to use.
1217
- chunk_index: The index of the final chunk.
1218
- total_chunks: Total number of chunks.
1219
-
1220
- Returns:
1221
- The status URL from the response.
1222
-
1223
- Raises:
1224
- FileUploadError: If there's an error uploading the final chunk.
1225
- """
1120
+ for future in as_completed(futures):
1121
+ chunk_index = futures[future]
1122
+ try:
1123
+ future.result()
1124
+ logger.debug(
1125
+ f"Uploaded chunk {chunk_index + 1}/{total_chunks} of {file_name}"
1126
+ )
1127
+ except Exception as e:
1128
+ logger.error(f"Error uploading chunk {chunk_index}: {e}")
1129
+ raise FileUploadError(
1130
+ f"Error uploading chunk {chunk_index} of {file_name}: {e}"
1131
+ ) from e
1132
+
1133
+ # Upload the final chunk with retry logic
1134
+ final_chunk_index = total_chunks - 1
1226
1135
  retries = 0
1227
1136
  max_retries = 3
1228
- retry_delay = 2.0 # seconds
1137
+ retry_delay = 2.0
1229
1138
 
1230
1139
  while retries < max_retries:
1231
1140
  try:
1232
1141
  with open(file_path, "rb") as f:
1233
1142
  # Read the final chunk from the file
1234
- f.seek(chunk_index * self.CHUNK_SIZE)
1143
+ f.seek(final_chunk_index * self.CHUNK_SIZE)
1235
1144
  chunk_data = f.read(self.CHUNK_SIZE)
1236
1145
 
1237
1146
  # Prepare and send the chunk
@@ -1250,7 +1159,7 @@ class RestClient:
1250
1159
  }
1251
1160
  data = {
1252
1161
  "file_name": file_name,
1253
- "chunk_index": chunk_index,
1162
+ "chunk_index": final_chunk_index,
1254
1163
  "total_chunks": total_chunks,
1255
1164
  "upload_id": upload_id,
1256
1165
  }
@@ -1277,7 +1186,7 @@ class RestClient:
1277
1186
  status_url = response_data.get("status_url")
1278
1187
 
1279
1188
  logger.debug(
1280
- f"Uploaded final chunk {chunk_index + 1}/{total_chunks} of {file_name}"
1189
+ f"Uploaded final chunk {final_chunk_index + 1}/{total_chunks} of {file_name}"
1281
1190
  )
1282
1191
  return status_url
1283
1192
 
@@ -1296,6 +1205,62 @@ class RestClient:
1296
1205
  f"Failed to upload final chunk of {file_name} after {max_retries} retries"
1297
1206
  )
1298
1207
 
1208
+ def _upload_single_chunk(
1209
+ self,
1210
+ job_name: str,
1211
+ file_path: Path,
1212
+ file_name: str,
1213
+ upload_id: str,
1214
+ chunk_index: int,
1215
+ total_chunks: int,
1216
+ ) -> None:
1217
+ """Upload a single chunk.
1218
+
1219
+ Args:
1220
+ job_name: The key of the crow to upload to.
1221
+ file_path: The path to the file to upload.
1222
+ file_name: The name to use for the file.
1223
+ upload_id: The upload ID to use.
1224
+ chunk_index: The index of this chunk.
1225
+ total_chunks: Total number of chunks.
1226
+
1227
+ Raises:
1228
+ Exception: If there's an error uploading the chunk.
1229
+ """
1230
+ with open(file_path, "rb") as f:
1231
+ # Read the chunk from the file
1232
+ f.seek(chunk_index * self.CHUNK_SIZE)
1233
+ chunk_data = f.read(self.CHUNK_SIZE)
1234
+
1235
+ # Prepare and send the chunk
1236
+ with tempfile.NamedTemporaryFile() as temp_file:
1237
+ temp_file.write(chunk_data)
1238
+ temp_file.flush()
1239
+
1240
+ # Create form data
1241
+ with open(temp_file.name, "rb") as chunk_file_obj:
1242
+ files = {
1243
+ "chunk": (
1244
+ file_name,
1245
+ chunk_file_obj,
1246
+ "application/octet-stream",
1247
+ )
1248
+ }
1249
+ data = {
1250
+ "file_name": file_name,
1251
+ "chunk_index": chunk_index,
1252
+ "total_chunks": total_chunks,
1253
+ "upload_id": upload_id,
1254
+ }
1255
+
1256
+ # Send the chunk
1257
+ response = self.multipart_client.post(
1258
+ f"/v0.1/crows/{job_name}/upload-chunk",
1259
+ files=files,
1260
+ data=data,
1261
+ )
1262
+ response.raise_for_status()
1263
+
1299
1264
  @retry(
1300
1265
  stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
1301
1266
  wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: futurehouse-client
3
- Version: 0.3.19.dev111
3
+ Version: 0.3.19.dev133
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
@@ -5,6 +5,7 @@ uv.lock
5
5
  docs/__init__.py
6
6
  docs/client_notebook.ipynb
7
7
  futurehouse_client/__init__.py
8
+ futurehouse_client/py.typed
8
9
  futurehouse_client.egg-info/PKG-INFO
9
10
  futurehouse_client.egg-info/SOURCES.txt
10
11
  futurehouse_client.egg-info/dependency_links.txt