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.
Files changed (33) hide show
  1. ansys/tools/common/__init__.py +26 -0
  2. ansys/tools/common/abstractions/__init__.py +25 -0
  3. ansys/tools/common/abstractions/connection.py +105 -0
  4. ansys/tools/common/abstractions/launcher.py +175 -0
  5. ansys/tools/common/example_download.py +213 -0
  6. ansys/tools/common/exceptions.py +136 -0
  7. ansys/tools/common/launcher/__init__.py +34 -0
  8. ansys/tools/common/launcher/_cli.py +279 -0
  9. ansys/tools/common/launcher/_plugins.py +115 -0
  10. ansys/tools/common/launcher/config.py +266 -0
  11. ansys/tools/common/launcher/helpers/__init__.py +27 -0
  12. ansys/tools/common/launcher/helpers/grpc.py +54 -0
  13. ansys/tools/common/launcher/helpers/ports.py +49 -0
  14. ansys/tools/common/launcher/interface.py +193 -0
  15. ansys/tools/common/launcher/launch.py +84 -0
  16. ansys/tools/common/launcher/product_instance.py +192 -0
  17. ansys/tools/common/logger.py +171 -0
  18. ansys/tools/common/logger_formatter.py +59 -0
  19. ansys/tools/common/path/__init__.py +82 -0
  20. ansys/tools/common/path/applications/__init__.py +49 -0
  21. ansys/tools/common/path/applications/dyna.py +41 -0
  22. ansys/tools/common/path/applications/mapdl.py +43 -0
  23. ansys/tools/common/path/applications/mechanical.py +46 -0
  24. ansys/tools/common/path/path.py +1336 -0
  25. ansys/tools/common/path/save.py +56 -0
  26. ansys/tools/common/py.typed +0 -0
  27. ansys/tools/common/report.py +255 -0
  28. ansys/tools/common/versioning.py +589 -0
  29. ansys_tools_common-0.2.0.dist-info/METADATA +111 -0
  30. ansys_tools_common-0.2.0.dist-info/RECORD +33 -0
  31. ansys_tools_common-0.2.0.dist-info/WHEEL +4 -0
  32. ansys_tools_common-0.2.0.dist-info/entry_points.txt +3 -0
  33. 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)