Skip to content

Commit d455f4e

Browse files
authored
Merge pull request #384 from kevin-bates/use-jupyter-server
Use jupyter server as base provider
2 parents 6a6a49e + 414cfe2 commit d455f4e

37 files changed

+1792
-1769
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ jobs:
3131
strategy:
3232
matrix:
3333
python:
34-
- "3.7"
3534
- "3.8"
3635
- "3.9"
36+
- "3.10"
37+
- "3.11"
3738

3839
runs-on: ubuntu-latest
3940

@@ -70,11 +71,12 @@ jobs:
7071
run: jupyter kernelgateway --help
7172

7273
- name: Run tests
73-
run: nosetests --process-restartworker --with-coverage --cover-package=kernel_gateway
74+
run: pytest -vv -W default --cov kernel_gateway --cov-branch --cov-report term-missing:skip-covered
7475
env:
7576
ASYNC_TEST_TIMEOUT: 10
7677

7778
- name: Upload coverage to Codecov
7879
uses: codecov/codecov-action@v1
7980
with:
81+
token: ${{ secrets.CODECOV_TOKEN }}
8082
fail_ci_if_error: true

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ sdist: ## Make a dist/*.tar.gz source distribution
7171
test: TEST?=
7272
test: ## Make a python3 test run
7373
ifeq ($(TEST),)
74-
$(SA) $(ENV) && nosetests
74+
$(SA) $(ENV) && pytest -vv
7575
else
76-
# e.g., make test TEST="test_gatewayapp.TestGatewayAppConfig"
77-
$(SA) $(ENV) && nosetests kernel_gateway.tests.$(TEST)
76+
# e.g., make test TEST="test_gatewayapp.py::TestGatewayAppConfig"
77+
$(SA) $(ENV) && pytest -vv kernel_gateway/tests/$(TEST)
7878
endif
7979

8080
release: POST_SDIST=register upload

codecov.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
threshold: 5%
7+
patch:
8+
default:
9+
target: 50%
10+
range: 80..100

conftest.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
import os
5+
import logging
6+
import pytest
7+
from binascii import hexlify
8+
from traitlets.config import Config
9+
from kernel_gateway.gatewayapp import KernelGatewayApp
10+
11+
pytest_plugins = ["pytest_jupyter.jupyter_core", "pytest_jupyter.jupyter_server"]
12+
13+
14+
@pytest.fixture(scope="function")
15+
def jp_configurable_serverapp(
16+
jp_nbconvert_templates, # this fixture must precede jp_environ
17+
jp_environ,
18+
jp_server_config,
19+
jp_argv,
20+
jp_http_port,
21+
jp_base_url,
22+
tmp_path,
23+
jp_root_dir,
24+
jp_logging_stream,
25+
jp_asyncio_loop,
26+
io_loop,
27+
):
28+
"""Starts a Jupyter Server instance based on
29+
the provided configuration values.
30+
The fixture is a factory; it can be called like
31+
a function inside a unit test. Here's a basic
32+
example of how use this fixture:
33+
34+
.. code-block:: python
35+
36+
def my_test(jp_configurable_serverapp):
37+
app = jp_configurable_serverapp(...)
38+
...
39+
"""
40+
KernelGatewayApp.clear_instance()
41+
42+
def _configurable_serverapp(
43+
config=jp_server_config,
44+
base_url=jp_base_url,
45+
argv=jp_argv,
46+
http_port=jp_http_port,
47+
**kwargs,
48+
):
49+
c = Config(config)
50+
51+
if "auth_token" not in c.KernelGatewayApp and not c.IdentityProvider.token:
52+
default_token = hexlify(os.urandom(4)).decode("ascii")
53+
c.IdentityProvider.token = default_token
54+
55+
app = KernelGatewayApp.instance(
56+
# Set the log level to debug for testing purposes
57+
log_level="DEBUG",
58+
port=http_port,
59+
port_retries=0,
60+
base_url=base_url,
61+
config=c,
62+
**kwargs,
63+
)
64+
app.log.propagate = True
65+
app.log.handlers = []
66+
# Initialize app without httpserver
67+
if jp_asyncio_loop.is_running():
68+
app.initialize(argv=argv, new_httpserver=False)
69+
else:
70+
71+
async def initialize_app():
72+
app.initialize(argv=argv, new_httpserver=False)
73+
74+
jp_asyncio_loop.run_until_complete(initialize_app())
75+
# Reroute all logging StreamHandlers away from stdin/stdout since pytest hijacks
76+
# these streams and closes them at unfortunate times.
77+
stream_handlers = [h for h in app.log.handlers if isinstance(h, logging.StreamHandler)]
78+
for handler in stream_handlers:
79+
handler.setStream(jp_logging_stream)
80+
app.log.propagate = True
81+
app.log.handlers = []
82+
app.start_app()
83+
return app
84+
85+
return _configurable_serverapp
86+
87+
88+
@pytest.fixture(autouse=True)
89+
def jp_server_cleanup(jp_asyncio_loop):
90+
yield
91+
app: KernelGatewayApp = KernelGatewayApp.instance()
92+
try:
93+
jp_asyncio_loop.run_until_complete(app.async_shutdown())
94+
except (RuntimeError, SystemExit) as e:
95+
print("ignoring cleanup error", e)
96+
if hasattr(app, "kernel_manager"):
97+
app.kernel_manager.context.destroy()
98+
KernelGatewayApp.clear_instance()
99+
100+
101+
@pytest.fixture
102+
def jp_auth_header(jp_serverapp):
103+
"""Configures an authorization header using the token from the serverapp fixture."""
104+
return {"Authorization": f"token {jp_serverapp.identity_provider.token}"}

docs/source/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ The Jupyter Kernel Gateway has the following features:
2424
* Generation of [Swagger specs](http://swagger.io/introducing-the-open-api-initiative/)
2525
for notebook-defined API in `notebook-http` mode
2626
* A CLI for launching the kernel gateway: `jupyter kernelgateway OPTIONS`
27-
* A Python 2.7 and 3.3+ compatible implementation
27+
* A Python 3.8+ compatible implementation

kernel_gateway/auth/identity.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
"""Gateway Identity Provider interface
4+
5+
This defines the _authentication_ layer of Jupyter Server,
6+
to be used in combination with Authorizer for _authorization_.
7+
"""
8+
from traitlets import default
9+
from tornado import web
10+
11+
from jupyter_server.auth.identity import IdentityProvider, User
12+
from jupyter_server.base.handlers import JupyterHandler
13+
14+
15+
class GatewayIdentityProvider(IdentityProvider):
16+
"""
17+
Interface for providing identity management and authentication for a Gateway server.
18+
"""
19+
20+
@default("token")
21+
def _token_default(self):
22+
return self.parent.auth_token
23+
24+
@property
25+
def auth_enabled(self):
26+
if not self.token:
27+
return False
28+
return True
29+
30+
def should_check_origin(self, handler: JupyterHandler) -> bool:
31+
"""Should the Handler check for CORS origin validation?
32+
33+
Origin check should be skipped for token-authenticated requests.
34+
35+
Returns:
36+
- True, if Handler must check for valid CORS origin.
37+
- False, if Handler should skip origin check since requests are token-authenticated.
38+
"""
39+
# Always check the origin unless operator configured gateway to allow any
40+
return handler.settings["kg_allow_origin"] != "*"
41+
42+
def generate_anonymous_user(self, handler: web.RequestHandler) -> User:
43+
"""Generate a random anonymous user.
44+
45+
For use when a single shared token is used,
46+
but does not identify a user.
47+
"""
48+
name = display_name = f"Anonymous"
49+
initials = "An"
50+
color = None
51+
return User(name.lower(), name, display_name, initials, None, color)
52+
53+
def is_token_authenticated(self, handler: web.RequestHandler) -> bool:
54+
"""The default authentication flow of Gateway is token auth.
55+
56+
The only other option is no auth
57+
"""
58+
return True

kernel_gateway/base/handlers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33
"""Tornado handlers for the base of the API."""
44

55
from tornado import web
6-
import notebook.base.handlers as notebook_handlers
6+
import jupyter_server.base.handlers as server_handlers
77
from ..mixins import TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin
88

9+
910
class APIVersionHandler(TokenAuthorizationMixin,
1011
CORSMixin,
1112
JSONErrorsMixin,
12-
notebook_handlers.APIVersionHandler):
13+
server_handlers.APIVersionHandler):
1314
"""Extends the notebook server base API handler with token auth, CORS, and
1415
JSON errors.
1516
"""
1617
pass
1718

19+
1820
class NotFoundHandler(JSONErrorsMixin, web.RequestHandler):
1921
"""Catches all requests and responds with 404 JSON messages.
2022
@@ -28,6 +30,7 @@ class NotFoundHandler(JSONErrorsMixin, web.RequestHandler):
2830
def prepare(self):
2931
raise web.HTTPError(404)
3032

33+
3134
default_handlers = [
3235
(r'/api', APIVersionHandler),
3336
(r'/(.*)', NotFoundHandler)

0 commit comments

Comments
 (0)