ansys-mechanical-core 0.11.12__py3-none-any.whl → 0.11.14__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 (29) hide show
  1. ansys/mechanical/core/__init__.py +0 -1
  2. ansys/mechanical/core/_version.py +48 -48
  3. ansys/mechanical/core/embedding/app.py +681 -610
  4. ansys/mechanical/core/embedding/background.py +11 -2
  5. ansys/mechanical/core/embedding/enum_importer.py +0 -1
  6. ansys/mechanical/core/embedding/global_importer.py +50 -0
  7. ansys/mechanical/core/embedding/imports.py +30 -58
  8. ansys/mechanical/core/embedding/initializer.py +6 -4
  9. ansys/mechanical/core/embedding/logger/__init__.py +219 -219
  10. ansys/mechanical/core/embedding/messages.py +190 -0
  11. ansys/mechanical/core/embedding/resolver.py +48 -41
  12. ansys/mechanical/core/embedding/rpc/__init__.py +36 -0
  13. ansys/mechanical/core/embedding/rpc/client.py +259 -0
  14. ansys/mechanical/core/embedding/rpc/server.py +403 -0
  15. ansys/mechanical/core/embedding/rpc/utils.py +118 -0
  16. ansys/mechanical/core/embedding/runtime.py +22 -0
  17. ansys/mechanical/core/embedding/transaction.py +51 -0
  18. ansys/mechanical/core/feature_flags.py +1 -0
  19. ansys/mechanical/core/ide_config.py +226 -212
  20. ansys/mechanical/core/mechanical.py +2399 -2324
  21. ansys/mechanical/core/misc.py +176 -176
  22. ansys/mechanical/core/pool.py +712 -712
  23. ansys/mechanical/core/run.py +321 -321
  24. {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.14.dist-info}/METADATA +40 -27
  25. ansys_mechanical_core-0.11.14.dist-info/RECORD +52 -0
  26. {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.14.dist-info}/WHEEL +1 -1
  27. ansys_mechanical_core-0.11.12.dist-info/RECORD +0 -45
  28. {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.14.dist-info}/entry_points.txt +0 -0
  29. {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.14.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,190 @@
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(self, filter="Severity;DisplayString"):
146
+ """Print all messages with full details.
147
+
148
+ Parameters
149
+ ----------
150
+ filter : str, optional
151
+ Semicolon separated list of message attributes to display.
152
+ Default is "severity;message".
153
+ if filter is "*", all available attributes will be displayed.
154
+
155
+ Examples
156
+ --------
157
+ >>> app.messages.show()
158
+ ... severity: info
159
+ ... message: Sample message.
160
+
161
+ >>> app.messages.show(filter="time_stamp;severity;message")
162
+ ... time_stamp: 1/30/2025 12:10:35 PM
163
+ ... severity: info
164
+ ... message: Sample message.
165
+ """
166
+ if self._messages.Count == 0:
167
+ print("No messages to display.")
168
+ return
169
+
170
+ if filter == "*":
171
+ selected_columns = [
172
+ "TimeStamp",
173
+ "Severity",
174
+ "DisplayString",
175
+ "Source",
176
+ "StringID",
177
+ "Location",
178
+ "RelatedObjects",
179
+ ]
180
+ else:
181
+ selected_columns = [col.strip() for col in filter.split(";")]
182
+
183
+ for msg in self._messages:
184
+ for key in selected_columns:
185
+ print(f"{key}: {getattr(msg, key, 'Specified attribute not found.')}")
186
+ print()
187
+
188
+ def clear(self):
189
+ """Clear all messages."""
190
+ self._messages.Clear()
@@ -1,41 +1,48 @@
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
- """This is the .NET assembly resolving for embedding Ansys Mechanical.
24
-
25
- Note that for some Mechanical Addons - additional resolving may be
26
- necessary. A resolve handler is shipped with Ansys Mechanical on Windows
27
- starting in version 23.1 and on Linux starting in version 23.2
28
- """
29
-
30
-
31
- def resolve(version):
32
- """Resolve function for all versions of Ansys Mechanical."""
33
- import clr # isort: skip
34
- import System # isort: skip
35
-
36
- clr.AddReference("Ansys.Mechanical.Embedding")
37
- import Ansys # isort: skip
38
-
39
- assembly_resolver = Ansys.Mechanical.Embedding.AssemblyResolver
40
- resolve_handler = assembly_resolver.MechanicalResolveEventHandler
41
- System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler
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
+ """This is the .NET assembly resolving for embedding Ansys Mechanical.
24
+
25
+ Note that for some Mechanical Addons - additional resolving may be
26
+ necessary. A resolve handler is shipped with Ansys Mechanical on Windows
27
+ starting in version 23.1 and on Linux starting in version 23.2
28
+ """
29
+
30
+
31
+ def resolve(version):
32
+ """Resolve function for all versions of Ansys Mechanical."""
33
+ import clr # isort: skip
34
+ import System # isort: skip
35
+
36
+ clr.AddReference("Ansys.Mechanical.Embedding")
37
+ import Ansys # isort: skip
38
+
39
+ try:
40
+ assembly_resolver = Ansys.Mechanical.Embedding.AssemblyResolver
41
+ resolve_handler = assembly_resolver.MechanicalResolveEventHandler
42
+ System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler
43
+ except AttributeError:
44
+ error_msg = f"""Unable to resolve Mechanical assemblies. Please ensure the following:
45
+ 1. Mechanical is installed.
46
+ 2. A folder with the name "Ansys" does not exist in the same directory as the script being run.
47
+ """
48
+ raise AttributeError(error_msg)
@@ -0,0 +1,36 @@
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
+ """RPC and Mechanical service implementation."""
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
30
+ from .server import (
31
+ DefaultServiceMethods,
32
+ MechanicalDefaultServer,
33
+ MechanicalEmbeddedServer,
34
+ MechanicalService,
35
+ )
36
+ from .utils import get_remote_methods, remote_method
@@ -0,0 +1,259 @@
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
+ """Client for Mechanical services."""
23
+
24
+ import os
25
+ import pathlib
26
+ import time
27
+
28
+ import rpyc
29
+
30
+ from ansys.mechanical.core.embedding.rpc.server import PYMECHANICAL_DEFAULT_RPC_PORT
31
+ from ansys.mechanical.core.mechanical import DEFAULT_CHUNK_SIZE
32
+
33
+
34
+ class Client:
35
+ """Client for connecting to Mechanical services."""
36
+
37
+ def __init__(self, host: str, port: int, timeout: float = 120.0, cleanup_on_exit=True):
38
+ """Initialize the client.
39
+
40
+ Parameters
41
+ ----------
42
+ host : str, optional
43
+ IP address to connect to the server. The default is ``None``
44
+ in which case ``localhost`` is used.
45
+ port : int, optional
46
+ Port to connect to the Mecahnical server. The default is ``None``,
47
+ in which case ``20000`` is used.
48
+ timeout : float, optional
49
+ Maximum allowable time for connecting to the Mechanical server.
50
+ The default is ``60.0``.
51
+
52
+ """
53
+ if host is None:
54
+ host = "localhost"
55
+ self.host = host
56
+ if port is None:
57
+ port = PYMECHANICAL_DEFAULT_RPC_PORT
58
+ self.port = port
59
+ self.timeout = timeout
60
+ self.connection = None
61
+ self.root = None
62
+ self._connect()
63
+ self._cleanup_on_exit = cleanup_on_exit
64
+
65
+ def __getattr__(self, attr):
66
+ """Get attribute from the root object."""
67
+ if hasattr(self.root, attr):
68
+ return getattr(self.root, attr)
69
+ propget_name = f"propget_{attr}"
70
+ if hasattr(self.root, propget_name):
71
+ exposed_fget = getattr(self.root, propget_name)
72
+ return exposed_fget()
73
+ return self.__dict__.items[attr]
74
+
75
+ # TODO - implement setattr
76
+ # def __setattr__(self, attr, value):
77
+ # if hasattr(self.root, attr):
78
+ # inner_prop = getattr(self.root.__class__, attr)
79
+ # if isinstance(inner_prop, property):
80
+ # inner_prop.fset(self.root, value)
81
+ # else:
82
+ # super().__setattr__(attr, value)
83
+
84
+ def _connect(self):
85
+ self._wait_until_ready()
86
+ self.root = self.connection.root
87
+ print(f"Connected to {self.host}:{self.port}")
88
+
89
+ def _exponential_backoff(self, max_time=60.0, base_time=0.1, factor=2):
90
+ """Generate exponential backoff timing."""
91
+ t_max = time.time() + max_time
92
+ t = base_time
93
+ while time.time() < t_max:
94
+ yield t
95
+ t = min(t * factor, max_time)
96
+
97
+ def _wait_until_ready(self):
98
+ """Wait until the server is ready."""
99
+ t_max = time.time() + self.timeout
100
+ for delay in self._exponential_backoff(max_time=self.timeout):
101
+ if time.time() >= t_max:
102
+ break # Exit if the timeout is reached
103
+ try:
104
+ self.connection = rpyc.connect(self.host, self.port)
105
+ self.connection.ping()
106
+ print("Server is ready.")
107
+ return
108
+ except Exception:
109
+ time.sleep(delay)
110
+
111
+ raise TimeoutError(
112
+ f"Server at {self.host}:{self.port} not ready within {self.timeout} seconds."
113
+ )
114
+
115
+ def close(self):
116
+ """Close the connection."""
117
+ self.connection.close()
118
+ print(f"Connection to {self.host}:{self.port} closed")
119
+
120
+ def upload(
121
+ self,
122
+ file_name,
123
+ file_location_destination=None,
124
+ chunk_size=DEFAULT_CHUNK_SIZE,
125
+ progress_bar=False,
126
+ ):
127
+ """Upload a file to the server."""
128
+ print(f"arg: {file_name}, {file_location_destination}")
129
+ print()
130
+ if not os.path.exists(file_name):
131
+ print(f"File {file_name} does not exist.")
132
+ return
133
+ file_base_name = os.path.basename(file_name)
134
+ remote_path = os.path.join(file_location_destination, file_base_name)
135
+
136
+ with open(file_name, "rb") as f:
137
+ file_data = f.read()
138
+ self.service_upload(remote_path, file_data)
139
+
140
+ print(f"File {file_name} uploaded to {file_location_destination}")
141
+
142
+ def download(
143
+ self,
144
+ files,
145
+ target_dir=None,
146
+ chunk_size=DEFAULT_CHUNK_SIZE,
147
+ progress_bar=None,
148
+ recursive=False,
149
+ ):
150
+ """Download a file from the server."""
151
+ out_files = []
152
+ os.makedirs(target_dir, exist_ok=True)
153
+
154
+ response = self.service_download(files)
155
+
156
+ if isinstance(response, dict) and response["is_directory"]:
157
+ for relative_file_path in response["files"]:
158
+ full_remote_path = os.path.join(files, relative_file_path)
159
+ local_file_path = os.path.join(target_dir, relative_file_path)
160
+ local_file_dir = os.path.dirname(local_file_path)
161
+ os.makedirs(local_file_dir, exist_ok=True)
162
+
163
+ out_file_path = self._download_file(
164
+ full_remote_path, local_file_path, chunk_size, overwrite=True
165
+ )
166
+ else:
167
+ out_file_path = self._download_file(
168
+ files, os.path.join(target_dir, os.path.basename(files)), chunk_size, overwrite=True
169
+ )
170
+ out_files.append(out_file_path)
171
+
172
+ return out_files
173
+
174
+ def _download_file(self, remote_file_path, local_file_path, chunk_size=1024, overwrite=False):
175
+ if os.path.exists(local_file_path) and not overwrite:
176
+ print(f"File {local_file_path} already exists locally. Skipping download.")
177
+ return
178
+ response = self.service_download(remote_file_path)
179
+ if isinstance(response, dict):
180
+ raise ValueError("Expected a file download, but got a directory response.")
181
+ file_data = response
182
+
183
+ # Write the file data to the local path
184
+ with open(local_file_path, "wb") as f:
185
+ f.write(file_data)
186
+
187
+ print(f"File {remote_file_path} downloaded to {local_file_path}")
188
+
189
+ return local_file_path
190
+
191
+ def download_project(self, extensions=None, target_dir=None, progress_bar=False):
192
+ """Download all project files in the working directory of the Mechanical instance."""
193
+ destination_directory = target_dir.rstrip("\\/")
194
+ if destination_directory:
195
+ path = pathlib.Path(destination_directory)
196
+ path.mkdir(parents=True, exist_ok=True)
197
+ else:
198
+ destination_directory = os.getcwd()
199
+ # relative directory?
200
+ if os.path.isdir(destination_directory):
201
+ if not os.path.isabs(destination_directory):
202
+ # construct full path
203
+ destination_directory = os.path.join(os.getcwd(), destination_directory)
204
+ _project_directory = self.project_directory
205
+ _project_directory = _project_directory.rstrip("\\/")
206
+
207
+ # this is where .mechddb resides
208
+ parent_directory = os.path.dirname(_project_directory)
209
+
210
+ list_of_files = []
211
+
212
+ if not extensions:
213
+ files = self.list_files()
214
+ else:
215
+ files = []
216
+ for each_extension in extensions:
217
+ # mechdb resides one level above project directory
218
+ if "mechdb" == each_extension.lower():
219
+ file_temp = os.path.join(parent_directory, f"*.{each_extension}")
220
+ else:
221
+ file_temp = os.path.join(_project_directory, "**", f"*.{each_extension}")
222
+
223
+ list_files = self._get_files(file_temp, recursive=False)
224
+
225
+ files.extend(list_files)
226
+
227
+ for file in files:
228
+ # create similar hierarchy locally
229
+ new_path = file.replace(parent_directory, destination_directory)
230
+ new_path_dir = os.path.dirname(new_path)
231
+ temp_files = self.download(
232
+ files=file, target_dir=new_path_dir, progress_bar=progress_bar
233
+ )
234
+ list_of_files.extend(temp_files)
235
+ return list_of_files
236
+
237
+ @property
238
+ def is_alive(self):
239
+ """Check if the Mechanical instance is alive."""
240
+ try:
241
+ self.connection.ping()
242
+ return True
243
+ except:
244
+ return False
245
+
246
+ def exit(self):
247
+ """Shuts down the Mechanical instance."""
248
+ print("Requesting server shutdown ...")
249
+ self.service_exit()
250
+ self.connection.close()
251
+ print("Disconnected from server")
252
+
253
+ def __del__(self): # pragma: no cover
254
+ """Clean up on exit."""
255
+ if self._cleanup_on_exit:
256
+ try:
257
+ self.exit()
258
+ except Exception as e:
259
+ print(f"Failed to exit cleanly: {e}")