Skip to content

Commit 82eda48

Browse files
extensions_manager: add extension_web_apps interface
* Add an interface for listing extension applications that provide a default URL (i.e. extensions which provide a web application). * Add an endpoint for querying this interface. * Partially addresses #1414 by allowing Jupyter web applications to query for the existence of other Jupyter web applications.
1 parent 74655ce commit 82eda48

File tree

4 files changed

+55
-0
lines changed

4 files changed

+55
-0
lines changed

jupyter_server/base/handlers.py

+18
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,23 @@ def get(self, path: str, include_body: bool = True) -> Coroutine[Any, Any, None]
11881188
return super().get(path, include_body)
11891189

11901190

1191+
class ExtensionAppsHandler(JupyterHandler):
1192+
"""Return Jupyter Server extension web applications."""
1193+
1194+
@allow_unauthenticated
1195+
def get(self) -> None:
1196+
self.set_header("Content-Type", "application/json")
1197+
if self.serverapp:
1198+
self.finish(
1199+
json.dumps(
1200+
self.serverapp.extension_manager.extension_web_apps()
1201+
)
1202+
)
1203+
else:
1204+
# self.serverapp can be None
1205+
raise web.HTTPError(500, 'Server has not started correctly.')
1206+
1207+
11911208
# -----------------------------------------------------------------------------
11921209
# URL pattern fragments for reuse
11931210
# -----------------------------------------------------------------------------
@@ -1205,4 +1222,5 @@ def get(self, path: str, include_body: bool = True) -> Coroutine[Any, Any, None]
12051222
(r"api", APIVersionHandler),
12061223
(r"/(robots\.txt|favicon\.ico)", PublicStaticFileHandler),
12071224
(r"/metrics", PrometheusMetricsHandler),
1225+
(r"/extensions", ExtensionAppsHandler),
12081226
]

jupyter_server/extension/manager.py

+22
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import importlib
66
from itertools import starmap
7+
import re
78

89
from tornado.gen import multi
910
from traitlets import Any, Bool, Dict, HasTraits, Instance, List, Unicode, default, observe
@@ -14,6 +15,9 @@
1415
from .utils import ExtensionMetadataError, ExtensionModuleNotFound, get_loader, get_metadata
1516

1617

18+
RE_SLASH = x = re.compile(r'/+') # match any number of slashes
19+
20+
1721
class ExtensionPoint(HasTraits):
1822
"""A simple API for connecting to a Jupyter Server extension
1923
point defined by metadata and importable from a Python package.
@@ -291,6 +295,24 @@ def extension_apps(self):
291295
for name, extension in self.extensions.items()
292296
}
293297

298+
@property
299+
def extension_web_apps(self):
300+
"""Return Jupyter Server extension web applications.
301+
302+
Some Jupyter Server extensions provide web applications
303+
(e.g. Jupyter Lab), other's don't (e.g. Jupyter LSP).
304+
305+
This returns a mapping of {extension_name: web_app_endpoint} for all
306+
extensions which provide a default_url (i.e. a web application).
307+
"""
308+
return {
309+
app.name: RE_SLASH.sub('/', f'{self.serverapp.base_url}/{app.default_url}')
310+
for extension_apps in self.serverapp.extension_manager.extension_apps.values()
311+
# filter out extensions that do not provide a default_url OR
312+
# set it to the root endpoint.
313+
for app in extension_apps if getattr(app, 'default_url', '/') != '/'
314+
}
315+
294316
@property
295317
def extension_points(self):
296318
"""Return mapping of extension point names and ExtensionPoint objects."""

tests/extension/mockextensions/app.py

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class MockExtensionApp(ExtensionAppJinjaMixin, ExtensionApp):
5050
static_paths = [STATIC_PATH] # type:ignore[assignment]
5151
mock_trait = Unicode("mock trait", config=True)
5252
loaded = False
53+
default_url = '/mockextension'
5354

5455
serverapp_config = {"jpserver_extensions": {"tests.extension.mockextensions.mock1": True}}
5556

tests/extension/test_app.py

+14
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,17 @@ async def test_events(jp_serverapp, jp_fetch):
191191
stream.truncate(0)
192192
stream.seek(0)
193193
assert output["msg"] == "Hello, world!"
194+
195+
196+
async def test_extension_web_apps(jp_serverapp):
197+
jp_serverapp.extension_manager.load_all_extensions()
198+
199+
# there should be (at least) two extension applications
200+
assert set(jp_serverapp.extension_manager.extension_apps) == {
201+
'tests.extension.mockextensions', 'jupyter_server_terminals'
202+
}
203+
204+
# but only one extension web application
205+
assert jp_serverapp.extension_manager.extension_web_apps == {
206+
'mockextension': '/a%40b/mockextension'
207+
}

0 commit comments

Comments
 (0)