ngio 0.1.5__py3-none-any.whl → 0.2.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 (87) hide show
  1. ngio/__init__.py +33 -5
  2. ngio/common/__init__.py +54 -0
  3. ngio/common/_array_pipe.py +265 -0
  4. ngio/common/_axes_transforms.py +64 -0
  5. ngio/common/_common_types.py +5 -0
  6. ngio/common/_dimensions.py +120 -0
  7. ngio/common/_masking_roi.py +158 -0
  8. ngio/common/_pyramid.py +228 -0
  9. ngio/common/_roi.py +165 -0
  10. ngio/common/_slicer.py +96 -0
  11. ngio/{pipes/_zoom_utils.py → common/_zoom.py} +51 -83
  12. ngio/hcs/__init__.py +5 -0
  13. ngio/hcs/plate.py +448 -0
  14. ngio/images/__init__.py +23 -0
  15. ngio/images/abstract_image.py +349 -0
  16. ngio/images/create.py +270 -0
  17. ngio/images/image.py +453 -0
  18. ngio/images/label.py +285 -0
  19. ngio/images/masked_image.py +273 -0
  20. ngio/images/ome_zarr_container.py +738 -0
  21. ngio/ome_zarr_meta/__init__.py +47 -0
  22. ngio/ome_zarr_meta/_meta_handlers.py +791 -0
  23. ngio/ome_zarr_meta/ngio_specs/__init__.py +71 -0
  24. ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
  25. ngio/ome_zarr_meta/ngio_specs/_channels.py +389 -0
  26. ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
  27. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +377 -0
  28. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +489 -0
  29. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +116 -0
  30. ngio/ome_zarr_meta/v04/__init__.py +23 -0
  31. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +485 -0
  32. ngio/tables/__init__.py +24 -6
  33. ngio/tables/_validators.py +190 -0
  34. ngio/tables/backends/__init__.py +8 -0
  35. ngio/tables/backends/_abstract_backend.py +71 -0
  36. ngio/tables/backends/_anndata_utils.py +198 -0
  37. ngio/tables/backends/_anndata_v1.py +76 -0
  38. ngio/tables/backends/_json_v1.py +56 -0
  39. ngio/tables/backends/_table_backends.py +102 -0
  40. ngio/tables/tables_container.py +310 -0
  41. ngio/tables/v1/__init__.py +5 -5
  42. ngio/tables/v1/_feature_table.py +182 -0
  43. ngio/tables/v1/_generic_table.py +160 -179
  44. ngio/tables/v1/_roi_table.py +366 -0
  45. ngio/utils/__init__.py +26 -10
  46. ngio/utils/_datasets.py +53 -0
  47. ngio/utils/_errors.py +10 -4
  48. ngio/utils/_fractal_fsspec_store.py +13 -0
  49. ngio/utils/_logger.py +3 -1
  50. ngio/utils/_zarr_utils.py +401 -0
  51. {ngio-0.1.5.dist-info → ngio-0.2.0.dist-info}/METADATA +31 -43
  52. ngio-0.2.0.dist-info/RECORD +54 -0
  53. ngio/core/__init__.py +0 -7
  54. ngio/core/dimensions.py +0 -122
  55. ngio/core/image_handler.py +0 -228
  56. ngio/core/image_like_handler.py +0 -549
  57. ngio/core/label_handler.py +0 -410
  58. ngio/core/ngff_image.py +0 -387
  59. ngio/core/roi.py +0 -92
  60. ngio/core/utils.py +0 -287
  61. ngio/io/__init__.py +0 -19
  62. ngio/io/_zarr.py +0 -88
  63. ngio/io/_zarr_array_utils.py +0 -0
  64. ngio/io/_zarr_group_utils.py +0 -61
  65. ngio/iterators/__init__.py +0 -1
  66. ngio/ngff_meta/__init__.py +0 -27
  67. ngio/ngff_meta/fractal_image_meta.py +0 -1267
  68. ngio/ngff_meta/meta_handler.py +0 -92
  69. ngio/ngff_meta/utils.py +0 -235
  70. ngio/ngff_meta/v04/__init__.py +0 -6
  71. ngio/ngff_meta/v04/specs.py +0 -158
  72. ngio/ngff_meta/v04/zarr_utils.py +0 -376
  73. ngio/pipes/__init__.py +0 -7
  74. ngio/pipes/_slicer_transforms.py +0 -176
  75. ngio/pipes/_transforms.py +0 -33
  76. ngio/pipes/data_pipe.py +0 -52
  77. ngio/tables/_ad_reader.py +0 -80
  78. ngio/tables/_utils.py +0 -301
  79. ngio/tables/tables_group.py +0 -252
  80. ngio/tables/v1/feature_tables.py +0 -182
  81. ngio/tables/v1/masking_roi_tables.py +0 -243
  82. ngio/tables/v1/roi_tables.py +0 -285
  83. ngio/utils/_common_types.py +0 -5
  84. ngio/utils/_pydantic_utils.py +0 -52
  85. ngio-0.1.5.dist-info/RECORD +0 -44
  86. {ngio-0.1.5.dist-info → ngio-0.2.0.dist-info}/WHEEL +0 -0
  87. {ngio-0.1.5.dist-info → ngio-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,485 @@
1
+ """Utilities for OME-Zarr v04 specs.
2
+
3
+ This module provides a set of classes to internally handle the metadata
4
+ of the OME-Zarr v04 specification.
5
+
6
+ For Images and Labels implements the following functionalities:
7
+ - A function to find if a dict view of the metadata is a valid OME-Zarr v04 metadata.
8
+ - A function to convert a v04 image metadata to a ngio image metadata.
9
+ - A function to convert a ngio image metadata to a v04 image metadata.
10
+ """
11
+
12
+ from ome_zarr_models.common.multiscales import ValidTransform as ValidTransformV04
13
+ from ome_zarr_models.v04.axes import Axis as AxisV04
14
+ from ome_zarr_models.v04.coordinate_transformations import VectorScale as VectorScaleV04
15
+ from ome_zarr_models.v04.coordinate_transformations import (
16
+ VectorTranslation as VectorTranslationV04,
17
+ )
18
+ from ome_zarr_models.v04.hcs import HCSAttrs as HCSAttrsV04
19
+ from ome_zarr_models.v04.image import ImageAttrs as ImageAttrsV04
20
+ from ome_zarr_models.v04.image_label import ImageLabelAttrs as LabelAttrsV04
21
+ from ome_zarr_models.v04.multiscales import Dataset as DatasetV04
22
+ from ome_zarr_models.v04.multiscales import Multiscale as MultiscaleV04
23
+ from ome_zarr_models.v04.omero import Channel as ChannelV04
24
+ from ome_zarr_models.v04.omero import Omero as OmeroV04
25
+ from ome_zarr_models.v04.omero import Window as WindowV04
26
+ from ome_zarr_models.v04.well import WellAttrs as WellAttrsV04
27
+ from pydantic import ValidationError
28
+
29
+ from ngio.ome_zarr_meta.ngio_specs import (
30
+ AxesSetup,
31
+ Axis,
32
+ AxisType,
33
+ Channel,
34
+ ChannelsMeta,
35
+ ChannelVisualisation,
36
+ Dataset,
37
+ ImageLabelSource,
38
+ NgioImageMeta,
39
+ NgioLabelMeta,
40
+ NgioPlateMeta,
41
+ NgioWellMeta,
42
+ default_channel_name,
43
+ )
44
+ from ngio.ome_zarr_meta.ngio_specs._ngio_image import NgffVersion
45
+
46
+
47
+ def _is_v04_image_meta(metadata: dict) -> ImageAttrsV04 | ValidationError:
48
+ """Check if the metadata is a valid OME-Zarr v04 metadata.
49
+
50
+ Args:
51
+ metadata (dict): The metadata to check.
52
+
53
+ Returns:
54
+ bool: True if the metadata is a valid OME-Zarr v04 metadata, False otherwise.
55
+ """
56
+ try:
57
+ return ImageAttrsV04(**metadata)
58
+ except ValidationError as e:
59
+ return e
60
+
61
+
62
+ def _is_v04_label_meta(metadata: dict) -> LabelAttrsV04 | ValidationError:
63
+ """Check if the metadata is a valid OME-Zarr v04 metadata.
64
+
65
+ Args:
66
+ metadata (dict): The metadata to check.
67
+
68
+ Returns:
69
+ bool: True if the metadata is a valid OME-Zarr v04 metadata, False otherwise.
70
+ """
71
+ try:
72
+ return LabelAttrsV04(**metadata)
73
+ except ValidationError as e:
74
+ return e
75
+ raise RuntimeError("Unreachable code")
76
+
77
+
78
+ def _v04_omero_to_channels(v04_omero: OmeroV04 | None) -> ChannelsMeta | None:
79
+ if v04_omero is None:
80
+ return None
81
+
82
+ ngio_channels = []
83
+ for idx, v04_channel in enumerate(v04_omero.channels):
84
+ channel_extra = v04_channel.model_extra
85
+
86
+ if channel_extra is None:
87
+ channel_extra = {}
88
+
89
+ if "label" in channel_extra:
90
+ label = channel_extra.pop("label")
91
+ else:
92
+ label = default_channel_name(idx)
93
+
94
+ if "wavelength_id" in channel_extra:
95
+ wavelength_id = channel_extra.pop("wavelength_id")
96
+ else:
97
+ wavelength_id = label
98
+
99
+ if "active" in channel_extra:
100
+ active = channel_extra.pop("active")
101
+ else:
102
+ active = True
103
+
104
+ channel_visualisation = ChannelVisualisation(
105
+ color=v04_channel.color,
106
+ start=v04_channel.window.start,
107
+ end=v04_channel.window.end,
108
+ min=v04_channel.window.min,
109
+ max=v04_channel.window.max,
110
+ active=active,
111
+ **channel_extra,
112
+ )
113
+
114
+ ngio_channels.append(
115
+ Channel(
116
+ label=label,
117
+ wavelength_id=wavelength_id,
118
+ channel_visualisation=channel_visualisation,
119
+ )
120
+ )
121
+
122
+ v04_omero_extra = v04_omero.model_extra if v04_omero.model_extra is not None else {}
123
+ return ChannelsMeta(channels=ngio_channels, **v04_omero_extra)
124
+
125
+
126
+ def _compute_scale_translation(
127
+ v04_transforms: ValidTransformV04,
128
+ scale: list[float],
129
+ translation: list[float],
130
+ ) -> tuple[list[float], list[float]]:
131
+ for v04_transform in v04_transforms:
132
+ if isinstance(v04_transform, VectorScaleV04):
133
+ scale = [t1 * t2 for t1, t2 in zip(scale, v04_transform.scale, strict=True)]
134
+
135
+ elif isinstance(v04_transform, VectorTranslationV04):
136
+ translation = [
137
+ t1 + t2
138
+ for t1, t2 in zip(translation, v04_transform.translation, strict=True)
139
+ ]
140
+ else:
141
+ raise NotImplementedError(
142
+ f"Coordinate transformation {v04_transform} is not supported."
143
+ )
144
+ return scale, translation
145
+
146
+
147
+ def _v04_to_ngio_datasets(
148
+ v04_multiscale: MultiscaleV04,
149
+ axes_setup: AxesSetup,
150
+ allow_non_canonical_axes: bool = False,
151
+ strict_canonical_order: bool = True,
152
+ ) -> list[Dataset]:
153
+ """Convert a v04 multiscale to a list of ngio datasets."""
154
+ datasets = []
155
+
156
+ global_scale = [1.0] * len(v04_multiscale.axes)
157
+ global_translation = [0.0] * len(v04_multiscale.axes)
158
+
159
+ if v04_multiscale.coordinateTransformations is not None:
160
+ global_scale, global_translation = _compute_scale_translation(
161
+ v04_multiscale.coordinateTransformations, global_scale, global_translation
162
+ )
163
+
164
+ for v04_dataset in v04_multiscale.datasets:
165
+ axes = []
166
+ for v04_axis in v04_multiscale.axes:
167
+ unit = v04_axis.unit
168
+ if unit is not None and not isinstance(unit, str):
169
+ unit = str(unit)
170
+ axes.append(
171
+ Axis(
172
+ on_disk_name=v04_axis.name,
173
+ axis_type=AxisType(v04_axis.type),
174
+ # (for some reason the type is a generic JsonValue,
175
+ # but it should be a string or None)
176
+ unit=v04_axis.unit, # type: ignore
177
+ )
178
+ )
179
+
180
+ _on_disk_scale, _on_disk_translation = _compute_scale_translation(
181
+ v04_dataset.coordinateTransformations, global_scale, global_translation
182
+ )
183
+ datasets.append(
184
+ Dataset(
185
+ path=v04_dataset.path,
186
+ on_disk_axes=axes,
187
+ on_disk_scale=_on_disk_scale,
188
+ on_disk_translation=_on_disk_translation,
189
+ axes_setup=axes_setup,
190
+ allow_non_canonical_axes=allow_non_canonical_axes,
191
+ strict_canonical_order=strict_canonical_order,
192
+ )
193
+ )
194
+ return datasets
195
+
196
+
197
+ def v04_to_ngio_image_meta(
198
+ metadata: dict,
199
+ axes_setup: AxesSetup | None = None,
200
+ allow_non_canonical_axes: bool = False,
201
+ strict_canonical_order: bool = True,
202
+ ) -> tuple[bool, NgioImageMeta | ValidationError]:
203
+ """Convert a v04 image metadata to a ngio image metadata.
204
+
205
+ Args:
206
+ metadata (dict): The v04 image metadata.
207
+ axes_setup (AxesSetup, optional): The axes setup. This is
208
+ required to convert image with non-canonical axes names.
209
+ allow_non_canonical_axes (bool, optional): Allow non-canonical axes.
210
+ strict_canonical_order (bool, optional): Strict canonical order.
211
+
212
+ Returns:
213
+ NgioImageMeta: The ngio image metadata.
214
+ """
215
+ v04_image = _is_v04_image_meta(metadata)
216
+ if isinstance(v04_image, ValidationError):
217
+ return False, v04_image
218
+
219
+ if len(v04_image.multiscales) > 1:
220
+ raise NotImplementedError(
221
+ "Multiple multiscales in a single image are not supported in ngio."
222
+ )
223
+
224
+ v04_muliscale = v04_image.multiscales[0]
225
+
226
+ channels_meta = _v04_omero_to_channels(v04_image.omero)
227
+ axes_setup = axes_setup if axes_setup is not None else AxesSetup()
228
+ datasets = _v04_to_ngio_datasets(
229
+ v04_muliscale,
230
+ axes_setup=axes_setup,
231
+ allow_non_canonical_axes=allow_non_canonical_axes,
232
+ strict_canonical_order=strict_canonical_order,
233
+ )
234
+
235
+ name = v04_muliscale.name
236
+ if name is not None and not isinstance(name, str):
237
+ name = str(name)
238
+ return True, NgioImageMeta(
239
+ version="0.4",
240
+ name=name,
241
+ datasets=datasets,
242
+ channels=channels_meta,
243
+ )
244
+
245
+
246
+ def v04_to_ngio_label_meta(
247
+ metadata: dict,
248
+ axes_setup: AxesSetup | None = None,
249
+ allow_non_canonical_axes: bool = False,
250
+ strict_canonical_order: bool = True,
251
+ ) -> tuple[bool, NgioLabelMeta | ValidationError]:
252
+ """Convert a v04 image metadata to a ngio image metadata.
253
+
254
+ Args:
255
+ metadata (dict): The v04 image metadata.
256
+ axes_setup (AxesSetup, optional): The axes setup. This is
257
+ required to convert image with non-canonical axes names.
258
+ allow_non_canonical_axes (bool, optional): Allow non-canonical axes.
259
+ strict_canonical_order (bool, optional): Strict canonical order.
260
+
261
+ Returns:
262
+ NgioImageMeta: The ngio image metadata.
263
+ """
264
+ v04_label = _is_v04_label_meta(metadata)
265
+ if isinstance(v04_label, ValidationError):
266
+ return False, v04_label
267
+
268
+ if len(v04_label.multiscales) > 1:
269
+ raise NotImplementedError(
270
+ "Multiple multiscales in a single image are not supported in ngio."
271
+ )
272
+
273
+ v04_muliscale = v04_label.multiscales[0]
274
+
275
+ axes_setup = axes_setup if axes_setup is not None else AxesSetup()
276
+ datasets = _v04_to_ngio_datasets(
277
+ v04_muliscale,
278
+ axes_setup=axes_setup,
279
+ allow_non_canonical_axes=allow_non_canonical_axes,
280
+ strict_canonical_order=strict_canonical_order,
281
+ )
282
+
283
+ source = v04_label.image_label.source
284
+ if source is None:
285
+ image_label_source = None
286
+ else:
287
+ source = v04_label.image_label.source
288
+ if source is None:
289
+ image_label_source = None
290
+ else:
291
+ image_label_source = source.image
292
+ image_label_source = ImageLabelSource(
293
+ version=NgffVersion.v04,
294
+ source={"image": image_label_source},
295
+ )
296
+ name = v04_muliscale.name
297
+ if name is not None and not isinstance(name, str):
298
+ name = str(name)
299
+
300
+ return True, NgioLabelMeta(
301
+ version="0.4",
302
+ name=name,
303
+ datasets=datasets,
304
+ image_label=image_label_source,
305
+ )
306
+
307
+
308
+ def _ngio_to_v04_multiscale(name: str | None, datasets: list[Dataset]) -> MultiscaleV04:
309
+ """Convert a ngio multiscale to a v04 multiscale.
310
+
311
+ Args:
312
+ name (str | None): The name of the multiscale.
313
+ datasets (list[Dataset]): The ngio datasets.
314
+
315
+ Returns:
316
+ MultiscaleV04: The v04 multiscale.
317
+ """
318
+ ax_mapper = datasets[0].axes_mapper
319
+ v04_axes = []
320
+ for axis in ax_mapper.on_disk_axes:
321
+ v04_axes.append(
322
+ AxisV04(
323
+ name=axis.on_disk_name,
324
+ type=axis.axis_type.value if axis.axis_type is not None else None,
325
+ unit=axis.unit.value if axis.unit is not None else None,
326
+ )
327
+ )
328
+
329
+ v04_datasets = []
330
+ for dataset in datasets:
331
+ transform = [VectorScaleV04(type="scale", scale=list(dataset._on_disk_scale))]
332
+ if sum(dataset._on_disk_translation) > 0:
333
+ transform = (
334
+ VectorScaleV04(type="scale", scale=list(dataset._on_disk_scale)),
335
+ VectorTranslationV04(
336
+ type="translation", translation=list(dataset._on_disk_translation)
337
+ ),
338
+ )
339
+ else:
340
+ transform = (
341
+ VectorScaleV04(type="scale", scale=list(dataset._on_disk_scale)),
342
+ )
343
+
344
+ v04_datasets.append(
345
+ DatasetV04(path=dataset.path, coordinateTransformations=transform)
346
+ )
347
+ return MultiscaleV04(
348
+ axes=v04_axes, datasets=tuple(v04_datasets), version="0.4", name=name
349
+ )
350
+
351
+
352
+ def _ngio_to_v04_omero(channels: ChannelsMeta | None) -> OmeroV04 | None:
353
+ """Convert a ngio channels to a v04 omero."""
354
+ if channels is None:
355
+ return None
356
+
357
+ v04_channels = []
358
+ for channel in channels.channels:
359
+ _model_extra = {
360
+ "label": channel.label,
361
+ "wavelength_id": channel.wavelength_id,
362
+ "active": channel.channel_visualisation.active,
363
+ }
364
+ if channel.channel_visualisation.model_extra is not None:
365
+ _model_extra.update(channel.channel_visualisation.model_extra)
366
+
367
+ v04_channels.append(
368
+ ChannelV04(
369
+ color=channel.channel_visualisation.valid_color,
370
+ window=WindowV04(
371
+ start=channel.channel_visualisation.start,
372
+ end=channel.channel_visualisation.end,
373
+ min=channel.channel_visualisation.min,
374
+ max=channel.channel_visualisation.max,
375
+ ),
376
+ **_model_extra,
377
+ )
378
+ )
379
+
380
+ _model_extra = channels.model_extra if channels.model_extra is not None else {}
381
+ return OmeroV04(channels=v04_channels, **_model_extra)
382
+
383
+
384
+ def ngio_to_v04_image_meta(metadata: NgioImageMeta) -> dict:
385
+ """Convert a ngio image metadata to a v04 image metadata.
386
+
387
+ Args:
388
+ metadata (NgioImageMeta): The ngio image metadata.
389
+
390
+ Returns:
391
+ dict: The v04 image metadata.
392
+ """
393
+ v04_muliscale = _ngio_to_v04_multiscale(
394
+ name=metadata.name, datasets=metadata.datasets
395
+ )
396
+ v04_omero = _ngio_to_v04_omero(metadata._channels_meta)
397
+
398
+ v04_image = ImageAttrsV04(multiscales=[v04_muliscale], omero=v04_omero)
399
+ return v04_image.model_dump(exclude_none=True, by_alias=True)
400
+
401
+
402
+ def ngio_to_v04_label_meta(metadata: NgioLabelMeta) -> dict:
403
+ """Convert a ngio image metadata to a v04 image metadata.
404
+
405
+ Args:
406
+ metadata (NgioImageMeta): The ngio image metadata.
407
+
408
+ Returns:
409
+ dict: The v04 image metadata.
410
+ """
411
+ v04_muliscale = _ngio_to_v04_multiscale(
412
+ name=metadata.name, datasets=metadata.datasets
413
+ )
414
+ labels_meta = {
415
+ "multiscales": [v04_muliscale],
416
+ "image-label": metadata.image_label.model_dump(),
417
+ }
418
+ v04_label = LabelAttrsV04(**labels_meta)
419
+ return v04_label.model_dump(exclude_none=True, by_alias=True)
420
+
421
+
422
+ def v04_to_ngio_well_meta(
423
+ metadata: dict,
424
+ ) -> tuple[bool, NgioWellMeta | ValidationError]:
425
+ """Convert a v04 well metadata to a ngio well metadata.
426
+
427
+ Args:
428
+ metadata (dict): The v04 well metadata.
429
+
430
+ Returns:
431
+ result (bool): True if the conversion was successful, False otherwise.
432
+ ngio_well_meta (NgioWellMeta): The ngio well metadata.
433
+ """
434
+ try:
435
+ v04_well = WellAttrsV04(**metadata)
436
+ except ValidationError as e:
437
+ return False, e
438
+
439
+ return True, NgioWellMeta(**v04_well.model_dump())
440
+
441
+
442
+ def v04_to_ngio_plate_meta(
443
+ metadata: dict,
444
+ ) -> tuple[bool, NgioPlateMeta | ValidationError]:
445
+ """Convert a v04 plate metadata to a ngio plate metadata.
446
+
447
+ Args:
448
+ metadata (dict): The v04 plate metadata.
449
+
450
+ Returns:
451
+ result (bool): True if the conversion was successful, False otherwise.
452
+ ngio_plate_meta (NgioPlateMeta): The ngio plate metadata.
453
+ """
454
+ try:
455
+ v04_plate = HCSAttrsV04(**metadata)
456
+ except ValidationError as e:
457
+ return False, e
458
+
459
+ return True, NgioPlateMeta(**v04_plate.model_dump())
460
+
461
+
462
+ def ngio_to_v04_well_meta(metadata: NgioWellMeta) -> dict:
463
+ """Convert a ngio well metadata to a v04 well metadata.
464
+
465
+ Args:
466
+ metadata (NgioWellMeta): The ngio well metadata.
467
+
468
+ Returns:
469
+ dict: The v04 well metadata.
470
+ """
471
+ v04_well = WellAttrsV04(**metadata.model_dump())
472
+ return v04_well.model_dump(exclude_none=True, by_alias=True)
473
+
474
+
475
+ def ngio_to_v04_plate_meta(metadata: NgioPlateMeta) -> dict:
476
+ """Convert a ngio plate metadata to a v04 plate metadata.
477
+
478
+ Args:
479
+ metadata (NgioPlateMeta): The ngio plate metadata.
480
+
481
+ Returns:
482
+ dict: The v04 plate metadata.
483
+ """
484
+ v04_plate = HCSAttrsV04(**metadata.model_dump())
485
+ return v04_plate.model_dump(exclude_none=True, by_alias=True)
ngio/tables/__init__.py CHANGED
@@ -1,11 +1,29 @@
1
- """Module for handling tables in the Fractal format."""
1
+ """Ngio Tables implementations."""
2
2
 
3
- from ngio.tables.tables_group import (
3
+ from ngio.tables.backends import ImplementedTableBackends
4
+ from ngio.tables.tables_container import (
4
5
  FeatureTable,
5
- MaskingROITable,
6
- ROITable,
6
+ GenericRoiTable,
7
+ MaskingRoiTable,
8
+ RoiTable,
7
9
  Table,
8
- TableGroup,
10
+ TablesContainer,
11
+ TypedTable,
12
+ open_table,
13
+ open_tables_container,
9
14
  )
15
+ from ngio.tables.v1._generic_table import GenericTable
10
16
 
11
- __all__ = ["Table", "ROITable", "FeatureTable", "MaskingROITable", "TableGroup"]
17
+ __all__ = [
18
+ "FeatureTable",
19
+ "GenericRoiTable",
20
+ "GenericTable",
21
+ "ImplementedTableBackends",
22
+ "MaskingRoiTable",
23
+ "RoiTable",
24
+ "Table",
25
+ "TablesContainer",
26
+ "TypedTable",
27
+ "open_table",
28
+ "open_tables_container",
29
+ ]
@@ -0,0 +1,190 @@
1
+ from collections.abc import Iterable
2
+ from typing import Protocol
3
+
4
+ import pandas as pd
5
+ import pandas.api.types as ptypes
6
+
7
+ from ngio.utils import NgioTableValidationError, NgioValueError
8
+
9
+
10
+ class TableValidator(Protocol):
11
+ def __call__(self, table: pd.DataFrame) -> pd.DataFrame:
12
+ """Validate the table DataFrame.
13
+
14
+ A Validator is just a simple callable that takes a
15
+ DataFrame and returns a DataFrame.
16
+
17
+ If the DataFrame is valid, the same DataFrame is returned.
18
+ If the DataFrame is invalid, the Validator can either modify the DataFrame
19
+ to make it valid or raise a NgioTableValidationError.
20
+
21
+ Args:
22
+ table (pd.DataFrame): The DataFrame to validate.
23
+
24
+ Returns:
25
+ pd.DataFrame: The validated DataFrame.
26
+
27
+ """
28
+ ...
29
+
30
+
31
+ def validate_table(
32
+ table_df: pd.DataFrame,
33
+ validators: Iterable[TableValidator] | None = None,
34
+ ) -> pd.DataFrame:
35
+ """Validate the table DataFrame.
36
+
37
+ Args:
38
+ table_df (pd.DataFrame): The DataFrame to validate.
39
+ validators (Collection[Validator] | None): A collection of functions
40
+ used to validate the table. Default is None.
41
+
42
+ Returns:
43
+ pd.DataFrame: The validated DataFrame.
44
+ """
45
+ validators = validators or []
46
+
47
+ # Apply all provided validators
48
+ for validator in validators:
49
+ table_df = validator(table_df)
50
+
51
+ return table_df
52
+
53
+
54
+ ####################################################################################################
55
+ #
56
+ # Common table validators
57
+ #
58
+ ####################################################################################################
59
+ def validate_index_key(
60
+ dataframe: pd.DataFrame, index_key: str | None, overwrite: bool = False
61
+ ) -> pd.DataFrame:
62
+ """Correctly set the index of the DataFrame.
63
+
64
+ This function checks if the index_key is present in the DataFrame.
65
+ If not it tries to set sensible defaults.
66
+
67
+ In order:
68
+ - If index_key is None, nothing can be done.
69
+ - If index_key is already the index of the DataFrame, nothing is done.
70
+ - If index_key is in the columns, we set the index to that column.
71
+ - If current index is None, we set the index to the index_key.
72
+ - If current index is not None and overwrite is True,
73
+ we set the index to the index_key.
74
+
75
+ """
76
+ if index_key is None:
77
+ # Nothing to do
78
+ return dataframe
79
+
80
+ if dataframe.index.name == index_key:
81
+ # Index is already set to index_key correctly
82
+ return dataframe
83
+
84
+ if index_key in dataframe.columns:
85
+ dataframe = dataframe.set_index(index_key)
86
+ return dataframe
87
+
88
+ if dataframe.index.name is None:
89
+ dataframe.index.name = index_key
90
+ return dataframe
91
+
92
+ elif overwrite:
93
+ dataframe.index.name = index_key
94
+ return dataframe
95
+ else:
96
+ raise NgioTableValidationError(
97
+ f"Index key {index_key} not found in DataFrame. "
98
+ f"Current index is {dataframe.index.name}. If you want to overwrite the "
99
+ "index set overwrite=True."
100
+ )
101
+
102
+
103
+ def validate_index_dtype(dataframe: pd.DataFrame, index_type: str) -> pd.DataFrame:
104
+ """Check if the index of the DataFrame has the correct dtype."""
105
+ match index_type:
106
+ case "str":
107
+ if ptypes.is_integer_dtype(dataframe.index):
108
+ # Convert the int index to string is generally safe
109
+ dataframe = dataframe.set_index(dataframe.index.astype(str))
110
+
111
+ if not ptypes.is_string_dtype(dataframe.index):
112
+ raise NgioTableValidationError(
113
+ f"Table index must be of string type, got {dataframe.index.dtype}"
114
+ )
115
+
116
+ case "int":
117
+ if ptypes.is_string_dtype(dataframe.index):
118
+ # Try to convert the string index to int
119
+ try:
120
+ dataframe = dataframe.set_index(dataframe.index.astype(int))
121
+ except ValueError as e:
122
+ if "invalid literal for int() with base 10" in str(e):
123
+ raise NgioTableValidationError(
124
+ "Table index must be of integer type, got str."
125
+ f" We tried implicit conversion and failed: {e}"
126
+ ) from None
127
+ else:
128
+ raise e from e
129
+
130
+ if not ptypes.is_integer_dtype(dataframe.index):
131
+ raise NgioTableValidationError(
132
+ f"Table index must be of integer type, got {dataframe.index.dtype}"
133
+ )
134
+ case _:
135
+ raise NgioValueError(f"index_type {index_type} not recognized")
136
+
137
+ return dataframe
138
+
139
+
140
+ def validate_columns(
141
+ table_df: pd.DataFrame,
142
+ required_columns: list[str],
143
+ optional_columns: list[str] | None = None,
144
+ ) -> pd.DataFrame:
145
+ """Validate the columns headers of the table.
146
+
147
+ If a required column is missing, a TableValidationError is raised.
148
+ If a list of optional columns is provided, only required and optional columns are
149
+ allowed in the table.
150
+
151
+ Args:
152
+ table_df (pd.DataFrame): The DataFrame to validate.
153
+ required_columns (list[str]): A list of required columns.
154
+ optional_columns (list[str] | None): A list of optional columns.
155
+ Default is None.
156
+
157
+ Returns:
158
+ pd.DataFrame: The validated DataFrame.
159
+ """
160
+ table_header = table_df.columns
161
+ for column in required_columns:
162
+ if column not in table_header:
163
+ raise NgioTableValidationError(
164
+ f"Could not find required column: {column} in the table"
165
+ )
166
+
167
+ if optional_columns is None:
168
+ return table_df
169
+
170
+ possible_columns = [*required_columns, *optional_columns]
171
+ for column in table_header:
172
+ if column not in possible_columns:
173
+ raise NgioTableValidationError(
174
+ f"Could not find column: {column} in the list of possible columns. ",
175
+ f"Possible columns are: {possible_columns}",
176
+ )
177
+
178
+ return table_df
179
+
180
+
181
+ def validate_unique_index(table_df: pd.DataFrame) -> pd.DataFrame:
182
+ """Validate that the index of the table is unique."""
183
+ if table_df.index.is_unique:
184
+ return table_df
185
+
186
+ # Find the duplicates
187
+ duplicates = table_df.index[table_df.index.duplicated()].tolist()
188
+ raise NgioTableValidationError(
189
+ f"Index of the table contains duplicates values. Duplicate: {duplicates}"
190
+ )