Skip to content

Commit

Permalink
extensions_manager: add extension_web_apps interface
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
oliver-sanders committed Nov 4, 2024
1 parent 74655ce commit 82eda48
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 0 deletions.
18 changes: 18 additions & 0 deletions jupyter_server/base/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,23 @@ def get(self, path: str, include_body: bool = True) -> Coroutine[Any, Any, None]
return super().get(path, include_body)


class ExtensionAppsHandler(JupyterHandler):
"""Return Jupyter Server extension web applications."""

@allow_unauthenticated
def get(self) -> None:
self.set_header("Content-Type", "application/json")
if self.serverapp:
self.finish(
json.dumps(
self.serverapp.extension_manager.extension_web_apps()
)
)
else:
# self.serverapp can be None
raise web.HTTPError(500, 'Server has not started correctly.')


# -----------------------------------------------------------------------------
# URL pattern fragments for reuse
# -----------------------------------------------------------------------------
Expand All @@ -1205,4 +1222,5 @@ def get(self, path: str, include_body: bool = True) -> Coroutine[Any, Any, None]
(r"api", APIVersionHandler),
(r"/(robots\.txt|favicon\.ico)", PublicStaticFileHandler),
(r"/metrics", PrometheusMetricsHandler),
(r"/extensions", ExtensionAppsHandler),
]
22 changes: 22 additions & 0 deletions jupyter_server/extension/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import importlib
from itertools import starmap
import re

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


RE_SLASH = x = re.compile(r'/+') # match any number of slashes


class ExtensionPoint(HasTraits):
"""A simple API for connecting to a Jupyter Server extension
point defined by metadata and importable from a Python package.
Expand Down Expand Up @@ -291,6 +295,24 @@ def extension_apps(self):
for name, extension in self.extensions.items()
}

@property
def extension_web_apps(self):
"""Return Jupyter Server extension web applications.
Some Jupyter Server extensions provide web applications
(e.g. Jupyter Lab), other's don't (e.g. Jupyter LSP).
This returns a mapping of {extension_name: web_app_endpoint} for all
extensions which provide a default_url (i.e. a web application).
"""
return {
app.name: RE_SLASH.sub('/', f'{self.serverapp.base_url}/{app.default_url}')
for extension_apps in self.serverapp.extension_manager.extension_apps.values()
# filter out extensions that do not provide a default_url OR
# set it to the root endpoint.
for app in extension_apps if getattr(app, 'default_url', '/') != '/'
}

@property
def extension_points(self):
"""Return mapping of extension point names and ExtensionPoint objects."""
Expand Down
1 change: 1 addition & 0 deletions tests/extension/mockextensions/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class MockExtensionApp(ExtensionAppJinjaMixin, ExtensionApp):
static_paths = [STATIC_PATH] # type:ignore[assignment]
mock_trait = Unicode("mock trait", config=True)
loaded = False
default_url = '/mockextension'

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

Expand Down
14 changes: 14 additions & 0 deletions tests/extension/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,17 @@ async def test_events(jp_serverapp, jp_fetch):
stream.truncate(0)
stream.seek(0)
assert output["msg"] == "Hello, world!"


async def test_extension_web_apps(jp_serverapp):
jp_serverapp.extension_manager.load_all_extensions()

# there should be (at least) two extension applications
assert set(jp_serverapp.extension_manager.extension_apps) == {
'tests.extension.mockextensions', 'jupyter_server_terminals'
}

# but only one extension web application
assert jp_serverapp.extension_manager.extension_web_apps == {
'mockextension': '/a%40b/mockextension'
}

0 comments on commit 82eda48

Please sign in to comment.