Skip to content

Commit c190772

Browse files
authored
Merge pull request #521 from jwindgassen/sp-hastraits
Make ServerProcess and LauncherEntry a HasTraits
2 parents 53c3a00 + 3a03808 commit c190772

File tree

6 files changed

+71
-47
lines changed

6 files changed

+71
-47
lines changed

docs/source/standalone.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jupyter standaloneproxy --address=localhost --port=8000 ...
5151

5252
### Disable Authentication
5353

54-
For testing, it can be useful to disable the authentication with JupyterHub. Passing `--skip-authentication` will
54+
For testing, it can be useful to disable the authentication with JupyterHub. Passing `--no-authentication` will
5555
not trigger the login process when accessing the application.
5656

5757
```{warning} Disabling authentication will leave the application open to anyone! Be careful with it,
@@ -76,7 +76,7 @@ c.StandaloneProxyServer.address = "localhost"
7676
c.StandaloneProxyServer.port = 8000
7777

7878
# Disable authentication
79-
c.StandaloneProxyServer.skip_authentication = True
79+
c.StandaloneProxyServer.no_authentication = True
8080
```
8181

8282
A default config file can be emitted by running `jupyter standaloneproxy --generate-config`

jupyter_server_proxy/__init__.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ._version import __version__ # noqa
44
from .api import IconHandler, ServersInfoHandler
55
from .config import ServerProxy as ServerProxyConfig
6-
from .config import get_entrypoint_server_processes, make_handlers, make_server_process
6+
from .config import get_entrypoint_server_processes, make_handlers
77
from .handlers import setup_handlers
88

99

@@ -41,11 +41,8 @@ def _load_jupyter_server_extension(nbapp):
4141
base_url = nbapp.web_app.settings["base_url"]
4242
serverproxy_config = ServerProxyConfig(parent=nbapp)
4343

44-
server_processes = [
45-
make_server_process(name, server_process_config, serverproxy_config)
46-
for name, server_process_config in serverproxy_config.servers.items()
47-
]
48-
server_processes += get_entrypoint_server_processes(serverproxy_config)
44+
server_processes = list(serverproxy_config.servers.values())
45+
server_processes += get_entrypoint_server_processes()
4946
server_handlers = make_handlers(base_url, server_processes)
5047
nbapp.web_app.add_handlers(".*", server_handlers)
5148

jupyter_server_proxy/config.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Callable,
2020
Dict,
2121
Float,
22+
HasTraits,
2223
Instance,
2324
Int,
2425
List,
@@ -35,7 +36,7 @@
3536
from .rawsocket import RawSocketHandler, SuperviseAndRawSocketHandler
3637

3738

38-
class LauncherEntry(Configurable):
39+
class LauncherEntry(HasTraits):
3940
enabled = Bool(
4041
True,
4142
help="""
@@ -76,7 +77,7 @@ class LauncherEntry(Configurable):
7677

7778

7879
class ServerProcess(Configurable):
79-
name = Unicode(help="Name of the server").tag(config=True)
80+
name = Unicode(help="Name of the server")
8081

8182
command = Union(
8283
[List(Unicode()), Callable()],
@@ -92,7 +93,7 @@ class ServerProcess(Configurable):
9293
process is assumed to be started ahead of time and already available
9394
to be proxied to.
9495
""",
95-
).tag(config=True)
96+
)
9697

9798
environment = Union(
9899
[Dict(Unicode()), Callable()],
@@ -115,14 +116,14 @@ class ServerProcess(Configurable):
115116
Proxy requests default to being rewritten to ``/``. If this is True,
116117
the absolute URL will be sent to the backend instead.
117118
""",
118-
).tag(config=True)
119+
)
119120

120121
port = Int(
121122
0,
122123
help="""
123124
Set the port that the service will listen on. The default is to automatically select an unused port.
124125
""",
125-
).tag(config=True)
126+
)
126127

127128
unix_socket = Union(
128129
[Bool(False), Unicode()],
@@ -135,7 +136,7 @@ class ServerProcess(Configurable):
135136
136137
Proxying websockets over a Unix socket requires Tornado >= 6.3.
137138
""",
138-
).tag(config=True)
139+
)
139140

140141
mappath = Union(
141142
[Dict(Unicode()), Callable()],
@@ -145,15 +146,15 @@ class ServerProcess(Configurable):
145146
Either a dictionary of request paths to proxied paths,
146147
or a callable that takes parameter ``path`` and returns the proxied path.
147148
""",
148-
).tag(config=True)
149+
)
149150

150151
launcher_entry = Union(
151152
[Instance(LauncherEntry), Dict()],
152153
allow_none=False,
153154
help="""
154-
A dictionary of various options for entries in classic notebook / jupyterlab launchers.
155+
Specify various options for entries in classic notebook / jupyterlab launchers.
155156
156-
Keys recognized are:
157+
Must be an instance of ``LauncherEntry`` or a dictionary with the following keys:
157158
158159
``enabled``
159160
Set to True (default) to make an entry in the launchers. Set to False to have no
@@ -174,13 +175,18 @@ class ServerProcess(Configurable):
174175
The category for the launcher item. Currently only used by the JupyterLab launcher.
175176
By default it is "Notebook".
176177
""",
177-
).tag(config=True)
178+
)
178179

179180
@validate("launcher_entry")
180181
def _validate_launcher_entry(self, proposal):
181-
kwargs = {"title": self.name, "path_info": self.name + "/"}
182-
kwargs.update(proposal["value"])
183-
return LauncherEntry(**kwargs)
182+
if isinstance(proposal["value"], LauncherEntry):
183+
proposal["value"].title = self.name
184+
proposal["value"].path_info = self.name + "/"
185+
return proposal["value"]
186+
else:
187+
kwargs = {"title": self.name, "path_info": self.name + "/"}
188+
kwargs.update(proposal["value"])
189+
return LauncherEntry(**kwargs)
184190

185191
@default("launcher_entry")
186192
def _default_launcher_entry(self):
@@ -201,7 +207,7 @@ def _default_launcher_entry(self):
201207
A dictionary of additional HTTP headers for the proxy request. As with
202208
the command traitlet, ``{port}``, ``{unix_socket}`` and ``{base_url}`` will be substituted.
203209
""",
204-
).tag(config=True)
210+
)
205211

206212
rewrite_response = Union(
207213
[Callable(), List(Callable())],
@@ -260,7 +266,7 @@ def cats_only(response, path):
260266
similar to running a websockify layer (https://github.com/novnc/websockify).
261267
All other HTTP requests return 405 (and thus this will also bypass rewrite_response).
262268
""",
263-
).tag(config=True)
269+
)
264270

265271
def get_proxy_base_class(self) -> tuple[type | None, dict]:
266272
"""
@@ -341,17 +347,17 @@ def get_timeout(self):
341347
return _Proxy, proxy_kwargs
342348

343349

344-
def get_entrypoint_server_processes(serverproxy_config):
345-
sps = []
350+
def get_entrypoint_server_processes():
351+
processes = []
346352
for entry_point in entry_points(group="jupyter_serverproxy_servers"):
347353
name = entry_point.name
348354
try:
349355
server_process_config = entry_point.load()()
350356
except Exception as e:
351357
warn(f"entry_point {name} was unable to be loaded: {str(e)}")
352358
continue
353-
sps.append(make_server_process(name, server_process_config, serverproxy_config))
354-
return sps
359+
processes.append(ServerProcess(name=name, **server_process_config))
360+
return processes
355361

356362

357363
def make_handlers(base_url: str, server_processes: list[ServerProcess]):
@@ -384,20 +390,36 @@ def _serverproxy_servers_help():
384390

385391
class ServerProxy(Configurable):
386392
servers = Dict(
387-
{},
393+
key_trait=Unicode(),
394+
value_trait=Union([Dict(), Instance(ServerProcess)]),
388395
help="""
389396
Dictionary of processes to supervise & proxy.
390397
391398
Key should be the name of the process. This is also used by default as
392399
the URL prefix, and all requests matching this prefix are routed to this process.
393400
394-
Value should be a dictionary with the following keys:
401+
Value should be an instance of ``ServerProcess`` or a dictionary with the following keys:
395402
396403
"""
397404
+ indent(_serverproxy_servers_help(), " "),
398405
config=True,
399406
)
400407

408+
@validate("servers")
409+
def _validate_servers(self, proposal):
410+
servers = {}
411+
412+
for name, server_process in proposal["value"].items():
413+
if isinstance(server_process, ServerProcess):
414+
server_process.name = server_process.name or name
415+
servers[name] = server_process
416+
else:
417+
kwargs = {"name": name}
418+
kwargs.update(**server_process)
419+
servers[name] = ServerProcess(**kwargs)
420+
421+
return servers
422+
401423
non_service_rewrite_response = Union(
402424
default_value=tuple(),
403425
trait_types=[List(), Tuple(), Callable()],

jupyter_server_proxy/standalone/app.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def _validate_prefix(self, proposal):
5858
prefix = prefix[:-1]
5959
return prefix
6060

61-
skip_authentication = Bool(
61+
no_authentication = Bool(
6262
default=False,
6363
help="""
6464
Do not authenticate access to the server via JupyterHub. When set,
@@ -146,11 +146,16 @@ def __init__(self, **kwargs):
146146
{"ServerProcess": {"raw_socket_proxy": True}},
147147
dedent(ServerProcess.raw_socket_proxy.help),
148148
),
149-
"skip-authentication": (
150-
{"StandaloneProxyServer": {"skip_authentication": True}},
151-
dedent(self.__class__.skip_authentication.help),
149+
"no-authentication": (
150+
{"StandaloneProxyServer": {"no_authentication": True}},
151+
dedent(self.__class__.no_authentication.help),
152152
),
153153
}
154+
self.flags.pop("y")
155+
156+
# Some traits in ServerProcess are not defined to be configurable, but we need that for the standalone proxy
157+
for name, trait in ServerProcess.class_own_traits().items():
158+
trait.tag(config=True)
154159

155160
# Create an Alias to all Traits defined in ServerProcess, with some
156161
# exceptions we do not need, for easier use of the CLI
@@ -164,20 +169,20 @@ def __init__(self, **kwargs):
164169
"command",
165170
]
166171
server_process_aliases = {
167-
trait: f"StandaloneProxyServer.{trait}"
172+
trait.replace("_", "-"): f"StandaloneProxyServer.{trait}"
168173
for trait in ServerProcess.class_traits(config=True)
169174
if trait not in ignore_traits and trait not in self.flags
170175
}
171176

172177
self.aliases = {
173178
**super().aliases,
174179
**server_process_aliases,
175-
"base_url": "StandaloneProxyServer.base_url",
180+
"base-url": "StandaloneProxyServer.base_url",
176181
"address": "StandaloneProxyServer.address",
177182
"port": "StandaloneProxyServer.port",
178-
"server_port": "StandaloneProxyServer.server_port",
179-
"activity_interval": "StandaloneProxyServer.activity_interval",
180-
"websocket_max_message_size": "StandaloneProxyServer.websocket_max_message_size",
183+
"server-port": "StandaloneProxyServer.server_port",
184+
"activity-interval": "StandaloneProxyServer.activity_interval",
185+
"websocket-max-message-size": "StandaloneProxyServer.websocket_max_message_size",
181186
}
182187

183188
def emit_alias_help(self):
@@ -206,7 +211,7 @@ def get_proxy_attributes(self) -> dict:
206211
attributes["proxy_base"] = "/"
207212

208213
attributes["requested_port"] = self.server_port
209-
attributes["skip_authentication"] = self.skip_authentication
214+
attributes["no_authentication"] = self.no_authentication
210215

211216
return attributes
212217

@@ -278,7 +283,7 @@ def _configure_ssl(self) -> dict | None:
278283
return ssl_options
279284

280285
def start(self):
281-
if self.skip_authentication:
286+
if self.no_authentication:
282287
self.log.warn("Disabling Authentication with JuypterHub Server!")
283288

284289
app = self.create_app()

jupyter_server_proxy/standalone/proxy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs):
3232
super().__init__(*args, **kwargs)
3333
self.environment = {}
3434
self.timeout = 60
35-
self.skip_authentication = False
35+
self.no_authentication = False
3636

3737
@property
3838
def log(self) -> Logger:
@@ -69,7 +69,7 @@ def write_error(self, status_code: int, **kwargs):
6969
return RequestHandler.write_error(self, status_code, **kwargs)
7070

7171
async def proxy(self, port, path):
72-
if self.skip_authentication:
72+
if self.no_authentication:
7373
return await super().proxy(port, path)
7474
else:
7575
return await ensure_async(self.oauth_proxy(port, path))

tests/test_standalone.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class _TestStandaloneBase(testing.AsyncHTTPTestCase):
4040
runTest = None # Required for Tornado 6.1
4141

4242
unix_socket: bool
43-
skip_authentication: bool
43+
no_authentication: bool
4444

4545
def get_app(self):
4646
command = [
@@ -55,7 +55,7 @@ def get_app(self):
5555
base_url="/some/prefix",
5656
unix_socket=self.unix_socket,
5757
timeout=60,
58-
skip_authentication=self.skip_authentication,
58+
no_authentication=self.no_authentication,
5959
log_level=logging.DEBUG,
6060
)
6161

@@ -69,7 +69,7 @@ class TestStandaloneProxyRedirect(_TestStandaloneBase):
6969
"""
7070

7171
unix_socket = False
72-
skip_authentication = True
72+
no_authentication = True
7373

7474
def test_add_slash(self):
7575
response = self.fetch("/some/prefix", follow_redirects=False)
@@ -97,7 +97,7 @@ def test_on_prefix(self):
9797
)
9898
class TestStandaloneProxyWithUnixSocket(_TestStandaloneBase):
9999
unix_socket = True
100-
skip_authentication = True
100+
no_authentication = True
101101

102102
def test_with_unix_socket(self):
103103
response = self.fetch("/some/prefix/")
@@ -115,7 +115,7 @@ class TestStandaloneProxyLogin(_TestStandaloneBase):
115115
"""
116116

117117
unix_socket = False
118-
skip_authentication = False
118+
no_authentication = False
119119

120120
def test_redirect_to_login_url(self):
121121
response = self.fetch("/some/prefix/", follow_redirects=False)

0 commit comments

Comments
 (0)