sutro 0.1.30__tar.gz → 0.1.32__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.
Potentially problematic release.
This version of sutro might be problematic. Click here for more details.
- sutro-0.1.32/PKG-INFO +176 -0
- sutro-0.1.32/README.md +153 -0
- {sutro-0.1.30 → sutro-0.1.32}/pyproject.toml +6 -1
- {sutro-0.1.30 → sutro-0.1.32}/sutro/cli.py +8 -5
- {sutro-0.1.30 → sutro-0.1.32}/sutro/sdk.py +180 -94
- sutro-0.1.30/PKG-INFO +0 -24
- sutro-0.1.30/README.md +0 -3
- {sutro-0.1.30 → sutro-0.1.32}/.gitignore +0 -0
- {sutro-0.1.30 → sutro-0.1.32}/LICENSE +0 -0
- {sutro-0.1.30 → sutro-0.1.32}/sutro/__init__.py +0 -0
sutro-0.1.32/PKG-INFO
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sutro
|
|
3
|
+
Version: 0.1.32
|
|
4
|
+
Summary: Sutro Python SDK
|
|
5
|
+
Project-URL: Homepage, https://sutro.sh
|
|
6
|
+
Project-URL: Documentation, https://docs.sutro.sh
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: click==8.1.7
|
|
11
|
+
Requires-Dist: colorama==0.4.4
|
|
12
|
+
Requires-Dist: numpy==2.1.1
|
|
13
|
+
Requires-Dist: pandas==2.2.3
|
|
14
|
+
Requires-Dist: polars==1.8.2
|
|
15
|
+
Requires-Dist: pyarrow==21.0.0
|
|
16
|
+
Requires-Dist: pydantic==2.11.4
|
|
17
|
+
Requires-Dist: requests==2.32.3
|
|
18
|
+
Requires-Dist: tqdm==4.67.1
|
|
19
|
+
Requires-Dist: yaspin==3.1.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: ruff==0.13.1; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
Sutro makes it easy to analyze and generate unstructured data using LLMs, from quick experiments to billion token jobs.
|
|
29
|
+
|
|
30
|
+
Whether you're generating synthetic data, running model evals, structuring unstructured data, classifying data, or generating embeddings - *batch inference is faster, cheaper, and easier* with Sutro.
|
|
31
|
+
|
|
32
|
+
Visit [sutro.sh](https://sutro.sh) to learn more and request access to the cloud beta.
|
|
33
|
+
|
|
34
|
+
## 🚀 Quickstart
|
|
35
|
+
|
|
36
|
+
Install:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
[uv] pip install sutro
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Authenticate:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
sutro login
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Run your first job:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import sutro as so
|
|
52
|
+
import polars as pl
|
|
53
|
+
from pydantic import BaseModel
|
|
54
|
+
|
|
55
|
+
# Load your data
|
|
56
|
+
df = pl.DataFrame({
|
|
57
|
+
"review": [
|
|
58
|
+
"The battery life is terrible.",
|
|
59
|
+
"Great camera and build quality!",
|
|
60
|
+
"Too expensive for what it offers."
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# Add a system prompt (optional)
|
|
65
|
+
system_prompt = "Classify the sentiment of the review as positive, neutral, or negative."
|
|
66
|
+
|
|
67
|
+
# Define an output schema (optional)
|
|
68
|
+
class Sentiment(BaseModel):
|
|
69
|
+
sentiment: str
|
|
70
|
+
|
|
71
|
+
# Run a prototyping (p0) job
|
|
72
|
+
df = so.infer(
|
|
73
|
+
df,
|
|
74
|
+
column="review",
|
|
75
|
+
model="qwen-3-32b",
|
|
76
|
+
output_schema=Sentiment
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
print(df)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Will produce a result like:
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
|
|
86
|
+
### Scaling up:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# load a larger dataset
|
|
90
|
+
df = pl.read_parquet('hf://datasets/sutro/synthetic-product-reviews-20k/results.parquet')
|
|
91
|
+
|
|
92
|
+
# Run a production (p1) job
|
|
93
|
+
job_id = so.infer(
|
|
94
|
+
df,
|
|
95
|
+
column="review_text",
|
|
96
|
+
model="qwen-3-32b",
|
|
97
|
+
output_schema=Sentiment,
|
|
98
|
+
job_priority=1 # <-- one line of code for near-limitless scale
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
You can track live progress of your job, view results, and share with your team from the Sutro web app:
|
|
103
|
+
|
|
104
|
+

|
|
105
|
+
|
|
106
|
+
## What is Sutro?
|
|
107
|
+
|
|
108
|
+
Sutro is a **serverless, high-throughput batch inference service for LLM workloads**. With just a few lines of Python, you can quickly run batch inference jobs using open-source foundation models—at scale, with strong cost/time guarantees, and without worrying about infrastructure.
|
|
109
|
+
|
|
110
|
+
Think of Sutro as **online analytical processing (OLAP) for AI**: you submit queries over unstructured data (documents, emails, product reviews, etc.), and Sutro handles the heavy lifting of job execution - from intelligent batching to cloud orchestration to inference framework and hardware optimizations. You just bring your data, and Sutro handles the rest.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## 📚 Documentation & Examples
|
|
114
|
+
|
|
115
|
+
- [Documentation](https://docs.sutro.sh/)
|
|
116
|
+
- Example Guides:
|
|
117
|
+
- [Synthetic Data Zero to Hero](https://docs.sutro.sh/examples/synthetic-data-zero-to-hero)
|
|
118
|
+
- [Synthetic Data for Privacy Preservation](https://docs.sutro.sh/examples/synthetic-data-privacy)
|
|
119
|
+
- [Large Scale Embedding Generation with Qwen3 0.6B](https://docs.sutro.sh/examples/large-scale-embeddings)
|
|
120
|
+
- More coming soon...
|
|
121
|
+
|
|
122
|
+
## ✨ Features
|
|
123
|
+
|
|
124
|
+
- **⚡ Run experiments faster**
|
|
125
|
+
Small scale jobs complete in minutes, large scale jobs run within 1 hour - more than 20x faster than competing cloud services.
|
|
126
|
+
|
|
127
|
+
- **📈 Seamless scaling**
|
|
128
|
+
Use the same interface to run jobs with a few tokens, or billions at a time.
|
|
129
|
+
|
|
130
|
+
- **💰 Decreased Costs and Transparent Pricing**
|
|
131
|
+
Up to 10x cheaper than alternative inference services. Use dry run mode to estimate costs before running large jobs.
|
|
132
|
+
|
|
133
|
+
- **🐍 Pythonic DataFrame and file integrations**
|
|
134
|
+
Submit and receive results directly as Pandas/Polars DataFrames, or upload CSV/Parquet files.
|
|
135
|
+
|
|
136
|
+
- **🏗️ Zero infrastructure setup**
|
|
137
|
+
No need to manage GPUs, tune inference frameworks, or orchestrate parallelization. Just data in, results out.
|
|
138
|
+
|
|
139
|
+
- **📊 Real-time observability dashboard**
|
|
140
|
+
Use the Sutro web app to monitor your jobs in real-time and see results as they are generated, tag jobs for easier tracking, and share results with your team.
|
|
141
|
+
|
|
142
|
+
- **🔒 Built with security in mind**
|
|
143
|
+
Custom data retention options, and bring-your-own s3-compatible storage options available.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## 🧑💻 Typical Use Cases
|
|
147
|
+
|
|
148
|
+
- **Synthetic data generation**: Create millions of product reviews, conversations, or paraphrases for pre-training or distillation.
|
|
149
|
+
- **Model evals**: Easily run LLM benchmarks on a scheduled basis to detect model regressions or performance degradation.
|
|
150
|
+
- **Unstructured data analytics**: Run analytical workloads over unstructured data (e.g. customer reviews, product descriptions, emails, etc.).
|
|
151
|
+
- **Semantic tagging**: Add boolean/numeric/closed-set tags to messy data (e.g. LinkedIn bios, company descriptions).
|
|
152
|
+
- **Structured Extraction**: Pull structured fields out of unstructured documents at scale.
|
|
153
|
+
- **Classification**: Apply consistent labels across large datasets (spam, sentiment, topic, compliance risk).
|
|
154
|
+
- **Embedding generation**: Generate and store embeddings for downstream search/analytics.
|
|
155
|
+
|
|
156
|
+
## 🔌 Integrations
|
|
157
|
+
|
|
158
|
+
- **DataFrames**: Pandas, Polars
|
|
159
|
+
- **Files**: CSV, Parquet
|
|
160
|
+
- **Storage**: S3-Compatible Object Stores (e.g. R2, S3, GCS, etc.)
|
|
161
|
+
|
|
162
|
+
## 📦 Hosting Options
|
|
163
|
+
|
|
164
|
+
- **Cloud**: Run Sutro on our secure, multi-tenant cloud.
|
|
165
|
+
- **Isolated Deployments**: Bring your own storage, models, or cloud resources.
|
|
166
|
+
- **Local and Self-Hosted**: Coming soon!
|
|
167
|
+
|
|
168
|
+
See our [pricing page](https://sutro.sh/pricing) for more details.
|
|
169
|
+
|
|
170
|
+
## 🤝 Contributing
|
|
171
|
+
|
|
172
|
+
We welcome contributions! Please reach out to us at [team@sutro.sh](mailto:team@sutro.sh) to get involved.
|
|
173
|
+
|
|
174
|
+
## 📄 License
|
|
175
|
+
|
|
176
|
+
Apache 2.0
|
sutro-0.1.32/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Sutro makes it easy to analyze and generate unstructured data using LLMs, from quick experiments to billion token jobs.
|
|
6
|
+
|
|
7
|
+
Whether you're generating synthetic data, running model evals, structuring unstructured data, classifying data, or generating embeddings - *batch inference is faster, cheaper, and easier* with Sutro.
|
|
8
|
+
|
|
9
|
+
Visit [sutro.sh](https://sutro.sh) to learn more and request access to the cloud beta.
|
|
10
|
+
|
|
11
|
+
## 🚀 Quickstart
|
|
12
|
+
|
|
13
|
+
Install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
[uv] pip install sutro
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Authenticate:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
sutro login
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Run your first job:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import sutro as so
|
|
29
|
+
import polars as pl
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
|
|
32
|
+
# Load your data
|
|
33
|
+
df = pl.DataFrame({
|
|
34
|
+
"review": [
|
|
35
|
+
"The battery life is terrible.",
|
|
36
|
+
"Great camera and build quality!",
|
|
37
|
+
"Too expensive for what it offers."
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
# Add a system prompt (optional)
|
|
42
|
+
system_prompt = "Classify the sentiment of the review as positive, neutral, or negative."
|
|
43
|
+
|
|
44
|
+
# Define an output schema (optional)
|
|
45
|
+
class Sentiment(BaseModel):
|
|
46
|
+
sentiment: str
|
|
47
|
+
|
|
48
|
+
# Run a prototyping (p0) job
|
|
49
|
+
df = so.infer(
|
|
50
|
+
df,
|
|
51
|
+
column="review",
|
|
52
|
+
model="qwen-3-32b",
|
|
53
|
+
output_schema=Sentiment
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
print(df)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Will produce a result like:
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
### Scaling up:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# load a larger dataset
|
|
67
|
+
df = pl.read_parquet('hf://datasets/sutro/synthetic-product-reviews-20k/results.parquet')
|
|
68
|
+
|
|
69
|
+
# Run a production (p1) job
|
|
70
|
+
job_id = so.infer(
|
|
71
|
+
df,
|
|
72
|
+
column="review_text",
|
|
73
|
+
model="qwen-3-32b",
|
|
74
|
+
output_schema=Sentiment,
|
|
75
|
+
job_priority=1 # <-- one line of code for near-limitless scale
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can track live progress of your job, view results, and share with your team from the Sutro web app:
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
## What is Sutro?
|
|
84
|
+
|
|
85
|
+
Sutro is a **serverless, high-throughput batch inference service for LLM workloads**. With just a few lines of Python, you can quickly run batch inference jobs using open-source foundation models—at scale, with strong cost/time guarantees, and without worrying about infrastructure.
|
|
86
|
+
|
|
87
|
+
Think of Sutro as **online analytical processing (OLAP) for AI**: you submit queries over unstructured data (documents, emails, product reviews, etc.), and Sutro handles the heavy lifting of job execution - from intelligent batching to cloud orchestration to inference framework and hardware optimizations. You just bring your data, and Sutro handles the rest.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## 📚 Documentation & Examples
|
|
91
|
+
|
|
92
|
+
- [Documentation](https://docs.sutro.sh/)
|
|
93
|
+
- Example Guides:
|
|
94
|
+
- [Synthetic Data Zero to Hero](https://docs.sutro.sh/examples/synthetic-data-zero-to-hero)
|
|
95
|
+
- [Synthetic Data for Privacy Preservation](https://docs.sutro.sh/examples/synthetic-data-privacy)
|
|
96
|
+
- [Large Scale Embedding Generation with Qwen3 0.6B](https://docs.sutro.sh/examples/large-scale-embeddings)
|
|
97
|
+
- More coming soon...
|
|
98
|
+
|
|
99
|
+
## ✨ Features
|
|
100
|
+
|
|
101
|
+
- **⚡ Run experiments faster**
|
|
102
|
+
Small scale jobs complete in minutes, large scale jobs run within 1 hour - more than 20x faster than competing cloud services.
|
|
103
|
+
|
|
104
|
+
- **📈 Seamless scaling**
|
|
105
|
+
Use the same interface to run jobs with a few tokens, or billions at a time.
|
|
106
|
+
|
|
107
|
+
- **💰 Decreased Costs and Transparent Pricing**
|
|
108
|
+
Up to 10x cheaper than alternative inference services. Use dry run mode to estimate costs before running large jobs.
|
|
109
|
+
|
|
110
|
+
- **🐍 Pythonic DataFrame and file integrations**
|
|
111
|
+
Submit and receive results directly as Pandas/Polars DataFrames, or upload CSV/Parquet files.
|
|
112
|
+
|
|
113
|
+
- **🏗️ Zero infrastructure setup**
|
|
114
|
+
No need to manage GPUs, tune inference frameworks, or orchestrate parallelization. Just data in, results out.
|
|
115
|
+
|
|
116
|
+
- **📊 Real-time observability dashboard**
|
|
117
|
+
Use the Sutro web app to monitor your jobs in real-time and see results as they are generated, tag jobs for easier tracking, and share results with your team.
|
|
118
|
+
|
|
119
|
+
- **🔒 Built with security in mind**
|
|
120
|
+
Custom data retention options, and bring-your-own s3-compatible storage options available.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## 🧑💻 Typical Use Cases
|
|
124
|
+
|
|
125
|
+
- **Synthetic data generation**: Create millions of product reviews, conversations, or paraphrases for pre-training or distillation.
|
|
126
|
+
- **Model evals**: Easily run LLM benchmarks on a scheduled basis to detect model regressions or performance degradation.
|
|
127
|
+
- **Unstructured data analytics**: Run analytical workloads over unstructured data (e.g. customer reviews, product descriptions, emails, etc.).
|
|
128
|
+
- **Semantic tagging**: Add boolean/numeric/closed-set tags to messy data (e.g. LinkedIn bios, company descriptions).
|
|
129
|
+
- **Structured Extraction**: Pull structured fields out of unstructured documents at scale.
|
|
130
|
+
- **Classification**: Apply consistent labels across large datasets (spam, sentiment, topic, compliance risk).
|
|
131
|
+
- **Embedding generation**: Generate and store embeddings for downstream search/analytics.
|
|
132
|
+
|
|
133
|
+
## 🔌 Integrations
|
|
134
|
+
|
|
135
|
+
- **DataFrames**: Pandas, Polars
|
|
136
|
+
- **Files**: CSV, Parquet
|
|
137
|
+
- **Storage**: S3-Compatible Object Stores (e.g. R2, S3, GCS, etc.)
|
|
138
|
+
|
|
139
|
+
## 📦 Hosting Options
|
|
140
|
+
|
|
141
|
+
- **Cloud**: Run Sutro on our secure, multi-tenant cloud.
|
|
142
|
+
- **Isolated Deployments**: Bring your own storage, models, or cloud resources.
|
|
143
|
+
- **Local and Self-Hosted**: Coming soon!
|
|
144
|
+
|
|
145
|
+
See our [pricing page](https://sutro.sh/pricing) for more details.
|
|
146
|
+
|
|
147
|
+
## 🤝 Contributing
|
|
148
|
+
|
|
149
|
+
We welcome contributions! Please reach out to us at [team@sutro.sh](mailto:team@sutro.sh) to get involved.
|
|
150
|
+
|
|
151
|
+
## 📄 License
|
|
152
|
+
|
|
153
|
+
Apache 2.0
|
|
@@ -9,7 +9,7 @@ installer = "uv"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "sutro"
|
|
12
|
-
version = "0.1.
|
|
12
|
+
version = "0.1.32"
|
|
13
13
|
description = "Sutro Python SDK"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -27,6 +27,11 @@ dependencies = [
|
|
|
27
27
|
"pyarrow==21.0.0",
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"ruff==0.13.1"
|
|
33
|
+
]
|
|
34
|
+
|
|
30
35
|
[project.scripts]
|
|
31
36
|
sutro = "sutro.cli:cli"
|
|
32
37
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import
|
|
1
|
+
from datetime import timezone
|
|
2
2
|
import click
|
|
3
3
|
from colorama import Fore, Style
|
|
4
4
|
import os
|
|
@@ -35,9 +35,7 @@ def check_auth():
|
|
|
35
35
|
def get_sdk():
|
|
36
36
|
config = load_config()
|
|
37
37
|
if config.get("base_url") != None:
|
|
38
|
-
return Sutro(
|
|
39
|
-
api_key=config.get("api_key"), base_url=config.get("base_url")
|
|
40
|
-
)
|
|
38
|
+
return Sutro(api_key=config.get("api_key"), base_url=config.get("base_url"))
|
|
41
39
|
else:
|
|
42
40
|
return Sutro(api_key=config.get("api_key"))
|
|
43
41
|
|
|
@@ -141,6 +139,7 @@ def jobs():
|
|
|
141
139
|
"""Manage jobs."""
|
|
142
140
|
pass
|
|
143
141
|
|
|
142
|
+
|
|
144
143
|
@jobs.command()
|
|
145
144
|
@click.option(
|
|
146
145
|
"--all", is_flag=True, help="Include all jobs, including cancelled and failed ones."
|
|
@@ -247,7 +246,8 @@ def results(
|
|
|
247
246
|
job_results = sdk.get_job_results(
|
|
248
247
|
job_id, include_inputs, include_cumulative_logprobs
|
|
249
248
|
)
|
|
250
|
-
if
|
|
249
|
+
if job_results is None or len(job_results) == 0:
|
|
250
|
+
print(Fore.YELLOW + "No results found for job " + job_id + "." + Style.RESET_ALL)
|
|
251
251
|
return
|
|
252
252
|
|
|
253
253
|
df = pl.DataFrame(job_results)
|
|
@@ -359,11 +359,13 @@ def download(dataset_id, file_name=None, output_path=None):
|
|
|
359
359
|
with open(output_path + "/" + file_name, "wb") as f:
|
|
360
360
|
f.write(file)
|
|
361
361
|
|
|
362
|
+
|
|
362
363
|
@cli.group()
|
|
363
364
|
def cache():
|
|
364
365
|
"""Manage the local job results cache."""
|
|
365
366
|
pass
|
|
366
367
|
|
|
368
|
+
|
|
367
369
|
@cache.command()
|
|
368
370
|
def clear():
|
|
369
371
|
"""Clear the local job results cache."""
|
|
@@ -371,6 +373,7 @@ def clear():
|
|
|
371
373
|
sdk._clear_job_results_cache()
|
|
372
374
|
click.echo(Fore.GREEN + "Job results cache cleared." + Style.RESET_ALL)
|
|
373
375
|
|
|
376
|
+
|
|
374
377
|
@cache.command()
|
|
375
378
|
def show():
|
|
376
379
|
"""Show the contents and size of the job results cache."""
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
-
from contextlib import contextmanager
|
|
4
1
|
from enum import Enum
|
|
5
|
-
|
|
6
2
|
import requests
|
|
7
3
|
import pandas as pd
|
|
8
4
|
import polars as pl
|
|
9
5
|
import json
|
|
10
|
-
from typing import Union, List, Optional, Literal,
|
|
6
|
+
from typing import Union, List, Optional, Literal, Dict, Any
|
|
11
7
|
import os
|
|
12
8
|
import sys
|
|
13
9
|
from yaspin import yaspin
|
|
14
10
|
from yaspin.spinners import Spinners
|
|
15
|
-
from colorama import init, Fore,
|
|
11
|
+
from colorama import init, Fore, Style
|
|
16
12
|
from tqdm import tqdm
|
|
17
13
|
import time
|
|
18
14
|
from pydantic import BaseModel
|
|
19
|
-
import json
|
|
20
15
|
import pyarrow.parquet as pq
|
|
21
16
|
import shutil
|
|
22
17
|
|
|
@@ -45,6 +40,7 @@ class JobStatus(str, Enum):
|
|
|
45
40
|
def is_terminal(self) -> bool:
|
|
46
41
|
return self in self.terminal_statuses()
|
|
47
42
|
|
|
43
|
+
|
|
48
44
|
# Initialize colorama (required for Windows)
|
|
49
45
|
init()
|
|
50
46
|
|
|
@@ -71,8 +67,13 @@ ModelOptions = Literal[
|
|
|
71
67
|
"qwen-3-32b-thinking",
|
|
72
68
|
"gemma-3-4b-it",
|
|
73
69
|
"gemma-3-27b-it",
|
|
74
|
-
"
|
|
75
|
-
"
|
|
70
|
+
"gpt-oss-120b",
|
|
71
|
+
"gpt-oss-20b",
|
|
72
|
+
"qwen-3-235b-a22b-thinking",
|
|
73
|
+
"qwen-3-30b-a3b-thinking",
|
|
74
|
+
"qwen-3-embedding-0.6b",
|
|
75
|
+
"qwen-3-embedding-6b",
|
|
76
|
+
"qwen-3-embedding-8b",
|
|
76
77
|
]
|
|
77
78
|
|
|
78
79
|
|
|
@@ -99,6 +100,7 @@ def to_colored_text(
|
|
|
99
100
|
# Default to blue for normal/processing states
|
|
100
101
|
return f"{Fore.BLUE}{text}{Style.RESET_ALL}"
|
|
101
102
|
|
|
103
|
+
|
|
102
104
|
# Isn't fully support in all terminals unfortunately. We should switch to Rich
|
|
103
105
|
# at some point, but even Rich links aren't clickable on MacOS Terminal
|
|
104
106
|
def make_clickable_link(url, text=None):
|
|
@@ -110,10 +112,9 @@ def make_clickable_link(url, text=None):
|
|
|
110
112
|
text = url
|
|
111
113
|
return f"\033]8;;{url}\033\\{text}\033]8;;\033\\"
|
|
112
114
|
|
|
115
|
+
|
|
113
116
|
class Sutro:
|
|
114
|
-
def __init__(
|
|
115
|
-
self, api_key: str = None, base_url: str = "https://api.sutro.sh/"
|
|
116
|
-
):
|
|
117
|
+
def __init__(self, api_key: str = None, base_url: str = "https://api.sutro.sh/"):
|
|
117
118
|
self.api_key = api_key or self.check_for_api_key()
|
|
118
119
|
self.base_url = base_url
|
|
119
120
|
|
|
@@ -220,7 +221,7 @@ class Sutro:
|
|
|
220
221
|
cost_estimate: bool,
|
|
221
222
|
stay_attached: Optional[bool],
|
|
222
223
|
random_seed_per_input: bool,
|
|
223
|
-
truncate_rows: bool
|
|
224
|
+
truncate_rows: bool,
|
|
224
225
|
):
|
|
225
226
|
input_data = self.handle_data_helper(data, column)
|
|
226
227
|
endpoint = f"{self.base_url}/batch-inference"
|
|
@@ -237,7 +238,7 @@ class Sutro:
|
|
|
237
238
|
"cost_estimate": cost_estimate,
|
|
238
239
|
"sampling_params": sampling_params,
|
|
239
240
|
"random_seed_per_input": random_seed_per_input,
|
|
240
|
-
"truncate_rows": truncate_rows
|
|
241
|
+
"truncate_rows": truncate_rows,
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
# There are two gotchas with yaspin:
|
|
@@ -266,13 +267,20 @@ class Sutro:
|
|
|
266
267
|
job_id = response_data["results"]
|
|
267
268
|
if cost_estimate:
|
|
268
269
|
spinner.write(
|
|
269
|
-
to_colored_text(
|
|
270
|
+
to_colored_text(
|
|
271
|
+
f"Awaiting cost estimates with job ID: {job_id}. You can safely detach and retrieve the cost estimates later."
|
|
272
|
+
)
|
|
270
273
|
)
|
|
271
274
|
spinner.stop()
|
|
272
|
-
self.await_job_completion(
|
|
275
|
+
self.await_job_completion(
|
|
276
|
+
job_id, obtain_results=False, is_cost_estimate=True
|
|
277
|
+
)
|
|
273
278
|
cost_estimate = self._get_job_cost_estimate(job_id)
|
|
274
279
|
spinner.write(
|
|
275
|
-
to_colored_text(
|
|
280
|
+
to_colored_text(
|
|
281
|
+
f"✔ Cost estimates retrieved for job {job_id}: ${cost_estimate}",
|
|
282
|
+
state="success",
|
|
283
|
+
)
|
|
276
284
|
)
|
|
277
285
|
return job_id
|
|
278
286
|
else:
|
|
@@ -283,12 +291,14 @@ class Sutro:
|
|
|
283
291
|
)
|
|
284
292
|
)
|
|
285
293
|
if not stay_attached:
|
|
286
|
-
clickable_link = make_clickable_link(
|
|
294
|
+
clickable_link = make_clickable_link(
|
|
295
|
+
f"https://app.sutro.sh/jobs/{job_id}"
|
|
296
|
+
)
|
|
287
297
|
spinner.write(
|
|
288
298
|
to_colored_text(
|
|
289
299
|
f"Use `so.get_job_status('{job_id}')` to check the status of the job, or monitor progress at {clickable_link}"
|
|
290
|
-
)
|
|
291
300
|
)
|
|
301
|
+
)
|
|
292
302
|
return job_id
|
|
293
303
|
except KeyboardInterrupt:
|
|
294
304
|
pass
|
|
@@ -298,22 +308,32 @@ class Sutro:
|
|
|
298
308
|
|
|
299
309
|
success = False
|
|
300
310
|
if stay_attached and job_id is not None:
|
|
301
|
-
spinner.write(
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
spinner.write(
|
|
312
|
+
to_colored_text(
|
|
313
|
+
"Awaiting job start...",
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
clickable_link = make_clickable_link(f"https://app.sutro.sh/jobs/{job_id}")
|
|
317
|
+
spinner.write(
|
|
318
|
+
to_colored_text(f"Progress can also be monitored at: {clickable_link}")
|
|
319
|
+
)
|
|
304
320
|
started = self._await_job_start(job_id)
|
|
305
321
|
if not started:
|
|
306
322
|
failure_reason = self._get_failure_reason(job_id)
|
|
307
|
-
spinner.write(
|
|
323
|
+
spinner.write(
|
|
324
|
+
to_colored_text(
|
|
325
|
+
f"Failure reason: {failure_reason['message']}", "fail"
|
|
326
|
+
)
|
|
327
|
+
)
|
|
308
328
|
return None
|
|
309
329
|
s = requests.Session()
|
|
310
330
|
pbar = None
|
|
311
331
|
|
|
312
332
|
try:
|
|
313
333
|
with requests.get(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
334
|
+
f"{self.base_url}/stream-job-progress/{job_id}",
|
|
335
|
+
headers=headers,
|
|
336
|
+
stream=True,
|
|
317
337
|
) as streaming_response:
|
|
318
338
|
streaming_response.raise_for_status()
|
|
319
339
|
spinner = yaspin(
|
|
@@ -324,9 +344,9 @@ class Sutro:
|
|
|
324
344
|
spinner.start()
|
|
325
345
|
|
|
326
346
|
token_state = {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
347
|
+
"input_tokens": 0,
|
|
348
|
+
"output_tokens": 0,
|
|
349
|
+
"total_tokens_processed_per_second": 0,
|
|
330
350
|
}
|
|
331
351
|
|
|
332
352
|
for line in streaming_response.iter_lines():
|
|
@@ -340,7 +360,7 @@ class Sutro:
|
|
|
340
360
|
if json_obj["update_type"] == "progress":
|
|
341
361
|
if pbar is None:
|
|
342
362
|
spinner.stop()
|
|
343
|
-
postfix =
|
|
363
|
+
postfix = "Input tokens processed: 0"
|
|
344
364
|
pbar = self.fancy_tqdm(
|
|
345
365
|
total=len(input_data),
|
|
346
366
|
desc="Progress",
|
|
@@ -357,7 +377,8 @@ class Sutro:
|
|
|
357
377
|
# Currently, the way the progress stream endpoint is defined,
|
|
358
378
|
# its possible to have updates come in that only have 1 or 2 fields
|
|
359
379
|
new = {
|
|
360
|
-
k: v
|
|
380
|
+
k: v
|
|
381
|
+
for k, v in json_obj.get("result", {}).items()
|
|
361
382
|
if k in token_state and v >= token_state[k]
|
|
362
383
|
}
|
|
363
384
|
token_state.update(new)
|
|
@@ -388,8 +409,8 @@ class Sutro:
|
|
|
388
409
|
# TODO: we implment retries in cases where the job hasn't written results yet
|
|
389
410
|
# it would be better if we could receive a fully succeeded status from the job
|
|
390
411
|
# and not have such a race condition
|
|
391
|
-
max_retries = 20
|
|
392
|
-
retry_delay = 5
|
|
412
|
+
max_retries = 20 # winds up being 100 seconds cumulative delay
|
|
413
|
+
retry_delay = 5 # initial delay in seconds
|
|
393
414
|
|
|
394
415
|
for _ in range(max_retries):
|
|
395
416
|
time.sleep(retry_delay)
|
|
@@ -446,7 +467,7 @@ class Sutro:
|
|
|
446
467
|
dry_run: bool = False,
|
|
447
468
|
stay_attached: Optional[bool] = None,
|
|
448
469
|
random_seed_per_input: bool = False,
|
|
449
|
-
truncate_rows: bool = False
|
|
470
|
+
truncate_rows: bool = False,
|
|
450
471
|
):
|
|
451
472
|
"""
|
|
452
473
|
Run inference on the provided data.
|
|
@@ -475,22 +496,29 @@ class Sutro:
|
|
|
475
496
|
"""
|
|
476
497
|
if isinstance(model, list) == False:
|
|
477
498
|
model_list = [model]
|
|
478
|
-
stay_attached =
|
|
499
|
+
stay_attached = (
|
|
500
|
+
stay_attached if stay_attached is not None else job_priority == 0
|
|
501
|
+
)
|
|
479
502
|
else:
|
|
480
503
|
model_list = model
|
|
481
504
|
stay_attached = False
|
|
482
505
|
|
|
483
506
|
# Convert BaseModel to dict if needed
|
|
484
507
|
if output_schema is not None:
|
|
485
|
-
if hasattr(
|
|
508
|
+
if hasattr(
|
|
509
|
+
output_schema, "model_json_schema"
|
|
510
|
+
): # Check for pydantic Model interface
|
|
486
511
|
json_schema = output_schema.model_json_schema()
|
|
487
512
|
elif isinstance(output_schema, dict):
|
|
488
513
|
json_schema = output_schema
|
|
489
514
|
else:
|
|
490
|
-
raise ValueError(
|
|
515
|
+
raise ValueError(
|
|
516
|
+
"Invalid output schema type. Must be a dictionary or a pydantic Model."
|
|
517
|
+
)
|
|
491
518
|
else:
|
|
492
519
|
json_schema = None
|
|
493
520
|
|
|
521
|
+
results = []
|
|
494
522
|
for model in model_list:
|
|
495
523
|
res = self._run_one_batch_inference(
|
|
496
524
|
data,
|
|
@@ -504,11 +532,16 @@ class Sutro:
|
|
|
504
532
|
dry_run,
|
|
505
533
|
stay_attached,
|
|
506
534
|
random_seed_per_input,
|
|
507
|
-
truncate_rows
|
|
535
|
+
truncate_rows,
|
|
508
536
|
)
|
|
509
|
-
|
|
510
|
-
|
|
537
|
+
results.append(res)
|
|
538
|
+
|
|
539
|
+
if len(results) > 1:
|
|
540
|
+
return results
|
|
541
|
+
elif len(results) == 1:
|
|
542
|
+
return results[0]
|
|
511
543
|
|
|
544
|
+
return None
|
|
512
545
|
|
|
513
546
|
def attach(self, job_id):
|
|
514
547
|
"""
|
|
@@ -530,9 +563,9 @@ class Sutro:
|
|
|
530
563
|
}
|
|
531
564
|
|
|
532
565
|
with yaspin(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
566
|
+
SPINNER,
|
|
567
|
+
text=to_colored_text("Looking for job..."),
|
|
568
|
+
color=YASPIN_COLOR,
|
|
536
569
|
) as spinner:
|
|
537
570
|
# Fetch the specific job we want to attach to
|
|
538
571
|
job = self._fetch_job(job_id)
|
|
@@ -550,10 +583,14 @@ class Sutro:
|
|
|
550
583
|
)
|
|
551
584
|
return
|
|
552
585
|
case "FAILED":
|
|
553
|
-
spinner.write(
|
|
586
|
+
spinner.write(
|
|
587
|
+
to_colored_text("❌ Job is in failed state.", state="fail")
|
|
588
|
+
)
|
|
554
589
|
return
|
|
555
590
|
case "CANCELLED":
|
|
556
|
-
spinner.write(
|
|
591
|
+
spinner.write(
|
|
592
|
+
to_colored_text("❌ Job was cancelled.", state="fail")
|
|
593
|
+
)
|
|
557
594
|
return
|
|
558
595
|
case _:
|
|
559
596
|
spinner.write(to_colored_text("✔ Job found!", state="success"))
|
|
@@ -563,9 +600,9 @@ class Sutro:
|
|
|
563
600
|
|
|
564
601
|
try:
|
|
565
602
|
with s.get(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
603
|
+
f"{self.base_url}/stream-job-progress/{job_id}",
|
|
604
|
+
headers=headers,
|
|
605
|
+
stream=True,
|
|
569
606
|
) as streaming_response:
|
|
570
607
|
streaming_response.raise_for_status()
|
|
571
608
|
spinner = yaspin(
|
|
@@ -573,8 +610,14 @@ class Sutro:
|
|
|
573
610
|
text=to_colored_text("Awaiting status updates..."),
|
|
574
611
|
color=YASPIN_COLOR,
|
|
575
612
|
)
|
|
576
|
-
clickable_link = make_clickable_link(
|
|
577
|
-
|
|
613
|
+
clickable_link = make_clickable_link(
|
|
614
|
+
f"https://app.sutro.sh/jobs/{job_id}"
|
|
615
|
+
)
|
|
616
|
+
spinner.write(
|
|
617
|
+
to_colored_text(
|
|
618
|
+
f"Progress can also be monitored at: {clickable_link}"
|
|
619
|
+
)
|
|
620
|
+
)
|
|
578
621
|
spinner.start()
|
|
579
622
|
for line in streaming_response.iter_lines():
|
|
580
623
|
if line:
|
|
@@ -587,7 +630,7 @@ class Sutro:
|
|
|
587
630
|
if json_obj["update_type"] == "progress":
|
|
588
631
|
if pbar is None:
|
|
589
632
|
spinner.stop()
|
|
590
|
-
postfix =
|
|
633
|
+
postfix = "Input tokens processed: 0"
|
|
591
634
|
pbar = self.fancy_tqdm(
|
|
592
635
|
total=total_rows,
|
|
593
636
|
desc="Progress",
|
|
@@ -621,8 +664,6 @@ class Sutro:
|
|
|
621
664
|
if spinner:
|
|
622
665
|
spinner.stop()
|
|
623
666
|
|
|
624
|
-
|
|
625
|
-
|
|
626
667
|
def fancy_tqdm(
|
|
627
668
|
self,
|
|
628
669
|
total: int,
|
|
@@ -738,7 +779,7 @@ class Sutro:
|
|
|
738
779
|
response = requests.get(endpoint, headers=headers)
|
|
739
780
|
if response.status_code != 200:
|
|
740
781
|
return None
|
|
741
|
-
return response.json().get(
|
|
782
|
+
return response.json().get("job")
|
|
742
783
|
|
|
743
784
|
def _get_job_cost_estimate(self, job_id: str):
|
|
744
785
|
"""
|
|
@@ -748,8 +789,8 @@ class Sutro:
|
|
|
748
789
|
if not job:
|
|
749
790
|
return None
|
|
750
791
|
|
|
751
|
-
return job.get(
|
|
752
|
-
|
|
792
|
+
return job.get("cost_estimate")
|
|
793
|
+
|
|
753
794
|
def _get_failure_reason(self, job_id: str):
|
|
754
795
|
"""
|
|
755
796
|
Get the failure reason for a job.
|
|
@@ -757,7 +798,7 @@ class Sutro:
|
|
|
757
798
|
job = self._fetch_job(job_id)
|
|
758
799
|
if not job:
|
|
759
800
|
return None
|
|
760
|
-
return job.get(
|
|
801
|
+
return job.get("failure_reason")
|
|
761
802
|
|
|
762
803
|
def _fetch_job_status(self, job_id: str):
|
|
763
804
|
"""
|
|
@@ -796,14 +837,16 @@ class Sutro:
|
|
|
796
837
|
str: The status of the job.
|
|
797
838
|
"""
|
|
798
839
|
with yaspin(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
840
|
+
SPINNER,
|
|
841
|
+
text=to_colored_text(f"Checking job status with ID: {job_id}"),
|
|
842
|
+
color=YASPIN_COLOR,
|
|
802
843
|
) as spinner:
|
|
803
844
|
try:
|
|
804
845
|
response_data = self._fetch_job_status(job_id)
|
|
805
|
-
spinner.write(
|
|
806
|
-
|
|
846
|
+
spinner.write(
|
|
847
|
+
to_colored_text("✔ Job status retrieved!", state="success")
|
|
848
|
+
)
|
|
849
|
+
return response_data
|
|
807
850
|
except requests.HTTPError as e:
|
|
808
851
|
spinner.write(
|
|
809
852
|
to_colored_text(
|
|
@@ -842,14 +885,13 @@ class Sutro:
|
|
|
842
885
|
Union[pl.DataFrame, pd.DataFrame]: The results as a DataFrame. By default, returns polars.DataFrame; when with_original_df is an instance of pandas.DataFrame, returns pandas.DataFrame.
|
|
843
886
|
"""
|
|
844
887
|
|
|
845
|
-
|
|
846
888
|
file_path = os.path.expanduser(f"~/.sutro/job-results/{job_id}.snappy.parquet")
|
|
847
889
|
expected_num_columns = 1 + include_inputs + include_cumulative_logprobs
|
|
848
890
|
contains_expected_columns = False
|
|
849
891
|
if os.path.exists(file_path):
|
|
850
892
|
num_columns = pq.read_table(file_path).num_columns
|
|
851
893
|
contains_expected_columns = num_columns == expected_num_columns
|
|
852
|
-
|
|
894
|
+
|
|
853
895
|
if disable_cache == False and contains_expected_columns:
|
|
854
896
|
with yaspin(
|
|
855
897
|
SPINNER,
|
|
@@ -857,7 +899,9 @@ class Sutro:
|
|
|
857
899
|
color=YASPIN_COLOR,
|
|
858
900
|
) as spinner:
|
|
859
901
|
results_df = pl.read_parquet(file_path)
|
|
860
|
-
spinner.write(
|
|
902
|
+
spinner.write(
|
|
903
|
+
to_colored_text("✔ Results loaded from cache", state="success")
|
|
904
|
+
)
|
|
861
905
|
else:
|
|
862
906
|
endpoint = f"{self.base_url}/job-results"
|
|
863
907
|
payload = {
|
|
@@ -894,38 +938,51 @@ class Sutro:
|
|
|
894
938
|
response_data = response.json()
|
|
895
939
|
results_df = pl.DataFrame(response_data["results"])
|
|
896
940
|
|
|
897
|
-
results_df = results_df.rename({
|
|
941
|
+
results_df = results_df.rename({"outputs": output_column})
|
|
898
942
|
|
|
899
943
|
if disable_cache == False:
|
|
900
944
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
901
945
|
results_df.write_parquet(file_path, compression="snappy")
|
|
902
|
-
spinner.write(
|
|
903
|
-
|
|
946
|
+
spinner.write(
|
|
947
|
+
to_colored_text("✔ Results saved to cache", state="success")
|
|
948
|
+
)
|
|
949
|
+
|
|
904
950
|
# Ordering inputs col first seems most logical/useful
|
|
905
951
|
column_config = [
|
|
906
|
-
(
|
|
952
|
+
("inputs", include_inputs),
|
|
907
953
|
(output_column, True),
|
|
908
|
-
(
|
|
954
|
+
("cumulative_logprobs", include_cumulative_logprobs),
|
|
909
955
|
]
|
|
910
956
|
|
|
911
|
-
columns_to_keep = [
|
|
912
|
-
|
|
957
|
+
columns_to_keep = [
|
|
958
|
+
col
|
|
959
|
+
for col, include in column_config
|
|
960
|
+
if include and col in results_df.columns
|
|
961
|
+
]
|
|
913
962
|
|
|
914
963
|
results_df = results_df.select(columns_to_keep)
|
|
915
964
|
|
|
916
965
|
if unpack_json:
|
|
917
966
|
try:
|
|
918
|
-
first_row = json.loads(
|
|
967
|
+
first_row = json.loads(
|
|
968
|
+
results_df.head(1)[output_column][0]
|
|
969
|
+
) # checks if the first row can be json decoded
|
|
919
970
|
results_df = results_df.with_columns(
|
|
920
|
-
pl.col(output_column)
|
|
971
|
+
pl.col(output_column)
|
|
972
|
+
.str.json_decode()
|
|
973
|
+
.alias("output_column_json_decoded")
|
|
921
974
|
)
|
|
922
975
|
json_decoded_fields = first_row.keys()
|
|
923
976
|
for field in json_decoded_fields:
|
|
924
977
|
results_df = results_df.with_columns(
|
|
925
|
-
pl.col("output_column_json_decoded")
|
|
978
|
+
pl.col("output_column_json_decoded")
|
|
979
|
+
.struct.field(field)
|
|
980
|
+
.alias(field)
|
|
926
981
|
)
|
|
927
982
|
# drop the output_column and the json decoded column
|
|
928
|
-
results_df = results_df.drop(
|
|
983
|
+
results_df = results_df.drop(
|
|
984
|
+
[output_column, "output_column_json_decoded"]
|
|
985
|
+
)
|
|
929
986
|
except json.JSONDecodeError:
|
|
930
987
|
# if the first row cannot be json decoded, do nothing
|
|
931
988
|
pass
|
|
@@ -1011,7 +1068,9 @@ class Sutro:
|
|
|
1011
1068
|
return
|
|
1012
1069
|
dataset_id = response.json()["dataset_id"]
|
|
1013
1070
|
spinner.write(
|
|
1014
|
-
to_colored_text(
|
|
1071
|
+
to_colored_text(
|
|
1072
|
+
f"✔ Dataset created with ID: {dataset_id}", state="success"
|
|
1073
|
+
)
|
|
1015
1074
|
)
|
|
1016
1075
|
return dataset_id
|
|
1017
1076
|
|
|
@@ -1079,8 +1138,7 @@ class Sutro:
|
|
|
1079
1138
|
"dataset_id": dataset_id,
|
|
1080
1139
|
}
|
|
1081
1140
|
|
|
1082
|
-
headers = {
|
|
1083
|
-
"Authorization": f"Key {self.api_key}"}
|
|
1141
|
+
headers = {"Authorization": f"Key {self.api_key}"}
|
|
1084
1142
|
|
|
1085
1143
|
count += 1
|
|
1086
1144
|
spinner.write(
|
|
@@ -1164,7 +1222,9 @@ class Sutro:
|
|
|
1164
1222
|
print(to_colored_text(f"Error: {response.json()}", state="fail"))
|
|
1165
1223
|
return
|
|
1166
1224
|
spinner.write(
|
|
1167
|
-
to_colored_text(
|
|
1225
|
+
to_colored_text(
|
|
1226
|
+
f"✔ Files listed in dataset: {dataset_id}", state="success"
|
|
1227
|
+
)
|
|
1168
1228
|
)
|
|
1169
1229
|
return response.json()["files"]
|
|
1170
1230
|
|
|
@@ -1286,7 +1346,13 @@ class Sutro:
|
|
|
1286
1346
|
return
|
|
1287
1347
|
return response.json()["quotas"]
|
|
1288
1348
|
|
|
1289
|
-
def await_job_completion(
|
|
1349
|
+
def await_job_completion(
|
|
1350
|
+
self,
|
|
1351
|
+
job_id: str,
|
|
1352
|
+
timeout: Optional[int] = 7200,
|
|
1353
|
+
obtain_results: bool = True,
|
|
1354
|
+
is_cost_estimate: bool = False,
|
|
1355
|
+
) -> list | None:
|
|
1290
1356
|
"""
|
|
1291
1357
|
Waits for job completion to occur and then returns the results upon
|
|
1292
1358
|
a successful completion.
|
|
@@ -1308,8 +1374,14 @@ class Sutro:
|
|
|
1308
1374
|
SPINNER, text=to_colored_text("Awaiting job completion"), color=YASPIN_COLOR
|
|
1309
1375
|
) as spinner:
|
|
1310
1376
|
if not is_cost_estimate:
|
|
1311
|
-
clickable_link = make_clickable_link(
|
|
1312
|
-
|
|
1377
|
+
clickable_link = make_clickable_link(
|
|
1378
|
+
f"https://app.sutro.sh/jobs/{job_id}"
|
|
1379
|
+
)
|
|
1380
|
+
spinner.write(
|
|
1381
|
+
to_colored_text(
|
|
1382
|
+
f"Progress can also be monitored at: {clickable_link}"
|
|
1383
|
+
)
|
|
1384
|
+
)
|
|
1313
1385
|
while (time.time() - start_time) < timeout:
|
|
1314
1386
|
try:
|
|
1315
1387
|
status = self._fetch_job_status(job_id)
|
|
@@ -1326,9 +1398,13 @@ class Sutro:
|
|
|
1326
1398
|
spinner.text = to_colored_text(f"Job status is {status} for {job_id}")
|
|
1327
1399
|
|
|
1328
1400
|
if status == JobStatus.SUCCEEDED:
|
|
1329
|
-
spinner.stop()
|
|
1401
|
+
spinner.stop() # Stop this spinner as `get_job_results` has its own spinner text
|
|
1330
1402
|
if obtain_results:
|
|
1331
|
-
spinner.write(
|
|
1403
|
+
spinner.write(
|
|
1404
|
+
to_colored_text(
|
|
1405
|
+
"Job completed! Retrieving results...", "success"
|
|
1406
|
+
)
|
|
1407
|
+
)
|
|
1332
1408
|
results = self.get_job_results(job_id)
|
|
1333
1409
|
break
|
|
1334
1410
|
if status == JobStatus.FAILED:
|
|
@@ -1338,12 +1414,11 @@ class Sutro:
|
|
|
1338
1414
|
spinner.write(to_colored_text("Job has been cancelled"))
|
|
1339
1415
|
return None
|
|
1340
1416
|
|
|
1341
|
-
|
|
1342
1417
|
time.sleep(POLL_INTERVAL)
|
|
1343
1418
|
|
|
1344
1419
|
return results
|
|
1345
1420
|
|
|
1346
|
-
def _clear_job_results_cache(self):
|
|
1421
|
+
def _clear_job_results_cache(self): # only to be called by the CLI
|
|
1347
1422
|
"""
|
|
1348
1423
|
Clears the cache for a job results.
|
|
1349
1424
|
"""
|
|
@@ -1356,29 +1431,41 @@ class Sutro:
|
|
|
1356
1431
|
"""
|
|
1357
1432
|
# get the size of the job-results directory
|
|
1358
1433
|
with yaspin(
|
|
1359
|
-
SPINNER,
|
|
1434
|
+
SPINNER,
|
|
1435
|
+
text=to_colored_text("Retrieving job results cache contents"),
|
|
1436
|
+
color=YASPIN_COLOR,
|
|
1360
1437
|
) as spinner:
|
|
1361
1438
|
if not os.path.exists(os.path.expanduser("~/.sutro/job-results")):
|
|
1362
1439
|
spinner.write(to_colored_text("No job results cache found", "success"))
|
|
1363
1440
|
return
|
|
1364
1441
|
total_size = 0
|
|
1365
1442
|
for file in os.listdir(os.path.expanduser("~/.sutro/job-results")):
|
|
1366
|
-
size =
|
|
1443
|
+
size = (
|
|
1444
|
+
os.path.getsize(os.path.expanduser(f"~/.sutro/job-results/{file}"))
|
|
1445
|
+
/ 1024
|
|
1446
|
+
/ 1024
|
|
1447
|
+
/ 1024
|
|
1448
|
+
)
|
|
1367
1449
|
total_size += size
|
|
1368
1450
|
spinner.write(to_colored_text(f"File: {file} - Size: {size} GB"))
|
|
1369
|
-
spinner.write(
|
|
1370
|
-
|
|
1451
|
+
spinner.write(
|
|
1452
|
+
to_colored_text(
|
|
1453
|
+
f"Total size of results cache at ~/.sutro/job-results: {total_size} GB",
|
|
1454
|
+
"success",
|
|
1455
|
+
)
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1371
1458
|
def _await_job_start(self, job_id: str, timeout: Optional[int] = 7200):
|
|
1372
1459
|
"""
|
|
1373
1460
|
Waits for job start to occur and then returns the results upon
|
|
1374
1461
|
a successful start.
|
|
1375
|
-
|
|
1462
|
+
|
|
1376
1463
|
"""
|
|
1377
1464
|
POLL_INTERVAL = 5
|
|
1378
1465
|
|
|
1379
1466
|
start_time = time.time()
|
|
1380
1467
|
with yaspin(
|
|
1381
|
-
|
|
1468
|
+
SPINNER, text=to_colored_text("Awaiting job completion"), color=YASPIN_COLOR
|
|
1382
1469
|
) as spinner:
|
|
1383
1470
|
while (time.time() - start_time) < timeout:
|
|
1384
1471
|
try:
|
|
@@ -1405,4 +1492,3 @@ class Sutro:
|
|
|
1405
1492
|
time.sleep(POLL_INTERVAL)
|
|
1406
1493
|
|
|
1407
1494
|
return False
|
|
1408
|
-
|
sutro-0.1.30/PKG-INFO
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sutro
|
|
3
|
-
Version: 0.1.30
|
|
4
|
-
Summary: Sutro Python SDK
|
|
5
|
-
Project-URL: Homepage, https://sutro.sh
|
|
6
|
-
Project-URL: Documentation, https://docs.sutro.sh
|
|
7
|
-
License-Expression: Apache-2.0
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Requires-Dist: click==8.1.7
|
|
11
|
-
Requires-Dist: colorama==0.4.4
|
|
12
|
-
Requires-Dist: numpy==2.1.1
|
|
13
|
-
Requires-Dist: pandas==2.2.3
|
|
14
|
-
Requires-Dist: polars==1.8.2
|
|
15
|
-
Requires-Dist: pyarrow==21.0.0
|
|
16
|
-
Requires-Dist: pydantic==2.11.4
|
|
17
|
-
Requires-Dist: requests==2.32.3
|
|
18
|
-
Requires-Dist: tqdm==4.67.1
|
|
19
|
-
Requires-Dist: yaspin==3.1.0
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
|
-
# sutro-client
|
|
23
|
-
|
|
24
|
-
The official Python client for Sutro. See [docs.sutro.sh](https://docs.sutro.sh/) for more information.
|
sutro-0.1.30/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|