sutro 0.1.17__tar.gz → 0.1.18__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sutro
3
- Version: 0.1.17
3
+ Version: 0.1.18
4
4
  Summary: Sutro Python SDK
5
5
  Project-URL: Homepage, https://sutro.sh
6
6
  Project-URL: Documentation, https://docs.sutro.sh
@@ -9,7 +9,7 @@ installer = "uv"
9
9
 
10
10
  [project]
11
11
  name = "sutro"
12
- version = "0.1.17"
12
+ version = "0.1.18"
13
13
  description = "Sutro Python SDK"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
@@ -266,11 +266,6 @@ class Sutro:
266
266
  "random_seed_per_input": random_seed_per_input,
267
267
  "truncate_rows": truncate_rows
268
268
  }
269
- if dry_run:
270
- spinner_text = to_colored_text("Retrieving cost estimates...")
271
- else:
272
- t = f"Creating priority {job_priority} job"
273
- spinner_text = to_colored_text(t)
274
269
 
275
270
  # There are two gotchas with yaspin:
276
271
  # 1. Can't use print while in spinner is running
@@ -279,6 +274,8 @@ class Sutro:
279
274
  # Terminal size {self._terminal_width} is too small to display spinner with the given settings.
280
275
  # https://github.com/pavdmyt/yaspin/blob/9c7430b499ab4611888ece39783a870e4a05fa45/yaspin/core.py#L568-L571
281
276
  job_id = None
277
+ t = f"Creating {'[dry run] ' if dry_run else ''}priority {job_priority} job"
278
+ spinner_text = to_colored_text(t)
282
279
  with yaspin(SPINNER, text=spinner_text, color=YASPIN_COLOR) as spinner:
283
280
  response = requests.post(
284
281
  endpoint, data=json.dumps(payload), headers=headers
@@ -292,13 +289,19 @@ class Sutro:
292
289
  print(to_colored_text(response.json(), state="fail"))
293
290
  return None
294
291
  else:
292
+ job_id = response_data["results"]
295
293
  if dry_run:
296
294
  spinner.write(
297
- to_colored_text(" Cost estimates retrieved", state="success")
295
+ to_colored_text(f"Awaiting cost estimates with job ID: {job_id}. You can safely detach and retrieve the cost estimates later.", state="info")
296
+ )
297
+ spinner.stop()
298
+ self.await_job_completion(job_id, obtain_results=False)
299
+ cost_estimate = self._get_job_cost_estimate(job_id)
300
+ spinner.write(
301
+ to_colored_text(f"✔ Cost estimates retrieved for job {job_id}: ${cost_estimate}", state="success")
298
302
  )
299
- return response_data["results"]
303
+ return job_id
300
304
  else:
301
- job_id = response_data["results"]
302
305
  spinner.write(
303
306
  to_colored_text(
304
307
  f"🛠️ Priority {job_priority} Job created with ID: {job_id}",
@@ -315,6 +318,13 @@ class Sutro:
315
318
 
316
319
  success = False
317
320
  if stay_attached and job_id is not None:
321
+ spinner.write(to_colored_text("Awaiting job start...", "info"))
322
+ started = self._await_job_start(job_id)
323
+ if not started:
324
+ failure_reason = self._get_failure_reason(job_id)
325
+ spinner.write(to_colored_text(f"Failure reason: {failure_reason['message']}", "fail"))
326
+ return None
327
+
318
328
  s = requests.Session()
319
329
  payload = {
320
330
  "job_id": job_id,
@@ -719,6 +729,40 @@ class Sutro:
719
729
  return
720
730
  return response.json()["jobs"]
721
731
 
732
+ def _list_jobs_helper(self):
733
+ """
734
+ Helper function to list jobs.
735
+ """
736
+ endpoint = f"{self.base_url}/list-jobs"
737
+ headers = {
738
+ "Authorization": f"Key {self.api_key}",
739
+ "Content-Type": "application/json",
740
+ }
741
+ response = requests.get(endpoint, headers=headers)
742
+ if response.status_code != 200:
743
+ return None
744
+ return response.json()["jobs"]
745
+
746
+ def _get_job_cost_estimate(self, job_id: str):
747
+ """
748
+ Get the cost estimate for a job.
749
+ """
750
+ all_jobs = self._list_jobs_helper()
751
+ for job in all_jobs:
752
+ if job["job_id"] == job_id:
753
+ return job["cost_estimate"]
754
+ return None
755
+
756
+ def _get_failure_reason(self, job_id: str):
757
+ """
758
+ Get the failure reason for a job.
759
+ """
760
+ all_jobs = self._list_jobs_helper()
761
+ for job in all_jobs:
762
+ if job["job_id"] == job_id:
763
+ return job["failure_reason"]
764
+ return None
765
+
722
766
  def _fetch_job_status(self, job_id: str):
723
767
  """
724
768
  Core logic to fetch job status from the API.
@@ -1162,7 +1206,7 @@ class Sutro:
1162
1206
  return
1163
1207
  return response.json()["quotas"]
1164
1208
 
1165
- def await_job_completion(self, job_id: str, timeout: Optional[int] = 7200) -> list | None:
1209
+ def await_job_completion(self, job_id: str, timeout: Optional[int] = 7200, obtain_results: bool = True) -> list | None:
1166
1210
  """
1167
1211
  Waits for job completion to occur and then returns the results upon
1168
1212
  a successful completion.
@@ -1181,7 +1225,7 @@ class Sutro:
1181
1225
  results = None
1182
1226
  start_time = time.time()
1183
1227
  with yaspin(
1184
- SPINNER, text=to_colored_text("Awaiting job completion"), color=YASPIN_COLOR
1228
+ SPINNER, text=to_colored_text("Awaiting job completion"), color=YASPIN_COLOR
1185
1229
  ) as spinner:
1186
1230
  while (time.time() - start_time) < timeout:
1187
1231
  try:
@@ -1201,7 +1245,8 @@ class Sutro:
1201
1245
  if status == JobStatus.SUCCEEDED:
1202
1246
  spinner.write(to_colored_text("Job completed! Retrieving results...", "success"))
1203
1247
  spinner.stop() # Stop this spinner as `get_job_results` has its own spinner text
1204
- results = self.get_job_results(job_id)
1248
+ if obtain_results:
1249
+ results = self.get_job_results(job_id)
1205
1250
  break
1206
1251
  if status == JobStatus.FAILED:
1207
1252
  spinner.write(to_colored_text("Job has failed", "fail"))
@@ -1213,4 +1258,43 @@ class Sutro:
1213
1258
 
1214
1259
  time.sleep(POLL_INTERVAL)
1215
1260
 
1216
- return results
1261
+ return results
1262
+
1263
+ def _await_job_start(self, job_id: str, timeout: Optional[int] = 7200):
1264
+ """
1265
+ Waits for job start to occur and then returns the results upon
1266
+ a successful start.
1267
+
1268
+ """
1269
+ POLL_INTERVAL = 5
1270
+
1271
+ start_time = time.time()
1272
+ with yaspin(
1273
+ SPINNER, text=to_colored_text("Awaiting job completion"), color=YASPIN_COLOR
1274
+ ) as spinner:
1275
+ while (time.time() - start_time) < timeout:
1276
+ try:
1277
+ status = self._fetch_job_status(job_id)
1278
+ except requests.HTTPError as e:
1279
+ spinner.write(
1280
+ to_colored_text(
1281
+ f"Bad status code: {e.response.status_code}", state="fail"
1282
+ )
1283
+ )
1284
+ spinner.stop()
1285
+ print(to_colored_text(e.response.json(), state="fail"))
1286
+ return None
1287
+
1288
+ spinner.text = to_colored_text(f"Job status is {status} for {job_id}")
1289
+
1290
+ if status == JobStatus.RUNNING or status == JobStatus.STARTING:
1291
+ return True
1292
+ if status == JobStatus.FAILED:
1293
+ return False
1294
+ if status == JobStatus.CANCELLED:
1295
+ return False
1296
+
1297
+ time.sleep(POLL_INTERVAL)
1298
+
1299
+ return False
1300
+
File without changes
File without changes
File without changes
File without changes
File without changes