[ENH]: Add read() method for storage-like objects #236
8 changed files with 175 additions and 13 deletions
1
docs/changes/newsfragments/236.change
Normal file
1
docs/changes/newsfragments/236.change
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add :meth:`.BaseFeatureStorage.read` method for storage-like objects by `Synchon Mandal`_
|
||||
1
docs/changes/newsfragments/236.enh
Normal file
1
docs/changes/newsfragments/236.enh
Normal file
|
|
@ -0,0 +1 @@
|
|||
Adapt :meth:`.HDF5FeatureStorage.read_df` due to addition of :meth:`.HDF5FeatureStorage.read` by `Synchon Mandal`_
|
||||
|
|
@ -104,6 +104,34 @@ class BaseFeatureStorage(ABC):
|
|||
klass=NotImplementedError,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def read(
|
||||
self,
|
||||
feature_name: Optional[str] = None,
|
||||
feature_md5: Optional[str] = None,
|
||||
) -> Dict[
|
||||
str, Union[str, List[Union[int, str, Dict[str, str]]], np.ndarray]
|
||||
]:
|
||||
"""Read stored feature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
feature_name : str, optional
|
||||
Name of the feature to read (default None).
|
||||
feature_md5 : str, optional
|
||||
MD5 hash of the feature to read (default None).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The stored feature as a dictionary.
|
||||
|
||||
"""
|
||||
raise_error(
|
||||
msg="Concrete classes need to implement read().",
|
||||
klass=NotImplementedError,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def read_df(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -328,15 +328,14 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
|||
|
||||
return data
|
||||
|
||||
def read_df(
|
||||
def read(
|
||||
self,
|
||||
feature_name: Optional[str] = None,
|
||||
feature_md5: Optional[bool] = None,
|
||||
) -> pd.DataFrame:
|
||||
"""Read feature into a pandas.DataFrame.
|
||||
|
||||
Either one of ``feature_name`` or ``feature_md5`` needs to be
|
||||
specified.
|
||||
feature_md5: Optional[str] = None,
|
||||
) -> Dict[
|
||||
str, Union[str, List[Union[int, str, Dict[str, str]]], np.ndarray]
|
||||
]:
|
||||
"""Read stored feature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -347,8 +346,8 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
|||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
The features as a dataframe.
|
||||
dict
|
||||
The stored feature as a dictionary.
|
||||
|
||||
Raises
|
||||
------
|
||||
|
|
@ -427,10 +426,46 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
|||
title=md5,
|
||||
slash="ignore",
|
||||
)
|
||||
return hdf_data
|
||||
|
||||
def read_df(
|
||||
self,
|
||||
feature_name: Optional[str] = None,
|
||||
feature_md5: Optional[str] = None,
|
||||
) -> pd.DataFrame:
|
||||
"""Read feature into a pandas.DataFrame.
|
||||
|
||||
Either one of ``feature_name`` or ``feature_md5`` needs to be
|
||||
specified.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
feature_name : str, optional
|
||||
Name of the feature to read (default None).
|
||||
feature_md5 : str, optional
|
||||
MD5 hash of the feature to read (default None).
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
The features as a dataframe.
|
||||
|
||||
Raises
|
||||
------
|
||||
IOError
|
||||
If HDF5 file does not exist.
|
||||
|
||||
"""
|
||||
hdf_data = self.read(
|
||||
feature_name=feature_name, feature_md5=feature_md5
|
||||
)
|
||||
reshaped_data = hdf_data["data"]
|
||||
|
||||
# Generate index for the data
|
||||
logger.debug(f"Generating pandas.MultiIndex for {md5} ...")
|
||||
logger.debug(
|
||||
"Generating pandas.MultiIndex for feature "
|
||||
f"{feature_name or feature_md5} ..."
|
||||
)
|
||||
|
||||
if hdf_data["kind"] == "matrix":
|
||||
# Set index for element
|
||||
|
|
@ -477,17 +512,26 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
|||
idx_df = pd.DataFrame(data=element_idx) # type: ignore
|
||||
# Create multiindex from dataframe
|
||||
hdf_data_idx = pd.MultiIndex.from_frame(df=idx_df)
|
||||
logger.debug(f"Generated pandas.MultiIndex for {md5} ...")
|
||||
logger.debug(
|
||||
"Generated pandas.MultiIndex for feature "
|
||||
f"{feature_name or feature_md5} ..."
|
||||
)
|
||||
|
||||
# Convert to DataFrame
|
||||
logger.debug(f"Converting HDF5 data for {md5} to pandas.DataFrame ...")
|
||||
logger.debug(
|
||||
"Converting HDF5 data for feature "
|
||||
f"{feature_name or feature_md5} to pandas.DataFrame ..."
|
||||
)
|
||||
df = pd.DataFrame(
|
||||
data=reshaped_data,
|
||||
index=hdf_data_idx,
|
||||
columns=columns, # type: ignore
|
||||
dtype=reshaped_data.dtype,
|
||||
)
|
||||
logger.debug(f"Converted HDF5 data for {md5} to pandas.DataFrame ...")
|
||||
logger.debug(
|
||||
"Converted HDF5 data for feature "
|
||||
f"{feature_name or feature_md5} to pandas.DataFrame ..."
|
||||
)
|
||||
|
||||
return df
|
||||
|
||||
|
|
|
|||
|
|
@ -235,6 +235,38 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
|
|||
out[md5][k] = json.loads(v)
|
||||
|
Maybe ask them to open an issue? Maybe ask them to open an issue?
we have a link on the doc that can be used for that. we have a link on the doc that can be used for that.
Are you referring to the Matrix channel link? Are you referring to the Matrix channel link?
https://juaml.github.io/junifer/main/help.html
|
||||
return out
|
||||
|
||||
def read(
|
||||
self,
|
||||
feature_name: Optional[str] = None,
|
||||
feature_md5: Optional[str] = None,
|
||||
) -> Dict[
|
||||
str, Union[str, List[Union[int, str, Dict[str, str]]], np.ndarray]
|
||||
]:
|
||||
"""Read stored feature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
feature_name : str, optional
|
||||
Name of the feature to read (default None).
|
||||
feature_md5 : str, optional
|
||||
MD5 hash of the feature to read (default None).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The stored feature as a dictionary.
|
||||
|
||||
"""
|
||||
raise_error(
|
||||
msg=(
|
||||
"read() for SQLiteFeatureStorage 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)."
|
||||
),
|
||||
klass=NotImplementedError,
|
||||
)
|
||||
|
||||
def read_df(
|
||||
self,
|
||||
feature_name: Optional[str] = None,
|
||||
|
|
|
|||
|
|
@ -220,6 +220,37 @@ def test_store_metadata_ignore_duplicate(tmp_path: Path) -> None:
|
|||
assert len(features) == 1
|
||||
|
||||
|
||||
def test_read(tmp_path: Path) -> None:
|
||||
"""Test read.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tmp_path : pathlib.Path
|
||||
The path to the test directory.
|
||||
|
||||
"""
|
||||
uri = tmp_path / "test_read.hdf5"
|
||||
storage = HDF5FeatureStorage(uri=uri)
|
||||
# Data to store
|
||||
to_store_data = {
|
||||
"meta": {
|
||||
"element": {"subject": "test"},
|
||||
"dependencies": ["numpy"],
|
||||
"marker": {"name": "test"},
|
||||
"type": "BOLD",
|
||||
},
|
||||
"data": np.array([[1, 10]]),
|
||||
"col_names": ["f1", "f2"],
|
||||
}
|
||||
# Store data
|
||||
storage.store(kind="vector", **to_store_data)
|
||||
# Read and check
|
||||
stored_data = storage.read(feature_name="BOLD_test")
|
||||
assert ["column_headers", "data", "element", "kind"] == list(
|
||||
stored_data.keys()
|
||||
)
|
||||
|
||||
|
||||
def test_read_df_params_error(tmp_path: Path) -> None:
|
||||
"""Test parameter validation errors for read_df.
|
||||
|
||||
|
|
|
|||
|
|
@ -253,6 +253,22 @@ def test_upsert_invalid_option(tmp_path: Path) -> None:
|
|||
SQLiteFeatureStorage(uri=uri, upsert="wrong")
|
||||
|
||||
|
||||
def test_read(tmp_path: Path) -> None:
|
||||
"""Test reading of stored feature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tmp_path : pathlib.Path
|
||||
The path to the test directory.
|
||||
|
||||
"""
|
||||
uri = tmp_path / "test_read.sqlite"
|
||||
storage = SQLiteFeatureStorage(uri=uri, upsert="ignore")
|
||||
# Check error
|
||||
with pytest.raises(NotImplementedError):
|
||||
storage.read()
|
||||
|
||||
|
||||
# TODO: can the tests be separated?
|
||||
def test_store_df_and_read_df(tmp_path: Path) -> None:
|
||||
"""Test storing dataframe and reading of stored table into dataframe.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ def test_BaseFeatureStorage() -> None:
|
|||
def list_features(self):
|
||||
super().list_features()
|
||||
|
||||
def read(self, feature_name=None, feature_md5=None):
|
||||
super().read(
|
||||
feature_name=feature_name,
|
||||
feature_md5=feature_md5,
|
||||
)
|
||||
|
||||
def read_df(self, feature_name=None, feature_md5=None):
|
||||
super().read_df(
|
||||
feature_name=feature_name,
|
||||
|
|
@ -66,6 +72,9 @@ def test_BaseFeatureStorage() -> None:
|
|||
with pytest.raises(NotImplementedError):
|
||||
st.list_features()
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
st.read(None)
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
st.read_df(None)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue
Can you rewrite this message so that users are hinted to contact the junifer devs if they need this feature? Something like that it is with the following: "This feature is not currently planned to be implemented in the near future. If you need such functionality, contact junifer developers (LINK TO HOW TO CONTACT)"