ansys-mechanical-core 0.11.11__py3-none-any.whl → 0.11.13__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.
- ansys/mechanical/core/_version.py +1 -0
- ansys/mechanical/core/embedding/app.py +2 -3
- ansys/mechanical/core/embedding/background.py +11 -2
- ansys/mechanical/core/embedding/enum_importer.py +1 -1
- ansys/mechanical/core/embedding/imports.py +2 -0
- ansys/mechanical/core/embedding/initializer.py +6 -1
- ansys/mechanical/core/embedding/logger/__init__.py +1 -1
- ansys/mechanical/core/embedding/poster.py +33 -1
- ansys/mechanical/core/embedding/resolver.py +12 -5
- ansys/mechanical/core/embedding/rpc/__init__.py +36 -0
- ansys/mechanical/core/embedding/rpc/client.py +237 -0
- ansys/mechanical/core/embedding/rpc/server.py +382 -0
- ansys/mechanical/core/embedding/rpc/utils.py +120 -0
- ansys/mechanical/core/embedding/runtime.py +22 -0
- ansys/mechanical/core/feature_flags.py +1 -0
- ansys/mechanical/core/ide_config.py +5 -5
- ansys/mechanical/core/mechanical.py +34 -15
- ansys/mechanical/core/misc.py +2 -2
- ansys/mechanical/core/pool.py +8 -8
- ansys/mechanical/core/run.py +3 -3
- {ansys_mechanical_core-0.11.11.dist-info → ansys_mechanical_core-0.11.13.dist-info}/METADATA +42 -29
- {ansys_mechanical_core-0.11.11.dist-info → ansys_mechanical_core-0.11.13.dist-info}/RECORD +25 -21
- {ansys_mechanical_core-0.11.11.dist-info → ansys_mechanical_core-0.11.13.dist-info}/LICENSE +0 -0
- {ansys_mechanical_core-0.11.11.dist-info → ansys_mechanical_core-0.11.13.dist-info}/WHEEL +0 -0
- {ansys_mechanical_core-0.11.11.dist-info → ansys_mechanical_core-0.11.13.dist-info}/entry_points.txt +0 -0
@@ -134,8 +134,7 @@ class App:
|
|
134
134
|
Create App with Mechanical project file and version:
|
135
135
|
|
136
136
|
>>> from ansys.mechanical.core import App
|
137
|
-
>>> app = App(db_file="path/to/file.mechdat", version=
|
138
|
-
|
137
|
+
>>> app = App(db_file="path/to/file.mechdat", version=251)
|
139
138
|
|
140
139
|
Disable copying the user profile when private appdata is enabled
|
141
140
|
|
@@ -182,8 +181,8 @@ class App:
|
|
182
181
|
profile.update_environment(os.environ)
|
183
182
|
atexit.register(_cleanup_private_appdata, profile)
|
184
183
|
|
185
|
-
self._app = _start_application(configuration, self._version, db_file)
|
186
184
|
runtime.initialize(self._version)
|
185
|
+
self._app = _start_application(configuration, self._version, db_file)
|
187
186
|
connect_warnings(self)
|
188
187
|
self._poster = None
|
189
188
|
|
@@ -76,12 +76,21 @@ class BackgroundApp:
|
|
76
76
|
"""
|
77
77
|
return BackgroundApp.__app
|
78
78
|
|
79
|
-
def
|
80
|
-
"""Post callable method to the background app thread."""
|
79
|
+
def _post(self, callable: typing.Callable, try_post: bool = False):
|
81
80
|
if BackgroundApp.__stopped:
|
82
81
|
raise RuntimeError("Cannot use BackgroundApp after stopping it.")
|
82
|
+
if try_post:
|
83
|
+
return BackgroundApp.__poster.try_post(callable)
|
83
84
|
return BackgroundApp.__poster.post(callable)
|
84
85
|
|
86
|
+
def post(self, callable: typing.Callable):
|
87
|
+
"""Post callable method to the background app thread."""
|
88
|
+
return self._post(callable)
|
89
|
+
|
90
|
+
def try_post(self, callable: typing.Callable):
|
91
|
+
"""Try post callable method to the background app thread."""
|
92
|
+
return self._post(callable, try_post=True)
|
93
|
+
|
85
94
|
def stop(self) -> None:
|
86
95
|
"""Stop the background app thread."""
|
87
96
|
if BackgroundApp.__stopped:
|
@@ -33,5 +33,5 @@ clr.AddReference("Ansys.ACT.Interfaces")
|
|
33
33
|
|
34
34
|
from Ansys.ACT.Interfaces.Common import * # noqa isort: skip
|
35
35
|
from Ansys.Mechanical.DataModel.Enums import * # noqa isort: skip
|
36
|
-
|
36
|
+
from Ansys.ACT.Interfaces.Analysis import * # noqa isort: skip
|
37
37
|
import Ansys # noqa isort: skip
|
@@ -55,6 +55,7 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t
|
|
55
55
|
# When ansys-pythonnet issue #14 is fixed, uncomment above
|
56
56
|
from Ansys.ACT.Core.Math import Point2D, Point3D
|
57
57
|
from Ansys.ACT.Math import Vector3D
|
58
|
+
from Ansys.ACT.Mechanical.Fields import VariableDefinitionType
|
58
59
|
from Ansys.Core.Units import Quantity
|
59
60
|
from Ansys.Mechanical.DataModel import MechanicalEnums
|
60
61
|
from Ansys.Mechanical.Graphics import Point, SectionPlane
|
@@ -74,6 +75,7 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t
|
|
74
75
|
vars["Point2D"] = Point2D
|
75
76
|
vars["Point3D"] = Point3D
|
76
77
|
vars["Vector3D"] = Vector3D
|
78
|
+
vars["VariableDefinitionType"] = VariableDefinitionType
|
77
79
|
|
78
80
|
if enums:
|
79
81
|
vars.update(get_all_enums())
|
@@ -34,7 +34,12 @@ from ansys.mechanical.core.embedding.resolver import resolve
|
|
34
34
|
INITIALIZED_VERSION = None
|
35
35
|
"""Constant for the initialized version."""
|
36
36
|
|
37
|
-
SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS = {
|
37
|
+
SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS = {
|
38
|
+
251: "2025R1",
|
39
|
+
242: "2024R2",
|
40
|
+
241: "2024R1",
|
41
|
+
232: "2023R2",
|
42
|
+
}
|
38
43
|
"""Supported Mechanical embedding versions on Windows."""
|
39
44
|
|
40
45
|
|
@@ -37,7 +37,7 @@ Configuring the logger can be done using the :class:`Configuration <ansys.mechan
|
|
37
37
|
from ansys.mechanical.core.embedding.logger import Configuration, Logger
|
38
38
|
|
39
39
|
Configuration.configure(level=logging.INFO, to_stdout=True, base_directory=None)
|
40
|
-
app = mech.App(version=
|
40
|
+
app = mech.App(version=251)
|
41
41
|
|
42
42
|
Then, the :class:`Logger <ansys.mechanical.core.embedding.logger.Logger>` class can be used to write messages to the log:
|
43
43
|
|
@@ -25,6 +25,19 @@
|
|
25
25
|
import typing
|
26
26
|
|
27
27
|
|
28
|
+
class PosterError(Exception):
|
29
|
+
"""Class which holds errors from the background thread posting system."""
|
30
|
+
|
31
|
+
def __init__(self, error: Exception):
|
32
|
+
"""Create an instance to hold the given error."""
|
33
|
+
self._error = error
|
34
|
+
|
35
|
+
@property
|
36
|
+
def error(self) -> Exception:
|
37
|
+
"""Get the underlying exception."""
|
38
|
+
return self._error
|
39
|
+
|
40
|
+
|
28
41
|
class Poster:
|
29
42
|
"""Class which can post a python callable function to Mechanical's main thread."""
|
30
43
|
|
@@ -37,7 +50,26 @@ class Poster:
|
|
37
50
|
|
38
51
|
self._poster = Ans.Common.WB1ManagedUtils.TaskPoster
|
39
52
|
|
40
|
-
def
|
53
|
+
def try_post(self, callable: typing.Callable) -> typing.Any:
|
54
|
+
"""Post the callable to Mechanical's main thread.
|
55
|
+
|
56
|
+
This does the same thing as `post` but if `callable`
|
57
|
+
raises an exception, try_post will raise the same
|
58
|
+
exception to the caller of `try_post`.
|
59
|
+
"""
|
60
|
+
|
61
|
+
def wrapped():
|
62
|
+
try:
|
63
|
+
return callable()
|
64
|
+
except Exception as e:
|
65
|
+
return PosterError(e)
|
66
|
+
|
67
|
+
result = self.post(wrapped)
|
68
|
+
if isinstance(result, PosterError):
|
69
|
+
raise result.error
|
70
|
+
return result
|
71
|
+
|
72
|
+
def post(self, callable: typing.Callable) -> typing.Any:
|
41
73
|
"""Post the callable to Mechanical's main thread.
|
42
74
|
|
43
75
|
The main thread needs to be receiving posted messages
|
@@ -23,8 +23,8 @@
|
|
23
23
|
"""This is the .NET assembly resolving for embedding Ansys Mechanical.
|
24
24
|
|
25
25
|
Note that for some Mechanical Addons - additional resolving may be
|
26
|
-
necessary. A resolve handler is shipped with Ansys Mechanical on
|
27
|
-
starting in version 23.1 and on
|
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
28
|
"""
|
29
29
|
|
30
30
|
|
@@ -36,6 +36,13 @@ def resolve(version):
|
|
36
36
|
clr.AddReference("Ansys.Mechanical.Embedding")
|
37
37
|
import Ansys # isort: skip
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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,237 @@
|
|
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.mechanical import DEFAULT_CHUNK_SIZE
|
31
|
+
|
32
|
+
|
33
|
+
class Client:
|
34
|
+
"""Client for connecting to Mechanical services."""
|
35
|
+
|
36
|
+
def __init__(self, host: str, port: int, timeout: float = 120.0):
|
37
|
+
"""Initialize the client.
|
38
|
+
|
39
|
+
Parameters
|
40
|
+
----------
|
41
|
+
host : str, optional
|
42
|
+
IP address to connect to the server. The default is ``None``
|
43
|
+
in which case ``localhost`` is used.
|
44
|
+
port : int, optional
|
45
|
+
Port to connect to the Mecahnical server. The default is ``None``,
|
46
|
+
in which case ``10000`` is used.
|
47
|
+
timeout : float, optional
|
48
|
+
Maximum allowable time for connecting to the Mechanical server.
|
49
|
+
The default is ``60.0``.
|
50
|
+
|
51
|
+
"""
|
52
|
+
self.host = host
|
53
|
+
self.port = port
|
54
|
+
self.timeout = timeout
|
55
|
+
self.connection = None
|
56
|
+
self.root = None
|
57
|
+
self._connect()
|
58
|
+
|
59
|
+
def __getattr__(self, attr):
|
60
|
+
"""Get attribute from the root object."""
|
61
|
+
if hasattr(self.root, attr):
|
62
|
+
return getattr(self.root, attr)
|
63
|
+
propget_name = f"propget_{attr}"
|
64
|
+
if hasattr(self.root, propget_name):
|
65
|
+
exposed_fget = getattr(self.root, propget_name)
|
66
|
+
return exposed_fget()
|
67
|
+
return self.__dict__.items[attr]
|
68
|
+
|
69
|
+
# TODO - implement setattr
|
70
|
+
# def __setattr__(self, attr, value):
|
71
|
+
# if hasattr(self.root, attr):
|
72
|
+
# inner_prop = getattr(self.root.__class__, attr)
|
73
|
+
# if isinstance(inner_prop, property):
|
74
|
+
# inner_prop.fset(self.root, value)
|
75
|
+
# else:
|
76
|
+
# super().__setattr__(attr, value)
|
77
|
+
|
78
|
+
def _connect(self):
|
79
|
+
self._wait_until_ready()
|
80
|
+
self.connection = rpyc.connect(self.host, self.port)
|
81
|
+
self.root = self.connection.root
|
82
|
+
print(f"Connected to {self.host}:{self.port}")
|
83
|
+
print(f"Installed methods")
|
84
|
+
|
85
|
+
def _wait_until_ready(self):
|
86
|
+
t_max = time.time() + self.timeout
|
87
|
+
while time.time() < t_max:
|
88
|
+
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
|
+
)
|
100
|
+
|
101
|
+
def close(self):
|
102
|
+
"""Close the connection."""
|
103
|
+
self.connection.close()
|
104
|
+
print(f"Connection to {self.host}:{self.port} closed")
|
105
|
+
|
106
|
+
def upload(
|
107
|
+
self,
|
108
|
+
file_name,
|
109
|
+
file_location_destination=None,
|
110
|
+
chunk_size=DEFAULT_CHUNK_SIZE,
|
111
|
+
progress_bar=False,
|
112
|
+
):
|
113
|
+
"""Upload a file to the server."""
|
114
|
+
print(f"arg: {file_name}, {file_location_destination}")
|
115
|
+
print()
|
116
|
+
if not os.path.exists(file_name):
|
117
|
+
print(f"File {file_name} does not exist.")
|
118
|
+
return
|
119
|
+
file_base_name = os.path.basename(file_name)
|
120
|
+
remote_path = os.path.join(file_location_destination, file_base_name)
|
121
|
+
|
122
|
+
with open(file_name, "rb") as f:
|
123
|
+
file_data = f.read()
|
124
|
+
self.service_upload(remote_path, file_data)
|
125
|
+
|
126
|
+
print(f"File {file_name} uploaded to {file_location_destination}")
|
127
|
+
|
128
|
+
def download(
|
129
|
+
self,
|
130
|
+
files,
|
131
|
+
target_dir=None,
|
132
|
+
chunk_size=DEFAULT_CHUNK_SIZE,
|
133
|
+
progress_bar=None,
|
134
|
+
recursive=False,
|
135
|
+
):
|
136
|
+
"""Download a file from the server."""
|
137
|
+
out_files = []
|
138
|
+
os.makedirs(target_dir, exist_ok=True)
|
139
|
+
|
140
|
+
response = self.service_download(files)
|
141
|
+
|
142
|
+
if isinstance(response, dict) and response["is_directory"]:
|
143
|
+
for relative_file_path in response["files"]:
|
144
|
+
full_remote_path = os.path.join(files, relative_file_path)
|
145
|
+
local_file_path = os.path.join(target_dir, relative_file_path)
|
146
|
+
local_file_dir = os.path.dirname(local_file_path)
|
147
|
+
os.makedirs(local_file_dir, exist_ok=True)
|
148
|
+
|
149
|
+
out_file_path = self._download_file(
|
150
|
+
full_remote_path, local_file_path, chunk_size, overwrite=True
|
151
|
+
)
|
152
|
+
else:
|
153
|
+
out_file_path = self._download_file(
|
154
|
+
files, os.path.join(target_dir, os.path.basename(files)), chunk_size, overwrite=True
|
155
|
+
)
|
156
|
+
out_files.append(out_file_path)
|
157
|
+
|
158
|
+
return out_files
|
159
|
+
|
160
|
+
def _download_file(self, remote_file_path, local_file_path, chunk_size=1024, overwrite=False):
|
161
|
+
if os.path.exists(local_file_path) and not overwrite:
|
162
|
+
print(f"File {local_file_path} already exists locally. Skipping download.")
|
163
|
+
return
|
164
|
+
response = self.service_download(remote_file_path)
|
165
|
+
if isinstance(response, dict):
|
166
|
+
raise ValueError("Expected a file download, but got a directory response.")
|
167
|
+
file_data = response
|
168
|
+
|
169
|
+
# Write the file data to the local path
|
170
|
+
with open(local_file_path, "wb") as f:
|
171
|
+
f.write(file_data)
|
172
|
+
|
173
|
+
print(f"File {remote_file_path} downloaded to {local_file_path}")
|
174
|
+
|
175
|
+
return local_file_path
|
176
|
+
|
177
|
+
def download_project(self, extensions=None, target_dir=None, progress_bar=False):
|
178
|
+
"""Download all project files in the working directory of the Mechanical instance."""
|
179
|
+
destination_directory = target_dir.rstrip("\\/")
|
180
|
+
if destination_directory:
|
181
|
+
path = pathlib.Path(destination_directory)
|
182
|
+
path.mkdir(parents=True, exist_ok=True)
|
183
|
+
else:
|
184
|
+
destination_directory = os.getcwd()
|
185
|
+
# relative directory?
|
186
|
+
if os.path.isdir(destination_directory):
|
187
|
+
if not os.path.isabs(destination_directory):
|
188
|
+
# construct full path
|
189
|
+
destination_directory = os.path.join(os.getcwd(), destination_directory)
|
190
|
+
_project_directory = self.project_directory
|
191
|
+
_project_directory = _project_directory.rstrip("\\/")
|
192
|
+
|
193
|
+
# this is where .mechddb resides
|
194
|
+
parent_directory = os.path.dirname(_project_directory)
|
195
|
+
|
196
|
+
list_of_files = []
|
197
|
+
|
198
|
+
if not extensions:
|
199
|
+
files = self.list_files()
|
200
|
+
else:
|
201
|
+
files = []
|
202
|
+
for each_extension in extensions:
|
203
|
+
# mechdb resides one level above project directory
|
204
|
+
if "mechdb" == each_extension.lower():
|
205
|
+
file_temp = os.path.join(parent_directory, f"*.{each_extension}")
|
206
|
+
else:
|
207
|
+
file_temp = os.path.join(_project_directory, "**", f"*.{each_extension}")
|
208
|
+
|
209
|
+
list_files = self._get_files(file_temp, recursive=False)
|
210
|
+
|
211
|
+
files.extend(list_files)
|
212
|
+
|
213
|
+
for file in files:
|
214
|
+
# create similar hierarchy locally
|
215
|
+
new_path = file.replace(parent_directory, destination_directory)
|
216
|
+
new_path_dir = os.path.dirname(new_path)
|
217
|
+
temp_files = self.download(
|
218
|
+
files=file, target_dir=new_path_dir, progress_bar=progress_bar
|
219
|
+
)
|
220
|
+
list_of_files.extend(temp_files)
|
221
|
+
return list_of_files
|
222
|
+
|
223
|
+
@property
|
224
|
+
def is_alive(self):
|
225
|
+
"""Check if the Mechanical instance is alive."""
|
226
|
+
try:
|
227
|
+
self.connection.ping()
|
228
|
+
return True
|
229
|
+
except:
|
230
|
+
return False
|
231
|
+
|
232
|
+
def exit(self):
|
233
|
+
"""Shuts down the Mechanical instance."""
|
234
|
+
print("Requesting server shutdown ...")
|
235
|
+
self.root.service_exit()
|
236
|
+
self.connection.close()
|
237
|
+
print("Disconnected from server")
|