ngio 0.2.9__py3-none-any.whl → 0.3.0__py3-none-any.whl

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