[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,
|
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
|
@abstractmethod
|
||||||
def read_df(
|
def read_df(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -328,15 +328,14 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def read_df(
|
def read(
|
||||||
self,
|
self,
|
||||||
feature_name: Optional[str] = None,
|
feature_name: Optional[str] = None,
|
||||||
feature_md5: Optional[bool] = None,
|
feature_md5: Optional[str] = None,
|
||||||
) -> pd.DataFrame:
|
) -> Dict[
|
||||||
"""Read feature into a pandas.DataFrame.
|
str, Union[str, List[Union[int, str, Dict[str, str]]], np.ndarray]
|
||||||
|
]:
|
||||||
Either one of ``feature_name`` or ``feature_md5`` needs to be
|
"""Read stored feature.
|
||||||
specified.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
@ -347,8 +346,8 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
pandas.DataFrame
|
dict
|
||||||
The features as a dataframe.
|
The stored feature as a dictionary.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
|
|
@ -427,10 +426,46 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
||||||
title=md5,
|
title=md5,
|
||||||
slash="ignore",
|
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"]
|
reshaped_data = hdf_data["data"]
|
||||||
|
|
||||||
# Generate index for the 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":
|
if hdf_data["kind"] == "matrix":
|
||||||
# Set index for element
|
# Set index for element
|
||||||
|
|
@ -477,17 +512,26 @@ class HDF5FeatureStorage(BaseFeatureStorage):
|
||||||
idx_df = pd.DataFrame(data=element_idx) # type: ignore
|
idx_df = pd.DataFrame(data=element_idx) # type: ignore
|
||||||
# Create multiindex from dataframe
|
# Create multiindex from dataframe
|
||||||
hdf_data_idx = pd.MultiIndex.from_frame(df=idx_df)
|
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
|
# 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(
|
df = pd.DataFrame(
|
||||||
data=reshaped_data,
|
data=reshaped_data,
|
||||||
index=hdf_data_idx,
|
index=hdf_data_idx,
|
||||||
columns=columns, # type: ignore
|
columns=columns, # type: ignore
|
||||||
dtype=reshaped_data.dtype,
|
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
|
return df
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,38 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
|
||||||
out[md5][k] = json.loads(v)
|
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
|
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(
|
def read_df(
|
||||||
self,
|
self,
|
||||||
feature_name: Optional[str] = None,
|
feature_name: Optional[str] = None,
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,37 @@ def test_store_metadata_ignore_duplicate(tmp_path: Path) -> None:
|
||||||
assert len(features) == 1
|
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:
|
def test_read_df_params_error(tmp_path: Path) -> None:
|
||||||
"""Test parameter validation errors for read_df.
|
"""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")
|
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?
|
# TODO: can the tests be separated?
|
||||||
def test_store_df_and_read_df(tmp_path: Path) -> None:
|
def test_store_df_and_read_df(tmp_path: Path) -> None:
|
||||||
"""Test storing dataframe and reading of stored table into dataframe.
|
"""Test storing dataframe and reading of stored table into dataframe.
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ def test_BaseFeatureStorage() -> None:
|
||||||
def list_features(self):
|
def list_features(self):
|
||||||
super().list_features()
|
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):
|
def read_df(self, feature_name=None, feature_md5=None):
|
||||||
super().read_df(
|
super().read_df(
|
||||||
feature_name=feature_name,
|
feature_name=feature_name,
|
||||||
|
|
@ -66,6 +72,9 @@ def test_BaseFeatureStorage() -> None:
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
st.list_features()
|
st.list_features()
|
||||||
|
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
st.read(None)
|
||||||
|
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
st.read_df(None)
|
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)"