ansys-mechanical-core 0.11.13__py3-none-any.whl → 0.11.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. ansys/mechanical/core/__init__.py +3 -4
  2. ansys/mechanical/core/embedding/app.py +97 -12
  3. ansys/mechanical/core/embedding/appdata.py +26 -22
  4. ansys/mechanical/core/embedding/enum_importer.py +5 -0
  5. ansys/mechanical/core/embedding/global_importer.py +50 -0
  6. ansys/mechanical/core/embedding/{viz → graphics}/embedding_plotter.py +1 -1
  7. ansys/mechanical/core/embedding/imports.py +30 -58
  8. ansys/mechanical/core/embedding/initializer.py +76 -4
  9. ansys/mechanical/core/embedding/messages.py +195 -0
  10. ansys/mechanical/core/embedding/resolver.py +1 -1
  11. ansys/mechanical/core/embedding/rpc/__init__.py +3 -7
  12. ansys/mechanical/core/embedding/rpc/client.py +55 -19
  13. ansys/mechanical/core/embedding/rpc/default_server.py +131 -0
  14. ansys/mechanical/core/embedding/rpc/server.py +171 -162
  15. ansys/mechanical/core/embedding/rpc/utils.py +18 -2
  16. ansys/mechanical/core/embedding/runtime.py +6 -0
  17. ansys/mechanical/core/embedding/transaction.py +51 -0
  18. ansys/mechanical/core/ide_config.py +22 -7
  19. ansys/mechanical/core/mechanical.py +86 -18
  20. {ansys_mechanical_core-0.11.13.dist-info → ansys_mechanical_core-0.11.15.dist-info}/METADATA +21 -17
  21. ansys_mechanical_core-0.11.15.dist-info/RECORD +53 -0
  22. {ansys_mechanical_core-0.11.13.dist-info → ansys_mechanical_core-0.11.15.dist-info}/WHEEL +1 -1
  23. ansys_mechanical_core-0.11.13.dist-info/RECORD +0 -49
  24. /ansys/mechanical/core/embedding/{viz → graphics}/__init__.py +0 -0
  25. /ansys/mechanical/core/embedding/{viz → graphics}/usd_converter.py +0 -0
  26. /ansys/mechanical/core/embedding/{viz → graphics}/utils.py +0 -0
  27. {ansys_mechanical_core-0.11.13.dist-info → ansys_mechanical_core-0.11.15.dist-info}/entry_points.txt +0 -0
  28. {ansys_mechanical_core-0.11.13.dist-info → ansys_mechanical_core-0.11.15.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,195 @@
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """Message Manager for App."""
24
+
25
+ # TODO: add functionality to filter only errors, warnings, info
26
+ # TODO: add max number of messages to display
27
+ # TODO: implement pep8 formatting
28
+
29
+ try: # noqa: F401
30
+ import pandas as pd
31
+
32
+ HAS_PANDAS = True
33
+ """Whether or not pandas exists."""
34
+ except ImportError:
35
+ HAS_PANDAS = False
36
+
37
+
38
+ class MessageManager:
39
+ """Message manager for adding, fetching, and printing messages."""
40
+
41
+ def __init__(self, app):
42
+ """Initialize the message manager."""
43
+ self._app = app
44
+
45
+ # Import necessary classes
46
+ from Ansys.Mechanical.Application import Message
47
+ from Ansys.Mechanical.DataModel.Enums import MessageSeverityType
48
+
49
+ self._message_severity = MessageSeverityType
50
+ self._message = Message
51
+ self._messages = self._app.ExtAPI.Application.Messages
52
+
53
+ def _create_messages_data(self): # pragma: no cover
54
+ """Update the local cache of messages."""
55
+ data = {
56
+ "Severity": [],
57
+ "TimeStamp": [],
58
+ "DisplayString": [],
59
+ "Source": [],
60
+ "StringID": [],
61
+ "Location": [],
62
+ "RelatedObjects": [],
63
+ }
64
+ for msg in self._app.ExtAPI.Application.Messages:
65
+ data["Severity"].append(str(msg.Severity).upper())
66
+ data["TimeStamp"].append(msg.TimeStamp)
67
+ data["DisplayString"].append(msg.DisplayString)
68
+ data["Source"].append(msg.Source)
69
+ data["StringID"].append(msg.StringID)
70
+ data["Location"].append(msg.Location)
71
+ data["RelatedObjects"].append(msg.RelatedObjects)
72
+
73
+ return data
74
+
75
+ def __repr__(self): # pragma: no cover
76
+ """Provide a DataFrame representation of all messages."""
77
+ if not HAS_PANDAS:
78
+ return "Pandas is not available. Please pip install pandas to display messages."
79
+ data = self._create_messages_data()
80
+ return repr(pd.DataFrame(data))
81
+
82
+ def __str__(self):
83
+ """Provide a custom string representation of the messages."""
84
+ if self._messages.Count == 0:
85
+ return "No messages to display."
86
+
87
+ formatted_messages = [f"[{msg.Severity}] : {msg.DisplayString}" for msg in self._messages]
88
+ return "\n".join(formatted_messages)
89
+
90
+ def __getitem__(self, index):
91
+ """Allow indexed access to messages."""
92
+ if len(self._messages) == 0:
93
+ raise IndexError("No messages are available.")
94
+ if index >= len(self._messages) or index < 0:
95
+ raise IndexError("Message index out of range.")
96
+ return self._messages[index]
97
+
98
+ def __len__(self):
99
+ """Return the number of messages."""
100
+ return self._messages.Count
101
+
102
+ def add(self, severity: str, text: str):
103
+ """Add a message and update the cache.
104
+
105
+ Parameters
106
+ ----------
107
+ severity : str
108
+ Severity of the message. Can be "info", "warning", or "error".
109
+ text : str
110
+ Message text.
111
+
112
+ Examples
113
+ --------
114
+ >>> app.messages.add("info", "User clicked the start button.")
115
+ """
116
+ severity_map = {
117
+ "info": self._message_severity.Info,
118
+ "warning": self._message_severity.Warning,
119
+ "error": self._message_severity.Error,
120
+ }
121
+
122
+ if severity.lower() not in severity_map:
123
+ raise ValueError(f"Invalid severity: {severity}")
124
+
125
+ _msg = self._message(text, severity_map[severity.lower()])
126
+ self._messages.Add(_msg)
127
+
128
+ def remove(self, index: int):
129
+ """Remove a message by index.
130
+
131
+ Parameters
132
+ ----------
133
+ index : int
134
+ Index of the message to remove.
135
+
136
+ Examples
137
+ --------
138
+ >>> app.messages.remove(0)
139
+ """
140
+ if index >= len(self._app.ExtAPI.Application.Messages) or index < 0:
141
+ raise IndexError("Message index out of range.")
142
+ _msg = self._messages[index]
143
+ self._messages.Remove(_msg)
144
+
145
+ def _show_string(self, filter: str = "Severity;DisplayString") -> str:
146
+ if self._messages.Count == 0:
147
+ return "No messages to display."
148
+
149
+ if filter == "*":
150
+ selected_columns = [
151
+ "TimeStamp",
152
+ "Severity",
153
+ "DisplayString",
154
+ "Source",
155
+ "StringID",
156
+ "Location",
157
+ "RelatedObjects",
158
+ ]
159
+ else:
160
+ selected_columns = [col.strip() for col in filter.split(";")]
161
+
162
+ lines = []
163
+ for msg in self._messages:
164
+ for key in selected_columns:
165
+ line = f"{key}: {getattr(msg, key, 'Specified attribute not found.')}"
166
+ lines.append(line)
167
+ return "\n".join(lines)
168
+
169
+ def show(self, filter="Severity;DisplayString") -> None:
170
+ """Print all messages with full details.
171
+
172
+ Parameters
173
+ ----------
174
+ filter : str, optional
175
+ Semicolon separated list of message attributes to display.
176
+ Default is "severity;message".
177
+ if filter is "*", all available attributes will be displayed.
178
+
179
+ Examples
180
+ --------
181
+ >>> app.messages.show()
182
+ ... severity: info
183
+ ... message: Sample message.
184
+
185
+ >>> app.messages.show(filter="time_stamp;severity;message")
186
+ ... time_stamp: 1/30/2025 12:10:35 PM
187
+ ... severity: info
188
+ ... message: Sample message.
189
+ """
190
+ show_string = self._show_string(filter)
191
+ print(show_string)
192
+
193
+ def clear(self):
194
+ """Clear all messages."""
195
+ self._messages.Clear()
@@ -41,7 +41,7 @@ def resolve(version):
41
41
  resolve_handler = assembly_resolver.MechanicalResolveEventHandler
42
42
  System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler
43
43
  except AttributeError:
44
- error_msg = f"""Unable to resolve Mechanical assemblies. Please ensure the following:
44
+ error_msg = """Unable to resolve Mechanical assemblies. Please ensure the following:
45
45
  1. Mechanical is installed.
46
46
  2. A folder with the name "Ansys" does not exist in the same directory as the script being run.
47
47
  """
@@ -22,15 +22,11 @@
22
22
 
23
23
  """RPC and Mechanical service implementation."""
24
24
  from .client import Client
25
-
26
- # todo - provide an implementation of Server (RemoteMechancial) that installs the below
27
- # from .default_server import RemoteMechanical
28
- # and remove them from this import statement
29
- # todo - combine Server and MechanicalService
25
+ from .default_server import DefaultServiceMethods, MechanicalDefaultServer
30
26
  from .server import (
31
- DefaultServiceMethods,
32
- MechanicalDefaultServer,
33
27
  MechanicalEmbeddedServer,
34
28
  MechanicalService,
35
29
  )
36
30
  from .utils import get_remote_methods, remote_method
31
+
32
+ # todo - combine Server and MechanicalService
@@ -27,13 +27,16 @@ import time
27
27
 
28
28
  import rpyc
29
29
 
30
+ from ansys.mechanical.core.embedding.rpc.utils import PYMECHANICAL_DEFAULT_RPC_PORT
30
31
  from ansys.mechanical.core.mechanical import DEFAULT_CHUNK_SIZE
31
32
 
32
33
 
33
34
  class Client:
34
35
  """Client for connecting to Mechanical services."""
35
36
 
36
- def __init__(self, host: str, port: int, timeout: float = 120.0):
37
+ def __init__(
38
+ self, host: str, port: int, timeout: float = 120.0, cleanup_on_exit=True, process=None
39
+ ):
37
40
  """Initialize the client.
38
41
 
39
42
  Parameters
@@ -43,18 +46,28 @@ class Client:
43
46
  in which case ``localhost`` is used.
44
47
  port : int, optional
45
48
  Port to connect to the Mecahnical server. The default is ``None``,
46
- in which case ``10000`` is used.
49
+ in which case ``20000`` is used.
47
50
  timeout : float, optional
48
51
  Maximum allowable time for connecting to the Mechanical server.
49
52
  The default is ``60.0``.
53
+ process: subprocess.Popen, optional
54
+ The process object that was connected to
50
55
 
51
56
  """
57
+ if host is None:
58
+ host = "localhost"
52
59
  self.host = host
60
+ if port is None:
61
+ port = PYMECHANICAL_DEFAULT_RPC_PORT
53
62
  self.port = port
63
+ self._process = process
54
64
  self.timeout = timeout
55
65
  self.connection = None
56
66
  self.root = None
57
67
  self._connect()
68
+ self._cleanup_on_exit = cleanup_on_exit
69
+ self._error_type = Exception
70
+ self._has_exited = False
58
71
 
59
72
  def __getattr__(self, attr):
60
73
  """Get attribute from the root object."""
@@ -77,29 +90,38 @@ class Client:
77
90
 
78
91
  def _connect(self):
79
92
  self._wait_until_ready()
80
- self.connection = rpyc.connect(self.host, self.port)
81
93
  self.root = self.connection.root
82
94
  print(f"Connected to {self.host}:{self.port}")
83
- print(f"Installed methods")
95
+
96
+ def _exponential_backoff(self, max_time=60.0, base_time=0.1, factor=2):
97
+ """Generate exponential backoff timing."""
98
+ t_max = time.time() + max_time
99
+ t = base_time
100
+ while time.time() < t_max:
101
+ yield t
102
+ t = min(t * factor, max_time)
84
103
 
85
104
  def _wait_until_ready(self):
105
+ """Wait until the server is ready."""
86
106
  t_max = time.time() + self.timeout
87
- while time.time() < t_max:
107
+ for delay in self._exponential_backoff(max_time=self.timeout):
108
+ if time.time() >= t_max:
109
+ break # Exit if the timeout is reached
88
110
  try:
89
- conn = rpyc.connect(self.host, self.port)
90
- conn.ping() # Simple ping to check if the connection is healthy
91
- conn.close()
92
- print("Server is ready to connect")
93
- break
94
- except:
95
- time.sleep(2)
96
- else:
97
- raise TimeoutError(
98
- f"Server at {self.host}:{self.port} not ready within {self.timeout} seconds."
99
- )
111
+ self.connection = rpyc.connect(self.host, self.port)
112
+ self.connection.ping()
113
+ print("Server is ready.")
114
+ return
115
+ except Exception:
116
+ time.sleep(delay)
117
+
118
+ raise TimeoutError(
119
+ f"Server at {self.host}:{self.port} not ready within {self.timeout} seconds."
120
+ )
100
121
 
101
122
  def close(self):
102
123
  """Close the connection."""
124
+ print("Closing the connection")
103
125
  self.connection.close()
104
126
  print(f"Connection to {self.host}:{self.port} closed")
105
127
 
@@ -220,6 +242,11 @@ class Client:
220
242
  list_of_files.extend(temp_files)
221
243
  return list_of_files
222
244
 
245
+ @property
246
+ def backend(self) -> str:
247
+ """Get the backend type."""
248
+ return "python"
249
+
223
250
  @property
224
251
  def is_alive(self):
225
252
  """Check if the Mechanical instance is alive."""
@@ -231,7 +258,16 @@ class Client:
231
258
 
232
259
  def exit(self):
233
260
  """Shuts down the Mechanical instance."""
234
- print("Requesting server shutdown ...")
235
- self.root.service_exit()
236
- self.connection.close()
261
+ if self._has_exited:
262
+ return
263
+ self.close()
264
+ self._has_exited = True
237
265
  print("Disconnected from server")
266
+
267
+ def __del__(self): # pragma: no cover
268
+ """Clean up on exit."""
269
+ if self._cleanup_on_exit:
270
+ try:
271
+ self.exit()
272
+ except Exception as e:
273
+ print(f"Failed to exit cleanly: {e}")
@@ -0,0 +1,131 @@
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+ """Remote Procedure Call (RPC) server."""
23
+
24
+ import fnmatch
25
+ import os
26
+
27
+ from ansys.mechanical.core.embedding.app import App
28
+
29
+ from .server import MechanicalEmbeddedServer
30
+ from .utils import remote_method
31
+
32
+
33
+ class DefaultServiceMethods:
34
+ """Default service methods for MechanicalEmbeddedServer."""
35
+
36
+ def __init__(self, app: App):
37
+ """Initialize the DefaultServiceMethods."""
38
+ self._app = app
39
+
40
+ def __repr__(self):
41
+ """Return the representation of the instance."""
42
+ return '"ServiceMethods instance"'
43
+
44
+ @remote_method
45
+ def run_python_script(
46
+ self, script: str, enable_logging=False, log_level="WARNING", progress_interval=2000
47
+ ):
48
+ """Run scripts using Internal python engine."""
49
+ result = self._app.execute_script(script)
50
+ return result
51
+
52
+ @remote_method
53
+ def run_python_script_from_file(
54
+ self,
55
+ file_path: str,
56
+ enable_logging=False,
57
+ log_level="WARNING",
58
+ progress_interval=2000,
59
+ ):
60
+ """Run scripts using Internal python engine."""
61
+ return self._app.execute_script_from_file(file_path)
62
+
63
+ @remote_method
64
+ def clear(self):
65
+ """Clear the current project."""
66
+ self._app.new()
67
+
68
+ @property
69
+ @remote_method
70
+ def project_directory(self):
71
+ """Get the project directory."""
72
+ return self._app.ExtAPI.DataModel.Project.ProjectDirectory
73
+
74
+ @remote_method
75
+ def list_files(self):
76
+ """List all files in the project directory."""
77
+ list = []
78
+ mechdbPath = self._app.ExtAPI.DataModel.Project.FilePath
79
+ if mechdbPath != "":
80
+ list.append(mechdbPath)
81
+ rootDir = self._app.ExtAPI.DataModel.Project.ProjectDirectory
82
+
83
+ for dirPath, dirNames, fileNames in os.walk(rootDir):
84
+ for fileName in fileNames:
85
+ list.append(os.path.join(dirPath, fileName))
86
+ files_out = "\n".join(list).splitlines()
87
+ if not files_out: # pragma: no cover
88
+ print("No files listed")
89
+ return files_out
90
+
91
+ @remote_method
92
+ def _get_files(self, files, recursive=False):
93
+ self_files = self.list_files() # to avoid calling it too much
94
+
95
+ if isinstance(files, str):
96
+ if files in self_files:
97
+ list_files = [files]
98
+ elif "*" in files:
99
+ list_files = fnmatch.filter(self_files, files)
100
+ if not list_files:
101
+ raise ValueError(
102
+ f"The `'files'` parameter ({files}) didn't match any file using "
103
+ f"glob expressions in the remote server."
104
+ )
105
+ else:
106
+ raise ValueError(
107
+ f"The `'files'` parameter ('{files}') does not match any file or pattern."
108
+ )
109
+
110
+ elif isinstance(files, (list, tuple)):
111
+ if not all([isinstance(each, str) for each in files]):
112
+ raise ValueError(
113
+ "The parameter `'files'` can be a list or tuple, but it "
114
+ "should only contain strings."
115
+ )
116
+ list_files = files
117
+ else:
118
+ raise ValueError(
119
+ f"The `file` parameter type ({type(files)}) is not supported."
120
+ "Only strings, tuple of strings, or list of strings are allowed."
121
+ )
122
+
123
+ return list_files
124
+
125
+
126
+ class MechanicalDefaultServer(MechanicalEmbeddedServer):
127
+ """Default server with default service methods."""
128
+
129
+ def __init__(self, **kwargs):
130
+ """Initialize the MechanicalDefaultServer."""
131
+ super().__init__(impl=DefaultServiceMethods, **kwargs)