Implemented metadata append, rename, delete, and update operations on the hdf5 manager object and refactored metadata update script based on yaml file to use said operations.

This commit is contained in:
2024-09-30 16:32:39 +02:00
parent afe31288a0
commit 6f5d4adcee
2 changed files with 259 additions and 43 deletions

View File

@ -16,6 +16,7 @@ import h5py
import yaml
import json
import copy
class HDF5DataOpsManager():
def __init__(self, file_path, mode = 'r+') -> None:
@ -24,14 +25,11 @@ class HDF5DataOpsManager():
self.mode = mode
self.file_path = file_path
self.file_obj = None
self._open_file()
#self._open_file()
self.list_of_datasets = []
# Define private methods
def _open_file(self):
if self.file_obj is None:
self.file_obj = h5py.File(self.file_path, self.mode)
# Define private methods
def _collect_dataset_names(self, name, obj, list_of_datasets):
if isinstance(obj, h5py.Dataset):
@ -39,6 +37,10 @@ class HDF5DataOpsManager():
# Define public methods
def open_file(self):
if self.file_obj is None:
self.file_obj = h5py.File(self.file_path, self.mode)
def close_file(self):
if self.file_obj:
self.file_obj.flush() # Ensure all data is written to disk
@ -73,7 +75,7 @@ class HDF5DataOpsManager():
try:
return pd.DataFrame(data)
except ValueError as exp:
logging.error(f"Failed to convert dataset '{dataset_name}' to DataFrame: {exp}")
logging.error(f"Failed to convert dataset '{dataset_name}' to DataFrame: {exp}. Instead, dataset will be returned as Numpy array.")
return data # 'data' is a NumPy array here
def append_dataset(self,dataset_dict, group_name):
@ -90,21 +92,196 @@ class HDF5DataOpsManager():
self.file_obj[group_name].create_dataset(dataset_dict['name'], data=dataset_dict['data'])
self.file_obj[group_name][dataset_dict['name']].attrs.update(dataset_dict['attributes'])
def append_annotations(self, obj_name, annotation_dict):
""" appends annotations in the form of a dictionary to the obj (group or dataset) spefified by obj_name"""
def append_metadata(self, obj_name, annotation_dict):
"""
Appends metadata attributes to the specified object (obj_name) based on the provided annotation_dict.
This method ensures that the provided metadata attributes do not overwrite any existing ones. If an attribute already exists,
a ValueError is raised. The function supports storing scalar values (int, float, str) and compound values such as dictionaries
that are converted into NumPy structured arrays before being added to the metadata.
Parameters:
-----------
obj_name: str
Path to the target object (dataset or group) within the HDF5 file.
annotation_dict: dict
A dictionary where the keys represent new attribute names (strings), and the values can be:
- Scalars: int, float, or str.
- Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays.
Example of a compound value:
Example:
----------
annotation_dict = {
"relative_humidity": {
"value": 65,
"units": "percentage",
"range": "[0,100]",
"definition": "amount of water vapor present ..."
}
}
"""
if self.file_obj is None:
self.open_file()
# Create a copy of annotation_dict to avoid modifying the original
annotation_dict_copy = copy.deepcopy(annotation_dict)
#with h5py.File(self.file_path, mode='r+') as file_obj:
obj = self.file_obj[obj_name]
# Check if any attribute already exists
if any(key in obj.attrs for key in annotation_dict_copy.keys()):
raise ValueError("Make sure the provided (key, value) pairs are not existing metadata elements or attributes. To modify or delete existing attributes use .modify_annotation() or .delete_annotation()")
# Process the dictionary values and convert them to structured arrays if needed
for key, value in annotation_dict_copy.items():
if isinstance(value, dict):
# Convert dictionaries to NumPy structured arrays for complex attributes
annotation_dict_copy[key] = utils.convert_attrdict_to_np_structured_array(value)
# Update the object's attributes with the new metadata
obj.attrs.update(annotation_dict_copy)
def update_metadata(self, obj_name, annotation_dict):
"""
Updates the value of existing metadata attributes of the specified object (obj_name) based on the provided annotation_dict.
The function disregards non-existing attributes and suggests to use the append_metadata() method to include those in the metadata.
Parameters:
-----------
obj_name : str
Path to the target object (dataset or group) within the HDF5 file.
annotation_dict: dict
A dictionary where the keys represent existing attribute names (strings), and the values can be:
- Scalars: int, float, or str.
- Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays.
Example of a compound value:
Example:
----------
annotation_dict = {
"relative_humidity": {
"value": 65,
"units": "percentage",
"range": "[0,100]",
"definition": "amount of water vapor present ..."
}
}
"""
if self.file_obj is None:
self.open_file()
update_dict = {}
#with h5py.File(self.file_path, mode='r+') as file_obj:
obj = self.file_obj[obj_name]
for key, value in annotation_dict.items():
if key in obj.attrs:
if isinstance(value, dict):
update_dict[key] = utils.convert_attrdict_to_np_structured_array(value)
else:
update_dict[key] = value
else:
# Optionally, log or warn about non-existing keys being ignored.
print(f"Warning: Key '{key}' does not exist and will be ignored.")
obj.attrs.update(update_dict)
def delete_metadata(self, obj_name, annotation_dict):
"""
Deletes metadata attributes of the specified object (obj_name) based on the provided annotation_dict.
Parameters:
-----------
obj_name: str
Path to the target object (dataset or group) within the HDF5 file.
annotation_dict: dict
Dictionary where keys represent attribute names, and values should be dictionaries containing
{"delete": True} to mark them for deletion.
Example:
--------
annotation_dict = {"attr_to_be_deleted": {"delete": True}}
Behavior:
---------
- Deletes the specified attributes from the object's metadata if marked for deletion.
- Issues a warning if the attribute is not found or not marked for deletion.
"""
if self.file_obj is None:
self.open_file()
#with h5py.File(self.file_path, mode='r+') as file_obj:
obj = self.file_obj[obj_name]
for attr_key, value in annotation_dict.items():
if attr_key in obj.attrs:
if isinstance(value, dict) and value.get('delete', False):
obj.attrs.__delitem__(attr_key)
else:
msg = f"Warning: Value for key '{attr_key}' is not marked for deletion or is invalid."
print(msg)
else:
msg = f"Warning: Key '{attr_key}' does not exist in metadata."
print(msg)
def rename_metadata(self, obj_name, renaming_map):
"""
Renames metadata attributes of the specified object (obj_name) based on the provided renaming_map.
Parameters:
-----------
obj_name: str
Path to the target object (dataset or group) within the HDF5 file.
renaming_map: dict
A dictionary where keys are current attribute names (strings), and values are the new attribute names (strings or byte strings) to rename to.
Example:
--------
renaming_map = {
"old_attr_name": "new_attr_name",
"old_attr_2": "new_attr_2"
}
"""
#with h5py.File(self.file_path, mode='r+') as file_obj:
if self.file_obj is None:
self.open_file()
obj = self.file_obj[obj_name]
# Verify if attributes to append are all new
if any(new_attr_key in obj.attrs.keys() for new_attr_key in annotation_dict.keys()):
self.close_file()
raise ValueError("Make sure the provided key, value pairs are not existing metadata elements or attributes. To modify or delete existing attributes use .modify_annotation() or .delete_annotation()")
# Iterate over the renaming_map to process renaming
for old_attr, new_attr in renaming_map.items():
if old_attr in obj.attrs:
# Get the old attribute's value
attr_value = obj.attrs[old_attr]
for new_attr_key in annotation_dict.keys():
value = annotation_dict[new_attr_key]
if isinstance(value, dict):
annotation_dict[new_attr_key] = utils.convert_attrdict_to_np_structured_array(annotation_dict[new_attr_key])
obj.attrs.update(annotation_dict)
# Create a new attribute with the new name
obj.attrs.create(new_attr, data=attr_value)
# Delete the old attribute
obj.attrs.__delitem__(old_attr)
else:
# Skip if the old attribute doesn't exist
msg = f"Skipping: Attribute '{old_attr}' does not exist."
print(msg) # Optionally, replace with warnings.warn(msg)
self.close_file()
def get_metadata(self, obj_path):
""" Get file attributes from object at path = obj_path. For example,
@ -249,9 +426,9 @@ def construct_attributes_dict(attrs_obj):
#attr_dict[key][subattr] = make_dtype_yaml_compatible(value[subattr])
attr_dict[key] = utils.to_serializable_dtype(value)
else:
attr_dict[key] = {"rename_as" : key,
"value" : utils.to_serializable_dtype(value)
}
attr_dict[key] = utils.to_serializable_dtype(value) # {"rename_as" : key,
#"value" : utils.to_serializable_dtype(value)
#}
#if isinstance(value,str):
# value.replace('\\','\\\\')