ngio 0.2.9__py3-none-any.whl → 0.3.0a1__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.
@@ -1,7 +1,7 @@
1
1
  """Abstract class for handling OME-NGFF images."""
2
2
 
3
+ import warnings
3
4
  from collections.abc import Collection
4
- from typing import Literal, overload
5
5
 
6
6
  import numpy as np
7
7
 
@@ -22,12 +22,15 @@ from ngio.ome_zarr_meta.ngio_specs import (
22
22
  TimeUnits,
23
23
  )
24
24
  from ngio.tables import (
25
+ ConditionTable,
25
26
  FeatureTable,
26
27
  GenericRoiTable,
27
28
  MaskingRoiTable,
28
29
  RoiTable,
29
30
  Table,
31
+ TableBackend,
30
32
  TablesContainer,
33
+ TableType,
31
34
  TypedTable,
32
35
  )
33
36
  from ngio.utils import (
@@ -264,9 +267,7 @@ class OmeZarrContainer:
264
267
  if masking_table_name is None:
265
268
  masking_table = masking_label.build_masking_roi_table()
266
269
  else:
267
- masking_table = self.get_table(
268
- masking_table_name, check_type="masking_roi_table"
269
- )
270
+ masking_table = self.get_masking_roi_table(name=masking_table_name)
270
271
 
271
272
  return MaskedImage(
272
273
  group_handler=image._group_handler,
@@ -345,84 +346,116 @@ class OmeZarrContainer:
345
346
  )
346
347
  return new_ome_zarr
347
348
 
348
- def list_tables(self) -> list[str]:
349
+ def list_tables(self, filter_types: str | None = None) -> list[str]:
349
350
  """List all tables in the image."""
350
- return self.tables_container.list()
351
+ return self.tables_container.list(filter_types=filter_types)
351
352
 
352
353
  def list_roi_tables(self) -> list[str]:
353
354
  """List all ROI tables in the image."""
354
355
  return self.tables_container.list_roi_tables()
355
356
 
356
- @overload
357
- def get_table(self, name: str) -> Table: ...
357
+ def get_roi_table(self, name: str) -> RoiTable:
358
+ """Get a ROI table from the image.
359
+
360
+ Args:
361
+ name (str): The name of the table.
362
+ """
363
+ table = self.tables_container.get(name=name, strict=True)
364
+ if not isinstance(table, RoiTable):
365
+ raise NgioValueError(f"Table {name} is not a ROI table. Got {type(table)}")
366
+ return table
367
+
368
+ def get_masking_roi_table(self, name: str) -> MaskingRoiTable:
369
+ """Get a masking ROI table from the image.
370
+
371
+ Args:
372
+ name (str): The name of the table.
373
+ """
374
+ table = self.tables_container.get(name=name, strict=True)
375
+ if not isinstance(table, MaskingRoiTable):
376
+ raise NgioValueError(
377
+ f"Table {name} is not a masking ROI table. Got {type(table)}"
378
+ )
379
+ return table
380
+
381
+ def get_feature_table(self, name: str) -> FeatureTable:
382
+ """Get a feature table from the image.
358
383
 
359
- @overload
360
- def get_table(self, name: str, check_type: None) -> Table: ...
384
+ Args:
385
+ name (str): The name of the table.
386
+ """
387
+ table = self.tables_container.get(name=name, strict=True)
388
+ if not isinstance(table, FeatureTable):
389
+ raise NgioValueError(
390
+ f"Table {name} is not a feature table. Got {type(table)}"
391
+ )
392
+ return table
361
393
 
362
- @overload
363
- def get_table(self, name: str, check_type: Literal["roi_table"]) -> RoiTable: ...
394
+ def get_generic_roi_table(self, name: str) -> GenericRoiTable:
395
+ """Get a generic ROI table from the image.
364
396
 
365
- @overload
366
- def get_table(
367
- self, name: str, check_type: Literal["masking_roi_table"]
368
- ) -> MaskingRoiTable: ...
397
+ Args:
398
+ name (str): The name of the table.
399
+ """
400
+ table = self.tables_container.get(name=name, strict=True)
401
+ if not isinstance(table, GenericRoiTable):
402
+ raise NgioValueError(
403
+ f"Table {name} is not a generic ROI table. Got {type(table)}"
404
+ )
405
+ return table
369
406
 
370
- @overload
371
- def get_table(
372
- self, name: str, check_type: Literal["feature_table"]
373
- ) -> FeatureTable: ...
407
+ def get_condition_table(self, name: str) -> ConditionTable:
408
+ """Get a condition table from the image.
374
409
 
375
- @overload
376
- def get_table(
377
- self, name: str, check_type: Literal["generic_roi_table"]
378
- ) -> GenericRoiTable: ...
410
+ Args:
411
+ name (str): The name of the table.
412
+ """
413
+ table = self.tables_container.get(name=name, strict=True)
414
+ if not isinstance(table, ConditionTable):
415
+ raise NgioValueError(
416
+ f"Table {name} is not a condition table. Got {type(table)}"
417
+ )
418
+ return table
379
419
 
380
420
  def get_table(self, name: str, check_type: TypedTable | None = None) -> Table:
381
421
  """Get a table from the image.
382
422
 
383
423
  Args:
384
424
  name (str): The name of the table.
385
- check_type (TypedTable | None): The type of the table. If None, the
386
- type is not checked. If a type is provided, the table must be of that
387
- type.
425
+ check_type (TypedTable | None): Deprecated. Please use
426
+ 'get_table_as' instead, or one of the type specific
427
+ get_*table() methods.
428
+
429
+ """
430
+ if check_type is not None:
431
+ warnings.warn(
432
+ "The 'check_type' argument is deprecated, and will be removed in "
433
+ "ngio=0.3. Use 'get_table_as' instead or one of the "
434
+ "type specific get_*table() methods.",
435
+ DeprecationWarning,
436
+ stacklevel=2,
437
+ )
438
+ return self.tables_container.get(name=name, strict=False)
439
+
440
+ def get_table_as(
441
+ self,
442
+ name: str,
443
+ table_cls: type[TableType],
444
+ backend: TableBackend | None = None,
445
+ ) -> TableType:
446
+ """Get a table from the image as a specific type.
447
+
448
+ Args:
449
+ name (str): The name of the table.
450
+ table_cls (type[TableType]): The type of the table.
451
+ backend (TableBackend | None): The backend to use. If None,
452
+ the default backend is used.
388
453
  """
389
- if check_type is None:
390
- table = self.tables_container.get(name, strict=False)
391
- return table
392
-
393
- table = self.tables_container.get(name, strict=True)
394
- match check_type:
395
- case "roi_table":
396
- if not isinstance(table, RoiTable):
397
- raise NgioValueError(
398
- f"Table '{name}' is not a ROI table. Found type: {table.type()}"
399
- )
400
- return table
401
- case "masking_roi_table":
402
- if not isinstance(table, MaskingRoiTable):
403
- raise NgioValueError(
404
- f"Table '{name}' is not a masking ROI table. "
405
- f"Found type: {table.type()}"
406
- )
407
- return table
408
-
409
- case "generic_roi_table":
410
- if not isinstance(table, GenericRoiTable):
411
- raise NgioValueError(
412
- f"Table '{name}' is not a generic ROI table. "
413
- f"Found type: {table.type()}"
414
- )
415
- return table
416
-
417
- case "feature_table":
418
- if not isinstance(table, FeatureTable):
419
- raise NgioValueError(
420
- f"Table '{name}' is not a feature table. "
421
- f"Found type: {table.type()}"
422
- )
423
- return table
424
- case _:
425
- raise NgioValueError(f"Unknown check_type: {check_type}")
454
+ return self.tables_container.get_as(
455
+ name=name,
456
+ table_cls=table_cls,
457
+ backend=backend,
458
+ )
426
459
 
427
460
  def build_image_roi_table(self, name: str = "image") -> RoiTable:
428
461
  """Compute the ROI table for an image."""
@@ -436,7 +469,7 @@ class OmeZarrContainer:
436
469
  self,
437
470
  name: str,
438
471
  table: Table,
439
- backend: str | None = None,
472
+ backend: TableBackend = "anndata",
440
473
  overwrite: bool = False,
441
474
  ) -> None:
442
475
  """Add a table to the image."""
@@ -499,9 +532,7 @@ class OmeZarrContainer:
499
532
  if masking_table_name is None:
500
533
  masking_table = masking_label.build_masking_roi_table()
501
534
  else:
502
- masking_table = self.get_table(
503
- masking_table_name, check_type="masking_roi_table"
504
- )
535
+ masking_table = self.get_masking_roi_table(name=masking_table_name)
505
536
 
506
537
  return MaskedLabel(
507
538
  group_handler=label._group_handler,
@@ -187,10 +187,6 @@ class GenericMetaHandler(
187
187
 
188
188
  raise NgioValueError(f"Could not load metadata: {meta_or_error}")
189
189
 
190
- def safe_load_meta(self) -> _image_meta | ConverterError:
191
- """Load the metadata from the store."""
192
- return self._load_meta(return_error=True)
193
-
194
190
  def _write_meta(self, meta) -> None:
195
191
  """Write the metadata to the store."""
196
192
  _meta = self._meta_exporter(metadata=meta)
@@ -217,6 +213,10 @@ class ImageMetaHandler(
217
213
  return meta
218
214
  raise NgioValueError(f"Could not load metadata: {meta}")
219
215
 
216
+ def safe_load_meta(self) -> NgioImageMeta | ConverterError:
217
+ """Load the metadata from the store."""
218
+ return self._load_meta(return_error=True)
219
+
220
220
 
221
221
  class LabelMetaHandler(
222
222
  GenericMetaHandler[NgioLabelMeta, LabelMetaImporter, LabelMetaExporter]
@@ -230,6 +230,10 @@ class LabelMetaHandler(
230
230
  return meta
231
231
  raise NgioValueError(f"Could not load metadata: {meta}")
232
232
 
233
+ def safe_load_meta(self) -> NgioLabelMeta | ConverterError:
234
+ """Load the metadata from the store."""
235
+ return self._load_meta(return_error=True)
236
+
233
237
 
234
238
  ###########################################################################
235
239
  #
@@ -267,10 +271,6 @@ class GenericHCSMetaHandler(Generic[_hcs_meta, _hcs_meta_importer, _hcs_meta_exp
267
271
 
268
272
  raise NgioValueError(f"Could not load metadata: {meta_or_error}")
269
273
 
270
- def safe_load_meta(self) -> _hcs_meta | ConverterError:
271
- """Load the metadata from the store."""
272
- return self._load_meta(return_error=True)
273
-
274
274
  def _write_meta(self, meta) -> None:
275
275
  _meta = self._meta_exporter(metadata=meta)
276
276
  self._group_handler.write_attrs(_meta)
@@ -295,6 +295,10 @@ class WellMetaHandler(
295
295
  return meta
296
296
  raise NgioValueError(f"Could not load metadata: {meta}")
297
297
 
298
+ def safe_load_meta(self) -> NgioWellMeta | ConverterError:
299
+ """Load the metadata from the store."""
300
+ return self._load_meta(return_error=True)
301
+
298
302
 
299
303
  class PlateMetaHandler(
300
304
  GenericHCSMetaHandler[NgioPlateMeta, PlateMetaImporter, PlateMetaExporter]
@@ -308,6 +312,10 @@ class PlateMetaHandler(
308
312
  return meta
309
313
  raise NgioValueError(f"Could not load metadata: {meta}")
310
314
 
315
+ def safe_load_meta(self) -> NgioPlateMeta | ConverterError:
316
+ """Load the metadata from the store."""
317
+ return self._load_meta(return_error=True)
318
+
311
319
 
312
320
  ###########################################################################
313
321
  #
@@ -2,12 +2,15 @@
2
2
 
3
3
  from collections.abc import Collection
4
4
  from enum import Enum
5
+ from logging import Logger
5
6
  from typing import Literal, TypeVar
6
7
 
7
8
  import numpy as np
8
9
  from pydantic import BaseModel, ConfigDict, Field
9
10
 
10
- from ngio.utils import NgioValidationError, NgioValueError, ngio_logger
11
+ from ngio.utils import NgioValidationError, NgioValueError
12
+
13
+ logger = Logger(__name__)
11
14
 
12
15
  T = TypeVar("T")
13
16
 
@@ -99,20 +102,20 @@ class Axis(BaseModel):
99
102
  def implicit_type_cast(self, cast_type: AxisType) -> "Axis":
100
103
  unit = self.unit
101
104
  if self.axis_type != cast_type:
102
- ngio_logger.warning(
105
+ logger.warning(
103
106
  f"Axis {self.on_disk_name} has type {self.axis_type}. "
104
107
  f"Casting to {cast_type}."
105
108
  )
106
109
 
107
110
  if cast_type == AxisType.time and unit is None:
108
- ngio_logger.warning(
111
+ logger.warning(
109
112
  f"Time axis {self.on_disk_name} has unit {self.unit}. "
110
113
  f"Casting to {DefaultSpaceUnit}."
111
114
  )
112
115
  unit = DefaultTimeUnit
113
116
 
114
117
  if cast_type == AxisType.space and unit is None:
115
- ngio_logger.warning(
118
+ logger.warning(
116
119
  f"Space axis {self.on_disk_name} has unit {unit}. "
117
120
  f"Casting to {DefaultSpaceUnit}."
118
121
  )
ngio/tables/__init__.py CHANGED
@@ -1,20 +1,28 @@
1
1
  """Ngio Tables implementations."""
2
2
 
3
- from ngio.tables.backends import ImplementedTableBackends
3
+ from ngio.tables.backends import (
4
+ ImplementedTableBackends,
5
+ TableBackend,
6
+ TableBackendProtocol,
7
+ )
4
8
  from ngio.tables.tables_container import (
9
+ ConditionTable,
5
10
  FeatureTable,
6
11
  GenericRoiTable,
7
12
  MaskingRoiTable,
8
13
  RoiTable,
9
14
  Table,
10
15
  TablesContainer,
16
+ TableType,
11
17
  TypedTable,
12
18
  open_table,
19
+ open_table_as,
13
20
  open_tables_container,
14
21
  )
15
22
  from ngio.tables.v1._generic_table import GenericTable
16
23
 
17
24
  __all__ = [
25
+ "ConditionTable",
18
26
  "FeatureTable",
19
27
  "GenericRoiTable",
20
28
  "GenericTable",
@@ -22,8 +30,12 @@ __all__ = [
22
30
  "MaskingRoiTable",
23
31
  "RoiTable",
24
32
  "Table",
33
+ "TableBackend",
34
+ "TableBackendProtocol",
35
+ "TableType",
25
36
  "TablesContainer",
26
37
  "TypedTable",
27
38
  "open_table",
39
+ "open_table_as",
28
40
  "open_tables_container",
29
41
  ]
@@ -0,0 +1,269 @@
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
+ ImplementedTableBackends,
14
+ TableBackend,
15
+ TableBackendProtocol,
16
+ TabularData,
17
+ convert_to_anndata,
18
+ convert_to_pandas,
19
+ convert_to_polars,
20
+ normalize_table,
21
+ )
22
+ from ngio.utils import NgioValueError, ZarrGroupHandler
23
+
24
+
25
+ class AbstractBaseTable(ABC):
26
+ """Abstract base class for a table.
27
+
28
+ This is used to define common methods and properties
29
+ for all tables.
30
+
31
+ This class is not meant to be used directly.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ table_data: TabularData | None = None,
37
+ *,
38
+ meta: BackendMeta | None = None,
39
+ ) -> None:
40
+ """Initialize the table."""
41
+ if meta is None:
42
+ meta = BackendMeta()
43
+
44
+ self._meta = meta
45
+ if table_data is not None:
46
+ table_data = normalize_table(
47
+ table_data,
48
+ index_key=meta.index_key,
49
+ index_type=meta.index_type,
50
+ )
51
+ self._table_data = table_data
52
+ self._table_backend = None
53
+
54
+ def __repr__(self) -> str:
55
+ """Return a string representation of the table."""
56
+ return f"{self.__class__.__name__}"
57
+
58
+ @staticmethod
59
+ @abstractmethod
60
+ def table_type() -> str:
61
+ """Return the type of the table."""
62
+ ...
63
+
64
+ @staticmethod
65
+ @abstractmethod
66
+ def version() -> str:
67
+ """The generic table does not have a version.
68
+
69
+ Since does not follow a specific schema.
70
+ """
71
+ ...
72
+
73
+ @property
74
+ def backend_name(self) -> str | None:
75
+ """Return the name of the backend."""
76
+ if self._table_backend is None:
77
+ return None
78
+ return self._table_backend.backend_name()
79
+
80
+ @property
81
+ def meta(self) -> BackendMeta:
82
+ """Return the metadata of the table."""
83
+ return self._meta
84
+
85
+ @property
86
+ def index_key(self) -> str | None:
87
+ """Get the index key."""
88
+ return self._meta.index_key
89
+
90
+ @property
91
+ def index_type(self) -> Literal["int", "str"] | None:
92
+ """Get the index type."""
93
+ return self._meta.index_type
94
+
95
+ def load_as_anndata(self) -> AnnData:
96
+ """Load the table as an AnnData object."""
97
+ if self._table_backend is None:
98
+ raise NgioValueError("No backend set for the table.")
99
+ return self._table_backend.load_as_anndata()
100
+
101
+ def load_as_pandas_df(self) -> pd.DataFrame:
102
+ """Load the table as a pandas DataFrame."""
103
+ if self._table_backend is None:
104
+ raise NgioValueError("No backend set for the table.")
105
+ return self._table_backend.load_as_pandas_df()
106
+
107
+ def load_as_polars_lf(self) -> pl.LazyFrame:
108
+ """Load the table as a polars LazyFrame."""
109
+ if self._table_backend is None:
110
+ raise NgioValueError("No backend set for the table.")
111
+ return self._table_backend.load_as_polars_lf()
112
+
113
+ @property
114
+ def table_data(self) -> TabularData:
115
+ """Return the table."""
116
+ if self._table_data is not None:
117
+ return self._table_data
118
+
119
+ if self._table_backend is None:
120
+ raise NgioValueError(
121
+ "The table does not have a DataFrame in memory nor a backend."
122
+ )
123
+
124
+ self._table_data = self._table_backend.load()
125
+ return self._table_data
126
+
127
+ @property
128
+ def dataframe(self) -> pd.DataFrame:
129
+ """Return the table as a DataFrame."""
130
+ return convert_to_pandas(
131
+ self.table_data, index_key=self.index_key, index_type=self.index_type
132
+ )
133
+
134
+ @property
135
+ def lazy_frame(self) -> pl.LazyFrame:
136
+ """Return the table as a LazyFrame."""
137
+ return convert_to_polars(
138
+ self.table_data, index_key=self.index_key, index_type=self.index_type
139
+ )
140
+
141
+ @property
142
+ def anndata(self) -> AnnData:
143
+ """Return the table as an AnnData object."""
144
+ return convert_to_anndata(self.table_data, index_key=self.index_key)
145
+
146
+ @staticmethod
147
+ def _load_backend(
148
+ meta: BackendMeta,
149
+ handler: ZarrGroupHandler,
150
+ backend: TableBackend,
151
+ ) -> TableBackendProtocol:
152
+ """Create a new ROI table from a Zarr group handler."""
153
+ if isinstance(backend, str):
154
+ return ImplementedTableBackends().get_backend(
155
+ backend_name=backend,
156
+ group_handler=handler,
157
+ index_key=meta.index_key,
158
+ index_type=meta.index_type,
159
+ )
160
+ backend.set_group_handler(
161
+ group_handler=handler,
162
+ index_key=meta.index_key,
163
+ index_type=meta.index_type,
164
+ )
165
+ return backend
166
+
167
+ def set_table_data(
168
+ self,
169
+ table_data: TabularData | None = None,
170
+ refresh: bool = False,
171
+ ) -> None:
172
+ """Set the table.
173
+
174
+ If an object is passed, it will be used as the table.
175
+ If None is passed, the table will be loaded from the backend.
176
+
177
+ If refresh is True, the table will be reloaded from the backend.
178
+ If table is not None, this will be ignored.
179
+ """
180
+ if table_data is not None:
181
+ if not isinstance(table_data, TabularData):
182
+ raise NgioValueError(
183
+ "The table must be a pandas DataFrame, polars LazyFrame, "
184
+ " or AnnData object."
185
+ )
186
+
187
+ self._table_data = normalize_table(
188
+ table_data,
189
+ index_key=self.index_key,
190
+ index_type=self.index_type,
191
+ )
192
+ return None
193
+
194
+ if self._table_data is not None and not refresh:
195
+ return None
196
+
197
+ if self._table_backend is None:
198
+ raise NgioValueError(
199
+ "The table does not have a DataFrame in memory nor a backend."
200
+ )
201
+ self._table_data = self._table_backend.load()
202
+
203
+ def set_backend(
204
+ self,
205
+ handler: ZarrGroupHandler | None = None,
206
+ backend: TableBackend = "anndata",
207
+ ) -> None:
208
+ """Set the backend of the table."""
209
+ if handler is None:
210
+ if self._table_backend is None:
211
+ raise NgioValueError(
212
+ "No backend set for the table yet. "
213
+ "A ZarrGroupHandler must be provided."
214
+ )
215
+ handler = self._table_backend.group_handler
216
+
217
+ meta = self._meta
218
+ _backend = self._load_backend(
219
+ meta=meta,
220
+ handler=handler,
221
+ backend=backend,
222
+ )
223
+ self._table_backend = _backend
224
+
225
+ @classmethod
226
+ def _from_handler(
227
+ cls,
228
+ handler: ZarrGroupHandler,
229
+ meta_model: builtins.type[BackendMeta],
230
+ backend: TableBackend | None = None,
231
+ ) -> Self:
232
+ """Create a new ROI table from a Zarr group handler."""
233
+ meta = meta_model(**handler.load_attrs())
234
+ table = cls(meta=meta)
235
+ if backend is None:
236
+ backend = meta.backend
237
+ table.set_backend(handler=handler, backend=backend)
238
+ return table
239
+
240
+ @classmethod
241
+ @abstractmethod
242
+ def from_handler(
243
+ cls,
244
+ handler: ZarrGroupHandler,
245
+ backend: TableBackend | None = None,
246
+ ) -> Self:
247
+ """Create a new ROI table from a Zarr group handler."""
248
+ pass
249
+
250
+ @classmethod
251
+ def from_table_data(cls, table_data: TabularData, meta: BackendMeta) -> Self:
252
+ """Create a new ROI table from a Zarr group handler."""
253
+ return cls(
254
+ table_data=table_data,
255
+ meta=meta,
256
+ )
257
+
258
+ def consolidate(self) -> None:
259
+ """Write the current state of the table to the Zarr file."""
260
+ if self._table_backend is None:
261
+ raise NgioValueError(
262
+ "No backend set for the table. "
263
+ "Please add the table to a OME-Zarr Image before calling consolidate."
264
+ )
265
+
266
+ self._table_backend.write(
267
+ self.table_data,
268
+ metadata=self._meta.model_dump(exclude_none=True),
269
+ )