# Copyright (c) 2024 Mira Geoscience Ltd.
#
# This file is part of geoh5py.
#
# geoh5py is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# geoh5py is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with geoh5py. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING
from uuid import UUID, uuid4
from warnings import warn
from ..data import Data, DataAssociationEnum
from ..shared.utils import (
find_unique_name,
remove_duplicates_in_list,
)
from .property_group_table import PropertyGroupTable
from .property_group_type import GroupTypeEnum
if TYPE_CHECKING: # pragma: no cover
from ..objects import ObjectBase
[docs]
class PropertyGroup:
"""
Property group listing data children of an object.
This group is not registered to the workspace and only visible to the parent object.
:param parent: Parent object.
:param association: Association of the data.
:param allow_delete: Allow deleting the group.
:param name: Name of the group.
:param on_file: Property group is on file.
:param uid: Unique identifier.
:param property_group_type: Type of property group.
:param properties: List of data or unique identifiers for the data.
"""
_attribute_map = {
"Association": "association",
"Group Name": "name",
"ID": "uid",
"Properties": "properties",
"Property Group Type": "property_group_type",
}
def __init__( # pylint: disable=too-many-arguments
self,
parent: ObjectBase,
*,
association: str | DataAssociationEnum | None = None,
allow_delete: bool = True,
name: str = "Property Group",
on_file: bool = False,
uid: UUID | None = None,
property_group_type: GroupTypeEnum | str = GroupTypeEnum.SIMPLE,
properties: Sequence[UUID | Data | str] | None = None,
**_,
):
self._parent: ObjectBase = self._validate_parent(parent)
self._property_group_type = self._validate_group_type(property_group_type)
self.allow_delete = allow_delete
self.name = name
self.on_file = on_file
self.uid = uid or uuid4()
properties_list = self._initialize_properties(properties)
self._association = self._validate_association(association, properties_list)
self._properties = self._validate_properties(properties_list)
self.parent.add_children([self])
self.parent.workspace.register(self)
def _initialize_properties(
self, properties: str | UUID | Data | Sequence[UUID | str | Data] | None
) -> list[Data] | None:
"""
Initialize the properties list.
:param properties: List of Data entities to validate.
:return: List of unique identifiers for the Data entities.
"""
if properties is None:
return None
if not isinstance(properties, Iterable):
properties = [properties]
return [self.parent.reference_to_data(prop) for prop in properties]
@staticmethod
def _validate_association(
value: str | DataAssociationEnum | None, properties: list[Data] | None
) -> DataAssociationEnum:
"""
Verify that the association is valid, or infer it from the properties.
:param value: The association to validate.
:param properties: A list of properties to infer the association from.
"""
if properties is None and value is None:
raise ValueError(
"At least one of 'properties' or 'association' must be provided."
)
if value is None and properties is not None:
value = properties[0].association
if isinstance(value, str):
value = getattr(DataAssociationEnum, value.upper())
if not isinstance(value, DataAssociationEnum):
raise TypeError(f"Association must be one of type {DataAssociationEnum}")
return value
def _validate_data(self, data: Data | UUID | str) -> Data:
"""
Verify that the data is in the parent and has the same association as the group.
:param data: The data to verify.
It can be the name, the uuid or the data itself.
:return: The uuid of the data.
"""
data = self.parent.reference_to_data(data)
if self.association != data.association:
raise ValueError(
f"Data '{data.name}' association ({data.association}) "
f"does not match group association ({self.association})."
)
return data
@staticmethod
def _validate_group_type(value: str | GroupTypeEnum) -> GroupTypeEnum:
"""
Verify that the group type is a valid GroupTypeEnum.
:param value: The group type to validate.
:return: The validated group type.
"""
if isinstance(value, str):
try:
value = GroupTypeEnum(value)
except ValueError as error:
raise ValueError(
f"'Property group type' must be one of "
f"{', '.join(GroupTypeEnum.__members__)}. Provided {value}"
) from error
if not isinstance(value, GroupTypeEnum):
raise TypeError(
f"'Property group type' must be of type {GroupTypeEnum}, "
f"provided {type(value)}"
)
return value
@staticmethod
def _validate_parent(parent: ObjectBase) -> ObjectBase:
"""
Verify that the parent is valid.
:param parent: The parent Object to validate.
:return: The parent Object.
"""
# define the parent
if not hasattr(parent, "_property_groups"):
raise TypeError(f"Parent {parent} must have a 'property_groups' attribute")
return parent
def _validate_properties(
self, data_list: Sequence[str | UUID | Data] | None
) -> list[UUID] | None:
"""
Validate the properties list.
:param data_list: List of Data entities to validate.
:return: List of unique identifiers for the Data entities.
"""
if not data_list:
return None
data_list_ = remove_duplicates_in_list(
[self._validate_data(uid) for uid in data_list]
)
self._property_group_type.verify(data_list_)
return [data.uid for data in data_list_]
[docs]
def add_properties(self, data: str | Data | Sequence[str | Data | UUID] | UUID):
"""
Add data to properties.
:param data: Data to add to the group.
It can be the name, the uuid or the data itself in a list or alone.
"""
if self._property_group_type.no_modify:
raise ValueError(
f"Cannot add properties to '{self._property_group_type}' property group type."
)
if isinstance(data, (str, UUID, Data)):
data = [data]
properties = self._validate_properties(
data if self.properties is None else self.properties + list(data)
)
if properties:
self._properties = remove_duplicates_in_list(properties)
self.parent.workspace.add_or_update_property_group(self)
@property
def allow_delete(self) -> bool:
"""
Allow deleting the group
"""
return self._allow_delete
@allow_delete.setter
def allow_delete(self, value: bool):
if not isinstance(value, bool):
raise TypeError("allow_delete must be a boolean")
self._allow_delete = value
@property
def association(self) -> DataAssociationEnum:
"""
The association of the data.
"""
return self._association
@property
def attribute_map(self) -> dict:
"""
Attribute names mapping between geoh5 and geoh5py
"""
return self._attribute_map
@property
def collect_values(self) -> list | None:
"""
The values of the properties in the group.
"""
warn(
"PropertyGroup.collect_values is deprecated, use PropertyGroup.table instead.",
DeprecationWarning,
)
if self._properties is None:
return None
return [self._parent.get_data(data)[0].values for data in self._properties]
@property
def name(self) -> str:
"""
Name of the group
"""
return self._name
@name.setter
def name(self, new_name: str):
if not isinstance(new_name, str):
raise TypeError("Name must be a string")
if getattr(self.parent, "_property_groups", None):
original_name = new_name
property_groups = (
self.parent.property_groups if self.parent.property_groups else []
)
new_name = find_unique_name(
new_name,
[prop_group.name for prop_group in property_groups],
)
if original_name != new_name:
warn(
f"Name '{original_name}' already exists in the parent object. "
f"Renamed to '{new_name}'."
)
self._name = new_name
@property
def on_file(self):
"""
Property group is on geoh5 file.
"""
return self._on_file
@on_file.setter
def on_file(self, value: bool):
if not isinstance(value, bool):
raise TypeError("Attribute 'on_file' must be a boolean.")
self._on_file = value
@property
def parent(self) -> ObjectBase:
"""
The parent of the PropertyGroup.
"""
return self._parent
@property
def properties(self) -> list[UUID] | None:
"""
List of unique identifiers for the :obj:`~geoh5py.data.data.Data`
contained in the property group.
"""
return self._properties
@property
def properties_name(self) -> list[str] | None:
"""
List of names of the properties`
"""
if self._properties is None:
return None
names: list[str] = []
for uid in self._properties:
data = self.parent.get_data(uid)[0]
name = str(data.uid) if data.name is None else data.name
names.append(find_unique_name(name, names))
return names
@property
def property_group_type(self) -> GroupTypeEnum:
"""
Type of property group.
"""
return self._property_group_type
[docs]
def remove_properties(self, data: str | Data | Sequence[str | Data | UUID] | UUID):
"""
Remove data from the properties.
:param data: Data to remove from the group.
It can be the name, the uuid or the data itself in a list or alone.
"""
if self._property_group_type.no_modify:
raise ValueError(
f"Cannot remove properties from '{self._property_group_type}' property group type."
)
if self.properties is None:
return
if isinstance(data, (str, UUID, Data)):
data = [data]
properties = self.properties
for elem in data:
elem = self.parent.reference_to_data(elem).uid
if elem in properties:
properties.remove(elem)
self._properties = self._validate_properties(properties)
if not self._properties:
self.parent.workspace.remove_entity(self)
return
self.parent.workspace.add_or_update_property_group(self)
@property
def table(self) -> PropertyGroupTable:
"""
Create an object to access the data of the property group.
"""
return PropertyGroupTable(self)
@property
def uid(self) -> UUID:
"""
Unique identifier
"""
return self._uid
@uid.setter
def uid(self, uid: str | UUID):
if isinstance(uid, str):
uid = UUID(uid)
if not isinstance(uid, UUID):
raise TypeError(f"Could not convert input uid {uid} to type UUID")
self._uid = uid