Source code for geoh5py.ui_json.forms

#  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 enum import Enum
from pathlib import Path
from typing import Annotated, Any
from uuid import UUID

import numpy as np
from pydantic import (
    BaseModel,
    ConfigDict,
    field_serializer,
    field_validator,
    model_validator,
)
from pydantic.alias_generators import to_camel
from pydantic.functional_validators import BeforeValidator

from geoh5py.groups import Group
from geoh5py.objects import ObjectBase
from geoh5py.shared.validators import (
    empty_string_to_uid,
    to_class,
    to_list,
    to_path,
    to_uuid,
)


[docs] class DependencyType(str, Enum): ENABLED = "enabled" DISABLED = "disabled"
[docs] class BaseForm(BaseModel): """ Base class for uijson forms :param label: Label for ui element. :param value: The parameter's value. :param optional: If True, ui element is rendered with a checkbox to control the enabled state. :param enabled: If False, ui element is rendered grey and value will be written to file as None. :param main: Controls whether ui element will render in the general parameters tab (True) or optional parameters (False). :param tooltip: String rendered on hover over ui element. :param group: Grouped ui elements will be rendered within a box labelled with the group name. :param group_optional: If True, ui group is rendered with a checkbox that controls the enabled state of all of the groups members :param dependency: Name of parameter that controls the enabled or visible state of the ui element. :param dependency_type: Controls whether the ui element is enabled or visible when the dependency is enabled if optional or True if a bool type. :param group_dependency: Name of parameter that controls the enabled or visible state of the ui group. :param group_dependency_type: Controls whether the ui group is enabled or visible when the group dependency is enabled if optional or True if a bool type. """ model_config = ConfigDict( extra="allow", frozen=True, populate_by_name=True, loc_by_alias=True, alias_generator=to_camel, ) label: str value: Any optional: bool = False enabled: bool = True main: bool = True tooltip: str = "" group: str = "" group_optional: bool = False dependency: str = "" dependency_type: DependencyType = DependencyType.ENABLED group_dependency: str = "" group_dependency_type: DependencyType = DependencyType.ENABLED @property def json_string(self): return self.model_dump_json(exclude_unset=True, by_alias=True)
[docs] def flatten(self): """Returns the data for the form.""" return self.value
[docs] def validate_data(self, params: dict[str, Any]): """Validate the form data."""
[docs] class StringForm(BaseForm): """ String valued uijson form. """ value: str = ""
[docs] class BoolForm(BaseForm): """ Boolean valued uijson form. """ value: bool = True
[docs] class IntegerForm(BaseForm): """ Integer valued uijson form. """ value: int = 1 min: float = -np.inf max: float = np.inf
[docs] class FloatForm(BaseForm): """ Float valued uijson form. """ value: float = 1.0 min: float = -np.inf max: float = np.inf precision: int = 2 line_edit: bool = True
[docs] class ChoiceForm(BaseForm): """ Choice list uijson form. """ value: list[str] choice_list: list[str] multi_select: bool = False
[docs] @field_validator("value", mode="before") @classmethod def to_list(cls, value): if not isinstance(value, list): value = [value] return value
[docs] @field_serializer("value", when_used="json") def string_if_single(self, value): if len(value) == 1: value = value[0] return value
[docs] @model_validator(mode="after") def valid_choice(self): bad_choices = [] for val in self.value: if val not in self.choice_list: bad_choices.append(val) if bad_choices: raise ValueError(f"Provided value(s): {bad_choices} are not valid choices.") return self
PathList = Annotated[ list[Path], BeforeValidator(to_path), BeforeValidator(to_list), ]
[docs] class FileForm(BaseForm): """ File path uijson form """ value: PathList file_description: list[str] file_type: list[str] file_multi: bool = False
[docs] @field_serializer("value", when_used="json") def to_string(self, value): return ";".join([str(path) for path in value])
[docs] @field_validator("value") @classmethod def valid_file(cls, value): bad_paths = [] for path in value: if not path.exists(): bad_paths.append(path) if any(bad_paths): raise ValueError(f"Provided path(s) {bad_paths} does not exist.") return value
[docs] @model_validator(mode="after") def same_length(self): if len(self.file_description) != len(self.file_type): raise ValueError("File description and type lists must be the same length.") return self
[docs] @model_validator(mode="before") @classmethod def value_file_type(cls, data): bad_paths = [] for path in data["value"].split(";"): if Path(path).suffix[1:] not in data["file_type"]: bad_paths.append(path) if any(bad_paths): raise ValueError(f"Provided paths {bad_paths} have invalid extensions.") return data
MeshTypes = Annotated[ list[type[ObjectBase] | type[Group]], BeforeValidator(to_class), BeforeValidator(to_uuid), BeforeValidator(to_list), ]
[docs] class ObjectForm(BaseForm): """ Geoh5py object uijson form. """ value: UUID = UUID("00000000-0000-0000-0000-000000000000") mesh_type: MeshTypes _empty_string_to_uid = field_validator("value", mode="before")(empty_string_to_uid)
[docs] class Association(str, Enum): """ Geoh5py object association types. """ VERTEX = "Vertex" CELL = "Cell" FACE = "Face"
[docs] class DataType(str, Enum): """ Geoh5py data types. """ INTEGER = "Integer" FLOAT = "Float" BOOLEAN = "Boolean" REFERENCED = "Referenced" VECTOR = "Vector" DATETIME = "DateTime" GEOMETRIC = "Geometric" TEXT = "Text"
[docs] class DataForm(BaseForm): """ Geoh5py data uijson form. """ value: UUID | float | int parent: str association: Association | list[Association] data_type: DataType | list[DataType] is_value: bool = False property: UUID = UUID("00000000-0000-0000-0000-000000000000") min: float = -np.inf max: float = np.inf precision: int = 2
[docs] @field_validator("property", mode="before") @classmethod def empty_string_to_uid(cls, val): if val == "": return UUID("00000000-0000-0000-0000-000000000000") return val
[docs] @model_validator(mode="after") def value_if_is_value(self): if ( "is_value" in self.model_fields_set # pylint: disable=unsupported-membership-test and self.is_value ): if isinstance(self.value, UUID): raise ValueError("Value must be numeric if is_value is True.") return self
[docs] @model_validator(mode="after") def property_if_not_is_value(self): if ( "is_value" in self.model_fields_set # pylint: disable=unsupported-membership-test and "property" not in self.model_fields_set # pylint: disable=unsupported-membership-test ): raise ValueError("A property must be provided if is_value is used.") return self
[docs] def flatten(self): """Returns the data for the form.""" if ( "is_value" in self.model_fields_set # pylint: disable=unsupported-membership-test and not self.is_value ): return self.property return self.value