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/__init__.py +10 -0
- adata/cli.py +1139 -0
- adata/commands/__init__.py +8 -0
- adata/commands/concat.py +21 -0
- adata/commands/create.py +93 -0
- adata/commands/export.py +105 -0
- adata/commands/import_data.py +144 -0
- adata/commands/info.py +199 -0
- adata/commands/ls.py +114 -0
- adata/commands/split.py +152 -0
- adata/commands/subset.py +17 -0
- adata/core/__init__.py +1 -0
- adata/core/concat.py +1084 -0
- adata/core/info.py +276 -0
- adata/core/query.py +268 -0
- adata/core/read.py +28 -0
- adata/core/select.py +122 -0
- adata/core/subset.py +767 -0
- adata/elements/__init__.py +10 -0
- adata/elements/read.py +310 -0
- adata/elements/spec.py +125 -0
- adata/elements/strings.py +90 -0
- adata/elements/write.py +255 -0
- adata/formats/__init__.py +1 -0
- adata/formats/array.py +109 -0
- adata/formats/common.py +51 -0
- adata/formats/dataframe.py +234 -0
- adata/formats/image.py +85 -0
- adata/formats/json_data.py +196 -0
- adata/formats/sparse.py +253 -0
- adata/formats/validate.py +180 -0
- adata/info.py +3 -0
- adata/py.typed +0 -0
- adata/read.py +19 -0
- adata/storage/__init__.py +529 -0
- adata/util/__init__.py +1 -0
- adata/util/path.py +9 -0
- pyadata_cli-0.5.0.dist-info/METADATA +135 -0
- pyadata_cli-0.5.0.dist-info/RECORD +42 -0
- pyadata_cli-0.5.0.dist-info/WHEEL +4 -0
- pyadata_cli-0.5.0.dist-info/entry_points.txt +4 -0
- pyadata_cli-0.5.0.dist-info/licenses/LICENSE +21 -0
adata/commands/split.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""`split` -- write one store per distinct value of an annotation column.
|
|
2
|
+
|
|
3
|
+
Requested in issue #2, modelled on cellgeni/scraft's `split_h5ad` but
|
|
4
|
+
streaming: the source is never loaded into memory, only the column being split
|
|
5
|
+
on is read, and each output is produced by the same subset machinery used by
|
|
6
|
+
`adata subset`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import csv
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
from adata.core.select import group_indices
|
|
20
|
+
from adata.core.subset import subset_h5ad
|
|
21
|
+
from adata.storage import detect_backend, open_store
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sanitize_label(label: str) -> str:
|
|
25
|
+
"""Turn a column value into something safe to use as a filename."""
|
|
26
|
+
text = (label or "").strip()
|
|
27
|
+
if not text:
|
|
28
|
+
return "NA"
|
|
29
|
+
text = text.replace("/", "_").replace("\\", "_")
|
|
30
|
+
text = re.sub(r"\s+", "_", text)
|
|
31
|
+
text = re.sub(r"[^A-Za-z0-9_.-]+", "", text)
|
|
32
|
+
return text or "NA"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def unique_names(labels: List[str]) -> Dict[str, str]:
|
|
36
|
+
"""Map each label to a distinct filename stem.
|
|
37
|
+
|
|
38
|
+
Sanitising can map two different labels onto the same stem ("a/b" and
|
|
39
|
+
"a b"), so collisions get a numeric suffix rather than silently
|
|
40
|
+
overwriting one another.
|
|
41
|
+
"""
|
|
42
|
+
used: Dict[str, int] = {}
|
|
43
|
+
mapping: Dict[str, str] = {}
|
|
44
|
+
for label in labels:
|
|
45
|
+
base = sanitize_label(label)
|
|
46
|
+
count = used.get(base, 0)
|
|
47
|
+
used[base] = count + 1
|
|
48
|
+
mapping[label] = base if count == 0 else f"{base}_{count}"
|
|
49
|
+
return mapping
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def split_store(
|
|
53
|
+
file: Path,
|
|
54
|
+
column: str,
|
|
55
|
+
output_dir: Path,
|
|
56
|
+
console: Console,
|
|
57
|
+
axis: str = "obs",
|
|
58
|
+
suffix: Optional[str] = None,
|
|
59
|
+
dry_run: bool = False,
|
|
60
|
+
manifest: bool = True,
|
|
61
|
+
min_size: int = 1,
|
|
62
|
+
chunk_rows: int = 1024,
|
|
63
|
+
zarr_format: Optional[int] = None,
|
|
64
|
+
) -> List[Tuple[str, Path, int]]:
|
|
65
|
+
"""Split `file` into one store per distinct value of `column`.
|
|
66
|
+
|
|
67
|
+
Returns ``(label, path, n_rows)`` for each group written. Groups smaller
|
|
68
|
+
than `min_size` are skipped with a warning rather than producing tiny
|
|
69
|
+
stores nobody asked for.
|
|
70
|
+
|
|
71
|
+
`zarr_format` overrides the Zarr version of the outputs; without it they
|
|
72
|
+
follow the source store's.
|
|
73
|
+
"""
|
|
74
|
+
if axis not in ("obs", "var"):
|
|
75
|
+
raise ValueError("--axis must be 'obs' or 'var'.")
|
|
76
|
+
|
|
77
|
+
if suffix is None:
|
|
78
|
+
suffix = ".zarr" if detect_backend(file) == "zarr" else ".h5ad"
|
|
79
|
+
|
|
80
|
+
with open_store(file, "r") as store:
|
|
81
|
+
groups, order = group_indices(store.root, axis, column)
|
|
82
|
+
if zarr_format is None:
|
|
83
|
+
zarr_format = store.zarr_format
|
|
84
|
+
|
|
85
|
+
if not groups:
|
|
86
|
+
raise ValueError(f"Column {column!r} produced no groups.")
|
|
87
|
+
|
|
88
|
+
names = unique_names(order)
|
|
89
|
+
console.print(
|
|
90
|
+
f"[cyan]Splitting on {axis}[{column}]: "
|
|
91
|
+
f"{len(order)} group{'s' if len(order) != 1 else ''}[/]"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
planned: List[Tuple[str, Path, int]] = []
|
|
95
|
+
for label in order:
|
|
96
|
+
indices = groups[label]
|
|
97
|
+
out_path = output_dir / f"{names[label]}{suffix}"
|
|
98
|
+
if len(indices) < min_size:
|
|
99
|
+
console.print(
|
|
100
|
+
f"[yellow]Skipping {label!r}: {len(indices)} "
|
|
101
|
+
f"{axis} < --min-size {min_size}[/]"
|
|
102
|
+
)
|
|
103
|
+
continue
|
|
104
|
+
planned.append((label, out_path, len(indices)))
|
|
105
|
+
|
|
106
|
+
for label, out_path, count in planned:
|
|
107
|
+
console.print(f" {label!r} -> {out_path} ({count} {axis})")
|
|
108
|
+
|
|
109
|
+
if dry_run:
|
|
110
|
+
console.print("[yellow]Dry run: nothing written.[/]")
|
|
111
|
+
return planned
|
|
112
|
+
|
|
113
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
|
|
115
|
+
for label, out_path, _ in planned:
|
|
116
|
+
indices = np.sort(groups[label])
|
|
117
|
+
subset_h5ad(
|
|
118
|
+
file=file,
|
|
119
|
+
output=out_path,
|
|
120
|
+
obs_file=None,
|
|
121
|
+
var_file=None,
|
|
122
|
+
chunk_rows=chunk_rows,
|
|
123
|
+
console=console,
|
|
124
|
+
obs_indices=indices if axis == "obs" else None,
|
|
125
|
+
var_indices=indices if axis == "var" else None,
|
|
126
|
+
zarr_format=zarr_format,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if manifest:
|
|
130
|
+
_write_manifest(file, output_dir, column, planned, console)
|
|
131
|
+
|
|
132
|
+
console.print(f"[green]Wrote[/] {len(planned)} stores to {output_dir}")
|
|
133
|
+
return planned
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _write_manifest(
|
|
137
|
+
file: Path,
|
|
138
|
+
output_dir: Path,
|
|
139
|
+
column: str,
|
|
140
|
+
planned: List[Tuple[str, Path, int]],
|
|
141
|
+
console: Console,
|
|
142
|
+
) -> Path:
|
|
143
|
+
"""Record what was written, for downstream pipelines to consume."""
|
|
144
|
+
source_id = file.stem if file.suffix else file.name
|
|
145
|
+
path = output_dir / f"{source_id}_manifest.csv"
|
|
146
|
+
with open(path, "w", newline="", encoding="utf-8") as fh:
|
|
147
|
+
writer = csv.writer(fh)
|
|
148
|
+
writer.writerow(["id", "column", "value", "anndatas", "n"])
|
|
149
|
+
for label, out_path, count in planned:
|
|
150
|
+
writer.writerow([source_id, column, label, str(out_path), count])
|
|
151
|
+
console.print(f"[dim]Manifest written to {path}[/]")
|
|
152
|
+
return path
|
adata/commands/subset.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from adata.core.subset import (
|
|
2
|
+
_read_name_file,
|
|
3
|
+
indices_from_name_set,
|
|
4
|
+
subset_axis_group,
|
|
5
|
+
subset_dense_matrix,
|
|
6
|
+
subset_h5ad,
|
|
7
|
+
subset_sparse_matrix_group,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"_read_name_file",
|
|
12
|
+
"indices_from_name_set",
|
|
13
|
+
"subset_axis_group",
|
|
14
|
+
"subset_dense_matrix",
|
|
15
|
+
"subset_h5ad",
|
|
16
|
+
"subset_sparse_matrix_group",
|
|
17
|
+
]
|
adata/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core logic shared by CLI commands and format handlers."""
|