ngio 0.5.0b6__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 +69 -0
- ngio/common/__init__.py +28 -0
- ngio/common/_dimensions.py +335 -0
- ngio/common/_masking_roi.py +153 -0
- ngio/common/_pyramid.py +408 -0
- ngio/common/_roi.py +315 -0
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +188 -0
- ngio/experimental/__init__.py +5 -0
- ngio/experimental/iterators/__init__.py +15 -0
- ngio/experimental/iterators/_abstract_iterator.py +390 -0
- ngio/experimental/iterators/_feature.py +189 -0
- ngio/experimental/iterators/_image_processing.py +130 -0
- ngio/experimental/iterators/_mappers.py +48 -0
- ngio/experimental/iterators/_rois_utils.py +126 -0
- ngio/experimental/iterators/_segmentation.py +235 -0
- ngio/hcs/__init__.py +19 -0
- ngio/hcs/_plate.py +1354 -0
- ngio/images/__init__.py +44 -0
- ngio/images/_abstract_image.py +967 -0
- ngio/images/_create_synt_container.py +132 -0
- ngio/images/_create_utils.py +423 -0
- ngio/images/_image.py +926 -0
- ngio/images/_label.py +411 -0
- ngio/images/_masked_image.py +531 -0
- ngio/images/_ome_zarr_container.py +1237 -0
- ngio/images/_table_ops.py +471 -0
- ngio/io_pipes/__init__.py +75 -0
- ngio/io_pipes/_io_pipes.py +361 -0
- ngio/io_pipes/_io_pipes_masked.py +488 -0
- ngio/io_pipes/_io_pipes_roi.py +146 -0
- ngio/io_pipes/_io_pipes_types.py +56 -0
- ngio/io_pipes/_match_shape.py +377 -0
- ngio/io_pipes/_ops_axes.py +344 -0
- ngio/io_pipes/_ops_slices.py +411 -0
- ngio/io_pipes/_ops_slices_utils.py +199 -0
- ngio/io_pipes/_ops_transforms.py +104 -0
- ngio/io_pipes/_zoom_transform.py +180 -0
- ngio/ome_zarr_meta/__init__.py +65 -0
- ngio/ome_zarr_meta/_meta_handlers.py +536 -0
- ngio/ome_zarr_meta/ngio_specs/__init__.py +77 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +515 -0
- ngio/ome_zarr_meta/ngio_specs/_channels.py +462 -0
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +89 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +539 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +438 -0
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +122 -0
- ngio/ome_zarr_meta/v04/__init__.py +27 -0
- ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v04/_v04_spec.py +473 -0
- ngio/ome_zarr_meta/v05/__init__.py +27 -0
- ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v05/_v05_spec.py +511 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
- ngio/resources/__init__.py +55 -0
- ngio/resources/resource_model.py +36 -0
- ngio/tables/__init__.py +43 -0
- ngio/tables/_abstract_table.py +270 -0
- ngio/tables/_tables_container.py +449 -0
- ngio/tables/backends/__init__.py +57 -0
- ngio/tables/backends/_abstract_backend.py +240 -0
- ngio/tables/backends/_anndata.py +139 -0
- ngio/tables/backends/_anndata_utils.py +90 -0
- ngio/tables/backends/_csv.py +19 -0
- ngio/tables/backends/_json.py +92 -0
- ngio/tables/backends/_parquet.py +19 -0
- ngio/tables/backends/_py_arrow_backends.py +222 -0
- ngio/tables/backends/_table_backends.py +226 -0
- ngio/tables/backends/_utils.py +608 -0
- ngio/tables/v1/__init__.py +23 -0
- ngio/tables/v1/_condition_table.py +71 -0
- ngio/tables/v1/_feature_table.py +125 -0
- ngio/tables/v1/_generic_table.py +49 -0
- ngio/tables/v1/_roi_table.py +575 -0
- ngio/transforms/__init__.py +5 -0
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/__init__.py +45 -0
- ngio/utils/_cache.py +48 -0
- ngio/utils/_datasets.py +165 -0
- ngio/utils/_errors.py +37 -0
- ngio/utils/_fractal_fsspec_store.py +42 -0
- ngio/utils/_zarr_utils.py +534 -0
- ngio-0.5.0b6.dist-info/METADATA +148 -0
- ngio-0.5.0b6.dist-info/RECORD +88 -0
- ngio-0.5.0b6.dist-info/WHEEL +4 -0
- ngio-0.5.0b6.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Implementation of a generic table class."""
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Literal, Self
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import polars as pl
|
|
9
|
+
from anndata import AnnData
|
|
10
|
+
|
|
11
|
+
from ngio.tables.backends import (
|
|
12
|
+
BackendMeta,
|
|
13
|
+
DefaultTableBackend,
|
|
14
|
+
ImplementedTableBackends,
|
|
15
|
+
TableBackend,
|
|
16
|
+
TableBackendProtocol,
|
|
17
|
+
TabularData,
|
|
18
|
+
convert_to_anndata,
|
|
19
|
+
convert_to_pandas,
|
|
20
|
+
convert_to_polars,
|
|
21
|
+
normalize_table,
|
|
22
|
+
)
|
|
23
|
+
from ngio.utils import NgioValueError, ZarrGroupHandler
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AbstractBaseTable(ABC):
|
|
27
|
+
"""Abstract base class for a table.
|
|
28
|
+
|
|
29
|
+
This is used to define common methods and properties
|
|
30
|
+
for all tables.
|
|
31
|
+
|
|
32
|
+
This class is not meant to be used directly.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
table_data: TabularData | None = None,
|
|
38
|
+
*,
|
|
39
|
+
meta: BackendMeta | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Initialize the table."""
|
|
42
|
+
if meta is None:
|
|
43
|
+
meta = BackendMeta()
|
|
44
|
+
|
|
45
|
+
self._meta = meta
|
|
46
|
+
if table_data is not None:
|
|
47
|
+
table_data = normalize_table(
|
|
48
|
+
table_data,
|
|
49
|
+
index_key=meta.index_key,
|
|
50
|
+
index_type=meta.index_type,
|
|
51
|
+
)
|
|
52
|
+
self._table_data = table_data
|
|
53
|
+
self._table_backend = None
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
"""Return a string representation of the table."""
|
|
57
|
+
return f"{self.__class__.__name__}"
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def table_type() -> str:
|
|
62
|
+
"""Return the type of the table."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def version() -> str:
|
|
68
|
+
"""The generic table does not have a version.
|
|
69
|
+
|
|
70
|
+
Since does not follow a specific schema.
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def backend_name(self) -> str | None:
|
|
76
|
+
"""Return the name of the backend."""
|
|
77
|
+
if self._table_backend is None:
|
|
78
|
+
return None
|
|
79
|
+
return self._table_backend.backend_name()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def meta(self) -> BackendMeta:
|
|
83
|
+
"""Return the metadata of the table."""
|
|
84
|
+
return self._meta
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def index_key(self) -> str | None:
|
|
88
|
+
"""Get the index key."""
|
|
89
|
+
return self._meta.index_key
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def index_type(self) -> Literal["int", "str"] | None:
|
|
93
|
+
"""Get the index type."""
|
|
94
|
+
return self._meta.index_type
|
|
95
|
+
|
|
96
|
+
def load_as_anndata(self) -> AnnData:
|
|
97
|
+
"""Load the table as an AnnData object."""
|
|
98
|
+
if self._table_backend is None:
|
|
99
|
+
raise NgioValueError("No backend set for the table.")
|
|
100
|
+
return self._table_backend.load_as_anndata()
|
|
101
|
+
|
|
102
|
+
def load_as_pandas_df(self) -> pd.DataFrame:
|
|
103
|
+
"""Load the table as a pandas DataFrame."""
|
|
104
|
+
if self._table_backend is None:
|
|
105
|
+
raise NgioValueError("No backend set for the table.")
|
|
106
|
+
return self._table_backend.load_as_pandas_df()
|
|
107
|
+
|
|
108
|
+
def load_as_polars_lf(self) -> pl.LazyFrame:
|
|
109
|
+
"""Load the table as a polars LazyFrame."""
|
|
110
|
+
if self._table_backend is None:
|
|
111
|
+
raise NgioValueError("No backend set for the table.")
|
|
112
|
+
return self._table_backend.load_as_polars_lf()
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def table_data(self) -> TabularData:
|
|
116
|
+
"""Return the table."""
|
|
117
|
+
if self._table_data is not None:
|
|
118
|
+
return self._table_data
|
|
119
|
+
|
|
120
|
+
if self._table_backend is None:
|
|
121
|
+
raise NgioValueError(
|
|
122
|
+
"The table does not have a DataFrame in memory nor a backend."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._table_data = self._table_backend.load()
|
|
126
|
+
return self._table_data
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def dataframe(self) -> pd.DataFrame:
|
|
130
|
+
"""Return the table as a DataFrame."""
|
|
131
|
+
return convert_to_pandas(
|
|
132
|
+
self.table_data, index_key=self.index_key, index_type=self.index_type
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def lazy_frame(self) -> pl.LazyFrame:
|
|
137
|
+
"""Return the table as a LazyFrame."""
|
|
138
|
+
return convert_to_polars(
|
|
139
|
+
self.table_data, index_key=self.index_key, index_type=self.index_type
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def anndata(self) -> AnnData:
|
|
144
|
+
"""Return the table as an AnnData object."""
|
|
145
|
+
return convert_to_anndata(self.table_data, index_key=self.index_key)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _load_backend(
|
|
149
|
+
meta: BackendMeta,
|
|
150
|
+
handler: ZarrGroupHandler,
|
|
151
|
+
backend: TableBackend,
|
|
152
|
+
) -> TableBackendProtocol:
|
|
153
|
+
"""Create a new ROI table from a Zarr group handler."""
|
|
154
|
+
if isinstance(backend, str):
|
|
155
|
+
return ImplementedTableBackends().get_backend(
|
|
156
|
+
backend_name=backend,
|
|
157
|
+
group_handler=handler,
|
|
158
|
+
index_key=meta.index_key,
|
|
159
|
+
index_type=meta.index_type,
|
|
160
|
+
)
|
|
161
|
+
backend.set_group_handler(
|
|
162
|
+
group_handler=handler,
|
|
163
|
+
index_key=meta.index_key,
|
|
164
|
+
index_type=meta.index_type,
|
|
165
|
+
)
|
|
166
|
+
return backend
|
|
167
|
+
|
|
168
|
+
def set_table_data(
|
|
169
|
+
self,
|
|
170
|
+
table_data: TabularData | None = None,
|
|
171
|
+
refresh: bool = False,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Set the table.
|
|
174
|
+
|
|
175
|
+
If an object is passed, it will be used as the table.
|
|
176
|
+
If None is passed, the table will be loaded from the backend.
|
|
177
|
+
|
|
178
|
+
If refresh is True, the table will be reloaded from the backend.
|
|
179
|
+
If table is not None, this will be ignored.
|
|
180
|
+
"""
|
|
181
|
+
if table_data is not None:
|
|
182
|
+
if not isinstance(table_data, TabularData):
|
|
183
|
+
raise NgioValueError(
|
|
184
|
+
"The table must be a pandas DataFrame, polars LazyFrame, "
|
|
185
|
+
" or AnnData object."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self._table_data = normalize_table(
|
|
189
|
+
table_data,
|
|
190
|
+
index_key=self.index_key,
|
|
191
|
+
index_type=self.index_type,
|
|
192
|
+
)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
if self._table_data is not None and not refresh:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
if self._table_backend is None:
|
|
199
|
+
raise NgioValueError(
|
|
200
|
+
"The table does not have a DataFrame in memory nor a backend."
|
|
201
|
+
)
|
|
202
|
+
self._table_data = self._table_backend.load()
|
|
203
|
+
|
|
204
|
+
def set_backend(
|
|
205
|
+
self,
|
|
206
|
+
handler: ZarrGroupHandler | None = None,
|
|
207
|
+
backend: TableBackend = DefaultTableBackend,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Set the backend of the table."""
|
|
210
|
+
if handler is None:
|
|
211
|
+
if self._table_backend is None:
|
|
212
|
+
raise NgioValueError(
|
|
213
|
+
"No backend set for the table yet. "
|
|
214
|
+
"A ZarrGroupHandler must be provided."
|
|
215
|
+
)
|
|
216
|
+
handler = self._table_backend.group_handler
|
|
217
|
+
|
|
218
|
+
meta = self._meta
|
|
219
|
+
_backend = self._load_backend(
|
|
220
|
+
meta=meta,
|
|
221
|
+
handler=handler,
|
|
222
|
+
backend=backend,
|
|
223
|
+
)
|
|
224
|
+
self._table_backend = _backend
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def _from_handler(
|
|
228
|
+
cls,
|
|
229
|
+
handler: ZarrGroupHandler,
|
|
230
|
+
meta_model: builtins.type[BackendMeta],
|
|
231
|
+
backend: TableBackend | None = None,
|
|
232
|
+
) -> Self:
|
|
233
|
+
"""Create a new ROI table from a Zarr group handler."""
|
|
234
|
+
meta = meta_model(**handler.load_attrs())
|
|
235
|
+
table = cls(meta=meta)
|
|
236
|
+
if backend is None:
|
|
237
|
+
backend = meta.backend
|
|
238
|
+
table.set_backend(handler=handler, backend=backend)
|
|
239
|
+
return table
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
@abstractmethod
|
|
243
|
+
def from_handler(
|
|
244
|
+
cls,
|
|
245
|
+
handler: ZarrGroupHandler,
|
|
246
|
+
backend: TableBackend | None = None,
|
|
247
|
+
) -> Self:
|
|
248
|
+
"""Create a new ROI table from a Zarr group handler."""
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def from_table_data(cls, table_data: TabularData, meta: BackendMeta) -> Self:
|
|
253
|
+
"""Create a new ROI table from a Zarr group handler."""
|
|
254
|
+
return cls(
|
|
255
|
+
table_data=table_data,
|
|
256
|
+
meta=meta,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def consolidate(self) -> None:
|
|
260
|
+
"""Write the current state of the table to the Zarr file."""
|
|
261
|
+
if self._table_backend is None:
|
|
262
|
+
raise NgioValueError(
|
|
263
|
+
"No backend set for the table. "
|
|
264
|
+
"Please add the table to a OME-Zarr Image before calling consolidate."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
self._table_backend.write(
|
|
268
|
+
self.table_data,
|
|
269
|
+
metadata=self._meta.model_dump(exclude_none=True),
|
|
270
|
+
)
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""Module for handling the /tables group in an OME-NGFF file."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Protocol, TypeVar
|
|
4
|
+
|
|
5
|
+
import anndata as ad
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
from ngio.tables.backends import (
|
|
10
|
+
BackendMeta,
|
|
11
|
+
DefaultTableBackend,
|
|
12
|
+
TableBackend,
|
|
13
|
+
TabularData,
|
|
14
|
+
)
|
|
15
|
+
from ngio.tables.v1 import (
|
|
16
|
+
ConditionTableV1,
|
|
17
|
+
FeatureTableV1,
|
|
18
|
+
GenericTable,
|
|
19
|
+
MaskingRoiTableV1,
|
|
20
|
+
RoiTableV1,
|
|
21
|
+
)
|
|
22
|
+
from ngio.tables.v1._roi_table import GenericRoiTableV1
|
|
23
|
+
from ngio.utils import (
|
|
24
|
+
AccessModeLiteral,
|
|
25
|
+
NgioValidationError,
|
|
26
|
+
NgioValueError,
|
|
27
|
+
StoreOrGroup,
|
|
28
|
+
ZarrGroupHandler,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
GenericRoiTable = GenericRoiTableV1
|
|
32
|
+
RoiTable = RoiTableV1
|
|
33
|
+
MaskingRoiTable = MaskingRoiTableV1
|
|
34
|
+
FeatureTable = FeatureTableV1
|
|
35
|
+
ConditionTable = ConditionTableV1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Table(Protocol):
|
|
39
|
+
"""Placeholder class for a table."""
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def table_type() -> str:
|
|
43
|
+
"""Return the type of the table."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def version() -> str:
|
|
48
|
+
"""Return the version of the table."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def backend_name(self) -> str | None:
|
|
53
|
+
"""The name of the backend."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def meta(self) -> BackendMeta:
|
|
58
|
+
"""Return the metadata for the table."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def dataframe(self) -> pd.DataFrame:
|
|
63
|
+
"""Return the table as a DataFrame."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def lazy_frame(self) -> pl.LazyFrame:
|
|
68
|
+
"""Return the table as a LazyFrame."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def anndata(self) -> ad.AnnData:
|
|
73
|
+
"""Return the table as an AnnData object."""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
def set_table_data(
|
|
77
|
+
self,
|
|
78
|
+
table_data: TabularData | None = None,
|
|
79
|
+
refresh: bool = False,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Make sure that the table data is set (exist in memory).
|
|
82
|
+
|
|
83
|
+
If an object is passed, it will be used as the table.
|
|
84
|
+
If None is passed, the table will be loaded from the backend.
|
|
85
|
+
|
|
86
|
+
If refresh is True, the table will be reloaded from the backend.
|
|
87
|
+
If table is not None, this will be ignored.
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def set_backend(
|
|
92
|
+
self,
|
|
93
|
+
handler: ZarrGroupHandler | None = None,
|
|
94
|
+
backend: TableBackend = DefaultTableBackend,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Set the backend store and path for the table.
|
|
97
|
+
|
|
98
|
+
Either a handler or a backend must be provided.
|
|
99
|
+
|
|
100
|
+
If the handler in none it will be inferred from the backend.
|
|
101
|
+
If the backend is none, it will be inferred from the group attrs
|
|
102
|
+
"""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_handler(
|
|
107
|
+
cls,
|
|
108
|
+
handler: ZarrGroupHandler,
|
|
109
|
+
backend: TableBackend | None = None,
|
|
110
|
+
) -> "Table":
|
|
111
|
+
"""Create a new table from a Zarr group handler."""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_table_data(cls, table_data: TabularData, meta: BackendMeta) -> "Table":
|
|
116
|
+
"""Create a new table from a DataFrame."""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def table_data(self) -> TabularData:
|
|
121
|
+
"""Return the table."""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
def consolidate(self) -> None:
|
|
125
|
+
"""Consolidate the table on disk."""
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
TypedTable = Literal[
|
|
130
|
+
"generic_table",
|
|
131
|
+
"roi_table",
|
|
132
|
+
"masking_roi_table",
|
|
133
|
+
"feature_table",
|
|
134
|
+
"condition_table",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
TypedRoiTable = Literal[
|
|
138
|
+
"roi_table",
|
|
139
|
+
"masking_roi_table",
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
TableType = TypeVar("TableType", bound=Table)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TableMeta(BackendMeta):
|
|
146
|
+
"""Base class for table metadata."""
|
|
147
|
+
|
|
148
|
+
table_version: str = "1"
|
|
149
|
+
type: str = "generic_table"
|
|
150
|
+
|
|
151
|
+
def unique_name(self) -> str:
|
|
152
|
+
"""Return the unique name for the table."""
|
|
153
|
+
return f"{self.type}_v{self.table_version}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _get_meta(handler: ZarrGroupHandler) -> TableMeta:
|
|
157
|
+
"""Get the metadata from the handler."""
|
|
158
|
+
attrs = handler.load_attrs()
|
|
159
|
+
meta = TableMeta(**attrs)
|
|
160
|
+
return meta
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ImplementedTables:
|
|
164
|
+
"""A singleton class to manage the available table handler plugins."""
|
|
165
|
+
|
|
166
|
+
_instance = None
|
|
167
|
+
_implemented_tables: dict[str, type[Table]]
|
|
168
|
+
|
|
169
|
+
def __new__(cls):
|
|
170
|
+
"""Create a new instance of the class if it does not exist."""
|
|
171
|
+
if cls._instance is None:
|
|
172
|
+
cls._instance = super().__new__(cls)
|
|
173
|
+
cls._instance._implemented_tables = {}
|
|
174
|
+
return cls._instance
|
|
175
|
+
|
|
176
|
+
def available_implementations(self) -> list[str]:
|
|
177
|
+
"""Get the available table handler versions."""
|
|
178
|
+
return list(self._implemented_tables.keys())
|
|
179
|
+
|
|
180
|
+
def get_table(
|
|
181
|
+
self,
|
|
182
|
+
meta: TableMeta,
|
|
183
|
+
handler: ZarrGroupHandler,
|
|
184
|
+
backend: TableBackend | None = None,
|
|
185
|
+
strict: bool = True,
|
|
186
|
+
) -> Table:
|
|
187
|
+
"""Try to get a handler for the given store based on the metadata version."""
|
|
188
|
+
if strict:
|
|
189
|
+
default = None
|
|
190
|
+
else:
|
|
191
|
+
default = GenericTable
|
|
192
|
+
|
|
193
|
+
table_cls = self._implemented_tables.get(meta.unique_name(), default)
|
|
194
|
+
if table_cls is None:
|
|
195
|
+
raise NgioValueError(
|
|
196
|
+
f"Table handler for {meta.unique_name()} not implemented."
|
|
197
|
+
)
|
|
198
|
+
table = table_cls.from_handler(handler=handler, backend=backend)
|
|
199
|
+
return table
|
|
200
|
+
|
|
201
|
+
def _add_implementation(
|
|
202
|
+
self, handler: type[Table], name: str, overwrite: bool = False
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Register a new table handler."""
|
|
205
|
+
if name in self._implemented_tables and not overwrite:
|
|
206
|
+
raise NgioValueError(
|
|
207
|
+
f"Table handler for {name} already implemented. "
|
|
208
|
+
"Use overwrite=True to replace it."
|
|
209
|
+
)
|
|
210
|
+
self._implemented_tables[name] = handler
|
|
211
|
+
|
|
212
|
+
def add_implementation(
|
|
213
|
+
self,
|
|
214
|
+
handler: type[Table],
|
|
215
|
+
overwrite: bool = False,
|
|
216
|
+
aliases: list[str] | None = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Register a new table handler."""
|
|
219
|
+
meta = TableMeta(
|
|
220
|
+
type=handler.table_type(),
|
|
221
|
+
table_version=handler.version(),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self._add_implementation(handler, meta.unique_name(), overwrite)
|
|
225
|
+
|
|
226
|
+
if aliases is not None:
|
|
227
|
+
for alias in aliases:
|
|
228
|
+
self._add_implementation(handler, alias, overwrite)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class TablesContainer:
|
|
232
|
+
"""A class to handle the /labels group in an OME-NGFF file."""
|
|
233
|
+
|
|
234
|
+
def __init__(self, group_handler: ZarrGroupHandler) -> None:
|
|
235
|
+
"""Initialize the LabelGroupHandler."""
|
|
236
|
+
self._group_handler = group_handler
|
|
237
|
+
|
|
238
|
+
# Validate the group
|
|
239
|
+
# Either contains a tables attribute or is empty
|
|
240
|
+
attrs = self._group_handler.load_attrs()
|
|
241
|
+
if len(attrs) == 0:
|
|
242
|
+
# It's an empty group
|
|
243
|
+
pass
|
|
244
|
+
elif "tables" in attrs and isinstance(attrs["tables"], list):
|
|
245
|
+
# It's a valid group
|
|
246
|
+
pass
|
|
247
|
+
else:
|
|
248
|
+
raise NgioValidationError(
|
|
249
|
+
f"Invalid /tables group. "
|
|
250
|
+
f"Expected a single tables attribute with a list of table names. "
|
|
251
|
+
f"Found: {attrs}"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _get_tables_list(self) -> list[str]:
|
|
255
|
+
"""Create the /tables group if it doesn't exist."""
|
|
256
|
+
attrs = self._group_handler.load_attrs()
|
|
257
|
+
return attrs.get("tables", [])
|
|
258
|
+
|
|
259
|
+
def _get_table_group_handler(self, name: str) -> ZarrGroupHandler:
|
|
260
|
+
"""Get the group handler for a table."""
|
|
261
|
+
handler = self._group_handler.get_handler(path=name)
|
|
262
|
+
return handler
|
|
263
|
+
|
|
264
|
+
def list(self, filter_types: TypedTable | str | None = None) -> list[str]:
|
|
265
|
+
"""List all labels in the group."""
|
|
266
|
+
tables = self._get_tables_list()
|
|
267
|
+
if filter_types is None:
|
|
268
|
+
return tables
|
|
269
|
+
|
|
270
|
+
filtered_tables = []
|
|
271
|
+
for table_name in tables:
|
|
272
|
+
tb_handler = self._get_table_group_handler(table_name)
|
|
273
|
+
table_type = _get_meta(tb_handler).type
|
|
274
|
+
if table_type == filter_types:
|
|
275
|
+
filtered_tables.append(table_name)
|
|
276
|
+
return filtered_tables
|
|
277
|
+
|
|
278
|
+
def get(
|
|
279
|
+
self,
|
|
280
|
+
name: str,
|
|
281
|
+
backend: TableBackend | None = None,
|
|
282
|
+
strict: bool = True,
|
|
283
|
+
) -> Table:
|
|
284
|
+
"""Get a label from the group."""
|
|
285
|
+
if name not in self.list():
|
|
286
|
+
raise NgioValueError(f"Table '{name}' not found in the group.")
|
|
287
|
+
|
|
288
|
+
table_handler = self._get_table_group_handler(name)
|
|
289
|
+
|
|
290
|
+
meta = _get_meta(table_handler)
|
|
291
|
+
return ImplementedTables().get_table(
|
|
292
|
+
meta=meta,
|
|
293
|
+
handler=table_handler,
|
|
294
|
+
backend=backend,
|
|
295
|
+
strict=strict,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def get_as(
|
|
299
|
+
self,
|
|
300
|
+
name: str,
|
|
301
|
+
table_cls: type[TableType],
|
|
302
|
+
backend: TableBackend | None = None,
|
|
303
|
+
) -> TableType:
|
|
304
|
+
"""Get a table from the group as a specific type."""
|
|
305
|
+
if name not in self.list():
|
|
306
|
+
raise NgioValueError(f"Table '{name}' not found in the group.")
|
|
307
|
+
|
|
308
|
+
table_handler = self._get_table_group_handler(name)
|
|
309
|
+
return table_cls.from_handler(
|
|
310
|
+
handler=table_handler,
|
|
311
|
+
backend=backend,
|
|
312
|
+
) # type: ignore[return-value]
|
|
313
|
+
|
|
314
|
+
def delete(self, name: str, missing_ok: bool = False) -> None:
|
|
315
|
+
"""Delete a table from the group.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
name (str): The name of the table to delete.
|
|
319
|
+
missing_ok (bool): If True, do not raise an error if
|
|
320
|
+
the table does not exist.
|
|
321
|
+
"""
|
|
322
|
+
existing_tables = self._get_tables_list()
|
|
323
|
+
if name not in existing_tables:
|
|
324
|
+
if missing_ok:
|
|
325
|
+
return
|
|
326
|
+
raise NgioValueError(
|
|
327
|
+
f"Table '{name}' not found in the Tables group. "
|
|
328
|
+
f"Available tables: {existing_tables}"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
self._group_handler.delete_group(name)
|
|
332
|
+
existing_tables.remove(name)
|
|
333
|
+
self._group_handler.write_attrs({"tables": existing_tables})
|
|
334
|
+
|
|
335
|
+
def add(
|
|
336
|
+
self,
|
|
337
|
+
name: str,
|
|
338
|
+
table: Table,
|
|
339
|
+
backend: TableBackend = DefaultTableBackend,
|
|
340
|
+
overwrite: bool = False,
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Add a table to the group."""
|
|
343
|
+
existing_tables = self._get_tables_list()
|
|
344
|
+
if name in existing_tables and not overwrite:
|
|
345
|
+
raise NgioValueError(
|
|
346
|
+
f"Table '{name}' already exists in the group. "
|
|
347
|
+
"Use overwrite=True to replace it."
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
table_handler = self._group_handler.get_handler(path=name, overwrite=overwrite)
|
|
351
|
+
|
|
352
|
+
if backend is None:
|
|
353
|
+
backend = table.backend_name
|
|
354
|
+
|
|
355
|
+
table.set_table_data()
|
|
356
|
+
table.set_backend(
|
|
357
|
+
handler=table_handler,
|
|
358
|
+
backend=backend,
|
|
359
|
+
)
|
|
360
|
+
table.consolidate()
|
|
361
|
+
if name not in existing_tables:
|
|
362
|
+
existing_tables.append(name)
|
|
363
|
+
self._group_handler.write_attrs({"tables": existing_tables})
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
ImplementedTables().add_implementation(RoiTableV1)
|
|
367
|
+
ImplementedTables().add_implementation(MaskingRoiTableV1)
|
|
368
|
+
ImplementedTables().add_implementation(FeatureTableV1)
|
|
369
|
+
ImplementedTables().add_implementation(ConditionTableV1)
|
|
370
|
+
|
|
371
|
+
###################################################################################
|
|
372
|
+
#
|
|
373
|
+
# Utility functions to open and write tables
|
|
374
|
+
#
|
|
375
|
+
###################################################################################
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def open_tables_container(
|
|
379
|
+
store: StoreOrGroup,
|
|
380
|
+
cache: bool = False,
|
|
381
|
+
mode: AccessModeLiteral = "r+",
|
|
382
|
+
) -> TablesContainer:
|
|
383
|
+
"""Open a table handler from a Zarr store."""
|
|
384
|
+
handler = ZarrGroupHandler(store=store, cache=cache, mode=mode)
|
|
385
|
+
return TablesContainer(handler)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def open_table(
|
|
389
|
+
store: StoreOrGroup,
|
|
390
|
+
backend: TableBackend | None = None,
|
|
391
|
+
cache: bool = False,
|
|
392
|
+
mode: AccessModeLiteral = "r+",
|
|
393
|
+
) -> Table:
|
|
394
|
+
"""Open a table from a Zarr store."""
|
|
395
|
+
handler = ZarrGroupHandler(
|
|
396
|
+
store=store,
|
|
397
|
+
cache=cache,
|
|
398
|
+
mode=mode,
|
|
399
|
+
)
|
|
400
|
+
meta = _get_meta(handler)
|
|
401
|
+
return ImplementedTables().get_table(
|
|
402
|
+
meta=meta, handler=handler, backend=backend, strict=False
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def open_table_as(
|
|
407
|
+
store: StoreOrGroup,
|
|
408
|
+
table_cls: type[TableType],
|
|
409
|
+
backend: TableBackend | None = None,
|
|
410
|
+
cache: bool = False,
|
|
411
|
+
mode: AccessModeLiteral = "r+",
|
|
412
|
+
) -> TableType:
|
|
413
|
+
"""Open a table from a Zarr store as a specific type."""
|
|
414
|
+
handler = ZarrGroupHandler(
|
|
415
|
+
store=store,
|
|
416
|
+
cache=cache,
|
|
417
|
+
mode=mode,
|
|
418
|
+
)
|
|
419
|
+
return table_cls.from_handler(
|
|
420
|
+
handler=handler,
|
|
421
|
+
backend=backend,
|
|
422
|
+
) # type: ignore[return-value]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def write_table(
|
|
426
|
+
store: StoreOrGroup,
|
|
427
|
+
table: Table,
|
|
428
|
+
backend: TableBackend = DefaultTableBackend,
|
|
429
|
+
cache: bool = False,
|
|
430
|
+
mode: AccessModeLiteral = "a",
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Write a table to a Zarr store.
|
|
433
|
+
|
|
434
|
+
A table will be created at the given store location.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
store (StoreOrGroup): The Zarr store or group to write the table to.
|
|
438
|
+
table (Table): The table to write.
|
|
439
|
+
backend (TableBackend): The backend to use for writing the table.
|
|
440
|
+
cache (bool): Whether to use caching for the Zarr group handler.
|
|
441
|
+
mode (AccessModeLiteral): The access mode to use for the Zarr group handler.
|
|
442
|
+
|
|
443
|
+
"""
|
|
444
|
+
handler = ZarrGroupHandler(store=store, cache=cache, mode=mode)
|
|
445
|
+
table.set_backend(
|
|
446
|
+
handler=handler,
|
|
447
|
+
backend=backend,
|
|
448
|
+
)
|
|
449
|
+
table.consolidate()
|