ngio 0.2.7__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.
@@ -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
+ TableBackendProtocol,
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: str | TableBackendProtocol | 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 (str | TableBackendProtocol | 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: str | TableBackendProtocol = "anndata_v1",
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,
@@ -9,7 +9,7 @@ from enum import Enum
9
9
  from typing import Any, TypeVar
10
10
 
11
11
  import numpy as np
12
- from pydantic import BaseModel, ConfigDict, Field, field_validator
12
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
13
13
 
14
14
  from ngio.utils import NgioValidationError, NgioValueError
15
15
 
@@ -124,7 +124,6 @@ class ChannelVisualisation(BaseModel):
124
124
  model_config = ConfigDict(extra="allow", frozen=True)
125
125
 
126
126
  @field_validator("color", mode="after")
127
- @classmethod
128
127
  def validate_color(cls, value: str | NgioColors) -> str:
129
128
  """Color validator.
130
129
 
@@ -145,6 +144,33 @@ class ChannelVisualisation(BaseModel):
145
144
  else:
146
145
  raise NgioValueError(f"Invalid color {value}.")
147
146
 
147
+ @model_validator(mode="before")
148
+ def check_start_end(cls, data):
149
+ """Check that the start and end values are valid.
150
+
151
+ If the start and end values are equal, set the end value to start + 1
152
+ """
153
+ start = data.get("start", None)
154
+ end = data.get("end", None)
155
+ if start is None or end is None:
156
+ return data
157
+ if abs(end - start) < 1e-6:
158
+ data["end"] = start + 1
159
+ return data
160
+
161
+ @model_validator(mode="after")
162
+ def check_model(self) -> "ChannelVisualisation":
163
+ """Check that the start and end values are within the min and max values."""
164
+ if self.start < self.min or self.start > self.max:
165
+ raise NgioValidationError(
166
+ f"Start value {self.start} is out of range [{self.min}, {self.max}]"
167
+ )
168
+ if self.end < self.min or self.end > self.max:
169
+ raise NgioValidationError(
170
+ f"End value {self.end} is out of range [{self.min}, {self.max}]"
171
+ )
172
+ return self
173
+
148
174
  @classmethod
149
175
  def default_init(
150
176
  cls,
ngio/tables/__init__.py CHANGED
@@ -1,20 +1,24 @@
1
1
  """Ngio Tables implementations."""
2
2
 
3
- from ngio.tables.backends import ImplementedTableBackends
3
+ from ngio.tables.backends import ImplementedTableBackends, TableBackendProtocol
4
4
  from ngio.tables.tables_container import (
5
+ ConditionTable,
5
6
  FeatureTable,
6
7
  GenericRoiTable,
7
8
  MaskingRoiTable,
8
9
  RoiTable,
9
10
  Table,
10
11
  TablesContainer,
12
+ TableType,
11
13
  TypedTable,
12
14
  open_table,
15
+ open_table_as,
13
16
  open_tables_container,
14
17
  )
15
18
  from ngio.tables.v1._generic_table import GenericTable
16
19
 
17
20
  __all__ = [
21
+ "ConditionTable",
18
22
  "FeatureTable",
19
23
  "GenericRoiTable",
20
24
  "GenericTable",
@@ -22,8 +26,11 @@ __all__ = [
22
26
  "MaskingRoiTable",
23
27
  "RoiTable",
24
28
  "Table",
29
+ "TableBackendProtocol",
30
+ "TableType",
25
31
  "TablesContainer",
26
32
  "TypedTable",
27
33
  "open_table",
34
+ "open_table_as",
28
35
  "open_tables_container",
29
36
  ]
@@ -0,0 +1,268 @@
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
+ TableBackendProtocol,
15
+ TabularData,
16
+ convert_to_anndata,
17
+ convert_to_pandas,
18
+ convert_to_polars,
19
+ normalize_table,
20
+ )
21
+ from ngio.utils import NgioValueError, ZarrGroupHandler
22
+
23
+
24
+ class AbstractBaseTable(ABC):
25
+ """Abstract base class for a table.
26
+
27
+ This is used to define common methods and properties
28
+ for all tables.
29
+
30
+ This class is not meant to be used directly.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ table_data: TabularData | None = None,
36
+ *,
37
+ meta: BackendMeta | None = None,
38
+ ) -> None:
39
+ """Initialize the table."""
40
+ if meta is None:
41
+ meta = BackendMeta()
42
+
43
+ self._meta = meta
44
+ if table_data is not None:
45
+ table_data = normalize_table(
46
+ table_data,
47
+ index_key=meta.index_key,
48
+ index_type=meta.index_type,
49
+ )
50
+ self._table_data = table_data
51
+ self._table_backend = None
52
+
53
+ def __repr__(self) -> str:
54
+ """Return a string representation of the table."""
55
+ return f"{self.__class__.__name__}"
56
+
57
+ @staticmethod
58
+ @abstractmethod
59
+ def table_type() -> str:
60
+ """Return the type of the table."""
61
+ ...
62
+
63
+ @staticmethod
64
+ @abstractmethod
65
+ def version() -> str:
66
+ """The generic table does not have a version.
67
+
68
+ Since does not follow a specific schema.
69
+ """
70
+ ...
71
+
72
+ @property
73
+ def backend_name(self) -> str | None:
74
+ """Return the name of the backend."""
75
+ if self._table_backend is None:
76
+ return None
77
+ return self._table_backend.backend_name()
78
+
79
+ @property
80
+ def meta(self) -> BackendMeta:
81
+ """Return the metadata of the table."""
82
+ return self._meta
83
+
84
+ @property
85
+ def index_key(self) -> str | None:
86
+ """Get the index key."""
87
+ return self._meta.index_key
88
+
89
+ @property
90
+ def index_type(self) -> Literal["int", "str"] | None:
91
+ """Get the index type."""
92
+ return self._meta.index_type
93
+
94
+ def load_as_anndata(self) -> AnnData:
95
+ """Load the table as an AnnData object."""
96
+ if self._table_backend is None:
97
+ raise NgioValueError("No backend set for the table.")
98
+ return self._table_backend.load_as_anndata()
99
+
100
+ def load_as_pandas_df(self) -> pd.DataFrame:
101
+ """Load the table as a pandas DataFrame."""
102
+ if self._table_backend is None:
103
+ raise NgioValueError("No backend set for the table.")
104
+ return self._table_backend.load_as_pandas_df()
105
+
106
+ def load_as_polars_lf(self) -> pl.LazyFrame:
107
+ """Load the table as a polars LazyFrame."""
108
+ if self._table_backend is None:
109
+ raise NgioValueError("No backend set for the table.")
110
+ return self._table_backend.load_as_polars_lf()
111
+
112
+ @property
113
+ def table_data(self) -> TabularData:
114
+ """Return the table."""
115
+ if self._table_data is not None:
116
+ return self._table_data
117
+
118
+ if self._table_backend is None:
119
+ raise NgioValueError(
120
+ "The table does not have a DataFrame in memory nor a backend."
121
+ )
122
+
123
+ self._table_data = self._table_backend.load()
124
+ return self._table_data
125
+
126
+ @property
127
+ def dataframe(self) -> pd.DataFrame:
128
+ """Return the table as a DataFrame."""
129
+ return convert_to_pandas(
130
+ self.table_data, index_key=self.index_key, index_type=self.index_type
131
+ )
132
+
133
+ @property
134
+ def lazy_frame(self) -> pl.LazyFrame:
135
+ """Return the table as a LazyFrame."""
136
+ return convert_to_polars(
137
+ self.table_data, index_key=self.index_key, index_type=self.index_type
138
+ )
139
+
140
+ @property
141
+ def anndata(self) -> AnnData:
142
+ """Return the table as an AnnData object."""
143
+ return convert_to_anndata(self.table_data, index_key=self.index_key)
144
+
145
+ @staticmethod
146
+ def _load_backend(
147
+ meta: BackendMeta,
148
+ handler: ZarrGroupHandler,
149
+ backend: str | TableBackendProtocol,
150
+ ) -> TableBackendProtocol:
151
+ """Create a new ROI table from a Zarr group handler."""
152
+ if isinstance(backend, str):
153
+ return ImplementedTableBackends().get_backend(
154
+ backend_name=backend,
155
+ group_handler=handler,
156
+ index_key=meta.index_key,
157
+ index_type=meta.index_type,
158
+ )
159
+ backend.set_group_handler(
160
+ group_handler=handler,
161
+ index_key=meta.index_key,
162
+ index_type=meta.index_type,
163
+ )
164
+ return backend
165
+
166
+ def set_table_data(
167
+ self,
168
+ table_data: TabularData | None = None,
169
+ refresh: bool = False,
170
+ ) -> None:
171
+ """Set the table.
172
+
173
+ If an object is passed, it will be used as the table.
174
+ If None is passed, the table will be loaded from the backend.
175
+
176
+ If refresh is True, the table will be reloaded from the backend.
177
+ If table is not None, this will be ignored.
178
+ """
179
+ if table_data is not None:
180
+ if not isinstance(table_data, TabularData):
181
+ raise NgioValueError(
182
+ "The table must be a pandas DataFrame, polars LazyFrame, "
183
+ " or AnnData object."
184
+ )
185
+
186
+ self._table_data = normalize_table(
187
+ table_data,
188
+ index_key=self.index_key,
189
+ index_type=self.index_type,
190
+ )
191
+ return None
192
+
193
+ if self._table_data is not None and not refresh:
194
+ return None
195
+
196
+ if self._table_backend is None:
197
+ raise NgioValueError(
198
+ "The table does not have a DataFrame in memory nor a backend."
199
+ )
200
+ self._table_data = self._table_backend.load()
201
+
202
+ def set_backend(
203
+ self,
204
+ handler: ZarrGroupHandler | None = None,
205
+ backend: str | TableBackendProtocol = "anndata_v1",
206
+ ) -> None:
207
+ """Set the backend of the table."""
208
+ if handler is None:
209
+ if self._table_backend is None:
210
+ raise NgioValueError(
211
+ "No backend set for the table yet. "
212
+ "A ZarrGroupHandler must be provided."
213
+ )
214
+ handler = self._table_backend.group_handler
215
+
216
+ meta = self._meta
217
+ _backend = self._load_backend(
218
+ meta=meta,
219
+ handler=handler,
220
+ backend=backend,
221
+ )
222
+ self._table_backend = _backend
223
+
224
+ @classmethod
225
+ def _from_handler(
226
+ cls,
227
+ handler: ZarrGroupHandler,
228
+ meta_model: builtins.type[BackendMeta],
229
+ backend: str | TableBackendProtocol | None = None,
230
+ ) -> Self:
231
+ """Create a new ROI table from a Zarr group handler."""
232
+ meta = meta_model(**handler.load_attrs())
233
+ table = cls(meta=meta)
234
+ if backend is None:
235
+ backend = meta.backend
236
+ table.set_backend(handler=handler, backend=backend)
237
+ return table
238
+
239
+ @classmethod
240
+ @abstractmethod
241
+ def from_handler(
242
+ cls,
243
+ handler: ZarrGroupHandler,
244
+ backend: str | TableBackendProtocol | None = None,
245
+ ) -> Self:
246
+ """Create a new ROI table from a Zarr group handler."""
247
+ pass
248
+
249
+ @classmethod
250
+ def from_table_data(cls, table_data: TabularData, meta: BackendMeta) -> Self:
251
+ """Create a new ROI table from a Zarr group handler."""
252
+ return cls(
253
+ table_data=table_data,
254
+ meta=meta,
255
+ )
256
+
257
+ def consolidate(self) -> None:
258
+ """Write the current state of the table to the Zarr file."""
259
+ if self._table_backend is None:
260
+ raise NgioValueError(
261
+ "No backend set for the table. "
262
+ "Please add the table to a OME-Zarr Image before calling consolidate."
263
+ )
264
+
265
+ self._table_backend.write(
266
+ self.table_data,
267
+ metadata=self._meta.model_dump(exclude_none=True),
268
+ )
@@ -1,34 +1,52 @@
1
1
  """Ngio Tables backend implementations."""
2
2
 
3
3
  from ngio.tables.backends._abstract_backend import AbstractTableBackend, BackendMeta
4
+ from ngio.tables.backends._anndata_v1 import AnnDataBackend
5
+ from ngio.tables.backends._csv_v1 import CsvTableBackend
6
+ from ngio.tables.backends._json_v1 import JsonTableBackend
7
+ from ngio.tables.backends._parquet_v1 import ParquetTableBackend
4
8
  from ngio.tables.backends._table_backends import (
5
9
  ImplementedTableBackends,
6
10
  TableBackendProtocol,
7
11
  )
8
12
  from ngio.tables.backends._utils import (
13
+ TabularData,
9
14
  convert_anndata_to_pandas,
10
15
  convert_anndata_to_polars,
11
16
  convert_pandas_to_anndata,
12
17
  convert_pandas_to_polars,
13
18
  convert_polars_to_anndata,
14
19
  convert_polars_to_pandas,
20
+ convert_to_anndata,
21
+ convert_to_pandas,
22
+ convert_to_polars,
15
23
  normalize_anndata,
16
24
  normalize_pandas_df,
17
25
  normalize_polars_lf,
26
+ normalize_table,
18
27
  )
19
28
 
20
29
  __all__ = [
21
30
  "AbstractTableBackend",
31
+ "AnnDataBackend",
22
32
  "BackendMeta",
33
+ "CsvTableBackend",
23
34
  "ImplementedTableBackends",
35
+ "JsonTableBackend",
36
+ "ParquetTableBackend",
24
37
  "TableBackendProtocol",
38
+ "TabularData",
25
39
  "convert_anndata_to_pandas",
26
40
  "convert_anndata_to_polars",
27
41
  "convert_pandas_to_anndata",
28
42
  "convert_pandas_to_polars",
29
43
  "convert_polars_to_anndata",
30
44
  "convert_polars_to_pandas",
45
+ "convert_to_anndata",
46
+ "convert_to_pandas",
47
+ "convert_to_polars",
31
48
  "normalize_anndata",
32
49
  "normalize_pandas_df",
33
50
  "normalize_polars_lf",
51
+ "normalize_table",
34
52
  ]