sutro 0.1.19__py3-none-any.whl → 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sutro might be problematic. Click here for more details.
- sutro/sdk.py +81 -132
- {sutro-0.1.19.dist-info → sutro-0.1.21.dist-info}/METADATA +1 -1
- sutro-0.1.21.dist-info/RECORD +8 -0
- sutro-0.1.19.dist-info/RECORD +0 -8
- {sutro-0.1.19.dist-info → sutro-0.1.21.dist-info}/WHEEL +0 -0
- {sutro-0.1.19.dist-info → sutro-0.1.21.dist-info}/entry_points.txt +0 -0
- {sutro-0.1.19.dist-info → sutro-0.1.21.dist-info}/licenses/LICENSE +0 -0
sutro/sdk.py
CHANGED
|
@@ -114,7 +114,6 @@ class Sutro:
|
|
|
114
114
|
):
|
|
115
115
|
self.api_key = api_key or self.check_for_api_key()
|
|
116
116
|
self.base_url = base_url
|
|
117
|
-
self.HEARTBEAT_INTERVAL_SECONDS = 15 # Keep in sync w what the backend expects
|
|
118
117
|
|
|
119
118
|
def check_for_api_key(self):
|
|
120
119
|
"""
|
|
@@ -286,69 +285,68 @@ class Sutro:
|
|
|
286
285
|
job_id = None
|
|
287
286
|
t = f"Creating {'[dry run] ' if dry_run else ''}priority {job_priority} job"
|
|
288
287
|
spinner_text = to_colored_text(t)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
response_data = response.json()
|
|
294
|
-
if response.status_code != 200:
|
|
295
|
-
spinner.write(
|
|
296
|
-
to_colored_text(f"Error: {response.status_code}", state="fail")
|
|
288
|
+
try:
|
|
289
|
+
with yaspin(SPINNER, text=spinner_text, color=YASPIN_COLOR) as spinner:
|
|
290
|
+
response = requests.post(
|
|
291
|
+
endpoint, data=json.dumps(payload), headers=headers
|
|
297
292
|
)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return None
|
|
301
|
-
else:
|
|
302
|
-
job_id = response_data["results"]
|
|
303
|
-
if dry_run:
|
|
293
|
+
response_data = response.json()
|
|
294
|
+
if response.status_code != 200:
|
|
304
295
|
spinner.write(
|
|
305
|
-
to_colored_text(f"
|
|
296
|
+
to_colored_text(f"Error: {response.status_code}", state="fail")
|
|
306
297
|
)
|
|
307
298
|
spinner.stop()
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
spinner.write(
|
|
311
|
-
to_colored_text(f"✔ Cost estimates retrieved for job {job_id}: ${cost_estimate}", state="success")
|
|
312
|
-
)
|
|
313
|
-
return job_id
|
|
299
|
+
print(to_colored_text(response.json(), state="fail"))
|
|
300
|
+
return None
|
|
314
301
|
else:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
state="
|
|
302
|
+
job_id = response_data["results"]
|
|
303
|
+
if dry_run:
|
|
304
|
+
spinner.write(
|
|
305
|
+
to_colored_text(f"Awaiting cost estimates with job ID: {job_id}. You can safely detach and retrieve the cost estimates later.", state="info")
|
|
319
306
|
)
|
|
320
|
-
|
|
321
|
-
|
|
307
|
+
spinner.stop()
|
|
308
|
+
self.await_job_completion(job_id, obtain_results=False)
|
|
309
|
+
cost_estimate = self._get_job_cost_estimate(job_id)
|
|
310
|
+
spinner.write(
|
|
311
|
+
to_colored_text(f"✔ Cost estimates retrieved for job {job_id}: ${cost_estimate}", state="success")
|
|
312
|
+
)
|
|
313
|
+
return job_id
|
|
314
|
+
else:
|
|
322
315
|
spinner.write(
|
|
323
316
|
to_colored_text(
|
|
324
|
-
f"
|
|
325
|
-
|
|
317
|
+
f"🛠 Priority {job_priority} Job created with ID: {job_id}.",
|
|
318
|
+
state="success",
|
|
326
319
|
)
|
|
327
|
-
|
|
320
|
+
)
|
|
321
|
+
if not stay_attached:
|
|
322
|
+
spinner.write(
|
|
323
|
+
to_colored_text(
|
|
324
|
+
f"Use `so.get_job_status('{job_id}')` to check the status of the job."
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
return job_id
|
|
328
|
+
except KeyboardInterrupt:
|
|
329
|
+
pass
|
|
330
|
+
finally:
|
|
331
|
+
if spinner:
|
|
332
|
+
spinner.stop()
|
|
328
333
|
|
|
329
334
|
success = False
|
|
330
335
|
if stay_attached and job_id is not None:
|
|
331
336
|
spinner.write(to_colored_text("Awaiting job start...", ))
|
|
332
|
-
|
|
337
|
+
clickable_link = make_clickable_link(f'https://app.sutro.sh/jobs/{job_id}')
|
|
338
|
+
spinner.write(to_colored_text(f'Progress can also be monitored at: {clickable_link}'))
|
|
333
339
|
started = self._await_job_start(job_id)
|
|
334
340
|
if not started:
|
|
335
341
|
failure_reason = self._get_failure_reason(job_id)
|
|
336
342
|
spinner.write(to_colored_text(f"Failure reason: {failure_reason['message']}", "fail"))
|
|
337
343
|
return None
|
|
338
|
-
|
|
339
344
|
s = requests.Session()
|
|
340
|
-
payload = {
|
|
341
|
-
"job_id": job_id,
|
|
342
|
-
}
|
|
343
345
|
pbar = None
|
|
344
346
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
# Use the heartbeat session context manager
|
|
349
|
-
with self.stream_heartbeat_session(job_id, session_token) as s:
|
|
350
|
-
with s.get(
|
|
351
|
-
f"{self.base_url}/stream-job-progress/{job_id}?request_session_token={session_token}",
|
|
347
|
+
try:
|
|
348
|
+
with requests.get(
|
|
349
|
+
f"{self.base_url}/stream-job-progress/{job_id}",
|
|
352
350
|
headers=headers,
|
|
353
351
|
stream=True,
|
|
354
352
|
) as streaming_response:
|
|
@@ -359,6 +357,13 @@ class Sutro:
|
|
|
359
357
|
color=YASPIN_COLOR,
|
|
360
358
|
)
|
|
361
359
|
spinner.start()
|
|
360
|
+
|
|
361
|
+
token_state = {
|
|
362
|
+
'input_tokens': 0,
|
|
363
|
+
'output_tokens': 0,
|
|
364
|
+
'total_tokens_processed_per_second': 0
|
|
365
|
+
}
|
|
366
|
+
|
|
362
367
|
for line in streaming_response.iter_lines():
|
|
363
368
|
if line:
|
|
364
369
|
try:
|
|
@@ -381,12 +386,30 @@ class Sutro:
|
|
|
381
386
|
pbar.update(json_obj["result"] - pbar.n)
|
|
382
387
|
pbar.refresh()
|
|
383
388
|
if json_obj["result"] == len(input_data):
|
|
384
|
-
pbar.close()
|
|
385
389
|
success = True
|
|
386
390
|
elif json_obj["update_type"] == "tokens":
|
|
391
|
+
# Update only the values that are present in this update
|
|
392
|
+
# Currently, the way the progress stream endpoint is defined,
|
|
393
|
+
# its possible to have updates come in that only have 1 or 2 fields
|
|
394
|
+
new = {
|
|
395
|
+
k: v for k, v in json_obj.get('result', {}).items()
|
|
396
|
+
if k in token_state and v >= token_state[k]
|
|
397
|
+
}
|
|
398
|
+
token_state.update(new)
|
|
399
|
+
|
|
387
400
|
if pbar is not None:
|
|
388
|
-
pbar.postfix = f"Input tokens processed: {
|
|
401
|
+
pbar.postfix = f"Input tokens processed: {token_state['input_tokens']}, Output tokens generated: {token_state['output_tokens']}, Total tokens/s: {token_state['total_tokens_processed_per_second']}"
|
|
389
402
|
pbar.refresh()
|
|
403
|
+
|
|
404
|
+
except KeyboardInterrupt:
|
|
405
|
+
pass
|
|
406
|
+
finally:
|
|
407
|
+
# Need to clean these up on keyboard exit otherwise it causes
|
|
408
|
+
# an error
|
|
409
|
+
if pbar is not None:
|
|
410
|
+
pbar.close()
|
|
411
|
+
if spinner is not None:
|
|
412
|
+
spinner.stop()
|
|
390
413
|
if success:
|
|
391
414
|
spinner.text = to_colored_text(
|
|
392
415
|
"✔ Job succeeded. Obtaining results...", state="success"
|
|
@@ -451,87 +474,6 @@ class Sutro:
|
|
|
451
474
|
return None
|
|
452
475
|
return None
|
|
453
476
|
|
|
454
|
-
def register_stream_listener(self, job_id: str) -> str:
|
|
455
|
-
"""Register a new stream listener and get a session token."""
|
|
456
|
-
headers = {
|
|
457
|
-
"Authorization": f"Key {self.api_key}",
|
|
458
|
-
"Content-Type": "application/json",
|
|
459
|
-
}
|
|
460
|
-
with requests.post(
|
|
461
|
-
f"{self.base_url}/register-stream-listener/{job_id}",
|
|
462
|
-
headers=headers,
|
|
463
|
-
) as response:
|
|
464
|
-
response.raise_for_status()
|
|
465
|
-
data = response.json()
|
|
466
|
-
return data["request_session_token"]
|
|
467
|
-
|
|
468
|
-
# This is a best effort action and is ok if it sometimes doesn't complete etc
|
|
469
|
-
def unregister_stream_listener(self, job_id: str, session_token: str):
|
|
470
|
-
"""Explicitly unregister a stream listener."""
|
|
471
|
-
headers = {
|
|
472
|
-
"Authorization": f"Key {self.api_key}",
|
|
473
|
-
"Content-Type": "application/json",
|
|
474
|
-
}
|
|
475
|
-
with requests.post(
|
|
476
|
-
f"{self.base_url}/unregister-stream-listener/{job_id}",
|
|
477
|
-
headers=headers,
|
|
478
|
-
json={"request_session_token": session_token},
|
|
479
|
-
) as response:
|
|
480
|
-
response.raise_for_status()
|
|
481
|
-
|
|
482
|
-
def start_heartbeat(
|
|
483
|
-
self,
|
|
484
|
-
job_id: str,
|
|
485
|
-
session_token: str,
|
|
486
|
-
session: requests.Session,
|
|
487
|
-
stop_event: threading.Event
|
|
488
|
-
):
|
|
489
|
-
"""Send heartbeats until stopped."""
|
|
490
|
-
while not stop_event.is_set():
|
|
491
|
-
try:
|
|
492
|
-
headers = {
|
|
493
|
-
"Authorization": f"Key {self.api_key}",
|
|
494
|
-
"Content-Type": "application/json",
|
|
495
|
-
}
|
|
496
|
-
response = session.post(
|
|
497
|
-
f"{self.base_url}/stream-heartbeat/{job_id}",
|
|
498
|
-
headers=headers,
|
|
499
|
-
params={"request_session_token": session_token},
|
|
500
|
-
)
|
|
501
|
-
response.raise_for_status()
|
|
502
|
-
except Exception as e:
|
|
503
|
-
if not stop_event.is_set(): # Only log if we weren't stopping anyway
|
|
504
|
-
print(f"Heartbeat failed for job {job_id}: {e}")
|
|
505
|
-
|
|
506
|
-
for _ in range(self.HEARTBEAT_INTERVAL_SECONDS):
|
|
507
|
-
if stop_event.is_set():
|
|
508
|
-
break
|
|
509
|
-
time.sleep(1)
|
|
510
|
-
|
|
511
|
-
@contextmanager
|
|
512
|
-
def stream_heartbeat_session(self, job_id: str, session_token: str) -> Generator[requests.Session, None, None]:
|
|
513
|
-
"""Context manager that handles session registration and heartbeat."""
|
|
514
|
-
session = requests.Session()
|
|
515
|
-
stop_heartbeat = threading.Event()
|
|
516
|
-
|
|
517
|
-
# Run this concurrently in a thread so we can not block main SDK path/behavior
|
|
518
|
-
# but still run heartbeat requests
|
|
519
|
-
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
520
|
-
executor.submit(
|
|
521
|
-
self.start_heartbeat,
|
|
522
|
-
job_id,
|
|
523
|
-
session_token,
|
|
524
|
-
session,
|
|
525
|
-
stop_heartbeat
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
try:
|
|
529
|
-
yield session
|
|
530
|
-
finally:
|
|
531
|
-
# Signal stop and cleanup
|
|
532
|
-
stop_heartbeat.set()
|
|
533
|
-
self.unregister_stream_listener(job_id, session_token)
|
|
534
|
-
session.close()
|
|
535
477
|
|
|
536
478
|
def attach(self, job_id):
|
|
537
479
|
"""
|
|
@@ -596,11 +538,9 @@ class Sutro:
|
|
|
596
538
|
total_rows = job["num_rows"]
|
|
597
539
|
success = False
|
|
598
540
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
with self.stream_heartbeat_session(job_id, session_token) as s:
|
|
541
|
+
try:
|
|
602
542
|
with s.get(
|
|
603
|
-
f"{self.base_url}/stream-job-progress/{job_id}
|
|
543
|
+
f"{self.base_url}/stream-job-progress/{job_id}",
|
|
604
544
|
headers=headers,
|
|
605
545
|
stream=True,
|
|
606
546
|
) as streaming_response:
|
|
@@ -610,7 +550,8 @@ class Sutro:
|
|
|
610
550
|
text=to_colored_text("Awaiting status updates..."),
|
|
611
551
|
color=YASPIN_COLOR,
|
|
612
552
|
)
|
|
613
|
-
|
|
553
|
+
clickable_link = make_clickable_link(f'https://app.sutro.sh/jobs/{job_id}')
|
|
554
|
+
spinner.write(to_colored_text(f'Progress can also be monitored at: {clickable_link}'))
|
|
614
555
|
spinner.start()
|
|
615
556
|
for line in streaming_response.iter_lines():
|
|
616
557
|
if line:
|
|
@@ -649,6 +590,13 @@ class Sutro:
|
|
|
649
590
|
)
|
|
650
591
|
)
|
|
651
592
|
spinner.stop()
|
|
593
|
+
except KeyboardInterrupt:
|
|
594
|
+
pass
|
|
595
|
+
finally:
|
|
596
|
+
if pbar:
|
|
597
|
+
pbar.close()
|
|
598
|
+
if spinner:
|
|
599
|
+
spinner.stop()
|
|
652
600
|
|
|
653
601
|
|
|
654
602
|
|
|
@@ -1287,7 +1235,8 @@ class Sutro:
|
|
|
1287
1235
|
with yaspin(
|
|
1288
1236
|
SPINNER, text=to_colored_text("Awaiting job completion"), color=YASPIN_COLOR
|
|
1289
1237
|
) as spinner:
|
|
1290
|
-
|
|
1238
|
+
clickable_link = make_clickable_link(f'https://app.sutro.sh/jobs/{job_id}')
|
|
1239
|
+
spinner.write(to_colored_text(f'Progress can also be monitored at: {clickable_link}'))
|
|
1291
1240
|
while (time.time() - start_time) < timeout:
|
|
1292
1241
|
try:
|
|
1293
1242
|
status = self._fetch_job_status(job_id)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sutro/__init__.py,sha256=yUiVwcZ8QamSqDdRHgzoANyTZ-x3cPzlt2Fs5OllR_w,402
|
|
2
|
+
sutro/cli.py,sha256=6Qy9Vwaaho92HeO8YA_z1De4zp1dEFkSX3bEnLvdbkE,13203
|
|
3
|
+
sutro/sdk.py,sha256=rZcTDcyfMaBF7bVSVqb7oklNTE747yUI9C5Ry3sqZNI,50621
|
|
4
|
+
sutro-0.1.21.dist-info/METADATA,sha256=Lp1-Nvb0OWCsGs426r1bRHu358z4QgeY2c-cz3uhHn8,669
|
|
5
|
+
sutro-0.1.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
sutro-0.1.21.dist-info/entry_points.txt,sha256=eXvr4dvMV4UmZgR0zmrY8KOmNpo64cJkhNDywiadRFM,40
|
|
7
|
+
sutro-0.1.21.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
8
|
+
sutro-0.1.21.dist-info/RECORD,,
|
sutro-0.1.19.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
sutro/__init__.py,sha256=yUiVwcZ8QamSqDdRHgzoANyTZ-x3cPzlt2Fs5OllR_w,402
|
|
2
|
-
sutro/cli.py,sha256=6Qy9Vwaaho92HeO8YA_z1De4zp1dEFkSX3bEnLvdbkE,13203
|
|
3
|
-
sutro/sdk.py,sha256=1FLepL3M7afptWwudF310KWmPQ9ZDQ51LiT0Xoh1S_o,52705
|
|
4
|
-
sutro-0.1.19.dist-info/METADATA,sha256=6Irt5RX_DaIyRC6ig4XvHr0p0CKnFK0nmWA23oi1mCA,669
|
|
5
|
-
sutro-0.1.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
-
sutro-0.1.19.dist-info/entry_points.txt,sha256=eXvr4dvMV4UmZgR0zmrY8KOmNpo64cJkhNDywiadRFM,40
|
|
7
|
-
sutro-0.1.19.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
8
|
-
sutro-0.1.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|