ngio 0.1.6__py3-none-any.whl → 0.2.0a2__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.
- ngio/__init__.py +31 -5
- ngio/common/__init__.py +44 -0
- ngio/common/_array_pipe.py +160 -0
- ngio/common/_axes_transforms.py +63 -0
- ngio/common/_common_types.py +5 -0
- ngio/common/_dimensions.py +113 -0
- ngio/common/_pyramid.py +223 -0
- ngio/{core/roi.py → common/_roi.py} +22 -23
- ngio/common/_slicer.py +97 -0
- ngio/{pipes/_zoom_utils.py → common/_zoom.py} +2 -78
- ngio/hcs/__init__.py +60 -0
- ngio/images/__init__.py +23 -0
- ngio/images/abstract_image.py +240 -0
- ngio/images/create.py +251 -0
- ngio/images/image.py +389 -0
- ngio/images/label.py +236 -0
- ngio/images/omezarr_container.py +535 -0
- ngio/ome_zarr_meta/__init__.py +35 -0
- ngio/ome_zarr_meta/_generic_handlers.py +320 -0
- ngio/ome_zarr_meta/_meta_handlers.py +142 -0
- ngio/ome_zarr_meta/ngio_specs/__init__.py +63 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
- ngio/ome_zarr_meta/ngio_specs/_channels.py +378 -0
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +5 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +434 -0
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +84 -0
- ngio/ome_zarr_meta/v04/__init__.py +11 -0
- ngio/ome_zarr_meta/v04/_meta_handlers.py +54 -0
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +412 -0
- ngio/tables/__init__.py +21 -5
- ngio/tables/_validators.py +192 -0
- ngio/tables/backends/__init__.py +8 -0
- ngio/tables/backends/_abstract_backend.py +71 -0
- ngio/tables/backends/_anndata_utils.py +194 -0
- ngio/tables/backends/_anndata_v1.py +75 -0
- ngio/tables/backends/_json_v1.py +56 -0
- ngio/tables/backends/_table_backends.py +102 -0
- ngio/tables/tables_container.py +300 -0
- ngio/tables/v1/__init__.py +6 -5
- ngio/tables/v1/_feature_table.py +161 -0
- ngio/tables/v1/_generic_table.py +99 -182
- ngio/tables/v1/_masking_roi_table.py +175 -0
- ngio/tables/v1/_roi_table.py +226 -0
- ngio/utils/__init__.py +23 -10
- ngio/utils/_datasets.py +51 -0
- ngio/utils/_errors.py +10 -4
- ngio/utils/_zarr_utils.py +378 -0
- {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/METADATA +18 -39
- ngio-0.2.0a2.dist-info/RECORD +53 -0
- ngio/core/__init__.py +0 -7
- ngio/core/dimensions.py +0 -122
- ngio/core/image_handler.py +0 -228
- ngio/core/image_like_handler.py +0 -549
- ngio/core/label_handler.py +0 -410
- ngio/core/ngff_image.py +0 -387
- ngio/core/utils.py +0 -287
- ngio/io/__init__.py +0 -19
- ngio/io/_zarr.py +0 -88
- ngio/io/_zarr_array_utils.py +0 -0
- ngio/io/_zarr_group_utils.py +0 -60
- ngio/iterators/__init__.py +0 -1
- ngio/ngff_meta/__init__.py +0 -27
- ngio/ngff_meta/fractal_image_meta.py +0 -1267
- ngio/ngff_meta/meta_handler.py +0 -92
- ngio/ngff_meta/utils.py +0 -235
- ngio/ngff_meta/v04/__init__.py +0 -6
- ngio/ngff_meta/v04/specs.py +0 -158
- ngio/ngff_meta/v04/zarr_utils.py +0 -376
- ngio/pipes/__init__.py +0 -7
- ngio/pipes/_slicer_transforms.py +0 -176
- ngio/pipes/_transforms.py +0 -33
- ngio/pipes/data_pipe.py +0 -52
- ngio/tables/_ad_reader.py +0 -80
- ngio/tables/_utils.py +0 -301
- ngio/tables/tables_group.py +0 -252
- ngio/tables/v1/feature_tables.py +0 -182
- ngio/tables/v1/masking_roi_tables.py +0 -243
- ngio/tables/v1/roi_tables.py +0 -285
- ngio/utils/_common_types.py +0 -5
- ngio/utils/_pydantic_utils.py +0 -52
- ngio-0.1.6.dist-info/RECORD +0 -44
- {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/WHEEL +0 -0
- {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Collection
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from anndata import AnnData
|
|
6
|
+
from pandas import DataFrame
|
|
7
|
+
|
|
8
|
+
from ngio.utils import ZarrGroupHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractTableBackend(ABC):
|
|
12
|
+
"""Abstract class for table backends."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
group_handler: ZarrGroupHandler,
|
|
17
|
+
index_key: str | None = None,
|
|
18
|
+
index_type: Literal["int", "str"] = "int",
|
|
19
|
+
):
|
|
20
|
+
"""Initialize the handler.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
group_handler (ZarrGroupHandler): An object to handle the Zarr group
|
|
24
|
+
containing the table data.
|
|
25
|
+
index_key (str): The column name to use as the index of the DataFrame.
|
|
26
|
+
index_type (str): The type of the index column in the DataFrame.
|
|
27
|
+
"""
|
|
28
|
+
self._group_handler = group_handler
|
|
29
|
+
self._index_key = index_key
|
|
30
|
+
self._index_type = index_type
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def backend_name() -> str:
|
|
35
|
+
"""The name of the backend."""
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def implements_anndata() -> bool:
|
|
41
|
+
"""Whether the handler implements the anndata protocol."""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def implements_dataframe() -> bool:
|
|
47
|
+
"""Whether the handler implements the dataframe protocol."""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def load_columns(self) -> list[str]:
|
|
52
|
+
"""List all labels in the group."""
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData:
|
|
56
|
+
"""Load the metadata in the store."""
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
|
|
60
|
+
"""List all labels in the group."""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def write_from_dataframe(
|
|
64
|
+
self, table: DataFrame, metadata: dict | None = None
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Consolidate the metadata in the store."""
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
def write_from_anndata(self, table: AnnData, metadata: dict | None = None) -> None:
|
|
70
|
+
"""Consolidate the metadata in the store."""
|
|
71
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
4
|
+
|
|
5
|
+
import anndata as ad
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pandas.api.types as ptypes
|
|
9
|
+
import zarr
|
|
10
|
+
from anndata import AnnData
|
|
11
|
+
from anndata._io.specs import read_elem
|
|
12
|
+
from anndata._io.utils import _read_legacy_raw
|
|
13
|
+
from anndata._io.zarr import read_dataframe
|
|
14
|
+
from anndata.compat import _clean_uns
|
|
15
|
+
from anndata.experimental import read_dispatched
|
|
16
|
+
|
|
17
|
+
from ngio.tables._validators import validate_index_dtype, validate_index_key
|
|
18
|
+
from ngio.utils import (
|
|
19
|
+
NgioTableValidationError,
|
|
20
|
+
StoreOrGroup,
|
|
21
|
+
open_group_wrapper,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable, Collection
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def custom_read_zarr(
|
|
29
|
+
store: StoreOrGroup, elem_to_read: Collection[str] | None = None
|
|
30
|
+
) -> AnnData:
|
|
31
|
+
"""Read from a hierarchical Zarr array store.
|
|
32
|
+
|
|
33
|
+
# Implementation originally from https://github.com/scverse/anndata/blob/main/src/anndata/_io/zarr.py
|
|
34
|
+
# Original implementation would not work with remote storages so we had to copy it
|
|
35
|
+
# here and slightly modified it to work with remote storages.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
store (StoreOrGroup): A store or group to read the AnnData from.
|
|
39
|
+
elem_to_read (Collection[str] | None): The elements to read from the store.
|
|
40
|
+
"""
|
|
41
|
+
group, _ = open_group_wrapper(store=store, mode="r")
|
|
42
|
+
|
|
43
|
+
if elem_to_read is None:
|
|
44
|
+
elem_to_read = [
|
|
45
|
+
"X",
|
|
46
|
+
"obs",
|
|
47
|
+
"var",
|
|
48
|
+
"uns",
|
|
49
|
+
"obsm",
|
|
50
|
+
"varm",
|
|
51
|
+
"obsp",
|
|
52
|
+
"varp",
|
|
53
|
+
"layers",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Read with handling for backwards compat
|
|
57
|
+
def callback(func: Callable, elem_name: str, elem: Any, iospec: Any) -> Any:
|
|
58
|
+
if iospec.encoding_type == "anndata" or elem_name.endswith("/"):
|
|
59
|
+
ad_kwargs = {}
|
|
60
|
+
# Some of these elem fail on https
|
|
61
|
+
# So we only include the ones that are strictly necessary
|
|
62
|
+
# for fractal tables
|
|
63
|
+
# This fails on some https
|
|
64
|
+
# base_elem += list(elem.keys())
|
|
65
|
+
for k in elem_to_read:
|
|
66
|
+
v = elem.get(k)
|
|
67
|
+
if v is not None and not k.startswith("raw."):
|
|
68
|
+
ad_kwargs[k] = read_dispatched(v, callback) # type: ignore
|
|
69
|
+
return AnnData(**ad_kwargs)
|
|
70
|
+
|
|
71
|
+
elif elem_name.startswith("/raw."):
|
|
72
|
+
return None
|
|
73
|
+
elif elem_name in {"/obs", "/var"}:
|
|
74
|
+
return read_dataframe(elem)
|
|
75
|
+
elif elem_name == "/raw":
|
|
76
|
+
# Backwards compat
|
|
77
|
+
return _read_legacy_raw(group, func(elem), read_dataframe, func)
|
|
78
|
+
return func(elem)
|
|
79
|
+
|
|
80
|
+
adata = read_dispatched(group, callback=callback) # type: ignore
|
|
81
|
+
|
|
82
|
+
# Backwards compat (should figure out which version)
|
|
83
|
+
if "raw.X" in group:
|
|
84
|
+
raw = AnnData(**_read_legacy_raw(group, adata.raw, read_dataframe, read_elem)) # type: ignore
|
|
85
|
+
raw.obs_names = adata.obs_names # type: ignore
|
|
86
|
+
adata.raw = raw # type: ignore
|
|
87
|
+
|
|
88
|
+
# Backwards compat for <0.7
|
|
89
|
+
if isinstance(group["obs"], zarr.Array):
|
|
90
|
+
_clean_uns(adata)
|
|
91
|
+
|
|
92
|
+
if not isinstance(adata, AnnData):
|
|
93
|
+
raise ValueError(f"Expected an AnnData object, but got {type(adata)}")
|
|
94
|
+
return adata
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _check_for_mixed_types(series: pd.Series) -> None:
|
|
98
|
+
"""Check if the column has mixed types."""
|
|
99
|
+
if series.apply(type).nunique() > 1: # type: ignore
|
|
100
|
+
raise NgioTableValidationError(
|
|
101
|
+
f"Column {series.name} has mixed types: "
|
|
102
|
+
f"{series.apply(type).unique()}. " # type: ignore
|
|
103
|
+
"Type of all elements must be the same."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _check_for_supported_types(series: pd.Series) -> Literal["str", "int", "numeric"]:
|
|
108
|
+
"""Check if the column has supported types."""
|
|
109
|
+
if ptypes.is_string_dtype(series):
|
|
110
|
+
return "str"
|
|
111
|
+
if ptypes.is_integer_dtype(series):
|
|
112
|
+
return "int"
|
|
113
|
+
if ptypes.is_numeric_dtype(series):
|
|
114
|
+
return "numeric"
|
|
115
|
+
raise NgioTableValidationError(
|
|
116
|
+
f"Column {series.name} has unsupported type: {series.dtype}."
|
|
117
|
+
" Supported types are string and numerics."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def dataframe_to_anndata(
|
|
122
|
+
dataframe: pd.DataFrame,
|
|
123
|
+
index_key: str | None = None,
|
|
124
|
+
overwrite: bool = False,
|
|
125
|
+
) -> ad.AnnData:
|
|
126
|
+
"""Convert a table DataFrame to an AnnData object.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
dataframe (pd.DataFrame): A pandas DataFrame representing a fractal table.
|
|
130
|
+
index_key (str): The column name to use as the index of the DataFrame.
|
|
131
|
+
Default is None.
|
|
132
|
+
overwrite (bool): Whether to overwrite the index if a different index is found.
|
|
133
|
+
Default is False.
|
|
134
|
+
"""
|
|
135
|
+
# Check if the index_key is present in the data frame + optional validations
|
|
136
|
+
dataframe = validate_index_key(dataframe, index_key, overwrite=overwrite)
|
|
137
|
+
dataframe = validate_index_dtype(dataframe, index_type="str")
|
|
138
|
+
|
|
139
|
+
str_columns, int_columns, num_columns = [], [], []
|
|
140
|
+
for c_name in dataframe.columns:
|
|
141
|
+
column_df = dataframe[c_name]
|
|
142
|
+
_check_for_mixed_types(column_df) # Mixed types are not allowed in the table
|
|
143
|
+
c_type = _check_for_supported_types(
|
|
144
|
+
column_df
|
|
145
|
+
) # Only string and numeric types are allowed
|
|
146
|
+
|
|
147
|
+
if c_type == "str":
|
|
148
|
+
str_columns.append(c_name)
|
|
149
|
+
|
|
150
|
+
elif c_type == "int":
|
|
151
|
+
int_columns.append(c_name)
|
|
152
|
+
|
|
153
|
+
elif c_type == "numeric":
|
|
154
|
+
num_columns.append(c_name)
|
|
155
|
+
|
|
156
|
+
# Converting all observations to string
|
|
157
|
+
obs_df = dataframe[str_columns + int_columns]
|
|
158
|
+
obs_df.index = dataframe.index
|
|
159
|
+
|
|
160
|
+
x_df = dataframe[num_columns]
|
|
161
|
+
|
|
162
|
+
if x_df.dtypes.nunique() > 1:
|
|
163
|
+
x_df = x_df.astype("float64")
|
|
164
|
+
|
|
165
|
+
if x_df.empty:
|
|
166
|
+
# If there are no numeric columns, create an empty array
|
|
167
|
+
# to avoid AnnData failing to create the object
|
|
168
|
+
x_df = np.zeros((len(obs_df), 0), dtype="float64")
|
|
169
|
+
|
|
170
|
+
return ad.AnnData(X=x_df, obs=obs_df)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def anndata_to_dataframe(
|
|
174
|
+
anndata: ad.AnnData,
|
|
175
|
+
index_key: str | None = "label",
|
|
176
|
+
index_type: str = "int",
|
|
177
|
+
overwrite: bool = False,
|
|
178
|
+
) -> pd.DataFrame:
|
|
179
|
+
"""Convert a AnnData object representing a fractal table to a pandas DataFrame.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
anndata (ad.AnnData): An AnnData object representing a fractal table.
|
|
183
|
+
index_key (str): The column name to use as the index of the DataFrame.
|
|
184
|
+
Default is 'label'.
|
|
185
|
+
index_type (str): The type of the index column in the DataFrame.
|
|
186
|
+
Either 'str' or 'int'. Default is 'int'.
|
|
187
|
+
overwrite (bool): Whether to overwrite the index if a different index is found.
|
|
188
|
+
Default is False.
|
|
189
|
+
"""
|
|
190
|
+
dataframe = anndata.to_df()
|
|
191
|
+
dataframe[anndata.obs_keys()] = anndata.obs
|
|
192
|
+
dataframe = validate_index_key(dataframe, index_key, overwrite=overwrite)
|
|
193
|
+
dataframe = validate_index_dtype(dataframe, index_type)
|
|
194
|
+
return dataframe
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from anndata import AnnData
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
|
|
7
|
+
from ngio.tables.backends._abstract_backend import AbstractTableBackend
|
|
8
|
+
from ngio.tables.backends._anndata_utils import (
|
|
9
|
+
anndata_to_dataframe,
|
|
10
|
+
custom_read_zarr,
|
|
11
|
+
dataframe_to_anndata,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AnnDataBackend(AbstractTableBackend):
|
|
16
|
+
"""A class to load and write tables from/to an AnnData object."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def backend_name() -> str:
|
|
20
|
+
"""The name of the backend."""
|
|
21
|
+
return "anndata_v1"
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def implements_anndata() -> bool:
|
|
25
|
+
"""Whether the handler implements the anndata protocol."""
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def implements_dataframe() -> bool:
|
|
30
|
+
"""Whether the handler implements the dataframe protocol."""
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
def load_columns(self) -> list[str]:
|
|
34
|
+
"""List all labels in the group."""
|
|
35
|
+
return list(self.load_as_dataframe().columns)
|
|
36
|
+
|
|
37
|
+
def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData:
|
|
38
|
+
"""Load the metadata in the store."""
|
|
39
|
+
anndata = custom_read_zarr(self._group_handler._group)
|
|
40
|
+
if columns is not None:
|
|
41
|
+
raise NotImplementedError(
|
|
42
|
+
"Selecting columns is not implemented for AnnData."
|
|
43
|
+
)
|
|
44
|
+
return anndata
|
|
45
|
+
|
|
46
|
+
def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
|
|
47
|
+
"""List all labels in the group."""
|
|
48
|
+
dataframe = anndata_to_dataframe(
|
|
49
|
+
self.load_as_anndata(),
|
|
50
|
+
index_key=self._index_key,
|
|
51
|
+
index_type=self._index_type,
|
|
52
|
+
)
|
|
53
|
+
if columns is not None:
|
|
54
|
+
dataframe = dataframe[columns]
|
|
55
|
+
return dataframe
|
|
56
|
+
|
|
57
|
+
def write_from_dataframe(
|
|
58
|
+
self, table: DataFrame, metadata: dict | None = None
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Consolidate the metadata in the store."""
|
|
61
|
+
anndata = dataframe_to_anndata(table, index_key=self._index_key)
|
|
62
|
+
self.write_from_anndata(anndata, metadata)
|
|
63
|
+
|
|
64
|
+
def write_from_anndata(self, table: AnnData, metadata: dict | None = None) -> None:
|
|
65
|
+
"""Consolidate the metadata in the store."""
|
|
66
|
+
store = self._group_handler.store
|
|
67
|
+
if not isinstance(store, str | Path):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"To write an AnnData object the store must be a local path/str."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
store = Path(store) / self._group_handler.group.path
|
|
73
|
+
table.write_zarr(store)
|
|
74
|
+
if metadata is not None:
|
|
75
|
+
self._group_handler.write_attrs(metadata)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from pandas import DataFrame
|
|
5
|
+
|
|
6
|
+
from ngio.tables.backends._abstract_backend import AbstractTableBackend
|
|
7
|
+
from ngio.utils import NgioFileNotFoundError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JsonTableBackend(AbstractTableBackend):
|
|
11
|
+
"""A class to load and write small tables in the zarr group .attrs (json)."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def backend_name() -> str:
|
|
15
|
+
"""The name of the backend."""
|
|
16
|
+
return "json_v1"
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def implements_anndata() -> bool:
|
|
20
|
+
"""Whether the handler implements the anndata protocol."""
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def implements_dataframe() -> bool:
|
|
25
|
+
"""Whether the handler implements the dataframe protocol."""
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def load_columns(self) -> list[str]:
|
|
29
|
+
"""List all labels in the group."""
|
|
30
|
+
return list(self.load_as_dataframe().columns)
|
|
31
|
+
|
|
32
|
+
def _get_table_group(self):
|
|
33
|
+
try:
|
|
34
|
+
table_group = self._group_handler.get_group(path="table")
|
|
35
|
+
except NgioFileNotFoundError:
|
|
36
|
+
table_group = self._group_handler.group.create_group("table")
|
|
37
|
+
return table_group
|
|
38
|
+
|
|
39
|
+
def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
|
|
40
|
+
"""List all labels in the group."""
|
|
41
|
+
table_group = self._get_table_group()
|
|
42
|
+
table_dict = dict(table_group.attrs)
|
|
43
|
+
data_frame = pd.DataFrame.from_dict(table_dict)
|
|
44
|
+
if columns is not None:
|
|
45
|
+
data_frame = data_frame[columns]
|
|
46
|
+
return data_frame
|
|
47
|
+
|
|
48
|
+
def write_from_dataframe(
|
|
49
|
+
self, table: DataFrame, metadata: dict | None = None
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Consolidate the metadata in the store."""
|
|
52
|
+
table_group = self._get_table_group()
|
|
53
|
+
table_group.attrs.clear()
|
|
54
|
+
table_group.attrs.update(table.to_dict())
|
|
55
|
+
if metadata is not None:
|
|
56
|
+
self._group_handler.write_attrs(metadata)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Protocol for table backends handlers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
from typing import Literal, Protocol
|
|
5
|
+
|
|
6
|
+
from anndata import AnnData
|
|
7
|
+
from pandas import DataFrame
|
|
8
|
+
|
|
9
|
+
from ngio.tables.backends._anndata_v1 import AnnDataBackend
|
|
10
|
+
from ngio.tables.backends._json_v1 import JsonTableBackend
|
|
11
|
+
from ngio.utils import NgioValueError, ZarrGroupHandler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TableBackendProtocol(Protocol):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
group_handler: ZarrGroupHandler,
|
|
18
|
+
index_key: str | None = None,
|
|
19
|
+
index_type: Literal["int", "str"] = "int",
|
|
20
|
+
): ...
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def backend_name() -> str: ...
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def implements_anndata() -> bool: ...
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def implements_dataframe() -> bool: ...
|
|
30
|
+
|
|
31
|
+
def load_columns(self) -> list[str]: ...
|
|
32
|
+
|
|
33
|
+
def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData: ...
|
|
34
|
+
|
|
35
|
+
def load_as_dataframe(
|
|
36
|
+
self, columns: Collection[str] | None = None
|
|
37
|
+
) -> DataFrame: ...
|
|
38
|
+
|
|
39
|
+
def write_from_dataframe(
|
|
40
|
+
self, table: DataFrame, metadata: dict | None = None
|
|
41
|
+
) -> None: ...
|
|
42
|
+
|
|
43
|
+
def write_from_anndata(
|
|
44
|
+
self, table: AnnData, metadata: dict | None = None
|
|
45
|
+
) -> None: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ImplementedTableBackends:
|
|
49
|
+
"""A class to manage the available table backends."""
|
|
50
|
+
|
|
51
|
+
_instance = None
|
|
52
|
+
_implemented_backends: dict[str, type[TableBackendProtocol]]
|
|
53
|
+
|
|
54
|
+
def __new__(cls):
|
|
55
|
+
"""Create a new instance of the class if it does not exist."""
|
|
56
|
+
if cls._instance is None:
|
|
57
|
+
cls._instance = super().__new__(cls)
|
|
58
|
+
cls._instance._implemented_backends = {}
|
|
59
|
+
return cls._instance
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def available_backends(self) -> list[str]:
|
|
63
|
+
"""Return the available table backends."""
|
|
64
|
+
return list(self._implemented_backends.keys())
|
|
65
|
+
|
|
66
|
+
def get_backend(
|
|
67
|
+
self,
|
|
68
|
+
backend_name: str | None,
|
|
69
|
+
group_handler: ZarrGroupHandler,
|
|
70
|
+
index_key: str | None = None,
|
|
71
|
+
index_type: Literal["int", "str"] = "int",
|
|
72
|
+
) -> TableBackendProtocol:
|
|
73
|
+
"""Try to get a handler for the given store based on the metadata version."""
|
|
74
|
+
if backend_name is None:
|
|
75
|
+
# Default to anndata since it is currently
|
|
76
|
+
# the only backend in use.
|
|
77
|
+
backend_name = "anndata_v1"
|
|
78
|
+
|
|
79
|
+
if backend_name not in self._implemented_backends:
|
|
80
|
+
raise NgioValueError(f"Table backend {backend_name} not implemented.")
|
|
81
|
+
handler = self._implemented_backends[backend_name](
|
|
82
|
+
group_handler=group_handler, index_key=index_key, index_type=index_type
|
|
83
|
+
)
|
|
84
|
+
return handler
|
|
85
|
+
|
|
86
|
+
def add_backend(
|
|
87
|
+
self,
|
|
88
|
+
table_beckend: type[TableBackendProtocol],
|
|
89
|
+
overwrite: bool = False,
|
|
90
|
+
):
|
|
91
|
+
"""Register a new handler."""
|
|
92
|
+
backend_name = table_beckend.backend_name()
|
|
93
|
+
if backend_name in self._implemented_backends and not overwrite:
|
|
94
|
+
raise NgioValueError(
|
|
95
|
+
f"Table backend {backend_name} already implemented. "
|
|
96
|
+
"Use the `overwrite=True` parameter to overwrite it."
|
|
97
|
+
)
|
|
98
|
+
self._implemented_backends[backend_name] = table_beckend
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
ImplementedTableBackends().add_backend(AnnDataBackend)
|
|
102
|
+
ImplementedTableBackends().add_backend(JsonTableBackend)
|