ngio 0.2.8__py3-none-any.whl → 0.3.0a0__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/common/__init__.py +16 -0
- ngio/common/_table_ops.py +471 -0
- ngio/hcs/plate.py +430 -72
- ngio/images/ome_zarr_container.py +99 -68
- ngio/tables/__init__.py +8 -1
- ngio/tables/abstract_table.py +268 -0
- ngio/tables/backends/__init__.py +18 -0
- ngio/tables/backends/_abstract_backend.py +58 -80
- ngio/tables/backends/_anndata_v1.py +4 -0
- ngio/tables/backends/_csv_v1.py +23 -150
- ngio/tables/backends/_json_v1.py +3 -0
- ngio/tables/backends/_non_zarr_backends_v1.py +196 -0
- ngio/tables/backends/_parquet_v1.py +47 -0
- ngio/tables/backends/_table_backends.py +34 -15
- ngio/tables/backends/_utils.py +147 -1
- ngio/tables/tables_container.py +180 -92
- ngio/tables/v1/__init__.py +8 -1
- ngio/tables/v1/_condition_table.py +67 -0
- ngio/tables/v1/_feature_table.py +62 -126
- ngio/tables/v1/_generic_table.py +14 -163
- ngio/tables/v1/_roi_table.py +281 -201
- ngio/utils/_fractal_fsspec_store.py +29 -0
- {ngio-0.2.8.dist-info → ngio-0.3.0a0.dist-info}/METADATA +3 -3
- {ngio-0.2.8.dist-info → ngio-0.3.0a0.dist-info}/RECORD +26 -22
- ngio/tables/_validators.py +0 -108
- {ngio-0.2.8.dist-info → ngio-0.3.0a0.dist-info}/WHEEL +0 -0
- {ngio-0.2.8.dist-info → ngio-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
ngio/tables/v1/_roi_table.py
CHANGED
|
@@ -5,17 +5,26 @@ https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
# Import _type to avoid name conflict with table.type
|
|
8
|
-
from builtins import type as _type
|
|
9
8
|
from collections.abc import Iterable
|
|
10
|
-
from
|
|
9
|
+
from functools import cache
|
|
10
|
+
from typing import Literal
|
|
11
11
|
|
|
12
12
|
import pandas as pd
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
15
|
from ngio.common import Roi
|
|
16
|
-
from ngio.tables.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
from ngio.tables.abstract_table import (
|
|
17
|
+
AbstractBaseTable,
|
|
18
|
+
TableBackendProtocol,
|
|
19
|
+
TabularData,
|
|
20
|
+
)
|
|
21
|
+
from ngio.tables.backends import BackendMeta, convert_to_pandas, normalize_pandas_df
|
|
22
|
+
from ngio.utils import (
|
|
23
|
+
NgioTableValidationError,
|
|
24
|
+
NgioValueError,
|
|
25
|
+
ZarrGroupHandler,
|
|
26
|
+
ngio_logger,
|
|
27
|
+
)
|
|
19
28
|
|
|
20
29
|
REQUIRED_COLUMNS = [
|
|
21
30
|
"x_micrometer",
|
|
@@ -26,6 +35,10 @@ REQUIRED_COLUMNS = [
|
|
|
26
35
|
"len_z_micrometer",
|
|
27
36
|
]
|
|
28
37
|
|
|
38
|
+
#####################
|
|
39
|
+
# Optional columns are not validated at the moment
|
|
40
|
+
# only a warning is raised if non optional columns are present
|
|
41
|
+
#####################
|
|
29
42
|
|
|
30
43
|
ORIGIN_COLUMNS = [
|
|
31
44
|
"x_micrometer_original",
|
|
@@ -34,36 +47,95 @@ ORIGIN_COLUMNS = [
|
|
|
34
47
|
|
|
35
48
|
TRANSLATION_COLUMNS = ["translation_x", "translation_y", "translation_z"]
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
PLATE_COLUMNS = ["plate_name", "row", "column", "path", "acquisition"]
|
|
38
51
|
|
|
52
|
+
INDEX_COLUMNS = [
|
|
53
|
+
"FieldIndex",
|
|
54
|
+
"label",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
OPTIONAL_COLUMNS = ORIGIN_COLUMNS + TRANSLATION_COLUMNS + PLATE_COLUMNS + INDEX_COLUMNS
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cache
|
|
61
|
+
def _check_optional_columns(col_name: str) -> None:
|
|
62
|
+
"""Check if the column name is in the optional columns."""
|
|
63
|
+
if col_name not in OPTIONAL_COLUMNS:
|
|
64
|
+
ngio_logger.warning(
|
|
65
|
+
f"Column {col_name} is not in the optional columns. "
|
|
66
|
+
f"Standard optional columns are: {OPTIONAL_COLUMNS}."
|
|
67
|
+
)
|
|
39
68
|
|
|
40
|
-
|
|
69
|
+
|
|
70
|
+
def _dataframe_to_rois(
|
|
71
|
+
dataframe: pd.DataFrame,
|
|
72
|
+
required_columns: list[str] | None = None,
|
|
73
|
+
) -> dict[str, Roi]:
|
|
41
74
|
"""Convert a DataFrame to a WorldCooROI object."""
|
|
75
|
+
if required_columns is None:
|
|
76
|
+
required_columns = REQUIRED_COLUMNS
|
|
77
|
+
|
|
78
|
+
# Validate the columns of the DataFrame
|
|
79
|
+
_required_columns = set(dataframe.columns).intersection(set(required_columns))
|
|
80
|
+
if len(_required_columns) != len(required_columns):
|
|
81
|
+
raise NgioTableValidationError(
|
|
82
|
+
f"Could not find required columns: {_required_columns} in the table."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
extra_columns = set(dataframe.columns).difference(set(required_columns))
|
|
86
|
+
|
|
87
|
+
for col in extra_columns:
|
|
88
|
+
_check_optional_columns(col)
|
|
89
|
+
|
|
90
|
+
extras = {}
|
|
91
|
+
|
|
42
92
|
rois = {}
|
|
43
|
-
for
|
|
93
|
+
for row in dataframe.itertuples(index=True):
|
|
44
94
|
# check if optional columns are present
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
translation = {col: row.get(col, None) for col in TRANSLATION_COLUMNS}
|
|
48
|
-
translation = dict(filter(lambda x: x[1] is not None, translation.items()))
|
|
95
|
+
if len(extra_columns) > 0:
|
|
96
|
+
extras = {col: getattr(row, col, None) for col in extra_columns}
|
|
49
97
|
|
|
50
98
|
roi = Roi(
|
|
51
|
-
name=str(
|
|
52
|
-
x=row
|
|
53
|
-
y=row
|
|
54
|
-
z=row
|
|
55
|
-
x_length=row
|
|
56
|
-
y_length=row
|
|
57
|
-
z_length=row
|
|
99
|
+
name=str(row.Index),
|
|
100
|
+
x=row.x_micrometer, # type: ignore
|
|
101
|
+
y=row.y_micrometer, # type: ignore
|
|
102
|
+
z=row.z_micrometer, # type: ignore
|
|
103
|
+
x_length=row.len_x_micrometer, # type: ignore
|
|
104
|
+
y_length=row.len_y_micrometer, # type: ignore
|
|
105
|
+
z_length=row.len_z_micrometer, # type: ignore
|
|
58
106
|
unit="micrometer", # type: ignore
|
|
59
|
-
**
|
|
60
|
-
**translation,
|
|
107
|
+
**extras,
|
|
61
108
|
)
|
|
62
109
|
rois[roi.name] = roi
|
|
63
110
|
return rois
|
|
64
111
|
|
|
65
112
|
|
|
66
|
-
def
|
|
113
|
+
def _table_to_rois(
|
|
114
|
+
table: TabularData,
|
|
115
|
+
index_key: str | None = None,
|
|
116
|
+
index_type: Literal["int", "str"] | None = None,
|
|
117
|
+
required_columns: list[str] | None = None,
|
|
118
|
+
) -> tuple[pd.DataFrame, dict[str, Roi]]:
|
|
119
|
+
"""Convert a table to a dictionary of ROIs.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
table: The table to convert.
|
|
123
|
+
index_key: The column name to use as the index of the DataFrame.
|
|
124
|
+
index_type: The type of the index column in the DataFrame.
|
|
125
|
+
required_columns: The required columns in the DataFrame.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A dictionary of ROIs.
|
|
129
|
+
"""
|
|
130
|
+
dataframe = convert_to_pandas(
|
|
131
|
+
table,
|
|
132
|
+
index_key=index_key,
|
|
133
|
+
index_type=index_type,
|
|
134
|
+
)
|
|
135
|
+
return dataframe, _dataframe_to_rois(dataframe, required_columns=required_columns)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _rois_to_dataframe(rois: dict[str, Roi], index_key: str | None) -> pd.DataFrame:
|
|
67
139
|
"""Convert a list of WorldCooROI objects to a DataFrame."""
|
|
68
140
|
data = []
|
|
69
141
|
for roi in rois.values():
|
|
@@ -78,153 +150,110 @@ def _rois_to_dataframe(rois: dict[str, Roi], index_key: str) -> pd.DataFrame:
|
|
|
78
150
|
}
|
|
79
151
|
|
|
80
152
|
extra = roi.model_extra or {}
|
|
81
|
-
for col in
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for col in TRANSLATION_COLUMNS:
|
|
86
|
-
if col in extra:
|
|
87
|
-
row[col] = extra[col]
|
|
153
|
+
for col in extra:
|
|
154
|
+
_check_optional_columns(col)
|
|
155
|
+
row[col] = extra[col]
|
|
88
156
|
data.append(row)
|
|
89
157
|
dataframe = pd.DataFrame(data)
|
|
90
|
-
dataframe = dataframe
|
|
158
|
+
dataframe = normalize_pandas_df(dataframe, index_key=index_key)
|
|
91
159
|
return dataframe
|
|
92
160
|
|
|
93
161
|
|
|
94
|
-
class
|
|
95
|
-
"""Metadata for the ROI table."""
|
|
96
|
-
|
|
97
|
-
fractal_table_version: Literal["1"] = "1"
|
|
98
|
-
type: Literal["roi_table"] = "roi_table"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class RegionMeta(BaseModel):
|
|
102
|
-
"""Metadata for the region."""
|
|
103
|
-
|
|
104
|
-
path: str
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class MaskingRoiTableV1Meta(BackendMeta):
|
|
108
|
-
"""Metadata for the ROI table."""
|
|
109
|
-
|
|
110
|
-
fractal_table_version: Literal["1"] = "1"
|
|
111
|
-
type: Literal["masking_roi_table"] = "masking_roi_table"
|
|
112
|
-
region: RegionMeta | None = None
|
|
113
|
-
instance_key: str = "label"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
_roi_meta = TypeVar("_roi_meta", RoiTableV1Meta, MaskingRoiTableV1Meta)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class _GenericRoiTableV1(Generic[_roi_meta]):
|
|
120
|
-
"""Class to a non-specific table."""
|
|
121
|
-
|
|
122
|
-
_meta: _roi_meta
|
|
123
|
-
|
|
162
|
+
class GenericRoiTableV1(AbstractBaseTable):
|
|
124
163
|
def __init__(
|
|
125
|
-
self,
|
|
164
|
+
self,
|
|
165
|
+
*,
|
|
166
|
+
rois: Iterable[Roi] | None = None,
|
|
167
|
+
meta: BackendMeta,
|
|
126
168
|
) -> None:
|
|
127
|
-
|
|
128
|
-
if meta is None:
|
|
129
|
-
raise NgioValueError("Metadata must be provided.")
|
|
130
|
-
self._meta = meta
|
|
131
|
-
self._table_backend = None
|
|
169
|
+
table = None
|
|
132
170
|
|
|
133
|
-
self._rois =
|
|
171
|
+
self._rois: dict[str, Roi] | None = None
|
|
134
172
|
if rois is not None:
|
|
173
|
+
self._rois = {}
|
|
135
174
|
self.add(rois)
|
|
175
|
+
table = _rois_to_dataframe(self._rois, index_key=meta.index_key)
|
|
176
|
+
|
|
177
|
+
super().__init__(table_data=table, meta=meta)
|
|
178
|
+
|
|
179
|
+
def __repr__(self) -> str:
|
|
180
|
+
"""Return a string representation of the table."""
|
|
181
|
+
rois = self.rois()
|
|
182
|
+
prop = f"num_rois={len(rois)}"
|
|
183
|
+
class_name = self.__class__.__name__
|
|
184
|
+
return f"{class_name}({prop})"
|
|
136
185
|
|
|
137
186
|
@staticmethod
|
|
138
|
-
def
|
|
187
|
+
def table_type() -> str:
|
|
139
188
|
"""Return the type of the table."""
|
|
140
|
-
|
|
189
|
+
return "generic_roi_table"
|
|
141
190
|
|
|
142
191
|
@staticmethod
|
|
143
192
|
def version() -> Literal["1"]:
|
|
144
193
|
"""Return the version of the fractal table."""
|
|
145
194
|
return "1"
|
|
146
195
|
|
|
147
|
-
@
|
|
148
|
-
def
|
|
149
|
-
"""Return the
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
@staticmethod
|
|
153
|
-
def _index_type() -> Literal["int", "str"]:
|
|
154
|
-
"""Return the index type of the table."""
|
|
155
|
-
raise NotImplementedError
|
|
196
|
+
@property
|
|
197
|
+
def table_data(self) -> TabularData:
|
|
198
|
+
"""Return the table."""
|
|
199
|
+
if self._rois is None:
|
|
200
|
+
return super().table_data
|
|
156
201
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
raise NotImplementedError
|
|
202
|
+
if len(self.rois()) > 0:
|
|
203
|
+
self._table_data = _rois_to_dataframe(self._rois, index_key=self.index_key)
|
|
204
|
+
return super().table_data
|
|
161
205
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if
|
|
206
|
+
def set_table_data(
|
|
207
|
+
self, table_data: TabularData | None = None, refresh: bool = False
|
|
208
|
+
) -> None:
|
|
209
|
+
if table_data is not None:
|
|
210
|
+
if not isinstance(table_data, TabularData):
|
|
211
|
+
raise NgioValueError(
|
|
212
|
+
"The table must be a pandas DataFrame, polars LazyFrame, "
|
|
213
|
+
" or AnnData object."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
table_data, rois = _table_to_rois(
|
|
217
|
+
table_data,
|
|
218
|
+
index_key=self.index_key,
|
|
219
|
+
index_type=self.index_type,
|
|
220
|
+
required_columns=REQUIRED_COLUMNS,
|
|
221
|
+
)
|
|
222
|
+
self._table_data = table_data
|
|
223
|
+
self._rois = rois
|
|
166
224
|
return None
|
|
167
|
-
return self._table_backend.backend_name()
|
|
168
225
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
cls, handler: ZarrGroupHandler, backend_name: str | None = None
|
|
172
|
-
) -> "_GenericRoiTableV1":
|
|
173
|
-
"""Create a new ROI table from a Zarr store."""
|
|
174
|
-
meta = cls._meta_type()(**handler.load_attrs())
|
|
175
|
-
|
|
176
|
-
if backend_name is None:
|
|
177
|
-
backend = ImplementedTableBackends().get_backend(
|
|
178
|
-
backend_name=meta.backend,
|
|
179
|
-
group_handler=handler,
|
|
180
|
-
index_key=cls._index_key(),
|
|
181
|
-
index_type=cls._index_type(),
|
|
182
|
-
)
|
|
183
|
-
else:
|
|
184
|
-
backend = ImplementedTableBackends().get_backend(
|
|
185
|
-
backend_name=backend_name,
|
|
186
|
-
group_handler=handler,
|
|
187
|
-
index_key=cls._index_key(),
|
|
188
|
-
index_type=cls._index_type(),
|
|
189
|
-
)
|
|
190
|
-
meta.backend = backend_name
|
|
226
|
+
if self._table_data is not None and not refresh:
|
|
227
|
+
return None
|
|
191
228
|
|
|
192
|
-
if
|
|
229
|
+
if self._table_backend is None:
|
|
193
230
|
raise NgioValueError(
|
|
194
|
-
"The
|
|
231
|
+
"The table does not have a DataFrame in memory nor a backend."
|
|
195
232
|
)
|
|
196
233
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
dataframe = backend.load_as_pandas_df()
|
|
203
|
-
dataframe = validate_columns(
|
|
204
|
-
dataframe,
|
|
234
|
+
table_data, rois = _table_to_rois(
|
|
235
|
+
self._table_backend.load(),
|
|
236
|
+
index_key=self.index_key,
|
|
237
|
+
index_type=self.index_type,
|
|
205
238
|
required_columns=REQUIRED_COLUMNS,
|
|
206
|
-
optional_columns=OPTIONAL_COLUMNS,
|
|
207
239
|
)
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
self._table_data = table_data
|
|
241
|
+
self._rois = rois
|
|
210
242
|
|
|
211
|
-
def
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
group_handler=handler,
|
|
220
|
-
index_key=self._index_key(),
|
|
221
|
-
index_type=self._index_type(),
|
|
222
|
-
)
|
|
223
|
-
self._meta.backend = backend_name
|
|
224
|
-
self._table_backend = backend
|
|
243
|
+
def _check_rois(self) -> None:
|
|
244
|
+
"""Load the ROIs from the table.
|
|
245
|
+
|
|
246
|
+
If the ROIs are already loaded, do nothing.
|
|
247
|
+
If the ROIs are not loaded, load them from the table.
|
|
248
|
+
"""
|
|
249
|
+
if self._rois is None:
|
|
250
|
+
self._rois = _dataframe_to_rois(self.dataframe)
|
|
225
251
|
|
|
226
252
|
def rois(self) -> list[Roi]:
|
|
227
253
|
"""List all ROIs in the table."""
|
|
254
|
+
self._check_rois()
|
|
255
|
+
if self._rois is None:
|
|
256
|
+
return []
|
|
228
257
|
return list(self._rois.values())
|
|
229
258
|
|
|
230
259
|
def add(self, roi: Roi | Iterable[Roi], overwrite: bool = False) -> None:
|
|
@@ -237,31 +266,49 @@ class _GenericRoiTableV1(Generic[_roi_meta]):
|
|
|
237
266
|
if isinstance(roi, Roi):
|
|
238
267
|
roi = [roi]
|
|
239
268
|
|
|
269
|
+
self._check_rois()
|
|
270
|
+
if self._rois is None:
|
|
271
|
+
self._rois = {}
|
|
272
|
+
|
|
240
273
|
for _roi in roi:
|
|
241
274
|
if not overwrite and _roi.name in self._rois:
|
|
242
275
|
raise NgioValueError(f"ROI {_roi.name} already exists in the table.")
|
|
243
276
|
self._rois[_roi.name] = _roi
|
|
244
277
|
|
|
245
|
-
def
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"Please add the table to a OME-Zarr Image before calling consolidate."
|
|
251
|
-
)
|
|
278
|
+
def get(self, roi_name: str) -> Roi:
|
|
279
|
+
"""Get an ROI from the table."""
|
|
280
|
+
self._check_rois()
|
|
281
|
+
if self._rois is None:
|
|
282
|
+
self._rois = {}
|
|
252
283
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
284
|
+
if roi_name not in self._rois:
|
|
285
|
+
raise NgioValueError(f"ROI {roi_name} not found in the table.")
|
|
286
|
+
return self._rois[roi_name]
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def from_table_data(
|
|
290
|
+
cls, table_data: TabularData, meta: BackendMeta
|
|
291
|
+
) -> "GenericRoiTableV1":
|
|
292
|
+
"""Create a new ROI table from a table data."""
|
|
293
|
+
_, rois = _table_to_rois(
|
|
294
|
+
table=table_data,
|
|
295
|
+
index_key=meta.index_key,
|
|
296
|
+
index_type=meta.index_type,
|
|
256
297
|
required_columns=REQUIRED_COLUMNS,
|
|
257
|
-
optional_columns=OPTIONAL_COLUMNS,
|
|
258
|
-
)
|
|
259
|
-
self._table_backend.write(
|
|
260
|
-
dataframe, metadata=self._meta.model_dump(exclude_none=True), mode="pandas"
|
|
261
298
|
)
|
|
299
|
+
return cls(rois=rois.values(), meta=meta)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class RoiTableV1Meta(BackendMeta):
|
|
303
|
+
"""Metadata for the ROI table."""
|
|
262
304
|
|
|
305
|
+
fractal_table_version: Literal["1"] = "1"
|
|
306
|
+
type: Literal["roi_table"] = "roi_table"
|
|
307
|
+
index_key: str | None = "FieldIndex"
|
|
308
|
+
index_type: Literal["str", "int"] | None = "str"
|
|
263
309
|
|
|
264
|
-
|
|
310
|
+
|
|
311
|
+
class RoiTableV1(GenericRoiTableV1):
|
|
265
312
|
"""Class to handle fractal ROI tables.
|
|
266
313
|
|
|
267
314
|
To know more about the ROI table format, please refer to the
|
|
@@ -269,43 +316,57 @@ class RoiTableV1(_GenericRoiTableV1[RoiTableV1Meta]):
|
|
|
269
316
|
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
270
317
|
"""
|
|
271
318
|
|
|
272
|
-
def __init__(
|
|
319
|
+
def __init__(
|
|
320
|
+
self, rois: Iterable[Roi] | None = None, *, meta: RoiTableV1Meta | None = None
|
|
321
|
+
) -> None:
|
|
273
322
|
"""Create a new ROI table."""
|
|
274
|
-
|
|
323
|
+
if meta is None:
|
|
324
|
+
meta = RoiTableV1Meta()
|
|
275
325
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
326
|
+
if meta.index_key is None:
|
|
327
|
+
meta.index_key = "FieldIndex"
|
|
328
|
+
|
|
329
|
+
if meta.index_type is None:
|
|
330
|
+
meta.index_type = "str"
|
|
331
|
+
super().__init__(meta=meta, rois=rois)
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_handler(
|
|
335
|
+
cls,
|
|
336
|
+
handler: ZarrGroupHandler,
|
|
337
|
+
backend: str | TableBackendProtocol | None = None,
|
|
338
|
+
) -> "RoiTableV1":
|
|
339
|
+
table = cls._from_handler(
|
|
340
|
+
handler=handler,
|
|
341
|
+
backend=backend,
|
|
342
|
+
meta_model=RoiTableV1Meta,
|
|
343
|
+
)
|
|
344
|
+
return table
|
|
280
345
|
|
|
281
346
|
@staticmethod
|
|
282
|
-
def
|
|
347
|
+
def table_type() -> Literal["roi_table"]:
|
|
283
348
|
"""Return the type of the table."""
|
|
284
349
|
return "roi_table"
|
|
285
350
|
|
|
286
|
-
@staticmethod
|
|
287
|
-
def _index_key() -> str:
|
|
288
|
-
"""Return the index key of the table."""
|
|
289
|
-
return "FieldIndex"
|
|
290
351
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"""Return the index type of the table."""
|
|
294
|
-
return "str"
|
|
352
|
+
class RegionMeta(BaseModel):
|
|
353
|
+
"""Metadata for the region."""
|
|
295
354
|
|
|
296
|
-
|
|
297
|
-
def _meta_type() -> _type[RoiTableV1Meta]:
|
|
298
|
-
"""Return the metadata type of the table."""
|
|
299
|
-
return RoiTableV1Meta
|
|
355
|
+
path: str
|
|
300
356
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
357
|
+
|
|
358
|
+
class MaskingRoiTableV1Meta(BackendMeta):
|
|
359
|
+
"""Metadata for the ROI table."""
|
|
360
|
+
|
|
361
|
+
fractal_table_version: Literal["1"] = "1"
|
|
362
|
+
type: Literal["masking_roi_table"] = "masking_roi_table"
|
|
363
|
+
region: RegionMeta | None = None
|
|
364
|
+
instance_key: str = "label"
|
|
365
|
+
index_key: str | None = "label"
|
|
366
|
+
index_type: Literal["int", "str"] | None = "int"
|
|
306
367
|
|
|
307
368
|
|
|
308
|
-
class MaskingRoiTableV1(
|
|
369
|
+
class MaskingRoiTableV1(GenericRoiTableV1):
|
|
309
370
|
"""Class to handle fractal ROI tables.
|
|
310
371
|
|
|
311
372
|
To know more about the ROI table format, please refer to the
|
|
@@ -316,54 +377,73 @@ class MaskingRoiTableV1(_GenericRoiTableV1[MaskingRoiTableV1Meta]):
|
|
|
316
377
|
def __init__(
|
|
317
378
|
self,
|
|
318
379
|
rois: Iterable[Roi] | None = None,
|
|
380
|
+
*,
|
|
319
381
|
reference_label: str | None = None,
|
|
382
|
+
meta: MaskingRoiTableV1Meta | None = None,
|
|
320
383
|
) -> None:
|
|
321
384
|
"""Create a new ROI table."""
|
|
322
|
-
meta
|
|
385
|
+
if meta is None:
|
|
386
|
+
meta = MaskingRoiTableV1Meta()
|
|
387
|
+
|
|
323
388
|
if reference_label is not None:
|
|
324
389
|
meta.region = RegionMeta(path=reference_label)
|
|
325
|
-
|
|
390
|
+
|
|
391
|
+
if meta.index_key is None:
|
|
392
|
+
meta.index_key = "label"
|
|
393
|
+
|
|
394
|
+
if meta.index_type is None:
|
|
395
|
+
meta.index_type = "int"
|
|
396
|
+
meta.instance_key = meta.index_key
|
|
397
|
+
super().__init__(meta=meta, rois=rois)
|
|
326
398
|
|
|
327
399
|
def __repr__(self) -> str:
|
|
328
400
|
"""Return a string representation of the table."""
|
|
329
|
-
|
|
401
|
+
rois = self.rois()
|
|
330
402
|
if self.reference_label is not None:
|
|
331
|
-
prop
|
|
403
|
+
prop = f"num_rois={len(rois)}, reference_label={self.reference_label}"
|
|
404
|
+
else:
|
|
405
|
+
prop = f"num_rois={len(rois)}"
|
|
332
406
|
return f"MaskingRoiTableV1({prop})"
|
|
333
407
|
|
|
408
|
+
@classmethod
|
|
409
|
+
def from_handler(
|
|
410
|
+
cls,
|
|
411
|
+
handler: ZarrGroupHandler,
|
|
412
|
+
backend: str | TableBackendProtocol | None = None,
|
|
413
|
+
) -> "MaskingRoiTableV1":
|
|
414
|
+
table = cls._from_handler(
|
|
415
|
+
handler=handler,
|
|
416
|
+
backend=backend,
|
|
417
|
+
meta_model=MaskingRoiTableV1Meta,
|
|
418
|
+
)
|
|
419
|
+
return table
|
|
420
|
+
|
|
334
421
|
@staticmethod
|
|
335
|
-
def
|
|
422
|
+
def table_type() -> Literal["masking_roi_table"]:
|
|
336
423
|
"""Return the type of the table."""
|
|
337
424
|
return "masking_roi_table"
|
|
338
425
|
|
|
339
|
-
@
|
|
340
|
-
def
|
|
341
|
-
"""Return the
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return "int"
|
|
348
|
-
|
|
349
|
-
@staticmethod
|
|
350
|
-
def _meta_type() -> _type[MaskingRoiTableV1Meta]:
|
|
351
|
-
"""Return the metadata type of the table."""
|
|
352
|
-
return MaskingRoiTableV1Meta
|
|
426
|
+
@property
|
|
427
|
+
def meta(self) -> MaskingRoiTableV1Meta:
|
|
428
|
+
"""Return the metadata of the table."""
|
|
429
|
+
if not isinstance(self._meta, MaskingRoiTableV1Meta):
|
|
430
|
+
raise NgioValueError(
|
|
431
|
+
"The metadata of the table is not of type MaskingRoiTableV1Meta."
|
|
432
|
+
)
|
|
433
|
+
return self._meta
|
|
353
434
|
|
|
354
435
|
@property
|
|
355
436
|
def reference_label(self) -> str | None:
|
|
356
437
|
"""Return the reference label."""
|
|
357
|
-
path = self.
|
|
438
|
+
path = self.meta.region
|
|
358
439
|
if path is None:
|
|
359
440
|
return None
|
|
441
|
+
|
|
360
442
|
path = path.path
|
|
361
443
|
path = path.split("/")[-1]
|
|
362
444
|
return path
|
|
363
445
|
|
|
364
|
-
def get(self, label: int) -> Roi:
|
|
446
|
+
def get(self, label: int | str) -> Roi: # type: ignore
|
|
365
447
|
"""Get an ROI from the table."""
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
raise KeyError(f"ROI {_label} not found in the table.")
|
|
369
|
-
return self._rois[_label]
|
|
448
|
+
roi_name = str(label)
|
|
449
|
+
return super().get(roi_name)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import fsspec.implementations.http
|
|
2
|
+
from aiohttp import ClientResponseError
|
|
3
|
+
|
|
4
|
+
from ngio.utils import NgioValueError
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
def fractal_fsspec_store(
|
|
@@ -9,5 +12,31 @@ def fractal_fsspec_store(
|
|
|
9
12
|
if fractal_token is not None:
|
|
10
13
|
client_kwargs["headers"] = {"Authorization": f"Bearer {fractal_token}"}
|
|
11
14
|
fs = fsspec.implementations.http.HTTPFileSystem(client_kwargs=client_kwargs)
|
|
15
|
+
|
|
12
16
|
store = fs.get_mapper(url)
|
|
17
|
+
|
|
18
|
+
possible_keys = [".zgroup", ".zarray"]
|
|
19
|
+
for key in possible_keys:
|
|
20
|
+
try:
|
|
21
|
+
value = store.get(key)
|
|
22
|
+
if value is not None:
|
|
23
|
+
break
|
|
24
|
+
except ClientResponseError as e:
|
|
25
|
+
if e.status == 401 and fractal_token is None:
|
|
26
|
+
raise NgioValueError(
|
|
27
|
+
"No auto token is provided. You need a valid "
|
|
28
|
+
f"'fractal_token' to access: {url}."
|
|
29
|
+
) from e
|
|
30
|
+
elif e.status == 401 and fractal_token is not None:
|
|
31
|
+
raise NgioValueError(
|
|
32
|
+
f"The 'fractal_token' provided is invalid for: {url}."
|
|
33
|
+
) from e
|
|
34
|
+
else:
|
|
35
|
+
raise e
|
|
36
|
+
else:
|
|
37
|
+
raise NgioValueError(
|
|
38
|
+
f"Store {url} can not be read. Possible problems are: \n"
|
|
39
|
+
"- The url does not exist. \n"
|
|
40
|
+
f"- The url is not a valid .zarr. \n"
|
|
41
|
+
)
|
|
13
42
|
return store
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ngio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0a0
|
|
4
4
|
Summary: Next Generation file format IO
|
|
5
|
-
Project-URL: homepage, https://github.com/
|
|
6
|
-
Project-URL: repository, https://github.com/
|
|
5
|
+
Project-URL: homepage, https://github.com/fractal-analytics-platform/ngio
|
|
6
|
+
Project-URL: repository, https://github.com/fractal-analytics-platform/ngio
|
|
7
7
|
Author-email: Lorenzo Cerrone <lorenzo.cerrone@uzh.ch>
|
|
8
8
|
License: BSD-3-Clause
|
|
9
9
|
License-File: LICENSE
|