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 +292 -11
- adata/commands/__init__.py +1 -0
- adata/commands/convert.py +50 -191
- adata/commands/recast.py +218 -0
- adata/commands/split.py +19 -44
- adata/conversion/__init__.py +186 -0
- adata/conversion/anndata_copy.py +90 -0
- adata/conversion/base.py +98 -0
- adata/conversion/tenx.py +812 -0
- adata/core/concat.py +55 -10
- adata/core/{convert.py → recast.py} +72 -51
- adata/core/subset.py +625 -312
- adata/storage/__init__.py +190 -12
- adata/util/names.py +69 -0
- adata/util/staging.py +63 -0
- {pyadata_cli-0.6.0.dist-info → pyadata_cli-0.7.0.dist-info}/METADATA +17 -7
- {pyadata_cli-0.6.0.dist-info → pyadata_cli-0.7.0.dist-info}/RECORD +20 -13
- {pyadata_cli-0.6.0.dist-info → pyadata_cli-0.7.0.dist-info}/WHEEL +0 -0
- {pyadata_cli-0.6.0.dist-info → pyadata_cli-0.7.0.dist-info}/entry_points.txt +0 -0
- {pyadata_cli-0.6.0.dist-info → pyadata_cli-0.7.0.dist-info}/licenses/LICENSE +0 -0
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.
|
|
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
|
-
|
|
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
|
|
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="
|
|
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="
|
|
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
|
|
567
|
-
adata
|
|
568
|
-
adata
|
|
569
|
-
adata
|
|
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
|
-
|
|
870
|
+
recast_store(
|
|
590
871
|
file=file,
|
|
591
872
|
entries=list(entries) if entries else None,
|
|
592
873
|
output=output,
|
adata/commands/__init__.py
CHANGED
|
@@ -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
|
|
1
|
+
"""The `convert` command: rewrite a store in another format.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
12
|
+
from typing import List, Optional
|
|
14
13
|
|
|
15
14
|
from rich.console import Console
|
|
16
15
|
|
|
17
|
-
from adata.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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:
|
|
52
|
-
return
|
|
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
|
-
|
|
67
|
-
output: Optional[Path],
|
|
35
|
+
output: Path,
|
|
68
36
|
console: Console,
|
|
69
37
|
*,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
105
|
-
"
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|