import os
import shlex
import sys
from string import Template
import rich_click as click
from harp import ROOT_DIR, get_logger
from harp.utils.network import get_available_network_port
HARP_DASHBOARD_SERVICE = "harp:dashboard"
HARP_DOCS_SERVICE = "harp:docs"
HARP_SERVER_SERVICE = "harp:server"
HARP_UI_SERVICE = "harp:ui"
logger = get_logger(__name__)
[docs]
def quote(x):
return shlex.quote(str(x))
[docs]
class HonchoManagerFactory:
defaults = {HARP_DASHBOARD_SERVICE, HARP_SERVER_SERVICE}
commands = {}
[docs]
def __init__(self, *, proxy_options=(), dashboard_devserver_port=None):
self.ports = {HARP_DASHBOARD_SERVICE: dashboard_devserver_port or get_available_network_port()}
self.proxy_ports = {}
self.proxy_options = proxy_options
self.cwds = {}
# copy to allow changes on this instance only
self.names = {HARP_DASHBOARD_SERVICE, HARP_SERVER_SERVICE, HARP_DOCS_SERVICE, HARP_UI_SERVICE}
self.commands = {**self.commands}
def _get_dashboard_executable(self, processes):
# todo make sure the frontend tools are available, in the right versions
if not os.path.exists(os.path.join(ROOT_DIR, "harp_apps/dashboard/frontend/node_modules")):
# todo better guidance
raise click.UsageError(
"Dashboard's frontend dependencies are not installed.\nYour options are: run a production version "
"(shortcut to come), install the dependencies (with `harp install-dev`), or do not run the dashboard."
)
host = "localhost"
return (
os.path.join(ROOT_DIR, "harp_apps/dashboard/frontend"),
" ".join(
[
"pnpm exec vite",
f"--host {host}",
f"--port {self.ports[HARP_DASHBOARD_SERVICE]}",
"--strictPort",
"--clearScreen false",
]
),
)
commands[HARP_DASHBOARD_SERVICE] = _get_dashboard_executable
def _get_server_executable(self, processes):
cmd = f"{sys.executable} -m harp server"
proxy_options = list(self.proxy_options)
if HARP_DASHBOARD_SERVICE in processes:
proxy_options.append(f"--set dashboard.devserver_port={quote(self.ports[HARP_DASHBOARD_SERVICE])}")
for _name, _port in self.proxy_ports.items():
proxy_options.append(f"--endpoint {quote(_name)}={_port}:http://localhost:{self.ports[_name]}")
if proxy_options:
cmd += " " + " ".join(proxy_options)
return ROOT_DIR, f'watchfiles --filter python "{cmd}" harp harp_apps'
commands[HARP_SERVER_SERVICE] = _get_server_executable
def _get_docs_executable(self, processes):
# todo add check available
return os.path.join(ROOT_DIR, "docs"), "poetry run sphinx-autobuild . _build/html"
commands[HARP_DOCS_SERVICE] = _get_docs_executable
def _get_ui_executable(self, processes):
# todo add check available
return os.path.join(ROOT_DIR, "harp_apps/dashboard/frontend"), "pnpm ui:serve"
commands[HARP_UI_SERVICE] = _get_ui_executable
[docs]
def build(self, processes, /, *, more_env=None):
from honcho.manager import Manager
from honcho.printer import Printer
manager = Manager(Printer(sys.stdout))
for name in processes:
if name not in self.commands:
raise ValueError(f"Unknown process: {name}")
if callable(self.commands[name]):
working_directory, command = self.commands[name](self, processes)
else:
working_directory, command = self.cwds.get(name, None), self.commands[name]
if working_directory is None:
working_directory = os.getcwd()
else:
working_directory = os.path.join(os.getcwd(), working_directory)
e = os.environ.copy()
more_env = more_env or {}
manager.add_process(name, command, cwd=working_directory, env=e | more_env.get(name, {}))
# this hack will change the class impl at runtime for frontend process to avoid misleading log at start.
if name == HARP_DASHBOARD_SERVICE:
from harp.commandline.utils._hacks import QuietViteHonchoProcess
manager._processes[name]["obj"].__class__ = QuietViteHonchoProcess
return manager
[docs]
def parse_server_subprocesses_options(server_subprocesses):
# TODO uniformise with --endpoint to be in format name=port:cmd
processes = {}
for server_subprocess in server_subprocesses:
try:
_name, _port, _path, _command = server_subprocess.split(":", 3)
except ValueError as exc:
raise click.UsageError(
"Invalid server subprocess configuration. Expected format: <name>:<port>:<path>:<command>."
) from exc
_target_port = get_available_network_port()
_command = Template(_command).safe_substitute(port=_target_port)
processes[_name] = (_port, _path, _command, _target_port)
return processes