Skip to content

Commit 8da5579

Browse files
committed
Make config.ServerProcess into a Configurable
This will allow us to reuse it
1 parent 3d0eac5 commit 8da5579

File tree

1 file changed

+208
-48
lines changed

1 file changed

+208
-48
lines changed

jupyter_server_proxy/config.py

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

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

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

51238

52239
def _make_proxy_handler(sp: ServerProcess):
@@ -132,34 +319,7 @@ def make_handlers(base_url, server_processes):
132319

133320

134321
def make_server_process(name, server_process_config, serverproxy_config):
135-
le = server_process_config.get("launcher_entry", {})
136-
return ServerProcess(
137-
name=name,
138-
command=server_process_config.get("command", list()),
139-
environment=server_process_config.get("environment", {}),
140-
timeout=server_process_config.get("timeout", 5),
141-
absolute_url=server_process_config.get("absolute_url", False),
142-
port=server_process_config.get("port", 0),
143-
unix_socket=server_process_config.get("unix_socket", None),
144-
mappath=server_process_config.get("mappath", {}),
145-
launcher_entry=LauncherEntry(
146-
enabled=le.get("enabled", True),
147-
icon_path=le.get("icon_path"),
148-
title=le.get("title", name),
149-
path_info=le.get("path_info", name + "/"),
150-
category=le.get("category", "Notebook"),
151-
),
152-
new_browser_tab=server_process_config.get("new_browser_tab", True),
153-
request_headers_override=server_process_config.get(
154-
"request_headers_override", {}
155-
),
156-
rewrite_response=server_process_config.get(
157-
"rewrite_response",
158-
tuple(),
159-
),
160-
update_last_activity=server_process_config.get("update_last_activity", True),
161-
raw_socket_proxy=server_process_config.get("raw_socket_proxy", False),
162-
)
322+
return ServerProcess(name=name, **server_process_config)
163323

164324

165325
class ServerProxy(Configurable):

0 commit comments

Comments
 (0)