ngio 0.2.0a2__py3-none-any.whl → 0.5.0b4__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 (106) hide show
  1. ngio/__init__.py +40 -12
  2. ngio/common/__init__.py +16 -32
  3. ngio/common/_dimensions.py +270 -48
  4. ngio/common/_masking_roi.py +153 -0
  5. ngio/common/_pyramid.py +267 -73
  6. ngio/common/_roi.py +290 -66
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +54 -22
  9. ngio/experimental/__init__.py +5 -0
  10. ngio/experimental/iterators/__init__.py +15 -0
  11. ngio/experimental/iterators/_abstract_iterator.py +390 -0
  12. ngio/experimental/iterators/_feature.py +189 -0
  13. ngio/experimental/iterators/_image_processing.py +130 -0
  14. ngio/experimental/iterators/_mappers.py +48 -0
  15. ngio/experimental/iterators/_rois_utils.py +126 -0
  16. ngio/experimental/iterators/_segmentation.py +235 -0
  17. ngio/hcs/__init__.py +17 -58
  18. ngio/hcs/_plate.py +1354 -0
  19. ngio/images/__init__.py +30 -9
  20. ngio/images/_abstract_image.py +968 -0
  21. ngio/images/_create_synt_container.py +132 -0
  22. ngio/images/_create_utils.py +423 -0
  23. ngio/images/_image.py +926 -0
  24. ngio/images/_label.py +417 -0
  25. ngio/images/_masked_image.py +531 -0
  26. ngio/images/_ome_zarr_container.py +1235 -0
  27. ngio/images/_table_ops.py +471 -0
  28. ngio/io_pipes/__init__.py +75 -0
  29. ngio/io_pipes/_io_pipes.py +361 -0
  30. ngio/io_pipes/_io_pipes_masked.py +488 -0
  31. ngio/io_pipes/_io_pipes_roi.py +146 -0
  32. ngio/io_pipes/_io_pipes_types.py +56 -0
  33. ngio/io_pipes/_match_shape.py +377 -0
  34. ngio/io_pipes/_ops_axes.py +344 -0
  35. ngio/io_pipes/_ops_slices.py +411 -0
  36. ngio/io_pipes/_ops_slices_utils.py +199 -0
  37. ngio/io_pipes/_ops_transforms.py +104 -0
  38. ngio/io_pipes/_zoom_transform.py +180 -0
  39. ngio/ome_zarr_meta/__init__.py +39 -15
  40. ngio/ome_zarr_meta/_meta_handlers.py +490 -96
  41. ngio/ome_zarr_meta/ngio_specs/__init__.py +24 -10
  42. ngio/ome_zarr_meta/ngio_specs/_axes.py +268 -234
  43. ngio/ome_zarr_meta/ngio_specs/_channels.py +125 -41
  44. ngio/ome_zarr_meta/ngio_specs/_dataset.py +42 -87
  45. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +536 -2
  46. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +202 -198
  47. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +72 -34
  48. ngio/ome_zarr_meta/v04/__init__.py +21 -5
  49. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  50. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +151 -90
  51. ngio/ome_zarr_meta/v05/__init__.py +27 -0
  52. ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
  53. ngio/ome_zarr_meta/v05/_v05_spec.py +511 -0
  54. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
  55. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
  56. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
  57. ngio/resources/__init__.py +55 -0
  58. ngio/resources/resource_model.py +36 -0
  59. ngio/tables/__init__.py +20 -4
  60. ngio/tables/_abstract_table.py +270 -0
  61. ngio/tables/_tables_container.py +449 -0
  62. ngio/tables/backends/__init__.py +50 -1
  63. ngio/tables/backends/_abstract_backend.py +200 -31
  64. ngio/tables/backends/_anndata.py +139 -0
  65. ngio/tables/backends/_anndata_utils.py +10 -114
  66. ngio/tables/backends/_csv.py +19 -0
  67. ngio/tables/backends/_json.py +92 -0
  68. ngio/tables/backends/_parquet.py +19 -0
  69. ngio/tables/backends/_py_arrow_backends.py +222 -0
  70. ngio/tables/backends/_table_backends.py +162 -38
  71. ngio/tables/backends/_utils.py +608 -0
  72. ngio/tables/v1/__init__.py +19 -4
  73. ngio/tables/v1/_condition_table.py +71 -0
  74. ngio/tables/v1/_feature_table.py +79 -115
  75. ngio/tables/v1/_generic_table.py +21 -90
  76. ngio/tables/v1/_roi_table.py +486 -137
  77. ngio/transforms/__init__.py +5 -0
  78. ngio/transforms/_zoom.py +19 -0
  79. ngio/utils/__init__.py +16 -14
  80. ngio/utils/_cache.py +48 -0
  81. ngio/utils/_datasets.py +121 -13
  82. ngio/utils/_fractal_fsspec_store.py +42 -0
  83. ngio/utils/_zarr_utils.py +374 -218
  84. ngio-0.5.0b4.dist-info/METADATA +147 -0
  85. ngio-0.5.0b4.dist-info/RECORD +88 -0
  86. {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/WHEEL +1 -1
  87. ngio/common/_array_pipe.py +0 -160
  88. ngio/common/_axes_transforms.py +0 -63
  89. ngio/common/_common_types.py +0 -5
  90. ngio/common/_slicer.py +0 -97
  91. ngio/images/abstract_image.py +0 -240
  92. ngio/images/create.py +0 -251
  93. ngio/images/image.py +0 -389
  94. ngio/images/label.py +0 -236
  95. ngio/images/omezarr_container.py +0 -535
  96. ngio/ome_zarr_meta/_generic_handlers.py +0 -320
  97. ngio/ome_zarr_meta/v04/_meta_handlers.py +0 -54
  98. ngio/tables/_validators.py +0 -192
  99. ngio/tables/backends/_anndata_v1.py +0 -75
  100. ngio/tables/backends/_json_v1.py +0 -56
  101. ngio/tables/tables_container.py +0 -300
  102. ngio/tables/v1/_masking_roi_table.py +0 -175
  103. ngio/utils/_logger.py +0 -29
  104. ngio-0.2.0a2.dist-info/METADATA +0 -95
  105. ngio-0.2.0a2.dist-info/RECORD +0 -53
  106. {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/licenses/LICENSE +0 -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()