ansys-tools-common 0.2.0__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/tools/common/__init__.py +26 -0
- ansys/tools/common/abstractions/__init__.py +25 -0
- ansys/tools/common/abstractions/connection.py +105 -0
- ansys/tools/common/abstractions/launcher.py +175 -0
- ansys/tools/common/example_download.py +213 -0
- ansys/tools/common/exceptions.py +136 -0
- ansys/tools/common/launcher/__init__.py +34 -0
- ansys/tools/common/launcher/_cli.py +279 -0
- ansys/tools/common/launcher/_plugins.py +115 -0
- ansys/tools/common/launcher/config.py +266 -0
- ansys/tools/common/launcher/helpers/__init__.py +27 -0
- ansys/tools/common/launcher/helpers/grpc.py +54 -0
- ansys/tools/common/launcher/helpers/ports.py +49 -0
- ansys/tools/common/launcher/interface.py +193 -0
- ansys/tools/common/launcher/launch.py +84 -0
- ansys/tools/common/launcher/product_instance.py +192 -0
- ansys/tools/common/logger.py +171 -0
- ansys/tools/common/logger_formatter.py +59 -0
- ansys/tools/common/path/__init__.py +82 -0
- ansys/tools/common/path/applications/__init__.py +49 -0
- ansys/tools/common/path/applications/dyna.py +41 -0
- ansys/tools/common/path/applications/mapdl.py +43 -0
- ansys/tools/common/path/applications/mechanical.py +46 -0
- ansys/tools/common/path/path.py +1336 -0
- ansys/tools/common/path/save.py +56 -0
- ansys/tools/common/py.typed +0 -0
- ansys/tools/common/report.py +255 -0
- ansys/tools/common/versioning.py +589 -0
- ansys_tools_common-0.2.0.dist-info/METADATA +111 -0
- ansys_tools_common-0.2.0.dist-info/RECORD +33 -0
- ansys_tools_common-0.2.0.dist-info/WHEEL +4 -0
- ansys_tools_common-0.2.0.dist-info/entry_points.txt +3 -0
- ansys_tools_common-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (C) 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
|
+
"""Main module."""
|
|
23
|
+
|
|
24
|
+
import importlib.metadata as importlib_metadata
|
|
25
|
+
|
|
26
|
+
__version__ = importlib_metadata.version("ansys-tools-common")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright (C) 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
|
+
"""Initialization module for abstractions."""
|
|
23
|
+
|
|
24
|
+
from .connection import AbstractGRPCConnection # noqa F401
|
|
25
|
+
from .launcher import LauncherProtocol # noqa F401
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright (C) 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
|
+
"""Module for abstract connection class."""
|
|
23
|
+
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import grpc
|
|
28
|
+
except ImportError: # pragma: no cover
|
|
29
|
+
import warnings
|
|
30
|
+
|
|
31
|
+
warnings.warn(
|
|
32
|
+
"grpc module is not available - reach out to the library maintainers to include it into their dependencies"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AbstractGRPCConnection(ABC):
|
|
37
|
+
"""Abstract class for managing gRPC connections.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
host : str
|
|
42
|
+
Host where the gRPC server is running.
|
|
43
|
+
port : str
|
|
44
|
+
Port where the gRPC server is listening.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def __init__(self, host: str, port: str) -> None:
|
|
49
|
+
"""Initialize the gRPC connection with host and port."""
|
|
50
|
+
pass # pragma: no cover
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def connect(self) -> None:
|
|
54
|
+
"""Establish a connection to the gRPC server."""
|
|
55
|
+
pass # pragma: no cover
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def close(self) -> None:
|
|
59
|
+
"""Disconnect from the gRPC server."""
|
|
60
|
+
pass # pragma: no cover
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def service(self):
|
|
65
|
+
"""GRPC stub for making requests."""
|
|
66
|
+
pass # pragma: no cover
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def _host(self) -> str:
|
|
70
|
+
"""Host for the gRPC connection."""
|
|
71
|
+
return self.__host
|
|
72
|
+
|
|
73
|
+
@_host.setter
|
|
74
|
+
def _host(self, value: str) -> None:
|
|
75
|
+
"""Set the host for the gRPC connection."""
|
|
76
|
+
self.__host = value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def _port(self) -> str:
|
|
80
|
+
"""Return the port for the gRPC connection."""
|
|
81
|
+
return self.__port
|
|
82
|
+
|
|
83
|
+
@_port.setter
|
|
84
|
+
def _port(self, value: str) -> None:
|
|
85
|
+
"""Set the port for the gRPC connection."""
|
|
86
|
+
self.__port = value
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def _channel(self, options: list = None) -> grpc.Channel:
|
|
90
|
+
"""Return the gRPC channel."""
|
|
91
|
+
return grpc.insecure_channel(f"{self._host}:{self._port}", options=options)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_closed(self) -> bool:
|
|
95
|
+
"""Flag indicating if the connection is closed.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
bool
|
|
100
|
+
``True`` if the connection is closed, ``False`` otherwise.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
return self._channel._connectivity_state != grpc.ChannelConnectivity.READY
|
|
104
|
+
except grpc.RpcError:
|
|
105
|
+
return True
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Copyright (C) 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
|
+
"""Interface definitions for implementing a local product launcher.
|
|
24
|
+
|
|
25
|
+
A plugin for the local product launcher must implement the :class:`LauncherProtocol` class and register it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from enum import Enum, auto
|
|
29
|
+
from typing import Any, ClassVar, Protocol, TypeVar
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"DataclassProtocol",
|
|
33
|
+
"FALLBACK_LAUNCH_MODE_NAME",
|
|
34
|
+
"LAUNCHER_CONFIG_T",
|
|
35
|
+
"LauncherProtocol",
|
|
36
|
+
"METADATA_KEY_DOC",
|
|
37
|
+
"METADATA_KEY_NOPROMPT",
|
|
38
|
+
"ServerType",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
METADATA_KEY_DOC = "launcher_doc"
|
|
42
|
+
"""Key used in the :py:class:`dataclasses.Field` ``metadata`` for the option description."""
|
|
43
|
+
|
|
44
|
+
METADATA_KEY_NOPROMPT = "launcher_noprompt"
|
|
45
|
+
"""
|
|
46
|
+
Key used in the :py:class:`dataclasses.Field` ``metadata`` to skip prompting for the option by default.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
FALLBACK_LAUNCH_MODE_NAME = "__fallback__"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DataclassProtocol(Protocol):
|
|
53
|
+
"""Provides the ``Protocol`` class for Python dataclasses."""
|
|
54
|
+
|
|
55
|
+
__dataclass_fields__: ClassVar[dict[str, Any]]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
LAUNCHER_CONFIG_T = TypeVar("LAUNCHER_CONFIG_T", bound=DataclassProtocol)
|
|
59
|
+
# This docstring is commented-out because numpydoc causes the documentation build to fail when
|
|
60
|
+
# it is included.
|
|
61
|
+
# """Type variable for launcher configuration objects."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ServerType(Enum):
|
|
65
|
+
"""Defines which protocols the server supports.
|
|
66
|
+
|
|
67
|
+
The ``ServerType`` class is used as values in the :attr:`LauncherProtocol.SERVER_SPEC`
|
|
68
|
+
attribute to define the capabilities of the servers started with a given product and launch method.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
GENERIC = auto()
|
|
72
|
+
"""Generic server, which responds at a given URL and port.
|
|
73
|
+
|
|
74
|
+
The generic server type can be used for any server. It does not
|
|
75
|
+
include information about which protocol should be used.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
GRPC = auto()
|
|
79
|
+
"""Server that can be accessed via gRPC.
|
|
80
|
+
|
|
81
|
+
Servers of this type are accessible via the :attr:`.ProductInstance.channels`
|
|
82
|
+
attribute.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class LauncherProtocol(Protocol[LAUNCHER_CONFIG_T]):
|
|
87
|
+
"""Provides the interface for managing a local product instance.
|
|
88
|
+
|
|
89
|
+
A plugin to the local product launcher must implement the interface defined in this class.
|
|
90
|
+
|
|
91
|
+
To check for compatibility, you should derive from this
|
|
92
|
+
class. For example ``MyLauncher(LauncherProtocol[MyConfigModel])``.
|
|
93
|
+
Check the resulting code with `Mypy <https://mypy.readthedocs.io>`_.
|
|
94
|
+
|
|
95
|
+
The ``__init__`` method should accept exactly one keyword-only
|
|
96
|
+
parameter: ``config``. Note that this is `not enforced by
|
|
97
|
+
Mypy <https://bugs.python.org/issue44807>`_.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
config :
|
|
102
|
+
Configuration options used to start the product. This parameter must be an instance of ``CONFIG_MODEL``.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
CONFIG_MODEL: type[LAUNCHER_CONFIG_T]
|
|
106
|
+
"""Defines the configuration options for the launcher.
|
|
107
|
+
|
|
108
|
+
The configuration options that this launcher accepts, specified
|
|
109
|
+
as a :py:func:`dataclass <dataclasses.dataclass>`. Note that
|
|
110
|
+
the ``default`` and ``metadata[METADATA_KEY_DOC]`` of the
|
|
111
|
+
fields are used in the configuration CLI, if available.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
SERVER_SPEC: dict[str, ServerType]
|
|
115
|
+
"""Defines the server types that are started.
|
|
116
|
+
|
|
117
|
+
Examples
|
|
118
|
+
--------
|
|
119
|
+
This code defines a server that is accessible via a URL
|
|
120
|
+
at the ``"MAIN"`` key and a server accessible via gRPC
|
|
121
|
+
at the ``"FILE_TRANSFER"`` key.
|
|
122
|
+
|
|
123
|
+
.. code:: python
|
|
124
|
+
|
|
125
|
+
SERVER_SPEC = {"MAIN": ServerType.GENERIC, "FILE_TRANSFER": ServerType.GRPC}
|
|
126
|
+
|
|
127
|
+
The :attr:`.ProductInstance.urls` attribute then has keys
|
|
128
|
+
``{"MAIN", "FILE_TRANSFER"}``, whereas the
|
|
129
|
+
:attr:`.ProductInstance.channels` attribute has only the
|
|
130
|
+
key ``"FILE_TRANSFER"``.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, *, config: LAUNCHER_CONFIG_T):
|
|
134
|
+
pass # pragma: no cover
|
|
135
|
+
|
|
136
|
+
def start(self) -> None:
|
|
137
|
+
"""Start the product instance."""
|
|
138
|
+
|
|
139
|
+
def stop(self, *, timeout: float | None = None) -> None:
|
|
140
|
+
"""Stop the product instance.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
timeout :
|
|
145
|
+
Time after which the instance can be forcefully stopped.
|
|
146
|
+
The timeout should be interpreted as a hint to the implementation.
|
|
147
|
+
It is *not required* to trigger a force-shutdown, but the stop *must* return within a finite time.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def check(self, *, timeout: float | None = None) -> bool:
|
|
151
|
+
"""Check if the product instance is responding to requests.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
timeout :
|
|
156
|
+
Timeout in seconds for the check.
|
|
157
|
+
The timeout should be interpreted as a hint to the implementation.
|
|
158
|
+
It is *not required* to return within the given time,
|
|
159
|
+
but the check *must* return within a finite time,
|
|
160
|
+
meaning it must not hang indefinitely.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
bool
|
|
165
|
+
Whether the product instance is responding.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def urls(self) -> dict[str, str]:
|
|
170
|
+
"""Dictionary of URLs that the server is listening on.
|
|
171
|
+
|
|
172
|
+
The keys of the returned dictionary must correspond
|
|
173
|
+
to the keys defined in the
|
|
174
|
+
:attr:`.LauncherProtocol.SERVER_SPEC` attribute.
|
|
175
|
+
"""
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Copyright (C) 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
|
+
"""Module for downloading examples from the PyAnsys Github ``example-data`` repository."""
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
import tempfile
|
|
26
|
+
from threading import Lock
|
|
27
|
+
from typing import Optional
|
|
28
|
+
from urllib.parse import urljoin, urlparse
|
|
29
|
+
|
|
30
|
+
import requests
|
|
31
|
+
|
|
32
|
+
__all__ = ["DownloadManager"]
|
|
33
|
+
|
|
34
|
+
BASE_URL = "https://github.com/ansys/example-data/raw/main"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DownloadManagerMeta(type):
|
|
38
|
+
"""Provides a thread-safe implementation of ``Singleton``.
|
|
39
|
+
|
|
40
|
+
https://refactoring.guru/design-patterns/singleton/python/example#example-1.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_instances = {}
|
|
44
|
+
_lock: Lock = Lock()
|
|
45
|
+
|
|
46
|
+
def __call__(cls, *args, **kwargs):
|
|
47
|
+
"""Call to the class."""
|
|
48
|
+
with cls._lock:
|
|
49
|
+
if cls not in cls._instances:
|
|
50
|
+
instance = super().__call__(*args, **kwargs)
|
|
51
|
+
cls._instances[cls] = instance
|
|
52
|
+
return cls._instances[cls]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DownloadManager(metaclass=DownloadManagerMeta):
|
|
56
|
+
"""Manages downloads of example files.
|
|
57
|
+
|
|
58
|
+
Manages the download of example from the ``example-data``
|
|
59
|
+
repository, which is at https://github.com/ansys/example-data.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
"""Initialize the download manager."""
|
|
64
|
+
self._downloads_list = []
|
|
65
|
+
|
|
66
|
+
def clear_download_cache(self):
|
|
67
|
+
"""Remove downloaded example files from the local path."""
|
|
68
|
+
for file in self._downloads_list:
|
|
69
|
+
Path(file).unlink()
|
|
70
|
+
self._downloads_list.clear()
|
|
71
|
+
|
|
72
|
+
def download_file(
|
|
73
|
+
self, filename: str, directory: str, destination: Optional[str] = None, force: bool = False
|
|
74
|
+
) -> str:
|
|
75
|
+
"""Download an example file from the ``example-data`` repository.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
filename : str
|
|
80
|
+
Name of the example file to download.
|
|
81
|
+
directory : str
|
|
82
|
+
Path under the ``example-data`` repository.
|
|
83
|
+
destination : str, default: None
|
|
84
|
+
Path to download the example file to. The default
|
|
85
|
+
is ``None``, in which case the default path for app data
|
|
86
|
+
is used.
|
|
87
|
+
force : bool, default: False
|
|
88
|
+
Whether to always download the example file. The default is
|
|
89
|
+
``False``, in which case if the example file is cached, it
|
|
90
|
+
is reused.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
tuple[str, str]
|
|
95
|
+
Tuple containing the filepath to use and the local
|
|
96
|
+
filepath of the downloaded directory. The two are
|
|
97
|
+
different in case of containers.
|
|
98
|
+
"""
|
|
99
|
+
# Convert to Path object
|
|
100
|
+
destination_path = Path(destination) if destination is not None else None
|
|
101
|
+
|
|
102
|
+
# If destination is not a dir, create it
|
|
103
|
+
if destination_path is not None and not destination_path.exists():
|
|
104
|
+
destination_path.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
# Check if it was able to create the dir, very rare case
|
|
107
|
+
if destination_path is not None and not destination_path.is_dir():
|
|
108
|
+
raise ValueError("Destination directory provided does not exist.") # pragma: no cover
|
|
109
|
+
|
|
110
|
+
url = self._get_filepath_on_default_server(filename, directory)
|
|
111
|
+
local_path = self._retrieve_data(url, filename, dest=destination, force=force)
|
|
112
|
+
|
|
113
|
+
# add path to downloaded files
|
|
114
|
+
self._add_file(local_path)
|
|
115
|
+
return local_path
|
|
116
|
+
|
|
117
|
+
def _add_file(self, file_path: str):
|
|
118
|
+
"""Add the path for a downloaded example file to a list.
|
|
119
|
+
|
|
120
|
+
This list keeps track of where example files are
|
|
121
|
+
downloaded so that a global cleanup of these files can be
|
|
122
|
+
performed when the client is closed.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
file_path : str
|
|
127
|
+
Local path of the downloaded example file.
|
|
128
|
+
"""
|
|
129
|
+
if file_path not in self._downloads_list:
|
|
130
|
+
self._downloads_list.append(file_path)
|
|
131
|
+
|
|
132
|
+
def _joinurl(self, base: str, directory: str) -> str:
|
|
133
|
+
"""Join multiple paths to a base URL.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
base : str
|
|
138
|
+
Base URL to append the directory path to.
|
|
139
|
+
If this URL doesn't end with '/', then it is added automatically.
|
|
140
|
+
directory : str
|
|
141
|
+
Directory path to append to the base URL.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
str
|
|
146
|
+
Joined URL with the base and paths.
|
|
147
|
+
"""
|
|
148
|
+
if base[-1] != "/":
|
|
149
|
+
base += "/"
|
|
150
|
+
return urljoin(base, directory)
|
|
151
|
+
|
|
152
|
+
def _get_filepath_on_default_server(self, filename: str, directory: str = None) -> str:
|
|
153
|
+
"""Get the full URL of the file on the default repository.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
filename : str
|
|
158
|
+
Name of the file to download.
|
|
159
|
+
directory : str, default: None
|
|
160
|
+
Path under the ``example-data`` repository.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
str
|
|
165
|
+
Full URL of the file on the default repository.
|
|
166
|
+
"""
|
|
167
|
+
if directory:
|
|
168
|
+
if directory[-1] != "/":
|
|
169
|
+
directory += "/"
|
|
170
|
+
return self._joinurl(BASE_URL, directory + filename)
|
|
171
|
+
else:
|
|
172
|
+
return self._joinurl(BASE_URL, filename)
|
|
173
|
+
|
|
174
|
+
def _retrieve_data(self, url: str, filename: str, dest: str = None, force: bool = False) -> str:
|
|
175
|
+
"""Retrieve data from a URL and save it to a local file.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
url : str
|
|
180
|
+
URL to download the file from.
|
|
181
|
+
filename : str
|
|
182
|
+
Name of the file to save the downloaded content as.
|
|
183
|
+
dest : str, default: None
|
|
184
|
+
Destination path of the file.
|
|
185
|
+
force : bool, default: False
|
|
186
|
+
Whether to force downloading to avoid cached examples.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
str
|
|
191
|
+
Local path where the file was saved.
|
|
192
|
+
"""
|
|
193
|
+
if dest is None:
|
|
194
|
+
dest = tempfile.gettempdir()
|
|
195
|
+
local_path = Path(dest) / Path(filename).name
|
|
196
|
+
|
|
197
|
+
if not force and local_path.is_file():
|
|
198
|
+
return str(local_path)
|
|
199
|
+
|
|
200
|
+
parsed_url = urlparse(url)
|
|
201
|
+
if parsed_url.scheme not in ("http", "https"):
|
|
202
|
+
raise ValueError(f"Unsafe URL scheme: {parsed_url.scheme}")
|
|
203
|
+
|
|
204
|
+
response = requests.get(url, timeout=60)
|
|
205
|
+
response.raise_for_status()
|
|
206
|
+
|
|
207
|
+
Path(local_path).write_bytes(response.content)
|
|
208
|
+
|
|
209
|
+
return str(local_path)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Create a singleton instance of DownloadManager
|
|
213
|
+
download_manager = DownloadManager()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Copyright (C) 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
|
+
"""Exceptions module."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AnsysError(Exception):
|
|
26
|
+
"""Base class for all exceptions raised by the Ansys API.
|
|
27
|
+
|
|
28
|
+
You can use this base class to catch all Ansys-related exceptions.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str) -> None:
|
|
32
|
+
"""Initialize the exception with a message."""
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.message = message
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ProductInstanceError(AnsysError):
|
|
38
|
+
"""Custom exception for ProductInstance lifecycle errors."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AnsysHostnameValueError(AnsysError):
|
|
42
|
+
"""Error raised when hostname value is not valid."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, msg):
|
|
45
|
+
"""Initialize the exception.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
msg : str
|
|
50
|
+
The message to be raised for the exception.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(msg)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AnsysPortValueError(AnsysError):
|
|
57
|
+
"""Error raised when port value is not valid."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, msg):
|
|
60
|
+
"""Initialize the exception.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
msg : str
|
|
65
|
+
The message to be raised for the exception.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
super().__init__(msg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AnsysTypeError(AnsysError):
|
|
72
|
+
"""Error raised when an argument is of the wrong type.
|
|
73
|
+
|
|
74
|
+
This exception is raised when Python-wise types would work, but internal
|
|
75
|
+
Ansys-specific typing is not right.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
expected_type : str | type
|
|
80
|
+
Expected type of the argument.
|
|
81
|
+
actual_type : str | type
|
|
82
|
+
Actual type of the argument.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, expected_type: str | type, actual_type: str | type = None) -> None:
|
|
86
|
+
"""Initialize the exception with expected and actual types."""
|
|
87
|
+
expected_type = expected_type if isinstance(expected_type, str) else expected_type.__name__
|
|
88
|
+
actual_type = actual_type if isinstance(actual_type, str) else actual_type.__name__
|
|
89
|
+
message = f"Expected type {expected_type}, but got {actual_type}."
|
|
90
|
+
super().__init__(message)
|
|
91
|
+
self.expected_type = expected_type
|
|
92
|
+
self.actual_type = actual_type
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AnsysLogicError(AnsysError):
|
|
96
|
+
"""Exception raised when an unexpected logical condition occurs.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
message : str
|
|
101
|
+
Error message.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, message: str) -> None:
|
|
105
|
+
"""Initialize the exception with a message."""
|
|
106
|
+
super().__init__(message)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class VersionSyntaxError(AnsysError):
|
|
110
|
+
"""An exception raised when an invalid version syntax is found."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, msg):
|
|
113
|
+
"""Initialize the exception.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
msg : str
|
|
118
|
+
Message to raise for the exception.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
super().__init__(msg)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class VersionError(AnsysError):
|
|
125
|
+
"""An exception raised when a version is not valid."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, msg):
|
|
128
|
+
"""Initialize the exception.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
msg : str
|
|
133
|
+
Message to raise for the exception.
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
super().__init__(msg)
|