pyadata-cli 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/PKG-INFO +19 -4
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/README.md +18 -3
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/pyproject.toml +1 -1
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/cli.py +142 -5
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/__init__.py +2 -1
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/concat.py +2 -2
- pyadata_cli-0.6.0/src/adata/commands/convert.py +218 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/core/concat.py +63 -26
- pyadata_cli-0.6.0/src/adata/core/convert.py +906 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/core/select.py +22 -2
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/storage/__init__.py +122 -5
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/LICENSE +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/__init__.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/create.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/export.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/import_data.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/info.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/ls.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/split.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/commands/subset.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/core/__init__.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/core/info.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/core/query.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/core/read.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/core/subset.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/elements/__init__.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/elements/read.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/elements/spec.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/elements/strings.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/elements/write.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/__init__.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/array.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/common.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/dataframe.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/image.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/json_data.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/sparse.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/formats/validate.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/info.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/py.typed +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/read.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/util/__init__.py +0 -0
- {pyadata_cli-0.5.0 → pyadata_cli-0.6.0}/src/adata/util/path.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyadata-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Streaming CLI for exploring and editing large AnnData .h5ad and .zarr stores
|
|
5
5
|
Keywords: anndata,h5ad,zarr,bioinformatics,single-cell,cli
|
|
6
6
|
Author: Aljes Binkevich
|
|
@@ -44,7 +44,7 @@ A command-line tool for exploring huge AnnData stores (`.h5ad` and `.zarr`) with
|
|
|
44
44
|
|
|
45
45
|
## Features
|
|
46
46
|
|
|
47
|
-
- Streaming access to very large `.h5ad` and `.zarr` stores
|
|
47
|
+
- Streaming access to very large `.h5ad` and `.zarr` stores — see [the benchmarks](docs/BENCHMARKS.md) for what that costs in practice, including where loading the file outright is faster
|
|
48
48
|
- Auto-detects `.h5ad` files vs `.zarr` directories
|
|
49
49
|
- Chunked processing for dense and sparse matrices (CSR/CSC)
|
|
50
50
|
- Reads every AnnData on-disk layout, from 0.7.x through the current spec, and always writes the current one
|
|
@@ -96,6 +96,7 @@ Run help at any level (e.g. `adata --help`, `adata export --help`).
|
|
|
96
96
|
- `subset` – stream and write a filtered copy, selected by obs/var name lists (`--obs`/`--var`) or by expression (`--obs-query`/`--var-query`).
|
|
97
97
|
- `split` – write one store per distinct value of an annotation column, with a CSV manifest.
|
|
98
98
|
- `concat` – concatenate stores along the obs axis, with `--join inner|outer` and merge strategies for var and uns.
|
|
99
|
+
- `convert` – change a matrix's dtype, layout (CSR/CSC/dense) or density, streaming; refuses a lossy cast or a large size increase unless forced.
|
|
99
100
|
- `export` – extract data from a store; subcommands: `dataframe` (any dataframe group to CSV), `array` (dense to `.npy`), `sparse` (CSR/CSC to `.mtx`), `dict` (JSON), `image` (PNG). Results go to stdout when no `--output` is given.
|
|
100
101
|
- `import` – write new data into a store at any path; subcommands: `dataframe` (CSV), `array` (`.npy`), `sparse` (`.mtx`), `dict` (JSON), `image` (PNG/JPEG/TIFF).
|
|
101
102
|
|
|
@@ -118,12 +119,26 @@ adata split data.h5ad --by sample -o per_sample/
|
|
|
118
119
|
adata concat per_sample/*.h5ad -o merged.h5ad --join outer --label sample
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
### Shrinking a store, and making encodings agree
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
adata convert data.h5ad X --inplace --dtype float32
|
|
126
|
+
adata convert data.h5ad --all -o small.h5ad --dtype float32 --indices-dtype int32
|
|
127
|
+
adata convert data.h5ad X -o csc.h5ad --layout csc
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Counts written as float64 halve with no loss — and `convert` proves that
|
|
131
|
+
before it writes, by casting every value and casting it back. `concat`
|
|
132
|
+
refuses inputs whose matrices disagree about CSR versus CSC; `--layout` is
|
|
133
|
+
how you make them agree.
|
|
134
|
+
|
|
121
135
|
## Documentation
|
|
122
136
|
|
|
123
137
|
- [Get started](docs/GET_STARTED.md) — a short tutorial
|
|
124
138
|
- [Command reference](docs/COMMANDS.md) — every command and flag
|
|
125
139
|
- [Element spec: HDF5](docs/ELEMENTS_h5ad.md) / [Zarr](docs/ELEMENTS_zarr.md) — the on-disk format, and what this tool does with it
|
|
126
|
-
- [Testing](docs/TESTING.md) — how the suite is organised,
|
|
140
|
+
- [Testing](docs/TESTING.md) — how the suite is organised, how compatibility is verified against six anndata releases, and the complexity guards that keep cost regressions out
|
|
141
|
+
- [Benchmarks](docs/BENCHMARKS.md) — peak memory and wall time against anndata and scanpy, remeasured on every release
|
|
127
142
|
- [Changelog](CHANGELOG.md)
|
|
128
143
|
|
|
129
144
|
## Docker
|
|
@@ -131,5 +146,5 @@ adata concat per_sample/*.h5ad -o merged.h5ad --join outer --label sample
|
|
|
131
146
|
A docker image is available on QUAY: `quay.io/cellgeni/adata-cli:latest`. Pull and run with:
|
|
132
147
|
|
|
133
148
|
```bash
|
|
134
|
-
docker run --rm -it -v /path/to/data:/data quay.io/cellgeni/adata-cli:latest view /data/your_file.h5ad
|
|
149
|
+
docker run --rm -it -v /path/to/data:/data quay.io/cellgeni/adata-cli:latest adata view /data/your_file.h5ad
|
|
135
150
|
```
|
|
@@ -4,7 +4,7 @@ A command-line tool for exploring huge AnnData stores (`.h5ad` and `.zarr`) with
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- Streaming access to very large `.h5ad` and `.zarr` stores
|
|
7
|
+
- Streaming access to very large `.h5ad` and `.zarr` stores — see [the benchmarks](docs/BENCHMARKS.md) for what that costs in practice, including where loading the file outright is faster
|
|
8
8
|
- Auto-detects `.h5ad` files vs `.zarr` directories
|
|
9
9
|
- Chunked processing for dense and sparse matrices (CSR/CSC)
|
|
10
10
|
- Reads every AnnData on-disk layout, from 0.7.x through the current spec, and always writes the current one
|
|
@@ -56,6 +56,7 @@ Run help at any level (e.g. `adata --help`, `adata export --help`).
|
|
|
56
56
|
- `subset` – stream and write a filtered copy, selected by obs/var name lists (`--obs`/`--var`) or by expression (`--obs-query`/`--var-query`).
|
|
57
57
|
- `split` – write one store per distinct value of an annotation column, with a CSV manifest.
|
|
58
58
|
- `concat` – concatenate stores along the obs axis, with `--join inner|outer` and merge strategies for var and uns.
|
|
59
|
+
- `convert` – change a matrix's dtype, layout (CSR/CSC/dense) or density, streaming; refuses a lossy cast or a large size increase unless forced.
|
|
59
60
|
- `export` – extract data from a store; subcommands: `dataframe` (any dataframe group to CSV), `array` (dense to `.npy`), `sparse` (CSR/CSC to `.mtx`), `dict` (JSON), `image` (PNG). Results go to stdout when no `--output` is given.
|
|
60
61
|
- `import` – write new data into a store at any path; subcommands: `dataframe` (CSV), `array` (`.npy`), `sparse` (`.mtx`), `dict` (JSON), `image` (PNG/JPEG/TIFF).
|
|
61
62
|
|
|
@@ -78,12 +79,26 @@ adata split data.h5ad --by sample -o per_sample/
|
|
|
78
79
|
adata concat per_sample/*.h5ad -o merged.h5ad --join outer --label sample
|
|
79
80
|
```
|
|
80
81
|
|
|
82
|
+
### Shrinking a store, and making encodings agree
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
adata convert data.h5ad X --inplace --dtype float32
|
|
86
|
+
adata convert data.h5ad --all -o small.h5ad --dtype float32 --indices-dtype int32
|
|
87
|
+
adata convert data.h5ad X -o csc.h5ad --layout csc
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Counts written as float64 halve with no loss — and `convert` proves that
|
|
91
|
+
before it writes, by casting every value and casting it back. `concat`
|
|
92
|
+
refuses inputs whose matrices disagree about CSR versus CSC; `--layout` is
|
|
93
|
+
how you make them agree.
|
|
94
|
+
|
|
81
95
|
## Documentation
|
|
82
96
|
|
|
83
97
|
- [Get started](docs/GET_STARTED.md) — a short tutorial
|
|
84
98
|
- [Command reference](docs/COMMANDS.md) — every command and flag
|
|
85
99
|
- [Element spec: HDF5](docs/ELEMENTS_h5ad.md) / [Zarr](docs/ELEMENTS_zarr.md) — the on-disk format, and what this tool does with it
|
|
86
|
-
- [Testing](docs/TESTING.md) — how the suite is organised,
|
|
100
|
+
- [Testing](docs/TESTING.md) — how the suite is organised, how compatibility is verified against six anndata releases, and the complexity guards that keep cost regressions out
|
|
101
|
+
- [Benchmarks](docs/BENCHMARKS.md) — peak memory and wall time against anndata and scanpy, remeasured on every release
|
|
87
102
|
- [Changelog](CHANGELOG.md)
|
|
88
103
|
|
|
89
104
|
## Docker
|
|
@@ -91,5 +106,5 @@ adata concat per_sample/*.h5ad -o merged.h5ad --join outer --label sample
|
|
|
91
106
|
A docker image is available on QUAY: `quay.io/cellgeni/adata-cli:latest`. Pull and run with:
|
|
92
107
|
|
|
93
108
|
```bash
|
|
94
|
-
docker run --rm -it -v /path/to/data:/data quay.io/cellgeni/adata-cli:latest view /data/your_file.h5ad
|
|
109
|
+
docker run --rm -it -v /path/to/data:/data quay.io/cellgeni/adata-cli:latest adata view /data/your_file.h5ad
|
|
95
110
|
```
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# so the distribution is published as `pyadata-cli`. The import package
|
|
4
4
|
# and the command are both still `adata`.
|
|
5
5
|
name = "pyadata-cli"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.6.0"
|
|
7
7
|
description = "Streaming CLI for exploring and editing large AnnData .h5ad and .zarr stores"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.12"
|
|
@@ -6,9 +6,11 @@ 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
10
|
from adata.commands import (
|
|
10
|
-
|
|
11
|
+
MERGE_CHOICES,
|
|
11
12
|
concat_stores,
|
|
13
|
+
convert_store,
|
|
12
14
|
create_store,
|
|
13
15
|
split_store,
|
|
14
16
|
list_store,
|
|
@@ -413,12 +415,14 @@ def concat(
|
|
|
413
415
|
merge: Optional[str] = typer.Option(
|
|
414
416
|
None,
|
|
415
417
|
"--merge",
|
|
416
|
-
help="How to reconcile var columns: same, unique, first, only
|
|
418
|
+
help="How to reconcile var columns: drop, same, unique, first, only "
|
|
419
|
+
"(default: drop)",
|
|
417
420
|
),
|
|
418
421
|
uns_merge: Optional[str] = typer.Option(
|
|
419
422
|
None,
|
|
420
423
|
"--uns-merge",
|
|
421
|
-
help="How to reconcile uns: same, unique, first, only
|
|
424
|
+
help="How to reconcile uns: drop, same, unique, first, only "
|
|
425
|
+
"(default: drop)",
|
|
422
426
|
),
|
|
423
427
|
fill_value: float = typer.Option(
|
|
424
428
|
0.0, "--fill-value", help="Value for dense cells introduced by an outer join"
|
|
@@ -442,13 +446,18 @@ def concat(
|
|
|
442
446
|
adata concat a.h5ad b.h5ad -o m.h5ad --keys a,b --index-unique - --uns-merge same
|
|
443
447
|
"""
|
|
444
448
|
for name, value in (("--merge", merge), ("--uns-merge", uns_merge)):
|
|
445
|
-
if value is not None and value not in
|
|
449
|
+
if value is not None and value not in MERGE_CHOICES:
|
|
446
450
|
console.print(
|
|
447
451
|
f"[bold red]Error:[/] {name} must be one of: "
|
|
448
|
-
f"{', '.join(
|
|
452
|
+
f"{', '.join(MERGE_CHOICES)}"
|
|
449
453
|
)
|
|
450
454
|
raise typer.Exit(code=1)
|
|
451
455
|
|
|
456
|
+
# "drop" is the default, and naming it explicitly has to be allowed: a
|
|
457
|
+
# config that spells out the default should not be rejected.
|
|
458
|
+
merge = None if merge == "drop" else merge
|
|
459
|
+
uns_merge = None if uns_merge == "drop" else uns_merge
|
|
460
|
+
|
|
452
461
|
try:
|
|
453
462
|
concat_stores(
|
|
454
463
|
files,
|
|
@@ -469,6 +478,134 @@ def concat(
|
|
|
469
478
|
raise typer.Exit(code=1)
|
|
470
479
|
|
|
471
480
|
|
|
481
|
+
# ============================================================================
|
|
482
|
+
# CONVERT command
|
|
483
|
+
# ============================================================================
|
|
484
|
+
@app.command("convert")
|
|
485
|
+
def convert(
|
|
486
|
+
file: Path = typer.Argument(
|
|
487
|
+
...,
|
|
488
|
+
help="Input .h5ad/.zarr",
|
|
489
|
+
exists=True,
|
|
490
|
+
readable=True,
|
|
491
|
+
dir_okay=True,
|
|
492
|
+
file_okay=True,
|
|
493
|
+
),
|
|
494
|
+
entries: Optional[List[str]] = typer.Argument(
|
|
495
|
+
None,
|
|
496
|
+
help="Matrix paths to convert, e.g. 'X', 'layers/counts', 'raw/X'",
|
|
497
|
+
),
|
|
498
|
+
output: Optional[Path] = typer.Option(
|
|
499
|
+
None,
|
|
500
|
+
"--output",
|
|
501
|
+
"-o",
|
|
502
|
+
help="Output .h5ad/.zarr path. Required unless --inplace.",
|
|
503
|
+
dir_okay=True,
|
|
504
|
+
file_okay=True,
|
|
505
|
+
),
|
|
506
|
+
inplace: bool = typer.Option(
|
|
507
|
+
False,
|
|
508
|
+
"--inplace",
|
|
509
|
+
help="Modify source file directly.",
|
|
510
|
+
),
|
|
511
|
+
convert_all: bool = typer.Option(
|
|
512
|
+
False,
|
|
513
|
+
"--all",
|
|
514
|
+
help="Convert X, every layer, and raw/X",
|
|
515
|
+
),
|
|
516
|
+
dtype: Optional[str] = typer.Option(
|
|
517
|
+
None,
|
|
518
|
+
"--dtype",
|
|
519
|
+
help="New dtype for the values, e.g. float32",
|
|
520
|
+
),
|
|
521
|
+
indices_dtype: Optional[str] = typer.Option(
|
|
522
|
+
None,
|
|
523
|
+
"--indices-dtype",
|
|
524
|
+
help="New dtype for sparse indices: int32 or int64",
|
|
525
|
+
),
|
|
526
|
+
layout: Optional[str] = typer.Option(
|
|
527
|
+
None,
|
|
528
|
+
"--layout",
|
|
529
|
+
help="Target layout: csr, csc, dense or sparse",
|
|
530
|
+
),
|
|
531
|
+
force: bool = typer.Option(
|
|
532
|
+
False,
|
|
533
|
+
"--force",
|
|
534
|
+
help="Convert despite a lossy cast or a large size increase",
|
|
535
|
+
),
|
|
536
|
+
in_memory: bool = typer.Option(
|
|
537
|
+
False,
|
|
538
|
+
"--in-memory",
|
|
539
|
+
help="Transpose in memory instead of streaming (faster if it fits)",
|
|
540
|
+
),
|
|
541
|
+
chunk_rows: int = typer.Option(
|
|
542
|
+
1024,
|
|
543
|
+
"--chunk",
|
|
544
|
+
"-C",
|
|
545
|
+
help="Row chunk size for dense matrices",
|
|
546
|
+
),
|
|
547
|
+
zarr_format: Optional[int] = typer.Option(
|
|
548
|
+
None,
|
|
549
|
+
"--zarr-format",
|
|
550
|
+
help="Zarr spec version to write (defaults to the source store's)",
|
|
551
|
+
),
|
|
552
|
+
) -> None:
|
|
553
|
+
"""
|
|
554
|
+
Change a matrix's dtype, layout or density.
|
|
555
|
+
|
|
556
|
+
Counts held as float64 cost twice the disk and twice the read for no
|
|
557
|
+
information; a tool that wants CSC cannot use a CSR store; and concat
|
|
558
|
+
refuses inputs whose encodings disagree. All three are this command.
|
|
559
|
+
|
|
560
|
+
A cast that would not round-trip is refused before anything is written,
|
|
561
|
+
as is a densification that would inflate the store; --force overrides
|
|
562
|
+
both. Transposing streams by default, so it works on matrices too large
|
|
563
|
+
to load.
|
|
564
|
+
|
|
565
|
+
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
|
|
570
|
+
"""
|
|
571
|
+
if not inplace and output is None:
|
|
572
|
+
console.print(
|
|
573
|
+
"[bold red]Error:[/] Output file is required. "
|
|
574
|
+
"Use --output/-o or --inplace.",
|
|
575
|
+
)
|
|
576
|
+
raise typer.Exit(code=1)
|
|
577
|
+
|
|
578
|
+
if layout is not None and layout not in LAYOUTS:
|
|
579
|
+
console.print(
|
|
580
|
+
f"[bold red]Error:[/] --layout must be one of: {', '.join(LAYOUTS)}."
|
|
581
|
+
)
|
|
582
|
+
raise typer.Exit(code=1)
|
|
583
|
+
|
|
584
|
+
if zarr_format is not None and zarr_format not in (2, 3):
|
|
585
|
+
console.print("[bold red]Error:[/] --zarr-format must be 2 or 3.")
|
|
586
|
+
raise typer.Exit(code=1)
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
convert_store(
|
|
590
|
+
file=file,
|
|
591
|
+
entries=list(entries) if entries else None,
|
|
592
|
+
output=output,
|
|
593
|
+
console=console,
|
|
594
|
+
dtype=dtype,
|
|
595
|
+
indices_dtype=indices_dtype,
|
|
596
|
+
layout=layout,
|
|
597
|
+
convert_all=convert_all,
|
|
598
|
+
inplace=inplace,
|
|
599
|
+
chunk_rows=chunk_rows,
|
|
600
|
+
in_memory=in_memory,
|
|
601
|
+
force=force,
|
|
602
|
+
zarr_format=zarr_format,
|
|
603
|
+
)
|
|
604
|
+
except Exception as e:
|
|
605
|
+
console.print(f"[bold red]Error:[/] {e}")
|
|
606
|
+
raise typer.Exit(code=1)
|
|
607
|
+
|
|
608
|
+
|
|
472
609
|
# ============================================================================
|
|
473
610
|
# SPLIT command
|
|
474
611
|
# ============================================================================
|
|
@@ -5,4 +5,5 @@ from adata.commands.import_data import import_object
|
|
|
5
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
|
-
from adata.commands.concat import MERGE_STRATEGIES, concat_stores
|
|
8
|
+
from adata.commands.concat import MERGE_CHOICES, MERGE_STRATEGIES, concat_stores
|
|
9
|
+
from adata.commands.convert import convert_store
|
|
@@ -7,9 +7,9 @@ from typing import Optional, Sequence
|
|
|
7
7
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
|
-
from adata.core.concat import MERGE_STRATEGIES, concat_on_disk
|
|
10
|
+
from adata.core.concat import MERGE_CHOICES, MERGE_STRATEGIES, concat_on_disk
|
|
11
11
|
|
|
12
|
-
__all__ = ["MERGE_STRATEGIES", "concat_stores"]
|
|
12
|
+
__all__ = ["MERGE_CHOICES", "MERGE_STRATEGIES", "concat_stores"]
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def concat_stores(
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""The `convert` command: rewrite matrices, copy everything else.
|
|
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.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, List, Optional
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from adata.core.convert import (
|
|
18
|
+
DEFAULT_CHUNK,
|
|
19
|
+
LAYOUTS,
|
|
20
|
+
Plan,
|
|
21
|
+
convert_matrix,
|
|
22
|
+
parse_dtype,
|
|
23
|
+
plan_conversion,
|
|
24
|
+
)
|
|
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
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _same_path(left: Path, right: Path) -> bool:
|
|
48
|
+
"""Do these name the same store, through symlinks and `..` alike?"""
|
|
49
|
+
try:
|
|
50
|
+
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
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def convert_store(
|
|
65
|
+
file: Path,
|
|
66
|
+
entries: Optional[List[str]],
|
|
67
|
+
output: Optional[Path],
|
|
68
|
+
console: Console,
|
|
69
|
+
*,
|
|
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.
|
|
103
|
+
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)
|
|
151
|
+
)
|
|
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
|
|
187
|
+
)
|
|
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
|
+
|
|
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)
|
|
211
|
+
|
|
212
|
+
|
|
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)
|