diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e44b37d4..f2b3adf0 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -28,7 +28,8 @@ OMCSessionException, OMCSession, OMCSessionLocal, - OMCPath, + + OMPathABC, ) # define logger using the current module name as ID @@ -387,13 +388,13 @@ def __init__( self._version = self._parse_om_version(version=version_str) self._simulated = False # True if the model has already been simulated - self._result_file: Optional[OMCPath] = None # for storing result file + self._result_file: Optional[OMPathABC] = None # for storing result file - self._work_dir: OMCPath = self.setWorkDirectory(work_directory) + self._work_dir: OMPathABC = self.setWorkDirectory(work_directory) self._model_name: Optional[str] = None self._libraries: Optional[list[str | tuple[str, str]]] = None - self._file_name: Optional[OMCPath] = None + self._file_name: Optional[OMPathABC] = None self._variable_filter: Optional[str] = None def get_session(self) -> OMCSession: @@ -411,7 +412,7 @@ def get_model_name(self) -> str: return self._model_name - def setWorkDirectory(self, work_directory: Optional[str | os.PathLike] = None) -> OMCPath: + def setWorkDirectory(self, work_directory: Optional[str | os.PathLike] = None) -> OMPathABC: """ Define the work directory for the ModelicaSystem / OpenModelica session. The model is build within this directory. If no directory is defined a unique temporary directory is created. @@ -433,7 +434,7 @@ def setWorkDirectory(self, work_directory: Optional[str | os.PathLike] = None) - # ... and also return the defined path return workdir - def getWorkDirectory(self) -> OMCPath: + def getWorkDirectory(self) -> OMPathABC: """ Return the defined working directory for this ModelicaSystem / OpenModelica session. """ @@ -458,7 +459,7 @@ def check_model_executable(self): if returncode != 0: raise ModelicaSystemError("Model executable not working!") - def _xmlparse(self, xml_file: OMCPath): + def _xmlparse(self, xml_file: OMPathABC): if not xml_file.is_file(): raise ModelicaSystemError(f"XML file not generated: {xml_file}") @@ -832,7 +833,7 @@ def _parse_om_version(version: str) -> tuple[int, int, int]: def _process_override_data( self, om_cmd: ModelExecutionCmd, - override_file: OMCPath, + override_file: OMPathABC, override_var: dict[str, str], override_sim: dict[str, str], ) -> None: @@ -868,7 +869,7 @@ def _process_override_data( def simulate_cmd( self, - result_file: OMCPath, + result_file: OMPathABC, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, ) -> ModelExecutionCmd: @@ -966,14 +967,14 @@ def simulate( if resultfile is None: # default result file generated by OM self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat" - elif isinstance(resultfile, OMCPath): + elif isinstance(resultfile, OMPathABC): self._result_file = resultfile else: self._result_file = self._session.omcpath(resultfile) if not self._result_file.is_absolute(): self._result_file = self.getWorkDirectory() / resultfile - if not isinstance(self._result_file, OMCPath): + if not isinstance(self._result_file, OMPathABC): raise ModelicaSystemError(f"Invalid result file path: {self._result_file} - must be an OMCPath object!") om_cmd = self.simulate_cmd( @@ -1298,7 +1299,7 @@ def setInputs( return True - def _createCSVData(self, csvfile: Optional[OMCPath] = None) -> OMCPath: + def _createCSVData(self, csvfile: Optional[OMPathABC] = None) -> OMPathABC: """ Create a csv file with inputs for the simulation/optimization of the model. If csvfile is provided as argument, this file is used; else a generic file name is created. @@ -1626,7 +1627,7 @@ def set_command_line_options(self, command_line_option: str): expr = f'setCommandLineOptions("{command_line_option}")' self.sendExpression(expr=expr, parsed=False) - def _loadFile(self, fileName: OMCPath): + def _loadFile(self, fileName: OMPathABC): # load file self.sendExpression(expr=f'loadFile("{fileName.as_posix()}")') @@ -2007,7 +2008,7 @@ def convertMo2Fmu( fmuType: str = "me_cs", fileNamePrefix: Optional[str] = None, includeResources: bool = True, - ) -> OMCPath: + ) -> OMPathABC: """Translate the model into a Functional Mockup Unit. Args: @@ -2046,7 +2047,7 @@ def convertMo2Fmu( def convertFmu2Mo( self, fmu: os.PathLike, - ) -> OMCPath: + ) -> OMPathABC: """ In order to load FMU, at first it needs to be translated into Modelica model. This method is used to generate Modelica model from the given FMU. It generates "fmuName_me_FMU.mo". @@ -2513,7 +2514,7 @@ def get_doe_solutions( def doe_get_solutions( msomc: ModelicaSystemOMC, - resultpath: OMCPath, + resultpath: OMPathABC, doe_def: Optional[dict] = None, var_list: Optional[list] = None, ) -> Optional[tuple[str] | dict[str, dict[str, np.ndarray]]]: diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index b95f36c1..242febf0 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -249,206 +249,272 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F return self._ask(question='getClassNames', opt=opt) -class OMCPathReal(pathlib.PurePosixPath): - """ - Implementation of a basic (PurePosix)Path object which uses OMC as backend. The connection to OMC is provided via an - instances of OMCSession* classes. - - PurePosixPath is selected to cover usage of OMC in docker or via WSL. Usage of specialised function could result in - errors as well as usage on a Windows system due to slightly different definitions (PureWindowsPath). - """ +# due to the compatibility layer to Python < 3.12, the OM(C)Path classes must be hidden behind the following if +# conditions. This is also the reason for OMPathABC, a simple base class to be used in ModelicaSystem* classes. +# Reason: before Python 3.12, pathlib.PurePosixPath can not be derived from; therefore, OMPathABC is not possible +if sys.version_info < (3, 12): + class OMPathCompatibility(pathlib.Path): + """ + Compatibility class for OMPathABC in Python < 3.12. This allows to run all code which uses OMPathABC (mainly + ModelicaSystem) on these Python versions. There are remaining limitation as only local execution is possible. + """ - def __init__(self, *path, session: OMCSession) -> None: - super().__init__(*path) - self._session = session + # modified copy of pathlib.Path.__new__() definition + def __new__(cls, *args, **kwargs): + logger.warning("Python < 3.12 - using a version of class OMCPath " + "based on pathlib.Path for local usage only.") - def with_segments(self, *pathsegments): - """ - Create a new OMCPath object with the given path segments. + if cls is OMPathCompatibility: + cls = OMPathCompatibilityWindows if os.name == 'nt' else OMPathCompatibilityPosix + self = cls._from_parts(args) + if not self._flavour.is_supported: + raise NotImplementedError(f"cannot instantiate {cls.__name__} on your system") + return self - The original definition of Path is overridden to ensure the OMC session is set. - """ - return type(self)(*pathsegments, session=self._session) + def size(self) -> int: + """ + Needed compatibility function to have the same interface as OMCPathReal + """ + return self.stat().st_size - def is_file(self, *, follow_symlinks=True) -> bool: + class OMPathCompatibilityPosix(pathlib.PosixPath, OMPathCompatibility): """ - Check if the path is a regular file. + Compatibility class for OMCPath on Posix systems (Python < 3.12) """ - return self._session.sendExpression(expr=f'regularFileExists("{self.as_posix()}")') - def is_dir(self, *, follow_symlinks=True) -> bool: + class OMPathCompatibilityWindows(pathlib.WindowsPath, OMPathCompatibility): """ - Check if the path is a directory. + Compatibility class for OMCPath on Windows systems (Python < 3.12) """ - return self._session.sendExpression(expr=f'directoryExists("{self.as_posix()}")') - def is_absolute(self): - """ - Check if the path is an absolute path considering the possibility that we are running locally on Windows. This - case needs special handling as the definition of is_absolute() differs. + OMPathABC = OMPathCompatibility + OMCPath = OMPathCompatibility +else: + class OMPathABC(pathlib.PurePosixPath, metaclass=abc.ABCMeta): """ - if isinstance(self._session, OMCSessionLocal) and platform.system() == 'Windows': - return pathlib.PureWindowsPath(self.as_posix()).is_absolute() - return super().is_absolute() + Implementation of a basic (PurePosix)Path object to be used within OMPython. The derived classes can use OMC as + backend and - thus - work on different configurations like docker or WSL. The connection to OMC is provided via + an instances of classes derived from BaseSession. - def read_text(self, encoding=None, errors=None, newline=None) -> str: + PurePosixPath is selected as it covers all but Windows systems (Linux, docker, WSL). However, the code is + written such that possible Windows system are taken into account. Nevertheless, the overall functionality is + limited compared to standard pathlib.Path objects. """ - Read the content of the file represented by this path as text. - The additional arguments `encoding`, `errors` and `newline` are only defined for compatibility with Path() - definition. - """ - return self._session.sendExpression(expr=f'readFile("{self.as_posix()}")') + def __init__(self, *path, session: OMCSession) -> None: + super().__init__(*path) + self._session = session - def write_text(self, data: str, encoding=None, errors=None, newline=None): - """ - Write text data to the file represented by this path. + def with_segments(self, *pathsegments): + """ + Create a new OMCPath object with the given path segments. - The additional arguments `encoding`, `errors`, and `newline` are only defined for compatibility with Path() - definitions. - """ - if not isinstance(data, str): - raise TypeError(f"data must be str, not {data.__class__.__name__}") + The original definition of Path is overridden to ensure the session data is set. + """ + return type(self)(*pathsegments, session=self._session) - data_omc = self._session.escape_str(data) - self._session.sendExpression(expr=f'writeFile("{self.as_posix()}", "{data_omc}", false);') + @abc.abstractmethod + def is_file(self) -> bool: + """ + Check if the path is a regular file. + """ - return len(data) + @abc.abstractmethod + def is_dir(self) -> bool: + """ + Check if the path is a directory. + """ - def mkdir(self, mode=0o777, parents=False, exist_ok=False): - """ - Create a directory at the path represented by this OMCPath object. + @abc.abstractmethod + def is_absolute(self): + """ + Check if the path is an absolute path. + """ - The additional arguments `mode`, and `parents` are only defined for compatibility with Path() definitions. - """ - if self.is_dir() and not exist_ok: - raise FileExistsError(f"Directory {self.as_posix()} already exists!") + @abc.abstractmethod + def read_text(self) -> str: + """ + Read the content of the file represented by this path as text. + """ - return self._session.sendExpression(expr=f'mkdir("{self.as_posix()}")') + @abc.abstractmethod + def write_text(self, data: str): + """ + Write text data to the file represented by this path. + """ - def cwd(self): - """ - Returns the current working directory as an OMCPath object. - """ - cwd_str = self._session.sendExpression(expr='cd()') - return OMCPath(cwd_str, session=self._session) + @abc.abstractmethod + def mkdir(self, parents: bool = True, exist_ok: bool = False): + """ + Create a directory at the path represented by this class. - def unlink(self, missing_ok: bool = False) -> None: - """ - Unlink (delete) the file or directory represented by this path. - """ - res = self._session.sendExpression(expr=f'deleteFile("{self.as_posix()}")') - if not res and not missing_ok: - raise FileNotFoundError(f"Cannot delete file {self.as_posix()} - it does not exists!") + The argument parents with default value True exists to ensure compatibility with the fallback solution for + Python < 3.12. In this case, pathlib.Path is used directly and this option ensures, that missing parent + directories are also created. + """ - def resolve(self, strict: bool = False): - """ - Resolve the path to an absolute path. This is done based on available OMC functions. - """ - if strict and not (self.is_file() or self.is_dir()): - raise OMCSessionException(f"Path {self.as_posix()} does not exist!") + @abc.abstractmethod + def cwd(self): + """ + Returns the current working directory as an OMPathABC object. + """ - if self.is_file(): - pathstr_resolved = self._omc_resolve(self.parent.as_posix()) - omcpath_resolved = self._session.omcpath(pathstr_resolved) / self.name - elif self.is_dir(): - pathstr_resolved = self._omc_resolve(self.as_posix()) - omcpath_resolved = self._session.omcpath(pathstr_resolved) - else: - raise OMCSessionException(f"Path {self.as_posix()} is neither a file nor a directory!") + @abc.abstractmethod + def unlink(self, missing_ok: bool = False) -> None: + """ + Unlink (delete) the file or directory represented by this path. + """ + + @abc.abstractmethod + def resolve(self, strict: bool = False): + """ + Resolve the path to an absolute path. + """ + + def absolute(self): + """ + Resolve the path to an absolute path. Just a wrapper for resolve(). + """ + return self.resolve() - if not omcpath_resolved.is_file() and not omcpath_resolved.is_dir(): - raise OMCSessionException(f"OMCPath resolve failed for {self.as_posix()} - path does not exist!") + def exists(self) -> bool: + """ + Semi replacement for pathlib.Path.exists(). + """ + return self.is_file() or self.is_dir() - return omcpath_resolved + @abc.abstractmethod + def size(self) -> int: + """ + Get the size of the file in bytes - this is an extra function and the best we can do using OMC. + """ - def _omc_resolve(self, pathstr: str) -> str: + class _OMCPath(OMPathABC): """ - Internal function to resolve the path of the OMCPath object using OMC functions *WITHOUT* changing the cwd - within OMC. + Implementation of a OMPathABC using OMC as backend. The connection to OMC is provided via an instances of an + OMCSession* classes. """ - expr = ('omcpath_cwd := cd(); ' - f'omcpath_check := cd("{pathstr}"); ' # check requested pathstring - 'cd(omcpath_cwd)') - try: - result = self._session.sendExpression(expr=expr, parsed=False) - result_parts = result.split('\n') - pathstr_resolved = result_parts[1] - pathstr_resolved = pathstr_resolved[1:-1] # remove quotes - except OMCSessionException as ex: - raise OMCSessionException(f"OMCPath resolve failed for {pathstr}!") from ex + def is_file(self) -> bool: + """ + Check if the path is a regular file. + """ + return self._session.sendExpression(expr=f'regularFileExists("{self.as_posix()}")') - return pathstr_resolved + def is_dir(self) -> bool: + """ + Check if the path is a directory. + """ + return self._session.sendExpression(expr=f'directoryExists("{self.as_posix()}")') - def absolute(self): - """ - Resolve the path to an absolute path. This is done by calling resolve() as it is the best we can do - using OMC functions. - """ - return self.resolve(strict=True) + def is_absolute(self): + """ + Check if the path is an absolute path. + """ + if isinstance(self._session, OMCSessionLocal) and platform.system() == 'Windows': + return pathlib.PureWindowsPath(self.as_posix()).is_absolute() + return super().is_absolute() - def exists(self, follow_symlinks=True) -> bool: - """ - Semi replacement for pathlib.Path.exists(). - """ - return self.is_file() or self.is_dir() + def read_text(self) -> str: + """ + Read the content of the file represented by this path as text. + """ + return self._session.sendExpression(expr=f'readFile("{self.as_posix()}")') - def size(self) -> int: - """ - Get the size of the file in bytes - this is an extra function and the best we can do using OMC. - """ - if not self.is_file(): - raise OMCSessionException(f"Path {self.as_posix()} is not a file!") + def write_text(self, data: str): + """ + Write text data to the file represented by this path. + """ + if not isinstance(data, str): + raise TypeError(f"data must be str, not {data.__class__.__name__}") - res = self._session.sendExpression(expr=f'stat("{self.as_posix()}")') - if res[0]: - return int(res[1]) + data_omc = self._session.escape_str(data) + self._session.sendExpression(expr=f'writeFile("{self.as_posix()}", "{data_omc}", false);') - raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!") + return len(data) + def mkdir(self, parents: bool = True, exist_ok: bool = False): + """ + Create a directory at the path represented by this class. -if sys.version_info < (3, 12): + The argument parents with default value True exists to ensure compatibility with the fallback solution for + Python < 3.12. In this case, pathlib.Path is used directly and this option ensures, that missing parent + directories are also created. + """ + if self.is_dir() and not exist_ok: + raise FileExistsError(f"Directory {self.as_posix()} already exists!") - class OMCPathCompatibility(pathlib.Path): - """ - Compatibility class for OMCPath in Python < 3.12. This allows to run all code which uses OMCPath (mainly - ModelicaSystem) on these Python versions. There is one remaining limitation: only OMCProcessLocal will work as - OMCPathCompatibility is based on the standard pathlib.Path implementation. - """ + return self._session.sendExpression(expr=f'mkdir("{self.as_posix()}")') - # modified copy of pathlib.Path.__new__() definition - def __new__(cls, *args, **kwargs): - logger.warning("Python < 3.12 - using a version of class OMCPath " - "based on pathlib.Path for local usage only.") + def cwd(self): + """ + Returns the current working directory as an OMPathABC object. + """ + cwd_str = self._session.sendExpression(expr='cd()') + return OMCPath(cwd_str, session=self._session) - if cls is OMCPathCompatibility: - cls = OMCPathCompatibilityWindows if os.name == 'nt' else OMCPathCompatibilityPosix - self = cls._from_parts(args) - if not self._flavour.is_supported: - raise NotImplementedError(f"cannot instantiate {cls.__name__} on your system") - return self + def unlink(self, missing_ok: bool = False) -> None: + """ + Unlink (delete) the file or directory represented by this path. + """ + res = self._session.sendExpression(expr=f'deleteFile("{self.as_posix()}")') + if not res and not missing_ok: + raise FileNotFoundError(f"Cannot delete file {self.as_posix()} - it does not exists!") - def size(self) -> int: + def resolve(self, strict: bool = False): """ - Needed compatibility function to have the same interface as OMCPathReal + Resolve the path to an absolute path. This is done based on available OMC functions. """ - return self.stat().st_size + if strict and not (self.is_file() or self.is_dir()): + raise OMCSessionException(f"Path {self.as_posix()} does not exist!") + + if self.is_file(): + pathstr_resolved = self._omc_resolve(self.parent.as_posix()) + omcpath_resolved = self._session.omcpath(pathstr_resolved) / self.name + elif self.is_dir(): + pathstr_resolved = self._omc_resolve(self.as_posix()) + omcpath_resolved = self._session.omcpath(pathstr_resolved) + else: + raise OMCSessionException(f"Path {self.as_posix()} is neither a file nor a directory!") - class OMCPathCompatibilityPosix(pathlib.PosixPath, OMCPathCompatibility): - """ - Compatibility class for OMCPath on Posix systems (Python < 3.12) - """ + if not omcpath_resolved.is_file() and not omcpath_resolved.is_dir(): + raise OMCSessionException(f"OMCPath resolve failed for {self.as_posix()} - path does not exist!") - class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility): - """ - Compatibility class for OMCPath on Windows systems (Python < 3.12) - """ + return omcpath_resolved + + def _omc_resolve(self, pathstr: str) -> str: + """ + Internal function to resolve the path of the OMCPath object using OMC functions *WITHOUT* changing the cwd + within OMC. + """ + expr = ('omcpath_cwd := cd(); ' + f'omcpath_check := cd("{pathstr}"); ' # check requested pathstring + 'cd(omcpath_cwd)') - OMCPath = OMCPathCompatibility + try: + result = self._session.sendExpression(expr=expr, parsed=False) + result_parts = result.split('\n') + pathstr_resolved = result_parts[1] + pathstr_resolved = pathstr_resolved[1:-1] # remove quotes + except OMCSessionException as ex: + raise OMCSessionException(f"OMCPath resolve failed for {pathstr}!") from ex -else: - OMCPath = OMCPathReal + return pathstr_resolved + + def size(self) -> int: + """ + Get the size of the file in bytes - this is an extra function and the best we can do using OMC. + """ + if not self.is_file(): + raise OMCSessionException(f"Path {self.as_posix()} is not a file!") + + res = self._session.sendExpression(expr=f'stat("{self.as_posix()}")') + if res[0]: + return int(res[1]) + + raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!") + + OMCPath = _OMCPath class ModelExecutionException(Exception): @@ -570,13 +636,13 @@ def escape_str(value: str) -> str: """ return OMCSession.escape_str(value=value) - def omcpath(self, *path) -> OMCPath: + def omcpath(self, *path) -> OMPathABC: """ Create an OMCPath object based on the given path segments and the current OMC process definition. """ return self.omc_process.omcpath(*path) - def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: + def omcpath_tempdir(self, tempdir_base: Optional[OMPathABC] = None) -> OMPathABC: """ Get a temporary directory using OMC. It is our own implementation as non-local usage relies on OMC to run all filesystem related access. @@ -796,21 +862,21 @@ def get_version(self) -> str: """ return self.sendExpression("getVersion()", parsed=True) - def set_workdir(self, workdir: OMCPath) -> None: + def set_workdir(self, workdir: OMPathABC) -> None: """ Set the workdir for this session. """ exp = f'cd("{workdir.as_posix()}")' self.sendExpression(exp) - def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: + def model_execution_prefix(self, cwd: Optional[OMPathABC] = None) -> list[str]: """ Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. """ return [] - def omcpath(self, *path) -> OMCPath: + def omcpath(self, *path) -> OMPathABC: """ Create an OMCPath object based on the given path segments and the current OMCSession* class. """ @@ -823,7 +889,7 @@ def omcpath(self, *path) -> OMCPath: raise OMCSessionException("OMCPath is supported for Python < 3.12 only if OMCSessionLocal is used!") return OMCPath(*path, session=self) - def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: + def omcpath_tempdir(self, tempdir_base: Optional[OMPathABC] = None) -> OMPathABC: """ Get a temporary directory using OMC. It is our own implementation as non-local usage relies on OMC to run all filesystem related access. @@ -840,10 +906,10 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: return self._tempdir(tempdir_base=tempdir_base) @staticmethod - def _tempdir(tempdir_base: OMCPath) -> OMCPath: + def _tempdir(tempdir_base: OMPathABC) -> OMPathABC: names = [str(uuid.uuid4()) for _ in range(100)] - tempdir: Optional[OMCPath] = None + tempdir: Optional[OMPathABC] = None for name in names: # create a unique temporary directory name tempdir = tempdir_base / name @@ -1243,15 +1309,15 @@ def get_docker_container_id(self) -> str: return self._docker_container_id - def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: + def model_execution_prefix(self, cwd: Optional[OMPathABC] = None) -> list[str]: """ Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. """ docker_cmd = [ "docker", "exec", "--user", str(self._getuid()), - ] - if isinstance(cwd, OMCPath): + ] + if isinstance(cwd, OMPathABC): docker_cmd += ["--workdir", cwd.as_posix()] docker_cmd += self._docker_extra_args if isinstance(self._docker_container_id, str): @@ -1520,7 +1586,7 @@ def __init__( # connect to the running omc instance using ZMQ self._omc_port = self._omc_port_get() - def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: + def model_execution_prefix(self, cwd: Optional[OMPathABC] = None) -> list[str]: """ Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. """ @@ -1530,7 +1596,7 @@ def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: wsl_cmd += ['--distribution', self._wsl_distribution] if isinstance(self._wsl_user, str): wsl_cmd += ['--user', self._wsl_user] - if isinstance(cwd, OMCPath): + if isinstance(cwd, OMPathABC): wsl_cmd += ['--cd', cwd.as_posix()] wsl_cmd += ['--'] diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 9f4408d5..ae47e747 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -23,7 +23,9 @@ doe_get_solutions, ) from OMPython.OMCSession import ( + OMPathABC, OMCPath, + OMCSession, ModelExecutionData, @@ -53,6 +55,7 @@ 'ModelicaDoEOMC', 'ModelicaSystemError', + 'OMPathABC', 'OMCPath', 'OMCSession',