Skip to content

Commit 66415bf

Browse files
fix: Local launcher fixes
1 parent c92cdce commit 66415bf

File tree

11 files changed

+440
-0
lines changed

11 files changed

+440
-0
lines changed

doc/source/conf.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,28 @@
123123

124124
# Ignore files
125125
exclude_patterns = ["changelog/*.md"]
126+
127+
# sphinx gallery options
128+
sphinx_gallery_conf = {
129+
# convert rst to md for ipynb
130+
"pypandoc": True,
131+
# path to your examples scripts
132+
"examples_dirs": ["../../examples"],
133+
# path where to save gallery generated examples
134+
"gallery_dirs": ["examples"],
135+
# Pattern to search for example files
136+
"filename_pattern": r"\.py",
137+
# Remove the "Download all examples" button from the top level gallery
138+
"download_all_examples": False,
139+
# Sort gallery example by file name instead of number of lines (default)
140+
"within_subsection_order": "FileNameSortKey",
141+
# directory where function granular galleries are stored
142+
"backreferences_dir": None,
143+
# Modules for which function level galleries are created. In
144+
"doc_module": "ansys-tools-common",
145+
"promote_jupyter_magic": True,
146+
"image_scrapers": ("matplotlib",),
147+
"ignore_pattern": r"__init__\.py",
148+
"thumbnail_size": (350, 350),
149+
"copyfile_regex": r".*\.rst",
150+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[build-system]
2+
requires = ["flit_core >=3.2,<4"]
3+
build-backend = "flit_core.buildapi"
4+
5+
[project]
6+
name = "example_httpserver_plugin"
7+
authors = [{name = "ANSYS, Inc.", email = "[email protected]"}]
8+
dynamic = ["version", "description"]
9+
dependencies = ["ansys-tools-local-product-launcher", "requests"]
10+
11+
[project.entry-points."ansys.tools.local_product_launcher.launcher"]
12+
"example_httpserver.direct" = "example_httpserver_plugin:Launcher"
13+
"example_httpserver.__fallback__" = "example_httpserver_plugin:Launcher"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
"""Example plugin for ansys-tools-local-product-launcher."""
24+
25+
from .launcher import Launcher, LauncherConfig
26+
27+
__version__ = "0.1.0"
28+
__all__ = ["LauncherConfig", "Launcher"]
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
"""Example launcher plugin, controlling an HTTP server."""
24+
25+
from __future__ import annotations
26+
27+
from dataclasses import dataclass, field
28+
import os
29+
import subprocess
30+
import sys
31+
32+
import requests
33+
34+
from ansys.tools.local_product_launcher.helpers.ports import find_free_ports
35+
from ansys.tools.local_product_launcher.interface import LauncherProtocol, ServerType
36+
37+
38+
# START_LAUNCHER_CONFIG
39+
@dataclass
40+
class LauncherConfig:
41+
"""Defines the configuration options for the HTTP server launcher."""
42+
43+
directory: str = field(default=os.getcwd())
44+
45+
46+
# END_LAUNCHER_CONFIG
47+
48+
49+
# START_LAUNCHER_CLS
50+
class Launcher(LauncherProtocol[LauncherConfig]):
51+
"""Implements launching an HTTP server."""
52+
53+
CONFIG_MODEL = LauncherConfig
54+
SERVER_SPEC = {"main": ServerType.GENERIC}
55+
56+
def __init__(self, *, config: LauncherConfig):
57+
"""Instantiate the HTTP server launcher."""
58+
self._config = config
59+
60+
def start(self) -> None:
61+
"""Start the HTTP server."""
62+
port = find_free_ports()[0]
63+
self._url = f"localhost:{port}"
64+
self._process = subprocess.Popen(
65+
[
66+
sys.executable,
67+
"-m",
68+
"http.server",
69+
"--directory",
70+
self._config.directory,
71+
str(port),
72+
],
73+
stdout=subprocess.DEVNULL,
74+
stderr=subprocess.DEVNULL,
75+
text=True,
76+
)
77+
78+
def stop(self, *, timeout: float | None = None) -> None:
79+
"""Stop the HTTP server."""
80+
self._process.terminate()
81+
try:
82+
self._process.wait(timeout=timeout)
83+
except subprocess.TimeoutExpired:
84+
self._process.kill()
85+
self._process.wait()
86+
87+
def check(self, timeout: float | None = None) -> bool:
88+
"""Check if the server is running."""
89+
try:
90+
# As a simple check, we try to get the main page from the
91+
# server. If it is accessible, we the server is running.
92+
# If not, we assume it is not running.
93+
requests.get(f"http://{self._url}")
94+
return True
95+
except requests.RequestException:
96+
return False
97+
98+
@property
99+
def urls(self) -> dict[str, str]:
100+
"""Addresses on which the server is serving content."""
101+
return {"main": self._url}
102+
103+
104+
# END_LAUNCHER_CLS

examples/local_launcher/example_httpserver_plugin/src/example_httpserver_plugin/py.typed

Whitespace-only changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
"""
24+
Configure the launcher from the command line
25+
--------------------------------------------
26+
27+
This example shows how to configure the ``example_httpserver`` plugin from the command line.
28+
It consists mostly of command-line interactions. With the exception of interactive commands,
29+
this example can be run when downloaded as a Jupyter notebook. The interactive commands and
30+
their outputs are simply shown as text.
31+
32+
The configuration contains only a single value, ``directory``, which specifies the
33+
where the HTTP server is to serve files from.
34+
35+
"""
36+
37+
# %%
38+
# To see the list of launch modes for the ``example_httpserver`` product, run
39+
# this code:
40+
#
41+
# .. code-block:: bash
42+
#
43+
# %%bash
44+
# ansys-launcher configure example_httpserver
45+
#
46+
# Here is the output:
47+
#
48+
# ::
49+
#
50+
# Usage: ansys-launcher configure example_httpserver [OPTIONS] COMMAND [ARGS]...
51+
#
52+
# Options:
53+
# --help Show this message and exit.
54+
#
55+
# Commands:
56+
# direct
57+
58+
# %%
59+
# Interactive configuration
60+
# ~~~~~~~~~~~~~~~~~~~~~~~~~
61+
#
62+
# To interactively specify the available configuration options, run
63+
# this command:
64+
#
65+
# .. code-block:: bash
66+
#
67+
# ansys-launcher configure example_httpserver direct
68+
#
69+
# The preceding command might result in a session like this:
70+
#
71+
# ::
72+
#
73+
# directory:
74+
# [<current-working-directory>]: /path/to/directory
75+
#
76+
# Updated /home/<your_username>/.config/ansys_tools_local_product_launcher/config.json
77+
78+
# %%
79+
# Non-interactive configuration
80+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
81+
#
82+
# Alternatively, you can specify the configuration fully from the command line (non-interactively):
83+
#
84+
# .. code-block:: bash
85+
#
86+
# %%bash
87+
# ansys-launcher configure example_httpserver direct --directory /path/to/directory
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Examples
2+
--------
3+
4+
This section provides examples of how to use the Local Product Launcher to launch a product on
5+
a local machine. For these examples, the Python's built-in ``http.server`` is the application that is
6+
launched.
7+
8+
.. toctree::
9+
:maxdepth: 2
10+
:caption: Contents
11+
12+
cli_configure
13+
py_configure
14+
plugin
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Define the HTTP Server launcher plugin
2+
--------------------------------------
3+
4+
This example shows the launcher plugin that is used to start the Python HTTP server.
5+
The full source is available in the `examples/example_httpserver_plugin directory <https://github.com/ansys/ansys-tools-local-product-launcher/tree/main/examples/example_httpserver_plugin>`_
6+
on GitHub.
7+
8+
While this example explains some aspects of the code, for information on how
9+
to create a plugin for the Local Product Launcher, see :ref:`plugin_creation`.
10+
11+
Launcher code
12+
~~~~~~~~~~~~~
13+
14+
The ``LauncherConfig`` class determines which options are available to the user when configuring the launcher. It exposes
15+
a single option ``directory``, which determines which directory the server is to serve files from.
16+
17+
.. include:: ../../../examples/example_httpserver_plugin/src/example_httpserver_plugin/launcher.py
18+
:literal:
19+
:start-after: # START_LAUNCHER_CONFIG
20+
:end-before: # END_LAUNCHER_CONFIG
21+
22+
23+
The ``Launcher`` class actually starts the server. It needs to fulfill the interface defined by the
24+
:class:`.LauncherProtocol` class. Namely, this interface consists of these endpoints:
25+
26+
- The ``start`` and ``stop`` methods for starting and stopping the server.
27+
- The ``check`` method for checking if the server is running.
28+
- The ``urls`` property for getting the URLs that the the server is serving requests on.
29+
30+
.. include:: ../../../examples/example_httpserver_plugin/src/example_httpserver_plugin/launcher.py
31+
:literal:
32+
:start-after: # START_LAUNCHER_CLS
33+
:end-before: # END_LAUNCHER_CLS
34+
35+
The local product launcher uses this minimal interface to construct a :class:`.ProductInstance` class,
36+
which adds some additional functionality on top. For example, the ``ProductInstance`` class automatically
37+
stops the server when the Python process terminates.
38+
39+
40+
Entrypoint configuration
41+
~~~~~~~~~~~~~~~~~~~~~~~~
42+
43+
Besides the launcher code, the plugin must be registered by adding an entrypoint to
44+
the ``pyproject.toml`` file, as described in :ref:`entrypoint`. In this example,
45+
``flit`` is used as a build tool. Thus, the ``pyproject.toml`` file looks like this:
46+
47+
.. include:: ../../../examples/example_httpserver_plugin/pyproject.toml
48+
:literal:
49+
50+
Two entrypoints for the local product launcher are defined:
51+
52+
- ``direct``: A launch mode that users can configure.
53+
- ``__fallback__``: A fallback mode that is used if no configuration is available.
54+
55+
Both entrypoints use the same launcher class.

0 commit comments

Comments
 (0)