From 795a23175b610a5bdb6437849e2bc80aafb32133 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:18:39 +0100 Subject: [PATCH 01/24] refactor: introduce _STORAGE_TYPES attribute for feature storage implementations --- junifer/storage/base.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/junifer/storage/base.py b/junifer/storage/base.py index fd6c879a7..0da3e9d64 100644 --- a/junifer/storage/base.py +++ b/junifer/storage/base.py @@ -6,8 +6,9 @@ from abc import ABC, abstractmethod from collections.abc import Iterable +from collections.abc import Sequence from pathlib import Path -from typing import Any, Optional, Union +from typing import Any, ClassVar, Optional, Union import numpy as np import pandas as pd @@ -29,37 +30,31 @@ class BaseFeatureStorage(ABC): ---------- uri : str or pathlib.Path The path to the storage. - storage_types : str or list of str - The available storage types for the class. single_output : bool, optional Whether to have single output (default True). Raises ------ - ValueError - If required storage type(s) is(are) missing from ``storage_types``. + AttributeError + If the storage does not have `_STORAGE_TYPES` attribute. """ + _STORAGE_TYPES: ClassVar[Sequence[str]] + def __init__( self, uri: Union[str, Path], - storage_types: Union[list[str], str], single_output: bool = True, ) -> None: - self.uri = uri - # Convert storage_types to list - if not isinstance(storage_types, list): - storage_types = [storage_types] - # Check if required inputs are found - if any(x not in self.get_valid_inputs() for x in storage_types): - wrong_storage_types = [ - x for x in storage_types if x not in self.get_valid_inputs() - ] + # Check for missing storage types attribute + if not hasattr(self, "_STORAGE_TYPES"): raise_error( - f"{self.__class__.__name__} cannot store {wrong_storage_types}" + msg="Missing `_STORAGE_TYPES` for the storage", + klass=AttributeError, + ) + self.uri = uri ) - self._valid_inputs = storage_types self.single_output = single_output def get_valid_inputs(self) -> list[str]: @@ -72,10 +67,7 @@ class BaseFeatureStorage(ABC): storage interface. """ - raise_error( - msg="Concrete classes need to implement get_valid_inputs().", - klass=NotImplementedError, - ) + return list(self._STORAGE_TYPES) def validate(self, input_: list[str]) -> None: """Validate the input to the pipeline step. @@ -91,11 +83,12 @@ class BaseFeatureStorage(ABC): If the ``input_`` is invalid. """ - if not any(x in input_ for x in self._valid_inputs): + # self._STORAGE_TYPES should be there already + if not any(x in input_ for x in self._STORAGE_TYPES): raise_error( "Input does not have the required data." f"\t Input: {input}" - f"\t Required (any of): {self._valid_inputs}" + f"\t Required (any of): {self._STORAGE_TYPES}" ) @abstractmethod @@ -206,7 +199,7 @@ class BaseFeatureStorage(ABC): """ # Do the check before calling the abstract methods, otherwise the # meta might be stored even if the data is not stored. - if kind not in self._valid_inputs: + if kind not in self._STORAGE_TYPES: raise_error( msg=f"I don't know how to store {kind}.", klass=ValueError, -- 2.52.0 From 4d7750490503f6a2972f5208ac78a670721cf29d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:22:38 +0100 Subject: [PATCH 02/24] update: adapt _STORAGE_TYPES attribute for HDF5 and SQLite storage implementations --- junifer/storage/hdf5.py | 48 ++++++++++------------------------ junifer/storage/pandas_base.py | 21 ++++++--------- junifer/storage/sqlite.py | 3 --- 3 files changed, 22 insertions(+), 50 deletions(-) diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index 99c2f406e..1ff6850d1 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -6,8 +6,9 @@ from collections import defaultdict from collections.abc import Iterable +from collections.abc import Sequence from pathlib import Path -from typing import Any, Optional, Union +from typing import Any, ClassVar, Optional, Union import numpy as np import pandas as pd @@ -135,6 +136,14 @@ class HDF5FeatureStorage(BaseFeatureStorage): """ + _STORAGE_TYPES: ClassVar[Sequence[str]] = [ + "vector", + "timeseries", + "matrix", + "scalar_table", + "timeseries_2d", + ] + def __init__( self, uri: Union[str, Path], @@ -156,43 +165,14 @@ class HDF5FeatureStorage(BaseFeatureStorage): ) uri.parent.mkdir(parents=True, exist_ok=True) - # Available storage kinds - storage_types = [ - "vector", - "timeseries", - "matrix", - "scalar_table", - "timeseries_2d", - ] - - super().__init__( - uri=uri, - storage_types=storage_types, - single_output=single_output, - ) - self.overwrite = overwrite self.compression = compression self.force_float32 = force_float32 self.chunk_size = chunk_size - - def get_valid_inputs(self) -> list[str]: - """Get valid storage types for input. - - Returns - ------- - list of str - The list of storage types that can be used as input for this - storage. - - """ - return [ - "matrix", - "vector", - "timeseries", - "scalar_table", - "timeseries_2d", - ] + super().__init__( + uri=uri, + single_output=single_output, + ) def _fetch_correct_uri_for_io(self, element: Optional[dict]) -> str: """Return proper URI for I/O based on `element`. diff --git a/junifer/storage/pandas_base.py b/junifer/storage/pandas_base.py index fbc66b9f4..7095b7919 100644 --- a/junifer/storage/pandas_base.py +++ b/junifer/storage/pandas_base.py @@ -6,8 +6,9 @@ import json from collections.abc import Iterable +from collections.abc import Sequence from pathlib import Path -from typing import Optional, Union +from typing import ClassVar, Optional, Union import numpy as np import pandas as pd @@ -40,23 +41,17 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): """ + _STORAGE_TYPES: ClassVar[Sequence[str]] = [ + "vector", + "timeseries", + "matrix", + ] + def __init__( self, uri: Union[str, Path], single_output: bool = True, **kwargs ) -> None: super().__init__(uri=uri, single_output=single_output, **kwargs) - def get_valid_inputs(self) -> list[str]: - """Get valid storage types for input. - - Returns - ------- - list of str - The list of storage types that can be used as input for this - storage interface. - - """ - return ["matrix", "vector", "timeseries"] - def _meta_row(self, meta: dict, meta_md5: str) -> pd.DataFrame: """Convert the metadata to a pandas DataFrame. diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index 6c0fb03d3..f38085a9d 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -81,11 +81,8 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): "does not exist, creating now." ) uri.parent.mkdir(parents=True, exist_ok=True) - # Available storage kinds - storage_types = ["vector", "timeseries", "matrix"] super().__init__( uri=uri, - storage_types=storage_types, single_output=single_output, **kwargs, ) -- 2.52.0 From 04e98188b33d4e221732e73b9e498c4a8e9e1d1b Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:26:08 +0100 Subject: [PATCH 03/24] refactor: centralise storage path creation logic in base class --- junifer/storage/base.py | 11 ++++++++++- junifer/storage/hdf5.py | 12 ------------ junifer/storage/sqlite.py | 10 ---------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/junifer/storage/base.py b/junifer/storage/base.py index 0da3e9d64..9d4bd0f68 100644 --- a/junifer/storage/base.py +++ b/junifer/storage/base.py @@ -13,7 +13,7 @@ from typing import Any, ClassVar, Optional, Union import numpy as np import pandas as pd -from ..utils import raise_error +from ..utils import logger, raise_error from .utils import process_meta @@ -53,8 +53,17 @@ class BaseFeatureStorage(ABC): msg="Missing `_STORAGE_TYPES` for the storage", klass=AttributeError, ) + # Convert str to Path + if not isinstance(uri, Path): + uri = Path(uri) self.uri = uri + # Create parent directories if not present + if not self.uri.parent.exists(): + logger.info( + f"Output directory: '{self.uri.parent.resolve()}' " + "does not exist, creating now" ) + self.uri.parent.mkdir(parents=True, exist_ok=True) self.single_output = single_output def get_valid_inputs(self) -> list[str]: diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index 1ff6850d1..22b7399c2 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -153,18 +153,6 @@ class HDF5FeatureStorage(BaseFeatureStorage): force_float32: bool = True, chunk_size: int = 100, ) -> None: - # Convert str to Path - if not isinstance(uri, Path): - uri = Path(uri) - - # Create parent directories if not present - if not uri.parent.exists(): - logger.info( - f"Output directory: '{uri.parent.resolve()}' " - "does not exist, creating now" - ) - uri.parent.mkdir(parents=True, exist_ok=True) - self.overwrite = overwrite self.compression = compression self.force_float32 = force_float32 diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index f38085a9d..89771056b 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -71,16 +71,6 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): "Must be either 'update' or 'ignore'." ) ) - # Convert str to Path - if not isinstance(uri, Path): - uri = Path(uri) - # Create parent directories if not present - if not uri.parent.exists(): - logger.info( - f"Output directory ({uri.parent.absolute()!s}) " - "does not exist, creating now." - ) - uri.parent.mkdir(parents=True, exist_ok=True) super().__init__( uri=uri, single_output=single_output, -- 2.52.0 From fbd75dd01ebf610376dc7fa39ca9cb70836b08ed Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:30:06 +0100 Subject: [PATCH 04/24] update: improve typing annotations for store methods of feature storage implementations --- junifer/storage/base.py | 33 ++++++++++++++++----------------- junifer/storage/hdf5.py | 33 ++++++++++++++++----------------- junifer/storage/pandas_base.py | 15 +++++++-------- junifer/storage/sqlite.py | 9 +++++---- 4 files changed, 44 insertions(+), 46 deletions(-) diff --git a/junifer/storage/base.py b/junifer/storage/base.py index 9d4bd0f68..6b715f050 100644 --- a/junifer/storage/base.py +++ b/junifer/storage/base.py @@ -5,7 +5,6 @@ # License: AGPL from abc import ABC, abstractmethod -from collections.abc import Iterable from collections.abc import Sequence from pathlib import Path from typing import Any, ClassVar, Optional, Union @@ -238,8 +237,8 @@ class BaseFeatureStorage(ABC): meta_md5: str, element: dict, data: np.ndarray, - col_names: Optional[Iterable[str]] = None, - row_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, + row_names: Optional[Sequence[str]] = None, matrix_kind: str = "full", diagonal: bool = True, ) -> None: @@ -253,9 +252,9 @@ class BaseFeatureStorage(ABC): The element as a dictionary. data : numpy.ndarray The matrix data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). - row_names : str, optional + row_names : list-like of str, optional The row labels (default None). matrix_kind : str, optional The kind of matrix: @@ -280,7 +279,7 @@ class BaseFeatureStorage(ABC): meta_md5: str, element: dict, data: Union[np.ndarray, list], - col_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, ) -> None: """Store vector. @@ -292,7 +291,7 @@ class BaseFeatureStorage(ABC): The element as a dictionary. data : numpy.ndarray or list The vector data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). """ @@ -306,7 +305,7 @@ class BaseFeatureStorage(ABC): meta_md5: str, element: dict, data: np.ndarray, - col_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, ) -> None: """Store timeseries. @@ -318,7 +317,7 @@ class BaseFeatureStorage(ABC): The element as a dictionary. data : numpy.ndarray The timeseries data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). """ @@ -332,8 +331,8 @@ class BaseFeatureStorage(ABC): meta_md5: str, element: dict, data: np.ndarray, - col_names: Optional[Iterable[str]] = None, - row_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, + row_names: Optional[Sequence[str]] = None, ) -> None: """Store 2D timeseries. @@ -345,9 +344,9 @@ class BaseFeatureStorage(ABC): The element as a dictionary. data : numpy.ndarray The timeseries data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). - row_names : list or tuple of str, optional + row_names : list-like of str, optional The row labels (default None). """ @@ -361,8 +360,8 @@ class BaseFeatureStorage(ABC): meta_md5: str, element: dict, data: np.ndarray, - col_names: Optional[Iterable[str]] = None, - row_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, + row_names: Optional[Sequence[str]] = None, row_header_col_name: Optional[str] = "feature", ) -> None: """Store table with scalar values. @@ -375,9 +374,9 @@ class BaseFeatureStorage(ABC): The element as a dictionary. data : numpy.ndarray The timeseries data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). - row_names : str, optional + row_names : list-like of str, optional The row labels (default None). row_header_col_name : str, optional The column name for the row header column (default "feature"). diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index 22b7399c2..dd6dcefb8 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -5,7 +5,6 @@ # License: AGPL from collections import defaultdict -from collections.abc import Iterable from collections.abc import Sequence from pathlib import Path from typing import Any, ClassVar, Optional, Union @@ -805,8 +804,8 @@ class HDF5FeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict[str, str], data: np.ndarray, - col_names: Optional[Iterable[str]] = None, - row_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, + row_names: Optional[Sequence[str]] = None, matrix_kind: str = "full", diagonal: bool = True, row_header_col_name: str = "ROI", @@ -824,9 +823,9 @@ class HDF5FeatureStorage(BaseFeatureStorage): The element as dictionary. data : numpy.ndarray The matrix data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). - row_names : list or tuple of str, optional + row_names : list-like of str, optional The row labels (default None). matrix_kind : str, optional The kind of matrix: @@ -884,7 +883,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict[str, str], data: Union[np.ndarray, list], - col_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, ) -> None: """Store vector. @@ -896,7 +895,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): The element as dictionary. data : numpy.ndarray or list The vector data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). """ @@ -927,7 +926,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict[str, str], data: np.ndarray, - col_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, ) -> None: """Store timeseries. @@ -939,7 +938,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): The element as dictionary. data : numpy.ndarray The timeseries data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). """ @@ -957,8 +956,8 @@ class HDF5FeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict[str, str], data: np.ndarray, - col_names: Optional[Iterable[str]] = None, - row_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, + row_names: Optional[Sequence[str]] = None, ) -> None: """Store a 2D timeseries. @@ -970,9 +969,9 @@ class HDF5FeatureStorage(BaseFeatureStorage): The element as dictionary. data : numpy.ndarray The 2D timeseries data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). - row_names : list or tuple of str, optional + row_names : list-like of str, optional The row labels (default None). """ @@ -995,8 +994,8 @@ class HDF5FeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict, data: np.ndarray, - col_names: Optional[Iterable[str]] = None, - row_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, + row_names: Optional[Sequence[str]] = None, row_header_col_name: Optional[str] = "feature", ) -> None: """Store table with scalar values. @@ -1009,9 +1008,9 @@ class HDF5FeatureStorage(BaseFeatureStorage): The element as a dictionary. data : numpy.ndarray The scalar table data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). - row_names : str, optional + row_names : list-like of str, optional The row labels (default None). row_header_col_name : str, optional The column name for the row header column (default "feature"). diff --git a/junifer/storage/pandas_base.py b/junifer/storage/pandas_base.py index 7095b7919..07995e50b 100644 --- a/junifer/storage/pandas_base.py +++ b/junifer/storage/pandas_base.py @@ -5,7 +5,6 @@ # License: AGPL import json -from collections.abc import Iterable from collections.abc import Sequence from pathlib import Path from typing import ClassVar, Optional, Union @@ -97,7 +96,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): """ # Make mapping between element access keys and values - elem_idx: dict[str, Iterable[str]] = { + elem_idx: dict[str, Sequence[str]] = { k: [v] * n_rows for k, v in element.items() } @@ -155,7 +154,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict, data: Union[np.ndarray, list], - col_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, rows_col_name: Optional[str] = None, ) -> None: """Store 2D data. @@ -168,7 +167,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): The element as a dictionary. data : numpy.ndarray or list The data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). rows_col_name : str, optional The column name to use in case number of rows greater than 1. @@ -194,7 +193,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict, data: Union[np.ndarray, list], - col_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, ) -> None: """Store vector. @@ -206,7 +205,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): The element as a dictionary. data : numpy.ndarray or list The vector data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). """ @@ -232,7 +231,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): meta_md5: str, element: dict, data: np.ndarray, - col_names: Optional[Iterable[str]] = None, + col_names: Optional[Sequence[str]] = None, ) -> None: """Store timeseries. @@ -244,7 +243,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): The element as a dictionary. data : numpy.ndarray The timeseries data to store. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). """ diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index 89771056b..3657cc679 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -5,6 +5,7 @@ # License: AGPL import json +from collections.abc import Sequence from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Union @@ -421,8 +422,8 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): meta_md5: str, element: dict, data: np.ndarray, - col_names: Optional[list[str]] = None, - row_names: Optional[list[str]] = None, + col_names: Optional[Sequence[str]] = None, + row_names: Optional[Sequence[str]] = None, matrix_kind: str = "full", diagonal: bool = True, ) -> None: @@ -438,9 +439,9 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): The matrix data to store. meta : dict The metadata as a dictionary. - col_names : list or tuple of str, optional + col_names : list-like of str, optional The column labels (default None). - row_names : str, optional + row_names : list-like of str, optional The row labels (optional None). matrix_kind : str, optional The kind of matrix: -- 2.52.0 From 902bb3c58493d40b8f02b56973c6ffffd1f74918 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:31:00 +0100 Subject: [PATCH 05/24] chore: improve commentary for BaseFeatureStorage --- junifer/storage/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/junifer/storage/base.py b/junifer/storage/base.py index 6b715f050..dc9d5a5be 100644 --- a/junifer/storage/base.py +++ b/junifer/storage/base.py @@ -22,7 +22,7 @@ __all__ = ["BaseFeatureStorage"] class BaseFeatureStorage(ABC): """Abstract base class for feature storage. - For every interface that is required, one needs to provide a concrete + For every storage, one needs to provide a concrete implementation of this abstract class. Parameters @@ -72,7 +72,7 @@ class BaseFeatureStorage(ABC): ------- list of str The list of storage types that can be used as input for this - storage interface. + storage. """ return list(self._STORAGE_TYPES) @@ -212,9 +212,11 @@ class BaseFeatureStorage(ABC): msg=f"I don't know how to store {kind}.", klass=ValueError, ) + # Process and store metadata t_meta = kwargs.pop("meta") meta_md5, t_meta, t_element = process_meta(t_meta) self.store_metadata(meta_md5=meta_md5, element=t_element, meta=t_meta) + # Store data if kind == "matrix": self.store_matrix(meta_md5=meta_md5, element=t_element, **kwargs) elif kind == "timeseries": -- 2.52.0 From acca92a683051f5600cbee8de6cb899f584f0696 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:31:24 +0100 Subject: [PATCH 06/24] chore: add no cover pragmas for BaseFeatureStorage --- junifer/storage/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/junifer/storage/base.py b/junifer/storage/base.py index dc9d5a5be..13f4448b7 100644 --- a/junifer/storage/base.py +++ b/junifer/storage/base.py @@ -114,7 +114,7 @@ class BaseFeatureStorage(ABC): raise_error( msg="Concrete classes need to implement list_features().", klass=NotImplementedError, - ) + ) # pragma: no cover @abstractmethod def read( @@ -142,7 +142,7 @@ class BaseFeatureStorage(ABC): raise_error( msg="Concrete classes need to implement read().", klass=NotImplementedError, - ) + ) # pragma: no cover @abstractmethod def read_df( @@ -168,7 +168,7 @@ class BaseFeatureStorage(ABC): raise_error( msg="Concrete classes need to implement read_df().", klass=NotImplementedError, - ) + ) # pragma: no cover @abstractmethod def store_metadata(self, meta_md5: str, element: dict, meta: dict) -> None: @@ -187,7 +187,7 @@ class BaseFeatureStorage(ABC): raise_error( msg="Concrete classes need to implement store_metadata().", klass=NotImplementedError, - ) + ) # pragma: no cover def store(self, kind: str, **kwargs) -> None: """Store extracted features data. @@ -395,7 +395,7 @@ class BaseFeatureStorage(ABC): raise_error( msg="Concrete classes need to implement collect().", klass=NotImplementedError, - ) + ) # pragma: no cover def __str__(self) -> str: """Represent object as string. -- 2.52.0 From 6c9dd8a345a23be2f42abcffd446b9936d9bb3fc Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:32:30 +0100 Subject: [PATCH 07/24] refactor: remove **kwargs from PandasBaseFeatureStorage and SQLiteFeatureStorage --- junifer/storage/pandas_base.py | 8 ++++---- junifer/storage/sqlite.py | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/junifer/storage/pandas_base.py b/junifer/storage/pandas_base.py index 07995e50b..3a37952a7 100644 --- a/junifer/storage/pandas_base.py +++ b/junifer/storage/pandas_base.py @@ -31,8 +31,6 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): The path to the storage. single_output : bool, optional Whether to have single output (default True). - **kwargs - Keyword arguments passed to superclass. See Also -------- @@ -47,9 +45,11 @@ class PandasBaseFeatureStorage(BaseFeatureStorage): ] def __init__( - self, uri: Union[str, Path], single_output: bool = True, **kwargs + self, + uri: Union[str, Path], + single_output: bool = True, ) -> None: - super().__init__(uri=uri, single_output=single_output, **kwargs) + super().__init__(uri=uri, single_output=single_output) def _meta_row(self, meta: dict, meta_md5: str) -> pd.DataFrame: """Convert the metadata to a pandas DataFrame. diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index 3657cc679..4c20dab54 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -47,8 +47,6 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): upsert : {"ignore", "update"}, optional Upsert mode. If "ignore" is used, the existing elements are ignored. If "update", the existing elements are updated (default "update"). - **kwargs : dict - The keyword arguments passed to the superclass. See Also -------- @@ -62,7 +60,6 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): uri: Union[str, Path], single_output: bool = True, upsert: str = "update", - **kwargs: str, ) -> None: # Check upsert argument value if upsert not in ["update", "ignore"]: @@ -75,7 +72,6 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): super().__init__( uri=uri, single_output=single_output, - **kwargs, ) # Set upsert self._upsert = upsert -- 2.52.0 From d2dc255c2e746d12fb666d2a99020a610ebb7628 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:34:31 +0100 Subject: [PATCH 08/24] update: improve commentary, update exception class and typing annotations for HDF5FeatureStorage --- junifer/storage/hdf5.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index dd6dcefb8..56be9f43a 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -162,9 +162,9 @@ class HDF5FeatureStorage(BaseFeatureStorage): ) def _fetch_correct_uri_for_io(self, element: Optional[dict]) -> str: - """Return proper URI for I/O based on `element`. + """Return proper URI for I/O based on ``element``. - If `element` is None, will return `self.uri`. + If ``element`` is None, will return ``self.uri``. Parameters ---------- @@ -176,6 +176,11 @@ class HDF5FeatureStorage(BaseFeatureStorage): str Formatted URI for accessing metadata and data. + Raises + ------ + RuntimeError + If ``element=None`` when ``single_output=False``. + """ if not self.single_output and not element: raise_error( @@ -341,8 +346,14 @@ class HDF5FeatureStorage(BaseFeatureStorage): Raises ------ + ValueError + If both ``feature_md5`` and ``feature_name`` are provided or + if none of ``feature_md5`` or ``feature_name`` is provided. IOError If HDF5 file does not exist. + RuntimeError + If feature is not found or + if duplicate feature is found with the same name. """ # Parameter conflict @@ -380,7 +391,10 @@ class HDF5FeatureStorage(BaseFeatureStorage): if feature_md5 in metadata: md5 = feature_md5 # type: ignore else: - raise_error(msg=f"Feature MD5 '{feature_md5}' not found") + raise_error( + msg=f"Feature MD5 '{feature_md5}' not found", + klass=RuntimeError, + ) # Consider feature_name elif feature_name: @@ -881,7 +895,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): def store_vector( self, meta_md5: str, - element: dict[str, str], + element: dict, data: Union[np.ndarray, list], col_names: Optional[Sequence[str]] = None, ) -> None: @@ -924,7 +938,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): def store_timeseries( self, meta_md5: str, - element: dict[str, str], + element: dict, data: np.ndarray, col_names: Optional[Sequence[str]] = None, ) -> None: @@ -954,19 +968,19 @@ class HDF5FeatureStorage(BaseFeatureStorage): def store_timeseries_2d( self, meta_md5: str, - element: dict[str, str], + element: dict, data: np.ndarray, col_names: Optional[Sequence[str]] = None, row_names: Optional[Sequence[str]] = None, ) -> None: - """Store a 2D timeseries. + """Store 2D timeseries. Parameters ---------- meta_md5 : str The metadata MD5 hash. element : dict - The element as dictionary. + The element as a dictionary. data : numpy.ndarray The 2D timeseries data to store. col_names : list-like of str, optional -- 2.52.0 From a77d758c4142b578872bf894031f1bca9e00031d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:35:37 +0100 Subject: [PATCH 09/24] update: update conditional for HDF5FeatureStorage --- junifer/storage/hdf5.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index 56be9f43a..537fe0c04 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -182,14 +182,12 @@ class HDF5FeatureStorage(BaseFeatureStorage): If ``element=None`` when ``single_output=False``. """ - if not self.single_output and not element: + if not self.single_output and element is None: raise_error( - msg=( - "`element` must be provided when `single_output` is False" - ), + msg="`element` must be provided when `single_output=False`", klass=RuntimeError, ) - elif not self.single_output and element: + elif not self.single_output and element is not None: # element access for multi output only prefix = element_to_prefix(element=element) else: -- 2.52.0 From e8267296549f226e979d74c42465b53600e73ca2 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:36:28 +0100 Subject: [PATCH 10/24] update: simplify globbing for HDF5FeatureStorage.collect() --- junifer/storage/hdf5.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index 537fe0c04..6ea0253b6 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -1058,11 +1058,6 @@ class HDF5FeatureStorage(BaseFeatureStorage): klass=NotImplementedError, ) - # Glob files - globbed_files = list( - self.uri.parent.glob(f"*_{self.uri.name}") # type: ignore - ) - # Create new storage instance out_storage = HDF5FeatureStorage(uri=self.uri, overwrite="update") @@ -1073,7 +1068,9 @@ class HDF5FeatureStorage(BaseFeatureStorage): # Collect element files per feature MD5 elements_per_feature_md5 = defaultdict(list) out_metadata = {} - for file_ in tqdm(globbed_files, desc="file-metadata"): + for file_ in tqdm( + self.uri.parent.glob(f"*_{self.uri.name}"), desc="file-metadata" + ): logger.debug(f"Reading HDF5 file: {file_} ...") # Create new storage instance to load metadata in_storage = HDF5FeatureStorage(uri=file_) -- 2.52.0 From 557cd139e4e0ceb3405470696f9ff8b905eb7ae0 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:37:23 +0100 Subject: [PATCH 11/24] chore: remove type ignore directives for HDF5FeatureStorage --- junifer/storage/hdf5.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index 6ea0253b6..13ade4af5 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -194,7 +194,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): # parent access for single output, ignore element prefix = "" # Format URI based on prefix - return f"{self.uri.parent}/{prefix}{self.uri.name}" # type: ignore + return f"{self.uri.parent}/{prefix}{self.uri.name}" def _read_metadata( self, element: Optional[dict[str, str]] = None @@ -261,7 +261,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): """ # Read metadata metadata = read_hdf5( - fname=str(self.uri.resolve()), # type: ignore + fname=str(self.uri.resolve()), title="meta", slash="ignore", ) @@ -372,7 +372,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): ) # Parameter check pass; read metadata metadata = read_hdf5( - fname=str(self.uri.resolve()), # type: ignore + fname=str(self.uri.resolve()), title="meta", slash="ignore", ) @@ -387,7 +387,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): ) # Validate MD5 if feature_md5 in metadata: - md5 = feature_md5 # type: ignore + md5 = feature_md5 else: raise_error( msg=f"Feature MD5 '{feature_md5}' not found", @@ -398,7 +398,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): elif feature_name: logger.debug( f"Validating feature name '{feature_name}' in metadata " - f"for: {self.uri.resolve()} ..." # type: ignore + f"for: {self.uri.resolve()} ..." ) # Retrieve MD5 for feature_name # Implicit counter for duplicate feature_name with different @@ -424,7 +424,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): # Read data from HDF5 hdf_data = read_hdf5( - fname=str(self.uri.resolve()), # type: ignore + fname=str(self.uri.resolve()), title=md5, slash="ignore", ) @@ -552,7 +552,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): reshaped_data = np.concatenate(all_data, axis=0) # Create dataframe for index - idx_df = pd.DataFrame(data=element_idx) # type: ignore + idx_df = pd.DataFrame(data=element_idx) # Create multiindex from dataframe hdf_data_idx = pd.MultiIndex.from_frame(df=idx_df) logger.debug( @@ -568,7 +568,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): df = pd.DataFrame( data=reshaped_data, index=hdf_data_idx, - columns=columns, # type: ignore + columns=columns, dtype=reshaped_data.dtype, ) logger.debug( @@ -600,7 +600,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): write_hdf5( fname=fname, data=processed_data, - overwrite=self.overwrite, # type: ignore + overwrite=self.overwrite, compression=self.compression, title=title, slash="error", @@ -874,8 +874,8 @@ class HDF5FeatureStorage(BaseFeatureStorage): matrix_kind=matrix_kind, diagonal=diagonal, data_shape=data.shape, - row_names_len=len(row_names), # type: ignore - col_names_len=len(col_names), # type: ignore + row_names_len=len(row_names), + col_names_len=len(col_names), ) # Store self._store_data( @@ -989,8 +989,8 @@ class HDF5FeatureStorage(BaseFeatureStorage): """ store_timeseries_2d_checks( data_shape=data.shape, - row_names_len=len(row_names), # type: ignore - col_names_len=len(col_names), # type: ignore + row_names_len=len(row_names) if row_names is not None else 0, + col_names_len=len(col_names) if col_names is not None else 0, ) self._store_data( kind="timeseries_2d", @@ -1063,7 +1063,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): # Run loop to collect metadata logger.info( - f"Collecting metadata from {self.uri.parent}/*_{self.uri.name}" # type: ignore + f"Collecting metadata from {self.uri.parent}/*_{self.uri.name}" ) # Collect element files per feature MD5 elements_per_feature_md5 = defaultdict(list) @@ -1089,7 +1089,7 @@ class HDF5FeatureStorage(BaseFeatureStorage): logger.info("Writing metadata to HDF5 file ...") # Save metadata out metadata out_storage._write_processed_data( - fname=str(self.uri.resolve()), # type: ignore + fname=str(self.uri.resolve()), processed_data=out_metadata, title="meta", ) @@ -1170,9 +1170,9 @@ class HDF5FeatureStorage(BaseFeatureStorage): # Write to HDF5 write_hdf5( - fname=str(self.uri.resolve()), # type: ignore + fname=str(self.uri.resolve()), data=to_write, - overwrite="update", # type: ignore + overwrite="update", compression=0, title=feature_md5, slash="error", -- 2.52.0 From d6cd2d4d726e0d4349b9853f275a96fc4234869e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:38:14 +0100 Subject: [PATCH 12/24] chore: remove type ignore directives for SQLiteFeatureStorage --- junifer/storage/sqlite.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index 4c20dab54..0eefd76f2 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union import numpy as np import pandas as pd from pandas.core.base import NoNewAttributesMixin -from pandas.io.sql import pandasSQL_builder # type: ignore +from pandas.io.sql import pandasSQL_builder from sqlalchemy import create_engine, inspect from tqdm import tqdm @@ -98,10 +98,7 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): raise_error(msg) prefix = element_to_prefix(element) # Format URI for engine creation - uri = ( - f"sqlite:///{self.uri.parent}/" # type: ignore - f"{prefix}{self.uri.name}" # type: ignore - ) + uri = f"sqlite:///{self.uri.parent}/{prefix}{self.uri.name}" return create_engine(uri, echo=False) def _save_upsert( @@ -463,8 +460,8 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): matrix_kind=matrix_kind, diagonal=diagonal, data_shape=data.shape, - row_names_len=len(row_names), # type: ignore - col_names_len=len(col_names), # type: ignore + row_names_len=len(row_names), + col_names_len=len(col_names), ) # Matrix to vector conversion flat_data, columns = matrix_to_vector( @@ -517,13 +514,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): msg="collect() is not implemented for single output.", klass=IOError, ) - logger.info( - f"Collecting data from {self.uri.parent}/*{self.uri.name}" # type: ignore - ) + logger.info(f"Collecting data from {self.uri.parent}/*{self.uri.name}") # Create new instance out_storage = SQLiteFeatureStorage(uri=self.uri, upsert="ignore") # Glob files - files = self.uri.parent.glob(f"*{self.uri.name}") # type: ignore + files = self.uri.parent.glob(f"*{self.uri.name}") for elem in tqdm(files, desc="file"): logger.debug(f"Reading from {elem.absolute()!s}") in_storage = SQLiteFeatureStorage(uri=elem) -- 2.52.0 From f63c4eda8eec617eb8822677ed6ddbfe6c6c42a3 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:38:58 +0100 Subject: [PATCH 13/24] chore: improve commentary, docstrings and logging for SQLiteFeatureStorage --- junifer/storage/sqlite.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index 0eefd76f2..6b4aca8cc 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -61,7 +61,7 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): single_output: bool = True, upsert: str = "update", ) -> None: - # Check upsert argument value + # Check and set upsert argument value if upsert not in ["update", "ignore"]: raise_error( msg=( @@ -89,6 +89,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): sqlalchemy.engine.Engine The sqlalchemy engine. + Raises + ------ + ValueError + If ``element=None`` when ``single_output=False``. + """ # Prefixed elements prefix = "" @@ -128,8 +133,8 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): Raises ------ ValueError - If the table exists and if_exists is "fail" or if invalid option is - passed to `if_exists`. + If the table exists and ``if_exists="fail"`` or + if invalid option is passed to ``if_exists``. """ # Get index names @@ -238,10 +243,14 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): dict The stored feature as a dictionary. + Raises + ------ + NotImplementedError + """ raise_error( msg=( - "read() for SQLiteFeatureStorage is not currently planned to " + "SQLiteFeatureStorage.read() is not currently planned to " "be implemented in the near future. If you need the " "functionality, contact the junifer developers " "(https://juaml.github.io/junifer/main/help.html)." -- 2.52.0 From d9f21e3f9f6cbaad72d6b627c526081a030ecf19 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:39:43 +0100 Subject: [PATCH 14/24] update: simplify conditional in SQLiteFeatureStorage --- junifer/storage/sqlite.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index 6b4aca8cc..cb121e284 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -99,9 +99,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): prefix = "" if self.single_output is False: if element is None: - msg = "element must be specified when single_output is False." - raise_error(msg) - prefix = element_to_prefix(element) + raise_error( + "`element` cannot be None when `single_output=False`" + ) + else: + prefix = element_to_prefix(element) # Format URI for engine creation uri = f"sqlite:///{self.uri.parent}/{prefix}{self.uri.name}" return create_engine(uri, echo=False) -- 2.52.0 From 2c3471fb817ff9c9703acb868b91d5a952421b27 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:42:53 +0100 Subject: [PATCH 15/24] update: make upsert public in SQLiteFeatureStorage --- junifer/storage/sqlite.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index cb121e284..63a8ced91 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -69,12 +69,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): "Must be either 'update' or 'ignore'." ) ) + self.upsert = upsert super().__init__( uri=uri, single_output=single_output, ) - # Set upsert - self._upsert = upsert def get_engine(self, element: Optional[dict] = None) -> "Engine": """Get engine. @@ -175,12 +174,12 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): warn_with_log( f"Some rows (n={len(existing)}) are already " "present in the database. The storage is " - f"configured to {self._upsert} the existing " + f"configured to {self.upsert} the existing " f"elements. The new rows (n={len(new)}) will be " "appended. This warning is shown because normally " "all of the elements should be updated." ) - if self._upsert == "update": + if self.upsert == "update": update_stmts = _generate_update_statements( table, index_col, existing ) -- 2.52.0 From cf35c38a62811fdda23d9fd3677c8c6b89cc7366 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:43:14 +0100 Subject: [PATCH 16/24] chore: update tests for HDF5FeatureStorage --- junifer/storage/tests/test_hdf5.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/storage/tests/test_hdf5.py b/junifer/storage/tests/test_hdf5.py index 0685d410e..9cb4c0d76 100644 --- a/junifer/storage/tests/test_hdf5.py +++ b/junifer/storage/tests/test_hdf5.py @@ -24,13 +24,13 @@ from junifer.storage.utils import ( def test_get_valid_inputs() -> None: """Test valid inputs.""" storage = HDF5FeatureStorage(uri="/tmp") - assert storage.get_valid_inputs() == [ + assert set(storage.get_valid_inputs()) == { "matrix", "vector", "timeseries", "scalar_table", "timeseries_2d", - ] + } def test_single_output(tmp_path: Path) -> None: @@ -285,7 +285,7 @@ def test_read_df_params_error(tmp_path: Path) -> None: with pytest.raises(ValueError, match="At least one of"): storage.read_df() - with pytest.raises(ValueError, match="Feature MD5"): + with pytest.raises(RuntimeError, match="Feature MD5"): storage.read_df(feature_md5="md5") -- 2.52.0 From cee85d8206d1a54e28e0ad3f10b5058980cf1997 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:43:28 +0100 Subject: [PATCH 17/24] chore: update tests for SQLiteFeatureStorage --- junifer/storage/tests/test_sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/storage/tests/test_sqlite.py b/junifer/storage/tests/test_sqlite.py index 5f496733d..ce205c08c 100644 --- a/junifer/storage/tests/test_sqlite.py +++ b/junifer/storage/tests/test_sqlite.py @@ -111,7 +111,7 @@ def test_get_engine_multi_output(tmp_path: Path) -> None: storage = SQLiteFeatureStorage( uri=uri, single_output=False, upsert="ignore" ) - with pytest.raises(ValueError, match="element must be specified"): + with pytest.raises(ValueError, match="`element` cannot be None"): storage.get_engine() -- 2.52.0 From a6da354c4813119e4b765bf5784b530321352c47 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:43:59 +0100 Subject: [PATCH 18/24] chore: update tests for BaseFeatureStorage --- junifer/storage/tests/test_storage_base.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/junifer/storage/tests/test_storage_base.py b/junifer/storage/tests/test_storage_base.py index 5463909bc..11083469d 100644 --- a/junifer/storage/tests/test_storage_base.py +++ b/junifer/storage/tests/test_storage_base.py @@ -12,10 +12,7 @@ from junifer.storage.base import BaseFeatureStorage def test_BaseFeatureStorage_abstractness() -> None: """Test BaseFeatureStorage is abstract base class.""" with pytest.raises(TypeError, match=r"abstract"): - BaseFeatureStorage( - uri="/tmp", - storage_types=["matrix"], # type: ignore - ) + BaseFeatureStorage(uri="/tmp") def test_BaseFeatureStorage() -> None: @@ -25,17 +22,14 @@ def test_BaseFeatureStorage() -> None: class MyFeatureStorage(BaseFeatureStorage): """Implement concrete class.""" + _STORAGE_TYPES = ["matrix", "vector", "timeseries", "timeseries_2d"] + def __init__(self, uri, single_output=True): - storage_types = ["matrix", "vector", "timeseries", "timeseries_2d"] super().__init__( uri=uri, - storage_types=storage_types, single_output=single_output, ) - def get_valid_inputs(self): - return ["matrix", "vector", "timeseries", "timeseries_2d"] - def list_features(self): super().list_features() @@ -107,4 +101,4 @@ def test_BaseFeatureStorage() -> None: with pytest.raises(ValueError): st.store(kind="lego", meta=meta) - assert st.uri == "/tmp" + assert str(st.uri) == "/tmp" -- 2.52.0 From 2dc2ac9804a9f3e0e66267bc614da7241964f421 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:44:26 +0100 Subject: [PATCH 19/24] chore: update typing annotations for storage utils --- junifer/storage/utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/junifer/storage/utils.py b/junifer/storage/utils.py index 743a39d56..b59b7eea4 100644 --- a/junifer/storage/utils.py +++ b/junifer/storage/utils.py @@ -6,7 +6,7 @@ import hashlib import json -from collections.abc import Iterable +from collections.abc import Sequence from importlib.metadata import PackageNotFoundError, version import numpy as np @@ -268,8 +268,8 @@ def store_timeseries_2d_checks( def matrix_to_vector( data: np.ndarray, - col_names: Iterable[str], - row_names: Iterable[str], + col_names: Sequence[str], + row_names: Sequence[str], matrix_kind: str, diagonal: bool, ) -> tuple[np.ndarray, list[str]]: @@ -279,9 +279,9 @@ def matrix_to_vector( ---------- data : 2D / 3D numpy.ndarray The matrix / tensor data to store / read. - col_names : list or tuple of str + col_names : list-like of str The column labels. - row_names : list or tuple of str + row_names : list-like of str The row labels. matrix_kind : str The kind of matrix: @@ -326,8 +326,8 @@ def matrix_to_vector( def timeseries2d_to_vector( data: np.ndarray, - col_names: Iterable[str], - row_names: Iterable[str], + col_names: Sequence[str], + row_names: Sequence[str], ) -> tuple[np.ndarray, list[str]]: """Convert matrix to vector based on parameters. @@ -335,9 +335,9 @@ def timeseries2d_to_vector( ---------- data : 2D / 3D numpy.ndarray The matrix / tensor data to store / read. - col_names : list or tuple of str + col_names : list-like of str The column labels. - row_names : list or tuple of str + row_names : list-like of str The row labels. Returns -- 2.52.0 From e3cb9d93644521224c42555a9359a0b5e2e530fd Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:45:00 +0100 Subject: [PATCH 20/24] chore: remove type ignore pragmas for storage utils --- junifer/storage/utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/junifer/storage/utils.py b/junifer/storage/utils.py index b59b7eea4..747ee6d78 100644 --- a/junifer/storage/utils.py +++ b/junifer/storage/utils.py @@ -208,13 +208,13 @@ def store_matrix_checks( klass=ValueError, ) # Row label validation - if row_names_len != data_shape[0]: # type: ignore + if row_names_len != data_shape[0]: raise_error( msg="Number of row names does not match number of rows", klass=ValueError, ) # Column label validation - if col_names_len != data_shape[1]: # type: ignore + if col_names_len != data_shape[1]: raise_error( msg="Number of column names does not match number of columns", klass=ValueError, @@ -253,13 +253,13 @@ def store_timeseries_2d_checks( ) # Row label validation - if row_names_len != data_shape[1]: # type: ignore + if row_names_len != data_shape[1]: raise_error( msg="Number of row names does not match number of rows", klass=ValueError, ) # Column label validation - if col_names_len != data_shape[2]: # type: ignore + if col_names_len != data_shape[2]: raise_error( msg="Number of column names does not match number of columns", klass=ValueError, @@ -317,7 +317,7 @@ def matrix_to_vector( flat_data = data[data_idx] # Generate flat 1D row X column names columns = [ - f"{row_names[i]}~{col_names[j]}" # type: ignore + f"{row_names[i]}~{col_names[j]}" for i, j in zip(data_idx[0], data_idx[1]) ] -- 2.52.0 From 4cae0daf06205a09888cfa8d7bd62a28bfa27f7f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 14:45:22 +0100 Subject: [PATCH 21/24] update: improve commentary and docstrings for storage utils --- junifer/storage/utils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/junifer/storage/utils.py b/junifer/storage/utils.py index 747ee6d78..0ab8be595 100644 --- a/junifer/storage/utils.py +++ b/junifer/storage/utils.py @@ -36,6 +36,11 @@ def get_dependency_version(dependency: str) -> str: str The version of the dependency. + Raises + ------ + PackageNotFoundError + If ``dependency`` is not found. + """ dep_version = "" try: @@ -141,6 +146,11 @@ def element_to_prefix(element: dict) -> str: str The element converted to prefix. + Raises + ------ + ValueError + If ``element`` is not a dict. + """ logger.debug(f"Converting element {element} to prefix.") prefix = "element" @@ -245,7 +255,7 @@ def store_timeseries_2d_checks( If the number of column names does not match the number of columns """ - # data validation + # Data validation if len(data_shape) != 3: raise_error( msg="Data must be a 3D array", -- 2.52.0 From 853e51961e13cd8486534d7de2b60df38495d909 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 15:01:32 +0100 Subject: [PATCH 22/24] docs: update understanding/storage.rst --- docs/understanding/storage.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/understanding/storage.rst b/docs/understanding/storage.rst index 714a02d5f..9f820479b 100644 --- a/docs/understanding/storage.rst +++ b/docs/understanding/storage.rst @@ -17,11 +17,11 @@ Storage is meant to be used inside the DataGrabber context but you can operate on them outside the context as long as the processed data is in the memory and the Python runtime has not garbage-collected it. -The :ref:`Markers ` are responsible for defining what *storage kind* -(``matrix``, ``vector``, ``timeseries``, ``scalar_table``) they support for -which :ref:`data type ` by overriding its ``get_output_type`` -method. The storage object in turn declares and provides implementation for -specific *storage kind*. For example, :class:`.SQLiteFeatureStorage` supports +The :ref:`Markers ` are responsible for mapping the input +:ref:`data type ` to its output :ref:`storage type ` +as shown :ref:`here `. +The storage object in turn declares and provides implementation for +specific *storage type*. For example, :class:`.SQLiteFeatureStorage` supports saving ``matrix``, ``vector`` and ``timeseries`` via ``store_matrix``, ``store_vector`` and ``store_timeseries`` methods respectively. @@ -57,6 +57,10 @@ Storage Types - A 2D square or non-square matrix of scalar values with column names - ``col_names`` - :meth:`.BaseFeatureStorage.store_timeseries` + * - ``timeseries_2d`` + - A 3D(2D+1D) square or non-square matrix of scalar values with column names across sessions + - ``col_names`` + - :meth:`.BaseFeatureStorage.store_timeseries_2d` * - ``scalar_table`` - | A 2D square or non-square matrix of scalar values with row name, column | name and row header column name @@ -83,4 +87,4 @@ Storage Interfaces * - :class:`.HDF5FeatureStorage` - ``.hdf5`` - HDF5 - - ``matrix``, ``vector``, ``timeseries``, ``scalar_table`` + - ``matrix``, ``vector``, ``timeseries``, ``timeseries_2d``, ``scalar_table`` -- 2.52.0 From 988401bbba3caedb5294bd8dbc8973e8e4df253e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 15:03:33 +0100 Subject: [PATCH 23/24] chore: add changelog 472.enh --- docs/changes/newsfragments/472.enh | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/newsfragments/472.enh diff --git a/docs/changes/newsfragments/472.enh b/docs/changes/newsfragments/472.enh new file mode 100644 index 000000000..52ce4c2c5 --- /dev/null +++ b/docs/changes/newsfragments/472.enh @@ -0,0 +1 @@ +Simplify ``Storage`` interface and implementations by `Synchon Mandal`_ -- 2.52.0 From eca0f41e4ef7f909c98e40367f75a5d090a6aaf0 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 27 Oct 2025 15:05:33 +0100 Subject: [PATCH 24/24] chore: lint --- junifer/storage/tests/test_storage_base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/junifer/storage/tests/test_storage_base.py b/junifer/storage/tests/test_storage_base.py index 11083469d..2fbe1b3b0 100644 --- a/junifer/storage/tests/test_storage_base.py +++ b/junifer/storage/tests/test_storage_base.py @@ -4,6 +4,9 @@ # Synchon Mandal # License: AGPL +from collections.abc import Sequence +from typing import ClassVar + import pytest from junifer.storage.base import BaseFeatureStorage @@ -22,7 +25,12 @@ def test_BaseFeatureStorage() -> None: class MyFeatureStorage(BaseFeatureStorage): """Implement concrete class.""" - _STORAGE_TYPES = ["matrix", "vector", "timeseries", "timeseries_2d"] + _STORAGE_TYPES: ClassVar[Sequence[str]] = [ + "matrix", + "vector", + "timeseries", + "timeseries_2d", + ] def __init__(self, uri, single_output=True): super().__init__( -- 2.52.0