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