Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5961372
Start deprecating InputFile
domfournier Mar 23, 2026
694b5b1
Merge branch 'develop' into GEOPY-2739
domfournier Mar 31, 2026
434493a
Start removing InputFile from mechanics
domfournier Mar 31, 2026
d05d6ce
Re-trigger
domfournier Mar 31, 2026
bebadf2
Update run functions
domfournier Apr 2, 2026
39f3306
Potential fix for pull request finding 'Unused import'
domfournier Apr 2, 2026
4276096
Remove test for drepcated Params class
domfournier Apr 2, 2026
e60ff45
Merge branch 'GEOPY-2739' of https://github.com/MiraGeoscience/geoapp…
domfournier Apr 2, 2026
30ba1c3
Remove more ref to TestParams
domfournier Apr 2, 2026
a5a0192
Re-lock on geoh5py branch
domfournier Apr 2, 2026
e60c41f
Apply suggestion from @Copilot
domfournier Apr 2, 2026
b11b6ed
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 2, 2026
efdf62a
Fixes from copilot
domfournier Apr 2, 2026
b192fab
Update geoapps_utils/base.py
domfournier Apr 2, 2026
bab0d64
Simplify conversion InputFile -> UIjson
domfournier Apr 2, 2026
11b1282
Merge branch 'GEOPY-2739' of https://github.com/MiraGeoscience/geoapp…
domfournier Apr 2, 2026
a9e7bec
do all the operations under the same context
Apr 3, 2026
92c740b
Merge pull request #191 from MiraGeoscience/GEOPY-2739-Mat
domfournier Apr 3, 2026
0e069e6
Merge branch 'develop' into GEOPY-2739
domfournier Apr 7, 2026
c6305f2
Move create_uijson in conftest. re-use in tests
domfournier Apr 8, 2026
ccd9d5a
Remove Params completely from tests
domfournier Apr 8, 2026
e160c56
INcrease coverage
domfournier Apr 8, 2026
1ad02a4
Removal of InputFile from sweep
domfournier Apr 8, 2026
2fcb67f
Small increase to coverage
domfournier Apr 8, 2026
1589cd5
Enforce input for build
domfournier Apr 8, 2026
aeece5e
Fix test
domfournier Apr 8, 2026
0503172
pylint fix
domfournier Apr 8, 2026
51c1155
Re-lock
domfournier Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion geoapps_utils-assets/uijson/base.ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@
"geoh5": "",
"monitoring_directory": "",
"workspace_geoh5": "",
"out_group": ""
"out_group": {
"label": "UIJson group",
"value": "",
"groupType": "{BB50AC61-A657-4926-9C82-067658E246A0}",
"visible": true,
"optional": true,
"enabled": false
}
}
210 changes: 98 additions & 112 deletions geoapps_utils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@
import tempfile
import warnings
from abc import ABC, abstractmethod
from copy import copy
from pathlib import Path
from typing import Any, ClassVar, GenericAlias, Self # type: ignore

from geoh5py import Workspace
from geoh5py.groups import UIJsonGroup
from geoh5py.objects import ObjectBase
from geoh5py.shared.utils import stringify
from geoh5py.ui_json import BaseUIJson, InputFile, monitored_directory_copy
from geoh5py.ui_json import InputFile, UIJson, monitored_directory_copy
from geoh5py.ui_json.utils import fetch_active_workspace
from pydantic import BaseModel, ConfigDict, ValidationError

from geoapps_utils import assets_path
from geoapps_utils.driver.params import BaseParams
from geoapps_utils.utils.formatters import recursive_flatten
from geoapps_utils.utils.importing import GeoAppsError
Expand All @@ -34,6 +33,26 @@
logger = get_logger(name=__name__, level_name=False, propagate=False, add_name=False)


def input_file_deprecation_warning(input_file: InputFile) -> Path:
"""
Warn the user of future deprecation and get a file path to an existing file.
"""

warnings.warn(
"The use of InputFile will be deprecated in future versions."
"Please start using UIJson class instead.",
Comment thread
domfournier marked this conversation as resolved.
Outdated
DeprecationWarning,
stacklevel=2,
)

if input_file.path_name is None or not Path(input_file.path_name).is_file():
temp_path = Path(tempfile.mkdtemp()) / "temp.ui.json"
Comment thread
domfournier marked this conversation as resolved.
Outdated
input_file.write_ui_json(path=temp_path.parent, name=temp_path.name)
return temp_path

return Path(input_file.path_name)


class Driver(ABC):
"""
# todo: Get rid of BaseParams to have a more robust DriverClass
Expand All @@ -43,8 +62,7 @@ class Driver(ABC):
:param params: Application parameters.
"""

_params_class: type[Options] | type[BaseParams]
_validations: dict | None = None
_params_class: type[Options]

def __init__(self, params: Options | BaseParams):
self._out_group: UIJsonGroup | None = None
Expand Down Expand Up @@ -86,39 +104,33 @@ def run(self):
"""Run the application."""

@classmethod
def read_ui_json(cls, filepath: str | Path, **kwargs) -> InputFile:
def start(
cls, filepath: str | Path | InputFile | UIJson, mode="r+", **kwargs
) -> Self:
"""
Read a ui.json file and return an InputFile object.
Run application specified by 'filepath' ui.json file.

:param filepath: Path to valid ui.json file for the application driver.
:param kwargs: Additional keyword arguments for InputFile read_ui_json.
:param mode: Mode to open the geoh5 file with.
:param kwargs: Additional keyword arguments for Options class.

:return: InputFile object.
:return: Self object.
"""
logger.info("Loading input file . . .")
filepath = Path(filepath).resolve()
return InputFile.read_ui_json(filepath, validations=cls._validations, **kwargs)

@classmethod
def start(cls, filepath: str | Path | InputFile, mode="r+", **kwargs) -> Self:
"""
Run application specified by 'filepath' ui.json file.
if isinstance(filepath, InputFile):
filepath = input_file_deprecation_warning(filepath)

:param filepath: Path to valid ui.json file for the application driver.
:param kwargs: Additional keyword arguments for InputFile read_ui_json.
"""
ifile = (
cls.read_ui_json(filepath, **kwargs)
if isinstance(filepath, str | Path)
else filepath
)
ifile = UIJson.read(filepath) if isinstance(filepath, str | Path) else filepath

if not isinstance(ifile, InputFile):
if not isinstance(ifile, UIJson):
raise TypeError("Input file must be a string path or an InputFile object.")
Comment thread
domfournier marked this conversation as resolved.

with ifile.geoh5.open(mode=mode):
if ifile.geoh5 is None:
raise GeoAppsError("The application needs a valid 'geoh5' file.")

params = cls._params_class.build(ifile, **kwargs)
Comment thread
domfournier marked this conversation as resolved.
Outdated
Comment thread
domfournier marked this conversation as resolved.
Outdated
with params.geoh5.open(mode=mode):
try:
params = cls._params_class.build(ifile)
logger.info("Initializing application . . .")
driver = cls(params)
logger.info("Running application . . .")
Expand All @@ -128,23 +140,18 @@ def start(cls, filepath: str | Path | InputFile, mode="r+", **kwargs) -> Self:
logger.warning("\n\nApplicationError: %s\n\n", error)
sys.exit(1)

return driver
return driver

def add_ui_json(self, entity: ObjectBase):
Comment thread
domfournier marked this conversation as resolved.
"""
Add ui.json file to entity.
Add ui.json as FileData to entity.

:param entity: Object to add ui.json file to.
"""
if (
self.params.input_file is None
or self.params.input_file.path is None
or self.params.input_file.name is None
):
raise ValueError("Input file and it's name and path must be set.")

file = self.params.input_file.write_ui_json(path=tempfile.mkdtemp())
entity.add_file(file)
with tempfile.TemporaryDirectory() as tmpdirname:
path = Path(tmpdirname) / self.params.title
Comment thread
domfournier marked this conversation as resolved.
Outdated
self.params.ui_json.write(path)
entity.add_file(path)

def update_monitoring_directory(
Comment thread
MatthieuCMira marked this conversation as resolved.
self, entity: ObjectBase, copy_children: bool = True
Expand Down Expand Up @@ -178,20 +185,17 @@ def get_default_ui_json_path(cls) -> Path | None:
return None

@classmethod
def get_default_ui_json(cls) -> BaseUIJson:
def get_default_ui_json(cls) -> UIJson:
"""
Load the driver's default ui.json template from disk
with no parameters filled in.

:return: The default ui.json configuration.
"""
ui_json_path = cls.get_default_ui_json_path()

if ui_json_path is None or not ui_json_path.exists():
raise ValueError(f"Driver {cls} does not have a default ui.json.")
if issubclass(cls._params_class, Options):
Comment thread
MatthieuCMira marked this conversation as resolved.
return cls._params_class.get_default_ui_json()

ui_json = BaseUIJson.read(ui_json_path)
return ui_json
raise ValueError(f"Driver {cls} does not have a default ui.json.")


class Options(BaseModel):
Expand All @@ -209,15 +213,14 @@ class Options(BaseModel):
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

name: ClassVar[str] = "base"
default_ui_json: ClassVar[Path | None] = None
default_ui_json: ClassVar[Path | None] = assets_path() / "uijson/base.ui.json"

title: str = "Base Data"
run_command: str = "geoapps_utils.base"
conda_environment: str | None = None
geoh5: Workspace
monitoring_directory: str | Path | None = None
out_group: UIJsonGroup | None = None
_input_file: InputFile | None = None

@staticmethod
def collect_input_from_dict(
Expand Down Expand Up @@ -262,20 +265,27 @@ def collect_input_from_dict(
return update

@classmethod
def build(cls, input_data: InputFile | dict | None = None, **kwargs) -> Self:
def build(
cls, input_data: InputFile | dict | None | UIJson = None, **kwargs
) -> Self:
"""
Build a dataclass from a dictionary or InputFile.
Build a dataclass from a dictionary or UIJson.

:param input_data: Dictionary of parameters and values.

:return: Dataclass of application parameters.
"""
data = input_data or {}
data = input_data if isinstance(input_data, dict | UIJson) else {}
Comment thread
domfournier marked this conversation as resolved.
Outdated

if isinstance(input_data, InputFile) and input_data.data is not None:
data = input_data.data.copy()
file_path = input_file_deprecation_warning(input_data)
data = UIJson.read(file_path)

if isinstance(data, UIJson):
data = data.to_params()
Comment thread
domfournier marked this conversation as resolved.
Outdated

if not isinstance(data, dict):
raise TypeError("Input data must be a dictionary or InputFile.")
raise TypeError("Input data must be a dictionary or UIJson.")

data.update(kwargs)
options = cls.collect_input_from_dict(cls, data) # type: ignore
Expand All @@ -292,9 +302,6 @@ def build(cls, input_data: InputFile | dict | None = None, **kwargs) -> Self:
f"Invalid input data for {cls.__name__}:\n - {summary}"
) from errors

if isinstance(input_data, InputFile):
out._input_file = input_data

return out

def _recursive_flatten(self, data: dict[str, Any]) -> dict[str, Any]:
Expand Down Expand Up @@ -322,67 +329,23 @@ def flatten(self) -> dict:
return out

@property
def input_file(self) -> InputFile:
def input_file(self) -> UIJson:
"""Create an InputFile with data matching current parameter state."""
Comment thread
domfournier marked this conversation as resolved.
Outdated

if self._input_file is None:
ifile = self._create_input_file_from_attributes()
else:
ifile = copy(self._input_file)
ifile.validate = False

return ifile

def _create_input_file_from_attributes(self) -> InputFile:
"""
Create an InputFile with data matching current parameter state.
"""
# ensure default uijson (PAth )exists or raise an error
if self.default_ui_json is None or not self.default_ui_json.exists():
ifile = InputFile(
ui_json=recursive_flatten(self.model_dump()), validate=False
)
else:
ifile = InputFile.read_ui_json(self.default_ui_json, validate=False)

if ifile.data is None:
raise ValueError(
f"Input file {self.default_ui_json} does not contain any data."
)

attributes = self.flatten()
ifile.update_ui_values(
{key: value for key, value in attributes.items() if value is not None}
warnings.warn(
"InputFile property is deprecated and will be removed in future versions. "
"Use `ui_json` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.ui_json

return ifile

def write_ui_json(self, path: Path) -> str:
"""
Write the ui.json file for the application.

:param path: Path to write the ui.json file.

:return: Path to the written ui.json file.
"""
if self._input_file is None:
self._input_file = self.input_file
self._input_file.name = path.name
self._input_file.path = str(path.parent)

return self.input_file.write_ui_json(path.name, str(path.parent))

def serialize(self):
def serialize(self, mode="python"):
"""Return a demoted uijson dictionary representation the params data."""

dump = self.model_dump(exclude_unset=True)
dump["geoh5"] = str(dump["geoh5"].h5file.resolve())
ifile = self.input_file
ifile.update_ui_values(recursive_flatten(dump))
assert ifile.ui_json is not None
options = stringify(ifile.ui_json)

return options
serialized = self.ui_json.model_dump(
exclude_unset=True, by_alias=True, mode=mode
)
return serialized

def update_out_group_options(self):
"""
Expand All @@ -392,5 +355,28 @@ def update_out_group_options(self):
raise ValueError("No output group defined to save options.")

with fetch_active_workspace(self.geoh5, mode="r+"):
self.out_group.options = self.serialize()
self.out_group.options = self.serialize(mode="json")
self.out_group.metadata = None

@property
def ui_json(self) -> UIJson:
"""
The parent UIJson object.
"""
ui_json = self.get_default_ui_json()
ui_json.set_values(**self.flatten())

return ui_json

@classmethod
def get_default_ui_json(cls) -> UIJson:
"""
Load the driver's default ui.json template from disk
with no parameters filled in.

:return: The default ui.json configuration.
"""
if cls.default_ui_json is None or not cls.default_ui_json.exists():
raise ValueError(f"Driver {cls} does not have a default ui.json.")

return UIJson.read(cls.default_ui_json)
Loading
Loading