From 92edb7756e411f7e8295013b5da6a4255e89ac75 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 22 Jan 2024 10:34:49 -0600 Subject: [PATCH] Apply auto-formatters (#391) * Apply auto-formatters * Apply auto-formatters * revert pre-commit changes * Revert "revert pre-commit changes" This reverts commit fad4b172c56eefe0930e829b3f52b08687431c7e. * Revert "Apply auto-formatters" This reverts commit 32b06a64bb5d6046ca6de97d223bb6c47ec5413b. * try again * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 43 +- CHANGELOG.md | 208 +++++----- LICENSE.md | 6 +- README.md | 14 +- conftest.py | 4 +- docs/source/conf.py | 212 +++++----- docs/source/config-options.md | 4 +- docs/source/devinstall.md | 10 +- docs/source/features.md | 30 +- docs/source/getting-started.md | 11 + docs/source/http-mode.md | 18 +- docs/source/plug-in.md | 18 +- docs/source/summary-changes.md | 202 ++++----- docs/source/uses.md | 10 +- docs/source/websocket-mode.md | 2 +- etc/api_examples/api_intro.ipynb | 154 +++---- etc/api_examples/endpoint_ordering.ipynb | 14 +- .../setting_response_metadata.ipynb | 85 ++-- kernel_gateway/__main__.py | 3 +- kernel_gateway/auth/identity.py | 2 +- kernel_gateway/base/handlers.py | 14 +- kernel_gateway/gatewayapp.py | 383 ++++++++++-------- kernel_gateway/jupyter_websocket/__init__.py | 42 +- kernel_gateway/jupyter_websocket/handlers.py | 19 +- kernel_gateway/jupyter_websocket/swagger.json | 92 +---- kernel_gateway/jupyter_websocket/swagger.yaml | 58 +-- kernel_gateway/mixins.py | 53 +-- kernel_gateway/notebook_http/__init__.py | 143 ++++--- kernel_gateway/notebook_http/cell/parser.py | 37 +- kernel_gateway/notebook_http/errors.py | 2 + kernel_gateway/notebook_http/handlers.py | 98 +++-- .../notebook_http/swagger/builders.py | 13 +- .../notebook_http/swagger/handlers.py | 14 +- .../notebook_http/swagger/parser.py | 170 ++++---- kernel_gateway/services/kernels/handlers.py | 53 ++- kernel_gateway/services/kernels/manager.py | 16 +- kernel_gateway/services/kernels/pool.py | 9 +- kernel_gateway/services/sessions/handlers.py | 12 +- .../services/sessions/sessionmanager.py | 41 +- .../tests/notebook_http/cell/test_parser.py | 18 +- .../notebook_http/swagger/test_builders.py | 30 +- .../notebook_http/swagger/test_parser.py | 147 +++++-- .../tests/notebook_http/test_request_utils.py | 167 +++++--- .../tests/resources/public/index.html | 12 +- .../tests/resources/responses.ipynb | 24 +- .../tests/resources/simple_api.ipynb | 4 +- .../resources/weirdly%20named#notebook.ipynb | 4 +- kernel_gateway/tests/test_gatewayapp.py | 2 +- .../tests/test_jupyter_websocket.py | 351 +++++++++------- kernel_gateway/tests/test_mixins.py | 5 + kernel_gateway/tests/test_notebook_http.py | 134 +++--- readthedocs.yml | 5 +- setup.py | 1 + 53 files changed, 1763 insertions(+), 1460 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a70f2e8..c8209b4 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,33 +13,32 @@ jobs: strategy: matrix: python: - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Checkout - uses: actions/checkout@v4 + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install dependencies + run: | + pip install -e . + pip freeze - - name: Install dependencies - run: | - pip install -e . - pip freeze + - name: Show help + run: jupyter kernelgateway --help - - name: Show help - run: jupyter kernelgateway --help + - name: Run tests + run: hatch run cov:test + env: + ASYNC_TEST_TIMEOUT: 10 - - name: Run tests - run: hatch run cov:test - env: - ASYNC_TEST_TIMEOUT: 10 - - - name: Build docs - run: hatch run docs:build + - name: Build docs + run: hatch run docs:build diff --git a/CHANGELOG.md b/CHANGELOG.md index 231d513..a66b04b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,185 +21,185 @@ ## 2.4.3 (2020-08-18) -* [PR-340](https://github.com/jupyter/kernel_gateway/pull/340) enable ssl_version as a JKG config option +- [PR-340](https://github.com/jupyter/kernel_gateway/pull/340) enable ssl_version as a JKG config option ## 2.4.2 (2020-08-10) -* [PR-338](https://github.com/jupyter/kernel_gateway/pull/338) Use appropriate maybe-future to handle asyncio futures +- [PR-338](https://github.com/jupyter/kernel_gateway/pull/338) Use appropriate maybe-future to handle asyncio futures ## 2.4.1 (2020-06-05) -* [PR-327](https://github.com/jupyter/kernel_gateway/pull/327) Use ==/!= to compare str, bytes, and int literals -* [PR-325](https://github.com/jupyter/kernel_gateway/pull/325) fix: module 'signal' has no attribute 'SIGHUP' on Windows +- [PR-327](https://github.com/jupyter/kernel_gateway/pull/327) Use ==/!= to compare str, bytes, and int literals +- [PR-325](https://github.com/jupyter/kernel_gateway/pull/325) fix: module 'signal' has no attribute 'SIGHUP' on Windows ## 2.4.0 (2019-08-11) -* [PR-323](https://github.com/jupyter/kernel_gateway/pull/323): Update handler not use deprecated maybe_future call -* [PR-322](https://github.com/jupyter/kernel_gateway/pull/322): Update handler compatibility with tornado/pyzmq updates -* [PR-321](https://github.com/jupyter/kernel_gateway/pull/321): Allow Notebook 6.x dependencies -* [PR-317](https://github.com/jupyter/kernel_gateway/pull/317): Better error toleration during server initialization +- [PR-323](https://github.com/jupyter/kernel_gateway/pull/323): Update handler not use deprecated maybe_future call +- [PR-322](https://github.com/jupyter/kernel_gateway/pull/322): Update handler compatibility with tornado/pyzmq updates +- [PR-321](https://github.com/jupyter/kernel_gateway/pull/321): Allow Notebook 6.x dependencies +- [PR-317](https://github.com/jupyter/kernel_gateway/pull/317): Better error toleration during server initialization ## 2.3.0 (2019-03-15) -* [PR-315](https://github.com/jupyter/kernel_gateway/pull/315): Call tornado StaticFileHandler.get() as a coroutine +- [PR-315](https://github.com/jupyter/kernel_gateway/pull/315): Call tornado StaticFileHandler.get() as a coroutine ## 2.2.0 (2019-02-26) -* [PR-314](https://github.com/jupyter/kernel_gateway/pull/314): Support serving kernelspec resources -* [PR-307](https://github.com/jupyter/kernel_gateway/pull/307): features.md: Fix a link typo -* [PR-304](https://github.com/jupyter/kernel_gateway/pull/304): Add ability for Kernel Gateway to ignore SIGHUP signal -* [PR-303](https://github.com/jupyter/kernel_gateway/pull/303): Fixed the link to section +- [PR-314](https://github.com/jupyter/kernel_gateway/pull/314): Support serving kernelspec resources +- [PR-307](https://github.com/jupyter/kernel_gateway/pull/307): features.md: Fix a link typo +- [PR-304](https://github.com/jupyter/kernel_gateway/pull/304): Add ability for Kernel Gateway to ignore SIGHUP signal +- [PR-303](https://github.com/jupyter/kernel_gateway/pull/303): Fixed the link to section ## 2.1.0 (2018-08-13) -* [PR-299](https://github.com/jupyter/kernel_gateway/pull/299): adds x_header configuration option for use behind proxies -* [PR-294](https://github.com/jupyter/kernel_gateway/pull/294): Allow access from remote hosts (Notebook 5.6) -* [PR-292](https://github.com/jupyter/kernel_gateway/pull/292): Update dependencies of Jupyter components -* [PR-290](https://github.com/jupyter/kernel_gateway/pull/290): Include LICENSE file in wheels -* [PR-285](https://github.com/jupyter/kernel_gateway/pull/285): Update Kernel Gateway test base class to be compatible with Tornado 5.0 -* [PR-284](https://github.com/jupyter/kernel_gateway/pull/284): Add reason argument to set_status() so that custom messages flow back to client -* [PR-280](https://github.com/jupyter/kernel_gateway/pull/280): Add whitelist of environment variables to be inherited from gateway process by kernel -* [PR-275](https://github.com/jupyter/kernel_gateway/pull/275): Fix broken links to notebook-http mode page in docs -* [PR-272](https://github.com/jupyter/kernel_gateway/pull/272): Fix bug when getting kernel language in notebook-http mode -* [PR-271](https://github.com/jupyter/kernel_gateway/pull/271): Fix IPerl notebooks running in notebook-http mode +- [PR-299](https://github.com/jupyter/kernel_gateway/pull/299): adds x_header configuration option for use behind proxies +- [PR-294](https://github.com/jupyter/kernel_gateway/pull/294): Allow access from remote hosts (Notebook 5.6) +- [PR-292](https://github.com/jupyter/kernel_gateway/pull/292): Update dependencies of Jupyter components +- [PR-290](https://github.com/jupyter/kernel_gateway/pull/290): Include LICENSE file in wheels +- [PR-285](https://github.com/jupyter/kernel_gateway/pull/285): Update Kernel Gateway test base class to be compatible with Tornado 5.0 +- [PR-284](https://github.com/jupyter/kernel_gateway/pull/284): Add reason argument to set_status() so that custom messages flow back to client +- [PR-280](https://github.com/jupyter/kernel_gateway/pull/280): Add whitelist of environment variables to be inherited from gateway process by kernel +- [PR-275](https://github.com/jupyter/kernel_gateway/pull/275): Fix broken links to notebook-http mode page in docs +- [PR-272](https://github.com/jupyter/kernel_gateway/pull/272): Fix bug when getting kernel language in notebook-http mode +- [PR-271](https://github.com/jupyter/kernel_gateway/pull/271): Fix IPerl notebooks running in notebook-http mode ## 2.0.2 (2017-11-10) -* [PR-266](https://github.com/jupyter/kernel_gateway/pull/266): Make KernelManager and KernelSpecManager configurable -* [PR-263](https://github.com/jupyter/kernel_gateway/pull/263): Correct JSONErrorsMixin for compatibility with notebook 5.2.0 +- [PR-266](https://github.com/jupyter/kernel_gateway/pull/266): Make KernelManager and KernelSpecManager configurable +- [PR-263](https://github.com/jupyter/kernel_gateway/pull/263): Correct JSONErrorsMixin for compatibility with notebook 5.2.0 ## 2.0.1 (2017-09-09) -* [PR-258](https://github.com/jupyter/kernel_gateway/pull/258): Remove auth token check for OPTIONS requests (CORS) +- [PR-258](https://github.com/jupyter/kernel_gateway/pull/258): Remove auth token check for OPTIONS requests (CORS) ## 2.0.0 (2017-05-30) -* Update compatibility to notebook>=5.0 -* Remove kernel activity API in favor of the one in the notebook package -* Update project overview in the documentation -* Inherit the server `PATH` when launching a new kernel via POST request +- Update compatibility to notebook>=5.0 +- Remove kernel activity API in favor of the one in the notebook package +- Update project overview in the documentation +- Inherit the server `PATH` when launching a new kernel via POST request with custom environment variables -* Fix kernel cleanup upon SIGTERM -* Fix security requirements in the swagger spec -* Fix configured headers for OPTIONS requests +- Fix kernel cleanup upon SIGTERM +- Fix security requirements in the swagger spec +- Fix configured headers for OPTIONS requests ## 1.2.2 (2017-05-30) -* Inherit the server `PATH` when launching a new kernel via POST request +- Inherit the server `PATH` when launching a new kernel via POST request with custom environment variables -* Fix kernel cleanup upon SIGTERM +- Fix kernel cleanup upon SIGTERM ## 1.2.1 (2017-04-01) -* Add support for auth token as a query parameter +- Add support for auth token as a query parameter ## 1.2.0 (2017-02-12) -* Add command line option to whitelist environment variables for `POST /api/kernels` -* Add support for HTTPS key and certificate files -* Improve the flow and explanations in the `api_intro` notebook -* Fix incorrect use of `metadata.kernelspec.name` as a language name instead of +- Add command line option to whitelist environment variables for `POST /api/kernels` +- Add support for HTTPS key and certificate files +- Improve the flow and explanations in the `api_intro` notebook +- Fix incorrect use of `metadata.kernelspec.name` as a language name instead of `metadata.language.info` -* Fix lingering kernel regression after Ctrl-C interrupt -* Switch to a conda-based dev setup from docker +- Fix lingering kernel regression after Ctrl-C interrupt +- Switch to a conda-based dev setup from docker ## 1.1.2 (2016-12-16) -* Fix compatibility with Notebook 4.3 session handler `create_session` call +- Fix compatibility with Notebook 4.3 session handler `create_session` call ## 1.1.1 (2016-09-10) -* Add LICENSE file to package distributions +- Add LICENSE file to package distributions ## 1.1.0 (2016-09-08) -* Add an option to force a specific kernel spec for all requests and seed notebooks -* Add support for specifying notebook-http APIs using full Swagger specs -* Add option to serve static web assets from Tornado in notebook-http mode -* Add command line aliases for common options (e.g., `--ip`) -* Fix Tornado 4.4 compatbility: sending an empty body string with a 204 response +- Add an option to force a specific kernel spec for all requests and seed notebooks +- Add support for specifying notebook-http APIs using full Swagger specs +- Add option to serve static web assets from Tornado in notebook-http mode +- Add command line aliases for common options (e.g., `--ip`) +- Fix Tornado 4.4 compatbility: sending an empty body string with a 204 response ## 1.0.0 (2016-07-15) -* Introduce an [API for developing mode plug-ins](https://jupyter-kernel-gateway.readthedocs.io/en/latest/plug-in.html) -* Separate `jupyter-websocket` and `notebook-http` modes into plug-in packages -* Move mode specific command line options into their respective packages (see `--help-all`) -* Report times with respect to UTC in `/_api/activity` responses +- Introduce an [API for developing mode plug-ins](https://jupyter-kernel-gateway.readthedocs.io/en/latest/plug-in.html) +- Separate `jupyter-websocket` and `notebook-http` modes into plug-in packages +- Move mode specific command line options into their respective packages (see `--help-all`) +- Report times with respect to UTC in `/_api/activity` responses ## 0.6.0 (2016-06-17) -* Switch HTTP status from 402 for 403 when server reaches the max kernel limit -* Explicitly shutdown kernels when the server shuts down -* Remove `KG_AUTH_TOKEN` from the environment of kernels -* Fix missing swagger document in release -* Add `--KernelGateway.port_retries` option like in Jupyter Notebook -* Fix compatibility with Notebook 4.2 session handler `create_session` call +- Switch HTTP status from 402 for 403 when server reaches the max kernel limit +- Explicitly shutdown kernels when the server shuts down +- Remove `KG_AUTH_TOKEN` from the environment of kernels +- Fix missing swagger document in release +- Add `--KernelGateway.port_retries` option like in Jupyter Notebook +- Fix compatibility with Notebook 4.2 session handler `create_session` call ## 0.5.1 (2016-04-20) -* Backport `--KernelGateway.port_retries` option like in Jupyter Notebook -* Fix compatibility with Notebook 4.2 session handler `create_session` call +- Backport `--KernelGateway.port_retries` option like in Jupyter Notebook +- Fix compatibility with Notebook 4.2 session handler `create_session` call ## 0.5.0 (2016-04-04) -* Support multiple cells per path in `notebook-http` mode -* Add a Swagger specification of the `jupyter-websocket` API -* Add `KERNEL_GATEWAY=1` to all kernel environments -* Support environment variables in `POST /api/kernels` -* numpydoc format docstrings on everything -* Convert README to Sphinx/ReadTheDocs site -* Convert `ActivityManager` to a traitlets `LoggingConfigurable` -* Fix `base_url` handling for all paths -* Fix unbounded growth of ignored kernels in `ActivityManager` -* Fix caching of Swagger spec in `notebook-http` mode -* Fix failure to install due to whitespace in `setup.py` version numbers -* Fix call to kernel manager base class when starting a kernel -* Fix test fixture hangs +- Support multiple cells per path in `notebook-http` mode +- Add a Swagger specification of the `jupyter-websocket` API +- Add `KERNEL_GATEWAY=1` to all kernel environments +- Support environment variables in `POST /api/kernels` +- numpydoc format docstrings on everything +- Convert README to Sphinx/ReadTheDocs site +- Convert `ActivityManager` to a traitlets `LoggingConfigurable` +- Fix `base_url` handling for all paths +- Fix unbounded growth of ignored kernels in `ActivityManager` +- Fix caching of Swagger spec in `notebook-http` mode +- Fix failure to install due to whitespace in `setup.py` version numbers +- Fix call to kernel manager base class when starting a kernel +- Fix test fixture hangs ## 0.4.1 (2016-04-20) -* Backport `--KernelGateway.port_retries` option like in Jupyter Notebook -* Fix compatibility with Notebook 4.2 session handler `create_session` call +- Backport `--KernelGateway.port_retries` option like in Jupyter Notebook +- Fix compatibility with Notebook 4.2 session handler `create_session` call ## 0.4.0 (2016-02-17) -* Enable `/_api/activity` resource with stats about kernels in `jupyter-websocket` mode -* Enable `/api/sessions` resource with in-memory name-to-kernel mapping for non-notebook clients that want to look-up kernels by associated session name -* Fix prespawn kernel logic regression for `jupyter-websocket` mode -* Fix all handlers so that they return application/json responses on error -* Fix missing output from cells that emit display data in `notebook-http` mode +- Enable `/_api/activity` resource with stats about kernels in `jupyter-websocket` mode +- Enable `/api/sessions` resource with in-memory name-to-kernel mapping for non-notebook clients that want to look-up kernels by associated session name +- Fix prespawn kernel logic regression for `jupyter-websocket` mode +- Fix all handlers so that they return application/json responses on error +- Fix missing output from cells that emit display data in `notebook-http` mode ## 0.3.1 (2016-01-25) -* Fix CORS and auth token headers for `/_api/spec/swagger.json` resource -* Fix `allow_origin` handling for non-browser clients -* Ensure base path is prefixed with a forward slash -* Filter stderr from all responses in `notebook-http` mode -* Set Tornado logging level and Jupyter logging level together with `--log-level` +- Fix CORS and auth token headers for `/_api/spec/swagger.json` resource +- Fix `allow_origin` handling for non-browser clients +- Ensure base path is prefixed with a forward slash +- Filter stderr from all responses in `notebook-http` mode +- Set Tornado logging level and Jupyter logging level together with `--log-level` ## 0.3.0 (2016-01-15) -* Support setting of status and headers in `notebook-http` mode -* Support automatic, minimal Swagger doc generation in `notebook-http` mode -* Support download of a notebook in `notebook-http` mode -* Support CORS and token auth in `notebook-http` mode -* Expose HTTP request headers in `notebook-http` mode -* Support multipart form encoding in `notebook-http` mode -* Fix request value JSON encoding when passing requests to kernels -* Fix kernel name handling when pre-spawning -* Fix lack of access logs in `notebook-http` mode +- Support setting of status and headers in `notebook-http` mode +- Support automatic, minimal Swagger doc generation in `notebook-http` mode +- Support download of a notebook in `notebook-http` mode +- Support CORS and token auth in `notebook-http` mode +- Expose HTTP request headers in `notebook-http` mode +- Support multipart form encoding in `notebook-http` mode +- Fix request value JSON encoding when passing requests to kernels +- Fix kernel name handling when pre-spawning +- Fix lack of access logs in `notebook-http` mode ## 0.2.0 (2015-12-15) -* Support notebook-defined HTTP APIs on a pool of kernels -* Disable kernel instance list by default +- Support notebook-defined HTTP APIs on a pool of kernels +- Disable kernel instance list by default ## 0.1.0 (2015-11-18) -* Support Jupyter Notebook kernel CRUD APIs and Jupyter kernel protocol over Websockets -* Support shared token auth -* Support CORS headers -* Support base URL -* Support seeding kernels code from a notebook at a file path or URL -* Support default kernel, kernel pre-spawning, and kernel count limit -* First PyPI release +- Support Jupyter Notebook kernel CRUD APIs and Jupyter kernel protocol over Websockets +- Support shared token auth +- Support CORS headers +- Support base URL +- Support seeding kernels code from a notebook at a file path or URL +- Support default kernel, kernel pre-spawning, and kernel count limit +- First PyPI release diff --git a/LICENSE.md b/LICENSE.md index 333a1d7..158172c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -57,5 +57,7 @@ change to one of the Jupyter repositories. With this in mind, the following banner should be used in any source code file to indicate the copyright and license terms: - # Copyright (c) Jupyter Development Team. - # Distributed under the terms of the Modified BSD License. +``` +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +``` diff --git a/README.md b/README.md index bc3a7cd..33b53e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Jupyter Kernel Gateway -[![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) +[![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) [![PyPI version](https://badge.fury.io/py/jupyter_kernel_gateway.svg)](https://badge.fury.io/py/jupyter_kernel_gateway) [![Build Status](https://github.com/jupyter/kernel_gateway/workflows/Tests/badge.svg)](https://github.com/jupyter/kernel_gateway/actions?query=workflow%3ATests) [![Documentation Status](http://readthedocs.org/projects/jupyter-kernel-gateway/badge/?version=latest)](https://jupyter-kernel-gateway.readthedocs.io/en/latest/?badge=latest) @@ -14,13 +14,13 @@ There are no provisions for editing notebooks through the Kernel Gateway. The following operation modes, called personalities, are supported out of the box: -* Send code snippets for execution using the +- Send code snippets for execution using the [Jupyter kernel protocol](https://jupyter-client.readthedocs.io/en/latest/messaging.html) over Websockets. Start and stop kernels through REST calls. This HTTP API is compatible with the respective API sections of the Jupyter Notebook server. -* Serve HTTP requests from annotated notebook cells. The code snippets +- Serve HTTP requests from annotated notebook cells. The code snippets are cells of a static notebook configured in the Kernel Gateway. Annotations define which HTTP verbs and resources it supports. Incoming requests are served by executing one of the cells in a kernel. @@ -31,13 +31,13 @@ It can be containerized and scaled out using common technologies like [tmpnb](ht ### Example Uses of Kernel Gateway -* Attach a local Jupyter Notebook server to a compute cluster in the cloud running near big data (e.g., interactive gateway to Spark) -* Enable a new breed of non-notebook web clients to provision and use kernels (e.g., web dashboards using [jupyter-js-services](https://github.com/jupyter/jupyter-js-services)) -* Create microservices from notebooks using the Kernel Gateway [`notebook-http` mode](https://jupyter-kernel-gateway.readthedocs.io/en/latest/http-mode.html) +- Attach a local Jupyter Notebook server to a compute cluster in the cloud running near big data (e.g., interactive gateway to Spark) +- Enable a new breed of non-notebook web clients to provision and use kernels (e.g., web dashboards using [jupyter-js-services](https://github.com/jupyter/jupyter-js-services)) +- Create microservices from notebooks using the Kernel Gateway [`notebook-http` mode](https://jupyter-kernel-gateway.readthedocs.io/en/latest/http-mode.html) ### Features -See the [Features page](https://jupyter-kernel-gateway.readthedocs.io/en/latest/features.html) in the +See the [Features page](https://jupyter-kernel-gateway.readthedocs.io/en/latest/features.html) in the documentation for a list of the Jupyter Kernel Gateway features. ## Installation diff --git a/conftest.py b/conftest.py index 706c30a..5ea6748 100644 --- a/conftest.py +++ b/conftest.py @@ -34,8 +34,8 @@ def jp_configurable_serverapp( .. code-block:: python def my_test(jp_configurable_serverapp): - app = jp_configurable_serverapp(...) - ... + app = jp_configurable_serverapp(...) + ... """ KernelGatewayApp.clear_instance() diff --git a/docs/source/conf.py b/docs/source/conf.py index 1994193..ae8d95e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,64 +9,62 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os -import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.3' +needs_sphinx = "1.3" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", "sphinx.ext.extlinks", "sphinx.ext.viewcode", - "myst_parser" + "myst_parser", ] myst_enable_extensions = ["attrs_block", "attrs_inline"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Jupyter Kernel Gateway' -copyright = u'2016, Project Jupyter Team' -author = u'Project Jupyter Team' +project = "Jupyter Kernel Gateway" +copyright = "2016, Project Jupyter Team" +author = "Project Jupyter Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -_version_py = '../../kernel_gateway/_version.py' +_version_py = "../../kernel_gateway/_version.py" version_ns = {} -exec(compile(open(_version_py).read(), _version_py, 'exec'), version_ns) +exec(compile(open(_version_py).read(), _version_py, "exec"), version_ns) # The short X.Y version. -version = '%i.%i' % version_ns['version_info'][:2] +version = "%i.%i" % version_ns["version_info"][:2] # The full version, including alpha/beta/rc tags. -release = version_ns['__version__'] +release = version_ns["__version__"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -77,9 +75,9 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -87,27 +85,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -117,31 +115,31 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -151,122 +149,121 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'KernelGatewaydoc' +htmlhelp_basename = "KernelGatewaydoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'KernelGateway.tex', u'Kernel Gateway Documentation', - u'Project Jupyter team', 'manual'), + ( + master_doc, + "KernelGateway.tex", + "Kernel Gateway Documentation", + "Project Jupyter team", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'kernel_gateway', u'Kernel Gateway Documentation', - [author], 1) -] +man_pages = [(master_doc, "kernel_gateway", "Kernel Gateway Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -275,22 +272,28 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'kernel_gateway', u'Kernel Gateway Documentation', - author, 'kernel_gateway', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "kernel_gateway", + "Kernel Gateway Documentation", + author, + "kernel_gateway", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- @@ -302,77 +305,78 @@ epub_copyright = copyright # The basename for the epub file. It defaults to the project name. -#epub_basename = project +# epub_basename = project # The HTML theme for the epub output. Since the default themes are not optimized # for small screen space, using the same theme for HTML and epub output is # usually not wise. This defaults to 'epub', a theme designed to save visual # space. -#epub_theme = 'epub' +# epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # A tuple containing the cover image and cover page html template filenames. -#epub_cover = () +# epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () +# epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True # Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' +# epub_tocscope = 'default' # Fix unsupported image types using the Pillow. -#epub_fix_images = False +# epub_fix_images = False # Scale large images. -#epub_max_image_width = 0 +# epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' +# epub_show_urls = 'inline' # If false, no index is generated. -#epub_use_index = True +# epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} # Read The Docs # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' + + html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it diff --git a/docs/source/config-options.md b/docs/source/config-options.md index 95a2ea8..a3ba92c 100644 --- a/docs/source/config-options.md +++ b/docs/source/config-options.md @@ -3,8 +3,8 @@ The kernel gateway adheres to the [Jupyter common configuration approach](https://jupyter.readthedocs.io/en/latest/use/config.html). You can configure an instance of the kernel gateway using: 1. A configuration file -2. Command line parameters -3. Environment variables +1. Command line parameters +1. Environment variables To generate a template configuration file, run the following: diff --git a/docs/source/devinstall.md b/docs/source/devinstall.md index 43bbd9d..1a5b503 100644 --- a/docs/source/devinstall.md +++ b/docs/source/devinstall.md @@ -69,8 +69,8 @@ make docs After modifying any of the APIs in `jupyter-websocket` mode, you must update the project's Swagger API specification. 1. Load the current -[swagger.yaml](https://github.com/jupyter/kernel_gateway/blob/master/kernel_gateway/jupyter_websocket/swagger.yaml) file into the [Swagger editor](http://editor.swagger.io/#/). -2. Make your changes. -3. Export both the `swagger.json` and `swagger.yaml` files. -4. Place the files in `kernel_gateway/jupyter_websocket`. -5. Add, commit, and PR the changes. + [swagger.yaml](https://github.com/jupyter/kernel_gateway/blob/master/kernel_gateway/jupyter_websocket/swagger.yaml) file into the [Swagger editor](http://editor.swagger.io/#/). +1. Make your changes. +1. Export both the `swagger.json` and `swagger.yaml` files. +1. Place the files in `kernel_gateway/jupyter_websocket`. +1. Add, commit, and PR the changes. diff --git a/docs/source/features.md b/docs/source/features.md index cc7feab..6f58e54 100644 --- a/docs/source/features.md +++ b/docs/source/features.md @@ -2,26 +2,26 @@ The Jupyter Kernel Gateway has the following features: -* [`jupyter-websocket` mode](websocket-mode.md) which provides a +- [`jupyter-websocket` mode](websocket-mode.md) which provides a Jupyter Notebook server-compatible API for requesting kernels and communicating with them using Websockets -* [`notebook-http` mode](http-mode.md) which maps HTTP requests to +- [`notebook-http` mode](http-mode.md) which maps HTTP requests to cells in annotated notebooks -* Option to enable other kernel communication mechanisms by plugging in third party personalities -* Option to set a shared authentication token and require it from clients -* Options to set CORS headers for servicing browser-based clients -* Option to set a custom base URL (e.g., for running under tmpnb) -* Option to limit the number kernel instances a gateway server will launch +- Option to enable other kernel communication mechanisms by plugging in third party personalities +- Option to set a shared authentication token and require it from clients +- Options to set CORS headers for servicing browser-based clients +- Option to set a custom base URL (e.g., for running under tmpnb) +- Option to limit the number kernel instances a gateway server will launch (e.g., to force scaling at the container level) -* Option to pre-spawn a set number of kernel instances -* Option to set a default kernel language to use when one is not specified +- Option to pre-spawn a set number of kernel instances +- Option to set a default kernel language to use when one is not specified in the request -* Option to pre-populate kernel memory from a notebook -* Option to serve annotated notebooks as HTTP endpoints, see +- Option to pre-populate kernel memory from a notebook +- Option to serve annotated notebooks as HTTP endpoints, see [notebook-http](http-mode.md) -* Option to allow downloading of the notebook source when running +- Option to allow downloading of the notebook source when running in `notebook-http` mode -* Generation of [Swagger specs](http://swagger.io/introducing-the-open-api-initiative/) +- Generation of [Swagger specs](http://swagger.io/introducing-the-open-api-initiative/) for notebook-defined API in `notebook-http` mode -* A CLI for launching the kernel gateway: `jupyter kernelgateway OPTIONS` -* A Python 3.8+ compatible implementation +- A CLI for launching the kernel gateway: `jupyter kernelgateway OPTIONS` +- A Python 3.8+ compatible implementation diff --git a/docs/source/getting-started.md b/docs/source/getting-started.md index 4ffc417..6bd90a7 100644 --- a/docs/source/getting-started.md +++ b/docs/source/getting-started.md @@ -4,6 +4,7 @@ This document describes some of the basics of installing and running the Jupyter Kernel Gateway. ## Install + ### Using pip We make stable releases of the kernel gateway to PyPI. You can use `pip` to install the latest version along with its dependencies. @@ -31,6 +32,7 @@ jupyter kernelgateway ``` For example, if we define an endpoint in a notebook `./my_example.ipynb` as follows: + ```python # GET /hello/world @@ -42,7 +44,9 @@ req = json.loads(REQUEST) res = dict(data=np.random.randn(5, 4).tolist(), request=req) print(json.dumps(res)) ``` + and then run the gateway in [http-mode](http-mode.md) and point specifically at that notebook, we should see some information printed in the logs: + ```bash jupyter kernelgateway --KernelGatewayApp.api=kernel_gateway.notebook_http --KernelGatewayApp.seed_uri=./my_example.ipynb --port=10100 [KernelGatewayApp] Kernel started: 12ac2daa-c62a-47e4-964a-336734557656 @@ -50,23 +54,30 @@ jupyter kernelgateway --KernelGatewayApp.api=kernel_gateway.notebook_http --Kern [KernelGatewayApp] Registering resource: /_api/spec/swagger.json, methods: (GET) [KernelGatewayApp] Jupyter Kernel Gateway at http://127.0.0.1:10100 ``` + We can curl against these endpoints to demonstrate it is working: + ```bash curl http://127.0.0.1:10100/hello/world {"data": [[0.25854873480479607, -0.7997878409880017, 1.1136688704814672, -1.3292395513862103], [1.9879386172897555, 0.43368279132553395, -0.8623363198491706, -0.1571285171759644], [0.4437134294167942, 1.1323758620715763, 1.7350545168735723, -0.7617257690860397], [-0.4219717996309759, 0.2912776236488964, -0.21468140988270742, -0.8286216351049279], [0.5754812112421828, -2.042429681534432, 2.992678912690803, -0.7231031350239057]], "request": {"body": "", "args": {}, "path": {}, "headers": {"Host": "127.0.0.1:10100", "User-Agent": "curl/7.68.0", "Accept": "*/*"}}} ``` + and the swagger spec: + ```bash curl http://127.0.0.1:10100/_api/spec/swagger.json {"swagger": "2.0", "paths": {"/hello/world": {"get": {"responses": {"200": {"description": "Success"}}}}}, "info": {"version": "0.0.0", "title": "my_example"}} ``` You can also run in the default [websocket-mode](websocket-mode.md): + ```bash jupyter kernelgateway --KernelGatewayApp.api=kernel_gateway.jupyter_websocket --port=10100 [KernelGatewayApp] Jupyter Kernel Gateway at http://127.0.0.1:10100 ``` + and again notice the output in the logs. This time we didn't point to a specific notebook but you can test against the kernelspecs endpoint or the swagger endpoint: + ```bash curl http://127.0.0.1:10100/api/kernelspecs {"default": "python3", "kernelspecs": {"python38364bit38conda21f48c44b19044fba5c7aa244072a647": {"name": "python38364bit38conda21f48c44b19044fba5c7aa244072a647", ... diff --git a/docs/source/http-mode.md b/docs/source/http-mode.md index 67b3203..cc0e8ef 100644 --- a/docs/source/http-mode.md +++ b/docs/source/http-mode.md @@ -41,10 +41,10 @@ You may specify path parameters when registering an endpoint by prepending a `:` The `REQUEST` object currently contains the following properties: -* `body` - The value of the body, see the [Body And Content Type](#request-content-type-and-request-body-processing) section below -* `args` - An object with keys representing query parameter names and their associated values. A query parameter name may be specified multiple times in a valid URL, and so each value is a sequence (e.g., list, array) of strings from the original URL. -* `path` - An object of key-value pairs representing path parameters and their values. -* `headers` - An object of key-value pairs where a key is a HTTP header name and a value is the HTTP header value. If there are multiple values are specified for a header, the value will be an array. +- `body` - The value of the body, see the [Body And Content Type](#request-content-type-and-request-body-processing) section below +- `args` - An object with keys representing query parameter names and their associated values. A query parameter name may be specified multiple times in a valid URL, and so each value is a sequence (e.g., list, array) of strings from the original URL. +- `path` - An object of key-value pairs representing path parameters and their values. +- `headers` - An object of key-value pairs where a key is a HTTP header name and a value is the HTTP header value. If there are multiple values are specified for a header, the value will be an array. {#request-content-type-and-request-body-processing} @@ -52,17 +52,17 @@ The `REQUEST` object currently contains the following properties: If the HTTP request to the kernel gateway has a `Content-Type` header the value of `REQUEST.body` may change. Below is the list of outcomes for various mime-types: -* `application/json` - The `REQUEST.body` will be an object of key-value pairs representing the request body -* `multipart/form-data` and `application/x-www-form-urlencoded` - The `REQUEST.body` will be an object of key-value pairs representing the parameters and their values. Files are currently not supported for `multipart/form-data` -* `text/plain` - The `REQUEST.body` will be the string value of the body -* All other types will be sent as strings +- `application/json` - The `REQUEST.body` will be an object of key-value pairs representing the request body +- `multipart/form-data` and `application/x-www-form-urlencoded` - The `REQUEST.body` will be an object of key-value pairs representing the parameters and their values. Files are currently not supported for `multipart/form-data` +- `text/plain` - The `REQUEST.body` will be the string value of the body +- All other types will be sent as strings ## Setting the Response Body The response from an annotated cell may be set in one of two ways: 1. Writing to stdout in a notebook cell -2. Emitting output in a notebook cell +1. Emitting output in a notebook cell The first method is preferred because it is explicit: a cell writes to stdout using the appropriate language statement or function (e.g. Python `print`, Scala `println`, R `print`, etc.). The kernel gateway collects all bytes from kernel stdout and returns the entire byte string verbatim as the response body. diff --git a/docs/source/plug-in.md b/docs/source/plug-in.md index 33820c5..834ba9a 100644 --- a/docs/source/plug-in.md +++ b/docs/source/plug-in.md @@ -2,32 +2,32 @@ The `KernelGatewayApp.api` can be set to the name of any module in the Python path supplying a personality. This allows for alternate kernel communications mechanisms. -The module must contain a ``create_personality`` function whose ``parent`` argument will be the kernel gateway application, and which must return a *personality* object. That object will take part in the kernel gateway's lifecycle and act as a delegate for certain responsibilities. An example module, subclassing ``LoggingConfigurable`` as recommended, is shown here: +The module must contain a `create_personality` function whose `parent` argument will be the kernel gateway application, and which must return a *personality* object. That object will take part in the kernel gateway's lifecycle and act as a delegate for certain responsibilities. An example module, subclassing `LoggingConfigurable` as recommended, is shown here: ``` from traitlets.config.configurable import LoggingConfigurable class TemplatePersonality(LoggingConfigurable): def init_configurables(self): - """This function will be called when the kernel gateway has completed its own + """This function will be called when the kernel gateway has completed its own `init_configurables`, typically after its traitlets have been evaluated.""" - pass + pass def shutdown(self): """During a proper shutdown of the kernel gateway, this will be called so that any held resources may be properly released.""" - pass + pass def create_request_handlers(self): """Returns a list of zero or more tuples of handler path, Tornado handler class - name, and handler arguments, that should be registered in the kernel gateway's - web application. Paths are used as given and should respect the kernel gateway's + name, and handler arguments, that should be registered in the kernel gateway's + web application. Paths are used as given and should respect the kernel gateway's `base_url` traitlet value.""" - pass + pass def should_seed_cell(self, code): - """Determines whether the kernel gateway will include the given notebook code - cell when seeding a new kernel. Will only be called if a seed notebook has + """Determines whether the kernel gateway will include the given notebook code + cell when seeding a new kernel. Will only be called if a seed notebook has been specified.""" pass diff --git a/docs/source/summary-changes.md b/docs/source/summary-changes.md index 180e574..e3c46f8 100644 --- a/docs/source/summary-changes.md +++ b/docs/source/summary-changes.md @@ -1,199 +1,203 @@ # Summary of changes See `git log` for a more detailed summary of changes. + ## 2.4 ### 2.4.0 (2019-08-11) -* [PR-323](https://github.com/jupyter/kernel_gateway/pull/323): Update handler not use deprecated maybe_future call -* [PR-322](https://github.com/jupyter/kernel_gateway/pull/322): Update handler compatibility with tornado/pyzmq updates -* [PR-321](https://github.com/jupyter/kernel_gateway/pull/321): Allow Notebook 6.x dependencies -* [PR-317](https://github.com/jupyter/kernel_gateway/pull/317): Better error toleration during server initialization +- [PR-323](https://github.com/jupyter/kernel_gateway/pull/323): Update handler not use deprecated maybe_future call +- [PR-322](https://github.com/jupyter/kernel_gateway/pull/322): Update handler compatibility with tornado/pyzmq updates +- [PR-321](https://github.com/jupyter/kernel_gateway/pull/321): Allow Notebook 6.x dependencies +- [PR-317](https://github.com/jupyter/kernel_gateway/pull/317): Better error toleration during server initialization ## 2.3 ### 2.3.0 (2019-03-15) -* [PR-315](https://github.com/jupyter/kernel_gateway/pull/315): Call tornado StaticFileHandler.get() as a coroutine +- [PR-315](https://github.com/jupyter/kernel_gateway/pull/315): Call tornado StaticFileHandler.get() as a coroutine ## 2.2 ### 2.2.0 (2019-02-26) -* [PR-314](https://github.com/jupyter/kernel_gateway/pull/314): Support serving kernelspec resources -* [PR-307](https://github.com/jupyter/kernel_gateway/pull/307): features.md: Fix a link typo -* [PR-304](https://github.com/jupyter/kernel_gateway/pull/304): Add ability for Kernel Gateway to ignore SIGHUP signal -* [PR-303](https://github.com/jupyter/kernel_gateway/pull/303): Fixed the link to section +- [PR-314](https://github.com/jupyter/kernel_gateway/pull/314): Support serving kernelspec resources +- [PR-307](https://github.com/jupyter/kernel_gateway/pull/307): features.md: Fix a link typo +- [PR-304](https://github.com/jupyter/kernel_gateway/pull/304): Add ability for Kernel Gateway to ignore SIGHUP signal +- [PR-303](https://github.com/jupyter/kernel_gateway/pull/303): Fixed the link to section ## 2.1 ### 2.1.0 (2018-08-13) -* [PR-299](https://github.com/jupyter/kernel_gateway/pull/299): adds x_header configuration option for use behind proxies -* [PR-294](https://github.com/jupyter/kernel_gateway/pull/294): Allow access from remote hosts (Notebook 5.6) -* [PR-292](https://github.com/jupyter/kernel_gateway/pull/292): Update dependencies of Jupyter components -* [PR-290](https://github.com/jupyter/kernel_gateway/pull/290): Include LICENSE file in wheels -* [PR-285](https://github.com/jupyter/kernel_gateway/pull/285): Update Kernel Gateway test base class to be compatible with Tornado 5.0 -* [PR-284](https://github.com/jupyter/kernel_gateway/pull/284): Add reason argument to set_status() so that custom messages flow back to client -* [PR-280](https://github.com/jupyter/kernel_gateway/pull/280): Add whitelist of environment variables to be inherited from gateway process by kernel -* [PR-275](https://github.com/jupyter/kernel_gateway/pull/275): Fix broken links to notebook-http mode page in docs -* [PR-272](https://github.com/jupyter/kernel_gateway/pull/272): Fix bug when getting kernel language in notebook-http mode -* [PR-271](https://github.com/jupyter/kernel_gateway/pull/271): Fix IPerl notebooks running in notebook-http mode +- [PR-299](https://github.com/jupyter/kernel_gateway/pull/299): adds x_header configuration option for use behind proxies +- [PR-294](https://github.com/jupyter/kernel_gateway/pull/294): Allow access from remote hosts (Notebook 5.6) +- [PR-292](https://github.com/jupyter/kernel_gateway/pull/292): Update dependencies of Jupyter components +- [PR-290](https://github.com/jupyter/kernel_gateway/pull/290): Include LICENSE file in wheels +- [PR-285](https://github.com/jupyter/kernel_gateway/pull/285): Update Kernel Gateway test base class to be compatible with Tornado 5.0 +- [PR-284](https://github.com/jupyter/kernel_gateway/pull/284): Add reason argument to set_status() so that custom messages flow back to client +- [PR-280](https://github.com/jupyter/kernel_gateway/pull/280): Add whitelist of environment variables to be inherited from gateway process by kernel +- [PR-275](https://github.com/jupyter/kernel_gateway/pull/275): Fix broken links to notebook-http mode page in docs +- [PR-272](https://github.com/jupyter/kernel_gateway/pull/272): Fix bug when getting kernel language in notebook-http mode +- [PR-271](https://github.com/jupyter/kernel_gateway/pull/271): Fix IPerl notebooks running in notebook-http mode ## 2.0 ### 2.0.2 (2017-11-10) -* [PR-266](https://github.com/jupyter/kernel_gateway/pull/266): Make KernelManager and KernelSpecManager configurable -* [PR-263](https://github.com/jupyter/kernel_gateway/pull/263): Correct JSONErrorsMixin for compatibility with notebook 5.2.0 +- [PR-266](https://github.com/jupyter/kernel_gateway/pull/266): Make KernelManager and KernelSpecManager configurable +- [PR-263](https://github.com/jupyter/kernel_gateway/pull/263): Correct JSONErrorsMixin for compatibility with notebook 5.2.0 ### 2.0.1 (2017-09-09) -* [PR-258](https://github.com/jupyter/kernel_gateway/pull/258): Remove auth token check for OPTIONS requests (CORS) +- [PR-258](https://github.com/jupyter/kernel_gateway/pull/258): Remove auth token check for OPTIONS requests (CORS) ### 2.0.0 (2017-05-30) -* Update compatibility to notebook>=5.0 -* Remove kernel activity API in favor of the one in the notebook package -* Update project overview in the documentation -* Inherit the server `PATH` when launching a new kernel via POST request +- Update compatibility to notebook>=5.0 +- Remove kernel activity API in favor of the one in the notebook package +- Update project overview in the documentation +- Inherit the server `PATH` when launching a new kernel via POST request with custom environment variables -* Fix kernel cleanup upon SIGTERM -* Fix security requirements in the swagger spec -* Fix configured headers for OPTIONS requests +- Fix kernel cleanup upon SIGTERM +- Fix security requirements in the swagger spec +- Fix configured headers for OPTIONS requests ## 1.2 ### 1.2.2 (2017-05-30) -* Inherit the server `PATH` when launching a new kernel via POST request +- Inherit the server `PATH` when launching a new kernel via POST request with custom environment variables -* Fix kernel cleanup upon SIGTERM +- Fix kernel cleanup upon SIGTERM ### 1.2.1 (2017-04-01) -* Add support for auth token as a query parameter +- Add support for auth token as a query parameter ### 1.2.0 (2017-02-12) -* Add command line option to whitelist environment variables for `POST /api/kernels` -* Add support for HTTPS key and certificate files -* Improve the flow and explanations in the `api_intro` notebook -* Fix incorrect use of `metadata.kernelspec.name` as a language name instead of +- Add command line option to whitelist environment variables for `POST /api/kernels` +- Add support for HTTPS key and certificate files +- Improve the flow and explanations in the `api_intro` notebook +- Fix incorrect use of `metadata.kernelspec.name` as a language name instead of `metadata.language.info` -* Fix lingering kernel regression after Ctrl-C interrupt -* Switch to a conda-based dev setup from docker +- Fix lingering kernel regression after Ctrl-C interrupt +- Switch to a conda-based dev setup from docker ## 1.1 ### 1.1.1 (2016-09-10) -* Add LICENSE file to package distributions +- Add LICENSE file to package distributions ### 1.1.0 (2016-09-08) -* Add an option to force a specific kernel spec for all requests and seed notebooks -* Add support for specifying notebook-http APIs using full Swagger specs -* Add option to serve static web assets from Tornado in notebook-http mode -* Add command line aliases for common options (e.g., `--ip`) -* Fix Tornado 4.4 compatbility: sending an empty body string with a 204 response +- Add an option to force a specific kernel spec for all requests and seed notebooks +- Add support for specifying notebook-http APIs using full Swagger specs +- Add option to serve static web assets from Tornado in notebook-http mode +- Add command line aliases for common options (e.g., `--ip`) +- Fix Tornado 4.4 compatbility: sending an empty body string with a 204 response ## 1.0 ### 1.0.0 (2016-07-15) -* Introduce an [API for developing mode plug-ins](https://jupyter-kernel-gateway.readthedocs.io/en/latest/plug-in.html) -* Separate `jupyter-websocket` and `notebook-http` modes into plug-in packages -* Move mode specific command line options into their respective packages (see `--help-all`) -* Report times with respect to UTC in `/_api/activity` responses +- Introduce an [API for developing mode plug-ins](https://jupyter-kernel-gateway.readthedocs.io/en/latest/plug-in.html) +- Separate `jupyter-websocket` and `notebook-http` modes into plug-in packages +- Move mode specific command line options into their respective packages (see `--help-all`) +- Report times with respect to UTC in `/_api/activity` responses ## 0.6 ### 0.6.0 (2016-06-17) -* Switch HTTP status from 402 for 403 when server reaches the max kernel limit -* Explicitly shutdown kernels when the server shuts down -* Remove `KG_AUTH_TOKEN` from the environment of kernels -* Fix missing swagger document in release -* Add `--KernelGateway.port_retries` option like in Jupyter Notebook -* Fix compatibility with Notebook 4.2 session handler `create_session` call +- Switch HTTP status from 402 for 403 when server reaches the max kernel limit +- Explicitly shutdown kernels when the server shuts down +- Remove `KG_AUTH_TOKEN` from the environment of kernels +- Fix missing swagger document in release +- Add `--KernelGateway.port_retries` option like in Jupyter Notebook +- Fix compatibility with Notebook 4.2 session handler `create_session` call ## 0.5 ### 0.5.1 (2016-04-20) -* Backport `--KernelGateway.port_retries` option like in Jupyter Notebook -* Fix compatibility with Notebook 4.2 session handler `create_session` call +- Backport `--KernelGateway.port_retries` option like in Jupyter Notebook +- Fix compatibility with Notebook 4.2 session handler `create_session` call ### 0.5.0 (2016-04-04) -* Support multiple cells per path in `notebook-http` mode -* Add a Swagger specification of the `jupyter-websocket` API -* Add `KERNEL_GATEWAY=1` to all kernel environments -* Support environment variables in `POST /api/kernels` -* numpydoc format docstrings on everything -* Convert README to Sphinx/ReadTheDocs site -* Convert `ActivityManager` to a traitlets `LoggingConfigurable` -* Fix `base_url` handling for all paths -* Fix unbounded growth of ignored kernels in `ActivityManager` -* Fix caching of Swagger spec in `notebook-http` mode -* Fix failure to install due to whitespace in `setup.py` version numbers -* Fix call to kernel manager base class when starting a kernel -* Fix test fixture hangs +- Support multiple cells per path in `notebook-http` mode +- Add a Swagger specification of the `jupyter-websocket` API +- Add `KERNEL_GATEWAY=1` to all kernel environments +- Support environment variables in `POST /api/kernels` +- numpydoc format docstrings on everything +- Convert README to Sphinx/ReadTheDocs site +- Convert `ActivityManager` to a traitlets `LoggingConfigurable` +- Fix `base_url` handling for all paths +- Fix unbounded growth of ignored kernels in `ActivityManager` +- Fix caching of Swagger spec in `notebook-http` mode +- Fix failure to install due to whitespace in `setup.py` version numbers +- Fix call to kernel manager base class when starting a kernel +- Fix test fixture hangs ## 0.4 ### 0.4.1 (2016-04-20) -* Backport `--KernelGateway.port_retries` option like in Jupyter Notebook -* Fix compatibility with Notebook 4.2 session handler `create_session` call +- Backport `--KernelGateway.port_retries` option like in Jupyter Notebook +- Fix compatibility with Notebook 4.2 session handler `create_session` call ### 0.4.0 (2016-02-17) -* Enable `/_api/activity` resource with stats about kernels in +- Enable `/_api/activity` resource with stats about kernels in `jupyter-websocket` mode -* Enable `/api/sessions` resource with in-memory name-to-kernel mapping for +- Enable `/api/sessions` resource with in-memory name-to-kernel mapping for non-notebook clients that want to look-up kernels by associated session name -* Fix prespawn kernel logic regression for `jupyter-websocket` mode -* Fix all handlers so that they return application/json responses on error -* Fix missing output from cells that emit display data in `notebook-http` mode +- Fix prespawn kernel logic regression for `jupyter-websocket` mode +- Fix all handlers so that they return application/json responses on error +- Fix missing output from cells that emit display data in `notebook-http` mode ## 0.3 + ### 0.3.1 (2016-01-25) -* Fix CORS and auth token headers for `/_api/spec/swagger.json` resource -* Fix `allow_origin` handling for non-browser clients -* Ensure base path is prefixed with a forward slash -* Filter stderr from all responses in `notebook-http` mode -* Set Tornado logging level and Jupyter logging level together with +- Fix CORS and auth token headers for `/_api/spec/swagger.json` resource +- Fix `allow_origin` handling for non-browser clients +- Ensure base path is prefixed with a forward slash +- Filter stderr from all responses in `notebook-http` mode +- Set Tornado logging level and Jupyter logging level together with `--log-level` ### 0.3.0 (2016-01-15) -* Support setting of status and headers in `notebook-http` mode -* Support automatic, minimal Swagger doc generation in `notebook-http` mode -* Support download of a notebook in `notebook-http` mode -* Support CORS and token auth in `notebook-http` mode -* Expose HTTP request headers in `notebook-http` mode -* Support multipart form encoding in `notebook-http` mode -* Fix request value JSON encoding when passing requests to kernels -* Fix kernel name handling when pre-spawning -* Fix lack of access logs in `notebook-http` mode +- Support setting of status and headers in `notebook-http` mode +- Support automatic, minimal Swagger doc generation in `notebook-http` mode +- Support download of a notebook in `notebook-http` mode +- Support CORS and token auth in `notebook-http` mode +- Expose HTTP request headers in `notebook-http` mode +- Support multipart form encoding in `notebook-http` mode +- Fix request value JSON encoding when passing requests to kernels +- Fix kernel name handling when pre-spawning +- Fix lack of access logs in `notebook-http` mode ## 0.2 + ### 0.2.0 (2015-12-15) -* Support notebook-defined HTTP APIs on a pool of kernels -* Disable kernel instance list by default +- Support notebook-defined HTTP APIs on a pool of kernels +- Disable kernel instance list by default ## 0.1 + ### 0.1.0 (2015-11-18) -* Support Jupyter Notebook kernel CRUD APIs and Jupyter kernel protocol over +- Support Jupyter Notebook kernel CRUD APIs and Jupyter kernel protocol over Websockets -* Support shared token auth -* Support CORS headers -* Support base URL -* Support seeding kernels code from a notebook at a file path or URL -* Support default kernel, kernel pre-spawning, and kernel count limit -* First PyPI release +- Support shared token auth +- Support CORS headers +- Support base URL +- Support seeding kernels code from a notebook at a file path or URL +- Support default kernel, kernel pre-spawning, and kernel count limit +- First PyPI release diff --git a/docs/source/uses.md b/docs/source/uses.md index 2db41fa..8e88a26 100644 --- a/docs/source/uses.md +++ b/docs/source/uses.md @@ -2,15 +2,15 @@ The Jupyter Kernel Gateway makes possible the following novel uses of kernels: -* Attach a local Jupyter Notebook server to a compute cluster in the cloud +- Attach a local Jupyter Notebook server to a compute cluster in the cloud running near big data (e.g., interactive gateway to Spark) -* Enable a new breed of non-notebook web clients to provision and use - kernels (e.g., dashboards using +- Enable a new breed of non-notebook web clients to provision and use + kernels (e.g., dashboards using [jupyter-js-services](https://github.com/jupyter/jupyter-js-services)) -* Scale kernels independently from clients (e.g., via +- Scale kernels independently from clients (e.g., via [tmpnb](https://github.com/jupyter/tmpnb), [Binder](http://mybinder.org/), or your favorite cluster manager) -* Create microservices from notebooks via +- Create microservices from notebooks via [`notebook-http` mode](http-mode.md) The following diagram shows how you might use `tmpnb` to deploy a pool of kernel gateway instances in Docker containers to support on-demand interactive compute: diff --git a/docs/source/websocket-mode.md b/docs/source/websocket-mode.md index cd27011..a98ca1b 100644 --- a/docs/source/websocket-mode.md +++ b/docs/source/websocket-mode.md @@ -3,7 +3,7 @@ The `KernelGatewayApp.api` command line argument defaults to `kernel_gateway.jupyter_websocket`. This mode, or *personality*, has the kernel gateway expose: 1. a superset of the HTTP API provided by the Jupyter Notebook server, and -2. the equivalent Websocket API implemented by the Jupyter Notebook server. +1. the equivalent Websocket API implemented by the Jupyter Notebook server. ## HTTP Resources diff --git a/etc/api_examples/api_intro.ipynb b/etc/api_examples/api_intro.ipynb index 540d86f..10d8abd 100644 --- a/etc/api_examples/api_intro.ipynb +++ b/etc/api_examples/api_intro.ipynb @@ -112,7 +112,7 @@ }, "outputs": [], "source": [ - "fields = ['name', 'phone', 'address']" + "fields = [\"name\", \"phone\", \"address\"]" ] }, { @@ -137,13 +137,15 @@ }, "outputs": [], "source": [ - "REQUEST = json.dumps({\n", - " 'body': {\n", - " 'name': 'Jane Doe',\n", - " 'phone': '888-555-5245',\n", - " 'address': '123 Bellview Drive, Somewhere, NC'\n", + "REQUEST = json.dumps(\n", + " {\n", + " \"body\": {\n", + " \"name\": \"Jane Doe\",\n", + " \"phone\": \"888-555-5245\",\n", + " \"address\": \"123 Bellview Drive, Somewhere, NC\",\n", + " }\n", " }\n", - "})" + ")" ] }, { @@ -178,12 +180,12 @@ "# decode the request\n", "req = json.loads(REQUEST)\n", "# pull out the body\n", - "body = req['body']\n", + "body = req[\"body\"]\n", "# generate a new contact ID\n", "new_contact_id = str(uuid.uuid4())\n", "# put what we can about the contact in the dictionary\n", "contacts[new_contact_id] = {field: body.get(field) for field in fields}\n", - "print(json.dumps({'contact_id': new_contact_id}))" + "print(json.dumps({\"contact_id\": new_contact_id}))" ] }, { @@ -254,15 +256,15 @@ }, "outputs": [], "source": [ - "REQUEST = json.dumps({\n", - " 'body': {\n", - " 'name': 'Jane and John Doe',\n", - " 'address': '321 Viewbell Lane, Somewhere Else, SC'\n", - " },\n", - " 'path': {\n", - " 'contact_id': globals().get('new_contact_id', '')\n", + "REQUEST = json.dumps(\n", + " {\n", + " \"body\": {\n", + " \"name\": \"Jane and John Doe\",\n", + " \"address\": \"321 Viewbell Lane, Somewhere Else, SC\",\n", + " },\n", + " \"path\": {\"contact_id\": globals().get(\"new_contact_id\", \"\")},\n", " }\n", - "})" + ")" ] }, { @@ -295,12 +297,12 @@ "source": [ "# PUT /contacts/:contact_id\n", "req = json.loads(REQUEST)\n", - "body = req['body']\n", - "contact_id = req['path']['contact_id']\n", + "body = req[\"body\"]\n", + "contact_id = req[\"path\"][\"contact_id\"]\n", "if contact_id in contacts:\n", " contacts[contact_id].update({field: body[field] for field in fields if field in body})\n", " status = 200\n", - " print(json.dumps({'contact_id': contacts[contact_id]}))\n", + " print(json.dumps({\"contact_id\": contacts[contact_id]}))\n", "else:\n", " status = 404" ] @@ -360,12 +362,7 @@ ], "source": [ "# ResponseInfo PUT /contacts/:contact_id\n", - "print(json.dumps({\n", - " \"status\" : status,\n", - " \"headers\" : {\n", - " \"Content-Type\" : \"application/json\"\n", - " }\n", - "}))" + "print(json.dumps({\"status\": status, \"headers\": {\"Content-Type\": \"application/json\"}}))" ] }, { @@ -400,7 +397,7 @@ "source": [ "# DELETE /contacts/:contact_id\n", "req = json.loads(REQUEST)\n", - "contact_id = req['path']['contact_id']\n", + "contact_id = req[\"path\"][\"contact_id\"]\n", "if contact_id in contacts:\n", " del contacts[contact_id]\n", " # HTTP status code for no body\n", @@ -429,10 +426,13 @@ ], "source": [ "# ResponseInfo DELETE /contacts/:contact_id\n", - "print(json.dumps({\n", - " \"status\" : status,\n", - " \n", - "}))" + "print(\n", + " json.dumps(\n", + " {\n", + " \"status\": status,\n", + " }\n", + " )\n", + ")" ] }, { @@ -472,24 +472,27 @@ "source": [ "def filter_by_name(name_regex, contacts):\n", " \"\"\"Get contacts with names matching the optional regex.\n", - " \n", + "\n", " Get all contacts if name_regex is None.\n", - " \n", + "\n", " Parameters\n", " ----------\n", " name_regex: str or None\n", " Regular expression to match to contact names\n", " contacts: list of dict\n", " Contacts to consider\n", - " \n", + "\n", " Returns\n", " -------\n", " list of dict\n", " Matching contacts\n", " \"\"\"\n", " if name_regex is not None:\n", - " return {contact_id: contact for contact_id, contact in contacts.items()\n", - " if re.search(name_regex, contact['name'], re.IGNORECASE)}\n", + " return {\n", + " contact_id: contact\n", + " for contact_id, contact in contacts.items()\n", + " if re.search(name_regex, contact[\"name\"], re.IGNORECASE)\n", + " }\n", " else:\n", " return contacts" ] @@ -522,7 +525,7 @@ "# GET /contacts\n", "req = json.loads(REQUEST)\n", "# query args appear as a list since they can be repeated in the URL\n", - "name_regex = req.get('args', {}).get('name', [None])[0]\n", + "name_regex = req.get(\"args\", {}).get(\"name\", [None])[0]\n", "hits = filter_by_name(name_regex, contacts)\n", "print(json.dumps(hits))" ] @@ -546,11 +549,7 @@ ], "source": [ "# ResponseInfo GET /contacts\n", - "print(json.dumps({\n", - " \"headers\" : {\n", - " \"Content-Type\" : \"application/json\"\n", - " }\n", - "}))" + "print(json.dumps({\"headers\": {\"Content-Type\": \"application/json\"}}))" ] }, { @@ -590,7 +589,7 @@ }, "outputs": [], "source": [ - "URL = 'http://127.0.0.1:8889'" + "URL = \"http://127.0.0.1:8889\"" ] }, { @@ -601,53 +600,60 @@ }, "outputs": [], "source": [ - "if 'KERNEL_GATEWAY' not in os.environ:\n", + "if \"KERNEL_GATEWAY\" not in os.environ:\n", " import requests\n", "\n", " # create a contact\n", - " post_resp = requests.post(URL+'/contacts', json={\n", - " 'name': 'Alice Adams',\n", - " 'phone': '919-555-6712',\n", - " 'address': '42 Wallaby Way, Sydney, NC'\n", - " })\n", + " post_resp = requests.post(\n", + " URL + \"/contacts\",\n", + " json={\n", + " \"name\": \"Alice Adams\",\n", + " \"phone\": \"919-555-6712\",\n", + " \"address\": \"42 Wallaby Way, Sydney, NC\",\n", + " },\n", + " )\n", " post_resp.raise_for_status()\n", - " print('created a contact:', post_resp.json())\n", - " \n", - " first_contact_id = post_resp.json()['contact_id']\n", + " print(\"created a contact:\", post_resp.json())\n", + "\n", + " first_contact_id = post_resp.json()[\"contact_id\"]\n", "\n", " # update the contact\n", - " put_resp = requests.put(URL+'/contacts/'+first_contact_id, {\n", - " 'phone': '919-444-5601'\n", - " }) \n", + " put_resp = requests.put(URL + \"/contacts/\" + first_contact_id, {\"phone\": \"919-444-5601\"})\n", " put_resp.raise_for_status()\n", - " print('\\nupdated a contact:', put_resp.json())\n", + " print(\"\\nupdated a contact:\", put_resp.json())\n", "\n", " # add two more contacts\n", - " requests.post(URL+'/contacts', json={\n", - " 'name': 'Bob Billiham',\n", - " 'phone': '860-555-1409',\n", - " 'address': '3712 Not Real Lane, Bridgeport, CT'\n", - " }).raise_for_status()\n", - " requests.post(URL+'/contacts', json={\n", - " 'name': 'Cathy Caritgan',\n", - " 'phone': '512-555-6925',\n", - " 'address': '11 Stringigent Road, Albany, NY'\n", - " }).raise_for_status()\n", - " print('\\added two more contacts')\n", + " requests.post(\n", + " URL + \"/contacts\",\n", + " json={\n", + " \"name\": \"Bob Billiham\",\n", + " \"phone\": \"860-555-1409\",\n", + " \"address\": \"3712 Not Real Lane, Bridgeport, CT\",\n", + " },\n", + " ).raise_for_status()\n", + " requests.post(\n", + " URL + \"/contacts\",\n", + " json={\n", + " \"name\": \"Cathy Caritgan\",\n", + " \"phone\": \"512-555-6925\",\n", + " \"address\": \"11 Stringigent Road, Albany, NY\",\n", + " },\n", + " ).raise_for_status()\n", + " print(\"\\added two more contacts\")\n", "\n", " # fetch contacts with 'billi' in the lowercased text\n", - " resp = requests.get(URL+'/contacts?name=billi')\n", + " resp = requests.get(URL + \"/contacts?name=billi\")\n", " resp.raise_for_status()\n", - " print('\\ncontacts w/ name Bill:', resp.json())\n", + " print(\"\\ncontacts w/ name Bill:\", resp.json())\n", "\n", " # delete a contact\n", - " requests.delete(URL+'/contacts/'+first_contact_id).raise_for_status()\n", - " print('\\ndeleted a contact')\n", - " \n", + " requests.delete(URL + \"/contacts/\" + first_contact_id).raise_for_status()\n", + " print(\"\\ndeleted a contact\")\n", + "\n", " # show all of the remaining contacts\n", - " resp = requests.get(URL+'/contacts')\n", + " resp = requests.get(URL + \"/contacts\")\n", " resp.raise_for_status()\n", - " print('\\nall contacts:', resp.json())" + " print(\"\\nall contacts:\", resp.json())" ] }, { diff --git a/etc/api_examples/endpoint_ordering.ipynb b/etc/api_examples/endpoint_ordering.ipynb index 4167a0c..8758ccc 100644 --- a/etc/api_examples/endpoint_ordering.ipynb +++ b/etc/api_examples/endpoint_ordering.ipynb @@ -20,7 +20,7 @@ "outputs": [], "source": [ "# GET /test/test/test/test\n", - "print('path param index test 1')" + "print(\"path param index test 1\")" ] }, { @@ -32,7 +32,7 @@ "outputs": [], "source": [ "# GET /test/test/test/:id\n", - "print('path param index test 2')" + "print(\"path param index test 2\")" ] }, { @@ -44,7 +44,7 @@ "outputs": [], "source": [ "# GET /test/test/:id\n", - "print('path param index test 3')" + "print(\"path param index test 3\")" ] }, { @@ -56,7 +56,7 @@ "outputs": [], "source": [ "# GET /t/t/:id\n", - "print('path param index test 3')" + "print(\"path param index test 3\")" ] }, { @@ -68,7 +68,7 @@ "outputs": [], "source": [ "# GET /test/:id\n", - "print('path param index test 4')" + "print(\"path param index test 4\")" ] }, { @@ -80,7 +80,7 @@ "outputs": [], "source": [ "# GET /t/:id\n", - "print('path param index test 4')" + "print(\"path param index test 4\")" ] }, { @@ -92,7 +92,7 @@ "outputs": [], "source": [ "# GET /:id\n", - "print('path param index test 5')" + "print(\"path param index test 5\")" ] } ], diff --git a/etc/api_examples/setting_response_metadata.ipynb b/etc/api_examples/setting_response_metadata.ipynb index 882dc5d..6a1931b 100644 --- a/etc/api_examples/setting_response_metadata.ipynb +++ b/etc/api_examples/setting_response_metadata.ipynb @@ -59,14 +59,7 @@ }, "outputs": [], "source": [ - "PERSON_INDEX = {\n", - " 1 : {\n", - " 'id' : 1,\n", - " 'name' : 'Corey',\n", - " 'age' : 26,\n", - " 'location' : 'Austin, TX'\n", - " }\n", - "}" + "PERSON_INDEX = {1: {\"id\": 1, \"name\": \"Corey\", \"age\": 26, \"location\": \"Austin, TX\"}}" ] }, { @@ -81,36 +74,39 @@ " person_id = int(person_id)\n", " return PERSON_INDEX[person_id]\n", "\n", + "\n", "def get_person_hash(person_id):\n", " hash_value = hashlib.md5()\n", " person = get_person(person_id)\n", - " hash_value.update(\"{}-{}-{}-{}\".format(\n", - " person['id'],\n", - " person['name'],\n", - " person['age'],\n", - " person['location']\n", - " ).encode('UTF-8'))\n", + " hash_value.update(\n", + " \"{}-{}-{}-{}\".format(\n", + " person[\"id\"], person[\"name\"], person[\"age\"], person[\"location\"]\n", + " ).encode(\"UTF-8\")\n", + " )\n", " return hash_value.hexdigest()\n", "\n", + "\n", "def serialize_person(person, content_type):\n", - " if content_type == 'application/json':\n", + " if content_type == \"application/json\":\n", " return json.dumps(person)\n", - " elif content_type == 'application/xml' or content_type == 'text/xml':\n", - " return dicttoxml(person).decode('UTF-8')\n", - " elif content_type == 'text/html':\n", - " return '''

{} is {} years old and lives in {}.

'''.format(\n", - " person['name'], person['age'], person['location']\n", + " elif content_type == \"application/xml\" or content_type == \"text/xml\":\n", + " return dicttoxml(person).decode(\"UTF-8\")\n", + " elif content_type == \"text/html\":\n", + " return \"\"\"

{} is {} years old and lives in {}.

\"\"\".format(\n", + " person[\"name\"], person[\"age\"], person[\"location\"]\n", " )\n", - " \n", + "\n", + "\n", "def get_request_content_type(request):\n", - " if 'headers' in request and 'Content-Type' in request['headers']:\n", - " return request['headers']['Content-Type']\n", + " if \"headers\" in request and \"Content-Type\" in request[\"headers\"]:\n", + " return request[\"headers\"][\"Content-Type\"]\n", " else:\n", - " return 'text/html'\n", + " return \"text/html\"\n", + "\n", "\n", "def get_request_etag(request):\n", - " if 'headers' in request and 'If-None-Match' in request['headers']:\n", - " return request['headers']['If-None-Match']\n", + " if \"headers\" in request and \"If-None-Match\" in request[\"headers\"]:\n", + " return request[\"headers\"][\"If-None-Match\"]\n", " else:\n", " return None" ] @@ -123,15 +119,15 @@ }, "outputs": [], "source": [ - "REQUEST = json.dumps({\n", - " 'path' : {\n", - " 'id' : '1'\n", - " }, \n", - " 'headers' : {\n", - " 'Content-Type' : 'application/json',\n", - " 'If-None-Match' : 'e958b9efafbd6429bfad0985df27a1fb'\n", + "REQUEST = json.dumps(\n", + " {\n", + " \"path\": {\"id\": \"1\"},\n", + " \"headers\": {\n", + " \"Content-Type\": \"application/json\",\n", + " \"If-None-Match\": \"e958b9efafbd6429bfad0985df27a1fb\",\n", + " },\n", " }\n", - "})" + ")" ] }, { @@ -153,12 +149,12 @@ "# GET /person/:id\n", "request = json.loads(REQUEST)\n", "etag = get_request_etag(request)\n", - "person_id = int(request['path']['id'])\n", + "person_id = int(request[\"path\"][\"id\"])\n", "current_person_hash = get_person_hash(person_id)\n", "status_code = 200\n", "if etag == current_person_hash:\n", " status_code = 304\n", - "else: \n", + "else:\n", " person = get_person(person_id)\n", " response_value = serialize_person(person, get_request_content_type(request))\n", " print(response_value)" @@ -181,13 +177,16 @@ "outputs": [], "source": [ "# ResponseInfo GET /person/:id\n", - "print(json.dumps({\n", - " 'headers' : {\n", - " 'Content-Type' : get_request_content_type(request),\n", - " 'ETag' : current_person_hash\n", - " },\n", - " 'status' : status_code\n", - " })\n", + "print(\n", + " json.dumps(\n", + " {\n", + " \"headers\": {\n", + " \"Content-Type\": get_request_content_type(request),\n", + " \"ETag\": current_person_hash,\n", + " },\n", + " \"status\": status_code,\n", + " }\n", + " )\n", ")" ] }, diff --git a/kernel_gateway/__main__.py b/kernel_gateway/__main__.py index 071ec23..9298bb4 100644 --- a/kernel_gateway/__main__.py +++ b/kernel_gateway/__main__.py @@ -3,6 +3,7 @@ """CLI entrypoint for the kernel gateway package.""" from __future__ import absolute_import -if __name__ == '__main__': +if __name__ == "__main__": import kernel_gateway.gatewayapp as app + app.launch_instance() diff --git a/kernel_gateway/auth/identity.py b/kernel_gateway/auth/identity.py index 2b1a63a..0d60e63 100644 --- a/kernel_gateway/auth/identity.py +++ b/kernel_gateway/auth/identity.py @@ -45,7 +45,7 @@ def generate_anonymous_user(self, handler: web.RequestHandler) -> User: For use when a single shared token is used, but does not identify a user. """ - name = display_name = f"Anonymous" + name = display_name = "Anonymous" initials = "An" color = None return User(name.lower(), name, display_name, initials, None, color) diff --git a/kernel_gateway/base/handlers.py b/kernel_gateway/base/handlers.py index 984cd17..0212a7d 100644 --- a/kernel_gateway/base/handlers.py +++ b/kernel_gateway/base/handlers.py @@ -7,13 +7,13 @@ from ..mixins import TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin -class APIVersionHandler(TokenAuthorizationMixin, - CORSMixin, - JSONErrorsMixin, - server_handlers.APIVersionHandler): +class APIVersionHandler( + TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin, server_handlers.APIVersionHandler +): """Extends the notebook server base API handler with token auth, CORS, and JSON errors. """ + pass @@ -27,11 +27,9 @@ class NotFoundHandler(JSONErrorsMixin, web.RequestHandler): tornado.web.HTTPError Always 404 Not Found """ + def prepare(self): raise web.HTTPError(404) -default_handlers = [ - (r'/api', APIVersionHandler), - (r'/(.*)', NotFoundHandler) -] +default_handlers = [(r"/api", APIVersionHandler), (r"/(.*)", NotFoundHandler)] diff --git a/kernel_gateway/gatewayapp.py b/kernel_gateway/gatewayapp.py index 2441751..4c70526 100644 --- a/kernel_gateway/gatewayapp.py +++ b/kernel_gateway/gatewayapp.py @@ -49,17 +49,19 @@ # Add additional command line aliases aliases = dict(base_aliases) -aliases.update({ - 'ip': 'KernelGatewayApp.ip', - 'port': 'KernelGatewayApp.port', - 'port_retries': 'KernelGatewayApp.port_retries', - 'api': 'KernelGatewayApp.api', - 'seed_uri': 'KernelGatewayApp.seed_uri', - 'keyfile': 'KernelGatewayApp.keyfile', - 'certfile': 'KernelGatewayApp.certfile', - 'client-ca': 'KernelGatewayApp.client_ca', - 'ssl_version': 'KernelGatewayApp.ssl_version' -}) +aliases.update( + { + "ip": "KernelGatewayApp.ip", + "port": "KernelGatewayApp.port", + "port_retries": "KernelGatewayApp.port_retries", + "api": "KernelGatewayApp.api", + "seed_uri": "KernelGatewayApp.seed_uri", + "keyfile": "KernelGatewayApp.keyfile", + "certfile": "KernelGatewayApp.certfile", + "client-ca": "KernelGatewayApp.client_ca", + "ssl_version": "KernelGatewayApp.ssl_version", + } +) class KernelGatewayApp(JupyterApp): @@ -71,7 +73,8 @@ class KernelGatewayApp(JupyterApp): - creates a Tornado HTTP server - starts the Tornado event loop """ - name = 'jupyter-kernel-gateway' + + name = "jupyter-kernel-gateway" version = __version__ description = """ Jupyter Kernel Gateway @@ -86,230 +89,268 @@ class KernelGatewayApp(JupyterApp): aliases = aliases # Server IP / PORT binding - port_env = 'KG_PORT' + port_env = "KG_PORT" port_default_value = 8888 - port = Integer(port_default_value, config=True, - help="Port on which to listen (KG_PORT env var)" + port = Integer( + port_default_value, config=True, help="Port on which to listen (KG_PORT env var)" ) - @default('port') + @default("port") def port_default(self): return int(os.getenv(self.port_env, self.port_default_value)) - port_retries_env = 'KG_PORT_RETRIES' + port_retries_env = "KG_PORT_RETRIES" port_retries_default_value = 50 - port_retries = Integer(port_retries_default_value, config=True, - help="Number of ports to try if the specified port is not available (KG_PORT_RETRIES env var)" + port_retries = Integer( + port_retries_default_value, + config=True, + help="Number of ports to try if the specified port is not available (KG_PORT_RETRIES env var)", ) - @default('port_retries') + @default("port_retries") def port_retries_default(self): return int(os.getenv(self.port_retries_env, self.port_retries_default_value)) - ip_env = 'KG_IP' - ip_default_value = '127.0.0.1' - ip = Unicode(ip_default_value, config=True, - help="IP address on which to listen (KG_IP env var)" + ip_env = "KG_IP" + ip_default_value = "127.0.0.1" + ip = Unicode( + ip_default_value, config=True, help="IP address on which to listen (KG_IP env var)" ) - @default('ip') + @default("ip") def ip_default(self): return os.getenv(self.ip_env, self.ip_default_value) # Base URL - base_url_env = 'KG_BASE_URL' - base_url_default_value = '/' - base_url = Unicode(base_url_default_value, config=True, - help="""The base path for mounting all API resources (KG_BASE_URL env var)""") + base_url_env = "KG_BASE_URL" + base_url_default_value = "/" + base_url = Unicode( + base_url_default_value, + config=True, + help="""The base path for mounting all API resources (KG_BASE_URL env var)""", + ) - @default('base_url') + @default("base_url") def base_url_default(self): return os.getenv(self.base_url_env, self.base_url_default_value) # Token authorization - auth_token_env = 'KG_AUTH_TOKEN' - auth_token = Unicode(config=True, - help='Authorization token required for all requests (KG_AUTH_TOKEN env var)' + auth_token_env = "KG_AUTH_TOKEN" + auth_token = Unicode( + config=True, help="Authorization token required for all requests (KG_AUTH_TOKEN env var)" ) - @default('auth_token') + @default("auth_token") def _auth_token_default(self): - return os.getenv(self.auth_token_env, '') + return os.getenv(self.auth_token_env, "") # CORS headers - allow_credentials_env = 'KG_ALLOW_CREDENTIALS' - allow_credentials = Unicode(config=True, - help='Sets the Access-Control-Allow-Credentials header. (KG_ALLOW_CREDENTIALS env var)' + allow_credentials_env = "KG_ALLOW_CREDENTIALS" + allow_credentials = Unicode( + config=True, + help="Sets the Access-Control-Allow-Credentials header. (KG_ALLOW_CREDENTIALS env var)", ) - @default('allow_credentials') + @default("allow_credentials") def allow_credentials_default(self): - return os.getenv(self.allow_credentials_env, '') + return os.getenv(self.allow_credentials_env, "") - allow_headers_env = 'KG_ALLOW_HEADERS' - allow_headers = Unicode(config=True, - help='Sets the Access-Control-Allow-Headers header. (KG_ALLOW_HEADERS env var)' + allow_headers_env = "KG_ALLOW_HEADERS" + allow_headers = Unicode( + config=True, help="Sets the Access-Control-Allow-Headers header. (KG_ALLOW_HEADERS env var)" ) - @default('allow_headers') + @default("allow_headers") def allow_headers_default(self): - return os.getenv(self.allow_headers_env, '') + return os.getenv(self.allow_headers_env, "") - allow_methods_env = 'KG_ALLOW_METHODS' - allow_methods = Unicode(config=True, - help='Sets the Access-Control-Allow-Methods header. (KG_ALLOW_METHODS env var)' + allow_methods_env = "KG_ALLOW_METHODS" + allow_methods = Unicode( + config=True, help="Sets the Access-Control-Allow-Methods header. (KG_ALLOW_METHODS env var)" ) - @default('allow_methods') + @default("allow_methods") def allow_methods_default(self): - return os.getenv(self.allow_methods_env, '') + return os.getenv(self.allow_methods_env, "") - allow_origin_env = 'KG_ALLOW_ORIGIN' - allow_origin = Unicode(config=True, - help='Sets the Access-Control-Allow-Origin header. (KG_ALLOW_ORIGIN env var)' + allow_origin_env = "KG_ALLOW_ORIGIN" + allow_origin = Unicode( + config=True, help="Sets the Access-Control-Allow-Origin header. (KG_ALLOW_ORIGIN env var)" ) - @default('allow_origin') + @default("allow_origin") def allow_origin_default(self): - return os.getenv(self.allow_origin_env, '') + return os.getenv(self.allow_origin_env, "") - expose_headers_env = 'KG_EXPOSE_HEADERS' - expose_headers = Unicode(config=True, - help='Sets the Access-Control-Expose-Headers header. (KG_EXPOSE_HEADERS env var)' + expose_headers_env = "KG_EXPOSE_HEADERS" + expose_headers = Unicode( + config=True, + help="Sets the Access-Control-Expose-Headers header. (KG_EXPOSE_HEADERS env var)", ) - @default('expose_headers') + @default("expose_headers") def expose_headers_default(self): - return os.getenv(self.expose_headers_env, '') + return os.getenv(self.expose_headers_env, "") - trust_xheaders_env = 'KG_TRUST_XHEADERS' - trust_xheaders = CBool(False, config=True, - help='Use x-* header values for overriding the remote-ip, useful when application is behing a proxy. (KG_TRUST_XHEADERS env var)' + trust_xheaders_env = "KG_TRUST_XHEADERS" + trust_xheaders = CBool( + False, + config=True, + help="Use x-* header values for overriding the remote-ip, useful when application is behing a proxy. (KG_TRUST_XHEADERS env var)", ) - @default('trust_xheaders') - def trust_xheaders_default(self): - return os.getenv(self.trust_xheaders_env, 'False').lower() == 'true' + @default("trust_xheaders") + def trust_xheaders_default(self): + return os.getenv(self.trust_xheaders_env, "False").lower() == "true" - max_age_env = 'KG_MAX_AGE' - max_age = Unicode(config=True, - help='Sets the Access-Control-Max-Age header. (KG_MAX_AGE env var)' + max_age_env = "KG_MAX_AGE" + max_age = Unicode( + config=True, help="Sets the Access-Control-Max-Age header. (KG_MAX_AGE env var)" ) - @default('max_age') + @default("max_age") def max_age_default(self): - return os.getenv(self.max_age_env, '') + return os.getenv(self.max_age_env, "") - max_kernels_env = 'KG_MAX_KERNELS' - max_kernels = Integer(None, config=True, + max_kernels_env = "KG_MAX_KERNELS" + max_kernels = Integer( + None, + config=True, allow_none=True, - help='Limits the number of kernel instances allowed to run by this gateway. Unbounded by default. (KG_MAX_KERNELS env var)' + help="Limits the number of kernel instances allowed to run by this gateway. Unbounded by default. (KG_MAX_KERNELS env var)", ) - @default('max_kernels') + @default("max_kernels") def max_kernels_default(self): val = os.getenv(self.max_kernels_env) return val if val is None else int(val) - seed_uri_env = 'KG_SEED_URI' - seed_uri = Unicode(None, config=True, + seed_uri_env = "KG_SEED_URI" + seed_uri = Unicode( + None, + config=True, allow_none=True, - help='Runs the notebook (.ipynb) at the given URI on every kernel launched. No seed by default. (KG_SEED_URI env var)' + help="Runs the notebook (.ipynb) at the given URI on every kernel launched. No seed by default. (KG_SEED_URI env var)", ) - @default('seed_uri') + @default("seed_uri") def seed_uri_default(self): return os.getenv(self.seed_uri_env) - prespawn_count_env = 'KG_PRESPAWN_COUNT' - prespawn_count = Integer(None, config=True, + prespawn_count_env = "KG_PRESPAWN_COUNT" + prespawn_count = Integer( + None, + config=True, allow_none=True, - help='Number of kernels to prespawn using the default language. No prespawn by default. (KG_PRESPAWN_COUNT env var)' + help="Number of kernels to prespawn using the default language. No prespawn by default. (KG_PRESPAWN_COUNT env var)", ) - @default('prespawn_count') + @default("prespawn_count") def prespawn_count_default(self): val = os.getenv(self.prespawn_count_env) return val if val is None else int(val) - default_kernel_name_env = 'KG_DEFAULT_KERNEL_NAME' - default_kernel_name = Unicode(config=True, - help='Default kernel name when spawning a kernel (KG_DEFAULT_KERNEL_NAME env var)') + default_kernel_name_env = "KG_DEFAULT_KERNEL_NAME" + default_kernel_name = Unicode( + config=True, + help="Default kernel name when spawning a kernel (KG_DEFAULT_KERNEL_NAME env var)", + ) - @default('default_kernel_name') + @default("default_kernel_name") def default_kernel_name_default(self): # defaults to Jupyter's default kernel name on empty string - return os.getenv(self.default_kernel_name_env, '') + return os.getenv(self.default_kernel_name_env, "") - force_kernel_name_env = 'KG_FORCE_KERNEL_NAME' - force_kernel_name = Unicode(config=True, - help='Override any kernel name specified in a notebook or request (KG_FORCE_KERNEL_NAME env var)') + force_kernel_name_env = "KG_FORCE_KERNEL_NAME" + force_kernel_name = Unicode( + config=True, + help="Override any kernel name specified in a notebook or request (KG_FORCE_KERNEL_NAME env var)", + ) - @default('force_kernel_name') + @default("force_kernel_name") def force_kernel_name_default(self): - return os.getenv(self.force_kernel_name_env, '') + return os.getenv(self.force_kernel_name_env, "") - env_process_whitelist_env = 'KG_ENV_PROCESS_WHITELIST' - env_process_whitelist = List(config=True, - help="""Environment variables allowed to be inherited from the spawning process by the kernel""") + env_process_whitelist_env = "KG_ENV_PROCESS_WHITELIST" + env_process_whitelist = List( + config=True, + help="""Environment variables allowed to be inherited from the spawning process by the kernel""", + ) - @default('env_process_whitelist') + @default("env_process_whitelist") def env_process_whitelist_default(self): - return os.getenv(self.env_process_whitelist_env, '').split(',') + return os.getenv(self.env_process_whitelist_env, "").split(",") - api_env = 'KG_API' - api_default_value = 'kernel_gateway.jupyter_websocket' - api = Unicode(api_default_value, + api_env = "KG_API" + api_default_value = "kernel_gateway.jupyter_websocket" + api = Unicode( + api_default_value, config=True, help="""Controls which API to expose, that of a Jupyter notebook server, the seed notebook's, or one provided by another module, respectively using values 'kernel_gateway.jupyter_websocket', 'kernel_gateway.notebook_http', or another fully qualified module name (KG_API env var) - """ + """, ) - @default('api') + @default("api") def api_default(self): return os.getenv(self.api_env, self.api_default_value) - @observe('api') + @observe("api") def api_changed(self, event): try: - self._load_api_module(event['new']) + self._load_api_module(event["new"]) except ImportError: # re-raise with more sensible message to help the user - raise ImportError('API module {} not found'.format(event['new'])) + raise ImportError("API module {} not found".format(event["new"])) - certfile_env = 'KG_CERTFILE' - certfile = Unicode(None, config=True, allow_none=True, - help="""The full path to an SSL/TLS certificate file. (KG_CERTFILE env var)""") + certfile_env = "KG_CERTFILE" + certfile = Unicode( + None, + config=True, + allow_none=True, + help="""The full path to an SSL/TLS certificate file. (KG_CERTFILE env var)""", + ) - @default('certfile') + @default("certfile") def certfile_default(self): return os.getenv(self.certfile_env) - keyfile_env = 'KG_KEYFILE' - keyfile = Unicode(None, config=True, allow_none=True, - help="""The full path to a private key file for usage with SSL/TLS. (KG_KEYFILE env var)""") + keyfile_env = "KG_KEYFILE" + keyfile = Unicode( + None, + config=True, + allow_none=True, + help="""The full path to a private key file for usage with SSL/TLS. (KG_KEYFILE env var)""", + ) - @default('keyfile') + @default("keyfile") def keyfile_default(self): return os.getenv(self.keyfile_env) - client_ca_env = 'KG_CLIENT_CA' - client_ca = Unicode(None, config=True, allow_none=True, - help="""The full path to a certificate authority certificate for SSL/TLS client authentication. (KG_CLIENT_CA env var)""") + client_ca_env = "KG_CLIENT_CA" + client_ca = Unicode( + None, + config=True, + allow_none=True, + help="""The full path to a certificate authority certificate for SSL/TLS client authentication. (KG_CLIENT_CA env var)""", + ) - @default('client_ca') + @default("client_ca") def client_ca_default(self): return os.getenv(self.client_ca_env) - ssl_version_env = 'KG_SSL_VERSION' + ssl_version_env = "KG_SSL_VERSION" ssl_version_default_value = ssl.PROTOCOL_TLSv1_2 - ssl_version = Integer(None, config=True, allow_none=True, - help="""Sets the SSL version to use for the web socket connection. (KG_SSL_VERSION env var)""") - - @default('ssl_version') + ssl_version = Integer( + None, + config=True, + allow_none=True, + help="""Sets the SSL version to use for the web socket connection. (KG_SSL_VERSION env var)""", + ) + + @default("ssl_version") def ssl_version_default(self): ssl_from_env = os.getenv(self.ssl_version_env) return ssl_from_env if ssl_from_env is None else int(ssl_from_env) @@ -390,14 +431,14 @@ def _default_log_format(self) -> str: help=""" The kernel spec manager class to use. Should be a subclass of `jupyter_client.kernelspec.KernelSpecManager`. - """ + """, ) kernel_manager_class = Type( klass=MappingKernelManager, default_value=SeedingMappingKernelManager, config=True, - help="""The kernel manager class to use.""" + help="""The kernel manager class to use.""", ) kernel_websocket_connection_class = Type( @@ -435,10 +476,10 @@ def _load_api_module(self, module_name): Module with the given name loaded using importlib.import_module """ # some compatibility allowances - if module_name == 'jupyter-websocket': - module_name = 'kernel_gateway.jupyter_websocket' - elif module_name == 'notebook-http': - module_name = 'kernel_gateway.notebook_http' + if module_name == "jupyter-websocket": + module_name = "kernel_gateway.jupyter_websocket" + elif module_name == "notebook-http": + module_name = "kernel_gateway.notebook_http" return importlib.import_module(module_name) def _load_notebook(self, uri): @@ -456,26 +497,34 @@ def _load_notebook(self, uri): """ parts = urlparse(uri) - if parts.scheme not in ('http', 'https'): + if parts.scheme not in ("http", "https"): # Local file - path = parts._replace(scheme='', netloc='').geturl() + path = parts._replace(scheme="", netloc="").geturl() with open(path) as nb_fh: notebook = nbformat.read(nb_fh, 4) else: # Remote file import requests + resp = requests.get(uri) resp.raise_for_status() notebook = nbformat.reads(resp.text, 4) # Error if no kernel spec can handle the language requested - kernel_name = self.force_kernel_name if self.force_kernel_name \ - else notebook['metadata']['kernelspec']['name'] + kernel_name = ( + self.force_kernel_name + if self.force_kernel_name + else notebook["metadata"]["kernelspec"]["name"] + ) self.kernel_spec_manager.get_kernel_spec(kernel_name) return notebook - def initialize(self, argv=None, new_httpserver=True,): + def initialize( + self, + argv=None, + new_httpserver=True, + ): """Initializes the base class, configurable manager instances, the Tornado web app, and the tornado HTTP server. @@ -517,7 +566,7 @@ def init_configurables(self): # adopt whatever default the kernel manager wants to use. kwargs = {} if self.default_kernel_name: - kwargs['default_kernel_name'] = self.default_kernel_name + kwargs["default_kernel_name"] = self.default_kernel_name self.kernel_spec_manager = self.kernel_spec_manager_class( parent=self, @@ -527,13 +576,10 @@ def init_configurables(self): log=self.log, connection_dir=self.runtime_dir, kernel_spec_manager=self.kernel_spec_manager, - **kwargs + **kwargs, ) - self.session_manager = SessionManager( - log=self.log, - kernel_manager=self.kernel_manager - ) + self.session_manager = SessionManager(log=self.log, kernel_manager=self.kernel_manager) self.contents_manager = None self.identity_provider = self.identity_provider_class(parent=self, log=self.log) @@ -548,10 +594,12 @@ def init_configurables(self): raise RuntimeError(msg) api_module = self._load_api_module(self.api) - func = getattr(api_module, 'create_personality') + func = getattr(api_module, "create_personality") self.personality = func(parent=self, log=self.log) - self.io_loop.call_later(0.1, lambda: asyncio.create_task(self.personality.init_configurables())) + self.io_loop.call_later( + 0.1, lambda: asyncio.create_task(self.personality.init_configurables()) + ) def init_webapp(self): """Initializes Tornado web application with uri handlers. @@ -604,12 +652,15 @@ def init_webapp(self): # promote the current personality's "config" tagged traitlet values to webapp settings for trait_name, trait_value in self.personality.class_traits(config=True).items(): - kg_name = 'kg_' + trait_name + kg_name = "kg_" + trait_name # a personality's traitlets may not overwrite the kernel gateway's if kg_name not in self.web_app.settings: self.web_app.settings[kg_name] = trait_value.get(obj=self.personality) else: - self.log.warning('The personality trait name, %s, conflicts with a kernel gateway trait.', trait_name) + self.log.warning( + "The personality trait name, %s, conflicts with a kernel gateway trait.", + trait_name, + ) def _build_ssl_options(self): """Build a dictionary of SSL options for the tornado HTTP server. @@ -618,20 +669,20 @@ def _build_ssl_options(self): """ ssl_options = {} if self.certfile: - ssl_options['certfile'] = self.certfile + ssl_options["certfile"] = self.certfile if self.keyfile: - ssl_options['keyfile'] = self.keyfile + ssl_options["keyfile"] = self.keyfile if self.client_ca: - ssl_options['ca_certs'] = self.client_ca + ssl_options["ca_certs"] = self.client_ca if self.ssl_version: - ssl_options['ssl_version'] = self.ssl_version + ssl_options["ssl_version"] = self.ssl_version if not ssl_options: # None indicates no SSL config ssl_options = None else: - ssl_options.setdefault('ssl_version', self.ssl_version_default_value) - if ssl_options.get('ca_certs', False): - ssl_options.setdefault('cert_reqs', ssl.CERT_REQUIRED) + ssl_options.setdefault("ssl_version", self.ssl_version_default_value) + if ssl_options.get("ca_certs", False): + ssl_options.setdefault("cert_reqs", ssl.CERT_REQUIRED) return ssl_options @@ -643,18 +694,18 @@ def init_http_server(self): the same logic as the Jupyer Notebook server. """ ssl_options = self._build_ssl_options() - self.http_server = httpserver.HTTPServer(self.web_app, - xheaders=self.trust_xheaders, - ssl_options=ssl_options) + self.http_server = httpserver.HTTPServer( + self.web_app, xheaders=self.trust_xheaders, ssl_options=ssl_options + ) - for port in random_ports(self.port, self.port_retries+1): + for port in random_ports(self.port, self.port_retries + 1): try: self.http_server.listen(port, self.ip) except socket.error as e: if e.errno == errno.EADDRINUSE: - self.log.info('The port %i is already in use, trying another port.' % port) + self.log.info("The port %i is already in use, trying another port." % port) continue - elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)): + elif e.errno in (errno.EACCES, getattr(errno, "WSAEACCES", errno.EACCES)): self.log.warning("Permission to listen on port %i denied" % port) continue else: @@ -663,8 +714,10 @@ def init_http_server(self): self.port = port break else: - self.log.critical('ERROR: the notebook server could not be started because ' - 'no available port could be found.') + self.log.critical( + "ERROR: the notebook server could not be started because " + "no available port could be found." + ) self.exit(1) def init_io_loop(self): @@ -737,18 +790,20 @@ def _signal_stop(self, sig, frame): self.stop(from_signal=True) def start_app(self): - """Starts the application (with ioloop to follow). """ + """Starts the application (with ioloop to follow).""" super().start() - self.log.info('Jupyter Kernel Gateway {} is available at http{}://{}:{}'.format( - KernelGatewayApp.version, 's' if self.keyfile else '', self.ip, self.port - )) + self.log.info( + "Jupyter Kernel Gateway {} is available at http{}://{}:{}".format( + KernelGatewayApp.version, "s" if self.keyfile else "", self.ip, self.port + ) + ) def start(self): """Starts an IO loop for the application.""" self.start_app() - if sys.platform != 'win32': + if sys.platform != "win32": signal.signal(signal.SIGHUP, signal.SIG_IGN) signal.signal(signal.SIGTERM, self._signal_stop) diff --git a/kernel_gateway/jupyter_websocket/__init__.py b/kernel_gateway/jupyter_websocket/__init__.py index ec87d5d..9ff8d8b 100644 --- a/kernel_gateway/jupyter_websocket/__init__.py +++ b/kernel_gateway/jupyter_websocket/__init__.py @@ -19,25 +19,28 @@ class JupyterWebsocketPersonality(LoggingConfigurable): endpoints that are part of the Jupyter Kernel Gateway API """ - list_kernels_env = 'KG_LIST_KERNELS' - list_kernels = Bool(config=True, + list_kernels_env = "KG_LIST_KERNELS" + list_kernels = Bool( + config=True, help="""Permits listing of the running kernels using API endpoints /api/kernels and /api/sessions (KG_LIST_KERNELS env var). Note: Jupyter Notebook - allows this by default but kernel gateway does not.""" + allows this by default but kernel gateway does not.""", ) - @default('list_kernels') + @default("list_kernels") def list_kernels_default(self): - return os.getenv(self.list_kernels_env, 'False') == 'True' + return os.getenv(self.list_kernels_env, "False") == "True" - env_whitelist_env = 'KG_ENV_WHITELIST' - env_whitelist = List(config=True, - help="""Environment variables allowed to be set when - a client requests a new kernel""") + env_whitelist_env = "KG_ENV_WHITELIST" + env_whitelist = List( + config=True, + help="""Environment variables allowed to be set when + a client requests a new kernel""", + ) - @default('env_whitelist') + @default("env_whitelist") def env_whitelist_default(self): - return os.getenv(self.env_whitelist_env, '').split(',') + return os.getenv(self.env_whitelist_env, "").split(",") kernel_pool: KernelPool @@ -46,10 +49,7 @@ def __init__(self, *args, **kwargs): self.kernel_pool = KernelPool() async def init_configurables(self): - await self.kernel_pool.initialize( - self.parent.prespawn_count, - self.parent.kernel_manager - ) + await self.kernel_pool.initialize(self.parent.prespawn_count, self.parent.kernel_manager) def create_request_handlers(self): """Create default Jupyter handlers and redefine them off of the @@ -59,14 +59,14 @@ def create_request_handlers(self): # append tuples for the standard kernel gateway endpoints for handler in ( - default_api_handlers + - default_kernel_handlers + - default_kernelspec_handlers + - default_session_handlers + - default_base_handlers + default_api_handlers + + default_kernel_handlers + + default_kernelspec_handlers + + default_session_handlers + + default_base_handlers ): # Create a new handler pattern rooted at the base_url - pattern = url_path_join('/', self.parent.base_url, handler[0]) + pattern = url_path_join("/", self.parent.base_url, handler[0]) # Some handlers take args, so retain those in addition to the # handler class ref new_handler = tuple([pattern] + list(handler[1:])) diff --git a/kernel_gateway/jupyter_websocket/handlers.py b/kernel_gateway/jupyter_websocket/handlers.py index 506d3c1..bce9787 100644 --- a/kernel_gateway/jupyter_websocket/handlers.py +++ b/kernel_gateway/jupyter_websocket/handlers.py @@ -9,10 +9,10 @@ class BaseSpecHandler(CORSMixin, web.StaticFileHandler): """Exposes the ability to return specifications from static files""" + @staticmethod def get_resource_metadata(): - """Returns the (resource, mime-type) for the handlers spec. - """ + """Returns the (resource, mime-type) for the handlers spec.""" pass def initialize(self): @@ -24,10 +24,9 @@ def initialize(self): web.StaticFileHandler.initialize(self, path=os.path.dirname(__file__)) async def get(self): - """Handler for a get on a specific handler - """ + """Handler for a get on a specific handler""" resource_name, content_type = self.get_resource_metadata() - self.set_header('Content-Type', content_type) + self.set_header("Content-Type", content_type) res = await web.StaticFileHandler.get(self, resource_name) return res @@ -38,19 +37,21 @@ def options(self, **kwargs): class SpecJsonHandler(BaseSpecHandler): """Exposes a JSON swagger specification""" + @staticmethod def get_resource_metadata(): - return 'swagger.json','application/json' + return "swagger.json", "application/json" class APIYamlHandler(BaseSpecHandler): """Exposes a YAML swagger specification""" + @staticmethod def get_resource_metadata(): - return 'swagger.yaml', 'text/x-yaml' + return "swagger.yaml", "text/x-yaml" default_handlers = [ - ('/api/{}'.format(SpecJsonHandler.get_resource_metadata()[0]), SpecJsonHandler), - ('/api/{}'.format(APIYamlHandler.get_resource_metadata()[0]), APIYamlHandler) + ("/api/{}".format(SpecJsonHandler.get_resource_metadata()[0]), SpecJsonHandler), + ("/api/{}".format(APIYamlHandler.get_resource_metadata()[0]), APIYamlHandler), ] diff --git a/kernel_gateway/jupyter_websocket/swagger.json b/kernel_gateway/jupyter_websocket/swagger.json index 71a9f9b..a011861 100644 --- a/kernel_gateway/jupyter_websocket/swagger.json +++ b/kernel_gateway/jupyter_websocket/swagger.json @@ -9,12 +9,8 @@ "url": "https://jupyter.org" } }, - "produces": [ - "application/json" - ], - "consumes": [ - "application/json" - ], + "produces": ["application/json"], + "consumes": ["application/json"], "parameters": { "kernel": { "name": "kernel_id", @@ -59,9 +55,7 @@ "/api": { "get": { "summary": "Get API info", - "tags": [ - "api" - ], + "tags": ["api"], "responses": { "200": { "description": "Returns information about the API", @@ -74,13 +68,9 @@ }, "/api/swagger.yaml": { "get": { - "produces": [ - "text/x-yaml" - ], + "produces": ["text/x-yaml"], "summary": "Get API info", - "tags": [ - "api" - ], + "tags": ["api"], "responses": { "200": { "description": "Returns a swagger specification in yaml" @@ -91,9 +81,7 @@ "/api/swagger.json": { "get": { "summary": "Get API info", - "tags": [ - "api" - ], + "tags": ["api"], "responses": { "200": { "description": "Returns a swagger specification in json" @@ -104,9 +92,7 @@ "/api/kernelspecs": { "get": { "summary": "Get kernel specs", - "tags": [ - "kernelspecs" - ], + "tags": ["kernelspecs"], "responses": { "200": { "description": "Returns the available kernel specs to spawn new kernels.", @@ -132,9 +118,7 @@ "/api/kernels": { "get": { "summary": "List the JSON data for all currently running kernels", - "tags": [ - "kernels" - ], + "tags": ["kernels"], "responses": { "200": { "description": "List of running kernels", @@ -155,9 +139,7 @@ }, "post": { "summary": "Start a kernel and return the uuid", - "tags": [ - "kernels" - ], + "tags": ["kernels"], "parameters": [ { "name": "name", @@ -204,9 +186,7 @@ ], "get": { "summary": "Get kernel information", - "tags": [ - "kernels" - ], + "tags": ["kernels"], "responses": { "200": { "description": "Information about the kernel", @@ -218,9 +198,7 @@ }, "delete": { "summary": "Kill a kernel and delete the kernel id", - "tags": [ - "kernels" - ], + "tags": ["kernels"], "responses": { "204": { "description": "Kernel deleted" @@ -236,9 +214,7 @@ ], "get": { "summary": "Upgrades the connection to a websocket connection.", - "tags": [ - "channels" - ], + "tags": ["channels"], "responses": { "200": { "description": "The connection will be upgraded to a websocket." @@ -254,9 +230,7 @@ ], "post": { "summary": "Interrupt a kernel", - "tags": [ - "kernels" - ], + "tags": ["kernels"], "responses": { "204": { "description": "Kernel interrupted" @@ -272,9 +246,7 @@ ], "post": { "summary": "Restart a kernel", - "tags": [ - "kernels" - ], + "tags": ["kernels"], "responses": { "200": { "description": "Kernel interrupted", @@ -295,9 +267,7 @@ "/api/sessions": { "get": { "summary": "List available sessions", - "tags": [ - "sessions" - ], + "tags": ["sessions"], "responses": { "200": { "description": "List of current sessions", @@ -318,9 +288,7 @@ }, "post": { "summary": "Create a new session, or return an existing session if a session of the same name already exists.", - "tags": [ - "sessions" - ], + "tags": ["sessions"], "parameters": [ { "name": "session", @@ -361,9 +329,7 @@ ], "get": { "summary": "Get session", - "tags": [ - "sessions" - ], + "tags": ["sessions"], "responses": { "200": { "description": "Session", @@ -375,9 +341,7 @@ }, "patch": { "summary": "This can be used to rename the session.", - "tags": [ - "sessions" - ], + "tags": ["sessions"], "parameters": [ { "name": "model", @@ -405,9 +369,7 @@ }, "delete": { "summary": "Delete a session", - "tags": [ - "sessions" - ], + "tags": ["sessions"], "responses": { "204": { "description": "Session (and kernel) were deleted" @@ -469,11 +431,7 @@ }, "KernelSpecFile": { "description": "Kernel spec json file", - "required": [ - "argv", - "display_name", - "language" - ], + "required": ["argv", "display_name", "language"], "properties": { "language": { "type": "string", @@ -506,10 +464,7 @@ "description": "Help items to be displayed in the help menu in the notebook UI.", "items": { "type": "object", - "required": [ - "text", - "url" - ], + "required": ["text", "url"], "properties": { "text": { "type": "string", @@ -527,10 +482,7 @@ }, "Kernel": { "description": "Kernel information", - "required": [ - "id", - "name" - ], + "required": ["id", "name"], "properties": { "id": { "type": "string", diff --git a/kernel_gateway/jupyter_websocket/swagger.yaml b/kernel_gateway/jupyter_websocket/swagger.yaml index cff9606..3d90e41 100644 --- a/kernel_gateway/jupyter_websocket/swagger.yaml +++ b/kernel_gateway/jupyter_websocket/swagger.yaml @@ -1,4 +1,4 @@ -swagger: '2.0' +swagger: "2.0" info: title: Jupyter Kernel Gateway API @@ -60,7 +60,7 @@ paths: 200: description: Returns information about the API schema: - $ref: '#/definitions/ApiInfo' + $ref: "#/definitions/ApiInfo" /api/swagger.yaml: get: produces: @@ -96,7 +96,7 @@ paths: kernelspecs: type: object additionalProperties: - $ref: '#/definitions/KernelSpec' + $ref: "#/definitions/KernelSpec" /api/kernels: get: summary: List the JSON data for all currently running kernels @@ -108,12 +108,12 @@ paths: schema: type: array items: - $ref: '#/definitions/Kernel' + $ref: "#/definitions/Kernel" 403: description: This method is not accessible when - `KernelGatewayApp.list_kernels` is `False`. + `KernelGatewayApp.list_kernels` is `False`. schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: summary: Start a kernel and return the uuid tags: @@ -121,7 +121,7 @@ paths: parameters: - name: name in: body - description: Kernel spec name (defaults to default kernel spec for + description: Kernel spec name (defaults to default kernel spec for server) schema: type: object @@ -132,7 +132,7 @@ paths: 201: description: The metadata about the newly created kernel. schema: - $ref: '#/definitions/Kernel' + $ref: "#/definitions/Kernel" headers: Location: description: Model for started kernel @@ -141,10 +141,10 @@ paths: 403: description: The maximum number of kernels have been created. schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/kernels/{kernel_id}: parameters: - - $ref: '#/parameters/kernel' + - $ref: "#/parameters/kernel" get: summary: Get kernel information tags: @@ -153,7 +153,7 @@ paths: 200: description: Information about the kernel schema: - $ref: '#/definitions/Kernel' + $ref: "#/definitions/Kernel" delete: summary: Kill a kernel and delete the kernel id tags: @@ -163,7 +163,7 @@ paths: description: Kernel deleted /api/kernels/{kernel_id}/channels: parameters: - - $ref: '#/parameters/kernel' + - $ref: "#/parameters/kernel" get: summary: Upgrades the connection to a websocket connection. tags: @@ -173,7 +173,7 @@ paths: description: The connection will be upgraded to a websocket. /kernels/{kernel_id}/interrupt: parameters: - - $ref: '#/parameters/kernel' + - $ref: "#/parameters/kernel" post: summary: Interrupt a kernel tags: @@ -183,7 +183,7 @@ paths: description: Kernel interrupted /kernels/{kernel_id}/restart: parameters: - - $ref: '#/parameters/kernel' + - $ref: "#/parameters/kernel" post: summary: Restart a kernel tags: @@ -197,7 +197,7 @@ paths: type: string format: url schema: - $ref: '#/definitions/Kernel' + $ref: "#/definitions/Kernel" /api/sessions: get: summary: List available sessions @@ -209,12 +209,12 @@ paths: schema: type: array items: - $ref: '#/definitions/Session' + $ref: "#/definitions/Session" 403: description: This method is not accessible when the kernel gateway when the `list_kernels` option is `False`. schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: summary: Create a new session, or return an existing session if a session of the same name already exists. @@ -224,12 +224,12 @@ paths: - name: session in: body schema: - $ref: '#/definitions/Session' + $ref: "#/definitions/Session" responses: 201: description: Session created or returned schema: - $ref: '#/definitions/Session' + $ref: "#/definitions/Session" headers: Location: description: URL for session commands @@ -238,11 +238,11 @@ paths: 501: description: Session not available schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/sessions/{session}: parameters: - - $ref: '#/parameters/session' + - $ref: "#/parameters/session" get: summary: Get session tags: @@ -251,7 +251,7 @@ paths: 200: description: Session schema: - $ref: '#/definitions/Session' + $ref: "#/definitions/Session" patch: summary: This can be used to rename the session. tags: @@ -261,16 +261,16 @@ paths: in: body required: true schema: - $ref: '#/definitions/Session' + $ref: "#/definitions/Session" responses: 200: description: Session schema: - $ref: '#/definitions/Session' + $ref: "#/definitions/Session" 400: description: No data provided schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: summary: Delete a session tags: @@ -300,7 +300,7 @@ definitions: type: string description: Unique name for kernel KernelSpecFile: - $ref: '#/definitions/KernelSpecFile' + $ref: "#/definitions/KernelSpecFile" description: Kernel spec json file resources: type: object @@ -350,8 +350,8 @@ definitions: items: type: object required: - - text - - url + - text + - url properties: text: type: string @@ -407,7 +407,7 @@ definitions: type: string description: session type kernel: - $ref: '#/definitions/Kernel' + $ref: "#/definitions/Kernel" ApiInfo: description: Information about the api type: object diff --git a/kernel_gateway/mixins.py b/kernel_gateway/mixins.py index 2b02469..2c2d4b0 100644 --- a/kernel_gateway/mixins.py +++ b/kernel_gateway/mixins.py @@ -10,13 +10,14 @@ class CORSMixin(object): """Mixes CORS headers into tornado.web.RequestHandlers.""" + SETTINGS_TO_HEADERS = { - 'kg_allow_credentials': 'Access-Control-Allow-Credentials', - 'kg_allow_headers': 'Access-Control-Allow-Headers', - 'kg_allow_methods': 'Access-Control-Allow-Methods', - 'kg_allow_origin': 'Access-Control-Allow-Origin', - 'kg_expose_headers': 'Access-Control-Expose-Headers', - 'kg_max_age': 'Access-Control-Max-Age' + "kg_allow_credentials": "Access-Control-Allow-Credentials", + "kg_allow_headers": "Access-Control-Allow-Headers", + "kg_allow_methods": "Access-Control-Allow-Methods", + "kg_allow_origin": "Access-Control-Allow-Origin", + "kg_expose_headers": "Access-Control-Expose-Headers", + "kg_max_age": "Access-Control-Max-Age", } def set_cors_headers(self): @@ -41,7 +42,7 @@ def set_cors_headers(self): self.set_header(header_name, header_value) # Don't set CSP: we're not serving frontend media types only JSON - self.clear_header('Content-Security-Policy') + self.clear_header("Content-Security-Policy") def options(self, *args, **kwargs): """Override the notebook implementation to return the headers @@ -55,6 +56,7 @@ class TokenAuthorizationMixin(object): """Mixes token auth into tornado.web.RequestHandlers and tornado.websocket.WebsocketHandlers. """ + header_prefix = "token " header_prefix_len = len(header_prefix) @@ -72,13 +74,13 @@ def prepare(self): with the `@web.authenticated` decorated methods in the notebook package. """ - server_token = self.settings.get('kg_auth_token') - if server_token and not self.request.method == 'OPTIONS': - client_token = self.get_argument('token', None) + server_token = self.settings.get("kg_auth_token") + if server_token and not self.request.method == "OPTIONS": + client_token = self.get_argument("token", None) if client_token is None: - client_token = self.request.headers.get('Authorization') + client_token = self.request.headers.get("Authorization") if client_token and client_token.startswith(self.header_prefix): - client_token = client_token[self.header_prefix_len:] + client_token = client_token[self.header_prefix_len :] else: client_token = None if client_token != server_token: @@ -90,11 +92,12 @@ class JSONErrorsMixin(object): """Mixes `write_error` into tornado.web.RequestHandlers to respond with JSON format errors. """ + def write_error(self, status_code, **kwargs): """Responds with an application/json error object. Overrides the APIHandler.write_error in the notebook server until it - properly sets the 'reason' field. + properly sets the 'reason' field. Parameters ---------- @@ -109,27 +112,27 @@ def write_error(self, status_code, **kwargs): -------- {"401", reason="Unauthorized", message="Invalid auth token"} """ - exc_info = kwargs.get('exc_info') - message = '' - reason = responses.get(status_code, 'Unknown HTTP Error') + exc_info = kwargs.get("exc_info") + message = "" + reason = responses.get(status_code, "Unknown HTTP Error") reply = { - 'reason': reason, - 'message': message, + "reason": reason, + "message": message, } if exc_info: exception = exc_info[1] # Get the custom message, if defined if isinstance(exception, web.HTTPError): - reply['message'] = exception.log_message or message + reply["message"] = exception.log_message or message else: - reply['message'] = 'Unknown server error' - reply['traceback'] = ''.join(traceback.format_exception(*exc_info)) + reply["message"] = "Unknown server error" + reply["traceback"] = "".join(traceback.format_exception(*exc_info)) # Construct the custom reason, if defined - custom_reason = getattr(exception, 'reason', '') + custom_reason = getattr(exception, "reason", "") if custom_reason: - reply['reason'] = custom_reason + reply["reason"] = custom_reason - self.set_header('Content-Type', 'application/json') - self.set_status(status_code, reason=reply['reason']) + self.set_header("Content-Type", "application/json") + self.set_status(status_code, reason=reply["reason"]) self.finish(json.dumps(reply)) diff --git a/kernel_gateway/notebook_http/__init__.py b/kernel_gateway/notebook_http/__init__.py index beedc11..06129f0 100644 --- a/kernel_gateway/notebook_http/__init__.py +++ b/kernel_gateway/notebook_http/__init__.py @@ -19,40 +19,45 @@ class NotebookHTTPPersonality(LoggingConfigurable): """Personality for notebook-http support, creating REST endpoints based on the notebook's annotated cells """ - cell_parser_env = 'KG_CELL_PARSER' - cell_parser = Unicode('kernel_gateway.notebook_http.cell.parser', + + cell_parser_env = "KG_CELL_PARSER" + cell_parser = Unicode( + "kernel_gateway.notebook_http.cell.parser", config=True, help="""Determines which module is used to parse the notebook for endpoints and documentation. Valid module names include 'kernel_gateway.notebook_http.cell.parser' and 'kernel_gateway.notebook_http.swagger.parser'. (KG_CELL_PARSER env var) - """ + """, ) - @default('cell_parser') + @default("cell_parser") def cell_parser_default(self): - return os.getenv(self.cell_parser_env, 'kernel_gateway.notebook_http.cell.parser') + return os.getenv(self.cell_parser_env, "kernel_gateway.notebook_http.cell.parser") # Intentionally not defining an env var option for a dict type - comment_prefix = Dict({ - 'scala': '//', - None: '#' - }, config=True, help='Maps kernel language to code comment syntax') - - allow_notebook_download_env = 'KG_ALLOW_NOTEBOOK_DOWNLOAD' - allow_notebook_download = Bool(config=True, - help="Optional API to download the notebook source code in notebook-http mode, defaults to not allow" + comment_prefix = Dict( + {"scala": "//", None: "#"}, config=True, help="Maps kernel language to code comment syntax" + ) + + allow_notebook_download_env = "KG_ALLOW_NOTEBOOK_DOWNLOAD" + allow_notebook_download = Bool( + config=True, + help="Optional API to download the notebook source code in notebook-http mode, defaults to not allow", ) - @default('allow_notebook_download') + @default("allow_notebook_download") def allow_notebook_download_default(self): - return os.getenv(self.allow_notebook_download_env, 'False') == 'True' + return os.getenv(self.allow_notebook_download_env, "False") == "True" - static_path_env = 'KG_STATIC_PATH' - static_path = Unicode(None, config=True, allow_none=True, - help="Serve static files on disk in the given path as /public, defaults to not serve" + static_path_env = "KG_STATIC_PATH" + static_path = Unicode( + None, + config=True, + allow_none=True, + help="Serve static files on disk in the given path as /public, defaults to not serve", ) - @default('static_path') + @default("static_path") def static_path_default(self): return os.getenv(self.static_path_env) @@ -63,23 +68,23 @@ def __init__(self, *args, **kwargs): # Import the module to use for cell endpoint parsing cell_parser_module = importlib.import_module(self.cell_parser) # Build the parser using the comment syntax for the notebook language - func = getattr(cell_parser_module, 'create_parser') + func = getattr(cell_parser_module, "create_parser") try: - kernel_language = self.parent.seed_notebook['metadata']['language_info']['name'] + kernel_language = self.parent.seed_notebook["metadata"]["language_info"]["name"] except (AttributeError, KeyError): kernel_language = None - prefix = self.comment_prefix.get(kernel_language, '#') - self.api_parser = func(parent=self, log=self.log, - comment_prefix=prefix, - notebook_cells=self.parent.seed_notebook.cells) + prefix = self.comment_prefix.get(kernel_language, "#") + self.api_parser = func( + parent=self, + log=self.log, + comment_prefix=prefix, + notebook_cells=self.parent.seed_notebook.cells, + ) self.kernel_language = kernel_language self.kernel_pool = ManagedKernelPool() async def init_configurables(self): - await self.kernel_pool.initialize( - self.parent.prespawn_count, - self.parent.kernel_manager - ) + await self.kernel_pool.initialize(self.parent.prespawn_count, self.parent.kernel_manager) def create_request_handlers(self): """Create handlers and redefine them off of the base_url path. Assumes @@ -89,57 +94,61 @@ def create_request_handlers(self): handlers = [] # Register the NotebookDownloadHandler if configuration allows if self.allow_notebook_download: - path = url_path_join('/', self.parent.base_url, r'/_api/source') - self.log.info('Registering resource: {}, methods: (GET)'.format(path)) - handlers.append(( - path, - NotebookDownloadHandler, - {'path': self.parent.seed_uri} - )) + path = url_path_join("/", self.parent.base_url, r"/_api/source") + self.log.info("Registering resource: {}, methods: (GET)".format(path)) + handlers.append((path, NotebookDownloadHandler, {"path": self.parent.seed_uri})) # Register a static path handler if configuration allows if self.static_path is not None: - path = url_path_join('/', self.parent.base_url, r'/public/(.*)') - self.log.info('Registering resource: {}, methods: (GET)'.format(path)) - handlers.append(( - path, - tornado.web.StaticFileHandler, - {'path': self.static_path} - )) + path = url_path_join("/", self.parent.base_url, r"/public/(.*)") + self.log.info("Registering resource: {}, methods: (GET)".format(path)) + handlers.append((path, tornado.web.StaticFileHandler, {"path": self.static_path})) # Discover the notebook endpoints and their implementations endpoints = self.api_parser.endpoints(self.parent.kernel_manager.seed_source) - response_sources = self.api_parser.endpoint_responses(self.parent.kernel_manager.seed_source) + response_sources = self.api_parser.endpoint_responses( + self.parent.kernel_manager.seed_source + ) if len(endpoints) == 0: - raise RuntimeError('No endpoints were discovered. Check your notebook to make sure your cells are annotated correctly.') + raise RuntimeError( + "No endpoints were discovered. Check your notebook to make sure your cells are annotated correctly." + ) # Cycle through the (endpoint_path, source) tuples and register their handlers for endpoint_path, verb_source_map in endpoints: parameterized_path = parameterize_path(endpoint_path) - parameterized_path = url_path_join('/', self.parent.base_url, parameterized_path) - self.log.info('Registering resource: {}, methods: ({})'.format( - parameterized_path, - list(verb_source_map.keys()) - )) - response_source_map = response_sources[endpoint_path] if endpoint_path in response_sources else {} - handler_args = { 'sources' : verb_source_map, - 'response_sources' : response_source_map, - 'kernel_pool' : self.kernel_pool, - 'kernel_name' : self.parent.kernel_manager.seed_kernelspec, - 'kernel_language' : self.kernel_language or '' + parameterized_path = url_path_join("/", self.parent.base_url, parameterized_path) + self.log.info( + "Registering resource: {}, methods: ({})".format( + parameterized_path, list(verb_source_map.keys()) + ) + ) + response_source_map = ( + response_sources[endpoint_path] if endpoint_path in response_sources else {} + ) + handler_args = { + "sources": verb_source_map, + "response_sources": response_source_map, + "kernel_pool": self.kernel_pool, + "kernel_name": self.parent.kernel_manager.seed_kernelspec, + "kernel_language": self.kernel_language or "", } handlers.append((parameterized_path, NotebookAPIHandler, handler_args)) # Register the swagger API spec handler - path = url_path_join('/', self.parent.base_url, r'/_api/spec/swagger.json') - handlers.append(( - path, - SwaggerSpecHandler, { - 'notebook_path' : self.parent.seed_uri, - 'source_cells': self.parent.seed_notebook.cells, - 'cell_parser' : self.api_parser - })) - self.log.info('Registering resource: {}, methods: (GET)'.format(path)) + path = url_path_join("/", self.parent.base_url, r"/_api/spec/swagger.json") + handlers.append( + ( + path, + SwaggerSpecHandler, + { + "notebook_path": self.parent.seed_uri, + "source_cells": self.parent.seed_notebook.cells, + "cell_parser": self.api_parser, + }, + ) + ) + self.log.info("Registering resource: {}, methods: (GET)".format(path)) # Add the 404 catch-all last handlers.append(default_base_handlers[-1]) @@ -149,7 +158,9 @@ def should_seed_cell(self, code): """Determines whether the given code cell source should be executed when seeding a new kernel.""" # seed cells that are uninvolved with the presented API - return not self.api_parser.is_api_cell(code) and not self.api_parser.is_api_response_cell(code) + return not self.api_parser.is_api_cell(code) and not self.api_parser.is_api_response_cell( + code + ) async def shutdown(self): """Stop all kernels in the pool.""" diff --git a/kernel_gateway/notebook_http/cell/parser.py b/kernel_gateway/notebook_http/cell/parser.py index 056e7fe..3df1a57 100644 --- a/kernel_gateway/notebook_http/cell/parser.py +++ b/kernel_gateway/notebook_http/cell/parser.py @@ -26,16 +26,16 @@ def first_path_param_index(endpoint): Examples -------- - >>> first_path_param_index('/foo/:bar') + >>> first_path_param_index("/foo/:bar") 1 - >>> first_path_param_index('/foo/quo/:bar') + >>> first_path_param_index("/foo/quo/:bar") 2 - >>> first_path_param_index('/foo/quo/bar') + >>> first_path_param_index("/foo/quo/bar") sys.maxsize """ index = sys.maxsize - if endpoint.find(':') >= 0: - index = endpoint.count('/', 0, endpoint.find(':')) - 1 + if endpoint.find(":") >= 0: + index = endpoint.count("/", 0, endpoint.find(":")) - 1 return index @@ -66,13 +66,18 @@ class APICellParser(LoggingConfigurable): api_response_indicator : str Regex pattern for API response metadata annotations """ - api_indicator = Unicode(default_value=r'{}\s+(GET|PUT|POST|DELETE)\s+(\/.*)+') - api_response_indicator = Unicode(default_value=r'{}\s+ResponseInfo\s+(GET|PUT|POST|DELETE)\s+(\/.*)+') + + api_indicator = Unicode(default_value=r"{}\s+(GET|PUT|POST|DELETE)\s+(\/.*)+") + api_response_indicator = Unicode( + default_value=r"{}\s+ResponseInfo\s+(GET|PUT|POST|DELETE)\s+(\/.*)+" + ) def __init__(self, comment_prefix, notebook_cells=None, **kwargs): super().__init__(**kwargs) self.kernelspec_api_indicator = re.compile(self.api_indicator.format(comment_prefix)) - self.kernelspec_api_response_indicator = re.compile(self.api_response_indicator.format(comment_prefix)) + self.kernelspec_api_response_indicator = re.compile( + self.api_response_indicator.format(comment_prefix) + ) def is_api_cell(self, cell_source): """Gets if the cell source is annotated as an API endpoint. @@ -143,11 +148,7 @@ def get_path_content(self, cell_source): Object describing the supported operation, at minimum, guidance for the eventual response output. """ - return { - 'responses': { - 200: {'description': 'Success'} - } - } + return {"responses": {200: {"description": "Success"}}} def endpoints(self, source_cells, sort_func=first_path_param_index): """Gets the list of all annotated endpoint HTTP paths and verbs. @@ -173,8 +174,8 @@ def endpoints(self, source_cells, sort_func=first_path_param_index): uri = matched.group(2).strip() verb = matched.group(1) - endpoints.setdefault(uri, {}).setdefault(verb, '') - endpoints[uri][verb] += cell_source + '\n' + endpoints.setdefault(uri, {}).setdefault(verb, "") + endpoints[uri][verb] += cell_source + "\n" sorted_keys = sorted(endpoints, key=sort_func, reverse=True) return [(key, endpoints[key]) for key in sorted_keys] @@ -203,8 +204,8 @@ def endpoint_responses(self, source_cells, sort_func=first_path_param_index): uri = matched.group(2).strip() verb = matched.group(1) - endpoints.setdefault(uri, {}).setdefault(verb, '') - endpoints[uri][verb] += cell_source + '\n' + endpoints.setdefault(uri, {}).setdefault(verb, "") + endpoints[uri][verb] += cell_source + "\n" return endpoints def get_default_api_spec(self): @@ -214,7 +215,7 @@ def get_default_api_spec(self): dictionary Dictionary with a root "swagger" property """ - return {'swagger': '2.0', 'paths': {}, 'info': {'version': '0.0.0'}} + return {"swagger": "2.0", "paths": {}, "info": {"version": "0.0.0"}} def create_parser(*args, **kwargs): diff --git a/kernel_gateway/notebook_http/errors.py b/kernel_gateway/notebook_http/errors.py index 54519c3..c075c76 100644 --- a/kernel_gateway/notebook_http/errors.py +++ b/kernel_gateway/notebook_http/errors.py @@ -7,6 +7,7 @@ class CodeExecutionError(Exception): """Raised when a notebook's code fails to execute in response to an API request. """ + pass @@ -14,4 +15,5 @@ class UnsupportedMethodError(Exception): """Raised when a notebook-defined API does not support the requested HTTP method. """ + pass diff --git a/kernel_gateway/notebook_http/handlers.py b/kernel_gateway/notebook_http/handlers.py index 341034c..507ad95 100644 --- a/kernel_gateway/notebook_http/handlers.py +++ b/kernel_gateway/notebook_http/handlers.py @@ -7,17 +7,22 @@ import tornado.web from tornado.log import access_log from typing import Optional -from .request_utils import parse_body, parse_args, format_request, headers_to_dict, parameterize_path +from .request_utils import ( + parse_body, + parse_args, + format_request, + headers_to_dict, + parameterize_path, +) from tornado.concurrent import Future from ..mixins import TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin from functools import partial from .errors import UnsupportedMethodError, CodeExecutionError -class NotebookAPIHandler(TokenAuthorizationMixin, - CORSMixin, - JSONErrorsMixin, - tornado.web.RequestHandler): +class NotebookAPIHandler( + TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin, tornado.web.RequestHandler +): """Executes code from a notebook cell in response to HTTP requests at the route registered in association with this class. @@ -46,7 +51,8 @@ class NotebookAPIHandler(TokenAuthorizationMixin, services.cell.parser.APICellParser for detail about how the source cells are identified, parsed, and associated with HTTP verbs and paths. """ - def initialize(self, sources, response_sources, kernel_pool, kernel_name, kernel_language=''): + + def initialize(self, sources, response_sources, kernel_pool, kernel_name, kernel_language=""): self.kernel_pool = kernel_pool self.sources = sources self.kernel_name = kernel_name @@ -74,15 +80,15 @@ def finish_future(self, future, result_accumulator): Dictionary of results from a kernel with at least the keys error, stream, and result """ - if result_accumulator['error']: - future.set_exception(CodeExecutionError(result_accumulator['error'])) - elif len(result_accumulator['stream']) > 0: - future.set_result(''.join(result_accumulator['stream'])) - elif result_accumulator['result']: - future.set_result(json.dumps(result_accumulator['result'])) + if result_accumulator["error"]: + future.set_exception(CodeExecutionError(result_accumulator["error"])) + elif len(result_accumulator["stream"]) > 0: + future.set_result("".join(result_accumulator["stream"])) + elif result_accumulator["result"]: + future.set_result(json.dumps(result_accumulator["result"])) else: # If nothing was set, return an empty value - future.set_result('') + future.set_result("") def on_recv(self, result_accumulator, future, parent_header, msg): """Collects ipoub messages associated with code execution request @@ -105,24 +111,27 @@ def on_recv(self, result_accumulator, future, parent_header, msg): msg : dict Kernel message received from the iopub channel """ - if msg['parent_header']['msg_id'] == parent_header: + if msg["parent_header"]["msg_id"] == parent_header: # On idle status, exit our loop - if msg['header']['msg_type'] == 'status' and msg['content']['execution_state'] == 'idle': + if ( + msg["header"]["msg_type"] == "status" + and msg["content"]["execution_state"] == "idle" + ): self.finish_future(future, result_accumulator) # Store the execute result - elif msg['header']['msg_type'] == 'execute_result': - result_accumulator['result'] = msg['content']['data'] + elif msg["header"]["msg_type"] == "execute_result": + result_accumulator["result"] = msg["content"]["data"] # Accumulate the stream messages - elif msg['header']['msg_type'] == 'stream': + elif msg["header"]["msg_type"] == "stream": # Only take stream output if it is on stdout or if the kernel # is non-confirming and does not name the stream - if 'name' not in msg['content'] or msg['content']['name'] == 'stdout': - result_accumulator['stream'].append((msg['content']['text'])) + if "name" not in msg["content"] or msg["content"]["name"] == "stdout": + result_accumulator["stream"].append((msg["content"]["text"])) # Store the error message - elif msg['header']['msg_type'] == 'error': - error_name = msg['content']['ename'] - error_value = msg['content']['evalue'] - result_accumulator['error'] = 'Error {}: {} \n'.format(error_name, error_value) + elif msg["header"]["msg_type"] == "error": + error_name = msg["content"]["ename"] + error_value = msg["content"]["evalue"] + result_accumulator["error"] = "Error {}: {} \n".format(error_name, error_value) def execute_code(self, kernel_client, kernel_id, source_code): """Executes `source_code` on the kernel specified. @@ -151,7 +160,7 @@ def execute_code(self, kernel_client, kernel_id, source_code): If the kernel returns any error """ future = Future() - result_accumulator = {'stream': [], 'error': None, 'result': None} + result_accumulator = {"stream": [], "error": None, "result": None} parent_header = kernel_client.execute(source_code) on_recv_func = partial(self.on_recv, result_accumulator, future, parent_header) self.kernel_pool.on_recv(kernel_id, on_recv_func) @@ -172,23 +181,25 @@ async def _handle_request(self): raise UnsupportedMethodError(self.request.method) # Set the Content-Type and status to default values - self.set_header('Content-Type', 'text/plain') + self.set_header("Content-Type", "text/plain") self.set_status(200) # Get the source to execute in response to this request source_code = self.sources[self.request.method] # Build the request dictionary - request = json.dumps({ - 'body': parse_body(self.request), - 'args': parse_args(self.request.query_arguments), - 'path': self.path_kwargs, - 'headers': headers_to_dict(self.request.headers) - }) + request = json.dumps( + { + "body": parse_body(self.request), + "args": parse_args(self.request.query_arguments), + "path": self.path_kwargs, + "headers": headers_to_dict(self.request.headers), + } + ) # Turn the request string into a valid code string request_code = format_request(request, self.kernel_language) # Run the request and source code and yield until there's a result - access_log.debug('Request code for notebook cell is: {}'.format(request_code)) + access_log.debug("Request code for notebook cell is: {}".format(request_code)) await self.execute_code(kernel_client, kernel_id, request_code) source_result = await self.execute_code(kernel_client, kernel_id, source_code) @@ -202,13 +213,13 @@ async def _handle_request(self): response = json.loads(response_result) # Copy all the header values into the tornado response - if 'headers' in response: - for header in response['headers']: - self.set_header(header, response['headers'][header]) + if "headers" in response: + for header in response["headers"]: + self.set_header(header, response["headers"][header]) # Set the status code if it exists - if 'status' in response: - self.set_status(response['status']) + if "status" in response: + self.set_status(response["status"]) # Write the result of the source code execution if source_result: @@ -246,12 +257,11 @@ def options(self, **kwargs): self.finish() -class NotebookDownloadHandler(TokenAuthorizationMixin, - CORSMixin, - JSONErrorsMixin, - tornado.web.StaticFileHandler): - """Handles requests to download the annotated notebook behind the web API. - """ +class NotebookDownloadHandler( + TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin, tornado.web.StaticFileHandler +): + """Handles requests to download the annotated notebook behind the web API.""" + def initialize(self, path: str, default_filename: Optional[str] = None): self.dirname, self.filename = os.path.split(path) super(NotebookDownloadHandler, self).initialize(self.dirname) diff --git a/kernel_gateway/notebook_http/swagger/builders.py b/kernel_gateway/notebook_http/swagger/builders.py index 16c5787..daedfa8 100644 --- a/kernel_gateway/notebook_http/swagger/builders.py +++ b/kernel_gateway/notebook_http/swagger/builders.py @@ -20,6 +20,7 @@ class SwaggerSpecBuilder(object): value : dict Python object representation of the Swagger spec """ + def __init__(self, cell_parser): self.cell_parser = cell_parser self.value = self.cell_parser.get_default_api_spec() @@ -37,9 +38,9 @@ def add_cell(self, cell_source): if self.cell_parser.is_api_cell(cell_source): path_name, verb = self.cell_parser.get_cell_endpoint_and_verb(cell_source) path_value = self.cell_parser.get_path_content(cell_source) - if not path_name in self.value['paths']: - self.value['paths'][path_name] = {} - self.value['paths'][path_name][verb.lower()] = path_value + if path_name not in self.value["paths"]: + self.value["paths"][path_name] = {} + self.value["paths"][path_name][verb.lower()] = path_value def set_default_title(self, path): """Stores the root of a notebook filename as the API title, if one is @@ -50,9 +51,11 @@ def set_default_title(self, path): path : url Path to the notebook file defining the API """ - if 'info' in self.value and 'title' not in self.value['info']: + if "info" in self.value and "title" not in self.value["info"]: basename = os.path.basename(path) - self.value['info']['title'] = basename.split('.')[0] if basename.find('.') > 0 else basename + self.value["info"]["title"] = ( + basename.split(".")[0] if basename.find(".") > 0 else basename + ) def build(self): """Gets the specification. diff --git a/kernel_gateway/notebook_http/swagger/handlers.py b/kernel_gateway/notebook_http/swagger/handlers.py index d51a1be..5c7f7a7 100644 --- a/kernel_gateway/notebook_http/swagger/handlers.py +++ b/kernel_gateway/notebook_http/swagger/handlers.py @@ -10,13 +10,13 @@ from ...mixins import TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin -class SwaggerSpecHandler(TokenAuthorizationMixin, - CORSMixin, - JSONErrorsMixin, - tornado.web.RequestHandler): +class SwaggerSpecHandler( + TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin, tornado.web.RequestHandler +): """Handles requests for the Swagger specification of a notebook defined API. """ + output = None def initialize(self, notebook_path, source_cells, cell_parser): @@ -34,13 +34,13 @@ def initialize(self, notebook_path, source_cells, cell_parser): if self.output is None: spec_builder = SwaggerSpecBuilder(cell_parser) for source_cell in source_cells: - if 'source' in source_cell: - spec_builder.add_cell(source_cell['source']) + if "source" in source_cell: + spec_builder.add_cell(source_cell["source"]) spec_builder.set_default_title(notebook_path) SwaggerSpecHandler.output = json.dumps(spec_builder.build()) async def get(self, **kwargs): """Responds with the spec in JSON format.""" - self.set_header('Content-Type', 'application/json') + self.set_header("Content-Type", "application/json") self.set_status(200) await self.finish(self.output) diff --git a/kernel_gateway/notebook_http/swagger/parser.py b/kernel_gateway/notebook_http/swagger/parser.py index 5527247..e475612 100644 --- a/kernel_gateway/notebook_http/swagger/parser.py +++ b/kernel_gateway/notebook_http/swagger/parser.py @@ -10,7 +10,7 @@ def _swaggerlet_from_markdown(cell_source): - """ Pulls apart the first block comment of a cell's source, + """Pulls apart the first block comment of a cell's source, then tries to parse it as a JSON object. If it contains a 'swagger' property, returns it. """ @@ -19,7 +19,7 @@ def _swaggerlet_from_markdown(cell_source): if len(lines) > 2: for i in range(0, len(lines)): if lines[i].startswith("```"): - lines = lines[i+1:] + lines = lines[i + 1 :] break for i in range(0, len(lines)): if lines[i].startswith("```"): @@ -27,8 +27,8 @@ def _swaggerlet_from_markdown(cell_source): break # parse the comment as JSON and check for a "swagger" property try: - json_comment = json.loads(''.join(lines)) - if 'swagger' in json_comment: + json_comment = json.loads("".join(lines)) + if "swagger" in json_comment: return json_comment except ValueError: # not a swaggerlet @@ -38,69 +38,78 @@ def _swaggerlet_from_markdown(cell_source): class SwaggerCellParser(LoggingConfigurable): """A utility class for parsing Jupyter code cells to find API annotations - of the form: - - `COMMENT (ResponseInfo)? operationId: ID` - - where: - - * `COMMENT` is the single line comment character of the notebook kernel - language - * `ResponseInfo` is a literal token. - * `ID` is an operation's ID as documented in a Swagger comment block - * `operationId` is a literal token. - - Parameters - ---------- - comment_prefix - Token indicating a comment in the notebook language - - Attributes - ---------- - notebook_cells : list - The cells from the target notebook, one of which must contain a Swagger spec in a commented block - operation_indicator : str - Regex pattern for API annotations - operation_response_indicator : str - Regex pattern for API response metadata annotations + of the form: + + `COMMENT (ResponseInfo)? operationId: ID` + + where: + + * `COMMENT` is the single line comment character of the notebook kernel + language + * `ResponseInfo` is a literal token. + * `ID` is an operation's ID as documented in a Swagger comment block + * `operationId` is a literal token. + + Parameters + ---------- + comment_prefix + Token indicating a comment in the notebook language + + Attributes + ---------- + notebook_cells : list + The cells from the target notebook, one of which must contain a Swagger spec in a commented block + operation_indicator : str + Regex pattern for API annotations + operation_response_indicator : str + Regex pattern for API response metadata annotations """ - operation_indicator = Unicode(default_value=r'{}\s*operationId:\s*(.*)') - operation_response_indicator = Unicode(default_value=r'{}\s*ResponseInfo\s+operationId:\s*(.*)') + + operation_indicator = Unicode(default_value=r"{}\s*operationId:\s*(.*)") + operation_response_indicator = Unicode(default_value=r"{}\s*ResponseInfo\s+operationId:\s*(.*)") notebook_cells = List() def __init__(self, comment_prefix, notebook_cells, **kwargs): super(SwaggerCellParser, self).__init__(**kwargs) self.notebook_cells = notebook_cells - self.kernelspec_operation_indicator = re.compile(self.operation_indicator.format(comment_prefix)) - self.kernelspec_operation_response_indicator = re.compile(self.operation_response_indicator.format(comment_prefix)) + self.kernelspec_operation_indicator = re.compile( + self.operation_indicator.format(comment_prefix) + ) + self.kernelspec_operation_response_indicator = re.compile( + self.operation_response_indicator.format(comment_prefix) + ) self.swagger = dict() operationIdsFound = [] operationIdsDeclared = [] for cell in self.notebook_cells: - if 'type' not in cell or cell['type'] == 'markdown': - json_swagger = _swaggerlet_from_markdown(cell['source']) + if "type" not in cell or cell["type"] == "markdown": + json_swagger = _swaggerlet_from_markdown(cell["source"]) if json_swagger is not None: self.swagger.update(dict(json_swagger)) - if 'type' not in cell or cell['type'] == 'code': - match = self.kernelspec_operation_indicator.match(cell['source']) + if "type" not in cell or cell["type"] == "code": + match = self.kernelspec_operation_indicator.match(cell["source"]) if match is not None: operationIdsFound.append(match.group(1).strip()) if len(self.swagger.values()) == 0: - self.log.warning('No Swagger documentation found') - if 'paths' in self.swagger: - for endpoint in self.swagger['paths'].keys(): - for verb in self.swagger['paths'][endpoint].keys(): - if 'operationId' in self.swagger['paths'][endpoint][verb]: - operationId = self.swagger['paths'][endpoint][verb]['operationId'] + self.log.warning("No Swagger documentation found") + if "paths" in self.swagger: + for endpoint in self.swagger["paths"].keys(): + for verb in self.swagger["paths"][endpoint].keys(): + if "operationId" in self.swagger["paths"][endpoint][verb]: + operationId = self.swagger["paths"][endpoint][verb]["operationId"] operationIdsDeclared.append(operationId) for operationId in operationIdsDeclared: if operationId not in operationIdsFound: - self.log.warning(f'Operation {operationId} was declared but not referenced in a cell') + self.log.warning( + f"Operation {operationId} was declared but not referenced in a cell" + ) for operationId in operationIdsFound: if operationId not in operationIdsDeclared: - self.log.warning(f'Operation {operationId} was referenced in a cell but not declared') + self.log.warning( + f"Operation {operationId} was referenced in a cell but not declared" + ) else: - self.log.warning('No paths documented in Swagger documentation') + self.log.warning("No paths documented in Swagger documentation") def is_api_cell(self, cell_source): """Gets if the cell source is documented as an API endpoint. @@ -153,7 +162,9 @@ def endpoints(self, source_cells, sort_func=first_path_param_index): tuple and a dict mapping HTTP verbs to cell sources as the second element of each tuple """ - endpoints = self._endpoint_verb_source_mappings(source_cells, self.kernelspec_operation_indicator) + endpoints = self._endpoint_verb_source_mappings( + source_cells, self.kernelspec_operation_indicator + ) sorted_keys = sorted(endpoints, key=sort_func, reverse=True) return [(key, endpoints[key]) for key in sorted_keys] @@ -174,7 +185,9 @@ def endpoint_responses(self, source_cells, sort_func=first_path_param_index): tuple and a dict mapping HTTP verbs to cell sources as the second element of each tuple """ - endpoints = self._endpoint_verb_source_mappings(source_cells, self.kernelspec_operation_response_indicator) + endpoints = self._endpoint_verb_source_mappings( + source_cells, self.kernelspec_operation_response_indicator + ) sorted_keys = sorted(endpoints, key=sort_func, reverse=True) return [(key, endpoints[key]) for key in sorted_keys] @@ -204,24 +217,29 @@ def _endpoint_verb_source_mappings(self, source_cells, operationIdRegex): if matched is not None: operationId = matched.group(1).strip() # stripping trailing whitespace, could be a gotcha - operationIds.setdefault(operationId, '') - operationIds[operationId] += cell_source + '\n' + operationIds.setdefault(operationId, "") + operationIds[operationId] += cell_source + "\n" # go through the declared swagger and assign source values per referenced operationIds - for endpoint in self.swagger['paths'].keys(): - for verb in self.swagger['paths'][endpoint].keys(): - if 'operationId' in self.swagger['paths'][endpoint][verb] and self.swagger['paths'][endpoint][verb]['operationId'] in operationIds: - operationId = self.swagger['paths'][endpoint][verb]['operationId'] - if 'parameters' in self.swagger['paths'][endpoint][verb]: + for endpoint in self.swagger["paths"].keys(): + for verb in self.swagger["paths"][endpoint].keys(): + if ( + "operationId" in self.swagger["paths"][endpoint][verb] + and self.swagger["paths"][endpoint][verb]["operationId"] in operationIds + ): + operationId = self.swagger["paths"][endpoint][verb]["operationId"] + if "parameters" in self.swagger["paths"][endpoint][verb]: endpoint_with_param = endpoint ## do we need to sort these names as well? - for parameter in self.swagger['paths'][endpoint][verb]['parameters']: - if 'name' in parameter: - endpoint_with_param = '/:'.join([endpoint_with_param, parameter['name']]) - mappings.setdefault(endpoint_with_param, {}).setdefault(verb, '') + for parameter in self.swagger["paths"][endpoint][verb]["parameters"]: + if "name" in parameter: + endpoint_with_param = "/:".join( + [endpoint_with_param, parameter["name"]] + ) + mappings.setdefault(endpoint_with_param, {}).setdefault(verb, "") mappings[endpoint_with_param][verb] = operationIds[operationId] else: - mappings.setdefault(endpoint, {}).setdefault(verb, '') + mappings.setdefault(endpoint, {}).setdefault(verb, "") mappings[endpoint][verb] = operationIds[operationId] return mappings @@ -245,9 +263,12 @@ def get_cell_endpoint_and_verb(self, cell_source): if matched is not None: operationId = matched.group(1) # go through the declared operationIds to find corresponding endpoints, methods, and parameters - for endpoint in self.swagger['paths'].keys(): - for verb in self.swagger['paths'][endpoint].keys(): - if 'operationId' in self.swagger['paths'][endpoint][verb] and self.swagger['paths'][endpoint][verb]['operationId'] == operationId: + for endpoint in self.swagger["paths"].keys(): + for verb in self.swagger["paths"][endpoint].keys(): + if ( + "operationId" in self.swagger["paths"][endpoint][verb] + and self.swagger["paths"][endpoint][verb]["operationId"] == operationId + ): return (endpoint, verb) return (None, None) @@ -267,16 +288,15 @@ def get_path_content(self, cell_source): matched = self.kernelspec_operation_indicator.match(cell_source) operationId = matched.group(1) # go through the declared operationIds to find corresponding endpoints, methods, and parameters - for endpoint in self.swagger['paths'].keys(): - for verb in self.swagger['paths'][endpoint].keys(): - if 'operationId' in self.swagger['paths'][endpoint][verb] and self.swagger['paths'][endpoint][verb]['operationId'] == operationId: - return self.swagger['paths'][endpoint][verb] + for endpoint in self.swagger["paths"].keys(): + for verb in self.swagger["paths"][endpoint].keys(): + if ( + "operationId" in self.swagger["paths"][endpoint][verb] + and self.swagger["paths"][endpoint][verb]["operationId"] == operationId + ): + return self.swagger["paths"][endpoint][verb] # mismatched operationId? return a default - return { - 'responses': { - 200: {'description': 'Success'} - } - } + return {"responses": {200: {"description": "Success"}}} def get_default_api_spec(self): """Gets the default minimum API spec to use when building a full spec @@ -287,7 +307,11 @@ def get_default_api_spec(self): """ if self.swagger is not None: return self.swagger - return {'swagger': '2.0', 'paths': {}, 'info': {'version': '0.0.0', 'title': 'Default Title'}} + return { + "swagger": "2.0", + "paths": {}, + "info": {"version": "0.0.0", "title": "Default Title"}, + } def create_parser(*args, **kwargs): diff --git a/kernel_gateway/services/kernels/handlers.py b/kernel_gateway/services/kernels/handlers.py index 677ecdf..c883b46 100644 --- a/kernel_gateway/services/kernels/handlers.py +++ b/kernel_gateway/services/kernels/handlers.py @@ -11,21 +11,20 @@ from ...mixins import TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin -class MainKernelHandler(TokenAuthorizationMixin, - CORSMixin, - JSONErrorsMixin, - server_handlers.MainKernelHandler): +class MainKernelHandler( + TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin, server_handlers.MainKernelHandler +): """Extends the notebook main kernel handler with token auth, CORS, and JSON errors. """ @property def env_whitelist(self): - return self.settings['kg_personality'].env_whitelist + return self.settings["kg_personality"].env_whitelist @property def env_process_whitelist(self): - return self.settings['kg_env_process_whitelist'] + return self.settings["kg_env_process_whitelist"] async def post(self): """Overrides the super class method to honor the max number of allowed @@ -41,27 +40,37 @@ async def post(self): tornado.web.HTTPError 403 Forbidden if the limit is reached """ - max_kernels = self.settings['kg_max_kernels'] + max_kernels = self.settings["kg_max_kernels"] if max_kernels is not None: - km = self.settings['kernel_manager'] + km = self.settings["kernel_manager"] kernels = km.list_kernels() if len(kernels) >= max_kernels: - raise tornado.web.HTTPError(403, 'Resource Limit') + raise tornado.web.HTTPError(403, "Resource Limit") # Try to get env vars from the request body model = self.get_json_body() - if model is not None and 'env' in model: - if not isinstance(model['env'], dict): + if model is not None and "env" in model: + if not isinstance(model["env"], dict): raise tornado.web.HTTPError(400) # Start with the PATH from the current env. Do not provide the entire environment # which might contain server secrets that should not be passed to kernels. - env = {'PATH': os.getenv('PATH', '')} + env = {"PATH": os.getenv("PATH", "")} # Whitelist environment variables from current process environment - env.update({key: value for key, value in os.environ.items() - if key in self.env_process_whitelist}) + env.update( + { + key: value + for key, value in os.environ.items() + if key in self.env_process_whitelist + } + ) # Whitelist KERNEL_* args and those allowed by configuration from client - env.update({key: value for key, value in model['env'].items() - if key.startswith('KERNEL_') or key in self.env_whitelist}) + env.update( + { + key: value + for key, value in model["env"].items() + if key.startswith("KERNEL_") or key in self.env_whitelist + } + ) # No way to override the call to start_kernel on the kernel manager # so do a temporary partial (ugh) orig_start = self.kernel_manager.start_kernel @@ -84,8 +93,8 @@ async def get(self): tornado.web.HTTPError 403 Forbidden if kernel listing is disabled """ - if not self.settings.get('kg_list_kernels'): - raise tornado.web.HTTPError(403, 'Forbidden') + if not self.settings.get("kg_list_kernels"): + raise tornado.web.HTTPError(403, "Forbidden") else: await super(MainKernelHandler, self).get() @@ -94,13 +103,13 @@ def options(self, **kwargs): self.finish() -class KernelHandler(TokenAuthorizationMixin, - CORSMixin, - JSONErrorsMixin, - server_handlers.KernelHandler): +class KernelHandler( + TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin, server_handlers.KernelHandler +): """Extends the notebook kernel handler with token auth, CORS, and JSON errors. """ + def options(self, **kwargs): """Method for properly handling CORS pre-flight""" self.finish() diff --git a/kernel_gateway/services/kernels/manager.py b/kernel_gateway/services/kernels/manager.py index 7a596cb..b35deab 100644 --- a/kernel_gateway/services/kernels/manager.py +++ b/kernel_gateway/services/kernels/manager.py @@ -21,9 +21,7 @@ def _default_root_dir(self): return os.getcwd() def _kernel_manager_class_default(self): - return ( - "kernel_gateway.services.kernels.manager.KernelGatewayIOLoopKernelManager" - ) + return "kernel_gateway.services.kernels.manager.KernelGatewayIOLoopKernelManager" @property def seed_kernelspec(self) -> Optional[str]: @@ -44,9 +42,7 @@ def seed_kernelspec(self) -> Optional[str]: if self.parent.force_kernel_name: self._seed_kernelspec = self.parent.force_kernel_name else: - self._seed_kernelspec = self.parent.seed_notebook["metadata"][ - "kernelspec" - ]["name"] + self._seed_kernelspec = self.parent.seed_notebook["metadata"]["kernelspec"]["name"] else: self._seed_kernelspec = None @@ -89,9 +85,7 @@ async def start_kernel(self, *args, **kwargs): """ if self.parent.force_kernel_name: kwargs["kernel_name"] = self.parent.force_kernel_name - kernel_id = await super(SeedingMappingKernelManager, self).start_kernel( - *args, **kwargs - ) + kernel_id = await super(SeedingMappingKernelManager, self).start_kernel(*args, **kwargs) if kernel_id and self.seed_source is not None: # Only run source if the kernel spec matches the notebook kernel spec @@ -122,9 +116,7 @@ async def start_kernel(self, *args, **kwargs): client.stop_channels() # Shutdown the kernel await self.shutdown_kernel(kernel_id) - raise RuntimeError( - "Error seeding kernel memory", msg["content"] - ) + raise RuntimeError("Error seeding kernel memory", msg["content"]) # Shutdown the channels to remove any lingering ZMQ messages client.stop_channels() return kernel_id diff --git a/kernel_gateway/services/kernels/pool.py b/kernel_gateway/services/kernels/pool.py index e74591f..8a3ad1d 100644 --- a/kernel_gateway/services/kernels/pool.py +++ b/kernel_gateway/services/kernels/pool.py @@ -83,6 +83,7 @@ class ManagedKernelPool(KernelPool): kernel_semaphore : tornado.locks.Semaphore Semaphore that controls access to the kernel pool """ + kernel_clients: dict on_recv_funcs: dict kernel_pool: list @@ -155,9 +156,8 @@ def _on_reply(self, kernel_id, session, msg_list): msg_list : list List of 0mq messages """ - if not kernel_id in self.on_recv_funcs: - self.log.warning( - "Could not find callback for kernel_id: {}".format(kernel_id)) + if kernel_id not in self.on_recv_funcs: + self.log.warning("Could not find callback for kernel_id: {}".format(kernel_id)) return idents, msg_list = session.feed_identities(msg_list) msg = session.deserialize(msg_list) @@ -200,8 +200,7 @@ def on_recv(self, kernel_id, func): self.on_recv_funcs[kernel_id] = func async def shutdown(self): - """Shuts down all kernels and their clients. - """ + """Shuts down all kernels and their clients.""" await self.managed_pool_initialized for kid in self.kernel_clients: self.kernel_clients[kid].stop_channels() diff --git a/kernel_gateway/services/sessions/handlers.py b/kernel_gateway/services/sessions/handlers.py index 1878e91..855b71c 100644 --- a/kernel_gateway/services/sessions/handlers.py +++ b/kernel_gateway/services/sessions/handlers.py @@ -7,13 +7,13 @@ from ...mixins import TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin -class SessionRootHandler(TokenAuthorizationMixin, - CORSMixin, - JSONErrorsMixin, - server_handlers.SessionRootHandler): +class SessionRootHandler( + TokenAuthorizationMixin, CORSMixin, JSONErrorsMixin, server_handlers.SessionRootHandler +): """Extends the notebook root session handler with token auth, CORS, and JSON errors. """ + async def get(self): """Overrides the super class method to honor the kernel listing configuration setting. @@ -23,8 +23,8 @@ async def get(self): tornado.web.HTTPError If kg_list_kernels is False, respond with 403 Forbidden """ - if 'kg_list_kernels' not in self.settings or self.settings['kg_list_kernels'] != True: - raise tornado.web.HTTPError(403, 'Forbidden') + if "kg_list_kernels" not in self.settings or self.settings["kg_list_kernels"] != True: + raise tornado.web.HTTPError(403, "Forbidden") else: await super(SessionRootHandler, self).get() diff --git a/kernel_gateway/services/sessions/sessionmanager.py b/kernel_gateway/services/sessions/sessionmanager.py index fa4bbe6..03a0a61 100644 --- a/kernel_gateway/services/sessions/sessionmanager.py +++ b/kernel_gateway/services/sessions/sessionmanager.py @@ -26,11 +26,12 @@ class SessionManager(LoggingConfigurable): _columns : list Session metadata key names """ + def __init__(self, kernel_manager, **kwargs): super(SessionManager, self).__init__(**kwargs) self.kernel_manager = kernel_manager self._sessions = [] - self._columns = ['session_id', 'path', 'kernel_id'] + self._columns = ["session_id", "path", "kernel_id"] def session_exists(self, path, *args, **kwargs) -> bool: """Checks to see if the session with the given path value exists. @@ -44,13 +45,15 @@ def session_exists(self, path, *args, **kwargs) -> bool: ------- bool """ - return bool([item for item in self._sessions if item['path'] == path]) + return bool([item for item in self._sessions if item["path"] == path]) def new_session_id(self) -> str: """Creates a uuid for a new session.""" return str(uuid.uuid4()) - async def create_session(self, path=None, kernel_name=None, kernel_id=None, *args, **kwargs) -> dict: + async def create_session( + self, path=None, kernel_name=None, kernel_id=None, *args, **kwargs + ) -> dict: """Creates a session and returns its model. Launches a kernel and stores the session metadata for later lookup. @@ -94,9 +97,7 @@ def save_session(self, session_id, path=None, kernel_id=None, *args, **kwargs) - dict Session model with `session_id`, `path`, and `kernel_id` keys """ - self._sessions.append({'session_id': session_id, - 'path':path, - 'kernel_id': kernel_id}) + self._sessions.append({"session_id": session_id, "path": path, "kernel_id": kernel_id}) return self.get_session(session_id=session_id) @@ -155,7 +156,7 @@ def get_session(self, **kwargs): row = self.get_session_by_key(column, kwargs[column]) if not row: - raise web.HTTPError(404, u'Session not found: %s' % kwargs[column]) + raise web.HTTPError(404, "Session not found: %s" % kwargs[column]) return self.row_to_model(row) @@ -181,22 +182,22 @@ def update_session(self, session_id, *args, **kwargs): # no changes return - row = self.get_session_by_key('session_id', session_id) + row = self.get_session_by_key("session_id", session_id) if not row: raise KeyError # if kernel_id is in kwargs, validate it prior to removing the row... - if 'kernel_id' in kwargs and kwargs['kernel_id'] not in self.kernel_manager: + if "kernel_id" in kwargs and kwargs["kernel_id"] not in self.kernel_manager: raise KeyError(f"Kernel '{kwargs['kernel_id']}' does not exist.") self._sessions.remove(row) - if 'path' in kwargs: - row['path'] = kwargs['path'] + if "path" in kwargs: + row["path"] = kwargs["path"] - if 'kernel_id' in kwargs: - row['kernel_id'] = kwargs['kernel_id'] + if "kernel_id" in kwargs: + row["kernel_id"] = kwargs["kernel_id"] self._sessions.append(row) @@ -210,7 +211,7 @@ def row_to_model(self, row, *args, **kwargs): `path`, and `kernel` to the kernel model looked up using the `kernel_id` """ - if row['kernel_id'] not in self.kernel_manager: + if row["kernel_id"] not in self.kernel_manager: # The kernel was killed or died without deleting the session. # We can't use delete_session here because that tries to find # and shut down the kernel. @@ -218,11 +219,9 @@ def row_to_model(self, row, *args, **kwargs): raise KeyError model = { - 'id': row['session_id'], - 'notebook': { - 'path': row['path'] - }, - 'kernel': self.kernel_manager.kernel_model(row['kernel_id']) + "id": row["session_id"], + "notebook": {"path": row["path"]}, + "kernel": self.kernel_manager.kernel_model(row["kernel_id"]), } return model @@ -247,9 +246,9 @@ async def delete_session(self, session_id, *args, **kwargs): If the `session_id` is not in the store """ # Check that session exists before deleting - s = self.get_session_by_key('session_id', session_id) + s = self.get_session_by_key("session_id", session_id) if not s: raise KeyError - await self.kernel_manager.shutdown_kernel(s['kernel_id']) + await self.kernel_manager.shutdown_kernel(s["kernel_id"]) self._sessions.remove(s) diff --git a/kernel_gateway/tests/notebook_http/cell/test_parser.py b/kernel_gateway/tests/notebook_http/cell/test_parser.py index 6ab63e2..df79714 100644 --- a/kernel_gateway/tests/notebook_http/cell/test_parser.py +++ b/kernel_gateway/tests/notebook_http/cell/test_parser.py @@ -8,6 +8,7 @@ class TestAPICellParser: """Unit tests the APICellParser class.""" + def test_is_api_cell(self): """Parser should correctly identify annotated API cells.""" parser = APICellParser(comment_prefix="#") @@ -20,7 +21,7 @@ def test_endpoint_sort_default_strategy(self): "# POST /:foo", "# POST /hello/:foo", "# GET /hello/:foo", - "# PUT /hello/world" + "# PUT /hello/world", ] parser = APICellParser(comment_prefix="#") endpoints = parser.endpoints(source_cells) @@ -34,11 +35,7 @@ def test_endpoint_sort_custom_strategy(self): """Parser should sort duplicate endpoint paths using a custom sort strategy. """ - source_cells = [ - "# POST /1", - "# POST /+", - "# GET /a" - ] + source_cells = ["# POST /1", "# POST /+", "# GET /a"] def custom_sort_fun(endpoint): index = sys.maxsize @@ -78,7 +75,7 @@ def test_endpoint_concatenation(self): "# POST /foo/:bar", "# POST /foo", "ignored", - "# GET /foo/:bar" + "# GET /foo/:bar", ] parser = APICellParser(comment_prefix="#") endpoints = parser.endpoints(source_cells) @@ -100,7 +97,7 @@ def test_endpoint_response_concatenation(self): "# ResponseInfo POST /foo/:bar", "# ResponseInfo POST /foo", "ignored", - "# ResponseInfo GET /foo/:bar" + "# ResponseInfo GET /foo/:bar", ] parser = APICellParser(comment_prefix="#") endpoints = parser.endpoint_responses(source_cells) @@ -110,5 +107,8 @@ def test_endpoint_response_concatenation(self): assert len(endpoints["/foo"]) == 1 assert len(endpoints["/foo/:bar"]) == 2 assert endpoints["/foo"]["POST"] == "# ResponseInfo POST /foo\n" - assert endpoints["/foo/:bar"]["POST"] == "# ResponseInfo POST /foo/:bar\n# ResponseInfo POST /foo/:bar\n" + assert ( + endpoints["/foo/:bar"]["POST"] + == "# ResponseInfo POST /foo/:bar\n# ResponseInfo POST /foo/:bar\n" + ) assert endpoints["/foo/:bar"]["GET"] == "# ResponseInfo GET /foo/:bar\n" diff --git a/kernel_gateway/tests/notebook_http/swagger/test_builders.py b/kernel_gateway/tests/notebook_http/swagger/test_builders.py index da6d348..693f1f2 100644 --- a/kernel_gateway/tests/notebook_http/swagger/test_builders.py +++ b/kernel_gateway/tests/notebook_http/swagger/test_builders.py @@ -11,6 +11,7 @@ class TestSwaggerBuilders: """Unit tests the swagger spec builder.""" + def test_add_title_adds_title_to_spec(self): """Builder should store an API title.""" expected = "Some New Title" @@ -21,13 +22,7 @@ def test_add_title_adds_title_to_spec(self): def test_add_cell_adds_api_cell_to_spec(self): """Builder should store an API cell annotation.""" - expected = { - "get": { - "responses": { - 200: {"description": "Success"} - } - } - } + expected = {"get": {"responses": {200: {"description": "Success"}}}} builder = SwaggerSpecBuilder(APICellParser(comment_prefix="#")) builder.add_cell("# GET /some/resource") result = builder.build() @@ -35,7 +30,7 @@ def test_add_cell_adds_api_cell_to_spec(self): def test_all_swagger_preserved_in_spec(self): """Builder should store the swagger documented cell.""" - expected = ''' + expected = """ { "swagger": "2.0", "info" : {"version" : "0.0.0", "title" : "Default Title"}, @@ -72,16 +67,25 @@ def test_all_swagger_preserved_in_spec(self): } } } - ''' - builder = SwaggerSpecBuilder(SwaggerCellParser(comment_prefix='#', notebook_cells=[{"source":expected}])) + """ + builder = SwaggerSpecBuilder( + SwaggerCellParser(comment_prefix="#", notebook_cells=[{"source": expected}]) + ) builder.add_cell(expected) result = builder.build() self.maxDiff = None - assert result["paths"]["/some/resource"]["get"]["description"] == json.loads(expected)["paths"]["/some/resource"]["get"]["description"], "description was not preserved" + assert ( + result["paths"]["/some/resource"]["get"]["description"] + == json.loads(expected)["paths"]["/some/resource"]["get"]["description"] + ), "description was not preserved" assert "info" in result, "info was not preserved" assert "title" in result["info"], "title was not present" - assert result["info"]["title"] == json.loads(expected)["info"]["title"], "title was not preserved" - assert json.dumps(result["paths"]["/some/resource"], sort_keys=True) == json.dumps(json.loads(expected)["paths"]["/some/resource"], sort_keys=True), "operations were not as expected" + assert ( + result["info"]["title"] == json.loads(expected)["info"]["title"] + ), "title was not preserved" + assert json.dumps(result["paths"]["/some/resource"], sort_keys=True) == json.dumps( + json.loads(expected)["paths"]["/some/resource"], sort_keys=True + ), "operations were not as expected" new_title = "new title. same contents." builder.set_default_title(new_title) diff --git a/kernel_gateway/tests/notebook_http/swagger/test_parser.py b/kernel_gateway/tests/notebook_http/swagger/test_parser.py index 649e5c8..523bbbe 100644 --- a/kernel_gateway/tests/notebook_http/swagger/test_parser.py +++ b/kernel_gateway/tests/notebook_http/swagger/test_parser.py @@ -7,54 +7,103 @@ class TestSwaggerAPICellParser: """Unit tests the SwaggerCellParser class.""" + def test_basic_swagger_parse(self): """Parser should correctly identify Swagger cells.""" - parser = SwaggerCellParser(comment_prefix='#', notebook_cells=[{"source":'```\n{"swagger":"2.0", "paths": {"": {"post": {"operationId": "foo", "parameters": [{"name": "foo"}]}}}}\n```\n'}]) - assert 'swagger' in parser.swagger, 'Swagger doc was not detected' + parser = SwaggerCellParser( + comment_prefix="#", + notebook_cells=[ + { + "source": '```\n{"swagger":"2.0", "paths": {"": {"post": {"operationId": "foo", "parameters": [{"name": "foo"}]}}}}\n```\n' + } + ], + ) + assert "swagger" in parser.swagger, "Swagger doc was not detected" def test_basic_is_api_cell(self): """Parser should correctly identify operation cells.""" - parser = SwaggerCellParser(comment_prefix='#', notebook_cells=[{"source":'```\n{"swagger":"2.0", "paths": {"": {"post": {"operationId": "foo", "parameters": [{"name": "foo"}]}}}}\n```\n'}]) - assert parser.is_api_cell('#operationId:foo'), 'API cell was not detected with ' + str(parser.kernelspec_operation_indicator) - assert parser.is_api_cell('# operationId:foo'), 'API cell was not detected with ' + str(parser.kernelspec_operation_indicator) - assert parser.is_api_cell('#operationId: foo'), 'API cell was not detected with ' + str(parser.kernelspec_operation_indicator) - assert parser.is_api_cell('no') is False, 'API cell was detected' - assert parser.is_api_cell('# another comment') is False, 'API cell was detected' + parser = SwaggerCellParser( + comment_prefix="#", + notebook_cells=[ + { + "source": '```\n{"swagger":"2.0", "paths": {"": {"post": {"operationId": "foo", "parameters": [{"name": "foo"}]}}}}\n```\n' + } + ], + ) + assert parser.is_api_cell("#operationId:foo"), "API cell was not detected with " + str( + parser.kernelspec_operation_indicator + ) + assert parser.is_api_cell("# operationId:foo"), "API cell was not detected with " + str( + parser.kernelspec_operation_indicator + ) + assert parser.is_api_cell("#operationId: foo"), "API cell was not detected with " + str( + parser.kernelspec_operation_indicator + ) + assert parser.is_api_cell("no") is False, "API cell was detected" + assert parser.is_api_cell("# another comment") is False, "API cell was detected" def test_basic_is_api_response_cell(self): """Parser should correctly identify ResponseInfo cells.""" - parser = SwaggerCellParser(comment_prefix='#', notebook_cells=[{"source":'```\n{"swagger":"2.0", "paths": {"": {"post": {"operationId": "foo", "parameters": [{"name": "foo"}]}}}}\n```\n'}]) - assert parser.is_api_response_cell('#ResponseInfo operationId:foo'), 'Response cell was not detected with ' + str(parser.kernelspec_operation_response_indicator) - assert parser.is_api_response_cell('# ResponseInfo operationId:foo'), 'Response cell was not detected with ' + str(parser.kernelspec_operation_response_indicator) - assert parser.is_api_response_cell('# ResponseInfo operationId: foo'), 'Response cell was not detected with ' + str(parser.kernelspec_operation_response_indicator) - assert parser.is_api_response_cell('#ResponseInfo operationId: foo'), 'Response cell was not detected with ' + str(parser.kernelspec_operation_response_indicator) - assert parser.is_api_response_cell('# operationId: foo') is False, 'API cell was detected as a ResponseInfo cell ' + str(parser.kernelspec_operation_response_indicator) - assert parser.is_api_response_cell('no') is False, 'API cell was detected' + parser = SwaggerCellParser( + comment_prefix="#", + notebook_cells=[ + { + "source": '```\n{"swagger":"2.0", "paths": {"": {"post": {"operationId": "foo", "parameters": [{"name": "foo"}]}}}}\n```\n' + } + ], + ) + assert parser.is_api_response_cell("#ResponseInfo operationId:foo"), ( + "Response cell was not detected with " + + str(parser.kernelspec_operation_response_indicator) + ) + assert parser.is_api_response_cell("# ResponseInfo operationId:foo"), ( + "Response cell was not detected with " + + str(parser.kernelspec_operation_response_indicator) + ) + assert parser.is_api_response_cell("# ResponseInfo operationId: foo"), ( + "Response cell was not detected with " + + str(parser.kernelspec_operation_response_indicator) + ) + assert parser.is_api_response_cell("#ResponseInfo operationId: foo"), ( + "Response cell was not detected with " + + str(parser.kernelspec_operation_response_indicator) + ) + assert parser.is_api_response_cell("# operationId: foo") is False, ( + "API cell was detected as a ResponseInfo cell " + + str(parser.kernelspec_operation_response_indicator) + ) + assert parser.is_api_response_cell("no") is False, "API cell was detected" def test_endpoint_sort_default_strategy(self): """Parser should sort duplicate endpoint paths.""" source_cells = [ - {"source": '\n```\n{"swagger":"2.0","paths":{"":{"post":{"operationId":"postRoot","parameters":[{"name":"foo"}]}},"/hello":{"post":{"operationId":"postHello","parameters":[{"name":"foo"}]},"get":{"operationId":"getHello","parameters":[{"name":"foo"}]}},"/hello/world":{"put":{"operationId":"putWorld"}}}}\n```\n'}, - {"source": '# operationId:putWorld'}, - {"source": '# operationId:getHello'}, - {"source": '# operationId:postHello'}, - {"source": '# operationId:postRoot'}, + { + "source": '\n```\n{"swagger":"2.0","paths":{"":{"post":{"operationId":"postRoot","parameters":[{"name":"foo"}]}},"/hello":{"post":{"operationId":"postHello","parameters":[{"name":"foo"}]},"get":{"operationId":"getHello","parameters":[{"name":"foo"}]}},"/hello/world":{"put":{"operationId":"putWorld"}}}}\n```\n' + }, + {"source": "# operationId:putWorld"}, + {"source": "# operationId:getHello"}, + {"source": "# operationId:postHello"}, + {"source": "# operationId:postRoot"}, ] - parser = SwaggerCellParser(comment_prefix='#', notebook_cells = source_cells) - endpoints = parser.endpoints(cell['source'] for cell in source_cells) + parser = SwaggerCellParser(comment_prefix="#", notebook_cells=source_cells) + endpoints = parser.endpoints(cell["source"] for cell in source_cells) - expected_values = ['/hello/world', '/hello/:foo', '/:foo'] + expected_values = ["/hello/world", "/hello/:foo", "/:foo"] try: for index in range(0, len(expected_values)): endpoint, _ = endpoints[index] - assert expected_values[index] == endpoint, 'Endpoint was not found in expected order' + assert ( + expected_values[index] == endpoint + ), "Endpoint was not found in expected order" except IndexError: raise RuntimeError(endpoints) def test_endpoint_sort_custom_strategy(self): """Parser should sort duplicate endpoint paths using a custom sort strategy.""" source_cells = [ - {"source": '```\n{"swagger": "2.0", "paths": {"/1": {"post": {"operationId": "post1"}},"/+": {"post": {"operationId": "postPlus"}},"/a": {"get": {"operationId": "getA"}}}}\n```\n'}, + { + "source": '```\n{"swagger": "2.0", "paths": {"/1": {"post": {"operationId": "post1"}},"/+": {"post": {"operationId": "postPlus"}},"/a": {"get": {"operationId": "getA"}}}}\n```\n' + }, {"source": "# operationId: post1"}, {"source": "# operationId: postPlus"}, {"source": "# operationId: getA"}, @@ -79,7 +128,14 @@ def custom_sort_fun(endpoint): def test_get_cell_endpoint_and_verb(self): """Parser should extract API endpoint and verb from cell annotations.""" - parser = SwaggerCellParser(comment_prefix='#', notebook_cells=[{'source':'```\n{"swagger":"2.0", "paths": {"/foo": {"get": {"operationId": "getFoo"}}, "/bar/quo": {"post": {"operationId": "post_bar_Quo"}}}}\n```\n'}]) + parser = SwaggerCellParser( + comment_prefix="#", + notebook_cells=[ + { + "source": '```\n{"swagger":"2.0", "paths": {"/foo": {"get": {"operationId": "getFoo"}}, "/bar/quo": {"post": {"operationId": "post_bar_Quo"}}}}\n```\n' + } + ], + ) endpoint, verb = parser.get_cell_endpoint_and_verb("# operationId: getFoo") assert endpoint == "/foo", "Endpoint was not extracted correctly" assert verb.lower() == "get", "Endpoint was not extracted correctly" @@ -88,19 +144,25 @@ def test_get_cell_endpoint_and_verb(self): assert verb.lower() == "post", "Endpoint was not extracted correctly" endpoint, verb = parser.get_cell_endpoint_and_verb("some regular code") - assert endpoint is None, "Endpoint was not extracted correctly (something was actually returned)" - assert verb is None, "Endpoint was not extracted correctly (something was actually returned)" + assert ( + endpoint is None + ), "Endpoint was not extracted correctly (something was actually returned)" + assert ( + verb is None + ), "Endpoint was not extracted correctly (something was actually returned)" def test_endpoint_concatenation(self): """Parser should concatenate multiple cells with the same verb+path.""" cells = [ - {"source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putFoo","parameters": [{"name": "bar"}]},"post":{"operationId":"postFooBody"},"get": {"operationId":"getFoo","parameters": [{"name": "bar"}]}}}}\n```\n'}, + { + "source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putFoo","parameters": [{"name": "bar"}]},"post":{"operationId":"postFooBody"},"get": {"operationId":"getFoo","parameters": [{"name": "bar"}]}}}}\n```\n' + }, {"source": "# operationId: postFooBody "}, {"source": "# unrelated comment "}, {"source": "# operationId: putFoo"}, {"source": "# operationId: puttFoo"}, {"source": "# operationId: getFoo"}, - {"source": "# operationId: putFoo"} + {"source": "# operationId: putFoo"}, ] parser = SwaggerCellParser(comment_prefix="#", notebook_cells=cells) endpoints = parser.endpoints(cell["source"] for cell in cells) @@ -116,13 +178,15 @@ def test_endpoint_concatenation(self): def test_endpoint_response_concatenation(self): """Parser should concatenate multiple response cells with the same verb+path.""" source_cells = [ - {"source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n'}, + { + "source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n' + }, {"source": "# ResponseInfo operationId: get"}, {"source": "# ResponseInfo operationId: postbar "}, {"source": "# ResponseInfo operationId: putbar"}, {"source": "# ResponseInfo operationId: puttbar"}, {"source": "ignored"}, - {"source": "# ResponseInfo operationId: putbar "} + {"source": "# ResponseInfo operationId: putbar "}, ] parser = SwaggerCellParser(comment_prefix="#", notebook_cells=source_cells) endpoints = parser.endpoint_responses(cell["source"] for cell in source_cells) @@ -132,13 +196,18 @@ def test_endpoint_response_concatenation(self): assert len(endpoints["/foo"]) == 1 assert len(endpoints["/foo/:bar"]) == 2 assert endpoints["/foo"]["post"] == "# ResponseInfo operationId: postbar \n" - assert endpoints["/foo/:bar"]["put"] == "# ResponseInfo operationId: putbar\n# ResponseInfo operationId: putbar \n" + assert ( + endpoints["/foo/:bar"]["put"] + == "# ResponseInfo operationId: putbar\n# ResponseInfo operationId: putbar \n" + ) assert endpoints["/foo/:bar"]["get"] == "# ResponseInfo operationId: get\n" def test_undeclared_operations(self, caplog): """Parser should warn about operations that aren't documented in the swagger cell.""" source_cells = [ - {"source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n'}, + { + "source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n' + }, {"source": "# operationId: get"}, {"source": "# operationId: postbar "}, {"source": "# operationId: putbar"}, @@ -155,7 +224,9 @@ def test_undeclared_operations_reversed(self, caplog): {"source": "# operationId: postbar "}, {"source": "# operationId: putbar"}, {"source": "# operationId: extraOperation"}, - {"source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n'}, + { + "source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n' + }, ] SwaggerCellParser(comment_prefix="#", notebook_cells=source_cells) @@ -164,10 +235,12 @@ def test_undeclared_operations_reversed(self, caplog): def test_unreferenced_operations(self, caplog): """Parser should warn about documented operations that aren"t referenced in a cell.""" source_cells = [ - {"source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n'}, + { + "source": '```\n{"swagger":"2.0", "paths": {"/foo": {"put": {"operationId":"putbar","parameters": [{"name": "bar"}]},"post":{"operationId":"postbar"},"get": {"operationId":"get","parameters": [{"name": "bar"}]}}}}\n```\n' + }, {"source": "# operationId: get"}, {"source": "# operationId: putbar"}, - {"source": "# operationId: putbar "} + {"source": "# operationId: putbar "}, ] SwaggerCellParser(comment_prefix="#", notebook_cells=source_cells) diff --git a/kernel_gateway/tests/notebook_http/test_request_utils.py b/kernel_gateway/tests/notebook_http/test_request_utils.py index 3db7e3b..e3b4ab1 100644 --- a/kernel_gateway/tests/notebook_http/test_request_utils.py +++ b/kernel_gateway/tests/notebook_http/test_request_utils.py @@ -4,8 +4,13 @@ import unittest import json -from kernel_gateway.notebook_http.request_utils import (format_request, - parse_body, parameterize_path, headers_to_dict, parse_args) +from kernel_gateway.notebook_http.request_utils import ( + format_request, + parse_body, + parameterize_path, + headers_to_dict, + parse_args, +) class MockRequest(dict): @@ -24,65 +29,56 @@ def get_all(self): class TestRequestUtils(unittest.TestCase): """Unit tests the request utility helper functions.""" + def test_parse_body_text(self): """Should parse the body text from a byte stream to a string.""" request = MockRequest() - request.body = b'test value' - request.headers = { - 'Content-Type' : 'text/plain' - } + request.body = b"test value" + request.headers = {"Content-Type": "text/plain"} result = parse_body(request) - self.assertEqual(result, "test value", 'Did not properly parse text body.') + self.assertEqual(result, "test value", "Did not properly parse text body.") def test_parse_body_json(self): """Should parse the body from a JSON byte stream to a dict.""" request = MockRequest() request.body = b'{ "foo" : "bar" }' - request.headers = { - 'Content-Type' : 'application/json' - } + request.headers = {"Content-Type": "application/json"} result = parse_body(request) - self.assertEqual(result, { 'foo' : 'bar' }, 'Did not properly parse json body.') + self.assertEqual(result, {"foo": "bar"}, "Did not properly parse json body.") def test_parse_body_bad_json(self): """Should parse the body from an invalid JSON byte stream to a string.""" request = MockRequest() request.body = b'{ "foo" "bar" }' - request.headers = { - 'Content-Type' : 'application/json' - } + request.headers = {"Content-Type": "application/json"} result = parse_body(request) - self.assertEqual(result, '{ "foo" "bar" }', 'Did not properly parse json body.') + self.assertEqual(result, '{ "foo" "bar" }', "Did not properly parse json body.") def test_parse_body_multipart_form(self): """Should parse body arguments from multipart form data to a dict.""" request = MockRequest() request.body = None - request.body_arguments = { 'foo' : [b'bar']} - request.headers = { - 'Content-Type' : 'multipart/form-data' - } + request.body_arguments = {"foo": [b"bar"]} + request.headers = {"Content-Type": "multipart/form-data"} result = parse_body(request) - self.assertEqual(result, { 'foo' : ['bar']}, 'Did not properly parse json body.') + self.assertEqual(result, {"foo": ["bar"]}, "Did not properly parse json body.") def test_parse_body_url_encoded_form(self): """Should parse body arguments from urlencoded form data to a dict.""" request = MockRequest() request.body = None - request.body_arguments = { 'foo' : [b'bar']} - request.headers = { - 'Content-Type' : 'application/x-www-form-urlencoded' - } + request.body_arguments = {"foo": [b"bar"]} + request.headers = {"Content-Type": "application/x-www-form-urlencoded"} result = parse_body(request) - self.assertEqual(result, { 'foo' : ['bar']}, 'Did not properly parse json body.') + self.assertEqual(result, {"foo": ["bar"]}, "Did not properly parse json body.") def test_parse_body_empty(self): """Should parse an empty body to an empty string.""" request = MockRequest() - request.body = b'' + request.body = b"" request.headers = {} result = parse_body(request) - self.assertEqual(result, '', 'Did not properly handle body = empty string.') + self.assertEqual(result, "", "Did not properly handle body = empty string.") def test_parse_body_defaults_to_text_plain(self): """Should parse a body to a string by default.""" @@ -90,83 +86,120 @@ def test_parse_body_defaults_to_text_plain(self): request.body = b'{"foo" : "bar"}' request.headers = {} result = parse_body(request) - self.assertEqual(result, '{"foo" : "bar"}', 'Did not properly handle body = empty string.') + self.assertEqual(result, '{"foo" : "bar"}', "Did not properly handle body = empty string.") def test_parse_args(self): """Should parse URL argument byte streams to strings.""" - result = parse_args({'arga': [ b'1234', b'4566'], 'argb' : [b'hello']}) + result = parse_args({"arga": [b"1234", b"4566"], "argb": [b"hello"]}) self.assertEqual( result, - {'arga': ['1234', '4566'], 'argb' : ['hello']}, - 'Did not properly convert query parameters.' + {"arga": ["1234", "4566"], "argb": ["hello"]}, + "Did not properly convert query parameters.", ) def test_parameterize_path(self): """Should parse URLs with path parameters into regular expressions.""" - result = parameterize_path('/foo/:bar') - self.assertEqual(result, r'/foo/(?P[^\/]+)') - result = parameterize_path('/foo/:bar/baz/:quo') - self.assertEqual(result, r'/foo/(?P[^\/]+)/baz/(?P[^\/]+)') + result = parameterize_path("/foo/:bar") + self.assertEqual(result, r"/foo/(?P[^\/]+)") + result = parameterize_path("/foo/:bar/baz/:quo") + self.assertEqual(result, r"/foo/(?P[^\/]+)/baz/(?P[^\/]+)") def test_whitespace_in_paths(self): """Should handle whitespace in the path.""" - result = parameterize_path('/foo/:bar ') - self.assertEqual(result, r'/foo/(?P[^\/]+)') - result = parameterize_path('/foo/:bar/baz ') - self.assertEqual(result, r'/foo/(?P[^\/]+)/baz') + result = parameterize_path("/foo/:bar ") + self.assertEqual(result, r"/foo/(?P[^\/]+)") + result = parameterize_path("/foo/:bar/baz ") + self.assertEqual(result, r"/foo/(?P[^\/]+)/baz") def test_headers_to_dict(self): """Should parse headers into a dictionary.""" - result = headers_to_dict(MockHeaders([('Content-Type', 'application/json'), ('Set-Cookie', 'A=B'), ('Set-Cookie', 'C=D')])) - self.assertEqual(result['Content-Type'], 'application/json','Single value for header was not assigned correctly') - self.assertEqual(result['Set-Cookie'], ['A=B','C=D'],'Single value for header was not assigned correctly') + result = headers_to_dict( + MockHeaders( + [("Content-Type", "application/json"), ("Set-Cookie", "A=B"), ("Set-Cookie", "C=D")] + ) + ) + self.assertEqual( + result["Content-Type"], + "application/json", + "Single value for header was not assigned correctly", + ) + self.assertEqual( + result["Set-Cookie"], + ["A=B", "C=D"], + "Single value for header was not assigned correctly", + ) def test_headers_to_dict_with_no_headers(self): """Should parse empty headers into an empty dictionary.""" result = headers_to_dict(MockHeaders([])) - self.assertEqual(result, {},'Empty headers handled incorrectly and did not make empty dict') + self.assertEqual( + result, {}, "Empty headers handled incorrectly and did not make empty dict" + ) def test_format_request_code_not_escaped(self): """Should handle quotes in headers.""" - test_request = ('''{"body": "", "headers": {"Accept-Language": "en-US,en;q=0.8", + test_request = """{"body": "", "headers": {"Accept-Language": "en-US,en;q=0.8", "If-None-Match": "9a28a9262f954494a8de7442c63d6d0715ce0998", - "Accept-Encoding": "gzip, deflate, sdch"}, "args": {}, "path": {}}''') + "Accept-Encoding": "gzip, deflate, sdch"}, "args": {}, "path": {}}""" request_code = format_request(test_request) - #Get the value of REQUEST = "{ to test for equality - test_request_js_value = request_code[request_code.index("\"{"):] - self.assertEqual(test_request, json.loads(test_request_js_value), "Request code without escaped quotes was not formatted correctly") + # Get the value of REQUEST = "{ to test for equality + test_request_js_value = request_code[request_code.index('"{') :] + self.assertEqual( + test_request, + json.loads(test_request_js_value), + "Request code without escaped quotes was not formatted correctly", + ) def test_format_request_code_escaped(self): """Should handle backslash escaped characeters in headers.""" - test_request = ('''{"body": "", "headers": {"Accept-Language": "en-US,en;q=0.8", + test_request = """{"body": "", "headers": {"Accept-Language": "en-US,en;q=0.8", "If-None-Match": "\"\"9a28a9262f954494a8de7442c63d6d0715ce0998\"\"", - "Accept-Encoding": "gzip, deflate, sdch"}, "args": {}, "path": {}}''') + "Accept-Encoding": "gzip, deflate, sdch"}, "args": {}, "path": {}}""" request_code = format_request(test_request) - #Get the value of REQUEST = "{ to test for equality - test_request_js_value = request_code[request_code.index("\"{"):] - self.assertEqual(test_request, json.loads(test_request_js_value), "Escaped Request code was not formatted correctly") + # Get the value of REQUEST = "{ to test for equality + test_request_js_value = request_code[request_code.index('"{') :] + self.assertEqual( + test_request, + json.loads(test_request_js_value), + "Escaped Request code was not formatted correctly", + ) def test_format_request_without_a_kernel_language_arg(self): - test_request = ('''{"body": "", "headers": {}, "args": {}, "path": {}}''') + test_request = """{"body": "", "headers": {}, "args": {}, "path": {}}""" request_code = format_request(test_request) - self.assertTrue(request_code.startswith("REQUEST"), "Call format_request without a kernel_language argument was not formatted correctly") + self.assertTrue( + request_code.startswith("REQUEST"), + "Call format_request without a kernel_language argument was not formatted correctly", + ) def test_format_request_with_a_kernel_language_python(self): - test_request = ('''{"body": "", "headers": {}, "args": {}, "path": {}}''') - request_code = format_request(test_request, 'python') - self.assertTrue(request_code.startswith("REQUEST"), 'Call format_request with a kernel_language "python" was not formatted correctly') + test_request = """{"body": "", "headers": {}, "args": {}, "path": {}}""" + request_code = format_request(test_request, "python") + self.assertTrue( + request_code.startswith("REQUEST"), + 'Call format_request with a kernel_language "python" was not formatted correctly', + ) def test_format_request_with_a_kernel_language_scala(self): - test_request = ('''{"body": "", "headers": {}, "args": {}, "path": {}}''') - request_code = format_request(test_request, 'scala') - self.assertTrue(request_code.startswith("REQUEST"), 'Call format_request with a kernel_language "scala" was not formatted correctly') + test_request = """{"body": "", "headers": {}, "args": {}, "path": {}}""" + request_code = format_request(test_request, "scala") + self.assertTrue( + request_code.startswith("REQUEST"), + 'Call format_request with a kernel_language "scala" was not formatted correctly', + ) def test_format_request_with_a_kernel_language_perl(self): - test_request = ('''{"body": "", "headers": {}, "args": {}, "path": {}}''') - request_code = format_request(test_request, 'perl') - self.assertTrue(request_code.startswith("my $REQUEST"), 'Call format_request with a kernel language "perl" was not formatted correctly') + test_request = """{"body": "", "headers": {}, "args": {}, "path": {}}""" + request_code = format_request(test_request, "perl") + self.assertTrue( + request_code.startswith("my $REQUEST"), + 'Call format_request with a kernel language "perl" was not formatted correctly', + ) def test_format_request_with_a_kernel_language_bash(self): - test_request = ('''{"body": "", "headers": {}, "args": {}, "path": {}}''') - request_code = format_request(test_request, 'bash') - self.assertTrue(request_code.startswith("REQUEST=\"{"), 'Call format_request with a kernel language "bash" was not formatted correctly') + test_request = """{"body": "", "headers": {}, "args": {}, "path": {}}""" + request_code = format_request(test_request, "bash") + self.assertTrue( + request_code.startswith('REQUEST="{'), + 'Call format_request with a kernel language "bash" was not formatted correctly', + ) diff --git a/kernel_gateway/tests/resources/public/index.html b/kernel_gateway/tests/resources/public/index.html index 4c03195..12eaa9f 100644 --- a/kernel_gateway/tests/resources/public/index.html +++ b/kernel_gateway/tests/resources/public/index.html @@ -1,9 +1,9 @@ - - Hello world! - - -

Hello world!

- + + Hello world! + + +

Hello world!

+ diff --git a/kernel_gateway/tests/resources/responses.ipynb b/kernel_gateway/tests/resources/responses.ipynb index 2355a07..0021108 100644 --- a/kernel_gateway/tests/resources/responses.ipynb +++ b/kernel_gateway/tests/resources/responses.ipynb @@ -20,7 +20,7 @@ "outputs": [], "source": [ "# GET /json\n", - "print('''{ \"hello\" : \"world\"}''')" + "print(\"\"\"{ \"hello\" : \"world\"}\"\"\")" ] }, { @@ -32,12 +32,7 @@ "outputs": [], "source": [ "# ResponseInfo GET /json\n", - "print(json.dumps({\n", - " 'headers' : {\n", - " 'Content-Type' : 'application/json'\n", - " }\n", - " })\n", - ")" + "print(json.dumps({\"headers\": {\"Content-Type\": \"application/json\"}}))" ] }, { @@ -60,10 +55,7 @@ "outputs": [], "source": [ "# ResponseInfo GET /nocontent\n", - "print(json.dumps({\n", - " 'status' : 204\n", - " })\n", - ")" + "print(json.dumps({\"status\": 204}))" ] }, { @@ -75,7 +67,7 @@ "outputs": [], "source": [ "# GET /etag\n", - "print('''{ \"hello\" : \"world\"}''')" + "print(\"\"\"{ \"hello\" : \"world\"}\"\"\")" ] }, { @@ -87,13 +79,7 @@ "outputs": [], "source": [ "# ResponseInfo GET /etag\n", - "print(json.dumps({\n", - " 'headers' : {\n", - " 'Content-Type' : 'application/json',\n", - " 'Etag' : '1234567890'\n", - " }\n", - " })\n", - ")" + "print(json.dumps({\"headers\": {\"Content-Type\": \"application/json\", \"Etag\": \"1234567890\"}}))" ] }, { diff --git a/kernel_gateway/tests/resources/simple_api.ipynb b/kernel_gateway/tests/resources/simple_api.ipynb index b6c8563..456cf53 100644 --- a/kernel_gateway/tests/resources/simple_api.ipynb +++ b/kernel_gateway/tests/resources/simple_api.ipynb @@ -19,7 +19,7 @@ }, "outputs": [], "source": [ - "name = 'Test Name'" + "name = \"Test Name\"" ] }, { @@ -44,7 +44,7 @@ "source": [ "# POST /name\n", "req = json.loads(REQUEST)\n", - "name = req['body']\n", + "name = req[\"body\"]\n", "print(name)" ] } diff --git a/kernel_gateway/tests/resources/weirdly%20named#notebook.ipynb b/kernel_gateway/tests/resources/weirdly%20named#notebook.ipynb index b6c8563..456cf53 100644 --- a/kernel_gateway/tests/resources/weirdly%20named#notebook.ipynb +++ b/kernel_gateway/tests/resources/weirdly%20named#notebook.ipynb @@ -19,7 +19,7 @@ }, "outputs": [], "source": [ - "name = 'Test Name'" + "name = \"Test Name\"" ] }, { @@ -44,7 +44,7 @@ "source": [ "# POST /name\n", "req = json.loads(REQUEST)\n", - "name = req['body']\n", + "name = req[\"body\"]\n", "print(name)" ] } diff --git a/kernel_gateway/tests/test_gatewayapp.py b/kernel_gateway/tests/test_gatewayapp.py index d250fde..4e1bb10 100644 --- a/kernel_gateway/tests/test_gatewayapp.py +++ b/kernel_gateway/tests/test_gatewayapp.py @@ -9,7 +9,7 @@ from kernel_gateway.gatewayapp import KernelGatewayApp from kernel_gateway import __version__ -RESOURCES = os.path.join(os.path.dirname(__file__), 'resources') +RESOURCES = os.path.join(os.path.dirname(__file__), "resources") class TestGatewayAppConfig: diff --git a/kernel_gateway/tests/test_jupyter_websocket.py b/kernel_gateway/tests/test_jupyter_websocket.py index 07d9414..1cc2f34 100644 --- a/kernel_gateway/tests/test_jupyter_websocket.py +++ b/kernel_gateway/tests/test_jupyter_websocket.py @@ -35,17 +35,17 @@ def jp_server_config(): def spawn_kernel(jp_fetch, jp_http_port, jp_base_url, jp_ws_fetch): """Spawns a kernel where request.param contains the request body and returns the websocket.""" - async def _spawn_kernel(body='{}'): + async def _spawn_kernel(body="{}"): # Request a kernel response = await jp_fetch("api", "kernels", method="POST", body=body) assert response.code == 201 # Connect to the kernel via websocket kernel = json_decode(response.body) - kernel_id = kernel['id'] + kernel_id = kernel["id"] ws = await jp_ws_fetch("api", "kernels", kernel_id, "channels") return ws - + return _spawn_kernel @@ -63,23 +63,18 @@ def get_execute_request(code: str) -> dict: The message """ return { - 'header': { - 'username': '', - 'version': '5.0', - 'session': '', - 'msg_id': 'fake-msg-id', - 'msg_type': 'execute_request' - }, - 'parent_header': {}, - 'channel': 'shell', - 'content': { - 'code': code, - 'silent': False, - 'store_history': False, - 'user_expressions': {} + "header": { + "username": "", + "version": "5.0", + "session": "", + "msg_id": "fake-msg-id", + "msg_type": "execute_request", }, - 'metadata': {}, - 'buffers': {} + "parent_header": {}, + "channel": "shell", + "content": {"code": code, "silent": False, "store_history": False, "user_expressions": {}}, + "metadata": {}, + "buffers": {}, } @@ -96,6 +91,7 @@ async def await_stream(ws): class TestDefaults: """Tests gateway behavior.""" + @pytest.mark.parametrize("jp_argv", (["--JupyterWebsocketPersonality.list_kernels=True"],)) async def test_startup(self, jp_fetch, jp_argv): """Root of kernels resource should be OK.""" @@ -119,17 +115,28 @@ async def test_headless(self, jp_fetch): async def test_check_origin(self, jp_fetch, jp_web_app): """Allow origin setting should pass through to base handlers.""" with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", "kernelspecs", - headers={'Origin': 'fake.com:8888'}, method="GET") + await jp_fetch("api", "kernelspecs", headers={"Origin": "fake.com:8888"}, method="GET") assert e.value.code == 404 - jp_web_app.settings['allow_origin'] = '*' + jp_web_app.settings["allow_origin"] = "*" - response = await jp_fetch("api", "kernelspecs", - headers={'Origin': 'fake.com:8888'}, method="GET") + response = await jp_fetch( + "api", "kernelspecs", headers={"Origin": "fake.com:8888"}, method="GET" + ) assert response.code == 200 - @pytest.mark.parametrize("jp_server_config", (Config({"KernelGatewayApp": {"api": "notebook-gopher", }}),)) + @pytest.mark.parametrize( + "jp_server_config", + ( + Config( + { + "KernelGatewayApp": { + "api": "notebook-gopher", + } + } + ), + ), + ) async def test_config_bad_api_value(self, jp_configurable_serverapp, jp_server_config): """Should raise an ImportError for nonexistent API personality modules.""" with pytest.raises(ImportError): @@ -138,10 +145,21 @@ async def test_config_bad_api_value(self, jp_configurable_serverapp, jp_server_c async def test_options_without_auth_token(self, jp_fetch, jp_web_app): """OPTIONS requests doesn't need to submit a token. Used for CORS preflight.""" # Confirm that OPTIONS request doesn't require token - response = await jp_fetch("api", method='OPTIONS') + response = await jp_fetch("api", method="OPTIONS") assert response.code == 200 - @pytest.mark.parametrize("jp_server_config", (Config({"KernelGatewayApp": {"auth_token": "fake-token", }}),)) + @pytest.mark.parametrize( + "jp_server_config", + ( + Config( + { + "KernelGatewayApp": { + "auth_token": "fake-token", + } + } + ), + ), + ) async def test_auth_token(self, jp_server_config, jp_fetch, jp_web_app, jp_ws_fetch): """All server endpoints should check the configured auth token.""" @@ -150,110 +168,123 @@ async def test_auth_token(self, jp_server_config, jp_fetch, jp_web_app, jp_ws_fe # to be set, so setting the empty authorization header is necessary for the tests # asserting 401. with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", method="GET", headers={'Authorization': ''}) + await jp_fetch("api", method="GET", headers={"Authorization": ""}) assert e.value.response.code == 401 # Now with it - response = await jp_fetch("api", method="GET", - headers={'Authorization': 'token fake-token'}) + response = await jp_fetch( + "api", method="GET", headers={"Authorization": "token fake-token"} + ) assert response.code == 200 # Request kernelspecs without the token with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", "kernelspecs", method="GET", headers={'Authorization': ''}) + await jp_fetch("api", "kernelspecs", method="GET", headers={"Authorization": ""}) assert e.value.response.code == 401 # Now with it - response = await jp_fetch("api", "kernelspecs", method="GET", - headers={'Authorization': 'token fake-token'}) + response = await jp_fetch( + "api", "kernelspecs", method="GET", headers={"Authorization": "token fake-token"} + ) assert response.code == 200 # Request a kernel without the token with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", "kernels", method="POST", body='{}', headers={'Authorization': ''}) + await jp_fetch( + "api", "kernels", method="POST", body="{}", headers={"Authorization": ""} + ) assert e.value.response.code == 401 # Now with it - response = await jp_fetch("api", "kernels", method="POST", body='{}', - headers={'Authorization': 'token fake-token'}) + response = await jp_fetch( + "api", + "kernels", + method="POST", + body="{}", + headers={"Authorization": "token fake-token"}, + ) assert response.code == 201 kernel = json_decode(response.body) - kernel_id = url_escape(kernel['id']) + kernel_id = url_escape(kernel["id"]) # Request kernel info without the token with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", "kernels", kernel_id, method="GET", headers={'Authorization': ''}) + await jp_fetch("api", "kernels", kernel_id, method="GET", headers={"Authorization": ""}) assert e.value.response.code == 401 # Now with it - response = await jp_fetch("api", "kernels", kernel_id, method="GET", - headers={'Authorization': 'token fake-token'}) + response = await jp_fetch( + "api", "kernels", kernel_id, method="GET", headers={"Authorization": "token fake-token"} + ) assert response.code == 200 # Request websocket connection without the token # No option to ignore errors so try/except with pytest.raises(HTTPClientError) as e: - await jp_ws_fetch("api", "kernels", kernel_id, "channels", headers={'Authorization': ''}) + await jp_ws_fetch( + "api", "kernels", kernel_id, "channels", headers={"Authorization": ""} + ) assert e.value.response.code == 401 # Now request the websocket with the token - ws = await jp_ws_fetch("api", "kernels", kernel_id, "channels", - headers={'Authorization': 'token fake-token'}) + ws = await jp_ws_fetch( + "api", "kernels", kernel_id, "channels", headers={"Authorization": "token fake-token"} + ) ws.close() async def test_cors_headers(self, jp_fetch, jp_web_app): """All kernel endpoints should respond with configured CORS headers.""" - jp_web_app.settings['kg_allow_credentials'] = 'false' - jp_web_app.settings['kg_allow_headers'] = 'Authorization,Content-Type' - jp_web_app.settings['kg_allow_methods'] = 'GET,POST' - jp_web_app.settings['kg_allow_origin'] = 'https://jupyter.org' - jp_web_app.settings['kg_expose_headers'] = 'X-My-Fake-Header' - jp_web_app.settings['kg_max_age'] = '600' - jp_web_app.settings['kg_list_kernels'] = True + jp_web_app.settings["kg_allow_credentials"] = "false" + jp_web_app.settings["kg_allow_headers"] = "Authorization,Content-Type" + jp_web_app.settings["kg_allow_methods"] = "GET,POST" + jp_web_app.settings["kg_allow_origin"] = "https://jupyter.org" + jp_web_app.settings["kg_expose_headers"] = "X-My-Fake-Header" + jp_web_app.settings["kg_max_age"] = "600" + jp_web_app.settings["kg_list_kernels"] = True # Get kernels to check headers response = await jp_fetch("api", "kernels", method="GET") assert response.code == 200 - assert response.headers['Access-Control-Allow-Credentials'] == 'false' - assert response.headers['Access-Control-Allow-Headers'] == 'Authorization,Content-Type' - assert response.headers['Access-Control-Allow-Methods'] == 'GET,POST' - assert response.headers['Access-Control-Allow-Origin'] == 'https://jupyter.org' - assert response.headers['Access-Control-Expose-Headers'] == 'X-My-Fake-Header' - assert response.headers['Access-Control-Max-Age'] == '600' - assert response.headers.get('Content-Security-Policy') is None + assert response.headers["Access-Control-Allow-Credentials"] == "false" + assert response.headers["Access-Control-Allow-Headers"] == "Authorization,Content-Type" + assert response.headers["Access-Control-Allow-Methods"] == "GET,POST" + assert response.headers["Access-Control-Allow-Origin"] == "https://jupyter.org" + assert response.headers["Access-Control-Expose-Headers"] == "X-My-Fake-Header" + assert response.headers["Access-Control-Max-Age"] == "600" + assert response.headers.get("Content-Security-Policy") is None async def test_cors_options_headers(self, jp_fetch, jp_web_app): """All preflight OPTIONS requests should return configured headers.""" - jp_web_app.settings['kg_allow_headers'] = 'X-XSRFToken' - jp_web_app.settings['kg_allow_methods'] = 'GET,POST,OPTIONS' + jp_web_app.settings["kg_allow_headers"] = "X-XSRFToken" + jp_web_app.settings["kg_allow_methods"] = "GET,POST,OPTIONS" - response = await jp_fetch("api", "kernelspecs", method='OPTIONS') + response = await jp_fetch("api", "kernelspecs", method="OPTIONS") assert response.code == 200 - assert response.headers['Access-Control-Allow-Methods'] == 'GET,POST,OPTIONS' - assert response.headers['Access-Control-Allow-Headers'] == 'X-XSRFToken' + assert response.headers["Access-Control-Allow-Methods"] == "GET,POST,OPTIONS" + assert response.headers["Access-Control-Allow-Headers"] == "X-XSRFToken" async def test_max_kernels(self, jp_fetch, jp_web_app): """Number of kernels should be limited.""" - jp_web_app.settings['kg_max_kernels'] = 1 + jp_web_app.settings["kg_max_kernels"] = 1 # Request a kernel - response = await jp_fetch("api", "kernels", method="POST", body='{}') + response = await jp_fetch("api", "kernels", method="POST", body="{}") assert response.code == 201 # Request another with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", "kernels", method="POST", body='{}') + await jp_fetch("api", "kernels", method="POST", body="{}") assert e.value.response.code == 403 # Shut down the kernel kernel = json_decode(response.body) - response = await jp_fetch("api", "kernels", url_escape(kernel['id']), method="DELETE") + response = await jp_fetch("api", "kernels", url_escape(kernel["id"]), method="DELETE") assert response.code == 204 # Try creation again - response = await jp_fetch("api", "kernels", method="POST", body='{}') + response = await jp_fetch("api", "kernels", method="POST", body="{}") assert response.code == 201 async def test_get_api(self, jp_fetch): @@ -261,26 +292,26 @@ async def test_get_api(self, jp_fetch): response = await jp_fetch("api", method="GET") assert response.code == 200 info = json_decode(response.body) - assert 'version' in info + assert "version" in info async def test_get_kernelspecs(self, jp_fetch): """Server should respond with kernel spec metadata.""" response = await jp_fetch("api", "kernelspecs", method="GET") assert response.code == 200 specs = json_decode(response.body) - assert 'kernelspecs' in specs - assert 'default' in specs + assert "kernelspecs" in specs + assert "default" in specs async def test_get_kernels(self, jp_fetch, jp_web_app): """Server should respond with running kernel information.""" - jp_web_app.settings['kg_list_kernels'] = True + jp_web_app.settings["kg_list_kernels"] = True response = await jp_fetch("api", "kernels", method="GET") assert response.code == 200 kernels = json_decode(response.body) assert len(kernels) == 0 # Launch a kernel - response = await jp_fetch("api", "kernels", method="POST", body='{}') + response = await jp_fetch("api", "kernels", method="POST", body="{}") assert response.code == 201 kernel = json_decode(response.body) @@ -289,36 +320,40 @@ async def test_get_kernels(self, jp_fetch, jp_web_app): assert response.code == 200 kernels = json_decode(response.body) assert len(kernels) == 1 - assert kernels[0]['id'] == kernel['id'] + assert kernels[0]["id"] == kernel["id"] async def test_kernel_comm(self, spawn_kernel): """Default kernel should launch and accept commands.""" ws = await spawn_kernel() # Send a request for kernel info - await ws.write_message(json_encode({ - 'header': { - 'username': '', - 'version': '5.0', - 'session': '', - 'msg_id': 'fake-msg-id', - 'msg_type': 'kernel_info_request' - }, - 'parent_header': {}, - 'channel': 'shell', - 'content': {}, - 'metadata': {}, - 'buffers': {} - })) + await ws.write_message( + json_encode( + { + "header": { + "username": "", + "version": "5.0", + "session": "", + "msg_id": "fake-msg-id", + "msg_type": "kernel_info_request", + }, + "parent_header": {}, + "channel": "shell", + "content": {}, + "metadata": {}, + "buffers": {}, + } + ) + ) # Assert the reply comes back. Test will timeout if this hangs. for _ in range(10): msg = await ws.read_message() msg = json_decode(msg) - if msg['msg_type'] == 'kernel_info_reply': + if msg["msg_type"] == "kernel_info_reply": break else: - raise AssertionError('never received kernel_info_reply') + raise AssertionError("never received kernel_info_reply") ws.close() async def test_no_discovery(self, jp_fetch): @@ -333,7 +368,7 @@ async def test_no_discovery(self, jp_fetch): async def test_crud_sessions(self, jp_fetch, jp_web_app): """Server should create, list, and delete sessions.""" - jp_web_app.settings['kg_list_kernels'] = True + jp_web_app.settings["kg_list_kernels"] = True # Ensure no sessions by default response = await jp_fetch("api", "sessions", method="GET") @@ -342,8 +377,12 @@ async def test_crud_sessions(self, jp_fetch, jp_web_app): assert len(sessions) == 0 # Launch a session - response = await jp_fetch("api", "sessions", method="POST", - body='{"id":"any","notebook":{"path":"anywhere"},"kernel":{"name":"python"}}') + response = await jp_fetch( + "api", + "sessions", + method="POST", + body='{"id":"any","notebook":{"path":"anywhere"},"kernel":{"name":"python"}}', + ) assert response.code == 201 session = json_decode(response.body) @@ -352,10 +391,10 @@ async def test_crud_sessions(self, jp_fetch, jp_web_app): assert response.code == 200 sessions = json_decode(response.body) assert len(sessions) == 1 - assert sessions[0]['id'] == session['id'] + assert sessions[0]["id"] == session["id"] # Delete the session - response = await jp_fetch("api", "sessions", session['id'], method="DELETE") + response = await jp_fetch("api", "sessions", session["id"], method="DELETE") assert response.code == 204 # Make sure the list is empty @@ -372,7 +411,7 @@ async def test_json_errors(self, jp_fetch): assert e.value.response.code == 403 body = json_decode(e.value.response.body) - assert body['reason'] == 'Forbidden' + assert body["reason"] == "Forbidden" # A handler from the notebook base with pytest.raises(HTTPClientError) as e: @@ -380,7 +419,7 @@ async def test_json_errors(self, jp_fetch): assert e.value.response.code == 404 body = json_decode(e.value.response.body) - assert "1-2-3-4-5" in body['message'] + assert "1-2-3-4-5" in body["message"] # The last resort not found handler with pytest.raises(HTTPClientError) as e: @@ -390,27 +429,30 @@ async def test_json_errors(self, jp_fetch): body = json_decode(e.value.response.body) assert body["reason"] == "Not Found" - @pytest.mark.parametrize("jp_argv", - (["--JupyterWebsocketPersonality.env_whitelist=TEST_VAR"],)) + @pytest.mark.parametrize("jp_argv", (["--JupyterWebsocketPersonality.env_whitelist=TEST_VAR"],)) async def test_kernel_env(self, spawn_kernel, jp_argv): """Kernel should start with environment vars defined in the request.""" - - kernel_body = json.dumps({ - "name": "python", - "env": { - "KERNEL_FOO": "kernel-foo-value", - "NOT_KERNEL": "ignored", - "KERNEL_GATEWAY": "overridden", - "TEST_VAR": "allowed" + + kernel_body = json.dumps( + { + "name": "python", + "env": { + "KERNEL_FOO": "kernel-foo-value", + "NOT_KERNEL": "ignored", + "KERNEL_GATEWAY": "overridden", + "TEST_VAR": "allowed", + }, } - }) + ) ws = await spawn_kernel(kernel_body) - req = get_execute_request('import os; print(os.getenv("KERNEL_FOO"), os.getenv("NOT_KERNEL"), ' - 'os.getenv("KERNEL_GATEWAY"), os.getenv("TEST_VAR"))') - + req = get_execute_request( + 'import os; print(os.getenv("KERNEL_FOO"), os.getenv("NOT_KERNEL"), ' + 'os.getenv("KERNEL_GATEWAY"), os.getenv("TEST_VAR"))' + ) + await ws.write_message(json_encode(req)) content = await await_stream(ws) - + assert content["name"] == "stdout" assert "kernel-foo-value" in content["text"] assert "ignored" not in content["text"] @@ -445,24 +487,31 @@ async def test_kernel_env_auth_token(self, monkeypatch, spawn_kernel): ws.close() -@pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.default_kernel_name=fake-kernel"],)) +@pytest.mark.parametrize("jp_argv", (["--KernelGatewayApp.default_kernel_name=fake-kernel"],)) class TestCustomDefaultKernel: """Tests gateway behavior when setting a custom default kernelspec.""" + async def test_default_kernel_name(self, jp_argv, jp_fetch): """The default kernel name should be used on empty requests.""" with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", "kernels", method="POST", body='') + await jp_fetch("api", "kernels", method="POST", body="") assert e.value.response.code == 500 assert "raise NoSuchKernel" in str(e.value.response.body) -@pytest.mark.parametrize("jp_argv", - (["--KernelGatewayApp.prespawn_count=2", - f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'zen.ipynb')}", - "--KernelGatewayApp.force_kernel_name=python3"],)) +@pytest.mark.parametrize( + "jp_argv", + ( + [ + "--KernelGatewayApp.prespawn_count=2", + f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'zen.ipynb')}", + "--KernelGatewayApp.force_kernel_name=python3", + ], + ), +) class TestForceKernel: """Tests gateway behavior when forcing a kernelspec.""" + async def test_force_kernel_name(self, jp_argv, jp_fetch): """Should create a Python kernel.""" response = await jp_fetch("api", "kernels", method="POST", body='{"name": "fake-kernel"}') @@ -473,25 +522,27 @@ async def test_force_kernel_name(self, jp_argv, jp_fetch): class TestEnableDiscovery: """Tests gateway behavior with kernel listing enabled.""" + @pytest.mark.parametrize("jp_argv", (["--JupyterWebsocketPersonality.list_kernels=True"],)) async def test_enable_kernel_list(self, jp_fetch, jp_argv): """The list of kernels, sessions, and activities should be available.""" response = await jp_fetch("api", "kernels", method="GET") assert response.code == 200 - assert '[]' in str(response.body) + assert "[]" in str(response.body) response = await jp_fetch("api", "sessions", method="GET") assert response.code == 200 - assert '[]' in str(response.body) + assert "[]" in str(response.body) class TestPrespawnKernels: """Tests gateway behavior when kernels are spawned at startup.""" + @pytest.mark.parametrize("jp_argv", (["--KernelGatewayApp.prespawn_count=2"],)) async def test_prespawn_count(self, jp_fetch, jp_web_app, jp_argv): """Server should launch the given number of kernels on startup.""" - jp_web_app.settings['kg_list_kernels'] = True + jp_web_app.settings["kg_list_kernels"] = True await sleep(0.5) response = await jp_fetch("api", "kernels", method="GET") assert response.code == 200 @@ -510,6 +561,7 @@ def test_prespawn_max_conflict(self): class TestBaseURL: """Tests gateway behavior when a custom base URL is configured.""" + @pytest.mark.parametrize("jp_argv", (["--JupyterWebsocketPersonality.list_kernels=True"],)) @pytest.mark.parametrize("jp_base_url", ("/fake/path",)) async def test_base_url(self, jp_base_url, jp_argv, jp_fetch): @@ -522,6 +574,7 @@ async def test_base_url(self, jp_base_url, jp_argv, jp_fetch): class TestRelativeBaseURL: """Tests gateway behavior when a relative base URL is configured.""" + @pytest.mark.parametrize("jp_argv", (["--JupyterWebsocketPersonality.list_kernels=True"],)) @pytest.mark.parametrize("jp_base_url", ("/fake/path",)) async def test_base_url(self, jp_base_url, jp_argv, jp_fetch): @@ -535,8 +588,10 @@ async def test_base_url(self, jp_base_url, jp_argv, jp_fetch): class TestSeedURI: """Tests gateway behavior when a seeding kernel memory with code from a notebook.""" - @pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'zen.ipynb')}"],)) + + @pytest.mark.parametrize( + "jp_argv", ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'zen.ipynb')}"],) + ) async def test_seed(self, jp_argv, spawn_kernel): """Kernel should have variables pre-seeded from the notebook.""" ws = await spawn_kernel() @@ -554,9 +609,16 @@ async def test_seed(self, jp_argv, spawn_kernel): class TestRemoteSeedURI: """Tests gateway behavior when a seeding kernel memory with code from a remote notebook.""" - @pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.seed_uri=" - f"https://gist.githubusercontent.com/parente/ccd36bd7db2f617d58ce/raw/zen3.ipynb"],)) + + @pytest.mark.parametrize( + "jp_argv", + ( + [ + "--KernelGatewayApp.seed_uri=" + "https://gist.githubusercontent.com/parente/ccd36bd7db2f617d58ce/raw/zen3.ipynb" + ], + ), + ) async def test_seed(self, jp_argv, spawn_kernel): """Kernel should have variables pre-seeded from the notebook.""" ws = await spawn_kernel() @@ -574,9 +636,16 @@ async def test_seed(self, jp_argv, spawn_kernel): class TestBadSeedURI: """Tests gateway behavior when seeding kernel memory with notebook code that fails.""" - @pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'failing_code.ipynb')}", - "--JupyterWebsocketPersonality.list_kernels=True"],)) + + @pytest.mark.parametrize( + "jp_argv", + ( + [ + f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'failing_code.ipynb')}", + "--JupyterWebsocketPersonality.list_kernels=True", + ], + ), + ) async def test_seed_error(self, jp_argv, jp_fetch): """ Server should shutdown kernel and respond with error when seed notebook @@ -585,7 +654,7 @@ async def test_seed_error(self, jp_argv, jp_fetch): # Request a kernel with pytest.raises(HTTPClientError) as e: - await jp_fetch("api", "kernels", method='POST', body='{}') + await jp_fetch("api", "kernels", method="POST", body="{}") assert e.value.response.code == 500 # No kernels should be running @@ -599,27 +668,34 @@ async def test_seed_kernel_not_available(self): Server should error because seed notebook requires a kernel that is not installed. """ app = KernelGatewayApp() - app.seed_uri = os.path.join(RESOURCES, 'unknown_kernel.ipynb') + app.seed_uri = os.path.join(RESOURCES, "unknown_kernel.ipynb") with pytest.raises(NoSuchKernel): app.init_configurables() -@pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'zen.ipynb')}", - "--KernelGatewayApp.prespawn_count=1"],)) +@pytest.mark.parametrize( + "jp_argv", + ( + [ + f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'zen.ipynb')}", + "--KernelGatewayApp.prespawn_count=1", + ], + ), +) class TestKernelLanguageSupport: """Tests gateway behavior when a client requests a specific kernel spec.""" + async def test_seed_language_support(self, jp_argv, spawn_kernel): """Kernel should have variables pre-seeded from notebook.""" ws = await spawn_kernel(body=json.dumps({"name": "python3"})) - code = 'print(this.s)' + code = "print(this.s)" # Print the encoded "zen of python" string, the kernel should have it imported req = get_execute_request(code) await ws.write_message(json_encode(req)) content = await await_stream(ws) - assert content['name'] == 'stdout' - assert 'Gur Mra bs Clguba' in content['text'] + assert content["name"] == "stdout" + assert "Gur Mra bs Clguba" in content["text"] ws.close() @@ -628,7 +704,6 @@ class TestSessionApi: """Test session object API to improve coverage.""" async def test_session_api(self, tmp_path, jp_environ): - # Create the manager instances akm = AsyncMappingKernelManager() sm = SessionManager(akm) diff --git a/kernel_gateway/tests/test_mixins.py b/kernel_gateway/tests/test_mixins.py index 3f41b98..455ac48 100644 --- a/kernel_gateway/tests/test_mixins.py +++ b/kernel_gateway/tests/test_mixins.py @@ -12,6 +12,7 @@ class SuperTokenAuthHandler(object): """Super class for the handler using TokenAuthorizationMixin.""" + is_prepared = False def prepare(self): @@ -21,6 +22,7 @@ def prepare(self): class CustomTokenAuthHandler(TokenAuthorizationMixin, SuperTokenAuthHandler): """Implementation that uses the TokenAuthorizationMixin for testing.""" + def __init__(self, token=""): self.settings = {"kg_auth_token": token} self.arguments = {} @@ -42,6 +44,7 @@ def auth_mixin(): class TestTokenAuthMixin: """Unit tests the Token authorization mixin.""" + def test_no_token_required(self, auth_mixin): """Status should be None.""" auth_mixin.request = Mock({}) @@ -121,6 +124,7 @@ def test_unset_client_token_with_options(self, auth_mixin): class CustomJSONErrorsHandler(JSONErrorsMixin): """Implementation that uses the JSONErrorsMixin for testing.""" + def __init__(self): self.headers = {} self.response = None @@ -150,6 +154,7 @@ def errors_mixin(): class TestJSONErrorsMixin: """Unit tests the JSON errors mixin.""" + def test_status(self, errors_mixin): """Status should be set on the response.""" errors_mixin.write_error(404) diff --git a/kernel_gateway/tests/test_notebook_http.py b/kernel_gateway/tests/test_notebook_http.py index 03b701e..15334dd 100644 --- a/kernel_gateway/tests/test_notebook_http.py +++ b/kernel_gateway/tests/test_notebook_http.py @@ -50,7 +50,9 @@ async def test_api_get_endpoint_with_query_param(self, jp_fetch): async def test_api_get_endpoint_with_multiple_query_params(self, jp_fetch): """GET HTTP method should be callable with multiple query params""" - response = await jp_fetch("hello", "persons", params={"person": "governor, rick"}, method="GET") + response = await jp_fetch( + "hello", "persons", params={"person": "governor, rick"}, method="GET" + ) assert response.code == 200, "GET endpoint did not return 200." assert response.body == b"hello governor, rick\n", "Unexpected body in response to GET." @@ -61,22 +63,36 @@ async def test_api_put_endpoint(self, jp_fetch): response = await jp_fetch("message", method="GET") assert response.code == 200, "GET endpoint did not return 200." - assert response.body == b"hola {}\n", "Unexpected body in response to GET after performing PUT." + assert ( + response.body == b"hola {}\n" + ), "Unexpected body in response to GET after performing PUT." async def test_api_post_endpoint(self, jp_fetch): """POST endpoint should be callable""" expected = b'["Rick", "Maggie", "Glenn", "Carol", "Daryl"]\n' - response = await jp_fetch("people", method="POST", body=expected.decode("UTF-8"), headers={"Content-Type": "application/json"}) + response = await jp_fetch( + "people", + method="POST", + body=expected.decode("UTF-8"), + headers={"Content-Type": "application/json"}, + ) assert response.code == 200, "POST endpoint did not return 200." assert response.body == expected, "Unexpected body in response to POST." async def test_api_delete_endpoint(self, jp_fetch): """DELETE HTTP method should be callable""" expected = b'["Rick", "Maggie", "Glenn", "Carol", "Daryl"]\n' - response = await jp_fetch("people", method="POST", body=expected.decode("UTF-8"), headers={"Content-Type": "application/json"}) + response = await jp_fetch( + "people", + method="POST", + body=expected.decode("UTF-8"), + headers={"Content-Type": "application/json"}, + ) response = await jp_fetch("people", "2", method="DELETE") assert response.code == 200, "DELETE endpoint did not return 200." - assert response.body == b'["Rick", "Maggie", "Carol", "Daryl"]\n', "Unexpected body in response to DELETE." + assert ( + response.body == b'["Rick", "Maggie", "Carol", "Daryl"]\n' + ), "Unexpected body in response to DELETE." async def test_api_error_endpoint(self, jp_fetch): """Error in a cell should cause 500 HTTP status""" @@ -90,19 +106,21 @@ async def test_api_stderr_endpoint(self, jp_fetch): assert response.body == b"I am text on stdout\n", "Unexpected text in response" async def test_api_unsupported_method(self, jp_fetch): - """Endpoints which do no support an HTTP verb should respond with 405. - """ + """Endpoints which do no support an HTTP verb should respond with 405.""" with pytest.raises(HTTPClientError) as e: await jp_fetch("message", method="DELETE") - assert e.value.code == 405, "Endpoint which exists, but does not support DELETE, did not return 405 status code." + assert ( + e.value.code == 405 + ), "Endpoint which exists, but does not support DELETE, did not return 405 status code." async def test_api_undefined(self, jp_fetch): - """Endpoints which are not registered at all should respond with 404. - """ + """Endpoints which are not registered at all should respond with 404.""" with pytest.raises(HTTPClientError) as e: await jp_fetch("not", "an", "endpoint", method="GET") - assert e.value.code == 404, "Endpoint which should not exist did not return 404 status code." + assert ( + e.value.code == 404 + ), "Endpoint which should not exist did not return 404 status code." body = json.loads(e.value.response.body.decode("UTF-8")) assert body["reason"] == "Not Found" @@ -110,15 +128,24 @@ async def test_api_access_http_header(self, jp_fetch): """HTTP endpoints should be able to access request headers""" content_types = ["text/plain", "application/json", "application/atom+xml", "foo"] for content_type in content_types: - response = await jp_fetch("content-type", method="GET", headers={"Content-Type": content_type}) + response = await jp_fetch( + "content-type", method="GET", headers={"Content-Type": content_type} + ) assert response.code == 200, "GET endpoint did not return 200." - assert response.body.decode(encoding="UTF-8") == f"{content_type}\n", "Unexpected value in response" + assert ( + response.body.decode(encoding="UTF-8") == f"{content_type}\n" + ), "Unexpected value in response" async def test_format_request_code_escaped_integration(self, jp_fetch): """Quotes should be properly escaped in request headers.""" # Test query with escaping of arguments and headers with multiple escaped quotes - response = await jp_fetch("hello", "person", params={"person": "governor"}, method="GET", - headers={"If-None-Match": '\"\"9a28a9262f954494a8de7442c63d6d0715ce0998\"\"'}) + response = await jp_fetch( + "hello", + "person", + params={"person": "governor"}, + method="GET", + headers={"If-None-Match": '""9a28a9262f954494a8de7442c63d6d0715ce0998""'}, + ) assert response.code == 200, "GET endpoint did not return 200." assert response.body == b"hello governor\n", "Unexpected body in response to GET." @@ -157,8 +184,9 @@ async def test_kernel_gateway_environment_set(self, jp_fetch): assert response.body == b"KERNEL_GATEWAY is 1\n", "Unexpected body in response to GET." -@pytest.mark.parametrize("jp_argv", - ([f"--NotebookHTTPPersonality.static_path={os.path.join(RESOURCES, 'public')}"],)) +@pytest.mark.parametrize( + "jp_argv", ([f"--NotebookHTTPPersonality.static_path={os.path.join(RESOURCES, 'public')}"],) +) class TestPublicStatic: """Tests gateway behavior when public static assets are enabled.""" @@ -169,8 +197,7 @@ async def test_get_public(self, jp_fetch, jp_argv): assert response.headers.get("Content-Type") == "text/html" -@pytest.mark.parametrize("jp_argv", - (["--NotebookHTTPPersonality.allow_notebook_download=True"],)) +@pytest.mark.parametrize("jp_argv", (["--NotebookHTTPPersonality.allow_notebook_download=True"],)) class TestSourceDownload: """Tests gateway behavior when notebook download is allowed.""" @@ -183,8 +210,9 @@ async def test_download_notebook_source(self, jp_fetch, jp_argv): assert key in nb -@pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'responses.ipynb')}"],)) +@pytest.mark.parametrize( + "jp_argv", ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'responses.ipynb')}"],) +) class TestCustomResponse: """Tests gateway behavior when the notebook contains ResponseInfo cells.""" @@ -193,7 +221,9 @@ async def test_setting_content_type(self, jp_fetch, jp_argv): response = await jp_fetch("json", method="GET") result = json.loads(response.body.decode("UTF-8")) assert response.code == 200, "Response status was not 200" - assert response.headers["Content-Type"] == "application/json", "Incorrect mime type was set on response" + assert ( + response.headers["Content-Type"] == "application/json" + ), "Incorrect mime type was set on response" assert result == {"hello": "world"}, "Incorrect response value." async def test_setting_response_status_code(self, jp_fetch, jp_argv): @@ -207,26 +237,31 @@ async def test_setting_etag_header(self, jp_fetch, jp_argv): response = await jp_fetch("etag", method="GET") result = json.loads(response.body.decode("UTF-8")) assert response.code == 200, "Response status was not 200" - assert response.headers["Content-Type"] == "application/json", "Incorrect mime type was set on response" - assert result, {"hello" : "world"} == "Incorrect response value." + assert ( + response.headers["Content-Type"] == "application/json" + ), "Incorrect mime type was set on response" + assert result, {"hello": "world"} == "Incorrect response value." assert response.headers["Etag"] == "1234567890", "Incorrect Etag header value." @pytest.mark.parametrize("jp_argv", (["--KernelGatewayApp.prespawn_count=3"],)) class TestKernelPool: - async def test_should_cycle_through_kernels(self, jp_fetch, jp_argv): """Requests should cycle through kernels""" - response = await jp_fetch("message", method="PUT", body='hola {}') - assert response.code == 200, 'PUT endpoint did not return 200.' + response = await jp_fetch("message", method="PUT", body="hola {}") + assert response.code == 200, "PUT endpoint did not return 200." for i in range(3): response = await jp_fetch("message", method="GET") if i != 2: - assert response.body == b"hello {}\n", "Unexpected body in response to GET after performing PUT." + assert ( + response.body == b"hello {}\n" + ), "Unexpected body in response to GET after performing PUT." else: - assert response.body == b"hola {}\n", "Unexpected body in response to GET after performing PUT." + assert ( + response.body == b"hola {}\n" + ), "Unexpected body in response to GET after performing PUT." @pytest.mark.timeout(10) async def test_concurrent_request_should_not_be_blocked(self, jp_fetch, jp_argv): @@ -235,14 +270,15 @@ async def test_concurrent_request_should_not_be_blocked(self, jp_fetch, jp_argv) assert response_long_running.done() is False, "Long HTTP Request is not running" response_short_running = await jp_fetch("sleep", "3", method="GET") - assert response_short_running.code == 200, "Short HTTP Request did not return proper status code of 200" + assert ( + response_short_running.code == 200 + ), "Short HTTP Request did not return proper status code of 200" assert response_long_running.done() is False, "Long HTTP Request is not running" while not response_long_running.done(): await asyncio.sleep(0.3) # let the long request complete async def test_locking_semaphore_of_kernel_resources(self, jp_fetch, jp_argv): - """Kernel pool should prevent more than one request from running on a kernel at a time. - """ + """Kernel pool should prevent more than one request from running on a kernel at a time.""" futures = [] for _ in range(7): futures.append(jp_fetch("sleep", "1", method="GET")) @@ -258,37 +294,43 @@ async def test_locking_semaphore_of_kernel_resources(self, jp_fetch, jp_argv): await future -@pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'simple_api.ipynb')}"],)) +@pytest.mark.parametrize( + "jp_argv", ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'simple_api.ipynb')}"],) +) class TestSwaggerSpec: async def test_generation_of_swagger_spec(self, jp_fetch, jp_argv): """Server should expose a swagger specification of its notebook-defined API. """ expected_response = { - "info": { - "version": "0.0.0", - "title": "simple_api" - }, + "info": {"version": "0.0.0", "title": "simple_api"}, "paths": { "/name": { "get": {"responses": {"200": {"description": "Success"}}}, - "post": {"responses": {"200": {"description": "Success"}}} + "post": {"responses": {"200": {"description": "Success"}}}, } }, - "swagger": "2.0" + "swagger": "2.0", } response = await jp_fetch("_api", "spec", "swagger.json", method="GET") result = json.loads(response.body.decode("UTF-8")) assert response.code == 200, "Swagger spec endpoint did not return the correct status code" assert result == expected_response, "Swagger spec endpoint did not return the correct value" - assert SwaggerSpecHandler.output is not None, "Swagger spec output wasn't cached for later requests" - - -@pytest.mark.parametrize("jp_argv", - ([f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'unknown_kernel.ipynb')}", - "--KernelGatewayApp.force_kernel_name=python3"],)) + assert ( + SwaggerSpecHandler.output is not None + ), "Swagger spec output wasn't cached for later requests" + + +@pytest.mark.parametrize( + "jp_argv", + ( + [ + f"--KernelGatewayApp.seed_uri={os.path.join(RESOURCES, 'unknown_kernel.ipynb')}", + "--KernelGatewayApp.force_kernel_name=python3", + ], + ), +) class TestForceKernel: async def test_force_kernel_spec(self, jp_fetch, jp_argv): """Should start properly..""" diff --git a/readthedocs.yml b/readthedocs.yml index f95fbe2..db58ec3 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,5 +1,4 @@ conda: - file: docs/environment.yml + file: docs/environment.yml python: - version: 3 - + version: 3 diff --git a/setup.py b/setup.py index 8bf1ba9..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,2 +1,3 @@ from setuptools import setup + setup()