pyadata-cli 0.6.0__py3-none-any.whl → 0.7.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 CHANGED
@@ -6,11 +6,13 @@ from typing import Optional, Sequence, List
6
6
  from rich.console import Console
7
7
  import typer
8
8
 
9
- from adata.core.convert import LAYOUTS
9
+ from adata.conversion import FORMATS, Options
10
+ from adata.core.recast import LAYOUTS
10
11
  from adata.commands import (
11
12
  MERGE_CHOICES,
12
13
  concat_stores,
13
14
  convert_store,
15
+ recast_store,
14
16
  create_store,
15
17
  split_store,
16
18
  list_store,
@@ -26,7 +28,7 @@ from adata.commands import export_image as export_image_cmd
26
28
 
27
29
  app = typer.Typer(
28
30
  help="Streaming CLI for huge AnnData .h5ad and .zarr stores "
29
- "(view, ls, subset, split, concat, export, import)."
31
+ "(view, ls, create, subset, split, concat, convert, recast, export, import)."
30
32
  )
31
33
  # Use stderr for status/progress to keep stdout clean for data output
32
34
  # force_terminal=True ensures Rich output is visible even in non-TTY environments
@@ -481,8 +483,285 @@ def concat(
481
483
  # ============================================================================
482
484
  # CONVERT command
483
485
  # ============================================================================
484
- @app.command("convert")
486
+ #: The flags `recast` has and format conversion does not. Until 1.0.0, any of
487
+ #: them on `convert` means the old meaning of the command, and it forwards.
488
+ _RECAST_FLAGS = {
489
+ "inplace": "--inplace",
490
+ "convert_all": "--all",
491
+ "dtype": "--dtype",
492
+ "indices_dtype": "--indices-dtype",
493
+ "layout": "--layout",
494
+ }
495
+
496
+ #: Flags that only shape a recast; given without one of the above they have
497
+ #: nothing to act on.
498
+ _RECAST_ONLY_FLAGS = {"in_memory": "--in-memory", "chunk_rows": "--chunk"}
499
+
500
+ #: Format-conversion flags, by parameter name, as `Options.given` spells them.
501
+ _CONVERT_FLAGS = {
502
+ "zarr_format": "--zarr-format",
503
+ "compression": "--compression",
504
+ "force": "--force",
505
+ "overwrite": "--overwrite",
506
+ "genome": "--genome",
507
+ "var_names": "--var-names",
508
+ "make_unique": "--make-unique",
509
+ "feature_type": "--feature-type",
510
+ "split_feature_types": "--split-feature-types",
511
+ "layer": "--layer",
512
+ "gene_ids_column": "--gene-ids-column",
513
+ "gene_names_column": "--gene-names-column",
514
+ "feature_types_column": "--feature-types-column",
515
+ }
516
+
517
+ #: Flags that only make sense for format conversion; with recast's flags,
518
+ #: they mean two commands' worth of request at once.
519
+ _FORMAT_ONLY_FLAGS = {
520
+ "from_format": "--from",
521
+ "to_format": "--to",
522
+ **{
523
+ name: flag
524
+ for name, flag in _CONVERT_FLAGS.items()
525
+ if flag not in ("--force", "--zarr-format")
526
+ },
527
+ }
528
+
529
+
530
+ def _given(ctx: typer.Context, flags: dict) -> List[str]:
531
+ """The flags among `flags` that were actually typed, not defaulted."""
532
+ from click.core import ParameterSource
533
+
534
+ return [
535
+ flag
536
+ for name, flag in flags.items()
537
+ if ctx.get_parameter_source(name) not in (None, ParameterSource.DEFAULT)
538
+ ]
539
+
540
+
541
+ @app.command("convert", context_settings={"allow_extra_args": True})
485
542
  def convert(
543
+ ctx: typer.Context,
544
+ file: Path = typer.Argument(
545
+ ...,
546
+ help="Input file or store",
547
+ exists=True,
548
+ readable=True,
549
+ dir_okay=True,
550
+ file_okay=True,
551
+ ),
552
+ output: Optional[Path] = typer.Option(
553
+ None,
554
+ "--output",
555
+ "-o",
556
+ help="Output path; its extension picks the format (.h5ad, .zarr, .h5)",
557
+ dir_okay=True,
558
+ file_okay=True,
559
+ ),
560
+ from_format: Optional[str] = typer.Option(
561
+ None,
562
+ "--from",
563
+ help=f"Input format, if it cannot be detected: {', '.join(FORMATS)}",
564
+ ),
565
+ to_format: Optional[str] = typer.Option(
566
+ None,
567
+ "--to",
568
+ help=f"Output format, if the extension does not say: {', '.join(FORMATS)}",
569
+ ),
570
+ zarr_format: Optional[int] = typer.Option(
571
+ None,
572
+ "--zarr-format",
573
+ help="Zarr spec version to write (defaults to the source store's, else 3)",
574
+ ),
575
+ compression: Optional[str] = typer.Option(
576
+ None,
577
+ "--compression",
578
+ help="none, gzip or lzf for h5ad; none, gzip, zstd or blosc for zarr "
579
+ "(defaults to the source's)",
580
+ ),
581
+ force: bool = typer.Option(
582
+ False,
583
+ "--force",
584
+ help="Convert despite a lossy or inflating result",
585
+ ),
586
+ overwrite: bool = typer.Option(
587
+ False,
588
+ "--overwrite",
589
+ help="Replace an existing output",
590
+ ),
591
+ genome: Optional[str] = typer.Option(
592
+ None,
593
+ "--genome",
594
+ help="10x input: the genome to keep (picks the group in a Cell Ranger 2 file)",
595
+ ),
596
+ var_names: str = typer.Option(
597
+ "gene_symbols",
598
+ "--var-names",
599
+ help="10x input: name features by gene_symbols or gene_ids",
600
+ ),
601
+ make_unique: bool = typer.Option(
602
+ False,
603
+ "--make-unique",
604
+ help="10x input: suffix repeated var names, as var_names_make_unique does",
605
+ ),
606
+ feature_type: Optional[str] = typer.Option(
607
+ None,
608
+ "--feature-type",
609
+ help="10x input: keep only this feature type, e.g. 'Gene Expression'",
610
+ ),
611
+ split_feature_types: bool = typer.Option(
612
+ False,
613
+ "--split-feature-types",
614
+ help="10x input: write one output per feature type, OUT.<type>.h5ad",
615
+ ),
616
+ layer: Optional[str] = typer.Option(
617
+ None,
618
+ "--layer",
619
+ help="10x output: the matrix to write: X (default), raw/X, or a layer",
620
+ ),
621
+ gene_ids_column: Optional[str] = typer.Option(
622
+ None,
623
+ "--gene-ids-column",
624
+ help="10x output: var column holding gene ids (default gene_ids)",
625
+ ),
626
+ gene_names_column: Optional[str] = typer.Option(
627
+ None,
628
+ "--gene-names-column",
629
+ help="10x output: var column holding gene names (default: var_names)",
630
+ ),
631
+ feature_types_column: Optional[str] = typer.Option(
632
+ None,
633
+ "--feature-types-column",
634
+ help="10x output: var column holding feature types (default feature_types)",
635
+ ),
636
+ # Deprecated: recast's flags, so `convert` keeps working as it did until
637
+ # 1.0.0. Hidden from --help; the old matrix paths arrive in ctx.args.
638
+ inplace: bool = typer.Option(False, "--inplace", hidden=True),
639
+ convert_all: bool = typer.Option(False, "--all", hidden=True),
640
+ dtype: Optional[str] = typer.Option(None, "--dtype", hidden=True),
641
+ indices_dtype: Optional[str] = typer.Option(None, "--indices-dtype", hidden=True),
642
+ layout: Optional[str] = typer.Option(None, "--layout", hidden=True),
643
+ in_memory: bool = typer.Option(False, "--in-memory", hidden=True),
644
+ chunk_rows: int = typer.Option(1024, "--chunk", "-C", hidden=True),
645
+ ) -> None:
646
+ """
647
+ Convert a store to another format.
648
+
649
+ The output's extension picks the format: .h5ad, .zarr, or .h5 for a 10x
650
+ Genomics matrix (Cell Ranger's *_feature_bc_matrix.h5). Between h5ad and
651
+ zarr every element is copied, streamed, keeping the source's chunking and
652
+ compression where the target can express them. A 10x matrix is read as
653
+ scanpy.read_10x_h5(gex_only=False) reads it, keeping integer counts, and
654
+ written as Cell Ranger writes it.
655
+
656
+ Nothing is written until every check has passed, and the output is moved
657
+ into place only once complete: a failed conversion leaves nothing
658
+ behind. An existing output is refused unless --overwrite is given.
659
+
660
+ To change a matrix's dtype, layout or density, use `recast`. Until 1.0.0,
661
+ `convert` given recast's flags still forwards to it, with a warning.
662
+
663
+ Examples:
664
+ adata convert data.h5ad -o data.zarr
665
+ adata convert data.zarr -o data.h5ad --compression gzip
666
+ adata convert data.zarr -o v3.zarr --zarr-format 3
667
+ adata convert filtered_feature_bc_matrix.h5 -o counts.h5ad
668
+ adata convert raw_feature_bc_matrix.h5 -o rna.h5ad --feature-type "Gene Expression"
669
+ adata convert counts.h5ad -o for_cellbender.h5 --layer counts
670
+ """
671
+ entries = list(ctx.args)
672
+ recast_flags = _given(ctx, _RECAST_FLAGS)
673
+
674
+ try:
675
+ if recast_flags:
676
+ mixed = _given(ctx, _FORMAT_ONLY_FLAGS)
677
+ if mixed:
678
+ raise ValueError(
679
+ f"{', '.join(mixed)} cannot be combined with "
680
+ f"{', '.join(recast_flags)}: the first change a store's "
681
+ "format, the second its matrices. Run `adata recast` for "
682
+ "the matrices."
683
+ )
684
+ console.print(
685
+ "[yellow]Warning:[/] 'convert' with "
686
+ f"{', '.join(recast_flags)} is deprecated and will be removed "
687
+ "in 1.0.0; use 'recast' instead.",
688
+ )
689
+ if not inplace and output is None:
690
+ raise ValueError("Output file is required. Use --output/-o or --inplace.")
691
+ if layout is not None and layout not in LAYOUTS:
692
+ raise ValueError(f"--layout must be one of: {', '.join(LAYOUTS)}.")
693
+ if zarr_format is not None and zarr_format not in (2, 3):
694
+ raise ValueError("--zarr-format must be 2 or 3.")
695
+ recast_store(
696
+ file=file,
697
+ entries=entries or None,
698
+ output=output,
699
+ console=console,
700
+ dtype=dtype,
701
+ indices_dtype=indices_dtype,
702
+ layout=layout,
703
+ convert_all=convert_all,
704
+ inplace=inplace,
705
+ chunk_rows=chunk_rows,
706
+ in_memory=in_memory,
707
+ force=force,
708
+ zarr_format=zarr_format,
709
+ )
710
+ return
711
+
712
+ if entries:
713
+ hint = (
714
+ f" To write it, pass -o {entries[0]}."
715
+ if output is None and len(entries) == 1
716
+ else ""
717
+ )
718
+ raise ValueError(
719
+ f"Unexpected argument{'s' if len(entries) > 1 else ''}: "
720
+ f"{' '.join(entries)}. convert takes one input.{hint} To change "
721
+ "a matrix's dtype or layout, use `adata recast`."
722
+ )
723
+ stray = _given(ctx, _RECAST_ONLY_FLAGS)
724
+ if stray:
725
+ raise ValueError(
726
+ f"{', '.join(stray)} {'applies' if len(stray) == 1 else 'apply'} "
727
+ "to `adata recast`, not to format conversion."
728
+ )
729
+ if output is None:
730
+ raise ValueError("Output path is required: pass -o/--output.")
731
+
732
+ convert_store(
733
+ file,
734
+ output,
735
+ console,
736
+ src_format=from_format,
737
+ dst_format=to_format,
738
+ options=Options(
739
+ given=frozenset(_given(ctx, _CONVERT_FLAGS)),
740
+ zarr_format=zarr_format,
741
+ compression=compression,
742
+ force=force,
743
+ overwrite=overwrite,
744
+ genome=genome,
745
+ var_names=var_names,
746
+ make_unique=make_unique,
747
+ feature_type=feature_type,
748
+ split_feature_types=split_feature_types,
749
+ layer=layer,
750
+ gene_ids_column=gene_ids_column,
751
+ gene_names_column=gene_names_column,
752
+ feature_types_column=feature_types_column,
753
+ ),
754
+ )
755
+ except Exception as e:
756
+ console.print(f"[bold red]Error:[/] {e}")
757
+ raise typer.Exit(code=1)
758
+
759
+
760
+ # ============================================================================
761
+ # RECAST command
762
+ # ============================================================================
763
+ @app.command("recast")
764
+ def recast(
486
765
  file: Path = typer.Argument(
487
766
  ...,
488
767
  help="Input .h5ad/.zarr",
@@ -493,7 +772,7 @@ def convert(
493
772
  ),
494
773
  entries: Optional[List[str]] = typer.Argument(
495
774
  None,
496
- help="Matrix paths to convert, e.g. 'X', 'layers/counts', 'raw/X'",
775
+ help="Matrix paths to recast, e.g. 'X', 'layers/counts', 'raw/X'",
497
776
  ),
498
777
  output: Optional[Path] = typer.Option(
499
778
  None,
@@ -511,7 +790,7 @@ def convert(
511
790
  convert_all: bool = typer.Option(
512
791
  False,
513
792
  "--all",
514
- help="Convert X, every layer, and raw/X",
793
+ help="Recast X, every layer, and raw/X",
515
794
  ),
516
795
  dtype: Optional[str] = typer.Option(
517
796
  None,
@@ -531,7 +810,7 @@ def convert(
531
810
  force: bool = typer.Option(
532
811
  False,
533
812
  "--force",
534
- help="Convert despite a lossy cast or a large size increase",
813
+ help="Recast despite a lossy cast or a large size increase",
535
814
  ),
536
815
  in_memory: bool = typer.Option(
537
816
  False,
@@ -562,11 +841,13 @@ def convert(
562
841
  both. Transposing streams by default, so it works on matrices too large
563
842
  to load.
564
843
 
844
+ This was `convert` until 0.7.0; `convert` now changes a store's format.
845
+
565
846
  Examples:
566
- adata convert data.h5ad X -o out.h5ad --dtype float32
567
- adata convert data.h5ad X -o out.h5ad --layout csc
568
- adata convert data.h5ad X --inplace --dtype float32 --indices-dtype int32
569
- adata convert data.h5ad --all -o out.h5ad --dtype float32
847
+ adata recast data.h5ad X -o out.h5ad --dtype float32
848
+ adata recast data.h5ad X -o out.h5ad --layout csc
849
+ adata recast data.h5ad X --inplace --dtype float32 --indices-dtype int32
850
+ adata recast data.h5ad --all -o out.h5ad --dtype float32
570
851
  """
571
852
  if not inplace and output is None:
572
853
  console.print(
@@ -586,7 +867,7 @@ def convert(
586
867
  raise typer.Exit(code=1)
587
868
 
588
869
  try:
589
- convert_store(
870
+ recast_store(
590
871
  file=file,
591
872
  entries=list(entries) if entries else None,
592
873
  output=output,
@@ -6,4 +6,5 @@ from adata.commands.ls import list_store
6
6
  from adata.commands.create import create_store
7
7
  from adata.commands.split import split_store
8
8
  from adata.commands.concat import MERGE_CHOICES, MERGE_STRATEGIES, concat_stores
9
+ from adata.commands.recast import recast_store
9
10
  from adata.commands.convert import convert_store
adata/commands/convert.py CHANGED
@@ -1,218 +1,77 @@
1
- """The `convert` command: rewrite matrices, copy everything else.
1
+ """The `convert` command: rewrite a store in another format.
2
2
 
3
- Writes a whole new store rather than editing one in place, for the same
4
- reason `subset` does: a conversion that fails half way leaves the original
5
- untouched, and `--inplace` becomes an atomic swap of a finished file rather
6
- than a partial edit of a live one.
3
+ Every refusal happens while building the plan, before an output exists, and
4
+ the outputs are written beside their destination and moved into place only
5
+ once complete. A conversion that fails therefore leaves nothing behind, and
6
+ never a half-written store that the next command would read as a whole one.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import shutil
12
11
  from pathlib import Path
13
- from typing import Any, List, Optional
12
+ from typing import List, Optional
14
13
 
15
14
  from rich.console import Console
16
15
 
17
- from adata.core.convert import (
18
- DEFAULT_CHUNK,
19
- LAYOUTS,
20
- Plan,
21
- convert_matrix,
22
- parse_dtype,
23
- plan_conversion,
16
+ from adata.conversion import (
17
+ Options,
18
+ check_options,
19
+ converter_for,
20
+ detect_format,
21
+ output_format,
24
22
  )
25
- from adata.elements import spec
26
- from adata.elements.write import ensure_anndata_skeleton
27
- from adata.storage import copy_tree, detect_backend, is_group, open_store
28
-
29
- #: What `--all` means. The matrices an AnnData object is built from, not
30
- #: every 2-D array in the file: obsm/varm hold embeddings, whose dtype is
31
- #: rarely what anyone is trying to shrink. Those still convert by path.
32
- ALL_PREFIXES = ("X", "layers", "raw/X")
33
-
34
-
35
- def discover_matrices(root: Any) -> List[str]:
36
- """Entry paths `--all` resolves to, in a stable order."""
37
- found: List[str] = []
38
- if "X" in root:
39
- found.append("X")
40
- if "layers" in root and is_group(root["layers"]):
41
- found.extend(f"layers/{key}" for key in sorted(root["layers"].keys()))
42
- if "raw" in root and is_group(root["raw"]) and "X" in root["raw"]:
43
- found.append("raw/X")
44
- return found
23
+ from adata.util.staging import staged_outputs
45
24
 
46
25
 
47
26
  def _same_path(left: Path, right: Path) -> bool:
48
- """Do these name the same store, through symlinks and `..` alike?"""
49
27
  try:
50
28
  return left.resolve() == right.resolve()
51
- except OSError: # pragma: no cover - unresolvable path
52
- return left.absolute() == right.absolute()
53
-
54
-
55
- def _resolve(root: Any, path: str) -> Any:
56
- obj = root
57
- for part in path.split("/"):
58
- if part not in obj:
59
- raise KeyError(f"{path!r} not found in the store.")
60
- obj = obj[part]
61
- return obj
29
+ except OSError:
30
+ return False
62
31
 
63
32
 
64
33
  def convert_store(
65
34
  file: Path,
66
- entries: Optional[List[str]],
67
- output: Optional[Path],
35
+ output: Path,
68
36
  console: Console,
69
37
  *,
70
- dtype: Optional[str] = None,
71
- indices_dtype: Optional[str] = None,
72
- layout: Optional[str] = None,
73
- convert_all: bool = False,
74
- inplace: bool = False,
75
- chunk: int = DEFAULT_CHUNK,
76
- chunk_rows: int = 1024,
77
- in_memory: bool = False,
78
- force: bool = False,
79
- zarr_format: Optional[int] = None,
80
- ) -> None:
81
- """Write a copy of `file` with the named matrices converted."""
82
- if not inplace and output is None:
83
- raise ValueError("Output file is required unless --inplace is specified.")
84
- if dtype is None and indices_dtype is None and layout is None:
85
- raise ValueError(
86
- "Nothing to do: pass at least one of --dtype, --indices-dtype "
87
- "or --layout."
88
- )
89
- if layout is not None and layout not in LAYOUTS:
90
- raise ValueError(f"--layout must be one of: {', '.join(LAYOUTS)}.")
91
-
92
- data_dtype = parse_dtype(dtype) if dtype else None
93
- index_dtype = (
94
- parse_dtype(indices_dtype, allowed=("int32", "int64"))
95
- if indices_dtype
96
- else None
97
- )
98
-
99
- if output is not None and not inplace and _same_path(file, output):
100
- # Opening the destination "w" clears it while the source is still
101
- # being read from it. HDF5 refuses; Zarr does not, and quietly
102
- # produced an empty store where the data used to be.
38
+ src_format: Optional[str] = None,
39
+ dst_format: Optional[str] = None,
40
+ options: Options = Options(),
41
+ ) -> List[Path]:
42
+ """Convert `file` into `output`; return the paths written."""
43
+ file, output = Path(file), Path(output)
44
+ if str(output) == "-":
103
45
  raise ValueError(
104
- f"Output path is the input: {output}. Use --inplace to replace "
105
- "it, which writes to a temporary file first."
106
- )
107
-
108
- if inplace:
109
- backend = detect_backend(file)
110
- if backend == "zarr":
111
- base = file.stem if file.suffix else file.name
112
- dst_path = file.with_name(f"{base}.convert-tmp.zarr")
113
- else:
114
- dst_path = file.with_name(f"{file.name}.convert-tmp")
115
- if dst_path.exists():
116
- raise FileExistsError(f"Temporary path already exists: {dst_path}")
117
- else:
118
- dst_path = output
119
-
120
- if zarr_format is None and detect_backend(file) == "zarr":
121
- with open_store(file, "r") as probe:
122
- zarr_format = probe.zarr_format
123
-
124
- with open_store(file, "r") as src_store:
125
- src = src_store.root
126
- targets = discover_matrices(src) if convert_all else list(entries or [])
127
- if not targets:
128
- raise ValueError(
129
- "No matrices selected. Name one (e.g. `X`) or pass --all."
130
- )
131
- # Every check first, before the destination exists. A refusal must
132
- # not leave behind a store holding a copy of obs and var and nothing
133
- # else -- the caller cannot tell that from a finished conversion.
134
- plans = {
135
- path: plan_conversion(
136
- _resolve(src, path),
137
- path,
138
- dtype=data_dtype,
139
- index_dtype=index_dtype,
140
- layout=layout,
141
- chunk=chunk,
142
- force=force,
143
- console=console,
144
- )
145
- for path in targets
146
- }
147
-
148
- console.print(
149
- f"[cyan]Converting {len(targets)} matrix/matrices:[/] "
150
- + ", ".join(targets)
46
+ "convert writes a store, which cannot go to stdout; give a path "
47
+ "with -o."
151
48
  )
152
-
153
- with open_store(dst_path, "w", zarr_format=zarr_format) as dst_store:
154
- dst = dst_store.root
155
- _write(
156
- src, dst, plans,
157
- chunk=chunk, chunk_rows=chunk_rows, in_memory=in_memory,
158
- console=console,
159
- )
160
- ensure_anndata_skeleton(dst)
161
-
162
- if inplace:
163
- if file.is_dir():
164
- shutil.rmtree(file)
165
- elif file.exists():
166
- file.unlink()
167
- if dst_path.is_dir():
168
- shutil.move(str(dst_path), str(file))
169
- else:
170
- dst_path.replace(file)
171
- console.print(f"[green]Converted[/] {file}")
172
- else:
173
- console.print(f"[green]Wrote[/] {dst_path}")
174
-
175
-
176
- def _write(src: Any, dst: Any, plans: dict, **options: Any) -> None:
177
- """Copy the store across, converting the targeted entries as they pass."""
178
- by_parent: dict = {}
179
- for path in plans:
180
- parent, _, leaf = path.rpartition("/")
181
- by_parent.setdefault(parent, {})[leaf] = plans[path]
182
-
183
- for key in src.keys():
184
- if key in by_parent.get("", {}):
185
- convert_matrix(
186
- src[key], dst, key, plan=by_parent[""][key], **options
49
+ if options.zarr_format not in (None, 2, 3):
50
+ raise ValueError("--zarr-format must be 2 or 3.")
51
+
52
+ src_format = src_format or detect_format(file)
53
+ dst_format = output_format(output, dst_format)
54
+ converter = converter_for(src_format, dst_format)
55
+ check_options(converter, options, src_format, dst_format)
56
+
57
+ plan = converter.plan(file, output, src_format, dst_format, options)
58
+ for out in plan.outputs:
59
+ if _same_path(file, out):
60
+ # Writing a Zarr store over its own source empties it before a
61
+ # single element has been read.
62
+ raise ValueError(f"Output path is the input: {out}")
63
+ if out.exists() and not options.overwrite:
64
+ raise FileExistsError(
65
+ f"'{out}' already exists. Pass --overwrite to replace it."
187
66
  )
188
- elif key in by_parent:
189
- # Any parent, not just layers and raw. Restricting it to those
190
- # two meant an explicitly named `obsm/X_pca` was copied
191
- # unconverted and the command still reported success.
192
- _write_group(src[key], dst, key, by_parent, **options)
193
- else:
194
- copy_tree(src[key], dst, key)
195
-
196
-
197
- def _write_group(
198
- group: Any, dst: Any, name: str, by_parent: dict, **options: Any
199
- ) -> None:
200
- """Recreate one container, converting the members that were named."""
201
- from adata.storage import copy_attrs, is_zarr_group
202
67
 
203
- out = dst.create_group(name)
204
- copy_attrs(
205
- group.attrs,
206
- out.attrs,
207
- target_backend="zarr" if is_zarr_group(dst) else "hdf5",
208
- )
209
- if not spec.encoding_type(out):
210
- spec.set_encoding(out, spec.RAW if name == "raw" else spec.DICT)
68
+ console.print(f"[cyan]Converting[/] {file}: {plan.summary}")
69
+ for warning in plan.warnings:
70
+ console.print(f"[yellow]Warning:[/] {warning}")
211
71
 
72
+ with staged_outputs(plan.outputs) as staged:
73
+ converter.write(plan, staged, console)
212
74
 
213
- wanted = by_parent.get(name, {})
214
- for key in group.keys():
215
- if key in wanted:
216
- convert_matrix(group[key], out, key, plan=wanted[key], **options)
217
- else:
218
- copy_tree(group[key], out, key)
75
+ for out in plan.outputs:
76
+ console.print(f"[green]Wrote[/] {out}")
77
+ return plan.outputs