microcafe 0.1.0__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.
- microcafe/__init__.py +54 -0
- microcafe/cli.py +354 -0
- microcafe/client.py +396 -0
- microcafe/exceptions.py +49 -0
- microcafe/models.py +145 -0
- microcafe/test_api.py +68 -0
- microcafe/test_sdk.py +345 -0
- microcafe-0.1.0.dist-info/METADATA +197 -0
- microcafe-0.1.0.dist-info/RECORD +11 -0
- microcafe-0.1.0.dist-info/WHEEL +4 -0
- microcafe-0.1.0.dist-info/entry_points.txt +2 -0
microcafe/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# src/microcafe/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
microCafe Python SDK - AI-powered microbiome analysis.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from microcafe import Client
|
|
7
|
+
|
|
8
|
+
client = Client(api_key="mc_your_key_here")
|
|
9
|
+
|
|
10
|
+
# Single sequence
|
|
11
|
+
result = client.classify("ATCGATCG...")
|
|
12
|
+
print(result.predictions[0].genus)
|
|
13
|
+
|
|
14
|
+
# File classification
|
|
15
|
+
summary = client.classify_file("sequences.fq")
|
|
16
|
+
print(summary.summary.raw_counts)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import Client
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
MicroCafeError,
|
|
22
|
+
AuthenticationError,
|
|
23
|
+
RateLimitError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
JobNotFoundError,
|
|
26
|
+
JobNotCompletedError,
|
|
27
|
+
ServerError,
|
|
28
|
+
)
|
|
29
|
+
from .models import (
|
|
30
|
+
Prediction,
|
|
31
|
+
ClassificationResult,
|
|
32
|
+
Summary,
|
|
33
|
+
JobStatus,
|
|
34
|
+
JobResults,
|
|
35
|
+
JobSummary,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__version__ = "0.1.0"
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Client",
|
|
41
|
+
"MicroCafeError",
|
|
42
|
+
"AuthenticationError",
|
|
43
|
+
"RateLimitError",
|
|
44
|
+
"ValidationError",
|
|
45
|
+
"JobNotFoundError",
|
|
46
|
+
"JobNotCompletedError",
|
|
47
|
+
"ServerError",
|
|
48
|
+
"Prediction",
|
|
49
|
+
"ClassificationResult",
|
|
50
|
+
"Summary",
|
|
51
|
+
"JobStatus",
|
|
52
|
+
"JobResults",
|
|
53
|
+
"JobSummary",
|
|
54
|
+
]
|
microcafe/cli.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# src/microcafe/cli.py
|
|
2
|
+
"""Command-line interface for microCafe."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from .client import Client
|
|
12
|
+
from .exceptions import MicroCafeError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_api_key(ctx_api_key: str = None) -> str:
|
|
16
|
+
"""Get API key from argument, env var, or config file."""
|
|
17
|
+
if ctx_api_key:
|
|
18
|
+
return ctx_api_key
|
|
19
|
+
|
|
20
|
+
env_key = os.environ.get("MICROCAFE_API_KEY")
|
|
21
|
+
if env_key:
|
|
22
|
+
return env_key
|
|
23
|
+
|
|
24
|
+
config_path = Path.home() / ".microcafe" / "config.json"
|
|
25
|
+
if config_path.exists():
|
|
26
|
+
try:
|
|
27
|
+
with open(config_path) as f:
|
|
28
|
+
config = json.load(f)
|
|
29
|
+
if config.get("api_key"):
|
|
30
|
+
return config["api_key"]
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
click.echo("Error: No API key provided.", err=True)
|
|
35
|
+
click.echo("Set via: --api-key, MICROCAFE_API_KEY env var, or 'microcafe configure'", err=True)
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def format_output(data: dict, fmt: str):
|
|
40
|
+
"""Output data in the requested format."""
|
|
41
|
+
if fmt == "json":
|
|
42
|
+
click.echo(json.dumps(data, indent=2))
|
|
43
|
+
else:
|
|
44
|
+
for key, value in data.items():
|
|
45
|
+
if isinstance(value, dict):
|
|
46
|
+
click.echo(f"\n{key}:")
|
|
47
|
+
for k, v in value.items():
|
|
48
|
+
click.echo(f" {k}: {v}")
|
|
49
|
+
else:
|
|
50
|
+
click.echo(f"{key}: {value}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.group()
|
|
54
|
+
@click.version_option(version="0.1.0", prog_name="microcafe")
|
|
55
|
+
def main():
|
|
56
|
+
"""microCafe CLI - AI-powered microbiome analysis."""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@main.command()
|
|
61
|
+
@click.argument("api_key")
|
|
62
|
+
def configure(api_key: str):
|
|
63
|
+
"""Save API key to config file."""
|
|
64
|
+
config_dir = Path.home() / ".microcafe"
|
|
65
|
+
config_dir.mkdir(exist_ok=True)
|
|
66
|
+
|
|
67
|
+
config_path = config_dir / "config.json"
|
|
68
|
+
config = {"api_key": api_key}
|
|
69
|
+
|
|
70
|
+
with open(config_path, "w") as f:
|
|
71
|
+
json.dump(config, f, indent=2)
|
|
72
|
+
|
|
73
|
+
click.echo(f"API key saved to {config_path}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@main.command()
|
|
77
|
+
@click.argument("sequence")
|
|
78
|
+
@click.option("--api-key", "-k", help="API key (or set MICROCAFE_API_KEY)")
|
|
79
|
+
@click.option("--top-k", "-n", default=5, help="Number of predictions (default: 5)")
|
|
80
|
+
@click.option("--format", "fmt", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
81
|
+
def classify(sequence: str, api_key: str, top_k: int, fmt: str):
|
|
82
|
+
"""Classify a single DNA sequence."""
|
|
83
|
+
api_key = get_api_key(api_key)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
client = Client(api_key=api_key)
|
|
87
|
+
result = client.classify(sequence, top_k=top_k)
|
|
88
|
+
|
|
89
|
+
if fmt == "json":
|
|
90
|
+
format_output({
|
|
91
|
+
"sequence_length": result.sequence_length,
|
|
92
|
+
"predictions": [
|
|
93
|
+
{"genus": p.genus, "confidence": p.confidence}
|
|
94
|
+
for p in result.predictions
|
|
95
|
+
]
|
|
96
|
+
}, fmt)
|
|
97
|
+
else:
|
|
98
|
+
click.echo(f"Sequence length: {result.sequence_length}")
|
|
99
|
+
click.echo("\nPredictions:")
|
|
100
|
+
for i, p in enumerate(result.predictions, 1):
|
|
101
|
+
bar = "█" * int(p.confidence * 20)
|
|
102
|
+
click.echo(f" {i}. {p.genus:<20} {p.confidence:.4f} {bar}")
|
|
103
|
+
|
|
104
|
+
except MicroCafeError as e:
|
|
105
|
+
click.echo(f"Error: {e.message}", err=True)
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@main.command("classify-file")
|
|
110
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
111
|
+
@click.option("--api-key", "-k", help="API key (or set MICROCAFE_API_KEY)")
|
|
112
|
+
@click.option("--top-k", "-n", default=5, help="Number of predictions (default: 5)")
|
|
113
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file for results")
|
|
114
|
+
@click.option("--no-wait", is_flag=True, help="Submit and return job ID without waiting")
|
|
115
|
+
@click.option("--format", "fmt", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
116
|
+
def classify_file(file_path: str, api_key: str, top_k: int, output: str, no_wait: bool, fmt: str):
|
|
117
|
+
"""Classify sequences from a FASTA/FASTQ file."""
|
|
118
|
+
api_key = get_api_key(api_key)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
client = Client(api_key=api_key)
|
|
122
|
+
|
|
123
|
+
click.echo(f"Uploading {file_path}...")
|
|
124
|
+
job = client.classify_file_async(file_path, top_k=top_k)
|
|
125
|
+
click.echo(f"Job submitted: {job.job_id}")
|
|
126
|
+
|
|
127
|
+
if no_wait:
|
|
128
|
+
click.echo(f"\nCheck status: microcafe status {job.job_id}")
|
|
129
|
+
click.echo(f"Get abundance: microcafe abundance {job.job_id}")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Poll with progress bar
|
|
133
|
+
with click.progressbar(length=100, label="Processing") as bar:
|
|
134
|
+
last_progress = 0
|
|
135
|
+
while True:
|
|
136
|
+
status = client.get_job_status(job.job_id)
|
|
137
|
+
|
|
138
|
+
increment = int(status.progress) - last_progress
|
|
139
|
+
if increment > 0:
|
|
140
|
+
bar.update(increment)
|
|
141
|
+
last_progress = int(status.progress)
|
|
142
|
+
|
|
143
|
+
if status.is_completed:
|
|
144
|
+
bar.update(100 - last_progress)
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
if status.is_failed:
|
|
148
|
+
click.echo(f"\nJob failed: {status.error_message}", err=True)
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
import time
|
|
152
|
+
time.sleep(2.0)
|
|
153
|
+
|
|
154
|
+
# Fetch abundance summary
|
|
155
|
+
summary = client.get_job_summary(job.job_id)
|
|
156
|
+
|
|
157
|
+
result_data = {
|
|
158
|
+
"job_id": summary.job_id,
|
|
159
|
+
"file_name": summary.file_name,
|
|
160
|
+
"total_sequences": summary.total_sequences,
|
|
161
|
+
"unique_genera": summary.summary.unique_genera,
|
|
162
|
+
"raw_counts": summary.summary.raw_counts,
|
|
163
|
+
"relative_abundance": summary.summary.relative_abundance,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if output:
|
|
167
|
+
with open(output, "w") as f:
|
|
168
|
+
json.dump(result_data, f, indent=2)
|
|
169
|
+
click.echo(f"\nResults saved to {output}")
|
|
170
|
+
elif fmt == "json":
|
|
171
|
+
format_output(result_data, fmt)
|
|
172
|
+
else:
|
|
173
|
+
click.echo(f"\nResults for {summary.file_name}:")
|
|
174
|
+
click.echo(f" Total sequences: {summary.total_sequences}")
|
|
175
|
+
click.echo(f" Unique genera: {summary.summary.unique_genera}")
|
|
176
|
+
click.echo(f"\nTop genera by abundance:")
|
|
177
|
+
sorted_abundance = sorted(
|
|
178
|
+
summary.summary.relative_abundance.items(),
|
|
179
|
+
key=lambda x: x[1],
|
|
180
|
+
reverse=True,
|
|
181
|
+
)[:10]
|
|
182
|
+
for i, (genus, abundance) in enumerate(sorted_abundance, 1):
|
|
183
|
+
count = summary.summary.raw_counts.get(genus, 0)
|
|
184
|
+
bar = "█" * int(abundance / 5)
|
|
185
|
+
click.echo(f" {i:>2}. {genus:<20} {abundance:>6.2f}% ({count:>5}) {bar}")
|
|
186
|
+
|
|
187
|
+
except MicroCafeError as e:
|
|
188
|
+
click.echo(f"Error: {e.message}", err=True)
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@main.command()
|
|
193
|
+
@click.argument("job_id")
|
|
194
|
+
@click.option("--api-key", "-k", help="API key (or set MICROCAFE_API_KEY)")
|
|
195
|
+
@click.option("--wait", "-w", is_flag=True, help="Poll until job completes")
|
|
196
|
+
@click.option("--format", "fmt", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
197
|
+
def status(job_id: str, api_key: str, wait: bool, fmt: str):
|
|
198
|
+
"""Check status of a classification job."""
|
|
199
|
+
api_key = get_api_key(api_key)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
client = Client(api_key=api_key)
|
|
203
|
+
|
|
204
|
+
if wait:
|
|
205
|
+
click.echo(f"Waiting for job {job_id}...")
|
|
206
|
+
with click.progressbar(length=100, label="Processing") as bar:
|
|
207
|
+
last_progress = 0
|
|
208
|
+
while True:
|
|
209
|
+
job_status = client.get_job_status(job_id)
|
|
210
|
+
|
|
211
|
+
increment = int(job_status.progress) - last_progress
|
|
212
|
+
if increment > 0:
|
|
213
|
+
bar.update(increment)
|
|
214
|
+
last_progress = int(job_status.progress)
|
|
215
|
+
|
|
216
|
+
if not job_status.is_running:
|
|
217
|
+
bar.update(100 - last_progress)
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
import time
|
|
221
|
+
time.sleep(2.0)
|
|
222
|
+
|
|
223
|
+
click.echo(f"\nJob {job_status.status}.")
|
|
224
|
+
if job_status.is_completed:
|
|
225
|
+
click.echo(f"\nGet results:")
|
|
226
|
+
click.echo(f" microcafe abundance {job_id}")
|
|
227
|
+
click.echo(f" microcafe predictions {job_id}")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
job_status = client.get_job_status(job_id)
|
|
231
|
+
|
|
232
|
+
if fmt == "json":
|
|
233
|
+
format_output({
|
|
234
|
+
"job_id": job_status.job_id,
|
|
235
|
+
"status": job_status.status,
|
|
236
|
+
"progress": job_status.progress,
|
|
237
|
+
"total_sequences": job_status.total_sequences,
|
|
238
|
+
"sequences_processed": job_status.sequences_processed,
|
|
239
|
+
"sequences_failed": job_status.sequences_failed,
|
|
240
|
+
"file_name": job_status.file_name,
|
|
241
|
+
"error_message": job_status.error_message,
|
|
242
|
+
}, fmt)
|
|
243
|
+
else:
|
|
244
|
+
click.echo(f"Job: {job_status.job_id}")
|
|
245
|
+
click.echo(f"Status: {job_status.status}")
|
|
246
|
+
click.echo(f"Progress: {job_status.progress:.1f}%")
|
|
247
|
+
click.echo(f"Sequences: {job_status.sequences_processed}/{job_status.total_sequences}")
|
|
248
|
+
if job_status.sequences_failed:
|
|
249
|
+
click.echo(f"Failed: {job_status.sequences_failed}")
|
|
250
|
+
if job_status.error_message:
|
|
251
|
+
click.echo(f"Error: {job_status.error_message}")
|
|
252
|
+
|
|
253
|
+
if job_status.is_completed:
|
|
254
|
+
click.echo(f"\nGet results:")
|
|
255
|
+
click.echo(f" microcafe abundance {job_id}")
|
|
256
|
+
click.echo(f" microcafe predictions {job_id}")
|
|
257
|
+
|
|
258
|
+
except MicroCafeError as e:
|
|
259
|
+
click.echo(f"Error: {e.message}", err=True)
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@main.command()
|
|
264
|
+
@click.argument("job_id")
|
|
265
|
+
@click.option("--api-key", "-k", help="API key (or set MICROCAFE_API_KEY)")
|
|
266
|
+
@click.option("--top", "-t", default=10, help="Number of top genera to show (default: 10)")
|
|
267
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file")
|
|
268
|
+
@click.option("--format", "fmt", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
269
|
+
def abundance(job_id: str, api_key: str, top: int, output: str, fmt: str):
|
|
270
|
+
"""Get abundance table (raw counts & relative abundance) of a completed job."""
|
|
271
|
+
api_key = get_api_key(api_key)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
client = Client(api_key=api_key)
|
|
275
|
+
job_summary = client.get_job_summary(job_id)
|
|
276
|
+
|
|
277
|
+
result_data = {
|
|
278
|
+
"job_id": job_summary.job_id,
|
|
279
|
+
"file_name": job_summary.file_name,
|
|
280
|
+
"total_sequences": job_summary.total_sequences,
|
|
281
|
+
"unique_genera": job_summary.summary.unique_genera,
|
|
282
|
+
"raw_counts": job_summary.summary.raw_counts,
|
|
283
|
+
"relative_abundance": job_summary.summary.relative_abundance,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if output:
|
|
287
|
+
with open(output, "w") as f:
|
|
288
|
+
json.dump(result_data, f, indent=2)
|
|
289
|
+
click.echo(f"Abundance data saved to {output}")
|
|
290
|
+
elif fmt == "json":
|
|
291
|
+
format_output(result_data, fmt)
|
|
292
|
+
else:
|
|
293
|
+
click.echo(f"Abundance for {job_summary.file_name}:")
|
|
294
|
+
click.echo(f" Total sequences: {job_summary.total_sequences}")
|
|
295
|
+
click.echo(f" Unique genera: {job_summary.summary.unique_genera}")
|
|
296
|
+
click.echo(f"\nTop {top} genera by abundance:")
|
|
297
|
+
sorted_abundance = sorted(
|
|
298
|
+
job_summary.summary.relative_abundance.items(),
|
|
299
|
+
key=lambda x: x[1],
|
|
300
|
+
reverse=True,
|
|
301
|
+
)[:top]
|
|
302
|
+
for i, (genus, abd) in enumerate(sorted_abundance, 1):
|
|
303
|
+
count = job_summary.summary.raw_counts.get(genus, 0)
|
|
304
|
+
bar = "█" * int(abd / 5)
|
|
305
|
+
click.echo(f" {i:>2}. {genus:<20} {abd:>6.2f}% ({count:>5}) {bar}")
|
|
306
|
+
|
|
307
|
+
except MicroCafeError as e:
|
|
308
|
+
click.echo(f"Error: {e.message}", err=True)
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@main.command()
|
|
313
|
+
@click.argument("job_id")
|
|
314
|
+
@click.option("--api-key", "-k", help="API key (or set MICROCAFE_API_KEY)")
|
|
315
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file")
|
|
316
|
+
def predictions(job_id: str, api_key: str, output: str):
|
|
317
|
+
"""Get full per-sequence predictions of a completed job."""
|
|
318
|
+
api_key = get_api_key(api_key)
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
client = Client(api_key=api_key)
|
|
322
|
+
job_results = client.get_job_results(job_id)
|
|
323
|
+
|
|
324
|
+
result_data = {
|
|
325
|
+
"job_id": job_results.job_id,
|
|
326
|
+
"total_sequences": job_results.total_sequences,
|
|
327
|
+
"model_version": job_results.model_version,
|
|
328
|
+
"predictions": [
|
|
329
|
+
{
|
|
330
|
+
"sequence_id": r.sequence_id,
|
|
331
|
+
"sequence_length": r.sequence_length,
|
|
332
|
+
"predictions": [
|
|
333
|
+
{"genus": p.genus, "confidence": p.confidence}
|
|
334
|
+
for p in r.predictions
|
|
335
|
+
],
|
|
336
|
+
}
|
|
337
|
+
for r in job_results.results
|
|
338
|
+
],
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if output:
|
|
342
|
+
with open(output, "w") as f:
|
|
343
|
+
json.dump(result_data, f, indent=2)
|
|
344
|
+
click.echo(f"Predictions saved to {output}")
|
|
345
|
+
else:
|
|
346
|
+
click.echo(json.dumps(result_data, indent=2))
|
|
347
|
+
|
|
348
|
+
except MicroCafeError as e:
|
|
349
|
+
click.echo(f"Error: {e.message}", err=True)
|
|
350
|
+
sys.exit(1)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
main()
|