Skip to content

Commit 1203253

Browse files
committed
Make config.ServerProcess into a Configurable
This will allow us to reuse it
1 parent 11fcf89 commit 1203253

File tree

1 file changed

+209
-48
lines changed

1 file changed

+209
-48
lines changed

jupyter_server_proxy/config.py

Lines changed: 209 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,21 @@
1212
from importlib.metadata import entry_points
1313

1414
from jupyter_server.utils import url_path_join as ujoin
15-
from traitlets import Callable, Dict, List, Tuple, Union, default, observe
15+
from traitlets import (
16+
Bool,
17+
Callable,
18+
Dict,
19+
Instance,
20+
Int,
21+
List,
22+
TraitError,
23+
Tuple,
24+
Unicode,
25+
Union,
26+
default,
27+
observe,
28+
validate,
29+
)
1630
from traitlets.config import Configurable
1731

1832
from .handlers import AddSlashHandler, NamedLocalProxyHandler, SuperviseAndProxyHandler
@@ -21,25 +35,199 @@
2135
LauncherEntry = namedtuple(
2236
"LauncherEntry", ["enabled", "icon_path", "title", "path_info", "category"]
2337
)
24-
ServerProcess = namedtuple(
25-
"ServerProcess",
26-
[
27-
"name",
28-
"command",
29-
"environment",
30-
"timeout",
31-
"absolute_url",
32-
"port",
33-
"unix_socket",
34-
"mappath",
35-
"launcher_entry",
36-
"new_browser_tab",
37-
"request_headers_override",
38-
"rewrite_response",
39-
"update_last_activity",
40-
"raw_socket_proxy",
41-
],
42-
)
38+
39+
40+
class ServerProcess(Configurable):
41+
name = Unicode(help="Name").tag(config=True)
42+
command = List(
43+
Unicode(),
44+
help="""\
45+
An optional list of strings that should be the full command to be executed.
46+
The optional template arguments {{port}}, {{unix_socket}} and {{base_url}}
47+
will be substituted with the port or Unix socket path the process should
48+
listen on and the base-url of the notebook.
49+
50+
Could also be a callable. It should return a list.
51+
52+
If the command is not specified or is an empty list, the server
53+
process is assumed to be started ahead of time and already available
54+
to be proxied to.
55+
""",
56+
).tag(config=True)
57+
58+
environment = Union(
59+
[Dict(Unicode()), Callable()],
60+
default_value={},
61+
help="""\
62+
A dictionary of environment variable mappings. As with the command
63+
traitlet, {{port}}, {{unix_socket}} and {{base_url}} will be substituted.
64+
65+
Could also be a callable. It should return a dictionary.
66+
""",
67+
).tag(config=True)
68+
69+
timeout = Int(
70+
5, help="Timeout in seconds for the process to become ready, default 5s."
71+
).tag(config=True)
72+
73+
absolute_url = Bool(
74+
False,
75+
help="""
76+
Proxy requests default to being rewritten to '/'. If this is True,
77+
the absolute URL will be sent to the backend instead.
78+
""",
79+
).tag(config=True)
80+
81+
port = Int(
82+
0,
83+
help="""
84+
Set the port that the service will listen on. The default is to automatically select an unused port.
85+
""",
86+
).tag(config=True)
87+
88+
unix_socket = Union(
89+
[Bool(False), Unicode()],
90+
default_value=None,
91+
help="""
92+
If set, the service will listen on a Unix socket instead of a TCP port.
93+
Set to True to use a socket in a new temporary folder, or a string
94+
path to a socket. This overrides port.
95+
96+
Proxying websockets over a Unix socket requires Tornado >= 6.3.
97+
""",
98+
).tag(config=True)
99+
100+
mappath = Union(
101+
[Dict(Unicode()), Callable()],
102+
default_value={},
103+
help="""
104+
Map request paths to proxied paths.
105+
Either a dictionary of request paths to proxied paths,
106+
or a callable that takes parameter ``path`` and returns the proxied path.
107+
""",
108+
).tag(config=True)
109+
110+
# Can't use Instance(LauncherEntry) because LauncherEntry is not a class
111+
launcher_entry = Union(
112+
[Instance(object), Dict()],
113+
allow_none=False,
114+
help="""
115+
A dictionary of various options for entries in classic notebook / jupyterlab launchers.
116+
117+
Keys recognized are:
118+
119+
enabled
120+
Set to True (default) to make an entry in the launchers. Set to False to have no
121+
explicit entry.
122+
123+
icon_path
124+
Full path to an svg icon that could be used with a launcher. Currently only used by the
125+
JupyterLab launcher
126+
127+
title
128+
Title to be used for the launcher entry. Defaults to the name of the server if missing.
129+
130+
path_info
131+
The trailing path that is appended to the user's server URL to access the proxied server.
132+
By default it is the name of the server followed by a trailing slash.
133+
134+
category
135+
The category for the launcher item. Currently only used by the JupyterLab launcher.
136+
By default it is "Notebook".
137+
""",
138+
).tag(config=True)
139+
140+
@validate("launcher_entry")
141+
def _validate_launcher_entry(self, proposal):
142+
le = proposal["value"]
143+
invalid_keys = set(le.keys()).difference(
144+
{"enabled", "icon_path", "title", "path_info", "category"}
145+
)
146+
if invalid_keys:
147+
raise TraitError(
148+
f"launcher_entry {le} contains invalid keys: {invalid_keys}"
149+
)
150+
return (
151+
LauncherEntry(
152+
enabled=le.get("enabled", True),
153+
icon_path=le.get("icon_path"),
154+
title=le.get("title", self.name),
155+
path_info=le.get("path_info", self.name + "/"),
156+
category=le.get("category", "Notebook"),
157+
),
158+
)
159+
160+
new_browser_tab = Bool(
161+
True,
162+
help="""
163+
Set to True (default) to make the proxied server interface opened as a new browser tab. Set to False
164+
to have it open a new JupyterLab tab. This has no effect in classic notebook.
165+
""",
166+
).tag(config=True)
167+
168+
request_headers_override = Dict(
169+
Unicode(),
170+
default_value={},
171+
help="""
172+
A dictionary of additional HTTP headers for the proxy request. As with
173+
the command traitlet, {{port}}, {{unix_socket}} and {{base_url}} will be substituted.
174+
""",
175+
).tag(config=True)
176+
177+
rewrite_response = Union(
178+
[Callable(), List(Callable())],
179+
default_value=[],
180+
help="""
181+
An optional function to rewrite the response for the given service.
182+
Input is a RewritableResponse object which is an argument that MUST be named
183+
``response``. The function should modify one or more of the attributes
184+
``.body``, ``.headers``, ``.code``, or ``.reason`` of the ``response``
185+
argument. For example:
186+
187+
def dog_to_cat(response):
188+
response.headers["I-Like"] = "tacos"
189+
response.body = response.body.replace(b'dog', b'cat')
190+
191+
c.ServerProxy.servers['my_server']['rewrite_response'] = dog_to_cat
192+
193+
The ``rewrite_response`` function can also accept several optional
194+
positional arguments. Arguments named ``host``, ``port``, and ``path`` will
195+
receive values corresponding to the URL ``/proxy/<host>:<port><path>``. In
196+
addition, the original Tornado ``HTTPRequest`` and ``HTTPResponse`` objects
197+
are available as arguments named ``request`` and ``orig_response``. (These
198+
objects should not be modified.)
199+
200+
A list or tuple of functions can also be specified for chaining multiple
201+
rewrites. For example:
202+
203+
def cats_only(response, path):
204+
if path.startswith("/cat-club"):
205+
response.code = 403
206+
response.body = b"dogs not allowed"
207+
208+
c.ServerProxy.servers['my_server']['rewrite_response'] = [dog_to_cat, cats_only]
209+
210+
Note that if the order is reversed to ``[cats_only, dog_to_cat]``, then accessing
211+
``/cat-club`` will produce a "403 Forbidden" response with body "cats not allowed"
212+
instead of "dogs not allowed".
213+
214+
Defaults to the empty tuple ``tuple()``.
215+
""",
216+
).tag(config=True)
217+
218+
update_last_activity = Bool(
219+
True, help="Will cause the proxy to report activity back to jupyter server."
220+
).tag(config=True)
221+
222+
raw_socket_proxy = Bool(
223+
False,
224+
help="""
225+
Proxy websocket requests as a raw TCP (or unix socket) stream.
226+
In this mode, only websockets are handled, and messages are sent to the backend,
227+
similar to running a websockify layer (https://github.com/novnc/websockify).
228+
All other HTTP requests return 405 (and thus this will also bypass rewrite_response).
229+
""",
230+
).tag(config=True)
43231

44232

45233
def _make_proxy_handler(sp: ServerProcess):
@@ -125,34 +313,7 @@ def make_handlers(base_url, server_processes):
125313

126314

127315
def make_server_process(name, server_process_config, serverproxy_config):
128-
le = server_process_config.get("launcher_entry", {})
129-
return ServerProcess(
130-
name=name,
131-
command=server_process_config.get("command", list()),
132-
environment=server_process_config.get("environment", {}),
133-
timeout=server_process_config.get("timeout", 5),
134-
absolute_url=server_process_config.get("absolute_url", False),
135-
port=server_process_config.get("port", 0),
136-
unix_socket=server_process_config.get("unix_socket", None),
137-
mappath=server_process_config.get("mappath", {}),
138-
launcher_entry=LauncherEntry(
139-
enabled=le.get("enabled", True),
140-
icon_path=le.get("icon_path"),
141-
title=le.get("title", name),
142-
path_info=le.get("path_info", name + "/"),
143-
category=le.get("category", "Notebook"),
144-
),
145-
new_browser_tab=server_process_config.get("new_browser_tab", True),
146-
request_headers_override=server_process_config.get(
147-
"request_headers_override", {}
148-
),
149-
rewrite_response=server_process_config.get(
150-
"rewrite_response",
151-
tuple(),
152-
),
153-
update_last_activity=server_process_config.get("update_last_activity", True),
154-
raw_socket_proxy=server_process_config.get("raw_socket_proxy", False),
155-
)
316+
return ServerProcess(name=name, **server_process_config)
156317

157318

158319
class ServerProxy(Configurable):

0 commit comments

Comments
 (0)