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 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()