Skip to content

Commit 55b2faf

Browse files
committed
Launcher: implement gRPC transport option handling
1 parent d4d4bc0 commit 55b2faf

File tree

11 files changed

+355
-126
lines changed

11 files changed

+355
-126
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.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.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.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.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.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.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.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, helpers, interface, product_instance, grpc_transport
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: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2025 ANSYS, Inc. and/or its affiliates.
1+
# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
22
# SPDX-License-Identifier: MIT
33
#
44
#
@@ -44,7 +44,7 @@ def format_prompt(*, field_name: str, description: str | None) -> str:
4444
"""Get the formatted prompt string from its field name and description."""
4545
prompt = f"\n{field_name}:"
4646
if description is not None:
47-
prompt += "\n" + textwrap.indent(description, " " * 4)
47+
prompt += f"\n" + textwrap.indent(description, " " * 4)
4848
prompt += "\n"
4949
return prompt
5050

@@ -64,7 +64,9 @@ def get_subcommands_from_plugins(
6464
for launch_mode, launcher_kls in launch_mode_configs.items():
6565
launcher_config_kls = launcher_kls.CONFIG_MODEL
6666

67-
_config_writer_callback = config_writer_callback_factory(launcher_config_kls, product_name, launch_mode)
67+
_config_writer_callback = config_writer_callback_factory(
68+
launcher_config_kls, product_name, launch_mode
69+
)
6870
launch_mode_command = click.Command(launch_mode, callback=_config_writer_callback)
6971
for field in dataclasses.fields(launcher_config_kls):
7072
option = get_option_from_field(field)
@@ -251,14 +253,19 @@ def show_config() -> None:
251253

252254
if not is_configured(product_name=product_name, launch_mode=launch_mode):
253255
try:
254-
config = get_config_for(product_name=product_name, launch_mode=launch_mode)
256+
config = get_config_for(
257+
product_name=product_name, launch_mode=launch_mode
258+
)
255259
click.echo(" No configuration is set (uses defaults).")
256-
except KeyError:
260+
except (KeyError, RuntimeError):
257261
click.echo(" No configuration is set (no defaults available).")
258262
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)}")
263+
try:
264+
config = get_config_for(product_name=product_name, launch_mode=launch_mode)
265+
for field in dataclasses.fields(config):
266+
click.echo(f" {field.name}: {getattr(config, field.name)}")
267+
except TypeError:
268+
click.echo(" No configuration is set (invalid configuration).")
262269
except KeyError:
263270
click.echo(" No configuration is set.")
264271
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.launcher"
3434

3535

3636
def get_launcher(*, product_name: str, launch_mode: str) -> type[LauncherProtocol[DataclassProtocol]]:
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
"""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+
61+
import enum
62+
63+
class StrEnum(str, enum.Enum):
64+
"""String enum."""
65+
66+
pass
67+
68+
69+
class TransportMode(StrEnum):
70+
"""Enumeration of transport modes supported by the FileTransfer Tool."""
71+
72+
UDS = "uds"
73+
WNUA = "wnua"
74+
MTLS = "mtls"
75+
INSECURE = "insecure"
76+
77+
78+
class TransportOptionsBase(ABC):
79+
"""Base class for transport options."""
80+
81+
_MODE: ClassVar[TransportMode]
82+
83+
@property
84+
def mode(self) -> TransportMode:
85+
"""Transport mode."""
86+
return self._MODE
87+
88+
def create_channel(self, **extra_kwargs: Any) -> grpc.Channel:
89+
"""Create a gRPC channel using the transport options.
90+
91+
Parameters
92+
----------
93+
extra_kwargs :
94+
Extra keyword arguments to pass to the channel creation function.
95+
96+
Returns
97+
-------
98+
:
99+
gRPC channel created using the transport options.
100+
"""
101+
return cyberchannel.create_channel(**self._to_cyberchannel_kwargs(), **extra_kwargs)
102+
103+
@abstractmethod
104+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
105+
"""Convert transport options to cyberchannel keyword arguments.
106+
107+
Returns
108+
-------
109+
:
110+
Dictionary of keyword arguments for cyberchannel.
111+
"""
112+
pass
113+
114+
115+
@dataclass(kw_only=True)
116+
class UDSOptions(TransportOptionsBase):
117+
"""Options for UDS transport mode."""
118+
119+
_MODE = TransportMode.UDS
120+
121+
uds_service: str
122+
uds_dir: str | Path | None = None
123+
uds_id: str | None = None
124+
125+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
126+
return asdict(self) | {"transport_mode": self.mode.value}
127+
128+
129+
@dataclass(kw_only=True)
130+
class WNUAOptions(TransportOptionsBase):
131+
"""Options for WNUA transport mode."""
132+
133+
_MODE = TransportMode.WNUA
134+
135+
port: int
136+
137+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
138+
return asdict(self) | {"transport_mode": self.mode.value, "host": "localhost"}
139+
140+
141+
@dataclass(kw_only=True)
142+
class MTLSOptions(TransportOptionsBase):
143+
"""Options for mTLS transport mode."""
144+
145+
_MODE = TransportMode.MTLS
146+
147+
certs_dir: str | Path | None = None
148+
host: str = "localhost"
149+
port: int
150+
allow_remote_host: bool = False
151+
152+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
153+
if not self.allow_remote_host:
154+
if self.host not in ("localhost", "127.0.0.1"):
155+
raise ValueError(
156+
f"Remote host '{self.host}' specified without setting 'allow_remote_host=True'."
157+
)
158+
res = asdict(self)
159+
res.pop("allow_remote_host", None)
160+
return res | {"transport_mode": self.mode.value}
161+
162+
163+
@dataclass(kw_only=True)
164+
class InsecureOptions(TransportOptionsBase):
165+
"""Options for insecure transport mode."""
166+
167+
_MODE = TransportMode.INSECURE
168+
169+
host: str = "localhost"
170+
port: int
171+
allow_remote_host: bool = False
172+
173+
def _to_cyberchannel_kwargs(self) -> dict[str, Any]:
174+
if not self.allow_remote_host:
175+
if self.host not in ("localhost", "127.0.0.1"):
176+
raise ValueError(
177+
f"Remote host '{self.host}' specified without setting 'allow_remote_host=True'."
178+
)
179+
res = asdict(self)
180+
res.pop("allow_remote_host", None)
181+
return res | {"transport_mode": self.mode.value}
182+
183+
184+
TransportOptionsType = UDSOptions | WNUAOptions | MTLSOptions | InsecureOptions

0 commit comments

Comments
 (0)