Skip to content

Commit

Permalink
Entrypoints can be traitlets Configurable
Browse files Browse the repository at this point in the history
This means a custom proxy can be installed, and configured using traitlets in a standard jupyter_server_config file
  • Loading branch information
manics committed Nov 6, 2024
1 parent c01de61 commit 89edf8c
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ jobs:
jupyter lab extension list
jupyter lab extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*'
- name: Install a dummy entrypoint so we can test its loaded correctly
run: |
pip install ./tests/resources/dummyentrypoint/
# we have installed a pre-built wheel and configured code coverage to
# inspect "jupyter_server_proxy", by re-locating to another directory,
# there is no confusion about "jupyter_server_proxy" referring to our
Expand Down
7 changes: 6 additions & 1 deletion jupyter_server_proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ._version import __version__ # noqa
from .api import IconHandler, ServersInfoHandler
from .config import ServerProcess, ServerProcessEntryPoint
from .config import ServerProxy as ServerProxyConfig
from .config import get_entrypoint_server_processes, make_handlers, make_server_process
from .handlers import setup_handlers
Expand Down Expand Up @@ -45,7 +46,9 @@ def _load_jupyter_server_extension(nbapp):
make_server_process(name, server_process_config, serverproxy_config)
for name, server_process_config in serverproxy_config.servers.items()
]
server_processes += get_entrypoint_server_processes(serverproxy_config)
server_processes += get_entrypoint_server_processes(
serverproxy_config, parent=nbapp
)
server_handlers = make_handlers(base_url, server_processes)
nbapp.web_app.add_handlers(".*", server_handlers)

Expand Down Expand Up @@ -81,3 +84,5 @@ def _load_jupyter_server_extension(nbapp):
# For backward compatibility
load_jupyter_server_extension = _load_jupyter_server_extension
_jupyter_server_extension_paths = _jupyter_server_extension_points

__all__ = ["ServerProcess", "ServerProcessEntryPoint"]
31 changes: 27 additions & 4 deletions jupyter_server_proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _default_path_info(self):
)


class ServerProcess(Configurable):
class _ServerProcess(Configurable):
name = Unicode(help="Name of the server").tag(config=True)

command = List(
Expand Down Expand Up @@ -264,6 +264,22 @@ def cats_only(response, path):
).tag(config=True)


class ServerProcess(_ServerProcess):
"""
A configurable server process for single standalone servers.
This is separate from ServerProcessEntryPoint so that we can configure it
independently of ServerProcessEntryPoint
"""


class ServerProcessEntryPoint(_ServerProcess):
"""
A ServeProcess entrypoint that is a Configurable.
This is separate from ServerProcess so that we can configure it
independently of ServerProcess
"""


def _make_proxy_handler(sp: ServerProcess):
"""
Create an appropriate handler with given parameters
Expand Down Expand Up @@ -319,16 +335,23 @@ def get_timeout(self):
return _Proxy


def get_entrypoint_server_processes(serverproxy_config):
def get_entrypoint_server_processes(serverproxy_config, parent):
sps = []
for entry_point in entry_points(group="jupyter_serverproxy_servers"):
name = entry_point.name
try:
server_process_config = entry_point.load()()
server_process_callable = entry_point.load()
if issubclass(server_process_callable, ServerProcessEntryPoint):
server_process = server_process_callable(name=name, parent=parent)
sps.append(server_process)
else:
server_process_config = server_process_callable()
sps.append(
make_server_process(name, server_process_config, serverproxy_config)
)
except Exception as e:
warn(f"entry_point {name} was unable to be loaded: {str(e)}")
continue
sps.append(make_server_process(name, server_process_config, serverproxy_config))
return sps


Expand Down
7 changes: 7 additions & 0 deletions tests/resources/dummyentrypoint/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "test-jsp-dummyentrypoint"
version = "0.0.0"


[project.entry-points.jupyter_serverproxy_servers]
test-serverprocessentrypoint = "test_jsp_dummyentrypoint:CustomServerProcessEntryPoint"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Test whether ServerProcessEntryPoint can be configured using traitlets
"""
import sys
from pathlib import Path

from traitlets.config import default

from jupyter_server_proxy import ServerProcessEntryPoint


class CustomServerProcessEntryPoint(ServerProcessEntryPoint):
@default("command")
def _default_command(self):
parent = Path(__file__).parent.resolve()
return [sys.executable, str(parent / "httpinfo.py"), "--port={port}"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
Simple webserver to respond with an echo of the sent request.
"""
import argparse
from http.server import BaseHTTPRequestHandler, HTTPServer


class EchoRequestInfo(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(f"{self.requestline}\n".encode())
self.wfile.write(f"{self.headers}\n".encode())


if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int)
args = ap.parse_args()

httpd = HTTPServer(("127.0.0.1", args.port), EchoRequestInfo)
httpd.serve_forever()
6 changes: 6 additions & 0 deletions tests/resources/jupyter_server_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ def my_env():
return {"MYVAR": "String with escaped {{var}}"}


# Traitlets configuration for test-serverprocessentrypoint in the dummyentrypoint package
c.CustomServerProcessEntryPoint.request_headers_override = {
"X-Custom-Header": "custom-configurable"
}


c.ServerProxy.servers = {
"python-http": {
"command": [sys.executable, _get_path("httpinfo.py"), "--port={port}"],
Expand Down
8 changes: 8 additions & 0 deletions tests/test_proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,11 @@ async def test_server_proxy_rawsocket(
await conn.write_message(msg)
res = await conn.read_message()
assert res == msg.swapcase()


def test_server_configurable_class(a_server_port_and_token: Tuple[int, str]) -> None:
PORT, TOKEN = a_server_port_and_token
r = request_get(PORT, "/test-serverprocessentrypoint/", TOKEN, host="127.0.0.1")
assert r.code == 200
s = r.read().decode("ascii")
assert "X-Custom-Header: custom-configurable\n" in s

0 comments on commit 89edf8c

Please sign in to comment.