mivia 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.
- mivia/__init__.py +46 -0
- mivia/cli/__init__.py +1 -0
- mivia/cli/main.py +414 -0
- mivia/client.py +445 -0
- mivia/exceptions.py +45 -0
- mivia/models.py +180 -0
- mivia/py.typed +0 -0
- mivia/sync_client.py +305 -0
- mivia-0.1.0.dist-info/METADATA +176 -0
- mivia-0.1.0.dist-info/RECORD +13 -0
- mivia-0.1.0.dist-info/WHEEL +4 -0
- mivia-0.1.0.dist-info/entry_points.txt +2 -0
- mivia-0.1.0.dist-info/licenses/LICENSE +21 -0
mivia/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""MiViA Python API client."""
|
|
2
|
+
|
|
3
|
+
from mivia.client import MiviaClient
|
|
4
|
+
from mivia.exceptions import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
JobTimeoutError,
|
|
7
|
+
MiviaError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
ServerError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
from mivia.models import (
|
|
13
|
+
CreateJobsRequest,
|
|
14
|
+
CreateReportRequest,
|
|
15
|
+
CustomizationDto,
|
|
16
|
+
ImageDto,
|
|
17
|
+
JobDto,
|
|
18
|
+
JobListResponse,
|
|
19
|
+
JobStatus,
|
|
20
|
+
ModelDto,
|
|
21
|
+
)
|
|
22
|
+
from mivia.sync_client import SyncMiviaClient
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Clients
|
|
26
|
+
"MiviaClient",
|
|
27
|
+
"SyncMiviaClient",
|
|
28
|
+
# Exceptions
|
|
29
|
+
"MiviaError",
|
|
30
|
+
"AuthenticationError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"ValidationError",
|
|
33
|
+
"ServerError",
|
|
34
|
+
"JobTimeoutError",
|
|
35
|
+
# Models
|
|
36
|
+
"ImageDto",
|
|
37
|
+
"ModelDto",
|
|
38
|
+
"CustomizationDto",
|
|
39
|
+
"JobDto",
|
|
40
|
+
"JobStatus",
|
|
41
|
+
"JobListResponse",
|
|
42
|
+
"CreateJobsRequest",
|
|
43
|
+
"CreateReportRequest",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
mivia/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MiViA CLI module."""
|
mivia/cli/main.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""MiViA CLI application."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich import print as rprint
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from mivia.exceptions import MiviaError
|
|
14
|
+
from mivia.sync_client import SyncMiviaClient
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="mivia",
|
|
18
|
+
help="MiViA (Microstructure Analysis) API client CLI",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
jobs_app = typer.Typer(help="Job management commands")
|
|
22
|
+
report_app = typer.Typer(help="Report generation commands")
|
|
23
|
+
|
|
24
|
+
app.add_typer(jobs_app, name="jobs")
|
|
25
|
+
app.add_typer(report_app, name="report")
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_client() -> SyncMiviaClient:
|
|
31
|
+
"""Get configured client."""
|
|
32
|
+
return SyncMiviaClient()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def handle_error(e: MiviaError) -> None:
|
|
36
|
+
"""Handle and display error."""
|
|
37
|
+
rprint(f"[red]Error:[/red] {e.message}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- Models ---
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def models():
|
|
46
|
+
"""List available models."""
|
|
47
|
+
try:
|
|
48
|
+
client = get_client()
|
|
49
|
+
result = client.list_models()
|
|
50
|
+
|
|
51
|
+
table = Table(title="Available Models")
|
|
52
|
+
table.add_column("ID", style="dim")
|
|
53
|
+
table.add_column("Name")
|
|
54
|
+
table.add_column("Display Name")
|
|
55
|
+
table.add_column("Access")
|
|
56
|
+
|
|
57
|
+
for m in result:
|
|
58
|
+
table.add_row(
|
|
59
|
+
str(m.id),
|
|
60
|
+
m.name,
|
|
61
|
+
m.display_name,
|
|
62
|
+
m.access_type.value,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
console.print(table)
|
|
66
|
+
except MiviaError as e:
|
|
67
|
+
handle_error(e)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- Customizations ---
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def customizations(
|
|
75
|
+
model_id: Annotated[str, typer.Argument(help="Model UUID")],
|
|
76
|
+
):
|
|
77
|
+
"""List customizations for a model."""
|
|
78
|
+
try:
|
|
79
|
+
client = get_client()
|
|
80
|
+
result = client.get_model_customizations(UUID(model_id))
|
|
81
|
+
|
|
82
|
+
if not result:
|
|
83
|
+
rprint(f"[yellow]No customizations available for model {model_id}[/yellow]")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
table = Table(title="Available Customizations")
|
|
87
|
+
table.add_column("ID", style="dim")
|
|
88
|
+
table.add_column("Name (EN)")
|
|
89
|
+
table.add_column("Name (DE)")
|
|
90
|
+
|
|
91
|
+
for c in result:
|
|
92
|
+
table.add_row(
|
|
93
|
+
str(c.id),
|
|
94
|
+
c.name.en,
|
|
95
|
+
c.name.de,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
console.print(table)
|
|
99
|
+
except MiviaError as e:
|
|
100
|
+
handle_error(e)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --- Upload ---
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command()
|
|
107
|
+
def upload(
|
|
108
|
+
files: Annotated[list[Path], typer.Argument(help="Image files to upload")],
|
|
109
|
+
forced: Annotated[bool, typer.Option("--forced", "-f", help="Bypass quality check")] = False,
|
|
110
|
+
):
|
|
111
|
+
"""Upload image(s)."""
|
|
112
|
+
try:
|
|
113
|
+
client = get_client()
|
|
114
|
+
forced_list = [forced] * len(files) if forced else None
|
|
115
|
+
result = client.upload_images(files, forced_list)
|
|
116
|
+
|
|
117
|
+
table = Table(title="Uploaded Images")
|
|
118
|
+
table.add_column("ID", style="dim")
|
|
119
|
+
table.add_column("Filename")
|
|
120
|
+
table.add_column("Size")
|
|
121
|
+
table.add_column("Validated")
|
|
122
|
+
|
|
123
|
+
for img in result:
|
|
124
|
+
size = f"{img.width}x{img.height}" if img.width else "-"
|
|
125
|
+
table.add_row(
|
|
126
|
+
str(img.id),
|
|
127
|
+
img.original_filename,
|
|
128
|
+
size,
|
|
129
|
+
"Yes" if img.validated else "No",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
console.print(table)
|
|
133
|
+
except MiviaError as e:
|
|
134
|
+
handle_error(e)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# --- Images ---
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command()
|
|
141
|
+
def images():
|
|
142
|
+
"""List uploaded images."""
|
|
143
|
+
try:
|
|
144
|
+
client = get_client()
|
|
145
|
+
result = client.list_images()
|
|
146
|
+
|
|
147
|
+
table = Table(title="Your Images")
|
|
148
|
+
table.add_column("ID", style="dim")
|
|
149
|
+
table.add_column("Filename")
|
|
150
|
+
table.add_column("Created")
|
|
151
|
+
table.add_column("Validated")
|
|
152
|
+
|
|
153
|
+
for img in result:
|
|
154
|
+
table.add_row(
|
|
155
|
+
str(img.id),
|
|
156
|
+
img.original_filename,
|
|
157
|
+
img.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
158
|
+
"Yes" if img.validated else "No",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
console.print(table)
|
|
162
|
+
except MiviaError as e:
|
|
163
|
+
handle_error(e)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --- Analyze ---
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def resolve_customization(
|
|
170
|
+
client: SyncMiviaClient,
|
|
171
|
+
model_id: UUID,
|
|
172
|
+
customization: str | None,
|
|
173
|
+
) -> UUID | None:
|
|
174
|
+
"""Resolve customization by UUID or name."""
|
|
175
|
+
if not customization:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Try UUID first
|
|
179
|
+
try:
|
|
180
|
+
return UUID(customization)
|
|
181
|
+
except ValueError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Search by name
|
|
185
|
+
customs = client.get_model_customizations(model_id)
|
|
186
|
+
for c in customs:
|
|
187
|
+
if customization.lower() in (c.name.en.lower(), c.name.de.lower()):
|
|
188
|
+
return c.id
|
|
189
|
+
|
|
190
|
+
# Partial match
|
|
191
|
+
for c in customs:
|
|
192
|
+
if customization.lower() in c.name.en.lower() or customization.lower() in c.name.de.lower():
|
|
193
|
+
return c.id
|
|
194
|
+
|
|
195
|
+
rprint(f"[red]Error:[/red] Customization '{customization}' not found")
|
|
196
|
+
if customs:
|
|
197
|
+
rprint("[yellow]Available customizations:[/yellow]")
|
|
198
|
+
for c in customs:
|
|
199
|
+
rprint(f" - {c.name.en} ({c.id})")
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command()
|
|
204
|
+
def analyze(
|
|
205
|
+
files: Annotated[list[Path], typer.Argument(help="Image files to analyze")],
|
|
206
|
+
model: Annotated[str, typer.Option("--model", "-m", help="Model UUID")] = None,
|
|
207
|
+
customization: Annotated[str, typer.Option("--customization", "-c", help="Customization UUID or name")] = None,
|
|
208
|
+
list_customizations: Annotated[bool, typer.Option("--list-customizations", "-l", help="List available customizations")] = False,
|
|
209
|
+
no_wait: Annotated[bool, typer.Option("--no-wait", help="Don't wait for completion")] = False,
|
|
210
|
+
timeout: Annotated[float, typer.Option("--timeout", "-t", help="Wait timeout in seconds")] = 300.0,
|
|
211
|
+
):
|
|
212
|
+
"""Upload images and run analysis."""
|
|
213
|
+
if not model:
|
|
214
|
+
rprint("[red]Error:[/red] --model is required")
|
|
215
|
+
raise typer.Exit(1)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
client = get_client()
|
|
219
|
+
model_id = UUID(model)
|
|
220
|
+
|
|
221
|
+
# List customizations if requested
|
|
222
|
+
if list_customizations:
|
|
223
|
+
customs = client.get_model_customizations(model_id)
|
|
224
|
+
if not customs:
|
|
225
|
+
rprint("[yellow]No customizations available for this model[/yellow]")
|
|
226
|
+
else:
|
|
227
|
+
rprint("[bold]Available customizations:[/bold]")
|
|
228
|
+
for c in customs:
|
|
229
|
+
rprint(f" - [cyan]{c.name.en}[/cyan] ({c.id})")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
customization_id = resolve_customization(client, model_id, customization)
|
|
233
|
+
|
|
234
|
+
rprint(f"[blue]Uploading {len(files)} file(s)...[/blue]")
|
|
235
|
+
|
|
236
|
+
result = client.analyze(
|
|
237
|
+
file_paths=files,
|
|
238
|
+
model_id=model_id,
|
|
239
|
+
customization_id=customization_id,
|
|
240
|
+
wait=not no_wait,
|
|
241
|
+
timeout=timeout,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
table = Table(title="Analysis Results")
|
|
245
|
+
table.add_column("Job ID", style="dim")
|
|
246
|
+
table.add_column("Image")
|
|
247
|
+
table.add_column("Status")
|
|
248
|
+
table.add_column("Has Results")
|
|
249
|
+
|
|
250
|
+
for job in result:
|
|
251
|
+
status_color = {
|
|
252
|
+
"CACHED": "green",
|
|
253
|
+
"NEW": "green",
|
|
254
|
+
"PENDING": "yellow",
|
|
255
|
+
"FAILED": "red",
|
|
256
|
+
}.get(job.status.value, "white")
|
|
257
|
+
|
|
258
|
+
table.add_row(
|
|
259
|
+
str(job.id),
|
|
260
|
+
job.image,
|
|
261
|
+
f"[{status_color}]{job.status.value}[/{status_color}]",
|
|
262
|
+
"Yes" if job.has_results else "No",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
console.print(table)
|
|
266
|
+
except MiviaError as e:
|
|
267
|
+
handle_error(e)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# --- Jobs Subcommands ---
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@jobs_app.command("list")
|
|
274
|
+
def jobs_list(
|
|
275
|
+
model: Annotated[str, typer.Option("--model", "-m", help="Filter by model UUID")] = None,
|
|
276
|
+
page: Annotated[int, typer.Option("--page", "-p", help="Page number")] = 1,
|
|
277
|
+
size: Annotated[int, typer.Option("--size", "-s", help="Page size")] = 10,
|
|
278
|
+
):
|
|
279
|
+
"""List jobs."""
|
|
280
|
+
try:
|
|
281
|
+
client = get_client()
|
|
282
|
+
model_id = UUID(model) if model else None
|
|
283
|
+
|
|
284
|
+
result = client.list_jobs(model_id=model_id, page=page, page_size=size)
|
|
285
|
+
|
|
286
|
+
table = Table(
|
|
287
|
+
title=f"Jobs (Page {result.pagination.page}/{result.pagination.total_pages})"
|
|
288
|
+
)
|
|
289
|
+
table.add_column("ID", style="dim")
|
|
290
|
+
table.add_column("Image")
|
|
291
|
+
table.add_column("Status")
|
|
292
|
+
table.add_column("Created")
|
|
293
|
+
|
|
294
|
+
for job in result.data:
|
|
295
|
+
status_color = {
|
|
296
|
+
"CACHED": "green",
|
|
297
|
+
"NEW": "green",
|
|
298
|
+
"PENDING": "yellow",
|
|
299
|
+
"FAILED": "red",
|
|
300
|
+
}.get(job.status.value, "white")
|
|
301
|
+
|
|
302
|
+
table.add_row(
|
|
303
|
+
str(job.id),
|
|
304
|
+
job.image,
|
|
305
|
+
f"[{status_color}]{job.status.value}[/{status_color}]",
|
|
306
|
+
job.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
console.print(table)
|
|
310
|
+
rprint(f"Total: {result.pagination.total} jobs")
|
|
311
|
+
except MiviaError as e:
|
|
312
|
+
handle_error(e)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@jobs_app.command("get")
|
|
316
|
+
def jobs_get(
|
|
317
|
+
job_id: Annotated[str, typer.Argument(help="Job UUID")],
|
|
318
|
+
):
|
|
319
|
+
"""Get job details."""
|
|
320
|
+
try:
|
|
321
|
+
client = get_client()
|
|
322
|
+
job = client.get_job(UUID(job_id))
|
|
323
|
+
|
|
324
|
+
rprint(f"[bold]Job ID:[/bold] {job.id}")
|
|
325
|
+
rprint(f"[bold]Image:[/bold] {job.image}")
|
|
326
|
+
rprint(f"[bold]Model ID:[/bold] {job.model_id}")
|
|
327
|
+
rprint(f"[bold]Status:[/bold] {job.status.value}")
|
|
328
|
+
rprint(f"[bold]Has Results:[/bold] {job.has_results}")
|
|
329
|
+
rprint(f"[bold]Created:[/bold] {job.created_at}")
|
|
330
|
+
|
|
331
|
+
if job.results:
|
|
332
|
+
rprint(f"\n[bold]Results:[/bold]")
|
|
333
|
+
for i, r in enumerate(job.results):
|
|
334
|
+
rprint(f" {i + 1}. {r}")
|
|
335
|
+
except MiviaError as e:
|
|
336
|
+
handle_error(e)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@jobs_app.command("wait")
|
|
340
|
+
def jobs_wait(
|
|
341
|
+
job_ids: Annotated[list[str], typer.Argument(help="Job UUIDs")],
|
|
342
|
+
timeout: Annotated[float, typer.Option("--timeout", "-t", help="Timeout in seconds")] = 300.0,
|
|
343
|
+
interval: Annotated[float, typer.Option("--interval", "-i", help="Poll interval")] = 2.0,
|
|
344
|
+
):
|
|
345
|
+
"""Wait for job(s) to complete."""
|
|
346
|
+
try:
|
|
347
|
+
client = get_client()
|
|
348
|
+
uuids = [UUID(jid) for jid in job_ids]
|
|
349
|
+
|
|
350
|
+
rprint(f"[blue]Waiting for {len(uuids)} job(s)...[/blue]")
|
|
351
|
+
|
|
352
|
+
result = client.wait_for_jobs(uuids, timeout=timeout, poll_interval=interval)
|
|
353
|
+
|
|
354
|
+
for job in result:
|
|
355
|
+
status_color = "green" if job.status.value in ("CACHED", "NEW") else "red"
|
|
356
|
+
rprint(f"Job {job.id}: [{status_color}]{job.status.value}[/{status_color}]")
|
|
357
|
+
except MiviaError as e:
|
|
358
|
+
handle_error(e)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# --- Report Subcommands ---
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@report_app.command("pdf")
|
|
365
|
+
def report_pdf(
|
|
366
|
+
job_ids: Annotated[list[str], typer.Argument(help="Job UUIDs")],
|
|
367
|
+
output: Annotated[Path, typer.Option("--output", "-o", help="Output file path")] = Path("report.pdf"),
|
|
368
|
+
):
|
|
369
|
+
"""Download PDF report."""
|
|
370
|
+
try:
|
|
371
|
+
client = get_client()
|
|
372
|
+
uuids = [UUID(jid) for jid in job_ids]
|
|
373
|
+
|
|
374
|
+
rprint(f"[blue]Generating PDF report...[/blue]")
|
|
375
|
+
path = client.download_pdf(uuids, output)
|
|
376
|
+
rprint(f"[green]Saved to:[/green] {path}")
|
|
377
|
+
except MiviaError as e:
|
|
378
|
+
handle_error(e)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@report_app.command("csv")
|
|
382
|
+
def report_csv(
|
|
383
|
+
job_ids: Annotated[list[str], typer.Argument(help="Job UUIDs")],
|
|
384
|
+
output: Annotated[Path, typer.Option("--output", "-o", help="Output file path")] = Path("report.zip"),
|
|
385
|
+
no_images: Annotated[bool, typer.Option("--no-images", help="Exclude images")] = False,
|
|
386
|
+
):
|
|
387
|
+
"""Download CSV report (as ZIP)."""
|
|
388
|
+
try:
|
|
389
|
+
client = get_client()
|
|
390
|
+
uuids = [UUID(jid) for jid in job_ids]
|
|
391
|
+
|
|
392
|
+
rprint(f"[blue]Generating CSV report...[/blue]")
|
|
393
|
+
path = client.download_csv(uuids, output, include_images=not no_images)
|
|
394
|
+
rprint(f"[green]Saved to:[/green] {path}")
|
|
395
|
+
except MiviaError as e:
|
|
396
|
+
handle_error(e)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# --- Config ---
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@app.command()
|
|
403
|
+
def config():
|
|
404
|
+
"""Show current configuration."""
|
|
405
|
+
api_key = os.environ.get("MIVIA_API_KEY", "")
|
|
406
|
+
base_url = os.environ.get("MIVIA_BASE_URL", "https://app.mivia.ai/api")
|
|
407
|
+
|
|
408
|
+
rprint("[bold]Configuration:[/bold]")
|
|
409
|
+
rprint(f" MIVIA_API_KEY: {'***' + api_key[-4:] if api_key else '[red]Not set[/red]'}")
|
|
410
|
+
rprint(f" MIVIA_BASE_URL: {base_url}")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
app()
|