sutro 0.1.19__py3-none-any.whl → 0.1.20__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 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,45 +285,51 @@ 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
- with yaspin(SPINNER, text=spinner_text, color=YASPIN_COLOR) as spinner:
290
- response = requests.post(
291
- endpoint, data=json.dumps(payload), headers=headers
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
- spinner.stop()
299
- print(to_colored_text(response.json(), state="fail"))
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"Awaiting cost estimates with job ID: {job_id}. You can safely detach and retrieve the cost estimates later.", state="info")
296
+ to_colored_text(f"Error: {response.status_code}", state="fail")
306
297
  )
307
298
  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
299
+ print(to_colored_text(response.json(), state="fail"))
300
+ return None
314
301
  else:
315
- spinner.write(
316
- to_colored_text(
317
- f"🛠 Priority {job_priority} Job created with ID: {job_id}.",
318
- state="success",
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
- if not stay_attached:
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"Use `so.get_job_status('{job_id}')` to check the status of the job."
325
- )
317
+ f"🛠 Priority {job_priority} Job created with ID: {job_id}.",
318
+ state="success",
326
319
  )
327
- return job_id
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:
@@ -335,20 +340,12 @@ class Sutro:
335
340
  failure_reason = self._get_failure_reason(job_id)
336
341
  spinner.write(to_colored_text(f"Failure reason: {failure_reason['message']}", "fail"))
337
342
  return None
338
-
339
343
  s = requests.Session()
340
- payload = {
341
- "job_id": job_id,
342
- }
343
344
  pbar = None
344
345
 
345
- # Register for stream and get session token
346
- session_token = self.register_stream_listener(job_id)
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}",
346
+ try:
347
+ with requests.get(
348
+ f"{self.base_url}/stream-job-progress/{job_id}",
352
349
  headers=headers,
353
350
  stream=True,
354
351
  ) as streaming_response:
@@ -359,6 +356,13 @@ class Sutro:
359
356
  color=YASPIN_COLOR,
360
357
  )
361
358
  spinner.start()
359
+
360
+ token_state = {
361
+ 'input_tokens': 0,
362
+ 'output_tokens': 0,
363
+ 'total_tokens_processed_per_second': 0
364
+ }
365
+
362
366
  for line in streaming_response.iter_lines():
363
367
  if line:
364
368
  try:
@@ -381,12 +385,30 @@ class Sutro:
381
385
  pbar.update(json_obj["result"] - pbar.n)
382
386
  pbar.refresh()
383
387
  if json_obj["result"] == len(input_data):
384
- pbar.close()
385
388
  success = True
386
389
  elif json_obj["update_type"] == "tokens":
390
+ # Update only the values that are present in this update
391
+ # Currently, the way the progress stream endpoint is defined,
392
+ # its possible to have updates come in that only have 1 or 2 fields
393
+ new = {
394
+ k: v for k, v in json_obj.get('result', {}).items()
395
+ if k in token_state and v >= token_state[k]
396
+ }
397
+ token_state.update(new)
398
+
387
399
  if pbar is not None:
388
- pbar.postfix = f"Input tokens processed: {json_obj['result']['input_tokens']}, Tokens generated: {json_obj['result']['output_tokens']}, Total tokens/s: {json_obj['result'].get('total_tokens_processed_per_second')}"
400
+ 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
401
  pbar.refresh()
402
+
403
+ except KeyboardInterrupt:
404
+ pass
405
+ finally:
406
+ # Need to clean these up on keyboard exit otherwise it causes
407
+ # an error
408
+ if pbar is not None:
409
+ pbar.close()
410
+ if spinner is not None:
411
+ spinner.stop()
390
412
  if success:
391
413
  spinner.text = to_colored_text(
392
414
  "✔ Job succeeded. Obtaining results...", state="success"
@@ -451,87 +473,6 @@ class Sutro:
451
473
  return None
452
474
  return None
453
475
 
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
476
 
536
477
  def attach(self, job_id):
537
478
  """
@@ -596,11 +537,9 @@ class Sutro:
596
537
  total_rows = job["num_rows"]
597
538
  success = False
598
539
 
599
- session_token = self.register_stream_listener(job_id)
600
-
601
- with self.stream_heartbeat_session(job_id, session_token) as s:
540
+ try:
602
541
  with s.get(
603
- f"{self.base_url}/stream-job-progress/{job_id}?request_session_token={session_token}",
542
+ f"{self.base_url}/stream-job-progress/{job_id}",
604
543
  headers=headers,
605
544
  stream=True,
606
545
  ) as streaming_response:
@@ -649,6 +588,13 @@ class Sutro:
649
588
  )
650
589
  )
651
590
  spinner.stop()
591
+ except KeyboardInterrupt:
592
+ pass
593
+ finally:
594
+ if pbar:
595
+ pbar.close()
596
+ if spinner:
597
+ spinner.stop()
652
598
 
653
599
 
654
600
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sutro
3
- Version: 0.1.19
3
+ Version: 0.1.20
4
4
  Summary: Sutro Python SDK
5
5
  Project-URL: Homepage, https://sutro.sh
6
6
  Project-URL: Documentation, https://docs.sutro.sh
@@ -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=ct-uGpVXeWq4oaFrXy0jtxQ5LmhuI5tNqo7pISxlPPo,50485
4
+ sutro-0.1.20.dist-info/METADATA,sha256=uZ3H5UCqRD7m5MCmDY7eAOGj8T-VzK5Zfa3zxgthCOM,669
5
+ sutro-0.1.20.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ sutro-0.1.20.dist-info/entry_points.txt,sha256=eXvr4dvMV4UmZgR0zmrY8KOmNpo64cJkhNDywiadRFM,40
7
+ sutro-0.1.20.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
8
+ sutro-0.1.20.dist-info/RECORD,,
@@ -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