ngio 0.2.1__py3-none-any.whl → 0.2.3__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 (38) hide show
  1. ngio/__init__.py +20 -2
  2. ngio/common/_pyramid.py +5 -1
  3. ngio/common/_roi.py +2 -2
  4. ngio/hcs/__init__.py +16 -2
  5. ngio/hcs/plate.py +496 -18
  6. ngio/images/abstract_image.py +11 -0
  7. ngio/images/create.py +25 -36
  8. ngio/images/image.py +80 -6
  9. ngio/images/label.py +38 -9
  10. ngio/images/ome_zarr_container.py +70 -33
  11. ngio/ome_zarr_meta/__init__.py +5 -3
  12. ngio/ome_zarr_meta/ngio_specs/__init__.py +10 -2
  13. ngio/ome_zarr_meta/ngio_specs/_axes.py +90 -65
  14. ngio/ome_zarr_meta/ngio_specs/_dataset.py +46 -8
  15. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +242 -70
  16. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +49 -11
  17. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +28 -11
  18. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  19. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +2 -2
  20. ngio/tables/_validators.py +1 -83
  21. ngio/tables/backends/__init__.py +27 -1
  22. ngio/tables/backends/_abstract_backend.py +207 -22
  23. ngio/tables/backends/_anndata_utils.py +3 -109
  24. ngio/tables/backends/_anndata_v1.py +43 -46
  25. ngio/tables/backends/_csv_v1.py +162 -0
  26. ngio/tables/backends/_json_v1.py +54 -18
  27. ngio/tables/backends/_table_backends.py +98 -18
  28. ngio/tables/backends/_utils.py +458 -0
  29. ngio/tables/tables_container.py +3 -1
  30. ngio/tables/v1/_feature_table.py +20 -11
  31. ngio/tables/v1/_generic_table.py +20 -15
  32. ngio/tables/v1/_roi_table.py +7 -9
  33. ngio/utils/_zarr_utils.py +46 -32
  34. {ngio-0.2.1.dist-info → ngio-0.2.3.dist-info}/METADATA +3 -1
  35. ngio-0.2.3.dist-info/RECORD +57 -0
  36. ngio-0.2.1.dist-info/RECORD +0 -54
  37. {ngio-0.2.1.dist-info → ngio-0.2.3.dist-info}/WHEEL +0 -0
  38. {ngio-0.2.1.dist-info → ngio-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -2,9 +2,8 @@ from collections.abc import Iterable
2
2
  from typing import Protocol
3
3
 
4
4
  import pandas as pd
5
- import pandas.api.types as ptypes
6
5
 
7
- from ngio.utils import NgioTableValidationError, NgioValueError
6
+ from ngio.utils import NgioTableValidationError
8
7
 
9
8
 
10
9
  class TableValidator(Protocol):
@@ -56,87 +55,6 @@ def validate_table(
56
55
  # Common table validators
57
56
  #
58
57
  ####################################################################################################
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
58
  def validate_columns(
141
59
  table_df: pd.DataFrame,
142
60
  required_columns: list[str],
@@ -1,8 +1,34 @@
1
1
  """Ngio Tables backend implementations."""
2
2
 
3
+ from ngio.tables.backends._abstract_backend import AbstractTableBackend, BackendMeta
3
4
  from ngio.tables.backends._table_backends import (
4
5
  ImplementedTableBackends,
5
6
  TableBackendProtocol,
6
7
  )
8
+ from ngio.tables.backends._utils import (
9
+ convert_anndata_to_pandas,
10
+ convert_anndata_to_polars,
11
+ convert_pandas_to_anndata,
12
+ convert_pandas_to_polars,
13
+ convert_polars_to_anndata,
14
+ convert_polars_to_pandas,
15
+ normalize_anndata,
16
+ normalize_pandas_df,
17
+ normalize_polars_lf,
18
+ )
7
19
 
8
- __all__ = ["ImplementedTableBackends", "TableBackendProtocol"]
20
+ __all__ = [
21
+ "AbstractTableBackend",
22
+ "BackendMeta",
23
+ "ImplementedTableBackends",
24
+ "TableBackendProtocol",
25
+ "convert_anndata_to_pandas",
26
+ "convert_anndata_to_polars",
27
+ "convert_pandas_to_anndata",
28
+ "convert_pandas_to_polars",
29
+ "convert_polars_to_anndata",
30
+ "convert_polars_to_pandas",
31
+ "normalize_anndata",
32
+ "normalize_pandas_df",
33
+ "normalize_polars_lf",
34
+ ]
@@ -1,11 +1,29 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import Collection
3
2
  from typing import Literal
4
3
 
5
4
  from anndata import AnnData
6
5
  from pandas import DataFrame
6
+ from polars import DataFrame as PolarsDataFrame
7
+ from polars import LazyFrame
8
+ from pydantic import BaseModel
7
9
 
8
- from ngio.utils import ZarrGroupHandler
10
+ from ngio.tables.backends._utils import (
11
+ convert_anndata_to_pandas,
12
+ convert_anndata_to_polars,
13
+ convert_pandas_to_anndata,
14
+ convert_pandas_to_polars,
15
+ convert_polars_to_anndata,
16
+ convert_polars_to_pandas,
17
+ )
18
+ from ngio.utils import NgioValueError, ZarrGroupHandler
19
+
20
+
21
+ class BackendMeta(BaseModel):
22
+ """Metadata for the backend."""
23
+
24
+ backend: str | None = None
25
+ index_key: str | None = None
26
+ index_type: Literal["int", "str"] | None = None
9
27
 
10
28
 
11
29
  class AbstractTableBackend(ABC):
@@ -15,10 +33,12 @@ class AbstractTableBackend(ABC):
15
33
  self,
16
34
  group_handler: ZarrGroupHandler,
17
35
  index_key: str | None = None,
18
- index_type: Literal["int", "str"] = "int",
36
+ index_type: Literal["int", "str"] | None = None,
19
37
  ):
20
38
  """Initialize the handler.
21
39
 
40
+ This is a base class for the table backends protocol.
41
+
22
42
  Args:
23
43
  group_handler (ZarrGroupHandler): An object to handle the Zarr group
24
44
  containing the table data.
@@ -32,40 +52,205 @@ class AbstractTableBackend(ABC):
32
52
  @staticmethod
33
53
  @abstractmethod
34
54
  def backend_name() -> str:
35
- """The name of the backend."""
55
+ """Return the name of the backend.
56
+
57
+ As a convention we set name as:
58
+ {backend_name}_v{version}
59
+
60
+ Where the version is a integer.
61
+ """
36
62
  raise NotImplementedError
37
63
 
38
64
  @staticmethod
39
65
  @abstractmethod
40
66
  def implements_anndata() -> bool:
41
- """Whether the handler implements the anndata protocol."""
67
+ """Check if the backend implements the anndata protocol.
68
+
69
+ If this is True, the backend should implement the
70
+ `load_as_anndata` and `write_from_anndata` methods.
71
+
72
+ If this is False, these methods should raise a
73
+ `NotImplementedError`.
74
+ """
42
75
  raise NotImplementedError
43
76
 
44
77
  @staticmethod
45
78
  @abstractmethod
46
- def implements_dataframe() -> bool:
47
- """Whether the handler implements the dataframe protocol."""
79
+ def implements_pandas() -> bool:
80
+ """Check if the backend implements the pandas protocol.
81
+
82
+ If this is True, the backend should implement the
83
+ `load_as_dataframe` and `write_from_dataframe` methods.
84
+
85
+ If this is False, these methods should raise a
86
+ `NotImplementedError`.
87
+ """
48
88
  raise NotImplementedError
49
89
 
90
+ @staticmethod
50
91
  @abstractmethod
51
- def load_columns(self) -> list[str]:
52
- """List all labels in the group."""
53
- raise NotImplementedError
92
+ def implements_polars() -> bool:
93
+ """Check if the backend implements the polars protocol.
54
94
 
55
- def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData:
56
- """Load the metadata in the store."""
57
- raise NotImplementedError
95
+ If this is True, the backend should implement the
96
+ `load_as_polars` and `write_from_polars` methods.
58
97
 
59
- def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
60
- """List all labels in the group."""
98
+ If this is False, these methods should raise a
99
+ `NotImplementedError`.
100
+ """
61
101
  raise NotImplementedError
62
102
 
63
- def write_from_dataframe(
64
- self, table: DataFrame, metadata: dict | None = None
103
+ @property
104
+ def group_handler(self) -> ZarrGroupHandler:
105
+ """Get the group handler."""
106
+ return self._group_handler
107
+
108
+ @property
109
+ def index_key(self) -> str | None:
110
+ """Get the index key."""
111
+ return self._index_key
112
+
113
+ @property
114
+ def index_type(self) -> Literal["int", "str"] | None:
115
+ """Get the index type."""
116
+ if self._index_type is None:
117
+ return None
118
+
119
+ if self._index_type not in ["int", "str"]:
120
+ raise NgioValueError(
121
+ f"Invalid index type {self._index_type}. Must be 'int' or 'str'."
122
+ )
123
+ return self._index_type # type: ignore[return-value]
124
+
125
+ def load_as_anndata(self) -> AnnData:
126
+ """Load the table as an AnnData object.
127
+
128
+ Since the AnnData object is more complex than a DataFrame,
129
+ selecting columns is not implemented, because it is not
130
+ straightforward to do so for an arbitrary AnnData object.
131
+ """
132
+ if self.implements_pandas():
133
+ return convert_pandas_to_anndata(
134
+ self.load_as_pandas_df(),
135
+ index_key=self.index_key,
136
+ )
137
+ elif self.implements_polars():
138
+ return convert_polars_to_anndata(
139
+ self.load_as_polars_lf(),
140
+ index_key=self.index_key,
141
+ )
142
+ else:
143
+ raise NgioValueError(
144
+ "Backend does not implement any of the protocols. "
145
+ "A backend should implement at least one of the "
146
+ "following protocols: anndata, pandas, polars."
147
+ )
148
+
149
+ def load_as_pandas_df(self) -> DataFrame:
150
+ """Load the table as a pandas DataFrame.
151
+
152
+ If columns are provided, the table should be filtered
153
+ """
154
+ if self.implements_anndata():
155
+ return convert_anndata_to_pandas(
156
+ self.load_as_anndata(),
157
+ index_key=self.index_key,
158
+ index_type=self.index_type,
159
+ )
160
+ elif self.implements_polars():
161
+ return convert_polars_to_pandas(
162
+ self.load_as_polars_lf(),
163
+ index_key=self.index_key,
164
+ index_type=self.index_type,
165
+ )
166
+ else:
167
+ raise NgioValueError(
168
+ "Backend does not implement any of the protocols. "
169
+ "A backend should implement at least one of the "
170
+ "following protocols: anndata, pandas, polars."
171
+ )
172
+
173
+ def load_as_polars_lf(self) -> LazyFrame:
174
+ """Load the table as a polars LazyFrame.
175
+
176
+ If columns are provided, the table should be filtered
177
+ """
178
+ if self.implements_anndata():
179
+ return convert_anndata_to_polars(
180
+ self.load_as_anndata(),
181
+ index_key=self.index_key,
182
+ index_type=self.index_type,
183
+ ).lazy()
184
+ elif self.implements_pandas():
185
+ return convert_pandas_to_polars(
186
+ self.load_as_pandas_df(),
187
+ index_key=self.index_key,
188
+ index_type=self.index_type,
189
+ ).lazy()
190
+ else:
191
+ raise NgioValueError(
192
+ "Backend does not implement any of the protocols. "
193
+ "A backend should implement at least one of the "
194
+ "following protocols: anndata, pandas, polars."
195
+ )
196
+
197
+ def write_from_pandas(self, table: DataFrame) -> None:
198
+ """Serialize the table from a pandas DataFrame."""
199
+ raise NotImplementedError(
200
+ f"Backend {self.backend_name()} does not support "
201
+ "serialization of DataFrame objects."
202
+ )
203
+
204
+ def write_from_anndata(self, table: AnnData) -> None:
205
+ """Serialize the table from an AnnData object."""
206
+ raise NotImplementedError(
207
+ f"Backend {self.backend_name()} does not support "
208
+ "serialization of AnnData objects."
209
+ )
210
+
211
+ def write_from_polars(self, table: PolarsDataFrame | LazyFrame) -> None:
212
+ """Serialize the table from a polars DataFrame or LazyFrame."""
213
+ raise NotImplementedError(
214
+ f"Backend {self.backend_name()} does not support "
215
+ "serialization of Polars objects."
216
+ )
217
+
218
+ def write_metadata(self, metadata: dict | None = None) -> None:
219
+ """Write the metadata to the store."""
220
+ if metadata is None:
221
+ metadata = {}
222
+
223
+ backend_metadata = BackendMeta(
224
+ backend=self.backend_name(),
225
+ index_key=self.index_key,
226
+ index_type=self.index_type,
227
+ ).model_dump(exclude_none=True)
228
+ metadata.update(backend_metadata)
229
+ self._group_handler.write_attrs(metadata)
230
+
231
+ def write(
232
+ self,
233
+ table: DataFrame | AnnData | PolarsDataFrame | LazyFrame,
234
+ metadata: dict | None = None,
235
+ mode: Literal["pandas", "anndata", "polars"] | None = None,
65
236
  ) -> None:
66
- """Consolidate the metadata in the store."""
67
- raise NotImplementedError
237
+ """Serialize the table to the store, and write the metadata.
68
238
 
69
- def write_from_anndata(self, table: AnnData, metadata: dict | None = None) -> None:
70
- """Consolidate the metadata in the store."""
71
- raise NotImplementedError
239
+ This is a generic write method.
240
+ Based on the explicit mode or the type of the table,
241
+ it will call the appropriate write method.
242
+ """
243
+ if mode == "pandas" or isinstance(table, DataFrame):
244
+ self.write_from_pandas(table) # type: ignore[arg-type]
245
+ elif mode == "anndata" or isinstance(table, AnnData):
246
+ self.write_from_anndata(table) # type: ignore[arg-type]
247
+ elif mode == "polars" or isinstance(table, PolarsDataFrame | LazyFrame):
248
+ self.write_from_polars(table)
249
+ else:
250
+ raise NgioValueError(
251
+ f"Unsupported table type {type(table)}. "
252
+ "Please specify the mode explicitly. "
253
+ "Supported serialization modes are: "
254
+ "'pandas', 'anndata', 'polars'."
255
+ )
256
+ self.write_metadata(metadata)
@@ -1,11 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Literal
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
- import anndata as ad
6
- import numpy as np
7
- import pandas as pd
8
- import pandas.api.types as ptypes
9
5
  import zarr
10
6
  from anndata import AnnData
11
7
  from anndata._io.specs import read_elem
@@ -14,9 +10,7 @@ from anndata._io.zarr import read_dataframe
14
10
  from anndata.compat import _clean_uns
15
11
  from anndata.experimental import read_dispatched
16
12
 
17
- from ngio.tables._validators import validate_index_dtype, validate_index_key
18
13
  from ngio.utils import (
19
- NgioTableValidationError,
20
14
  NgioValueError,
21
15
  StoreOrGroup,
22
16
  open_group_wrapper,
@@ -26,7 +20,7 @@ if TYPE_CHECKING:
26
20
  from collections.abc import Callable, Collection
27
21
 
28
22
 
29
- def custom_read_zarr(
23
+ def custom_anndata_read_zarr(
30
24
  store: StoreOrGroup, elem_to_read: Collection[str] | None = None
31
25
  ) -> AnnData:
32
26
  """Read from a hierarchical Zarr array store.
@@ -39,7 +33,7 @@ def custom_read_zarr(
39
33
  store (StoreOrGroup): A store or group to read the AnnData from.
40
34
  elem_to_read (Collection[str] | None): The elements to read from the store.
41
35
  """
42
- group, _ = open_group_wrapper(store=store, mode="r")
36
+ group = open_group_wrapper(store=store, mode="r")
43
37
 
44
38
  if not isinstance(group.store, zarr.DirectoryStore):
45
39
  elem_to_read = ["X", "obs", "var"]
@@ -96,103 +90,3 @@ def custom_read_zarr(
96
90
  if not isinstance(adata, AnnData):
97
91
  raise NgioValueError(f"Expected an AnnData object, but got {type(adata)}")
98
92
  return adata
99
-
100
-
101
- def _check_for_mixed_types(series: pd.Series) -> None:
102
- """Check if the column has mixed types."""
103
- if series.apply(type).nunique() > 1: # type: ignore
104
- raise NgioTableValidationError(
105
- f"Column {series.name} has mixed types: "
106
- f"{series.apply(type).unique()}. " # type: ignore
107
- "Type of all elements must be the same."
108
- )
109
-
110
-
111
- def _check_for_supported_types(series: pd.Series) -> Literal["str", "int", "numeric"]:
112
- """Check if the column has supported types."""
113
- if ptypes.is_string_dtype(series):
114
- return "str"
115
- if ptypes.is_integer_dtype(series):
116
- return "int"
117
- if ptypes.is_numeric_dtype(series):
118
- return "numeric"
119
- raise NgioTableValidationError(
120
- f"Column {series.name} has unsupported type: {series.dtype}."
121
- " Supported types are string and numerics."
122
- )
123
-
124
-
125
- def dataframe_to_anndata(
126
- dataframe: pd.DataFrame,
127
- index_key: str | None = None,
128
- overwrite: bool = False,
129
- ) -> ad.AnnData:
130
- """Convert a table DataFrame to an AnnData object.
131
-
132
- Args:
133
- dataframe (pd.DataFrame): A pandas DataFrame representing a fractal table.
134
- index_key (str): The column name to use as the index of the DataFrame.
135
- Default is None.
136
- overwrite (bool): Whether to overwrite the index if a different index is found.
137
- Default is False.
138
- """
139
- # Check if the index_key is present in the data frame + optional validations
140
- dataframe = validate_index_key(dataframe, index_key, overwrite=overwrite)
141
- dataframe = validate_index_dtype(dataframe, index_type="str")
142
-
143
- str_columns, int_columns, num_columns = [], [], []
144
- for c_name in dataframe.columns:
145
- column_df = dataframe[c_name]
146
- _check_for_mixed_types(column_df) # Mixed types are not allowed in the table
147
- c_type = _check_for_supported_types(
148
- column_df
149
- ) # Only string and numeric types are allowed
150
-
151
- if c_type == "str":
152
- str_columns.append(c_name)
153
-
154
- elif c_type == "int":
155
- int_columns.append(c_name)
156
-
157
- elif c_type == "numeric":
158
- num_columns.append(c_name)
159
-
160
- # Converting all observations to string
161
- obs_df = dataframe[str_columns + int_columns]
162
- obs_df.index = dataframe.index
163
-
164
- x_df = dataframe[num_columns]
165
-
166
- if x_df.dtypes.nunique() > 1:
167
- x_df = x_df.astype("float64")
168
-
169
- if x_df.empty:
170
- # If there are no numeric columns, create an empty array
171
- # to avoid AnnData failing to create the object
172
- x_df = np.zeros((len(obs_df), 0), dtype="float64")
173
-
174
- return ad.AnnData(X=x_df, obs=obs_df)
175
-
176
-
177
- def anndata_to_dataframe(
178
- anndata: ad.AnnData,
179
- index_key: str | None = "label",
180
- index_type: str = "int",
181
- overwrite: bool = False,
182
- ) -> pd.DataFrame:
183
- """Convert a AnnData object representing a fractal table to a pandas DataFrame.
184
-
185
- Args:
186
- anndata (ad.AnnData): An AnnData object representing a fractal table.
187
- index_key (str): The column name to use as the index of the DataFrame.
188
- Default is 'label'.
189
- index_type (str): The type of the index column in the DataFrame.
190
- Either 'str' or 'int'. Default is 'int'.
191
- overwrite (bool): Whether to overwrite the index if a different index is found.
192
- Default is False.
193
- """
194
- dataframe = anndata.to_df()
195
- dataframe[anndata.obs_keys()] = anndata.obs
196
- dataframe = validate_index_key(dataframe, index_key, overwrite=overwrite)
197
- dataframe = validate_index_dtype(dataframe, index_type)
198
- return dataframe
@@ -1,14 +1,16 @@
1
- from collections.abc import Collection
2
- from pathlib import Path
3
-
4
1
  from anndata import AnnData
5
2
  from pandas import DataFrame
3
+ from polars import DataFrame as PolarsDataFrame
4
+ from polars import LazyFrame
6
5
 
7
6
  from ngio.tables.backends._abstract_backend import AbstractTableBackend
8
7
  from ngio.tables.backends._anndata_utils import (
9
- anndata_to_dataframe,
10
- custom_read_zarr,
11
- dataframe_to_anndata,
8
+ custom_anndata_read_zarr,
9
+ )
10
+ from ngio.tables.backends._utils import (
11
+ convert_pandas_to_anndata,
12
+ convert_polars_to_anndata,
13
+ normalize_anndata,
12
14
  )
13
15
  from ngio.utils import NgioValueError
14
16
 
@@ -18,59 +20,54 @@ class AnnDataBackend(AbstractTableBackend):
18
20
 
19
21
  @staticmethod
20
22
  def backend_name() -> str:
21
- """The name of the backend."""
23
+ """Return the name of the backend."""
22
24
  return "anndata_v1"
23
25
 
24
26
  @staticmethod
25
27
  def implements_anndata() -> bool:
26
- """Whether the handler implements the anndata protocol."""
28
+ """Check if the backend implements the anndata protocol."""
27
29
  return True
28
30
 
29
31
  @staticmethod
30
- def implements_dataframe() -> bool:
32
+ def implements_pandas() -> bool:
31
33
  """Whether the handler implements the dataframe protocol."""
32
34
  return True
33
35
 
34
- def load_columns(self) -> list[str]:
35
- """List all labels in the group."""
36
- return list(self.load_as_dataframe().columns)
36
+ @staticmethod
37
+ def implements_polars() -> bool:
38
+ """Whether the handler implements the polars protocol."""
39
+ return True
37
40
 
38
- def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData:
39
- """Load the metadata in the store."""
40
- anndata = custom_read_zarr(self._group_handler._group)
41
- if columns is not None:
42
- raise NotImplementedError(
43
- "Selecting columns is not implemented for AnnData."
44
- )
41
+ def load_as_anndata(self) -> AnnData:
42
+ """Load the table as an AnnData object."""
43
+ anndata = custom_anndata_read_zarr(self._group_handler._group)
44
+ anndata = normalize_anndata(anndata, index_key=self.index_key)
45
45
  return anndata
46
46
 
47
- def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
48
- """List all labels in the group."""
49
- dataframe = anndata_to_dataframe(
50
- self.load_as_anndata(),
51
- index_key=self._index_key,
52
- index_type=self._index_type,
53
- )
54
- if columns is not None:
55
- dataframe = dataframe[columns]
56
- return dataframe
57
-
58
- def write_from_dataframe(
59
- self, table: DataFrame, metadata: dict | None = None
60
- ) -> None:
61
- """Consolidate the metadata in the store."""
62
- anndata = dataframe_to_anndata(table, index_key=self._index_key)
63
- self.write_from_anndata(anndata, metadata)
64
-
65
- def write_from_anndata(self, table: AnnData, metadata: dict | None = None) -> None:
66
- """Consolidate the metadata in the store."""
67
- store = self._group_handler.store
68
- if not isinstance(store, str | Path):
47
+ def write_from_anndata(self, table: AnnData) -> None:
48
+ """Serialize the table from an AnnData object."""
49
+ full_url = self._group_handler.full_url
50
+ if full_url is None:
69
51
  raise NgioValueError(
70
- "To write an AnnData object the store must be a local path/str."
52
+ f"Ngio does not support writing file from a "
53
+ f"store of type {type(self._group_handler)}. "
54
+ "Please make sure to use a compatible "
55
+ "store like a zarr.DirectoryStore."
71
56
  )
57
+ table.write_zarr(full_url) # type: ignore
72
58
 
73
- store = Path(store) / self._group_handler.group.path
74
- table.write_zarr(store)
75
- if metadata is not None:
76
- self._group_handler.write_attrs(metadata)
59
+ def write_from_pandas(self, table: DataFrame) -> None:
60
+ """Serialize the table from a pandas DataFrame."""
61
+ anndata = convert_pandas_to_anndata(
62
+ table,
63
+ index_key=self.index_key,
64
+ )
65
+ self.write_from_anndata(anndata)
66
+
67
+ def write_from_polars(self, table: PolarsDataFrame | LazyFrame) -> None:
68
+ """Consolidate the metadata in the store."""
69
+ anndata = convert_polars_to_anndata(
70
+ table,
71
+ index_key=self.index_key,
72
+ )
73
+ self.write_from_anndata(anndata)