ngio 0.1.6__py3-none-any.whl → 0.2.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.
Files changed (84) hide show
  1. ngio/__init__.py +31 -5
  2. ngio/common/__init__.py +44 -0
  3. ngio/common/_array_pipe.py +160 -0
  4. ngio/common/_axes_transforms.py +63 -0
  5. ngio/common/_common_types.py +5 -0
  6. ngio/common/_dimensions.py +113 -0
  7. ngio/common/_pyramid.py +222 -0
  8. ngio/{core/roi.py → common/_roi.py} +22 -23
  9. ngio/common/_slicer.py +97 -0
  10. ngio/{pipes/_zoom_utils.py → common/_zoom.py} +2 -78
  11. ngio/hcs/__init__.py +60 -0
  12. ngio/images/__init__.py +23 -0
  13. ngio/images/abstract_image.py +240 -0
  14. ngio/images/create.py +251 -0
  15. ngio/images/image.py +383 -0
  16. ngio/images/label.py +96 -0
  17. ngio/images/omezarr_container.py +512 -0
  18. ngio/ome_zarr_meta/__init__.py +35 -0
  19. ngio/ome_zarr_meta/_generic_handlers.py +320 -0
  20. ngio/ome_zarr_meta/_meta_handlers.py +142 -0
  21. ngio/ome_zarr_meta/ngio_specs/__init__.py +63 -0
  22. ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
  23. ngio/ome_zarr_meta/ngio_specs/_channels.py +378 -0
  24. ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
  25. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +5 -0
  26. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +434 -0
  27. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +84 -0
  28. ngio/ome_zarr_meta/v04/__init__.py +11 -0
  29. ngio/ome_zarr_meta/v04/_meta_handlers.py +54 -0
  30. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +412 -0
  31. ngio/tables/__init__.py +21 -5
  32. ngio/tables/_validators.py +192 -0
  33. ngio/tables/backends/__init__.py +8 -0
  34. ngio/tables/backends/_abstract_backend.py +71 -0
  35. ngio/tables/backends/_anndata_utils.py +194 -0
  36. ngio/tables/backends/_anndata_v1.py +75 -0
  37. ngio/tables/backends/_json_v1.py +56 -0
  38. ngio/tables/backends/_table_backends.py +102 -0
  39. ngio/tables/tables_container.py +300 -0
  40. ngio/tables/v1/__init__.py +6 -5
  41. ngio/tables/v1/_feature_table.py +161 -0
  42. ngio/tables/v1/_generic_table.py +99 -182
  43. ngio/tables/v1/_masking_roi_table.py +175 -0
  44. ngio/tables/v1/_roi_table.py +226 -0
  45. ngio/utils/__init__.py +23 -10
  46. ngio/utils/_datasets.py +51 -0
  47. ngio/utils/_errors.py +10 -4
  48. ngio/utils/_zarr_utils.py +378 -0
  49. {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/METADATA +18 -39
  50. ngio-0.2.0a1.dist-info/RECORD +53 -0
  51. ngio/core/__init__.py +0 -7
  52. ngio/core/dimensions.py +0 -122
  53. ngio/core/image_handler.py +0 -228
  54. ngio/core/image_like_handler.py +0 -549
  55. ngio/core/label_handler.py +0 -410
  56. ngio/core/ngff_image.py +0 -387
  57. ngio/core/utils.py +0 -287
  58. ngio/io/__init__.py +0 -19
  59. ngio/io/_zarr.py +0 -88
  60. ngio/io/_zarr_array_utils.py +0 -0
  61. ngio/io/_zarr_group_utils.py +0 -60
  62. ngio/iterators/__init__.py +0 -1
  63. ngio/ngff_meta/__init__.py +0 -27
  64. ngio/ngff_meta/fractal_image_meta.py +0 -1267
  65. ngio/ngff_meta/meta_handler.py +0 -92
  66. ngio/ngff_meta/utils.py +0 -235
  67. ngio/ngff_meta/v04/__init__.py +0 -6
  68. ngio/ngff_meta/v04/specs.py +0 -158
  69. ngio/ngff_meta/v04/zarr_utils.py +0 -376
  70. ngio/pipes/__init__.py +0 -7
  71. ngio/pipes/_slicer_transforms.py +0 -176
  72. ngio/pipes/_transforms.py +0 -33
  73. ngio/pipes/data_pipe.py +0 -52
  74. ngio/tables/_ad_reader.py +0 -80
  75. ngio/tables/_utils.py +0 -301
  76. ngio/tables/tables_group.py +0 -252
  77. ngio/tables/v1/feature_tables.py +0 -182
  78. ngio/tables/v1/masking_roi_tables.py +0 -243
  79. ngio/tables/v1/roi_tables.py +0 -285
  80. ngio/utils/_common_types.py +0 -5
  81. ngio/utils/_pydantic_utils.py +0 -52
  82. ngio-0.1.6.dist-info/RECORD +0 -44
  83. {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/WHEEL +0 -0
  84. {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,378 @@
1
+ """Module to handle the channel information in the metadata.
2
+
3
+ Stores the same information as the Omero section of the ngff 0.4 metadata.
4
+ """
5
+
6
+ from collections.abc import Collection
7
+ from difflib import SequenceMatcher
8
+ from enum import Enum
9
+ from typing import Any, TypeVar
10
+
11
+ import numpy as np
12
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
13
+
14
+ from ngio.utils import NgioValidationError, NgioValueError
15
+
16
+ ################################################################################################
17
+ #
18
+ # Omero Section of the Metadata is used to store channel information and visualisation
19
+ # settings.
20
+ # This section is transitory and will be likely changed in the future.
21
+ #
22
+ #################################################################################################
23
+
24
+
25
+ class NgioColors(str, Enum):
26
+ """Default colors for the channels."""
27
+
28
+ dapi = "0000FF"
29
+ hoechst = "0000FF"
30
+ gfp = "00FF00"
31
+ cy3 = "FFFF00"
32
+ cy5 = "FF0000"
33
+ brightfield = "808080"
34
+ red = "FF0000"
35
+ yellow = "FFFF00"
36
+ magenta = "FF00FF"
37
+ cyan = "00FFFF"
38
+ gray = "808080"
39
+ green = "00FF00"
40
+
41
+ @staticmethod
42
+ def semi_random_pick(channel_name: str | None = None) -> "NgioColors":
43
+ """Try to fuzzy match the color to the channel name.
44
+
45
+ - If a channel name is given will try to match the channel name to the color.
46
+ - If name has the paatern 'channel_x' cyclic rotate over a list of colors
47
+ [cyan, magenta, yellow, green]
48
+ - If no channel name is given will return a random color.
49
+ """
50
+ available_colors = NgioColors._member_names_
51
+
52
+ if channel_name is None:
53
+ # Purely random color
54
+ color_str = available_colors[np.random.randint(0, len(available_colors))]
55
+ return NgioColors.__members__[color_str]
56
+
57
+ if channel_name.startswith("channel_"):
58
+ # Rotate over a list of colors
59
+ defaults_colors = [
60
+ NgioColors.cyan,
61
+ NgioColors.magenta,
62
+ NgioColors.yellow,
63
+ NgioColors.green,
64
+ ]
65
+
66
+ try:
67
+ index = int(channel_name.split("_")[-1]) % len(defaults_colors)
68
+ return defaults_colors[index]
69
+ except ValueError:
70
+ # If the name of the channel is something like
71
+ # channel_dapi this will fail an proceed to the
72
+ # standard fuzzy match
73
+ pass
74
+
75
+ similarity = {}
76
+ for color in available_colors:
77
+ # try to match the color to the channel name
78
+ similarity[color] = SequenceMatcher(None, channel_name, color).ratio()
79
+ # Get the color with the highest similarity
80
+ color_str = max(similarity, key=similarity.get) # type: ignore
81
+ return NgioColors.__members__[color_str]
82
+
83
+
84
+ def valid_hex_color(v: str) -> bool:
85
+ """Validate a hexadecimal color.
86
+
87
+ Check that `color` is made of exactly six elements which are letters
88
+ (a-f or A-F) or digits (0-9).
89
+
90
+ Implementation source:
91
+ https://github.com/fractal-analytics-platform/fractal-tasks-core/fractal_tasks_core/channels.py#L87
92
+ Original authors:
93
+ - Tommaso Comparin <tommaso.comparin@exact-lab.it>
94
+ """
95
+ if len(v) != 6:
96
+ return False
97
+ allowed_characters = "abcdefABCDEF0123456789"
98
+ for character in v:
99
+ if character not in allowed_characters:
100
+ return False
101
+ return True
102
+
103
+
104
+ class ChannelVisualisation(BaseModel):
105
+ """Channel visualisation model.
106
+
107
+ Contains the information about the visualisation of a channel.
108
+
109
+ Attributes:
110
+ color(str): The color of the channel in hexadecimal format or a color name.
111
+ min(int | float): The minimum value of the channel.
112
+ max(int | float): The maximum value of the channel.
113
+ start(int | float): The start value of the channel.
114
+ end(int | float): The end value of the channel.
115
+ active(bool): Whether the channel is active.
116
+ """
117
+
118
+ color: str | NgioColors | None = Field(default=None, validate_default=True)
119
+ min: int | float = 0
120
+ max: int | float = 65535
121
+ start: int | float = 0
122
+ end: int | float = 65535
123
+ active: bool = True
124
+ model_config = ConfigDict(extra="allow", frozen=True)
125
+
126
+ @field_validator("color", mode="after")
127
+ @classmethod
128
+ def validate_color(cls, value: str | NgioColors) -> str:
129
+ """Color validator.
130
+
131
+ There are three possible values to set a color:
132
+ - A hexadecimal string.
133
+ - A color name.
134
+ - A NgioColors element.
135
+ """
136
+ if value is None:
137
+ return NgioColors.semi_random_pick().value
138
+ if isinstance(value, str) and valid_hex_color(value):
139
+ return value
140
+ elif isinstance(value, NgioColors):
141
+ return value.value
142
+ elif isinstance(value, str):
143
+ value_lower = value.lower()
144
+ return NgioColors.semi_random_pick(value_lower).value
145
+ else:
146
+ raise NgioValueError(f"Invalid color {value}.")
147
+
148
+ @classmethod
149
+ def default_init(
150
+ cls,
151
+ color: str | NgioColors | None = None,
152
+ start: int | float | None = None,
153
+ end: int | float | None = None,
154
+ active: bool = True,
155
+ data_type: Any = np.uint16,
156
+ ) -> "ChannelVisualisation":
157
+ """Create a ChannelVisualisation object with the default unit.
158
+
159
+ Args:
160
+ color(str): The color of the channel in hexadecimal format or a color name.
161
+ start(int | float | None): The start value of the channel.
162
+ end(int | float | None): The end value of the channel.
163
+ data_type(Any): The data type of the channel.
164
+ active(bool): Whether the channel should be shown by default.
165
+ """
166
+ for func in [np.iinfo, np.finfo]:
167
+ try:
168
+ min_value = func(data_type).min
169
+ max_value = func(data_type).max
170
+ break
171
+ except ValueError:
172
+ continue
173
+ else:
174
+ raise NgioValueError(f"Invalid data type {data_type}.")
175
+
176
+ start = start if start is not None else min_value
177
+ end = end if end is not None else max_value
178
+ return cls(
179
+ color=color,
180
+ min=min_value,
181
+ max=max_value,
182
+ start=start,
183
+ end=end,
184
+ active=active,
185
+ )
186
+
187
+ @property
188
+ def valid_color(self) -> str:
189
+ """Return the valid color."""
190
+ if isinstance(self.color, NgioColors):
191
+ return self.color.value
192
+ elif isinstance(self.color, str):
193
+ return self.color
194
+ else:
195
+ raise NgioValueError(f"Invalid color {self.color}.")
196
+
197
+
198
+ def default_channel_name(index: int) -> str:
199
+ """Return the default channel name."""
200
+ return f"channel_{index}"
201
+
202
+
203
+ class Channel(BaseModel):
204
+ """Information about a channel in the image.
205
+
206
+ Attributes:
207
+ label(str): The label of the channel.
208
+ wavelength_id(str): The wavelength ID of the channel.
209
+ extra_fields(dict): To reduce the api surface, extra fields are stored in the
210
+ the channel attributes will be stored in the extra_fields attribute.
211
+ """
212
+
213
+ label: str
214
+ wavelength_id: str | None = None
215
+ channel_visualisation: ChannelVisualisation
216
+ model_config = ConfigDict(extra="allow", frozen=True)
217
+
218
+ @classmethod
219
+ def default_init(
220
+ cls,
221
+ label: str,
222
+ wavelength_id: str | None = None,
223
+ color: str | NgioColors | None = None,
224
+ start: int | float | None = None,
225
+ end: int | float | None = None,
226
+ active: bool = True,
227
+ data_type: Any = np.uint16,
228
+ ) -> "Channel":
229
+ """Create a Channel object with the default unit.
230
+
231
+ Args:
232
+ label(str): The label of the channel.
233
+ wavelength_id(str | None): The wavelength ID of the channel.
234
+ color(str): The color of the channel in hexadecimal format or a color name.
235
+ If None, the color will be picked based on the label.
236
+ start(int | float | None): The start value of the channel.
237
+ end(int | float | None): The end value of the channel.
238
+ active(bool): Whether the channel should be shown by default.
239
+ data_type(Any): The data type of the channel.
240
+ """
241
+ if color is None:
242
+ # If no color is provided, try to pick a color based on the label
243
+ # See the NgioColors.semi_random_pick method for more details.
244
+ color = label
245
+
246
+ channel_visualization = ChannelVisualisation.default_init(
247
+ color=color, start=start, end=end, active=active, data_type=data_type
248
+ )
249
+
250
+ if wavelength_id is None:
251
+ # TODO Evaluate if a better default value can be used
252
+ wavelength_id = label
253
+
254
+ return cls(
255
+ label=label,
256
+ wavelength_id=wavelength_id,
257
+ channel_visualisation=channel_visualization,
258
+ )
259
+
260
+
261
+ T = TypeVar("T")
262
+
263
+
264
+ def _check_elements(elements: Collection[T], expected_type: Any) -> Collection[T]:
265
+ """Check that the elements are of the same type."""
266
+ if len(elements) == 0:
267
+ raise NgioValidationError("At least one element must be provided.")
268
+
269
+ for element in elements:
270
+ if not isinstance(element, expected_type):
271
+ raise NgioValidationError(
272
+ f"All elements must be of the same type {expected_type}. Got {element}."
273
+ )
274
+
275
+ return elements
276
+
277
+
278
+ def _check_unique(elements: Collection[T]) -> Collection[T]:
279
+ """Check that the elements are unique."""
280
+ if len(set(elements)) != len(elements):
281
+ raise NgioValidationError("All elements must be unique.")
282
+ return elements
283
+
284
+
285
+ class ChannelsMeta(BaseModel):
286
+ """Information about the channels in the image.
287
+
288
+ This model is roughly equivalent to the Omero section of the ngff 0.4 metadata.
289
+
290
+ Attributes:
291
+ channels(list[Channel]): The list of channels in the image.
292
+ """
293
+
294
+ channels: list[Channel] = Field(default_factory=list)
295
+ model_config = ConfigDict(extra="allow", frozen=True)
296
+
297
+ @field_validator("channels", mode="after")
298
+ def validate_channels(cls, value: list[Channel]) -> list[Channel]:
299
+ """Check that the channels are unique."""
300
+ _check_unique([ch.label for ch in value])
301
+ return value
302
+
303
+ @classmethod
304
+ def default_init(
305
+ cls,
306
+ labels: Collection[str] | int,
307
+ wavelength_id: Collection[str] | None = None,
308
+ colors: Collection[str | NgioColors] | None = None,
309
+ start: Collection[int | float] | int | float | None = None,
310
+ end: Collection[int | float] | int | float | None = None,
311
+ active: Collection[bool] | None = None,
312
+ data_type: Any = np.uint16,
313
+ **omero_kwargs: dict,
314
+ ) -> "ChannelsMeta":
315
+ """Create a ChannelsMeta object with the default unit.
316
+
317
+ Args:
318
+ labels(Collection[str] | int): The list of channels names in the image.
319
+ If an integer is provided, the channels will be named "channel_i".
320
+ wavelength_id(Collection[str] | None): The wavelength ID of the channel.
321
+ If None, the wavelength ID will be the same as the channel name.
322
+ colors(Collection[str, NgioColors] | None): The list of colors for the
323
+ channels. If None, the colors will be random.
324
+ start(Collection[int | float] | int | float | None): The start value of the
325
+ channel. If None, the start value will be the minimum value of the
326
+ data type.
327
+ end(Collection[int | float] | int | float | None): The end value of the
328
+ channel. If None, the end value will be the maximum value of the
329
+ data type.
330
+ data_type(Any): The data type of the channel. Will be used to set the
331
+ min and max values of the channel.
332
+ active (Collection[bool] | None):active(bool): Whether the channel should
333
+ be shown by default.
334
+ omero_kwargs(dict): Extra fields to store in the omero attributes.
335
+ """
336
+ if isinstance(labels, int):
337
+ labels = [default_channel_name(i) for i in range(labels)]
338
+
339
+ labels = _check_elements(labels, str)
340
+ labels = _check_unique(labels)
341
+
342
+ _wavelength_id: Collection[str | None] = [None] * len(labels)
343
+ if isinstance(wavelength_id, Collection):
344
+ _wavelength_id = _check_elements(wavelength_id, str)
345
+ _wavelength_id = _check_unique(wavelength_id)
346
+
347
+ _colors: Collection[str | NgioColors] = ["random"] * len(labels)
348
+ if isinstance(colors, Collection):
349
+ _colors = _check_elements(colors, str | NgioColors)
350
+
351
+ _start: Collection[int | float | None] = [None] * len(labels)
352
+ if isinstance(start, Collection):
353
+ _start = _check_elements(start, (int, float))
354
+
355
+ _end: Collection[int | float | None] = [None] * len(labels)
356
+ if isinstance(end, Collection):
357
+ _end = _check_elements(end, (int, float))
358
+
359
+ _active: Collection[bool] = [True] * len(labels)
360
+ if isinstance(active, Collection):
361
+ _active = _check_elements(active, bool)
362
+
363
+ channels = []
364
+ for ch_name, w_id, color, s, e, a in zip(
365
+ labels, _wavelength_id, _colors, _start, _end, _active, strict=True
366
+ ):
367
+ channels.append(
368
+ Channel.default_init(
369
+ label=ch_name,
370
+ wavelength_id=w_id,
371
+ color=color,
372
+ start=s,
373
+ end=e,
374
+ active=a,
375
+ data_type=data_type,
376
+ )
377
+ )
378
+ return cls(channels=channels, **omero_kwargs)
@@ -0,0 +1,134 @@
1
+ """Fractal internal module for dataset metadata handling."""
2
+
3
+ from collections.abc import Collection
4
+
5
+ from ngio.ome_zarr_meta.ngio_specs._axes import (
6
+ AxesMapper,
7
+ AxesSetup,
8
+ Axis,
9
+ SpaceUnits,
10
+ TimeUnits,
11
+ )
12
+ from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
13
+ from ngio.utils import NgioValidationError
14
+
15
+
16
+ class Dataset:
17
+ """Model for a dataset in the multiscale.
18
+
19
+ To initialize the Dataset object, the path, the axes, scale, and translation list
20
+ can be provided with on_disk order.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ # args coming from ngff specs
27
+ path: str,
28
+ on_disk_axes: Collection[Axis],
29
+ on_disk_scale: Collection[float],
30
+ on_disk_translation: Collection[float] | None = None,
31
+ # user defined args
32
+ axes_setup: AxesSetup | None = None,
33
+ allow_non_canonical_axes: bool = False,
34
+ strict_canonical_order: bool = False,
35
+ ):
36
+ """Initialize the Dataset object.
37
+
38
+ Args:
39
+ path (str): The path of the dataset.
40
+ on_disk_axes (list[Axis]): The list of axes in the multiscale.
41
+ on_disk_scale (list[float]): The list of scale transformation.
42
+ The scale transformation must have the same length as the axes.
43
+ on_disk_translation (list[float] | None): The list of translation.
44
+ axes_setup (AxesSetup): The axes setup object
45
+ allow_non_canonical_axes (bool): Allow non-canonical axes.
46
+ strict_canonical_order (bool): Strict canonical order.
47
+ """
48
+ self._path = path
49
+ self._axes_mapper = AxesMapper(
50
+ on_disk_axes=on_disk_axes,
51
+ axes_setup=axes_setup,
52
+ allow_non_canonical_axes=allow_non_canonical_axes,
53
+ strict_canonical_order=strict_canonical_order,
54
+ )
55
+
56
+ if len(on_disk_scale) != len(on_disk_axes):
57
+ raise NgioValidationError(
58
+ "The length of the scale transformation must be the same as the axes."
59
+ )
60
+ self._on_disk_scale = list(on_disk_scale)
61
+
62
+ on_disk_translation = on_disk_translation or [0.0] * len(on_disk_axes)
63
+ if len(on_disk_translation) != len(on_disk_axes):
64
+ raise NgioValidationError(
65
+ "The length of the translation must be the same as the axes."
66
+ )
67
+ self._on_disk_translation = list(on_disk_translation)
68
+
69
+ def get_scale(self, axis_name: str) -> float:
70
+ """Return the scale for a given axis."""
71
+ idx = self._axes_mapper.get_index(axis_name)
72
+ if idx is None:
73
+ return 1.0
74
+ return self._on_disk_scale[idx]
75
+
76
+ def get_translation(self, axis_name: str) -> float:
77
+ """Return the translation for a given axis."""
78
+ idx = self._axes_mapper.get_index(axis_name)
79
+ if idx is None:
80
+ return 0.0
81
+ return self._on_disk_translation[idx]
82
+
83
+ @property
84
+ def path(self) -> str:
85
+ """Return the path of the dataset."""
86
+ return self._path
87
+
88
+ @property
89
+ def space_unit(self) -> SpaceUnits:
90
+ """Return the space unit for a given axis."""
91
+ x_axis = self._axes_mapper.get_axis("x")
92
+ y_axis = self._axes_mapper.get_axis("y")
93
+
94
+ if x_axis is None or y_axis is None:
95
+ raise NgioValidationError(
96
+ "The dataset must have x and y axes to determine the space unit."
97
+ )
98
+
99
+ if x_axis.unit == y_axis.unit:
100
+ if not isinstance(x_axis.unit, SpaceUnits):
101
+ raise NgioValidationError("The space unit must be of type SpaceUnits.")
102
+ return x_axis.unit
103
+ else:
104
+ raise NgioValidationError(
105
+ "Inconsistent space units. "
106
+ f"x={x_axis.unit} and y={y_axis.unit} should have the same unit."
107
+ )
108
+
109
+ @property
110
+ def time_unit(self) -> TimeUnits | None:
111
+ """Return the time unit for a given axis."""
112
+ t_axis = self._axes_mapper.get_axis("t")
113
+ if t_axis is None:
114
+ return None
115
+ if not isinstance(t_axis.unit, TimeUnits):
116
+ raise NgioValidationError("The time unit must be of type TimeUnits.")
117
+ return t_axis.unit
118
+
119
+ @property
120
+ def pixel_size(self) -> PixelSize:
121
+ """Return the pixel size for the dataset."""
122
+ return PixelSize(
123
+ x=self.get_scale("x"),
124
+ y=self.get_scale("y"),
125
+ z=self.get_scale("z"),
126
+ t=self.get_scale("t"),
127
+ space_unit=self.space_unit,
128
+ time_unit=self.time_unit,
129
+ )
130
+
131
+ @property
132
+ def axes_mapper(self) -> AxesMapper:
133
+ """Return the axes mapper object."""
134
+ return self._axes_mapper
@@ -0,0 +1,5 @@
1
+ """HCS (High Content Screening) specific metadata classes for NGIO."""
2
+
3
+
4
+ class NgioWellMeta:
5
+ images: list[str]