Skip to content
Open
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 modelon/impact/client/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ def __set_name__(self, owner: str, name: str) -> None:
else:
delattr(owner, name)

def __call__(self, *args: Any, **kwargs: Any) -> None:
self.fn(*args, **kwargs)
def __call__(self, *args: Any, **kwargs: Any) -> Any:
return self.fn(*args, **kwargs)
9 changes: 9 additions & 0 deletions modelon/impact/client/entities/interfaces/package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from abc import ABC, abstractmethod


class PackageInterface(ABC):
@property
@abstractmethod
def name(self) -> str:
"""Package name."""
raise NotImplementedError
61 changes: 59 additions & 2 deletions modelon/impact/client/entities/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import logging
import os
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union

from modelon.impact.client.configuration import Experimental
from modelon.impact.client.entities.interfaces.model import ModelInterface
from modelon.impact.client.entities.model_executable import ModelExecutable
from modelon.impact.client.entities.project import Project
Expand All @@ -30,12 +32,19 @@
from modelon.impact.client.entities.experiment import Experiment
from modelon.impact.client.entities.external_result import ExternalResult
from modelon.impact.client.operations.base import BaseOperation
from modelon.impact.client.sal.modeling import ModelingService
from modelon.impact.client.sal.service import Service

CaseOrExperimentOrExternalResult = Union[Case, Experiment, ExternalResult]

logger = logging.getLogger(__name__)


@dataclass
class ModelParameter:
name: str


CompilationOperations = Union[ModelExecutableOperation, CachedModelExecutableOperation]

RuntimeOptionsOrDict = Union[RuntimeOptions, Dict[str, Any]]
Expand Down Expand Up @@ -78,12 +87,18 @@ class Model(ModelInterface):
"""Class containing Model functionalities."""

def __init__(
self, class_name: str, workspace_id: str, project_id: str, service: Service
self,
class_name: str,
workspace_id: str,
project_id: str,
service: Service,
modeling_sal_getter: Optional[Callable[[], "ModelingService"]] = None,
):
self._class_name = class_name
self._workspace_id = workspace_id
self._project_id = project_id
self._sal = service
self._modeling_sal_getter = modeling_sal_getter

def __repr__(self) -> str:
return f"Model name '{self._class_name}'"
Expand Down Expand Up @@ -399,6 +414,48 @@ def import_fmu(
Model.from_operation,
)

def _require_modeling_session(
self, method_name: str
) -> Callable[[], "ModelingService"]:
if self._modeling_sal_getter is None:
raise RuntimeError(
"This Model instance has no modeling session. "
f"Obtain the model via modeling_session.get_model() to enable "
f"{method_name}()."
)
return self._modeling_sal_getter

@Experimental
def get_parameters(self) -> List[ModelParameter]:
"""Returns the parameters for this model's class.

Example::

with workspace.new_modeling_session() as session:
model = session.get_model("LibA.Model")
parameters = model.get_parameters()

"""
modeling_sal = self._require_modeling_session("get_parameters")
return [
ModelParameter(name=name)
for name in modeling_sal().get_model_parameters(self._class_name)
]

@Experimental
def get_source(self) -> str:
"""Returns the Modelica source code for this model's class.

Example::

with workspace.new_modeling_session() as session:
model = session.get_model("LibA.Model")
source = model.get_source()

"""
modeling_sal = self._require_modeling_session("get_source")
return modeling_sal().get_model_source(self._class_name)

@classmethod
def from_operation(cls, operation: BaseOperation[Model], **kwargs: Any) -> Model:
assert isinstance(operation, FMUImportOperation)
Expand Down
76 changes: 76 additions & 0 deletions modelon/impact/client/entities/modeling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

from typing import TYPE_CHECKING, List

from modelon.impact.client.configuration import Experimental
from modelon.impact.client.entities.package import Package
from modelon.impact.client.sal.modeling import ModelingService
from modelon.impact.client.sal.service import Service

if TYPE_CHECKING:
from modelon.impact.client.entities.model import Model


class ModelingSession:
"""Class containing ModelingService functionalities."""

def __init__(
self,
session_id: str,
workspace_id: str,
project_id: str,
modeling_sal: ModelingService,
service: Service,
):
self._session_id = session_id
self._workspace_id = workspace_id
self._project_id = project_id
self._modeling_sal = modeling_sal
self._sal = service

def __eq__(self, obj: object) -> bool:
return isinstance(obj, ModelingSession) and obj._session_id == self._session_id

def __repr__(self) -> str:
return f"Modeling session with ID '{self._session_id}'"

def close(self) -> None:
return self._modeling_sal.close_session()

@Experimental
def get_model(self, class_name: str) -> "Model":
"""Returns a Model class object backed by this modeling session.

Example::

with workspace.new_modeling_session() as session:
model = session.get_model("LibA.Model")
parameters = model.get_parameters()

"""
from modelon.impact.client.entities.model import Model

return Model(
class_name,
self._workspace_id,
self._project_id,
self._sal,
modeling_sal_getter=lambda: self._modeling_sal,
)

@Experimental
def get_top_level_packages(self) -> List[Package]:
libraries = self._modeling_sal.get_top_classes()
packages = []
for project_id, top_classes in libraries.items():
for top_class in top_classes:
packages.append(
Package(
top_class["name"],
self._workspace_id,
project_id,
self._sal,
self._modeling_sal,
)
)
return packages
80 changes: 80 additions & 0 deletions modelon/impact/client/entities/package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Any, Dict, List

from modelon.impact.client.entities.interfaces.package import PackageInterface
from modelon.impact.client.entities.model import Model
from modelon.impact.client.sal.modeling import ModelingService
from modelon.impact.client.sal.service import Service


class Package(PackageInterface):
"""Class containing Package functionalities."""

def __init__(
self,
class_path: str,
workspace_id: str,
project_id: str,
service: Service,
modeling_sal: ModelingService,
):
self._class_path = class_path
self._workspace_id = workspace_id
self._project_id = project_id
self._sal = service
self._modeling_sal = modeling_sal

def __repr__(self) -> str:
return f"Package name '{self._class_path}'"

def __eq__(self, obj: object) -> bool:
return isinstance(obj, Package) and obj._class_path == self._class_path

@property
def name(self) -> str:
"""Package name."""
return self._class_path

@property
def project_id(self) -> str:
"""Project ID."""
return self._project_id

def _info(self) -> Dict[str, Any]:
return self._modeling_sal.get_top_class_info(self._class_path)

@property
def is_editable(self) -> bool:
"""Returns True if the package is editable."""
return self._info()["editable"]

@property
def is_structured(self) -> bool:
"""Returns True if the package is structured."""
return self._info()["structured"]

@property
def is_enabled(self) -> bool:
"""Returns True if the package is enabled."""
return self._info()["enabled"]

def get_uses(self) -> Dict[str, str]:
"""Returns the libraries the package has dependencies on."""
return self._info()["uses"]

def get_models(self) -> List[Model]:
return [
Model(class_path, self._workspace_id, self._project_id, self._sal)
for class_path in self._modeling_sal.get_models(self._class_path)
]

def get_subpackages(self) -> List["Package"]:
return [
Package(
class_path,
self._workspace_id,
self._project_id,
self._sal,
self._modeling_sal,
)
for class_path in self._modeling_sal.get_subpackages(self._class_path)
]
28 changes: 26 additions & 2 deletions modelon/impact/client/entities/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import json
import logging
import os
from contextlib import contextmanager
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union

from modelon.impact.client.configuration import Experimental
from modelon.impact.client.entities.custom_function import CustomFunction
Expand All @@ -15,6 +16,7 @@
from modelon.impact.client.entities.interfaces.workspace import WorkspaceInterface
from modelon.impact.client.entities.model import Model
from modelon.impact.client.entities.model_executable import ModelExecutable
from modelon.impact.client.entities.modeling import ModelingSession
from modelon.impact.client.entities.project import Project, VcsUri
from modelon.impact.client.exceptions import (
NoAssociatedPublishedWorkspaceError,
Expand Down Expand Up @@ -678,6 +680,23 @@ def name(self) -> str:
"""Workspace name."""
return self.definition.name

@Experimental
@contextmanager
def new_modeling_session(
self, project: Optional[Project] = None
) -> Generator["ModelingSession", None, None]:
"""Context manager for an isolated modeling session with explicit lifetime
control."""
project = project or self.get_default_project()
modeling_service = self._sal.start_modeling_session(workspace_id=self.id)
session = ModelingSession(
modeling_service.id, self.id, project.id, modeling_service, self._sal
)
try:
yield session
finally:
session.close()

def rename(self, new_name: str) -> None:
"""Renames a workspace.

Expand Down Expand Up @@ -872,7 +891,12 @@ def get_model(self, class_name: str, project: Optional[Project] = None) -> Model

"""
project = project or self.get_default_project()
return Model(class_name, self._workspace_id, project.id, self._sal)
return Model(
class_name,
self._workspace_id,
project.id,
self._sal,
)

def get_fmus(self) -> List[ModelExecutable]:
"""Returns a list of ModelExecutable class objects.
Expand Down
4 changes: 4 additions & 0 deletions modelon/impact/client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ class RemotePublishedWorkspaceLinkError(Error):

class AuthenticationError(Error):
pass


class FailedToStartModelingServer(Error):
pass
47 changes: 47 additions & 0 deletions modelon/impact/client/sal/modeling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Modeling service module."""
from typing import Any, Dict, List

from modelon.impact.client.sal.ws import SyncWebSocketClient


class ModelingService:
def __init__(self, ws_client: SyncWebSocketClient):
self._ws_client = ws_client

@property
def id(self) -> str:
return self._ws_client.session_id

def get_top_class_info(self, class_path: str) -> Any:
params = {"className": class_path}
return self._ws_client.get_json_response("impact/getTopClassInfo", params)

def get_model_parameters(self, modelica_path: str) -> List[str]:
return self._ws_client.get_json_response(
"impact/getModelParameters", modelica_path
)

def get_model_source(self, class_path: str) -> str:
params = {"className": class_path}
return self._ws_client.get_json_response("impact/getSource", params)

def get_models(self, modelica_path: str) -> List[str]:
return self._ws_client.get_json_response("impact/listModels", modelica_path)

def get_subpackages(self, modelica_path: str) -> List[str]:
return self._ws_client.get_json_response("impact/listPackages", modelica_path)

def get_projects(self) -> Any:
return self._ws_client.get_json_response("impact/getProjects", None)

def get_top_classes(self) -> Dict[str, List[Any]]:
projects = self.get_projects()
return {
project.get("id"): [
top_class for top_class in project.get("topClasses", [])
]
for project in projects
}

def close_session(self) -> None:
return self._ws_client.close()
Loading