hca-anndata-tools 0.2.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.
- hca_anndata_tools-0.2.0/PKG-INFO +60 -0
- hca_anndata_tools-0.2.0/README.md +34 -0
- hca_anndata_tools-0.2.0/pyproject.toml +26 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/__init__.py +49 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/_io.py +27 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/_serialize.py +24 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/cap.py +162 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/convert.py +136 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/edit.py +206 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/files.py +25 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/plot.py +81 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/schema/__init__.py +1 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/schema/core.py +1583 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/schema/helpers.py +108 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/stats.py +129 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/storage.py +73 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/summary.py +78 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/testing.py +148 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/view.py +135 -0
- hca_anndata_tools-0.2.0/src/hca_anndata_tools/write.py +198 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hca-anndata-tools
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Library for inspection, summarization, and statistics of AnnData h5ad files
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Clever Canary
|
|
7
|
+
Author-email: dave@clevercanary.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: anndata (>=0.10,<1)
|
|
17
|
+
Requires-Dist: h5py (>=3.8)
|
|
18
|
+
Requires-Dist: matplotlib (>=3.7)
|
|
19
|
+
Requires-Dist: numpy (<3)
|
|
20
|
+
Requires-Dist: pandas (>2,<3)
|
|
21
|
+
Requires-Dist: pydantic (>=2,<3)
|
|
22
|
+
Requires-Dist: scanpy (>=1.10)
|
|
23
|
+
Project-URL: Homepage, https://github.com/clevercanary/hca-validation-tools
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# hca-anndata-tools
|
|
27
|
+
|
|
28
|
+
Library for inspection, summarization, and statistics of AnnData h5ad files.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install hca-anndata-tools
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from hca_anndata_tools import get_summary, get_descriptive_stats, locate_files
|
|
40
|
+
|
|
41
|
+
# Find h5ad files
|
|
42
|
+
files = locate_files("/path/to/data")
|
|
43
|
+
|
|
44
|
+
# Get structural overview
|
|
45
|
+
summary = get_summary("/path/to/my_atlas.h5ad")
|
|
46
|
+
|
|
47
|
+
# Get column statistics
|
|
48
|
+
stats = get_descriptive_stats("/path/to/my_atlas.h5ad", columns=["cell_type"], value_counts=True)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
- **locate_files** - Find all .h5ad files in a directory
|
|
54
|
+
- **get_summary** - Structural overview: cell/gene counts, columns, embeddings, layers
|
|
55
|
+
- **get_storage_info** - HDF5 compression, chunking, sparse format details
|
|
56
|
+
- **get_descriptive_stats** - Descriptive statistics and value counts for obs/var columns
|
|
57
|
+
- **view_data** - View slices of any attribute (obs, var, X, obsm, uns, etc.)
|
|
58
|
+
- **plot_embedding** - UMAP/PCA scatter plots as base64 PNG
|
|
59
|
+
- **get_cap_annotations** - Inspect CAP cell annotation metadata
|
|
60
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# hca-anndata-tools
|
|
2
|
+
|
|
3
|
+
Library for inspection, summarization, and statistics of AnnData h5ad files.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hca-anndata-tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from hca_anndata_tools import get_summary, get_descriptive_stats, locate_files
|
|
15
|
+
|
|
16
|
+
# Find h5ad files
|
|
17
|
+
files = locate_files("/path/to/data")
|
|
18
|
+
|
|
19
|
+
# Get structural overview
|
|
20
|
+
summary = get_summary("/path/to/my_atlas.h5ad")
|
|
21
|
+
|
|
22
|
+
# Get column statistics
|
|
23
|
+
stats = get_descriptive_stats("/path/to/my_atlas.h5ad", columns=["cell_type"], value_counts=True)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
- **locate_files** - Find all .h5ad files in a directory
|
|
29
|
+
- **get_summary** - Structural overview: cell/gene counts, columns, embeddings, layers
|
|
30
|
+
- **get_storage_info** - HDF5 compression, chunking, sparse format details
|
|
31
|
+
- **get_descriptive_stats** - Descriptive statistics and value counts for obs/var columns
|
|
32
|
+
- **view_data** - View slices of any attribute (obs, var, X, obsm, uns, etc.)
|
|
33
|
+
- **plot_embedding** - UMAP/PCA scatter plots as base64 PNG
|
|
34
|
+
- **get_cap_annotations** - Inspect CAP cell annotation metadata
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "hca-anndata-tools"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Library for inspection, summarization, and statistics of AnnData h5ad files"
|
|
5
|
+
authors = ["Clever Canary <dave@clevercanary.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
homepage = "https://github.com/clevercanary/hca-validation-tools"
|
|
9
|
+
packages = [{include = "hca_anndata_tools", from = "src"}]
|
|
10
|
+
|
|
11
|
+
[tool.poetry.dependencies]
|
|
12
|
+
python = "^3.10"
|
|
13
|
+
anndata = ">=0.10,<1"
|
|
14
|
+
pandas = ">2,<3"
|
|
15
|
+
numpy = "<3"
|
|
16
|
+
h5py = ">=3.8"
|
|
17
|
+
scanpy = ">=1.10"
|
|
18
|
+
matplotlib = ">=3.7"
|
|
19
|
+
pydantic = ">=2,<3"
|
|
20
|
+
|
|
21
|
+
[tool.poetry.group.dev.dependencies]
|
|
22
|
+
pytest = "^8.0"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""HCA AnnData Tools — inspection, summarization, and statistics for h5ad files."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"locate_files",
|
|
7
|
+
"get_summary",
|
|
8
|
+
"get_descriptive_stats",
|
|
9
|
+
"view_data",
|
|
10
|
+
"get_storage_info",
|
|
11
|
+
"get_cap_annotations",
|
|
12
|
+
"plot_embedding",
|
|
13
|
+
"write_h5ad",
|
|
14
|
+
"strip_timestamp",
|
|
15
|
+
"generate_output_path",
|
|
16
|
+
"generate_timestamp",
|
|
17
|
+
"EDIT_LOG_KEY",
|
|
18
|
+
"resolve_latest",
|
|
19
|
+
"set_uns",
|
|
20
|
+
"list_uns_fields",
|
|
21
|
+
"convert_cellxgene_to_hca",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
_LAZY_IMPORTS = {
|
|
25
|
+
"locate_files": ".files",
|
|
26
|
+
"get_summary": ".summary",
|
|
27
|
+
"get_descriptive_stats": ".stats",
|
|
28
|
+
"view_data": ".view",
|
|
29
|
+
"get_storage_info": ".storage",
|
|
30
|
+
"get_cap_annotations": ".cap",
|
|
31
|
+
"plot_embedding": ".plot",
|
|
32
|
+
"write_h5ad": ".write",
|
|
33
|
+
"strip_timestamp": ".write",
|
|
34
|
+
"generate_output_path": ".write",
|
|
35
|
+
"generate_timestamp": ".write",
|
|
36
|
+
"EDIT_LOG_KEY": ".write",
|
|
37
|
+
"resolve_latest": ".write",
|
|
38
|
+
"set_uns": ".edit",
|
|
39
|
+
"list_uns_fields": ".edit",
|
|
40
|
+
"convert_cellxgene_to_hca": ".convert",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __getattr__(name: str):
|
|
45
|
+
if name in _LAZY_IMPORTS:
|
|
46
|
+
import importlib
|
|
47
|
+
module = importlib.import_module(_LAZY_IMPORTS[name], __name__)
|
|
48
|
+
return getattr(module, name)
|
|
49
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Internal I/O utilities for AnnData file access."""
|
|
2
|
+
|
|
3
|
+
import gc
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
import anndata as ad
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@contextmanager
|
|
10
|
+
def open_h5ad(path: str, backed: str | None = "r"):
|
|
11
|
+
"""Open an h5ad file with automatic cleanup.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
path: Absolute path to an .h5ad file.
|
|
15
|
+
backed: Backing mode. Use "r" for read-only (default), None for full in-memory read.
|
|
16
|
+
|
|
17
|
+
Yields:
|
|
18
|
+
An AnnData object.
|
|
19
|
+
"""
|
|
20
|
+
adata = ad.read_h5ad(path, backed=backed)
|
|
21
|
+
try:
|
|
22
|
+
yield adata
|
|
23
|
+
finally:
|
|
24
|
+
if hasattr(adata, "file") and adata.file is not None:
|
|
25
|
+
adata.file.close()
|
|
26
|
+
del adata
|
|
27
|
+
gc.collect()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared serialization utilities for converting numpy types to JSON-serializable Python types."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def make_serializable(obj):
|
|
7
|
+
"""Recursively convert numpy types to JSON-serializable Python types."""
|
|
8
|
+
if isinstance(obj, (np.integer,)):
|
|
9
|
+
return int(obj)
|
|
10
|
+
if isinstance(obj, (np.floating,)):
|
|
11
|
+
return float(obj)
|
|
12
|
+
if isinstance(obj, (np.bool_,)):
|
|
13
|
+
return bool(obj)
|
|
14
|
+
if isinstance(obj, (np.str_, np.bytes_)):
|
|
15
|
+
return str(obj)
|
|
16
|
+
if isinstance(obj, np.ndarray):
|
|
17
|
+
return obj.tolist()
|
|
18
|
+
if isinstance(obj, bytes):
|
|
19
|
+
return obj.decode("utf-8", errors="replace")
|
|
20
|
+
if isinstance(obj, dict):
|
|
21
|
+
return {k: make_serializable(v) for k, v in obj.items()}
|
|
22
|
+
if isinstance(obj, (list, tuple)):
|
|
23
|
+
return [make_serializable(v) for v in obj]
|
|
24
|
+
return obj
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""CAP (Cell Annotation Platform) schema inspection for AnnData files."""
|
|
2
|
+
|
|
3
|
+
from ._io import open_h5ad
|
|
4
|
+
from ._serialize import make_serializable as _make_serializable
|
|
5
|
+
|
|
6
|
+
# CAP obs column suffixes per the cell-annotation-schema spec
|
|
7
|
+
_REQUIRED_SUFFIXES = [
|
|
8
|
+
"", # the cell label column itself
|
|
9
|
+
"--cell_fullname",
|
|
10
|
+
"--cell_ontology_exists",
|
|
11
|
+
"--cell_ontology_term_id",
|
|
12
|
+
"--cell_ontology_term",
|
|
13
|
+
"--rationale",
|
|
14
|
+
"--marker_gene_evidence",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
_OPTIONAL_SUFFIXES = [
|
|
18
|
+
"--rationale_dois",
|
|
19
|
+
"--canonical_marker_genes",
|
|
20
|
+
"--synonyms",
|
|
21
|
+
"--category_fullname",
|
|
22
|
+
"--category_cell_ontology_term_id",
|
|
23
|
+
"--category_cell_ontology_term",
|
|
24
|
+
"--cell_ontology_assessment",
|
|
25
|
+
"--confidence_score",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
_UNS_REQUIRED_KEYS = [
|
|
29
|
+
"cellannotation_schema_version",
|
|
30
|
+
"cellannotation_metadata",
|
|
31
|
+
"title",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
_UNS_OPTIONAL_KEYS = [
|
|
35
|
+
"publication_timestamp",
|
|
36
|
+
"publication_version",
|
|
37
|
+
"description",
|
|
38
|
+
"cap_dataset_url",
|
|
39
|
+
"cap_publication_title",
|
|
40
|
+
"cap_publication_description",
|
|
41
|
+
"cap_publication_url",
|
|
42
|
+
"authors_list",
|
|
43
|
+
"hierarchy",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _find_annotation_sets(obs_columns: list[str]) -> list[str]:
|
|
48
|
+
"""Identify CAP annotation set names from obs columns with -- separator."""
|
|
49
|
+
sets = set()
|
|
50
|
+
for col in obs_columns:
|
|
51
|
+
if "--" in col:
|
|
52
|
+
setname = col.split("--")[0]
|
|
53
|
+
sets.add(setname)
|
|
54
|
+
return sorted(sets)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_unique_values(series, max_values: int = 50) -> list:
|
|
58
|
+
"""Get unique values from a series, capped at max_values."""
|
|
59
|
+
uniques = [_make_serializable(v) for v in series.dropna().unique().tolist()]
|
|
60
|
+
if len(uniques) > max_values:
|
|
61
|
+
return uniques[:max_values] + [f"... and {len(uniques) - max_values} more"]
|
|
62
|
+
return uniques
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_cap_annotations(path: str, annotation_set: str | None = None) -> dict:
|
|
66
|
+
"""Inspect CAP (Cell Annotation Platform) annotations in an AnnData file.
|
|
67
|
+
|
|
68
|
+
Detects CAP-formatted annotation sets, lists cell labels with their ontology
|
|
69
|
+
mappings, marker genes, and rationale. Reports schema compliance.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
path: Absolute path to an .h5ad file.
|
|
73
|
+
annotation_set: Specific annotation set name to inspect. If None, lists all sets found.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
with open_h5ad(path) as adata:
|
|
77
|
+
obs_columns = list(adata.obs.columns)
|
|
78
|
+
annotation_sets = _find_annotation_sets(obs_columns)
|
|
79
|
+
|
|
80
|
+
uns_keys = list(adata.uns.keys())
|
|
81
|
+
uns_present = [k for k in _UNS_REQUIRED_KEYS if k in uns_keys]
|
|
82
|
+
uns_missing = [k for k in _UNS_REQUIRED_KEYS if k not in uns_keys]
|
|
83
|
+
uns_optional_present = [k for k in _UNS_OPTIONAL_KEYS if k in uns_keys]
|
|
84
|
+
|
|
85
|
+
has_cap = len(annotation_sets) > 0 or "cellannotation_metadata" in uns_keys
|
|
86
|
+
|
|
87
|
+
result = {
|
|
88
|
+
"has_cap_annotations": has_cap,
|
|
89
|
+
"annotation_sets": annotation_sets,
|
|
90
|
+
"uns_metadata": {
|
|
91
|
+
"required_present": uns_present,
|
|
92
|
+
"required_missing": uns_missing,
|
|
93
|
+
"optional_present": uns_optional_present,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if not has_cap and annotation_set is None:
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
if "cellannotation_metadata" in uns_keys:
|
|
101
|
+
result["cellannotation_metadata"] = _make_serializable(adata.uns["cellannotation_metadata"])
|
|
102
|
+
|
|
103
|
+
if "authors_list" in uns_keys:
|
|
104
|
+
result["authors_list"] = _make_serializable(adata.uns["authors_list"])
|
|
105
|
+
|
|
106
|
+
sets_to_inspect = []
|
|
107
|
+
if annotation_set:
|
|
108
|
+
if annotation_set in annotation_sets:
|
|
109
|
+
sets_to_inspect = [annotation_set]
|
|
110
|
+
else:
|
|
111
|
+
return {"error": f"Annotation set '{annotation_set}' not found. Available: {annotation_sets}"}
|
|
112
|
+
elif len(annotation_sets) <= 3:
|
|
113
|
+
sets_to_inspect = annotation_sets
|
|
114
|
+
|
|
115
|
+
set_details = {}
|
|
116
|
+
for setname in sets_to_inspect:
|
|
117
|
+
detail = {}
|
|
118
|
+
|
|
119
|
+
required_found = []
|
|
120
|
+
required_missing = []
|
|
121
|
+
for suffix in _REQUIRED_SUFFIXES:
|
|
122
|
+
col = f"{setname}{suffix}" if suffix else setname
|
|
123
|
+
if col in obs_columns:
|
|
124
|
+
required_found.append(col)
|
|
125
|
+
else:
|
|
126
|
+
required_missing.append(col)
|
|
127
|
+
|
|
128
|
+
optional_found = []
|
|
129
|
+
for suffix in _OPTIONAL_SUFFIXES:
|
|
130
|
+
col = f"{setname}{suffix}"
|
|
131
|
+
if col in obs_columns:
|
|
132
|
+
optional_found.append(col)
|
|
133
|
+
|
|
134
|
+
detail["required_columns_present"] = required_found
|
|
135
|
+
detail["required_columns_missing"] = required_missing
|
|
136
|
+
detail["optional_columns_present"] = optional_found
|
|
137
|
+
|
|
138
|
+
if setname in obs_columns:
|
|
139
|
+
vc = adata.obs[setname].value_counts()
|
|
140
|
+
detail["cell_labels"] = {str(k): int(v) for k, v in vc.items()}
|
|
141
|
+
|
|
142
|
+
ont_col = f"{setname}--cell_ontology_term_id"
|
|
143
|
+
if ont_col in obs_columns:
|
|
144
|
+
detail["ontology_terms"] = _get_unique_values(adata.obs[ont_col])
|
|
145
|
+
|
|
146
|
+
marker_col = f"{setname}--marker_gene_evidence"
|
|
147
|
+
if marker_col in obs_columns:
|
|
148
|
+
detail["marker_genes"] = _get_unique_values(adata.obs[marker_col])
|
|
149
|
+
|
|
150
|
+
rat_col = f"{setname}--rationale"
|
|
151
|
+
if rat_col in obs_columns:
|
|
152
|
+
detail["rationale"] = _get_unique_values(adata.obs[rat_col])
|
|
153
|
+
|
|
154
|
+
set_details[setname] = detail
|
|
155
|
+
|
|
156
|
+
if set_details:
|
|
157
|
+
result["set_details"] = set_details
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
return {"error": str(e)}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Convert CellxGENE h5ad files to HCA schema format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from ._io import open_h5ad
|
|
13
|
+
from ._serialize import make_serializable
|
|
14
|
+
from .write import generate_timestamp, write_h5ad
|
|
15
|
+
from . import __version__
|
|
16
|
+
|
|
17
|
+
# CellxGENE reserved uns keys — moved to cellxgene_source, not deleted
|
|
18
|
+
_CELLXGENE_RESERVED_UNS = ["schema_version", "schema_reference", "citation"]
|
|
19
|
+
|
|
20
|
+
# CellxGENE uns keys that need to be broadcast to obs
|
|
21
|
+
_UNS_TO_OBS = ["organism_ontology_term_id", "organism"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _slugify(text: str, max_length: int = 80) -> str:
|
|
25
|
+
"""Convert text to a filename-safe slug."""
|
|
26
|
+
slug = text.lower()
|
|
27
|
+
slug = re.sub(r"[^a-z0-9]+", "-", slug)
|
|
28
|
+
slug = slug.strip("-")
|
|
29
|
+
if len(slug) > max_length:
|
|
30
|
+
slug = slug[:max_length].rstrip("-")
|
|
31
|
+
return slug or "untitled"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def convert_cellxgene_to_hca(
|
|
35
|
+
path: str,
|
|
36
|
+
output_dir: str | None = None,
|
|
37
|
+
) -> dict:
|
|
38
|
+
"""Convert a CellxGENE h5ad file to HCA schema format.
|
|
39
|
+
|
|
40
|
+
Preserves cellxgene provenance in uns['cellxgene_source'], broadcasts
|
|
41
|
+
organism from uns to obs, and renames the output from the dataset title.
|
|
42
|
+
Does not add missing HCA-specific fields — wranglers fill those later.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
path: Path to a CellxGENE h5ad file.
|
|
46
|
+
output_dir: Directory for the output file. Defaults to same as source.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict with 'output_path', 'source', 'title', 'conversions' on success,
|
|
50
|
+
or 'error' on failure.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
with open_h5ad(path, backed=None) as adata:
|
|
54
|
+
# Verify this is a cellxgene 6.0+ file (organism in uns, single species)
|
|
55
|
+
if "schema_version" not in adata.uns:
|
|
56
|
+
return {"error": "Not a CellxGENE file — uns['schema_version'] is missing"}
|
|
57
|
+
|
|
58
|
+
schema_ver = str(adata.uns["schema_version"]).strip()
|
|
59
|
+
if not schema_ver:
|
|
60
|
+
return {"error": "uns['schema_version'] is empty — cannot determine CellxGENE schema version"}
|
|
61
|
+
major = int(schema_ver.split(".")[0]) if schema_ver[0].isdigit() else 0
|
|
62
|
+
if major < 6:
|
|
63
|
+
return {
|
|
64
|
+
"error": (
|
|
65
|
+
f"CellxGENE schema {schema_ver} is not supported. "
|
|
66
|
+
f"Requires 6.0+ (organism in uns, single species per dataset)."
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
title = adata.uns.get("title", "")
|
|
71
|
+
if not title:
|
|
72
|
+
return {"error": "File has no title in uns — cannot generate output filename"}
|
|
73
|
+
|
|
74
|
+
conversions = []
|
|
75
|
+
|
|
76
|
+
# --- 1. Preserve cellxgene provenance ---
|
|
77
|
+
cellxgene_source = {}
|
|
78
|
+
for key in _CELLXGENE_RESERVED_UNS:
|
|
79
|
+
if key in adata.uns:
|
|
80
|
+
cellxgene_source[key] = make_serializable(adata.uns[key])
|
|
81
|
+
del adata.uns[key]
|
|
82
|
+
|
|
83
|
+
if cellxgene_source:
|
|
84
|
+
adata.uns["cellxgene_source"] = cellxgene_source
|
|
85
|
+
conversions.append(
|
|
86
|
+
f"Moved cellxgene reserved keys to uns['cellxgene_source']: "
|
|
87
|
+
f"{list(cellxgene_source.keys())}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# --- 2. Broadcast organism from uns → obs ---
|
|
91
|
+
for key in _UNS_TO_OBS:
|
|
92
|
+
if key in adata.uns:
|
|
93
|
+
value = adata.uns[key]
|
|
94
|
+
adata.obs[key] = pd.Categorical.from_codes(
|
|
95
|
+
np.zeros(adata.n_obs, dtype=np.int8),
|
|
96
|
+
categories=[value],
|
|
97
|
+
)
|
|
98
|
+
del adata.uns[key]
|
|
99
|
+
conversions.append(f"{key}: uns → obs (broadcast '{value}')")
|
|
100
|
+
|
|
101
|
+
# --- 3. Build output path from title slug ---
|
|
102
|
+
slug = _slugify(title)
|
|
103
|
+
timestamp = generate_timestamp()
|
|
104
|
+
out_filename = f"{slug}-edit-{timestamp}.h5ad"
|
|
105
|
+
directory = output_dir if output_dir is not None else os.path.dirname(path)
|
|
106
|
+
output_path = os.path.join(directory, out_filename)
|
|
107
|
+
|
|
108
|
+
# --- 4. Build edit log entry ---
|
|
109
|
+
entry = {
|
|
110
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
111
|
+
"tool": "hca-anndata-tools",
|
|
112
|
+
"tool_version": __version__,
|
|
113
|
+
"operation": "import_cellxgene",
|
|
114
|
+
"description": f"Imported from CellxGENE Discover: {title}",
|
|
115
|
+
"details": {
|
|
116
|
+
"source_schema_version": cellxgene_source.get("schema_version"),
|
|
117
|
+
"source_citation": cellxgene_source.get("citation"),
|
|
118
|
+
"conversions": conversions,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# --- 5. Write ---
|
|
123
|
+
result = write_h5ad(adata, path, [entry], output_path=output_path)
|
|
124
|
+
|
|
125
|
+
if "error" in result:
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
**result,
|
|
130
|
+
"source": os.path.basename(path),
|
|
131
|
+
"title": title,
|
|
132
|
+
"conversions": conversions,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
return {"error": str(e)}
|