Files
ResInsight/GrpcInterface/Python/rips/pdmobject.py
T

719 lines
27 KiB
Python
Raw Normal View History

# pylint: disable=no-self-use
"""
ResInsight caf::PdmObject connection module
"""
2020-01-21 08:48:56 +01:00
from enum import Enum
2025-05-16 15:46:45 +02:00
from functools import wraps
2020-01-21 08:48:56 +01:00
import grpc
import re
import inspect
2020-01-21 08:48:56 +01:00
2020-08-19 07:30:51 +02:00
import PdmObject_pb2
import PdmObject_pb2_grpc
import Commands_pb2
import Commands_pb2_grpc
2020-04-16 16:06:18 +02:00
2025-05-07 11:34:59 +02:00
from .exception import RipsError
2021-01-26 20:48:01 +01:00
2025-05-16 15:46:45 +02:00
from typing import Any, Callable, TypeVar, Union, List, Optional, Type
from typing_extensions import ParamSpec
2023-07-12 11:42:17 +02:00
# TypeVar for generic return types in PdmObjectBase methods
# Using string bound for forward reference since PdmObjectBase is defined later
PdmObjectT = TypeVar("PdmObjectT", bound="PdmObjectBase")
2023-07-12 11:42:17 +02:00
def camel_to_snake(name: str) -> str:
2021-01-26 20:48:01 +01:00
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
2020-04-16 16:06:18 +02:00
2023-07-12 11:42:17 +02:00
def snake_to_camel(name: str) -> str:
2021-01-26 20:48:01 +01:00
return "".join(word.title() for word in name.split("_"))
2020-04-16 16:06:18 +02:00
2023-07-12 11:42:17 +02:00
F = TypeVar("F", bound=Callable[..., Any])
C = TypeVar("C")
def _is_closed_channel_error(exc: BaseException) -> bool:
# gRPC raises ValueError("Cannot invoke RPC on closed channel!") when an
# RPC is attempted after the channel has been closed (e.g. by the
# heartbeat after detecting a dead server). Translate to RipsError so
# callers see a typed exception instead of a bare ValueError.
return isinstance(exc, ValueError) and "closed channel" in str(exc)
def _rips_error_from_rpc_error(exc: grpc.RpcError) -> RipsError:
"""Translate a gRPC failure into a RipsError, adding context for the
failure modes whose raw gRPC text says nothing useful."""
details = exc.details() or ""
message = "%s" % details
if "Channel closed" in details or exc.code() == grpc.StatusCode.UNAVAILABLE:
# The connection to ResInsight went away mid-call. This is either a
# crashed/closed server, or the local channel being closed by the
# heartbeat. Say so instead of surfacing a bare "Channel closed!".
message = (
f"{details}: lost the connection to ResInsight while the call was "
"running (the server may have crashed or been shut down)"
)
elif exc.code() == grpc.StatusCode.RESOURCE_EXHAUSTED:
message = f"{details}: the gRPC message size limit was exceeded"
return RipsError(message, code=exc.code(), details=details or None)
2023-07-12 11:42:17 +02:00
def add_method(cls: C) -> Callable[[F], F]:
def decorator(func: F) -> F:
2025-05-07 11:34:59 +02:00
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except grpc.RpcError as e:
raise _rips_error_from_rpc_error(e) from None
except ValueError as e:
if _is_closed_channel_error(e):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from e
raise
2025-05-07 11:34:59 +02:00
# Explicitly preserve signature for Sphinx documentation
2025-05-07 11:34:59 +02:00
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
wrapper.__signature__ = inspect.signature(func) # type: ignore
wrapper.__annotations__ = getattr(func, "__annotations__", {})
2025-05-07 11:34:59 +02:00
# Add the wrapped function to the class
setattr(cls, func.__name__, wrapper)
2020-04-16 16:06:18 +02:00
return func # returning func means func can still be used normally
2021-01-26 20:48:01 +01:00
return decorator
2020-04-16 16:06:18 +02:00
2023-07-12 11:42:17 +02:00
P = ParamSpec("P")
T = TypeVar("T")
def add_static_method(cls: C) -> Callable[[Callable[P, T]], Callable[P, T]]:
def decorator(func: Callable[P, T]) -> Callable[P, T]:
2020-04-16 16:06:18 +02:00
@wraps(func)
2023-07-12 11:42:17 +02:00
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
return func(*args, **kwargs)
2021-01-26 20:48:01 +01:00
setattr(cls, func.__name__, wrapper)
# Note we are not binding func, but wrapper which accepts self but does exactly the same as func
2020-04-16 16:06:18 +02:00
return func # returning func means func can still be used normally
2021-01-26 20:48:01 +01:00
return decorator
2020-08-19 07:30:51 +02:00
class PdmObjectBase:
"""
2020-08-19 07:30:51 +02:00
The ResInsight base class for the Project Data Model
"""
2023-07-12 11:42:17 +02:00
__custom_init__ = None
def _execute_command(self, **command_params) -> Any:
2020-08-19 07:30:51 +02:00
self.__warnings = []
response, call = self._commands.Execute.with_call(
2021-01-26 20:48:01 +01:00
Commands_pb2.CommandParams(**command_params)
)
2020-08-19 07:30:51 +02:00
for key, value in call.trailing_metadata():
2021-01-26 20:48:01 +01:00
value = value.replace(";;", "\n")
if key == "warning":
2020-08-19 07:30:51 +02:00
self.__warnings.append(value)
return response
2023-07-12 11:42:17 +02:00
def __init__(
self,
pb2_object: Optional[PdmObject_pb2.PdmObject],
channel: Optional[grpc.Channel],
) -> None:
2020-08-19 07:30:51 +02:00
self.__warnings = []
self.__chunk_size = 8160
self._channel = channel
# Create stubs
if self._channel:
2021-01-26 20:48:01 +01:00
self._pdm_object_stub = PdmObject_pb2_grpc.PdmObjectServiceStub(
self._channel
)
2020-08-19 07:30:51 +02:00
self._commands = Commands_pb2_grpc.CommandsStub(self._channel)
if pb2_object is not None:
# Copy parameters from ResInsight
2021-01-26 20:48:01 +01:00
assert isinstance(pb2_object, PdmObject_pb2.PdmObject)
2020-08-19 07:30:51 +02:00
self._pb2_object = pb2_object
for camel_keyword in self._pb2_object.parameters:
snake_keyword = camel_to_snake(camel_keyword)
setattr(self, snake_keyword, self.__get_grpc_value(camel_keyword))
else:
# Copy parameters from PdmObject defaults
2021-01-26 20:48:01 +01:00
self._pb2_object = PdmObject_pb2.PdmObject(
class_keyword=self.__class__.__name__
)
2020-08-19 07:30:51 +02:00
self.__copy_to_pb2()
2023-07-12 11:42:17 +02:00
def copy_from(self, obj: object) -> None:
2021-01-26 20:48:01 +01:00
"""Copy attribute values from object to self"""
2023-07-12 11:42:17 +02:00
for attribute in dir(obj):
2021-01-26 20:48:01 +01:00
if not attribute.startswith("__"):
2023-07-12 11:42:17 +02:00
value = getattr(obj, attribute)
# This is crucial to avoid overwriting methods
if not callable(value):
2020-08-19 07:30:51 +02:00
setattr(self, attribute, value)
if self.__custom_init__ is not None:
self.__custom_init__(self._pb2_object, self._channel)
self.update()
2023-07-12 11:42:17 +02:00
def warnings(self) -> List[str]:
2020-08-19 07:30:51 +02:00
return self.__warnings
2023-07-12 11:42:17 +02:00
def has_warnings(self) -> bool:
2020-08-19 07:30:51 +02:00
return len(self.__warnings) > 0
2023-07-12 11:42:17 +02:00
def __copy_to_pb2(self) -> None:
2020-08-19 07:30:51 +02:00
if self._pb2_object is not None:
for snake_kw in dir(self):
2021-01-26 20:48:01 +01:00
if not snake_kw.startswith("_"):
2020-08-19 07:30:51 +02:00
value = getattr(self, snake_kw)
# This is crucial to avoid overwriting methods
if not callable(value):
camel_kw = snake_to_camel(snake_kw)
self.__set_grpc_value(camel_kw, value)
2023-07-12 11:42:17 +02:00
def pb2_object(self) -> PdmObject_pb2.PdmObject:
"""Private method"""
2020-08-19 07:30:51 +02:00
return self._pb2_object
2023-07-12 11:42:17 +02:00
def channel(self) -> grpc.Channel:
"""Private method"""
2020-08-19 07:30:51 +02:00
return self._channel
2023-07-12 11:42:17 +02:00
def address(self) -> Any:
2020-08-19 07:30:51 +02:00
"""Get the unique address of the PdmObject
Returns:
A 64-bit unsigned integer address
"""
return self._pb2_object.address
2023-07-12 11:42:17 +02:00
def set_visible(self, visible: bool) -> None:
2020-08-19 07:30:51 +02:00
"""Set the visibility of the object in the ResInsight project tree"""
self._pb2_object.visible = visible
2023-07-12 11:42:17 +02:00
def visible(self) -> bool:
2020-08-19 07:30:51 +02:00
"""Get the visibility of the object in the ResInsight project tree"""
2023-07-12 11:42:17 +02:00
return bool(self._pb2_object.visible)
2020-08-19 07:30:51 +02:00
2023-07-12 11:42:17 +02:00
def print_object_info(self) -> None:
2020-08-19 07:30:51 +02:00
"""Print the structure and data content of the PdmObject"""
print("=========== " + self.__class__.__name__ + " =================")
print("Object Attributes: ")
for snake_kw in dir(self):
if not snake_kw.startswith("_") and not callable(getattr(self, snake_kw)):
2021-01-26 20:48:01 +01:00
print(
" "
+ snake_kw
+ " ["
+ type(getattr(self, snake_kw)).__name__
+ "]: "
+ str(getattr(self, snake_kw))
)
2020-08-19 07:30:51 +02:00
print("Object Methods:")
for snake_kw in dir(self):
if not snake_kw.startswith("_") and callable(getattr(self, snake_kw)):
print(" " + snake_kw)
2023-07-12 11:42:17 +02:00
Value = Union[bool, str, float, int, "ValueArray"]
ValueArray = List[Value]
def __convert_from_grpc_value(self, value: str) -> Value:
2021-01-26 20:48:01 +01:00
if value.lower() == "false":
2020-08-19 07:30:51 +02:00
return False
2021-01-26 20:48:01 +01:00
if value.lower() == "true":
2020-08-19 07:30:51 +02:00
return True
try:
2020-08-19 07:30:51 +02:00
int_val = int(value)
return int_val
except ValueError:
2020-08-19 07:30:51 +02:00
try:
float_val = float(value)
return float_val
except ValueError:
# We may have a string. Remove the outer pair of quotes
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
value = value[1:-1]
2020-08-19 07:30:51 +02:00
if self.__islist(value):
return self.__makelist(value)
2026-03-06 12:01:14 +01:00
if self.__istuple(value):
return self.__maketuple(value)
return self.__unescape_string(value)
2020-08-19 07:30:51 +02:00
2023-07-12 11:42:17 +02:00
def __convert_to_grpc_value(self, value: Any) -> str:
2020-08-19 07:30:51 +02:00
if isinstance(value, bool):
if value:
return "true"
return "false"
if isinstance(value, Enum):
return str(value.value)
if isinstance(value, PdmObjectBase):
2020-08-19 07:30:51 +02:00
return value.__class__.__name__ + ":" + str(value.address())
if isinstance(value, list):
2020-09-07 13:59:08 +02:00
list_of_values = []
2020-08-19 07:30:51 +02:00
for val in value:
2020-09-07 13:59:08 +02:00
list_of_values.append(self.__convert_to_grpc_value(val))
return "[" + ", ".join(list_of_values) + "]"
2026-03-06 12:01:14 +01:00
if isinstance(value, tuple):
list_of_values = []
for val in value:
list_of_values.append(self.__convert_to_grpc_value(val))
return "(" + ", ".join(list_of_values) + ")"
2020-08-19 07:30:51 +02:00
return str(value)
2023-07-12 11:42:17 +02:00
def __get_grpc_value(self, camel_keyword: str) -> Value:
2021-01-26 20:48:01 +01:00
return self.__convert_from_grpc_value(
self._pb2_object.parameters[camel_keyword]
)
2020-08-19 07:30:51 +02:00
2023-07-12 11:42:17 +02:00
def __set_grpc_value(self, camel_keyword: str, value: str) -> None:
2020-08-19 07:30:51 +02:00
self._pb2_object.parameters[camel_keyword] = self.__convert_to_grpc_value(value)
2023-07-12 11:42:17 +02:00
def set_value(self, snake_keyword: str, value: object) -> None:
2020-08-19 07:30:51 +02:00
"""Set the value associated with the provided keyword and updates ResInsight
2020-08-19 07:30:51 +02:00
Arguments:
keyword(str): A string containing the parameter keyword
value(varying): A value matching the type of the parameter.
See keyword documentation and/or print_object_info() to find
the correct data type.
"""
setattr(self, snake_keyword, value)
self.update()
2026-03-06 12:01:14 +01:00
def __istuple(self, value) -> bool:
return value.startswith("(") and value.endswith(")")
2023-07-12 11:42:17 +02:00
def __islist(self, value: str) -> bool:
2020-08-19 07:30:51 +02:00
return value.startswith("[") and value.endswith("]")
2026-03-06 12:01:14 +01:00
def __maketuple(self, tuple_string: str) -> Value:
tuple_string = tuple_string.removeprefix("(")
tuple_string = tuple_string.removesuffix(")")
if not tuple_string:
return ()
if "), (" in tuple_string:
subtuples = re.split(r"\), \(", tuple_string)
return [self.__makelist(subtuple) for subtuple in subtuples]
else:
strings = tuple_string.split(", ")
if len(strings) == 2:
return (
self.__convert_from_grpc_value(strings[0]),
self.__convert_from_grpc_value(strings[1]),
)
return ()
def __unescape_string(self, value: str) -> str:
result = []
i = 0
while i < len(value):
if value[i] == "\\" and i + 1 < len(value):
next_ch = value[i + 1]
if next_ch == '"' or next_ch == "\\":
result.append(next_ch)
i += 2
continue
result.append(value[i])
i += 1
return "".join(result)
2023-07-12 11:42:17 +02:00
def __makelist(self, list_string: str) -> Value:
2025-04-07 09:14:41 +02:00
list_string = list_string.removeprefix("[")
list_string = list_string.removesuffix("]")
if not list_string:
return []
2025-04-07 09:14:41 +02:00
# Quote-aware split: track quote state, escape state, and bracket depth
# so commas inside "..." or [...] are not treated as separators.
items = []
current = []
in_quotes = False
escape_next = False
bracket_depth = 0
for ch in list_string:
if escape_next:
current.append(ch)
escape_next = False
continue
if ch == "\\" and in_quotes:
current.append(ch)
escape_next = True
continue
if ch == '"':
in_quotes = not in_quotes
current.append(ch)
continue
if not in_quotes:
if ch == "[":
bracket_depth += 1
elif ch == "]":
bracket_depth -= 1
elif ch == "," and bracket_depth == 0:
# Separator: expect ", " so skip the following space
items.append("".join(current))
current = []
continue
elif ch == " " and not current:
# Skip space after comma separator
continue
current.append(ch)
if current:
items.append("".join(current))
values = []
for item in items:
values.append(self.__convert_from_grpc_value(item))
return values
2020-08-19 07:30:51 +02:00
2023-07-12 11:42:17 +02:00
def __from_pb2_to_resinsight_classes(
self,
pb2_object_list: List[PdmObject_pb2.PdmObject],
super_class_definition: Type[PdmObjectT],
) -> List[PdmObjectT]:
2020-08-19 07:30:51 +02:00
pdm_object_list = []
from .generated.generated_classes import class_from_keyword
2021-01-26 20:48:01 +01:00
2020-08-19 07:30:51 +02:00
for pb2_object in pb2_object_list:
child_class_definition = class_from_keyword(pb2_object.class_keyword)
2020-08-19 07:30:51 +02:00
if child_class_definition is None:
child_class_definition = super_class_definition
2023-07-12 11:42:17 +02:00
assert child_class_definition is not None
2021-01-26 20:48:01 +01:00
pdm_object = child_class_definition(
pb2_object=pb2_object, channel=self.channel()
)
2023-07-12 11:42:17 +02:00
2020-08-19 07:30:51 +02:00
pdm_object_list.append(pdm_object)
return pdm_object_list
def descendants(self, class_definition: Type[PdmObjectT]) -> List[PdmObjectT]:
2020-08-19 07:30:51 +02:00
"""Get a list of all project tree descendants matching the class keyword
Arguments:
class_definition[class]: A class definition matching the type of class wanted
Returns:
A list of PdmObjects matching the class_definition
"""
2021-01-26 20:48:01 +01:00
assert inspect.isclass(class_definition)
2020-08-19 07:30:51 +02:00
class_keyword = class_definition.__name__
try:
request = PdmObject_pb2.PdmDescendantObjectRequest(
2021-01-26 20:48:01 +01:00
object=self._pb2_object, child_keyword=class_keyword
)
object_list = self._pdm_object_stub.GetDescendantPdmObjects(request).objects
return self.__from_pb2_to_resinsight_classes(object_list, class_definition)
2020-08-19 07:30:51 +02:00
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.NOT_FOUND:
return [] # Valid empty result
raise e
def children(
self, child_field: str, class_definition: Type[PdmObjectT]
) -> List[PdmObjectT]:
2020-08-19 07:30:51 +02:00
"""Get a list of all direct project tree children inside the provided child_field
Arguments:
child_field[str]: A field name
Returns:
A list of PdmObjects inside the child_field
"""
2021-01-26 20:48:01 +01:00
request = PdmObject_pb2.PdmChildObjectRequest(
object=self._pb2_object, child_field=child_field
)
2020-08-19 07:30:51 +02:00
try:
object_list = self._pdm_object_stub.GetChildPdmObjects(request).objects
return self.__from_pb2_to_resinsight_classes(object_list, class_definition)
2020-08-19 07:30:51 +02:00
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.NOT_FOUND:
return []
raise e
2023-07-12 11:42:17 +02:00
def add_new_object(
self, class_definition: Type[PdmObjectT], child_field: str = ""
) -> Optional[PdmObjectT]:
2021-06-25 14:18:36 +02:00
"""Create and add an object to the specified child field
Arguments:
class_definition[class]: Class definition of the object to create
child_field[str]: The keyword for the field to create a new object in. If empty, the first child array field is used.
Returns:
The created PdmObject inside the child_field
"""
from .generated.generated_classes import class_from_keyword
assert inspect.isclass(class_definition)
class_keyword = class_definition.__name__
request = PdmObject_pb2.CreatePdmChildObjectRequest(
object=self._pb2_object,
child_field=child_field,
class_keyword=class_keyword,
)
try:
pb2_object = self._pdm_object_stub.CreateChildPdmObject(request)
child_class_definition = class_from_keyword(pb2_object.class_keyword)
if child_class_definition is None:
2023-07-12 11:42:17 +02:00
child_class_definition = class_definition
assert child_class_definition.__name__ == class_definition.__name__
pdm_object = class_definition(pb2_object=pb2_object, channel=self.channel())
2021-06-25 14:18:36 +02:00
return pdm_object
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.NOT_FOUND:
return None
raise e
def ancestor(self, class_definition: Type[PdmObjectT]) -> Optional[PdmObjectT]:
2020-08-19 07:30:51 +02:00
"""Find the first ancestor that matches the provided class_keyword
Arguments:
class_definition[class]: A class definition matching the type of class wanted
"""
2021-01-26 20:48:01 +01:00
assert inspect.isclass(class_definition)
2020-08-19 07:30:51 +02:00
class_keyword = class_definition.__name__
request = PdmObject_pb2.PdmParentObjectRequest(
2021-01-26 20:48:01 +01:00
object=self._pb2_object, parent_keyword=class_keyword
)
2020-08-19 07:30:51 +02:00
try:
pb2_object = self._pdm_object_stub.GetAncestorPdmObject(request)
2023-07-12 11:42:17 +02:00
pdm_object = class_definition(pb2_object=pb2_object, channel=self.channel())
2020-08-19 07:30:51 +02:00
return pdm_object
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.NOT_FOUND:
return None
raise e
2023-07-12 11:42:17 +02:00
def _call_get_method_async(self, method_name: str):
2021-01-26 20:48:01 +01:00
request = PdmObject_pb2.PdmObjectGetterRequest(
object=self._pb2_object, method=method_name
)
2020-08-19 07:30:51 +02:00
for chunk in self._pdm_object_stub.CallPdmObjectGetter(request):
yield chunk
2023-07-12 11:42:17 +02:00
def _call_get_method(self, method_name: str):
2020-08-19 07:30:51 +02:00
all_values = []
generator = self._call_get_method_async(method_name)
for chunk in generator:
2021-01-26 20:48:01 +01:00
data = getattr(chunk, chunk.WhichOneof("data"))
2020-08-19 07:30:51 +02:00
for value in data.data:
all_values.append(value)
return all_values
def __generate_set_method_chunks(self, array, method_request):
index = -1
while index < len(array):
chunk = PdmObject_pb2.PdmObjectSetterChunk()
if index == -1:
2021-01-26 20:48:01 +01:00
chunk.set_request.CopyFrom(
PdmObject_pb2.PdmObjectSetterRequest(
request=method_request, data_count=len(array)
)
)
2020-08-19 07:30:51 +02:00
index += 1
else:
actual_chunk_size = min(len(array) - index + 1, self.__chunk_size)
if isinstance(array[0], float):
chunk.CopyFrom(
2021-01-26 20:48:01 +01:00
PdmObject_pb2.PdmObjectSetterChunk(
doubles=PdmObject_pb2.DoubleArray(
data=array[index : index + actual_chunk_size]
)
)
)
2020-08-19 07:30:51 +02:00
elif isinstance(array[0], int):
chunk.CopyFrom(
2021-01-26 20:48:01 +01:00
PdmObject_pb2.PdmObjectSetterChunk(
ints=PdmObject_pb2.IntArray(
data=array[index : index + actual_chunk_size]
)
)
)
2020-08-19 07:30:51 +02:00
elif isinstance(array[0], str):
chunk.CopyFrom(
2021-01-26 20:48:01 +01:00
PdmObject_pb2.PdmObjectSetterChunk(
strings=PdmObject_pb2.StringArray(
data=array[index : index + actual_chunk_size]
)
)
)
2020-08-19 07:30:51 +02:00
else:
raise Exception("Wrong data type for set method")
index += actual_chunk_size
yield chunk
# Final empty message to signal completion
chunk = PdmObject_pb2.PdmObjectSetterChunk()
yield chunk
2020-04-16 16:06:18 +02:00
2023-07-12 11:42:17 +02:00
def _call_set_method(self, method_name: str, values) -> None:
2020-08-19 07:30:51 +02:00
method_request = PdmObject_pb2.PdmObjectGetterRequest(
2021-01-26 20:48:01 +01:00
object=self._pb2_object, method=method_name
)
2020-08-19 07:30:51 +02:00
request_iterator = self.__generate_set_method_chunks(values, method_request)
reply = self._pdm_object_stub.CallPdmObjectSetter(request_iterator)
if reply.accepted_value_count < len(values):
raise IndexError
2023-07-12 11:42:17 +02:00
def _call_pdm_method_void(self, method_name: str, **kwargs: Any) -> None:
pb2_params = PdmObject_pb2.PdmObject(class_keyword=method_name)
for key, value in kwargs.items():
pb2_params.parameters[snake_to_camel(key)] = self.__convert_to_grpc_value(
value
)
request = PdmObject_pb2.PdmObjectMethodRequest(
object=self._pb2_object, method=method_name, params=pb2_params
)
2025-05-07 11:34:59 +02:00
try:
self._pdm_object_stub.CallPdmObjectMethod(request)
except grpc.RpcError as exc:
raise _rips_error_from_rpc_error(exc) from None
except ValueError as exc:
if _is_closed_channel_error(exc):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from exc
raise
2023-07-12 11:42:17 +02:00
def _call_pdm_method_return_value(
self, method_name: str, class_definition: Type[PdmObjectT], **kwargs: Any
) -> PdmObjectT:
2023-07-12 11:42:17 +02:00
pb2_params = PdmObject_pb2.PdmObject(class_keyword=method_name)
for key, value in kwargs.items():
pb2_params.parameters[snake_to_camel(key)] = self.__convert_to_grpc_value(
value
)
request = PdmObject_pb2.PdmObjectMethodRequest(
object=self._pb2_object, method=method_name, params=pb2_params
)
2025-05-07 11:34:59 +02:00
try:
pb2_object = self._pdm_object_stub.CallPdmObjectMethod(request)
# Prefer the actual class the server returned over the statically declared one
# so that generic methods (e.g. AddFolder registered on a shared base) yield
# the most-derived Python proxy and inherited accessors work as expected.
from .generated.generated_classes import class_from_keyword
actual_class = class_from_keyword(pb2_object.class_keyword)
if actual_class is not None and issubclass(actual_class, class_definition):
return actual_class(pb2_object=pb2_object, channel=self.channel())
return class_definition(pb2_object=pb2_object, channel=self.channel())
2025-05-07 11:34:59 +02:00
except grpc.RpcError as exc:
raise _rips_error_from_rpc_error(exc) from None
except ValueError as exc:
if _is_closed_channel_error(exc):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from exc
raise
2023-07-12 11:42:17 +02:00
def _call_pdm_method_return_optional_value(
self, method_name: str, class_definition: Type[PdmObjectT], **kwargs: Any
) -> Optional[PdmObjectT]:
2020-08-19 07:30:51 +02:00
pb2_params = PdmObject_pb2.PdmObject(class_keyword=method_name)
for key, value in kwargs.items():
2021-01-26 20:48:01 +01:00
pb2_params.parameters[snake_to_camel(key)] = self.__convert_to_grpc_value(
value
)
2020-08-19 07:30:51 +02:00
request = PdmObject_pb2.PdmObjectMethodRequest(
2021-01-26 20:48:01 +01:00
object=self._pb2_object, method=method_name, params=pb2_params
)
2025-05-07 11:34:59 +02:00
try:
pb2_object = self._pdm_object_stub.CallPdmObjectMethod(request)
2025-05-07 11:34:59 +02:00
from .generated.generated_classes import class_from_keyword
child_class_definition = class_from_keyword(pb2_object.class_keyword)
if child_class_definition is None:
return None
assert issubclass(child_class_definition, class_definition)
pdm_object = child_class_definition(
pb2_object=pb2_object, channel=self.channel()
)
2025-05-07 11:34:59 +02:00
return pdm_object
2025-05-07 11:34:59 +02:00
except grpc.RpcError as exc:
raise _rips_error_from_rpc_error(exc) from None
except ValueError as exc:
if _is_closed_channel_error(exc):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from exc
raise
2020-04-16 16:06:18 +02:00
2023-07-12 11:42:17 +02:00
def update(self) -> None:
"""Sync all fields from the Python Object to ResInsight
If validation fails on the server, all local changes are rolled back
to the last known valid state, and the exception is re-raised.
"""
if self._pdm_object_stub is None:
2021-01-26 20:48:01 +01:00
raise Exception(
"Object is not connected to GRPC service so cannot update ResInsight"
)
2024-01-29 08:46:45 +01:00
# Store a snapshot of the last known valid protobuf state
pb2_snapshot = PdmObject_pb2.PdmObject()
pb2_snapshot.CopyFrom(self._pb2_object)
try:
self.__copy_to_pb2()
self._pdm_object_stub.UpdateExistingPdmObject(self._pb2_object)
except Exception:
# Rollback: restore protobuf object to previous state
self._pb2_object.CopyFrom(pb2_snapshot)
# Restore Python attributes from the protobuf snapshot
for camel_keyword in pb2_snapshot.parameters:
snake_keyword = camel_to_snake(camel_keyword)
value = self.__convert_from_grpc_value(
pb2_snapshot.parameters[camel_keyword]
)
setattr(self, snake_keyword, value)
# Re-raise the exception so the caller knows update failed
raise
2024-01-29 08:46:45 +01:00
def delete(self) -> None:
"""Delete object in ResInsight"""
self.__copy_to_pb2()
if self._pdm_object_stub is not None:
self._pdm_object_stub.DeleteExistingPdmObject(self._pb2_object)
else:
raise Exception(
"Object is not connected to GRPC service so cannot update ResInsight"
)