pyadata-cli 0.5.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.
adata/cli.py ADDED
@@ -0,0 +1,1139 @@
1
+ """CLI for AnnData stores (.h5ad / .zarr) with export and import subcommands."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, List
5
+
6
+ from rich.console import Console
7
+ import typer
8
+
9
+ from adata.commands import (
10
+ MERGE_STRATEGIES,
11
+ concat_stores,
12
+ create_store,
13
+ split_store,
14
+ list_store,
15
+ show_info,
16
+ subset_h5ad,
17
+ export_mtx,
18
+ export_npy,
19
+ export_json,
20
+ export_table,
21
+ )
22
+
23
+ from adata.commands import export_image as export_image_cmd
24
+
25
+ app = typer.Typer(
26
+ help="Streaming CLI for huge AnnData .h5ad and .zarr stores "
27
+ "(view, ls, subset, split, concat, export, import)."
28
+ )
29
+ # Use stderr for status/progress to keep stdout clean for data output
30
+ # force_terminal=True ensures Rich output is visible even in non-TTY environments
31
+ console = Console(stderr=True, force_terminal=True)
32
+ # Results go to stdout so they can be piped; status and errors stay on stderr.
33
+ out_console = Console()
34
+
35
+
36
+ def _version_callback(value: bool) -> None:
37
+ if value:
38
+ from adata import __version__
39
+
40
+ out_console.print(__version__, highlight=False)
41
+ raise typer.Exit()
42
+
43
+
44
+ # Create sub-apps for export and import
45
+ export_app = typer.Typer(help="Export objects from an AnnData store.")
46
+ import_app = typer.Typer(help="Import objects into an AnnData store.")
47
+ app.add_typer(export_app, name="export")
48
+ app.add_typer(import_app, name="import")
49
+
50
+
51
+ @app.callback()
52
+ def _root(
53
+ version: bool = typer.Option(
54
+ False,
55
+ "--version",
56
+ "-V",
57
+ help="Show the installed version and exit.",
58
+ callback=_version_callback,
59
+ is_eager=True,
60
+ ),
61
+ ) -> None:
62
+ """Streaming CLI for huge AnnData .h5ad and .zarr stores."""
63
+
64
+
65
+ # ============================================================================
66
+ # VIEW command
67
+ # ============================================================================
68
+ @app.command("view")
69
+ def view(
70
+ file: Path = typer.Argument(
71
+ ...,
72
+ help="Path to the .h5ad/.zarr store",
73
+ exists=True,
74
+ readable=True,
75
+ dir_okay=True,
76
+ file_okay=True,
77
+ ),
78
+ entry: Optional[str] = typer.Argument(
79
+ None,
80
+ help="Entry path to inspect (e.g., 'obsm/X_pca', 'X', 'uns')",
81
+ ),
82
+ tree: bool = typer.Option(
83
+ False,
84
+ "--tree",
85
+ "-t",
86
+ help="Show a tree of all entries",
87
+ ),
88
+ depth: int = typer.Option(
89
+ None,
90
+ "--depth",
91
+ "-d",
92
+ help="Maximum recursion depth for tree display (only with --tree)",
93
+ ),
94
+ ) -> None:
95
+ """
96
+ Show high-level information about an AnnData store.
97
+
98
+ Use --tree to see a tree of all entries.
99
+ Pass an entry path to inspect a specific entry in detail.
100
+
101
+ Examples:
102
+ adata view data.h5ad
103
+ adata view --tree data.h5ad
104
+ adata view data.h5ad obsm/X_pca
105
+ """
106
+ try:
107
+ show_info(
108
+ file,
109
+ console,
110
+ show_types=tree,
111
+ depth=depth,
112
+ entry_path=entry,
113
+ out_console=out_console,
114
+ )
115
+ except Exception as e:
116
+ console.print(f"[bold red]Error:[/] {e}")
117
+ raise typer.Exit(code=1)
118
+
119
+
120
+ @app.command("info", hidden=True)
121
+ def info(
122
+ file: Path = typer.Argument(
123
+ ...,
124
+ help="Path to the .h5ad/.zarr store",
125
+ exists=True,
126
+ readable=True,
127
+ dir_okay=True,
128
+ file_okay=True,
129
+ ),
130
+ entry: Optional[str] = typer.Argument(
131
+ None,
132
+ help="Entry path to inspect (e.g., 'obsm/X_pca', 'X', 'uns')",
133
+ ),
134
+ tree: bool = typer.Option(False, "--tree", "-t", help="Show a tree of all entries"),
135
+ depth: int = typer.Option(
136
+ None, "--depth", "-d", help="Maximum recursion depth for tree display"
137
+ ),
138
+ ) -> None:
139
+ """Deprecated alias for 'view'. Removed in 1.0.0."""
140
+ console.print(
141
+ "[yellow]Warning:[/] 'info' is deprecated and will be removed in 1.0.0; "
142
+ "use 'view' instead.",
143
+ )
144
+ view(file=file, entry=entry, tree=tree, depth=depth)
145
+
146
+
147
+ # ============================================================================
148
+ # CREATE command
149
+ # ============================================================================
150
+ @app.command("create")
151
+ def create(
152
+ output: Path = typer.Argument(
153
+ ...,
154
+ help="Path of the store to create (.h5ad or .zarr)",
155
+ dir_okay=True,
156
+ file_okay=True,
157
+ ),
158
+ n_obs: Optional[int] = typer.Option(
159
+ None, "--n-obs", help="Number of observations (cells)"
160
+ ),
161
+ n_var: Optional[int] = typer.Option(
162
+ None, "--n-var", help="Number of variables (genes)"
163
+ ),
164
+ obs_names: Optional[Path] = typer.Option(
165
+ None,
166
+ "--obs-names",
167
+ help="File of obs names, one per line (sets --n-obs)",
168
+ exists=True,
169
+ readable=True,
170
+ ),
171
+ var_names: Optional[Path] = typer.Option(
172
+ None,
173
+ "--var-names",
174
+ help="File of var names, one per line (sets --n-var)",
175
+ exists=True,
176
+ readable=True,
177
+ ),
178
+ zarr_format: Optional[int] = typer.Option(
179
+ None, "--zarr-format", help="Zarr spec version to write (2 or 3)"
180
+ ),
181
+ force: bool = typer.Option(
182
+ False, "--force", "-f", help="Overwrite an existing store"
183
+ ),
184
+ ) -> None:
185
+ """
186
+ Create a new, empty AnnData store.
187
+
188
+ The result is a valid AnnData object straight away; fill it in with
189
+ `adata import`. Give each axis either a size or a file of names.
190
+
191
+ Examples:
192
+ adata create out.h5ad --n-obs 5000 --n-var 2000
193
+ adata create out.zarr --obs-names cells.txt --var-names genes.txt
194
+ adata import sparse out.h5ad X counts.mtx --inplace
195
+ """
196
+ if zarr_format is not None and zarr_format not in (2, 3):
197
+ console.print("[bold red]Error:[/] --zarr-format must be 2 or 3.")
198
+ raise typer.Exit(code=1)
199
+
200
+ try:
201
+ create_store(
202
+ output,
203
+ console,
204
+ n_obs=n_obs,
205
+ n_var=n_var,
206
+ obs_names=obs_names,
207
+ var_names=var_names,
208
+ zarr_format=zarr_format,
209
+ force=force,
210
+ )
211
+ except Exception as e:
212
+ console.print(f"[bold red]Error:[/] {e}")
213
+ raise typer.Exit(code=1)
214
+
215
+
216
+ # ============================================================================
217
+ # LS command
218
+ # ============================================================================
219
+ @app.command("ls")
220
+ def ls(
221
+ file: Path = typer.Argument(
222
+ ...,
223
+ help="Path to any .h5ad/.zarr/.h5 store",
224
+ exists=True,
225
+ readable=True,
226
+ dir_okay=True,
227
+ file_okay=True,
228
+ ),
229
+ entry: Optional[str] = typer.Argument(
230
+ None, help="Only list below this path (e.g. 'obsm', 'uns/spatial')"
231
+ ),
232
+ depth: Optional[int] = typer.Option(
233
+ None, "--depth", "-d", help="Maximum depth to descend"
234
+ ),
235
+ long: bool = typer.Option(
236
+ False, "--long", "-l", help="Show type, shape, dtype and encoding"
237
+ ),
238
+ plain: bool = typer.Option(
239
+ False, "-1", "--plain", help="One bare path per line, for piping"
240
+ ),
241
+ ) -> None:
242
+ """
243
+ List the contents of any HDF5 or Zarr store.
244
+
245
+ Makes no AnnData assumptions, so it also works on .loom and plain .h5
246
+ files. Use -1 to pipe paths into other tools.
247
+
248
+ Examples:
249
+ adata ls data.h5ad
250
+ adata ls data.h5ad --long
251
+ adata ls data.h5ad obsm --depth 1
252
+ adata ls data.h5ad -1 | grep spatial
253
+ """
254
+ try:
255
+ list_store(
256
+ file,
257
+ out_console,
258
+ entry_path=entry,
259
+ depth=depth,
260
+ long=long,
261
+ plain=plain,
262
+ )
263
+ except Exception as e:
264
+ console.print(f"[bold red]Error:[/] {e}")
265
+ raise typer.Exit(code=1)
266
+
267
+
268
+ # ============================================================================
269
+ # SUBSET command
270
+ # ============================================================================
271
+ @app.command()
272
+ def subset(
273
+ file: Path = typer.Argument(
274
+ ...,
275
+ help="Input .h5ad/.zarr",
276
+ exists=True,
277
+ readable=True,
278
+ dir_okay=True,
279
+ file_okay=True,
280
+ ),
281
+ output: Optional[Path] = typer.Option(
282
+ None,
283
+ "--output",
284
+ "-o",
285
+ help="Output .h5ad/.zarr path. Required unless --inplace.",
286
+ dir_okay=True,
287
+ file_okay=True,
288
+ ),
289
+ inplace: bool = typer.Option(
290
+ False,
291
+ "--inplace",
292
+ help="Modify source file directly.",
293
+ ),
294
+ obs: Optional[Path] = typer.Option(
295
+ None,
296
+ "--obs",
297
+ help="File with obs names (one per line)",
298
+ exists=True,
299
+ readable=True,
300
+ ),
301
+ var: Optional[Path] = typer.Option(
302
+ None,
303
+ "--var",
304
+ help="File with var names (one per line)",
305
+ exists=True,
306
+ readable=True,
307
+ ),
308
+ obs_query: Optional[str] = typer.Option(
309
+ None,
310
+ "--obs-query",
311
+ "-q",
312
+ help="Keep obs matching an expression, e.g. \"cluster == Cortex_2\"",
313
+ ),
314
+ var_query: Optional[str] = typer.Option(
315
+ None,
316
+ "--var-query",
317
+ help="Keep var matching an expression, e.g. \"highly_variable == True\"",
318
+ ),
319
+ chunk_rows: int = typer.Option(
320
+ 1024,
321
+ "--chunk",
322
+ "-C",
323
+ "--chunk-rows",
324
+ "-r",
325
+ help="Row chunk size for dense matrices",
326
+ ),
327
+ zarr_format: Optional[int] = typer.Option(
328
+ None,
329
+ "--zarr-format",
330
+ help="Zarr spec version to write (defaults to the source store's)",
331
+ ),
332
+ ) -> None:
333
+ """
334
+ Subset an AnnData store by obs and/or var.
335
+
336
+ Select either by name list (--obs/--var) or by expression
337
+ (--obs-query/--var-query). Expressions support ==, !=, <, <=, >, >=, in,
338
+ not in, and, or, not, and parentheses.
339
+
340
+ Examples:
341
+ adata subset data.h5ad -o out.h5ad --obs barcodes.txt
342
+ adata subset data.h5ad -o out.h5ad --obs-query "cluster == Cortex_2"
343
+ adata subset data.h5ad -o out.h5ad -q "n_counts > 1000 and cluster in A,B"
344
+ adata subset data.h5ad -o out.h5ad --var-query "highly_variable == True"
345
+ """
346
+ if obs is None and var is None and obs_query is None and var_query is None:
347
+ console.print(
348
+ "[bold red]Error:[/] Provide at least one of --obs, --var, "
349
+ "--obs-query or --var-query.",
350
+ )
351
+ raise typer.Exit(code=1)
352
+
353
+ if not inplace and output is None:
354
+ console.print(
355
+ "[bold red]Error:[/] Output file is required. "
356
+ "Use --output/-o or --inplace.",
357
+ )
358
+ raise typer.Exit(code=1)
359
+
360
+ try:
361
+ subset_h5ad(
362
+ file=file,
363
+ output=output,
364
+ obs_file=obs,
365
+ var_file=var,
366
+ chunk_rows=chunk_rows,
367
+ console=console,
368
+ inplace=inplace,
369
+ obs_query=obs_query,
370
+ var_query=var_query,
371
+ zarr_format=zarr_format,
372
+ )
373
+ except Exception as e:
374
+ console.print(f"[bold red]Error:[/] {e}")
375
+ raise typer.Exit(code=1)
376
+
377
+
378
+ # ============================================================================
379
+ # CONCAT command
380
+ # ============================================================================
381
+ @app.command("concat")
382
+ def concat(
383
+ files: List[Path] = typer.Argument(
384
+ ...,
385
+ help="Two or more .h5ad/.zarr stores to concatenate",
386
+ exists=True,
387
+ readable=True,
388
+ dir_okay=True,
389
+ file_okay=True,
390
+ ),
391
+ output: Path = typer.Option(
392
+ ..., "--output", "-o", help="Output .h5ad/.zarr path", dir_okay=True
393
+ ),
394
+ join: str = typer.Option(
395
+ "inner",
396
+ "--join",
397
+ "-j",
398
+ help="Align var by intersection ('inner') or union ('outer')",
399
+ ),
400
+ label: Optional[str] = typer.Option(
401
+ None, "--label", help="Add an obs column recording which input each cell came from"
402
+ ),
403
+ keys: Optional[str] = typer.Option(
404
+ None,
405
+ "--keys",
406
+ help="Comma separated names for the inputs (defaults to their filenames)",
407
+ ),
408
+ index_unique: Optional[str] = typer.Option(
409
+ None,
410
+ "--index-unique",
411
+ help="Delimiter used to suffix obs names with their key, e.g. '-'",
412
+ ),
413
+ merge: Optional[str] = typer.Option(
414
+ None,
415
+ "--merge",
416
+ help="How to reconcile var columns: same, unique, first, only (default: drop)",
417
+ ),
418
+ uns_merge: Optional[str] = typer.Option(
419
+ None,
420
+ "--uns-merge",
421
+ help="How to reconcile uns: same, unique, first, only (default: drop)",
422
+ ),
423
+ fill_value: float = typer.Option(
424
+ 0.0, "--fill-value", help="Value for dense cells introduced by an outer join"
425
+ ),
426
+ chunk_rows: int = typer.Option(
427
+ 1024, "--chunk", "-C", help="Row chunk size while streaming"
428
+ ),
429
+ zarr_format: Optional[int] = typer.Option(
430
+ None, "--zarr-format", help="Zarr spec version to write (2 or 3)"
431
+ ),
432
+ ) -> None:
433
+ """
434
+ Concatenate stores along the obs axis.
435
+
436
+ Streams each input in turn, so memory use is set by --chunk rather than by
437
+ the size of the inputs. obsp, varp and raw are not carried over.
438
+
439
+ Examples:
440
+ adata concat a.h5ad b.h5ad -o merged.h5ad
441
+ adata concat *.h5ad -o merged.h5ad --join outer --label sample
442
+ adata concat a.h5ad b.h5ad -o m.h5ad --keys a,b --index-unique - --uns-merge same
443
+ """
444
+ for name, value in (("--merge", merge), ("--uns-merge", uns_merge)):
445
+ if value is not None and value not in MERGE_STRATEGIES:
446
+ console.print(
447
+ f"[bold red]Error:[/] {name} must be one of: "
448
+ f"{', '.join(MERGE_STRATEGIES)}"
449
+ )
450
+ raise typer.Exit(code=1)
451
+
452
+ try:
453
+ concat_stores(
454
+ files,
455
+ output,
456
+ console,
457
+ join=join,
458
+ label=label,
459
+ keys=[k.strip() for k in keys.split(",")] if keys else None,
460
+ index_unique=index_unique,
461
+ merge=merge,
462
+ uns_merge=uns_merge,
463
+ fill_value=fill_value,
464
+ chunk_rows=chunk_rows,
465
+ zarr_format=zarr_format,
466
+ )
467
+ except Exception as e:
468
+ console.print(f"[bold red]Error:[/] {e}")
469
+ raise typer.Exit(code=1)
470
+
471
+
472
+ # ============================================================================
473
+ # SPLIT command
474
+ # ============================================================================
475
+ @app.command("split")
476
+ def split(
477
+ file: Path = typer.Argument(
478
+ ...,
479
+ help="Input .h5ad/.zarr",
480
+ exists=True,
481
+ readable=True,
482
+ dir_okay=True,
483
+ file_okay=True,
484
+ ),
485
+ by: str = typer.Option(
486
+ ..., "--by", "-b", help="Column to split on (e.g. 'sample', 'cell_type')"
487
+ ),
488
+ output_dir: Path = typer.Option(
489
+ ..., "--output-dir", "-o", help="Directory to write the split stores into"
490
+ ),
491
+ axis: str = typer.Option(
492
+ "obs", "--axis", help="Axis the column belongs to ('obs' or 'var')"
493
+ ),
494
+ suffix: Optional[str] = typer.Option(
495
+ None,
496
+ "--suffix",
497
+ help="Output extension (defaults to the source store's format)",
498
+ ),
499
+ min_size: int = typer.Option(
500
+ 1, "--min-size", help="Skip groups with fewer rows than this"
501
+ ),
502
+ manifest: bool = typer.Option(
503
+ True, "--manifest/--no-manifest", help="Write a CSV manifest of the outputs"
504
+ ),
505
+ dry_run: bool = typer.Option(
506
+ False, "--dry-run", help="Show what would be written without writing it"
507
+ ),
508
+ chunk_rows: int = typer.Option(
509
+ 1024, "--chunk", "-C", help="Row chunk size for dense matrices"
510
+ ),
511
+ zarr_format: Optional[int] = typer.Option(
512
+ None,
513
+ "--zarr-format",
514
+ help="Zarr spec version to write (defaults to the source store's)",
515
+ ),
516
+ ) -> None:
517
+ """
518
+ Split a store into one file per distinct value of a column.
519
+
520
+ Streams the source, so it works on stores far larger than memory.
521
+
522
+ Examples:
523
+ adata split data.h5ad --by sample -o per_sample/
524
+ adata split data.h5ad --by cell_type -o clusters/ --dry-run
525
+ adata split data.h5ad --by cluster -o out/ --min-size 50
526
+ """
527
+ if axis not in ("obs", "var"):
528
+ console.print("[bold red]Error:[/] --axis must be 'obs' or 'var'.")
529
+ raise typer.Exit(code=1)
530
+
531
+ if zarr_format is not None and zarr_format not in (2, 3):
532
+ console.print("[bold red]Error:[/] --zarr-format must be 2 or 3.")
533
+ raise typer.Exit(code=1)
534
+
535
+ try:
536
+ split_store(
537
+ file=file,
538
+ column=by,
539
+ output_dir=output_dir,
540
+ console=console,
541
+ axis=axis,
542
+ suffix=suffix,
543
+ dry_run=dry_run,
544
+ manifest=manifest,
545
+ min_size=min_size,
546
+ chunk_rows=chunk_rows,
547
+ zarr_format=zarr_format,
548
+ )
549
+ except Exception as e:
550
+ console.print(f"[bold red]Error:[/] {e}")
551
+ raise typer.Exit(code=1)
552
+
553
+
554
+ # ============================================================================
555
+ # EXPORT subcommands
556
+ # ============================================================================
557
+ @export_app.command("dataframe")
558
+ def export_dataframe(
559
+ file: Path = typer.Argument(
560
+ ...,
561
+ help="Path to the .h5ad/.zarr store",
562
+ exists=True,
563
+ readable=True,
564
+ dir_okay=True,
565
+ file_okay=True,
566
+ ),
567
+ entry: str = typer.Argument(
568
+ ..., help="Path of the dataframe to export (e.g. 'obs', 'var', 'raw/var')"
569
+ ),
570
+ output: Path = typer.Option(
571
+ None, "--output", "-o", writable=True, help="Output CSV file path"
572
+ ),
573
+ columns: Optional[str] = typer.Option(
574
+ None,
575
+ "--columns",
576
+ "-c",
577
+ help="Comma separated column names to include",
578
+ ),
579
+ chunk_rows: int = typer.Option(
580
+ 10_000,
581
+ "--chunk",
582
+ "-C",
583
+ "--chunk-rows",
584
+ "-r",
585
+ help="Number of rows to read per chunk",
586
+ ),
587
+ head: Optional[int] = typer.Option(
588
+ None, "--head", "-n", help="Output only the first n entries"
589
+ ),
590
+ ) -> None:
591
+ """
592
+ Export any dataframe-encoded group to CSV.
593
+
594
+ Not limited to obs/var -- any path holding a dataframe works, including
595
+ ones under raw/, obsm/ or uns/.
596
+
597
+ Examples:
598
+ adata export dataframe data.h5ad obs --output obs.csv
599
+ adata export dataframe data.h5ad var --columns gene_id,mean
600
+ adata export dataframe data.h5ad raw/var --head 100
601
+ """
602
+
603
+ col_list: Optional[List[str]] = None
604
+ if columns:
605
+ col_list = [col.strip() for col in columns.split(",") if col.strip()]
606
+
607
+ try:
608
+ export_table(
609
+ file=file,
610
+ axis=entry,
611
+ columns=col_list,
612
+ out=output,
613
+ chunk_rows=chunk_rows,
614
+ head=head,
615
+ console=console,
616
+ )
617
+ except Exception as e:
618
+ console.print(f"[bold red]Error:[/] {e}")
619
+ raise typer.Exit(code=1)
620
+
621
+
622
+ @export_app.command("array")
623
+ def export_array(
624
+ file: Path = typer.Argument(
625
+ ...,
626
+ help="Path to the .h5ad/.zarr store",
627
+ exists=True,
628
+ readable=True,
629
+ dir_okay=True,
630
+ file_okay=True,
631
+ ),
632
+ entry: str = typer.Argument(
633
+ ..., help="Entry path to export (e.g., 'obsm/X_pca', 'varm/PCs', 'X')"
634
+ ),
635
+ output: Optional[Path] = typer.Option(
636
+ None,
637
+ "--output",
638
+ "-o",
639
+ help="Output .npy file path (defaults to stdout)",
640
+ writable=True,
641
+ ),
642
+ chunk_elements: int = typer.Option(
643
+ 100_000,
644
+ "--chunk",
645
+ "-C",
646
+ help="Number of elements to read per chunk",
647
+ ),
648
+ ) -> None:
649
+ """
650
+ Export a dense array or matrix to NumPy .npy format.
651
+
652
+ Writes to stdout when no --output is given. Writing to a file streams in
653
+ chunks; stdout is not seekable, so that path holds the array in memory.
654
+
655
+ Examples:
656
+ adata export array data.h5ad obsm/X_pca -o pca.npy
657
+ adata export array data.h5ad X -o matrix.npy
658
+ adata export array data.h5ad obsm/X_umap > umap.npy
659
+ """
660
+
661
+ try:
662
+ export_npy(
663
+ file=file,
664
+ obj=entry,
665
+ out=output,
666
+ chunk_elements=chunk_elements,
667
+ console=console,
668
+ )
669
+ except Exception as e:
670
+ console.print(f"[bold red]Error:[/] {e}")
671
+ raise typer.Exit(code=1)
672
+
673
+
674
+ @export_app.command("sparse")
675
+ def export_sparse(
676
+ file: Path = typer.Argument(
677
+ ...,
678
+ help="Path to the .h5ad/.zarr store",
679
+ exists=True,
680
+ readable=True,
681
+ dir_okay=True,
682
+ file_okay=True,
683
+ ),
684
+ entry: str = typer.Argument(
685
+ ..., help="Entry path to export (e.g., 'X', 'layers/counts')"
686
+ ),
687
+ output: Optional[Path] = typer.Option(
688
+ None,
689
+ "--output",
690
+ "-o",
691
+ writable=True,
692
+ help="Output .mtx file path (defaults to stdout)",
693
+ ),
694
+ head: Optional[int] = typer.Option(
695
+ None, "--head", "-n", help="Output only the first n entries of mtx file"
696
+ ),
697
+ chunk_elements: int = typer.Option(
698
+ 1_000,
699
+ "--chunk",
700
+ "-C",
701
+ help="Number of rows/columns (depends on compression format) to process per chunk",
702
+ ),
703
+ in_memory: bool = typer.Option(
704
+ False,
705
+ "--in-memory",
706
+ "-m",
707
+ help="Load the entire sparse matrix into memory before exporting (may be faster for small matrices)",
708
+ ),
709
+ ) -> None:
710
+ """
711
+ Export a sparse matrix (CSR/CSC) to Matrix Market (.mtx) format.
712
+
713
+ Examples:
714
+ h5ad export sparse data.h5ad X matrix.mtx
715
+ h5ad export sparse data.h5ad layers/counts counts.mtx
716
+ h5ad export sparse data.h5ad X --head 100
717
+ """
718
+
719
+ try:
720
+ export_mtx(
721
+ file=file,
722
+ obj=entry,
723
+ out=output,
724
+ head=head,
725
+ chunk_elements=chunk_elements,
726
+ in_memory=in_memory,
727
+ console=console,
728
+ )
729
+ except Exception as e:
730
+ console.print(f"[bold red]Error:[/] {e}")
731
+ raise typer.Exit(code=1)
732
+
733
+
734
+ @export_app.command("dict")
735
+ def export_dict(
736
+ file: Path = typer.Argument(
737
+ ...,
738
+ help="Path to the .h5ad/.zarr store",
739
+ exists=True,
740
+ readable=True,
741
+ dir_okay=True,
742
+ file_okay=True,
743
+ ),
744
+ entry: str = typer.Argument(
745
+ ..., help="Entry path to export (e.g., 'uns', 'uns/colors')"
746
+ ),
747
+ output_arg: Optional[Path] = typer.Argument(None, help="Output .json file path"),
748
+ output: Optional[Path] = typer.Option(
749
+ None, "--output", "-o", help="Output .json file path"
750
+ ),
751
+ max_elements: int = typer.Option(
752
+ 100_000,
753
+ "--max-elements",
754
+ help="Maximum array elements for JSON export",
755
+ ),
756
+ include_attrs: bool = typer.Option(
757
+ False, "--include-attrs", help="Include HDF5 attributes in JSON export"
758
+ ),
759
+ ) -> None:
760
+ """
761
+ Export a dict/group or scalar to JSON format.
762
+
763
+ Examples:
764
+ h5ad export dict data.h5ad uns metadata.json
765
+ h5ad export dict data.h5ad uns/colors colors.json
766
+ """
767
+
768
+ try:
769
+ out_path = output if output is not None else output_arg
770
+ export_json(
771
+ file=file,
772
+ obj=entry,
773
+ out=out_path,
774
+ max_elements=max_elements,
775
+ include_attrs=include_attrs,
776
+ console=console,
777
+ )
778
+ except Exception as e:
779
+ console.print(f"[bold red]Error:[/] {e}")
780
+ raise typer.Exit(code=1)
781
+
782
+
783
+ @export_app.command("image")
784
+ def export_image(
785
+ file: Path = typer.Argument(
786
+ ...,
787
+ help="Path to the .h5ad/.zarr store",
788
+ exists=True,
789
+ readable=True,
790
+ dir_okay=True,
791
+ file_okay=True,
792
+ ),
793
+ entry: str = typer.Argument(..., help="Entry path to export (2D or 3D array)"),
794
+ output: Optional[Path] = typer.Option(
795
+ None, "--output", "-o", help="Output image file (.png, .jpg, .tiff)"
796
+ ),
797
+ ) -> None:
798
+ """
799
+ Export an image-like array to PNG/JPG/TIFF format.
800
+
801
+ The array should be 2D (H,W) or 3D (H,W,C) with C in {1,3,4}.
802
+
803
+ Examples:
804
+ h5ad export image data.h5ad uns/spatial/image tissue.png
805
+ """
806
+
807
+ try:
808
+ export_image_cmd(file=file, obj=entry, out=output, console=console)
809
+ except Exception as e:
810
+ console.print(f"[bold red]Error:[/] {e}")
811
+ raise typer.Exit(code=1)
812
+
813
+
814
+ # ============================================================================
815
+ # IMPORT subcommands
816
+ # ============================================================================
817
+ def _get_target_file(file: Path, output: Optional[Path], inplace: bool) -> Path:
818
+ """Determine target path and copy/convert if needed."""
819
+ from adata.commands.import_data import _prepare_target_path
820
+
821
+ return _prepare_target_path(file, output, inplace, console)
822
+
823
+
824
+ @import_app.command("dataframe")
825
+ def import_dataframe(
826
+ file: Path = typer.Argument(
827
+ ...,
828
+ help="Path to the source .h5ad/.zarr store",
829
+ exists=True,
830
+ readable=True,
831
+ dir_okay=True,
832
+ file_okay=True,
833
+ ),
834
+ entry: str = typer.Argument(
835
+ ..., help="Dataframe path to create/replace (e.g. 'obs', 'var', 'raw/var')"
836
+ ),
837
+ input_file: Path = typer.Argument(
838
+ ..., help="Input CSV file", exists=True, readable=True
839
+ ),
840
+ output: Optional[Path] = typer.Option(
841
+ None,
842
+ "--output",
843
+ "-o",
844
+ help="Output .h5ad/.zarr path. Required unless --inplace.",
845
+ dir_okay=True,
846
+ file_okay=True,
847
+ ),
848
+ inplace: bool = typer.Option(
849
+ False,
850
+ "--inplace",
851
+ help="Modify source file directly.",
852
+ ),
853
+ index_column: Optional[str] = typer.Option(
854
+ None,
855
+ "--index-column",
856
+ "-i",
857
+ help="Column to use as index. Defaults to first column.",
858
+ ),
859
+ categorical: Optional[str] = typer.Option(
860
+ None,
861
+ "--categorical",
862
+ help="Comma separated columns to force to categorical",
863
+ ),
864
+ auto_categorical: bool = typer.Option(
865
+ True,
866
+ "--auto-categorical/--no-auto-categorical",
867
+ help="Infer categoricals from low-cardinality string columns",
868
+ ),
869
+ ) -> None:
870
+ """
871
+ Import a CSV file as a dataframe.
872
+
873
+ String columns with few distinct values become categoricals; pass
874
+ --no-auto-categorical to keep them as plain strings, or --categorical to
875
+ force specific ones.
876
+
877
+ Examples:
878
+ adata import dataframe data.h5ad obs cells.csv -o out.h5ad -i cell_id
879
+ adata import dataframe data.h5ad var genes.csv --inplace -i gene_id
880
+ adata import dataframe data.h5ad obs cells.csv --inplace --categorical batch
881
+ """
882
+ from adata.commands.import_data import _import_csv
883
+
884
+ if not inplace and output is None:
885
+ console.print(
886
+ "[bold red]Error:[/] Output file is required. "
887
+ "Use --output/-o or --inplace.",
888
+ )
889
+ raise typer.Exit(code=1)
890
+
891
+ try:
892
+ target = _get_target_file(file, output, inplace)
893
+ cat_list = (
894
+ [c.strip() for c in categorical.split(",") if c.strip()]
895
+ if categorical
896
+ else None
897
+ )
898
+ _import_csv(
899
+ target,
900
+ entry,
901
+ input_file,
902
+ index_column,
903
+ console,
904
+ categorical=cat_list,
905
+ auto_categorical=auto_categorical,
906
+ )
907
+ except Exception as e:
908
+ console.print(f"[bold red]Error:[/] {e}")
909
+ raise typer.Exit(code=1)
910
+
911
+
912
+ @import_app.command("array")
913
+ def import_array(
914
+ file: Path = typer.Argument(
915
+ ...,
916
+ help="Path to the source .h5ad/.zarr store",
917
+ exists=True,
918
+ readable=True,
919
+ dir_okay=True,
920
+ file_okay=True,
921
+ ),
922
+ entry: str = typer.Argument(
923
+ ..., help="Entry path to create/replace (e.g., 'X', 'obsm/X_pca')"
924
+ ),
925
+ input_file: Path = typer.Argument(
926
+ ..., help="Input .npy file", exists=True, readable=True
927
+ ),
928
+ output: Optional[Path] = typer.Option(
929
+ None,
930
+ "--output",
931
+ "-o",
932
+ help="Output .h5ad/.zarr path. Required unless --inplace.",
933
+ dir_okay=True,
934
+ file_okay=True,
935
+ ),
936
+ inplace: bool = typer.Option(
937
+ False,
938
+ "--inplace",
939
+ help="Modify source file directly.",
940
+ ),
941
+ ) -> None:
942
+ """
943
+ Import a NumPy .npy file as a dense array.
944
+
945
+ Dimensions are validated against existing obs/var.
946
+
947
+ Examples:
948
+ h5ad import array data.h5ad obsm/X_pca pca.npy -o output.h5ad
949
+ h5ad import array data.h5ad X matrix.npy --inplace
950
+ """
951
+ from adata.commands.import_data import _import_npy
952
+
953
+ if not inplace and output is None:
954
+ console.print(
955
+ "[bold red]Error:[/] Output file is required. "
956
+ "Use --output/-o or --inplace.",
957
+ )
958
+ raise typer.Exit(code=1)
959
+
960
+ try:
961
+ target = _get_target_file(file, output, inplace)
962
+ _import_npy(target, entry, input_file, console)
963
+ except Exception as e:
964
+ console.print(f"[bold red]Error:[/] {e}")
965
+ raise typer.Exit(code=1)
966
+
967
+
968
+ @import_app.command("sparse")
969
+ def import_sparse(
970
+ file: Path = typer.Argument(
971
+ ...,
972
+ help="Path to the source .h5ad/.zarr store",
973
+ exists=True,
974
+ readable=True,
975
+ dir_okay=True,
976
+ file_okay=True,
977
+ ),
978
+ obj: str = typer.Argument(
979
+ ..., help="Object path to create/replace (e.g., 'X', 'layers/counts')"
980
+ ),
981
+ input_file: Path = typer.Argument(
982
+ ..., help="Input .mtx file", exists=True, readable=True
983
+ ),
984
+ output: Optional[Path] = typer.Option(
985
+ None,
986
+ "--output",
987
+ "-o",
988
+ help="Output .h5ad/.zarr path. Required unless --inplace.",
989
+ dir_okay=True,
990
+ file_okay=True,
991
+ ),
992
+ inplace: bool = typer.Option(
993
+ False,
994
+ "--inplace",
995
+ help="Modify source file directly.",
996
+ ),
997
+ ) -> None:
998
+ """
999
+ Import a Matrix Market (.mtx) file as a CSR sparse matrix.
1000
+
1001
+ Dimensions are validated against existing obs/var.
1002
+
1003
+ Examples:
1004
+ h5ad import sparse data.h5ad X matrix.mtx -o output.h5ad
1005
+ h5ad import sparse data.h5ad layers/counts counts.mtx --inplace
1006
+ """
1007
+ from adata.commands.import_data import _import_mtx
1008
+
1009
+ if not inplace and output is None:
1010
+ console.print(
1011
+ "[bold red]Error:[/] Output file is required. "
1012
+ "Use --output/-o or --inplace.",
1013
+ )
1014
+ raise typer.Exit(code=1)
1015
+
1016
+ try:
1017
+ target = _get_target_file(file, output, inplace)
1018
+ _import_mtx(target, obj, input_file, console)
1019
+ except Exception as e:
1020
+ console.print(f"[bold red]Error:[/] {e}")
1021
+ raise typer.Exit(code=1)
1022
+
1023
+
1024
+ @import_app.command("dict")
1025
+ def import_dict(
1026
+ file: Path = typer.Argument(
1027
+ ...,
1028
+ help="Path to the source .h5ad/.zarr store",
1029
+ exists=True,
1030
+ readable=True,
1031
+ dir_okay=True,
1032
+ file_okay=True,
1033
+ ),
1034
+ obj: str = typer.Argument(
1035
+ ..., help="Object path to create/replace (e.g., 'uns', 'uns/metadata')"
1036
+ ),
1037
+ input_file: Path = typer.Argument(
1038
+ ..., help="Input .json file", exists=True, readable=True
1039
+ ),
1040
+ output: Optional[Path] = typer.Option(
1041
+ None,
1042
+ "--output",
1043
+ "-o",
1044
+ help="Output .h5ad/.zarr path. Required unless --inplace.",
1045
+ dir_okay=True,
1046
+ file_okay=True,
1047
+ ),
1048
+ inplace: bool = typer.Option(
1049
+ False,
1050
+ "--inplace",
1051
+ help="Modify source file directly.",
1052
+ ),
1053
+ ) -> None:
1054
+ """
1055
+ Import a JSON file into uns or other dict-like groups.
1056
+
1057
+ Examples:
1058
+ h5ad import dict data.h5ad uns/metadata config.json -o output.h5ad
1059
+ h5ad import dict data.h5ad uns settings.json --inplace
1060
+ """
1061
+ from adata.commands.import_data import _import_json
1062
+
1063
+ if not inplace and output is None:
1064
+ console.print(
1065
+ "[bold red]Error:[/] Output file is required. "
1066
+ "Use --output/-o or --inplace.",
1067
+ )
1068
+ raise typer.Exit(code=1)
1069
+
1070
+ try:
1071
+ target = _get_target_file(file, output, inplace)
1072
+ _import_json(target, obj, input_file, console)
1073
+ except Exception as e:
1074
+ console.print(f"[bold red]Error:[/] {e}")
1075
+ raise typer.Exit(code=1)
1076
+
1077
+
1078
+ @import_app.command("image")
1079
+ def import_image_cmd(
1080
+ file: Path = typer.Argument(
1081
+ ...,
1082
+ help="Path to the source .h5ad/.zarr store",
1083
+ exists=True,
1084
+ readable=True,
1085
+ dir_okay=True,
1086
+ file_okay=True,
1087
+ ),
1088
+ entry: str = typer.Argument(
1089
+ ..., help="Entry path to create/replace (e.g. 'uns/spatial/hires')"
1090
+ ),
1091
+ input_file: Path = typer.Argument(
1092
+ ..., help="Input image file (.png, .jpg, .tiff)", exists=True, readable=True
1093
+ ),
1094
+ output: Optional[Path] = typer.Option(
1095
+ None,
1096
+ "--output",
1097
+ "-o",
1098
+ help="Output .h5ad/.zarr path. Required unless --inplace.",
1099
+ dir_okay=True,
1100
+ file_okay=True,
1101
+ ),
1102
+ inplace: bool = typer.Option(
1103
+ False, "--inplace", help="Modify source file directly."
1104
+ ),
1105
+ ) -> None:
1106
+ """
1107
+ Import an image file as a dense array.
1108
+
1109
+ Examples:
1110
+ adata import image data.h5ad uns/spatial/hires tissue.png --inplace
1111
+ """
1112
+ from adata.commands.import_data import _import_image
1113
+
1114
+ if not inplace and output is None:
1115
+ console.print(
1116
+ "[bold red]Error:[/] Output file is required. "
1117
+ "Use --output/-o or --inplace.",
1118
+ )
1119
+ raise typer.Exit(code=1)
1120
+
1121
+ try:
1122
+ target = _get_target_file(file, output, inplace)
1123
+ _import_image(target, entry, input_file, console)
1124
+ except Exception as e:
1125
+ console.print(f"[bold red]Error:[/] {e}")
1126
+ raise typer.Exit(code=1)
1127
+
1128
+
1129
+ def main(argv: Optional[Sequence[str]] = None) -> None:
1130
+ app(standalone_mode=True)
1131
+
1132
+
1133
+ def main_deprecated(argv: Optional[Sequence[str]] = None) -> None:
1134
+ """Entry point for the deprecated `h5ad` command name."""
1135
+ console.print(
1136
+ "[yellow]Warning:[/] the 'h5ad' command is deprecated and will be removed "
1137
+ "in 1.0.0; use 'adata' instead.",
1138
+ )
1139
+ main(argv)