meditopia-cli 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.
meditopia/__init__.py ADDED
File without changes
meditopia/cli.py ADDED
@@ -0,0 +1,413 @@
1
+ """
2
+ medi — Meditopia CLI
3
+
4
+ Usage examples:
5
+ medi login --token mtc_xxxx
6
+ medi whoami
7
+ medi add dataset ./my-data --to pedram/projects/radiology/chest-xray --privacy public --modality imaging
8
+ medi add model ./my-model --to pedram/projects/radiology/chexnet --privacy public --task radiology --framework pytorch
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from typing import List, Optional
15
+
16
+ import typer
17
+ from rich import print as rprint
18
+ from rich.table import Table
19
+ from rich import box
20
+
21
+ from . import config, client
22
+ from .uploader import collect_files
23
+
24
+ app = typer.Typer(
25
+ name="medi",
26
+ help="Meditopia CLI — push models and datasets from your terminal.",
27
+ add_completion=False,
28
+ )
29
+ add_app = typer.Typer(help="Upload a model or dataset to Meditopia.")
30
+ download_app = typer.Typer(help="Download a model or dataset from Meditopia.")
31
+ app.add_typer(add_app, name="add")
32
+ app.add_typer(download_app, name="download")
33
+
34
+
35
+ # ── Helpers ───────────────────────────────────────────────────────────────────
36
+
37
+
38
+ def _parse_to(to: str) -> tuple[str, str, str]:
39
+ """
40
+ Split 'owner/collection/path/name' into (owner, collection_path, slug).
41
+ The last segment is the slug; everything between owner and slug is collection_path.
42
+ e.g. 'pedram/projects/radiology/chest-xray' → ('pedram', 'projects/radiology', 'chest-xray')
43
+ 'pedram/chest-xray' → ('pedram', '', 'chest-xray')
44
+ """
45
+ parts = to.strip("/").split("/")
46
+ if len(parts) < 2:
47
+ rprint(f"[red]--to must be at least owner/name, got: {to!r}[/red]")
48
+ raise typer.Exit(1)
49
+ owner = parts[0]
50
+ slug = parts[-1]
51
+ collection_path = "/".join(parts[1:-1])
52
+ return owner, collection_path, slug
53
+
54
+
55
+ def _abort_if_error(r: dict) -> None:
56
+ if "detail" in r or "error" in r:
57
+ rprint(f"[red]API error:[/red] {r.get('detail') or r.get('error')}")
58
+ raise typer.Exit(1)
59
+
60
+
61
+ # ── login ─────────────────────────────────────────────────────────────────────
62
+
63
+
64
+ @app.command()
65
+ def login(
66
+ token: str = typer.Option(..., help="Your Meditopia API token (starts with mtc_)"),
67
+ api_url: str = typer.Option(config.DEFAULT_API_URL, help="API base URL"),
68
+ ):
69
+ """Save your API token and verify it against the server."""
70
+ config.set_credentials(token, api_url)
71
+ try:
72
+ me = client.whoami()
73
+ except Exception as exc:
74
+ rprint(f"[red]Login failed:[/red] {exc}")
75
+ config.clear()
76
+ raise typer.Exit(1)
77
+ rprint(
78
+ f"[green]✓[/green] Logged in as [bold]{me['username']}[/bold] ({me.get('plan', 'free')} plan)"
79
+ )
80
+
81
+
82
+ # ── whoami ────────────────────────────────────────────────────────────────────
83
+
84
+
85
+ @app.command()
86
+ def whoami():
87
+ """Show the currently authenticated user."""
88
+ try:
89
+ me = client.whoami()
90
+ except RuntimeError as exc:
91
+ rprint(f"[yellow]{exc}[/yellow]")
92
+ raise typer.Exit(1)
93
+ rprint(
94
+ f"[bold]{me['username']}[/bold] · {me.get('email', '')} · {me.get('plan', 'free')} plan"
95
+ )
96
+
97
+
98
+ # ── add dataset ───────────────────────────────────────────────────────────────
99
+
100
+
101
+ @add_app.command("dataset")
102
+ def add_dataset(
103
+ local_path: str = typer.Argument(..., help="Local file or directory to upload"),
104
+ to: str = typer.Option(..., help="Destination path: owner[/collection/...]/ name"),
105
+ privacy: str = typer.Option("public", help="public | private | restricted"),
106
+ modality: str = typer.Option(
107
+ ...,
108
+ help="imaging | ehr | genomics | clinical-notes | waveform | multimodal | other",
109
+ ),
110
+ description: str = typer.Option("", help="Short description of the dataset"),
111
+ license: str = typer.Option(
112
+ "unknown", help="License identifier, e.g. mit, apache-2.0, cc-by-4.0"
113
+ ),
114
+ institution: str = typer.Option("", help="Publishing institution"),
115
+ dua: bool = typer.Option(False, help="Require a Data Use Agreement"),
116
+ compliance: Optional[List[str]] = typer.Option(
117
+ None, help="Compliance badges: hipaa, gdpr, fda-track, ce-mark, fhir"
118
+ ),
119
+ tags: Optional[List[str]] = typer.Option(None, help="Free-form tags"),
120
+ num_patients: Optional[int] = typer.Option(
121
+ None, help="Number of patients in the dataset"
122
+ ),
123
+ num_records: Optional[int] = typer.Option(None, help="Number of records"),
124
+ ):
125
+ """Upload a local dataset (files + metadata) to Meditopia."""
126
+ owner, collection_path, slug = _parse_to(to)
127
+
128
+ rprint(
129
+ f"[bold]Creating dataset[/bold] [cyan]{owner}/{collection_path}/{slug}[/cyan] …"
130
+ )
131
+
132
+ payload = {
133
+ "name": slug,
134
+ "description": description,
135
+ "modality": modality,
136
+ "visibility": privacy,
137
+ "license": license,
138
+ "institution": institution,
139
+ "requires_dua": dua,
140
+ "compliance": compliance or [],
141
+ "tags": tags or [],
142
+ "collection_path": collection_path,
143
+ "num_patients": num_patients,
144
+ "num_records": num_records,
145
+ }
146
+
147
+ try:
148
+ ds = client.create_dataset(payload)
149
+ except Exception as exc:
150
+ rprint(f"[red]Failed to create dataset:[/red] {exc}")
151
+ raise typer.Exit(1)
152
+
153
+ _abort_if_error(ds)
154
+ rprint(f"[green]✓[/green] Dataset created: [bold]{ds['full_path']}[/bold]")
155
+
156
+ # Upload files
157
+ rprint(f"Collecting files from [cyan]{local_path}[/cyan] …")
158
+ try:
159
+ file_tuples = collect_files(local_path)
160
+ except FileNotFoundError:
161
+ rprint(f"[red]Path not found:[/red] {local_path}")
162
+ raise typer.Exit(1)
163
+
164
+ if not file_tuples:
165
+ rprint("[yellow]No files found — dataset created with no files.[/yellow]")
166
+ return
167
+
168
+ rprint(f"Uploading [bold]{len(file_tuples)}[/bold] file(s) …")
169
+ try:
170
+ result = client.upload_dataset_files(owner, ds["slug"], file_tuples)
171
+ except Exception as exc:
172
+ rprint(f"[red]Upload failed:[/red] {exc}")
173
+ raise typer.Exit(1)
174
+
175
+ _print_upload_summary(result)
176
+ rprint(
177
+ f"\n[green]✓ Done.[/green] View at: {config.get_api_url().replace('/api/v1', '')}/datasets/{ds['full_path']}"
178
+ )
179
+
180
+
181
+ # ── add model ─────────────────────────────────────────────────────────────────
182
+
183
+
184
+ @add_app.command("model")
185
+ def add_model(
186
+ local_path: str = typer.Argument(..., help="Local file or directory to upload"),
187
+ to: str = typer.Option(..., help="Destination path: owner[/collection/...]/name"),
188
+ privacy: str = typer.Option("public", help="public | private | restricted"),
189
+ task: str = typer.Option(
190
+ ...,
191
+ help="diagnosis | radiology | pathology | genomics | clinical-nlp | drug-discovery | risk-scoring | cardiology | neurology | dermatology | ophthalmology | other",
192
+ ),
193
+ framework: str = typer.Option(
194
+ ..., help="pytorch | tensorflow | jax | onnx | scikit-learn | other"
195
+ ),
196
+ modality: str = typer.Option(
197
+ "",
198
+ help="Imaging modality if applicable: CT | MRI | X-ray | PET | Ultrasound | other",
199
+ ),
200
+ description: str = typer.Option("", help="Short description of the model"),
201
+ license: str = typer.Option("unknown", help="License identifier"),
202
+ institution: str = typer.Option("", help="Publishing institution"),
203
+ compliance: Optional[List[str]] = typer.Option(None, help="Compliance badges"),
204
+ tags: Optional[List[str]] = typer.Option(None, help="Free-form tags"),
205
+ auc: Optional[float] = typer.Option(None, help="Primary AUC metric (0–1)"),
206
+ ):
207
+ """Upload a local model (weights + metadata) to Meditopia."""
208
+ owner, collection_path, slug = _parse_to(to)
209
+
210
+ rprint(
211
+ f"[bold]Creating model[/bold] [cyan]{owner}/{collection_path}/{slug}[/cyan] …"
212
+ )
213
+
214
+ payload = {
215
+ "name": slug,
216
+ "description": description,
217
+ "task": task,
218
+ "framework": framework,
219
+ "modality": modality,
220
+ "visibility": privacy,
221
+ "license": license,
222
+ "institution": institution,
223
+ "compliance": compliance or [],
224
+ "tags": tags or [],
225
+ "collection_path": collection_path,
226
+ "auc": auc,
227
+ }
228
+
229
+ try:
230
+ model = client.create_model(payload)
231
+ except Exception as exc:
232
+ rprint(f"[red]Failed to create model:[/red] {exc}")
233
+ raise typer.Exit(1)
234
+
235
+ _abort_if_error(model)
236
+ rprint(f"[green]✓[/green] Model created: [bold]{model['full_path']}[/bold]")
237
+
238
+ rprint(f"Collecting files from [cyan]{local_path}[/cyan] …")
239
+ try:
240
+ file_tuples = collect_files(local_path)
241
+ except FileNotFoundError:
242
+ rprint(f"[red]Path not found:[/red] {local_path}")
243
+ raise typer.Exit(1)
244
+
245
+ if not file_tuples:
246
+ rprint("[yellow]No files found — model created with no files.[/yellow]")
247
+ return
248
+
249
+ rprint(f"Uploading [bold]{len(file_tuples)}[/bold] file(s) …")
250
+ try:
251
+ result = client.upload_model_files(owner, model["slug"], file_tuples)
252
+ except Exception as exc:
253
+ rprint(f"[red]Upload failed:[/red] {exc}")
254
+ raise typer.Exit(1)
255
+
256
+ _print_upload_summary(result)
257
+ rprint(
258
+ f"\n[green]✓ Done.[/green] View at: {config.get_api_url().replace('/api/v1', '')}/models/{model['full_path']}"
259
+ )
260
+
261
+
262
+ # ── download dataset ──────────────────────────────────────────────────────────
263
+
264
+
265
+ @download_app.command("dataset")
266
+ def download_dataset(
267
+ path: str = typer.Argument(..., help="Dataset path: owner/slug"),
268
+ to: str = typer.Option(".", help="Directory to save the downloaded ZIP"),
269
+ extract: bool = typer.Option(
270
+ False, "--extract/--no-extract", help="Extract the ZIP after downloading"
271
+ ),
272
+ ):
273
+ """Download all files of a dataset as a ZIP."""
274
+ owner, slug = _parse_owner_slug(path)
275
+ dest = _resolve_dest(to, slug)
276
+
277
+ rprint(f"Downloading dataset [cyan]{owner}/{slug}[/cyan] → [bold]{dest}[/bold] …")
278
+ try:
279
+ total = client.download_dataset(owner, slug, dest)
280
+ except Exception as exc:
281
+ rprint(f"[red]Download failed:[/red] {exc}")
282
+ raise typer.Exit(1)
283
+
284
+ rprint(f"[green]✓[/green] {total / 1_048_576:.2f} MB saved to [bold]{dest}[/bold]")
285
+ if extract:
286
+ _extract_zip(dest, to)
287
+
288
+
289
+ # ── download model ────────────────────────────────────────────────────────────
290
+
291
+
292
+ @download_app.command("model")
293
+ def download_model(
294
+ path: str = typer.Argument(..., help="Model path: owner/slug"),
295
+ to: str = typer.Option(".", help="Directory to save the downloaded ZIP"),
296
+ extract: bool = typer.Option(
297
+ False, "--extract/--no-extract", help="Extract the ZIP after downloading"
298
+ ),
299
+ ):
300
+ """Download all files of a model as a ZIP."""
301
+ owner, slug = _parse_owner_slug(path)
302
+ dest = _resolve_dest(to, slug)
303
+
304
+ rprint(f"Downloading model [cyan]{owner}/{slug}[/cyan] → [bold]{dest}[/bold] …")
305
+ try:
306
+ total = client.download_model(owner, slug, dest)
307
+ except Exception as exc:
308
+ rprint(f"[red]Download failed:[/red] {exc}")
309
+ raise typer.Exit(1)
310
+
311
+ rprint(f"[green]✓[/green] {total / 1_048_576:.2f} MB saved to [bold]{dest}[/bold]")
312
+ if extract:
313
+ _extract_zip(dest, to)
314
+
315
+
316
+ # ── download function ─────────────────────────────────────────────────────────
317
+
318
+ _LANG_EXT = {"python": "py", "rust": "rs", "api": "json"}
319
+
320
+
321
+ @download_app.command("function")
322
+ def download_function(
323
+ name: str = typer.Argument(..., help="Function name"),
324
+ to: str = typer.Option(".", help="Directory to save the function script + metadata"),
325
+ ):
326
+ """Download a function's script (+ a .meta.json sidecar) for use in Airflow DAGs."""
327
+ import os
328
+ import json
329
+
330
+ rprint(f"Fetching function [cyan]{name}[/cyan] …")
331
+ try:
332
+ fn = client.get_function(name)
333
+ except Exception as exc:
334
+ rprint(f"[red]Download failed:[/red] {exc}")
335
+ raise typer.Exit(1)
336
+
337
+ _abort_if_error(fn)
338
+
339
+ os.makedirs(to, exist_ok=True)
340
+ ext = _LANG_EXT.get(fn["language"], "txt")
341
+ script_path = os.path.join(to, f"{name}.{ext}")
342
+ meta_path = os.path.join(to, f"{name}.meta.json")
343
+
344
+ with open(script_path, "w") as f:
345
+ f.write(fn["script"])
346
+
347
+ with open(meta_path, "w") as f:
348
+ json.dump(
349
+ {
350
+ "name": fn["name"],
351
+ "display_name": fn["display_name"],
352
+ "description": fn["description"],
353
+ "language": fn["language"],
354
+ "tags": fn["tags"],
355
+ "input_ports": fn["input_ports"],
356
+ "output_ports": fn["output_ports"],
357
+ },
358
+ f,
359
+ indent=2,
360
+ )
361
+
362
+ rprint(f"[green]✓[/green] Saved [bold]{script_path}[/bold] and [bold]{meta_path}[/bold]")
363
+ if fn["language"] == "rust":
364
+ rprint(
365
+ "[yellow]Note:[/yellow] rust functions are downloaded as source only — "
366
+ "there is no build/compile step yet, so this script cannot be executed "
367
+ "via functions/_run.py until that's implemented (known gap, carried over "
368
+ "from the old pipelines executor)."
369
+ )
370
+
371
+
372
+ # ── helpers ───────────────────────────────────────────────────────────────────
373
+
374
+
375
+ def _parse_owner_slug(path: str) -> tuple[str, str]:
376
+ parts = path.strip("/").split("/")
377
+ if len(parts) < 2:
378
+ rprint(f"[red]Path must be owner/slug, got: {path!r}[/red]")
379
+ raise typer.Exit(1)
380
+ return parts[0], parts[-1]
381
+
382
+
383
+ def _resolve_dest(directory: str, slug: str) -> str:
384
+ import os
385
+
386
+ os.makedirs(directory, exist_ok=True)
387
+ return os.path.join(directory, f"{slug}.zip")
388
+
389
+
390
+ def _extract_zip(zip_path: str, dest_dir: str) -> None:
391
+ import zipfile
392
+
393
+ rprint(f"Extracting [bold]{zip_path}[/bold] …")
394
+ with zipfile.ZipFile(zip_path, "r") as zf:
395
+ zf.extractall(dest_dir)
396
+ rprint(f"[green]✓[/green] Extracted to [bold]{dest_dir}[/bold]")
397
+
398
+
399
+ def _print_upload_summary(result: dict) -> None:
400
+ table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
401
+ table.add_column("File")
402
+ table.add_column("Status", style="green")
403
+ for f in result.get("files", []):
404
+ table.add_row(f, "✓ uploaded")
405
+ rprint(table)
406
+ mb = result.get("bytes", 0) / 1_048_576
407
+ rprint(
408
+ f"Total: [bold]{result.get('uploaded', 0)}[/bold] files · [bold]{mb:.2f} MB[/bold]"
409
+ )
410
+
411
+
412
+ if __name__ == "__main__":
413
+ app()
meditopia/client.py ADDED
@@ -0,0 +1,116 @@
1
+ """
2
+ HTTP client for the Meditopia API.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import httpx
8
+ from typing import Any
9
+
10
+ from . import config
11
+
12
+
13
+ def _headers() -> dict[str, str]:
14
+ token = config.get_token()
15
+ if not token:
16
+ raise RuntimeError("Not authenticated. Run: medi login --token <your-token>")
17
+ return {"Authorization": f"Bearer {token}"}
18
+
19
+
20
+ def _base() -> str:
21
+ return config.get_api_url().rstrip("/")
22
+
23
+
24
+ # ── Auth ──────────────────────────────────────────────────────────────────────
25
+
26
+
27
+ def whoami() -> dict[str, Any]:
28
+ r = httpx.get(f"{_base()}/auth/me", headers=_headers(), timeout=10)
29
+ r.raise_for_status()
30
+ return r.json()
31
+
32
+
33
+ # ── Datasets ──────────────────────────────────────────────────────────────────
34
+
35
+
36
+ def create_dataset(payload: dict[str, Any]) -> dict[str, Any]:
37
+ r = httpx.post(f"{_base()}/datasets", json=payload, headers=_headers(), timeout=30)
38
+ r.raise_for_status()
39
+ return r.json()
40
+
41
+
42
+ def upload_dataset_files(
43
+ owner: str, slug: str, file_tuples: list[tuple]
44
+ ) -> dict[str, Any]:
45
+ """
46
+ file_tuples: list of (filename, file_bytes, content_type)
47
+ """
48
+ files = [("files", (name, data, ct)) for name, data, ct in file_tuples]
49
+ r = httpx.post(
50
+ f"{_base()}/datasets/{owner}/{slug}/upload",
51
+ headers=_headers(),
52
+ files=files,
53
+ timeout=300,
54
+ )
55
+ r.raise_for_status()
56
+ return r.json()
57
+
58
+
59
+ # ── Models ────────────────────────────────────────────────────────────────────
60
+
61
+
62
+ def create_model(payload: dict[str, Any]) -> dict[str, Any]:
63
+ r = httpx.post(f"{_base()}/models", json=payload, headers=_headers(), timeout=30)
64
+ r.raise_for_status()
65
+ return r.json()
66
+
67
+
68
+ def upload_model_files(
69
+ owner: str, slug: str, file_tuples: list[tuple]
70
+ ) -> dict[str, Any]:
71
+ files = [("files", (name, data, ct)) for name, data, ct in file_tuples]
72
+ r = httpx.post(
73
+ f"{_base()}/models/{owner}/{slug}/upload",
74
+ headers=_headers(),
75
+ files=files,
76
+ timeout=300,
77
+ )
78
+ r.raise_for_status()
79
+ return r.json()
80
+
81
+
82
+ def download_model(owner: str, slug: str, dest: str) -> int:
83
+ """Stream the model ZIP to *dest*. Returns bytes written."""
84
+ return _stream_zip(f"{_base()}/models/{owner}/{slug}/download", dest)
85
+
86
+
87
+ # ── Functions ──────────────────────────────────────────────────────────────────
88
+
89
+
90
+ def get_function(name: str) -> dict[str, Any]:
91
+ """Fetch a function's metadata + script by name. Public endpoint, no auth needed."""
92
+ r = httpx.get(f"{_base()}/functions/{name}", timeout=15)
93
+ r.raise_for_status()
94
+ return r.json()
95
+
96
+
97
+ # ── Downloads (shared) ────────────────────────────────────────────────────────
98
+
99
+
100
+ def download_dataset(owner: str, slug: str, dest: str) -> int:
101
+ """Stream the dataset ZIP to *dest*. Returns bytes written."""
102
+ return _stream_zip(f"{_base()}/datasets/{owner}/{slug}/download", dest)
103
+
104
+
105
+ def _stream_zip(url: str, dest: str) -> int:
106
+ """Stream a ZIP from *url* into file *dest*, return total bytes."""
107
+ with httpx.stream(
108
+ "GET", url, headers=_headers(), timeout=600, follow_redirects=True
109
+ ) as r:
110
+ r.raise_for_status()
111
+ total = 0
112
+ with open(dest, "wb") as fh:
113
+ for chunk in r.iter_bytes(chunk_size=1024 * 256):
114
+ fh.write(chunk)
115
+ total += len(chunk)
116
+ return total
meditopia/config.py ADDED
@@ -0,0 +1,42 @@
1
+ """
2
+ Manages ~/.meditopia/config.json — stores token and API URL.
3
+ """
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ CONFIG_DIR = Path.home() / ".meditopia"
9
+ CONFIG_FILE = CONFIG_DIR / "config.json"
10
+
11
+ DEFAULT_API_URL = "http://localhost:8000/api/v1"
12
+
13
+
14
+ def load() -> dict:
15
+ if CONFIG_FILE.exists():
16
+ return json.loads(CONFIG_FILE.read_text())
17
+ return {}
18
+
19
+
20
+ def save(data: dict) -> None:
21
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
22
+ CONFIG_FILE.write_text(json.dumps(data, indent=2))
23
+
24
+
25
+ def get_token() -> str | None:
26
+ return load().get("token")
27
+
28
+
29
+ def get_api_url() -> str:
30
+ return load().get("api_url", DEFAULT_API_URL)
31
+
32
+
33
+ def set_credentials(token: str, api_url: str = DEFAULT_API_URL) -> None:
34
+ cfg = load()
35
+ cfg["token"] = token
36
+ cfg["api_url"] = api_url
37
+ save(cfg)
38
+
39
+
40
+ def clear() -> None:
41
+ if CONFIG_FILE.exists():
42
+ CONFIG_FILE.unlink()
meditopia/uploader.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ Walks a local directory and returns file tuples ready for upload.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import mimetypes
8
+ import os
9
+ from pathlib import Path
10
+
11
+
12
+ def collect_files(local_path: str) -> list[tuple[str, bytes, str]]:
13
+ """
14
+ Walk *local_path* (file or directory) and return a list of
15
+ (relative_filename, file_bytes, mime_type) tuples.
16
+ """
17
+ root = Path(local_path).resolve()
18
+ tuples: list[tuple[str, bytes, str]] = []
19
+
20
+ if root.is_file():
21
+ mime = mimetypes.guess_type(root.name)[0] or "application/octet-stream"
22
+ tuples.append((root.name, root.read_bytes(), mime))
23
+ return tuples
24
+
25
+ for dirpath, _, filenames in os.walk(root):
26
+ for fname in filenames:
27
+ abs_path = Path(dirpath) / fname
28
+ rel_path = str(abs_path.relative_to(root))
29
+ mime = mimetypes.guess_type(fname)[0] or "application/octet-stream"
30
+ tuples.append((rel_path, abs_path.read_bytes(), mime))
31
+
32
+ return tuples
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: meditopia-cli
3
+ Version: 0.1.0
4
+ Summary: CLI tool for uploading models and datasets to Meditopia
5
+ License: MIT
6
+ Project-URL: Homepage, https://meditopia.io
7
+ Project-URL: Repository, https://github.com/meditopia/meditopia-cli
8
+ Keywords: meditopia,cli,medical-ai,machine-learning
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: typer>=0.12
15
+ Requires-Dist: httpx>=0.27
16
+ Requires-Dist: rich>=13
17
+
18
+ # meditopia-cli
19
+
20
+ Official CLI for [MedOpia](https://meditopia.io) — upload and manage clinical AI models and datasets from your terminal.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install meditopia-cli
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```bash
31
+ medi login --token <your-api-token>
32
+ medi upload model ./my-model --name "chest-xray-classifier"
33
+ medi upload dataset ./my-dataset --name "mimic-cxr-subset"
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ ```
39
+ medi --help
40
+ ```
41
+
42
+ ## Getting an API token
43
+
44
+ 1. Log in to [meditopia.io](https://meditopia.io)
45
+ 2. Go to **Settings → API Tokens**
46
+ 3. Create a new token and copy it
47
+ 4. Run `medi login --token <token>`
@@ -0,0 +1,10 @@
1
+ meditopia/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ meditopia/cli.py,sha256=QAEyw-6VU-wBqv6J8ce5PNob2sYqK0rSQCgrrLaFLyk,15294
3
+ meditopia/client.py,sha256=t5L00JrcxM1ie9f6o7vO-NCVyPtvXre9mOaFlybkDiY,3984
4
+ meditopia/config.py,sha256=P58V4uF1MuyldcKAWDfx6F_1XotK8nfehN_Dmat7t3M,877
5
+ meditopia/uploader.py,sha256=I6wGk9kyRPGVqktGp-48pKfNQh5yEaeN6kttwVv-SPY,984
6
+ meditopia_cli-0.1.0.dist-info/METADATA,sha256=mA-AgRf8-005M-xtTufe3EAT2j1_3HoKIrNZT4QAtgg,1184
7
+ meditopia_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ meditopia_cli-0.1.0.dist-info/entry_points.txt,sha256=3_F6FNHXSSwHq9GnQfFU0Rqw1r70nAION7og6t938g4,43
9
+ meditopia_cli-0.1.0.dist-info/top_level.txt,sha256=YRMv_tbnISbMxeZwdXvMtTuWIz3gdmjrefeU7UcwBOU,10
10
+ meditopia_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ medi = meditopia.cli:app
@@ -0,0 +1 @@
1
+ meditopia