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.
- ansys/mechanical/core/__init__.py +0 -1
- ansys/mechanical/core/_version.py +48 -48
- ansys/mechanical/core/embedding/app.py +681 -610
- ansys/mechanical/core/embedding/background.py +11 -2
- ansys/mechanical/core/embedding/enum_importer.py +0 -1
- ansys/mechanical/core/embedding/global_importer.py +50 -0
- ansys/mechanical/core/embedding/imports.py +30 -58
- ansys/mechanical/core/embedding/initializer.py +6 -4
- ansys/mechanical/core/embedding/logger/__init__.py +219 -219
- ansys/mechanical/core/embedding/messages.py +190 -0
- ansys/mechanical/core/embedding/resolver.py +48 -41
- ansys/mechanical/core/embedding/rpc/__init__.py +36 -0
- ansys/mechanical/core/embedding/rpc/client.py +259 -0
- ansys/mechanical/core/embedding/rpc/server.py +403 -0
- ansys/mechanical/core/embedding/rpc/utils.py +118 -0
- ansys/mechanical/core/embedding/runtime.py +22 -0
- ansys/mechanical/core/embedding/transaction.py +51 -0
- ansys/mechanical/core/feature_flags.py +1 -0
- ansys/mechanical/core/ide_config.py +226 -212
- ansys/mechanical/core/mechanical.py +2399 -2324
- ansys/mechanical/core/misc.py +176 -176
- ansys/mechanical/core/pool.py +712 -712
- ansys/mechanical/core/run.py +321 -321
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.14.dist-info}/METADATA +40 -27
- ansys_mechanical_core-0.11.14.dist-info/RECORD +52 -0
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.14.dist-info}/WHEEL +1 -1
- ansys_mechanical_core-0.11.12.dist-info/RECORD +0 -45
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.14.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
40
|
-
|
41
|
-
|
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}")
|