Skip to content

Commit 52aa7f8

Browse files
committed
Launcher: implement gRPC transport option handling
1 parent 1fb2d85 commit 52aa7f8

File tree

11 files changed

+328
-95
lines changed

11 files changed

+328
-95
lines changed

doc/source/user_guide/launcher/plugin_creation.rst

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ there's quite a lot going on in this code, descriptions of each part are provide
4343
from typing import Optional
4444
import subprocess
4545
46-
from ansys.tools.local_product_launcher.interface import LauncherProtocol, ServerType
47-
from ansys.tools.local_product_launcher.helpers.ports import find_free_ports
48-
from ansys.tools.local_product_launcher.helpers.grpc import check_grpc_health
46+
from ansys.tools.common.launcher.interface import LauncherProtocol, ServerType
47+
from ansys.tools.common.launcher.helpers.ports import find_free_ports
48+
from ansys.tools.common.launcher.helpers.grpc import check_grpc_health
4949
5050
5151
class DirectLauncher(LauncherProtocol[LauncherConfig]):
@@ -190,15 +190,15 @@ You define the entrypoint in your package's build configuration. The exact synta
190190

191191
.. code:: toml
192192
193-
[project.entry-points."ansys.tools.local_product_launcher.launcher"]
193+
[project.entry-points."ansys.tools.common.launcher"]
194194
"ACP.direct" = "<your.module.name>:DirectLauncher"
195195
196196
In a ``setup.cfg`` file:
197197

198198
.. code:: cfg
199199
200200
[options.entry_points]
201-
ansys.tools.local_product_launcher.launcher =
201+
ansys.tools.common.launcher =
202202
ACP.direct = <your.module.name>:DirectLauncher
203203
204204
In a ``setup.py`` file:
@@ -210,7 +210,7 @@ You define the entrypoint in your package's build configuration. The exact synta
210210
setup(
211211
# ...,
212212
entry_points={
213-
"ansys.tools.local_product_launcher.launcher": [
213+
"ansys.tools.common.launcher": [
214214
"ACP.direct = <your.module.name>:DirectLauncher"
215215
]
216216
}
@@ -225,7 +225,7 @@ You define the entrypoint in your package's build configuration. The exact synta
225225

226226
.. code:: toml
227227
228-
[project.entry-points."ansys.tools.local_product_launcher.launcher"]
228+
[project.entry-points."ansys.tools.common.launcher"]
229229
"ACP.direct" = "<your.module.name>:DirectLauncher"
230230
231231
For more information, see the `Entry points sections <https://flit.pypa.io/en/stable/pyproject_toml.html#pyproject-project-entrypoints>`_ in the Flit documentation.
@@ -236,12 +236,12 @@ You define the entrypoint in your package's build configuration. The exact synta
236236

237237
.. code:: toml
238238
239-
[tool.poetry.plugins."ansys.tools.local_product_launcher.launcher"]
239+
[tool.poetry.plugins."ansys.tools.common.launcher"]
240240
"ACP.direct" = "<your.module.name>:DirectLauncher"
241241
242242
For more information, see the `plugins <https://python-poetry.org/docs/pyproject#plugins>`_ in the Poetry documentation.
243243

244-
In all cases, ``ansys.tools.local_product_launcher.launcher`` is an identifier specifying that the entrypoint defines a local product launcher plugin. It must be kept the same.
244+
In all cases, ``ansys.tools.common.launcher`` is an identifier specifying that the entrypoint defines a local product launcher plugin. It must be kept the same.
245245

246246
The entrypoint itself has two parts:
247247

@@ -275,7 +275,7 @@ the ``binary_path``:
275275
from typing import Union
276276
277277
from ansys.tools.path import get_available_ansys_installations
278-
from ansys.tools.local_product_launcher.interface import METADATA_KEY_DOC
278+
from ansys.tools.common.launcher.interface import METADATA_KEY_DOC
279279
280280
281281
def get_default_binary_path() -> str:
@@ -316,7 +316,7 @@ For example, to make ``DirectLauncher`` the fallback for ACP, add this entry poi
316316

317317
.. code:: toml
318318
319-
[project.entry-points."ansys.tools.local_product_launcher.launcher"]
319+
[project.entry-points."ansys.tools.common.launcher"]
320320
"ACP.__fallback__" = "<your.module.name>:DirectLauncher"
321321
322322
The fallback launch mode is used with its default configuration. This means that the configuration class must have default values for all its fields.
@@ -333,7 +333,7 @@ to ``True`` in the ``metadata`` dictionary:
333333
334334
import dataclasses
335335
336-
from ansys.tools.local_product_launcher.interface import METADATA_KEY_NOPROMPT
336+
from ansys.tools.common.launcher.interface import METADATA_KEY_NOPROMPT
337337
338338
339339
@dataclasses.dataclass

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ urls.Documentation = "https://ansys.tools.docs.pyansys.com/"
4848
urls.Issues = "https://github.com/ansys/ansys-tools-common/issues"
4949
urls.Releases = "https://github.com/ansys/ansys-tools-common/releases/"
5050
urls.Source = "https://github.com/ansys/ansys-tools-common"
51-
scripts.ansys-launcher = "ansys.tools.local_product_launcher._cli:cli"
51+
scripts.ansys-launcher = "ansys.tools.common.launcher._cli:cli"
5252
scripts.save-ansys-path = "ansys.tools.common.path.save:cli"
5353

5454
[dependency-groups]

src/ansys/tools/common/launcher/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222

2323
"""Local product launcher."""
2424

25-
from . import config, helpers, interface, product_instance
25+
from . import config, grpc_transport, helpers, interface, product_instance
2626
from .launch import launch_product
2727

2828
__all__ = [
29-
"interface",
30-
"helpers",
3129
"config",
32-
"product_instance",
30+
"grpc_transport",
31+
"helpers",
32+
"interface",
3333
"launch_product",
34+
"product_instance",
3435
]

src/ansys/tools/common/launcher/_cli.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,15 @@ def show_config() -> None:
253253
try:
254254
config = get_config_for(product_name=product_name, launch_mode=launch_mode)
255255
click.echo(" No configuration is set (uses defaults).")
256-
except KeyError:
256+
except (KeyError, RuntimeError):
257257
click.echo(" No configuration is set (no defaults available).")
258258
continue
259-
config = get_config_for(product_name=product_name, launch_mode=launch_mode)
260-
for field in dataclasses.fields(config):
261-
click.echo(f" {field.name}: {getattr(config, field.name)}")
259+
try:
260+
config = get_config_for(product_name=product_name, launch_mode=launch_mode)
261+
for field in dataclasses.fields(config):
262+
click.echo(f" {field.name}: {getattr(config, field.name)}")
263+
except TypeError:
264+
click.echo(" No configuration is set (invalid configuration).")
262265
except KeyError:
263266
click.echo(" No configuration is set.")
264267
click.echo("")

src/ansys/tools/common/launcher/_plugins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
from .interface import FALLBACK_LAUNCH_MODE_NAME, DataclassProtocol, LauncherProtocol
3232

33-
LAUNCHER_ENTRY_POINT = "ansys.tools.local_product_launcher.launcher"
33+
LAUNCHER_ENTRY_POINT = "ansys.tools.common.launcher"
3434

3535

3636
def get_launcher(*, product_name: str, launch_mode: str) -> type[LauncherProtocol[DataclassProtocol]]:
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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+
"""Defines options for connecting to a gRPC server."""
24+
25+
from abc import ABC, abstractmethod
26+
from dataclasses import asdict, dataclass
27+
import enum
28+
from pathlib import Path
29+
from typing import TYPE_CHECKING, Any, ClassVar
30+
31+
import grpc
32+
33+
from .. import cyberchannel
34+
35+
__all__ = [
36+
"TransportMode",
37+
"UDSOptions",
38+
"WNUAOptions",
39+
"MTLSOptions",
40+
"InsecureOptions",
41+
"TransportOptionsType",
42+
]
43+
44+
# For Python 3.10 and below, emulate the behavior of StrEnum by
45+
# inheriting from str and enum.Enum.
46+
# Note that this does *not* work on Python 3.11+, since the default
47+
# Enum format method has changed and will not return the value of
48+
# the enum member.
49+
# When type checking, always use the Python 3.10 workaround, otherwise
50+
# the StrEnum resolves as 'Any'.
51+
if TYPE_CHECKING: # pragma: no cover
52+
53+
class StrEnum(str, enum.Enum):
54+
"""String enum."""
55+
56+
else:
57+
try:
58+
from enum import StrEnum
59+
except ImportError:
60+
import enum
61+
62+
class StrEnum(str, enum.Enum):
63+
"""String enum."""
64+
65+
pass
66+
67+
68+
class TransportMode(StrEnum):
69+
"""Enumeration of transport modes supported by the FileTransfer Tool."""
70+
71+
UDS = "uds"
72+
WNUA = "wnua"
73+
MTLS = "mtls"
74+
INSECURE = "insecure"
75+
76+
77+
class TransportOptionsBase(ABC):
78+
"""Base class for transport options."""
79+
80+
_MODE: ClassVar[TransportMode]
81+
82+
@property
83+
def mode(self) -> TransportMode:
84+
"""Transport mode."""
85+
return self._MODE
86+
87+
def create_channel(self, **extra_kwargs: Any) -> grpc.Channel:
88+
"""Create a gRPC channel using the transport options.
89+
90+
Parameters
91+
----------
92+
extra_kwargs :
93+
Extra keyword arguments to pass to the channel creation function.
94+
95+
Returns
96+
-------
97+
:
98+
gRPC channel created using the transport options.
99+
"""
100+
return cyberchannel.create_channel(**self._to_cyberchannel_kwargs(), **extra_kwargs)
101+
102+
@abstractmethod
103+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
104+
"""Convert transport options to cyberchannel keyword arguments.
105+
106+
Returns
107+
-------
108+
:
109+
Dictionary of keyword arguments for cyberchannel.
110+
"""
111+
pass
112+
113+
114+
@dataclass(kw_only=True)
115+
class UDSOptions(TransportOptionsBase):
116+
"""Options for UDS transport mode."""
117+
118+
_MODE = TransportMode.UDS
119+
120+
uds_service: str
121+
uds_dir: str | Path | None = None
122+
uds_id: str | None = None
123+
124+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
125+
return asdict(self) | {"transport_mode": self.mode.value}
126+
127+
128+
@dataclass(kw_only=True)
129+
class WNUAOptions(TransportOptionsBase):
130+
"""Options for WNUA transport mode."""
131+
132+
_MODE = TransportMode.WNUA
133+
134+
port: int
135+
136+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
137+
return asdict(self) | {"transport_mode": self.mode.value, "host": "localhost"}
138+
139+
140+
@dataclass(kw_only=True)
141+
class MTLSOptions(TransportOptionsBase):
142+
"""Options for mTLS transport mode."""
143+
144+
_MODE = TransportMode.MTLS
145+
146+
certs_dir: str | Path | None = None
147+
host: str = "localhost"
148+
port: int
149+
allow_remote_host: bool = False
150+
151+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
152+
if not self.allow_remote_host:
153+
if self.host not in ("localhost", "127.0.0.1"):
154+
raise ValueError(f"Remote host '{self.host}' specified without setting 'allow_remote_host=True'.")
155+
res = asdict(self)
156+
res.pop("allow_remote_host", None)
157+
return res | {"transport_mode": self.mode.value}
158+
159+
160+
@dataclass(kw_only=True)
161+
class InsecureOptions(TransportOptionsBase):
162+
"""Options for insecure transport mode."""
163+
164+
_MODE = TransportMode.INSECURE
165+
166+
host: str = "localhost"
167+
port: int
168+
allow_remote_host: bool = False
169+
170+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
171+
if not self.allow_remote_host:
172+
if self.host not in ("localhost", "127.0.0.1"):
173+
raise ValueError(f"Remote host '{self.host}' specified without setting 'allow_remote_host=True'.")
174+
res = asdict(self)
175+
res.pop("allow_remote_host", None)
176+
return res | {"transport_mode": self.mode.value}
177+
178+
179+
TransportOptionsType = UDSOptions | WNUAOptions | MTLSOptions | InsecureOptions

0 commit comments

Comments
 (0)