Skip to content

feat: add logic to check user modification in trame-geos #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/typing-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
max-parallel: 3
matrix:
# add packages to check typing
package-name: ["geos-geomechanics", "geos-posp", "geos-timehistory", "geos-utils", "geos-xml-tools", "hdf5-wrapper"]
package-name: ["geos-geomechanics", "geos-posp", "geos-timehistory", "geos-utils", "geos-trame", "geos-xml-tools", "hdf5-wrapper"]

steps:
- uses: actions/checkout@v4
Expand All @@ -30,7 +30,7 @@ jobs:
# working-directory: ./${{ matrix.package-name }}
run: |
python -m pip install --upgrade pip
python -m pip install mypy ruff
python -m pip install mypy ruff types-PyYAML

- name: Typing check with mypy
# working-directory: ./${{ matrix.package-name }}
Expand Down
26 changes: 16 additions & 10 deletions geos-trame/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
repos:
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
exclude: ^.*\b(schema_generated)\b.*$
entry: black --check --force-exclude

- repo: https://github.com/codespell-project/codespell
rev: v2.1.0
hooks:
- id: codespell

- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
hooks:
- id: ruff
args: ["--config", "./.ruff.toml"]

- repo: https://github.com/google/yapf
rev: v0.43.0
hooks:
- id: yapf
args: ["-ir", "--style", "./.style.yapf"]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.16.0
hooks:
- id: flake8
- id: mypy
additional_dependencies: [types-PyYAML]
19 changes: 0 additions & 19 deletions geos-trame/CONTRIBUTING.rst

This file was deleted.

11 changes: 10 additions & 1 deletion geos-trame/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,23 @@ To be able to run the test suite, make sure to install the additionals dependenc

Then you can run the test with `pytest .`

Optional
--------

To use pre-commit hooks (ruff, mypy, yapf,...), make sure to install the dev dependencies:

.. code-block:: console

pip install -e '.[dev]'

Regarding GEOS
--------------

This application takes an XML file from the GEOS project to load dynamically all of its components.
To be able to do that, we need first to generate the corresponding python class based on a
xsd schema provided by GEOS.

`For more details <src/geos_trame/schema_generated/README.md>`_
`For more details <src/geos/trame/schema_generated/README.md>`_

Features
--------
Expand Down
26 changes: 7 additions & 19 deletions geos-trame/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies = [
"funcy==2.0",
"typing_inspect==0.9.0",
"typing_extensions>=4.12",
"PyYAML",
]

[project.optional-dependencies]
Expand All @@ -56,14 +57,16 @@ build = [
dev = [
"pylint",
"mypy",
"black",
"isort"
"types-PyYAML",
"ruff",
"pre-commit"
]
test = [
"pytest==8.3.3",
"pytest-seleniumbase==4.31.6",
"pixelmatch==0.3.0",
"Pillow==11.0.0",
"pytest-mypy==0.10.3",
"pytest-xprocess==1.0.2"
]

Expand All @@ -72,10 +75,10 @@ file = "README.md"
content-type = "text/markdown"

[project.scripts]
geos-trame = "geos_trame.app.__main__:main"
geos-trame = "geos.trame.app.main:main"

[project.entry-points.jupyter_serverproxy_servers]
geos-trame = "geos_trame.app.jupyter:jupyter_proxy_info"
geos-trame = "geos.trame.app.jupyter:jupyter_proxy_info"

[tool.setuptools]
license-files = ["LICENSE"]
Expand Down Expand Up @@ -116,18 +119,3 @@ disable = [
"R0913", # (too-many-arguments)
"W0105", # (pointless-string-statement)
]

[tool.black]
line-length = 88
target-version = ['py310']
include = '\.pyi?$'
extend-exclude = '''
src/geos_trame/schema_generated/*.py
'''

[tool.isort]
profile = "black"
src_paths = ["src", "tests"]
blackArgs = ["--preview"]
py_version = 310

Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@


class AlertHandler( vuetify3.VContainer ):
"""
Vuetify component used to display an alert status.
"""Vuetify component used to display an alert status.

This alert will be displayed in the bottom right corner of the screen.
It will be displayed until closed by the user or after 10 seconds if it is a success or warning.
"""

def __init__( self ):
def __init__( self ) -> None:
"""Constructor."""
super().__init__(
fluid=True,
classes="pa-0 ma-0",
Expand All @@ -31,31 +31,32 @@ def __init__( self ):

self.generate_alert_ui()

def generate_alert_ui( self ):
"""
Generate the alert UI.
def generate_alert_ui( self ) -> None:
"""Generate the alert UI.

The alert will be displayed in the bottom right corner of the screen.

Use an abritary z-index value to put the alert on top of the other components.
"""
with self:
with vuetify3.VCol( style="width: 40%; position: fixed; right: 50px; bottom: 50px; z-index: 100;", ):
vuetify3.VAlert(
style="max-height: 20vh; overflow-y: auto",
classes="ma-2",
v_for=( "(status, index) in alerts", ),
key="status",
type=( "status.type", "info" ),
text=( "status.message", "" ),
title=( "status.title", "" ),
closable=True,
click_close=( self.on_close, f"[status.id]" ),
)

def add_alert( self, type: str, title: str, message: str ):
"""
Add a status to the stack with a unique id.
with (
self,
vuetify3.VCol( style="width: 40%; position: fixed; right: 50px; bottom: 50px; z-index: 100;", ),
):
vuetify3.VAlert(
style="max-height: 20vh; overflow-y: auto",
classes="ma-2",
v_for=( "(status, index) in alerts", ),
key="status",
type=( "status.type", "info" ),
text=( "status.message", "" ),
title=( "status.title", "" ),
closable=True,
click_close=( self.on_close, "[status.id]" ),
)

def add_alert( self, type: str, title: str, message: str ) -> None:
"""Add a status to the stack with a unique id.

If there are more than 5 alerts displayed, remove the oldest.
A warning will be automatically closed after 10 seconds.
"""
Expand All @@ -77,21 +78,15 @@ def add_alert( self, type: str, title: str, message: str ):
if type == "warning":
asyncio.get_event_loop().call_later( self.__lifetime_of_alert, self.on_close, alert_id )

async def add_warning( self, title: str, message: str ):
"""
Add an alert of type "warning"
"""
async def add_warning( self, title: str, message: str ) -> None:
"""Add an alert of type 'warning'."""
self.add_alert( "warning", title, message )

async def add_error( self, title: str, message: str ):
"""
Add an alert of type "error"
"""
async def add_error( self, title: str, message: str ) -> None:
"""Add an alert of type 'error'."""
self.add_alert( "error", title, message )

def on_close( self, alert_id ):
"""
Remove in the state the alert associated to the given id.
"""
def on_close( self, alert_id: int ) -> None:
"""Remove in the state the alert associated to the given id."""
self.state.alerts = list( filter( lambda i: i[ "id" ] != alert_id, self.state.alerts ) )
self.state.flush()
90 changes: 90 additions & 0 deletions geos-trame/src/geos/trame/app/components/properties_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
# SPDX-FileContributor: Kitware
from typing import Any

from trame_client.widgets.core import AbstractElement
from trame_simput import get_simput_manager

from geos.trame.app.data_types.field_status import FieldStatus
from geos.trame.app.data_types.renderable import Renderable
from geos.trame.app.deck.tree import DeckTree
from geos.trame.app.ui.viewer.regionViewer import RegionViewer
from geos.trame.app.utils.geos_utils import group_name_ref_array_to_list

# Doc reference: https://geosx-geosx.readthedocs-hosted.com/en/latest/docs/sphinx/datastructure/CompleteXMLSchema.html
attributes_to_check = [ ( "region_attribute", str ), ( "fields_to_import", list ), ( "surfacicFieldsToImport", list ) ]


class PropertiesChecker( AbstractElement ):
"""Validity checker of properties within a deck tree."""

def __init__( self, tree: DeckTree, region_viewer: RegionViewer, **kwargs: Any ) -> None:
"""Constructor."""
super().__init__( "div", **kwargs )

self.tree = tree
self.region_viewer = region_viewer
self.simput_manager = get_simput_manager( id=self.state.sm_id )

def check_fields( self ) -> None:
"""Check all the fields in the deck_tree.

Get the names of all the cell data arrays from the input of the region viewer, then check that
all the attributes in `attributes_to_check` have a value corresponding to one of the array names.
"""
array_names = self._get_array_names()
for field in self.state.deck_tree:
self._check_field( field, array_names )
self.state.dirty( "deck_tree" )
self.state.flush()

def _check_field( self, field: dict, array_names: list[ str ] ) -> None:
"""Check that all the attributes in `attributes_to_check` have a value corresponding to one of the array names.

Set the `valid` property to the result of this check, and if necessary, indicate which properties are invalid.
"""
if len( array_names ) == 0 and Renderable.VTKMESH.value in field[ "id" ]:
self.ctrl.load_vtkmesh_from_id( field[ "id" ] )
array_names = self._get_array_names()
field[ "drawn" ] = True
field[ "valid" ] = FieldStatus.VALID.value
field[ "invalid_properties" ] = []

proxy = self.simput_manager.proxymanager.get( field[ "id" ] )
if proxy is not None:
for attr, expected_type in attributes_to_check:
if attr in proxy.definition:
if ( expected_type is str and proxy[ attr ] # value is not empty (valid)
and proxy[ attr ] not in array_names # value is not in the expected names
):
field[ "invalid_properties" ].append( attr )
elif expected_type is list:
arrays: list[ str ] | None = group_name_ref_array_to_list( proxy[ attr ] )
if arrays is None:
field[ "invalid_properties" ].append( attr )
continue
for array_name in arrays:
if array_name not in array_names:
field[ "invalid_properties" ].append( attr )
break

if len( field[ "invalid_properties" ] ) != 0:
field[ "valid" ] = FieldStatus.INVALID.value
else:
field.pop( "invalid_properties", None )

if field[ "children" ] is not None:
# Parents are only valid if all children are valid
field[ "invalid_children" ] = []
for child in field[ "children" ]:
self._check_field( child, array_names )
if child[ "valid" ] == FieldStatus.INVALID.value:
field[ "valid" ] = FieldStatus.INVALID.value
field[ "invalid_children" ].append( child[ "title" ] )
if len( field[ "invalid_children" ] ) == 0:
field.pop( "invalid_children", None )

def _get_array_names( self ) -> list[ str ]:
cellData = self.region_viewer.input.GetCellData()
return [ cellData.GetArrayName( i ) for i in range( cellData.GetNumberOfArrays() ) ]
Loading