Compare commits

...

189 Commits
main ... 4.x

Author SHA1 Message Date
Grant Nestor 2c96ad2d42 Backport PR #2421: Cannot run Jupyter lab subcommands from root
9 years ago
Grant Nestor bfb47641e0 release 4.4.1
9 years ago
Grant Nestor 3b65aae93b release 4.4.0
9 years ago
Grant Nestor 2c35de536d Merge branch '4.x' of https://github.com/jupyter/notebook into jupyter/4.x
9 years ago
Grant Nestor 860d0d8335 Merge pull request #2164 from gnestor/backport-4.4
9 years ago
Grant Nestor 76980079bc Add 4.4 to changelog
9 years ago
Sylvain Corlay abb9f739d2 Merge pull request #2022 from jasongrout/callbackid-4.x
9 years ago
Sylvain Corlay 82b59d65f4 Merge pull request #2022 from jasongrout/callbackid-4.x
9 years ago
Grant Nestor b8c53928ea release 4.3.2
9 years ago
Thomas Kluyver 48b8d47d01 Merge pull request #2103 from gnestor/4.3.2
9 years ago
Grant Nestor 5497ef7ac6 Update changelog
9 years ago
Min RK cd258caec4 Merge pull request #2090 from Carreau/backport-2066
9 years ago
Steven Silvester b5a431fb11 Merge pull request #2092 from minrk/pip-travis
9 years ago
Min RK 2c8f618e73 upgrade pip, setuptools on Travis
9 years ago
Matthias Bussonnier 3ce1670833 Merge pull request #11 from gnestor/backport-2066
9 years ago
Grant Nestor d2f2e7acbd Add `shutil` and remove `npm_components`
9 years ago
Min RK 9c34b44959 Backport PR #2066 on branch 4.x
9 years ago
Matthias Bussonnier 0273b3fb7c Merge pull request #2039 from jupyter/auto-backport-of-pr-1482
9 years ago
Matthias Bussonnier 5302351b14 Backport PR #1482: Respect 'editable' cell metadata
9 years ago
Jason Grout 77ba557156 Backport #1958: Add an output callback override stack
9 years ago
Min RK 4727efcaf8 release 4.3.1
9 years ago
Min RK ed9399d9ad Merge branch 'fix-xsrf' into 4.x
9 years ago
Min RK 6d3cf8b180 Backport PR #2004: Downgrade to CodeMirror 5.16
9 years ago
Min RK c6485a33c2 note downgrade of CodeMirror in changelog
9 years ago
Min RK b371d8aed1 prose review
9 years ago
Min RK c231fd2cdf changelog notes for 4.3.1
9 years ago
Min RK e98fba2a50 allow disabling xsrf check
9 years ago
Min RK de60f6d6bd run Python tests with a token
9 years ago
Min RK efdbef117f get xsrf from cookie, not body data
9 years ago
Min RK c5bb329bf8 use tornado xsrf token in API
9 years ago
Min RK fb8640a072 add token_authenticated property
9 years ago
Min RK 8abc6dff77 enable tornado xsrf cookie
9 years ago
Min RK 18596deffa Backport PR #2003: Add 4.3.1 to changelog
9 years ago
Min RK 8e3cc4eab4 traitlets api backport
9 years ago
Grant Nestor 8b77f0002e Backport PR #1994: Further highlight token info in log output
9 years ago
Min RK dd8bf2b91b Backport PR #1985: Ignore resize events that bubbled up and didn't come from window.
9 years ago
Min RK 6ada253bd9 Backport PR #1975: add Authorization to allowed CORS headers
9 years ago
Thomas Kluyver f50aa57dab Merge pull request #1983 from minrk/fixCR
9 years ago
Min RK a77d5c3133 fix carriage return handling
9 years ago
Thomas Kluyver ad5c1a2c2e Backport PR #1972: better docs for token auth
9 years ago
Min RK 7cb285d813 Backport PR #1956: Make the font size more robust against fickle browser values.
9 years ago
Grant Nestor a53a2152dc release 4.3
9 years ago
Kyle Kelley 700495993c Backport PR #1949: Fixed typo in 1st line of Browser Compatibility section
9 years ago
Thomas Kluyver 9e50aef668 Backport PR #1955: Add 4.3 to changelog
9 years ago
Kyle Kelley 8f85f90b59 Backport PR #1947: Ensure variable is set if exc_info is falsey
9 years ago
Min RK 5c5c45fc5d Backport PR #1939: Add debug log for static file paths
9 years ago
Kyle Kelley ab791441b2 Backport PR #1938: don't check origin on token-authenticated requests
9 years ago
Grant Nestor 45503a8c87 Backport PR #1952: Set mimerender index in `OutputArea.display_order`
9 years ago
Thomas Kluyver 19eac71065 Backport PR #1941: Catch and log handler exceptions in events.trigger
9 years ago
Kyle Kelley 0b4e999b15 Backport PR #1921: CodeMirror 5.21
9 years ago
Kyle Kelley 1292d8bcc9 Backport PR #1919: remove leftover print statement
9 years ago
Matthias Bussonnier 17c551e92c Backport PR #1871: Allow None for post_save_hook
9 years ago
Thomas Kluyver 8d3c7fc58c Backport PR #1861: fix notebook mime-type on download links
9 years ago
Thomas Kluyver 87c8b1af3c Backport PR #1822: Convert readthedocs links for their .org -> .io migration for hosted projects
9 years ago
Matthias Bussonnier 8ea91b94d9 Backport PR #1792: Updated Jupyter Logos to Fit New Guidelines
9 years ago
Grant Nestor 97a2de59f3 Backport PR #1753: docs: Update dead links
9 years ago
Min RK 1ab053447a Backport PR #1736: Attempted to correct a URL that was a dead link.
9 years ago
Carol Willing 49fc47e2bb Backport PR #1713: Add note on JupyterHub to docs
9 years ago
Matthias Bussonnier 9d44ea2182 Backport PR #1669: If kernel is broken, start a new session
9 years ago
Thomas Kluyver 3008da041b Backport PR #1912: Fix highlighting of Python code blocks
9 years ago
Matthias Bussonnier 930823b22a Backport PR #1908: Improve `Notebook.render_cell_output`
9 years ago
Matthias Bussonnier 8eb55fd6ce Backport PR #1907: Add `Notebook.render_cell_output` method
9 years ago
Thomas Kluyver bffc23b704 Backport PR #1904: json_errors should be outermost decorator on API handlers
9 years ago
Min RK c93bbb3dc9 Backport PR #1903: Allow websocket connections from scripts
9 years ago
Kyle Kelley b66a495073 Backport PR #1896: Accept JSON output data with mime type "application/*+json"
9 years ago
Kyle Kelley e389144301 Backport PR #1895: Allow kernelspecs to have spaces in them for backward compat
9 years ago
Kyle Kelley f1b73dcb8c Merge pull request #1917 from minrk/typeahead.min
9 years ago
Thomas Kluyver 5fdc14df42 Backport PR #1887: Fix bug when attempting to remove old nbserver info files
9 years ago
Thomas Kluyver d42825d4dc Backport PR #1851: Update security docs to reflect new signature system
9 years ago
Safia Abdalla b3f7bc4a73 Backport PR #1503: remove discussion of IPython profiles from notebook signatures docs
9 years ago
Min RK 35998b6baa load minified typeahead
9 years ago
Thomas Kluyver 8489faa4cf Backport PR #1838: terminal: Bump xterm.js to 2.0.1
9 years ago
Yuvi Panda 5d05b7d8d3 Backport PR #1759: Fix terminal styles bug
9 years ago
Kyle Kelley 754f397e9e Backport PR #1831: enable token-authentication by default
9 years ago
Matthias Bussonnier 38c19214e4 Backport PR #1798: Add base aliases for nbextensions apps
9 years ago
Min RK 93be35d9f2 Backport PR #1764: Upgrade Codemirror to 5.18
9 years ago
Thomas Kluyver 543d07c21a Backport PR #1763: Include `@` operator in Codemirror ipython mode
9 years ago
Thomas Kluyver 3fbf5bbb5c Backport PR #1650: Load extension in predictable order.
9 years ago
Min RK 4031f8ac5d Backport PR #1359: Update the session api in preparation for file and console sessions
9 years ago
Kyle Kelley 6d1bf20add Backport PR #1875: Minor edits to comms doc
9 years ago
Carol Willing 9ff725cee5 Backport PR #1874: Start documenting how to use Comms
9 years ago
Thomas Kluyver 5f6563a0b0 Backport PR #1868: Fix for default value changes in ipython/traitlets/pull/332
9 years ago
Matthias Bussonnier 7dd947f738 Backport PR #1866: Add a `register_mime_type` method to OutputArea
9 years ago
Matthias Bussonnier ab07e5d106 Backport PR #1836: Fix: Carriage symbol should behave like in console
9 years ago
Thomas Kluyver b696033280 Backport PR #1807: set dirty flag when output arrives
9 years ago
Min RK 833b78d522 Backport PR #1802: Set ws-url data attribute when accessing a notebook terminal
9 years ago
Thomas Kluyver 8b08271d9f Backport PR #1735: Extend mathjax_url docstring
9 years ago
Kyle Kelley 09b2a8b37c Merge pull request #1860 from samarsultan/4.x
9 years ago
samarsultan fc8dd8acba Adding GUI Mirroring
9 years ago
samarsultan fe845e85c4 Adding GUI Mirroring
9 years ago
samarsultan 0a054df334 Adding GUI Mirroring
9 years ago
samarsultan 40b1af189d Merge branch '4.x' of https://github.com/samarsultan/notebook into 4.x
9 years ago
samarsultan fff954afb7 Adding Gui Mirroring
9 years ago
Min RK cf7dae8fe5 Release notes for 4.2.3
9 years ago
Min RK c89fed4d24 Backport PR #1727: fix outdated links to ipython-doc
9 years ago
Min RK 60293ebac4 Backport PR #1665: Typo hunting: notebookapp.py
9 years ago
Grant Nestor a512263aa3 Merge pull request #1732 from minrk/ya-custom-load
9 years ago
Min RK 54162921a4 yet another error-catching custom.js loader
10 years ago
Min RK 9e1459fbc5 Changelog for 4.2.2
10 years ago
Min RK ea5caba32f use `$.text` to put latex on the page
10 years ago
Matthias Bussonnier 0b0962c08c Merge pull request #1653 from minrk/fixup-4.x
10 years ago
Min RK ce79029d38 fix trailing comma in notebook/js/main.js
10 years ago
Min RK 5d2bc6c420 Revert "Rebase PR 1468 : Allow root user to run tests"
10 years ago
Min RK ed86d6e426 Backport PR #1577: Directory listing should not show hidden or system files
10 years ago
Min RK 9f28479c23 Backport PR #1518: Allow requests to POST in OPTIONS requests
10 years ago
Min RK de75ee0a93 Backport PR #1645: Improve the error message for the nbextension.
10 years ago
Matthias Bussonnier 88250b895a Backport PR #1646: Improved message for conflicting args
10 years ago
Matthias Bussonnier 48f809bca7 Backport PR #1642: include cross-origin check when allowing login URL redirects
10 years ago
wenjun.swj 797d7541a8 Backport PR #1631: scape file names in attachment headers
10 years ago
Matthias Bussonnier 69db442305 Backport PR #1552: avoid clobbering ssl_options.ssl_version
10 years ago
Matthias Bussonnier d81fff2727 Backport PR #1526: reverse nbconfig load order
10 years ago
Justin Tyberg e1ff71d3cf Rebase PR #1468 : Allow root user to run tests
10 years ago
Matthias Bussonnier cc83c2f32c Backport PR #1418: log exceptions in nbconvert handlers
10 years ago
Pierre Gerold 817b67ef85 Backport PR 1378
10 years ago
Min RK c10430ef4c Merge pull request #1634 from yuvipanda/4.xtermjs
10 years ago
YuviPanda 9881625ecc Bump xterm version to 1.0
10 years ago
YuviPanda bb5b8a467b Switch to xterm.js from term.js for terminal
10 years ago
Min RK 04d8760518 Merge pull request #1633 from minrk/4.x
10 years ago
Min RK 8946056750 pin casperjs to 1.1.1
10 years ago
Min RK 2b595506e3 stop coveralls on travis
10 years ago
Min RK 9ed3d50093 Revert "Backport PR #1490: Unify the codemirror imports"
10 years ago
Min RK 10cd77ed60 Backport PR #1515: setup doesn't require genutils
10 years ago
Min RK ca70441436 Backport PR #1490: Unify the codemirror imports
10 years ago
Min RK 1a5d8be45f release 4.2.1
10 years ago
Min RK a6c312923f Backport PR #1511: changes for 4.2.1
10 years ago
Min RK 0ce9daac27 regen rst notebooks
10 years ago
Min RK e15938d1a6 Backport PR #1471: Fixes for reconnecting on a flaky network
10 years ago
Min RK 557586474c Backport PR #1412: Check if sys_info is null
10 years ago
Min RK d9ad88f32e Backport PR #1403: Don't pass destination when installing nbextension from Python package
10 years ago
Min RK e075309e7f Backport PR #1391: specify destination for nbextension install
10 years ago
Min RK 783fc14930 Backport PR #1361: Missing newline impacting RTD rendering
10 years ago
Min RK 3d39c92b57 Backport PR #1360: Override mimetype for .css files
10 years ago
Min RK a58b9d736a Merge pull request #1510 from SylvainCorlay/Fix_widgets_warning
10 years ago
Sylvain Corlay 7330bd491a Fix widget warning in case of ipywidgets 5.x
10 years ago
Min RK 885979d506 Merge pull request #1399 from SylvainCorlay/_enable_old_widgets
10 years ago
Sylvain Corlay ee44d1947f Enable widgets 4.1
10 years ago
Min RK cc7e592f48 release 4.2.0
10 years ago
Min RK a90b4bc54d Backport PR #1353: Escape file names in attachment headers
10 years ago
Min RK ea4a0c7ccc Backport PR #1338: avoid double-base-url when launching browser
10 years ago
Min RK f10ce5d4ed Backport PR #1237: Allow a session to connect to an existing kernel
10 years ago
Min RK c5eb054f4d Backport PR #1235: Allow kernel id to take precedence over name
10 years ago
Min RK ba5e7fe1b2 Backport PR #1206: Allow modifying kernel associated with a session via PATCH
10 years ago
Min RK 925fd7febe Backport PR #1333: remove what's new from index
10 years ago
Min RK 69535708ff release 4.2.0b1
10 years ago
Carol Willing f42030b997 Merge pull request #1332 from minrk/4.2-notes
10 years ago
Min RK 945dd808a5 4.2.0 release notes
10 years ago
Min RK 6172f337d2 Backport PR #1328: The path for widgets has changed
10 years ago
Min RK 8e9fa93492 Backport PR #1327: avoid writing nbextension config to user dir during tests
10 years ago
Jonathan Frederic 9d250230a8 Merge pull request #1326 from minrk/backport-widget-hardcode
10 years ago
Min RK d542745a68 Backport pull request #1279: Downgraded ipywidget hack
10 years ago
Min RK 46b6829839 Backport PR #1325: Fix handling of preflight requests
10 years ago
Min RK 02e8e4377e Backport PR #1310: allow using sqlite from pysqlite2
10 years ago
Min RK efe922cd99 Backport PR #1288: Correctly render markdown code blocks with unknown language
10 years ago
Min RK 42132cac86 Backport PR #1275: fix validation warnings when validating
10 years ago
Min RK 5a43bdb685 Backport PR #998: use x-access for directory listing test in is_hidden
10 years ago
Min RK 6d3731c9b8 Backport PR #1273: some tests, fixes for nbextension aliases
10 years ago
Min RK 8151f2f35c Backport PR #879: New nbextensions installation API
10 years ago
Min RK ebaa23a027 Backport PR #1143: Exempt javascript files from files check
10 years ago
Min RK 6a20fb3bb6 Backport PR #1079: remove the 'random' from the random-port message
10 years ago
Min RK c73dac23d7 Backport PR #1021: don't install prereleases on Travis
10 years ago
Min RK f6f763ec37 Backport PR #1011: Workaround Firefox bug showing beforeunload twice
10 years ago
Min RK 8d9ebbafe8 Backport PR #1002: Implement delayed evaluation for the cell executions happening before a kernel is set up
10 years ago
Min RK 3eaaeb864c Backport PR #1240: fix a few remaining IPython Notebook references
10 years ago
Min RK 4f7825fe92 Backport PR #1231: Add `cookie_options` to make cookie args configurable
10 years ago
Min RK 3d218d6ee5 Backport PR #1216: Add missing pager.append function
10 years ago
Min RK ae6096e2ae Backport PR #1215: Toggle Header in Edit page
10 years ago
Min RK 41ede3ec56 Backport PR #1187: add missing url-encoding of base_url in terminal/edit templates
10 years ago
Min RK b3c97cd6a6 Backport PR #1168: allow notebook-dir to be root
10 years ago
Min RK 25b0846998 Backport PR #1136: channel.closed is a method
10 years ago
Min RK 8120459e4e Backport PR #1127: Don't force save before download if notebook isn't writable
10 years ago
Min RK fd765a4e8d Backport PR #1120: Removed misplaced docs
10 years ago
Min RK 89044886e3 Backport PR #1119: Clarify keyboard shortcut help for "split cell"
10 years ago
Min RK cb92bc323f Backport PR #1113: Fix long lines escaping from the cell container
10 years ago
Min RK a247c6689d Backport PR #1076: Lowercase file extension before looking it up in CodeMirror
10 years ago
Min RK c206169d78 Backport PR #1071: Focus selected cell after move.
10 years ago
Min RK a79f634716 Backport PR #1061: plural cell actions
10 years ago
Min RK f778c9cd33 Backport PR #1055: Allow all MathJax output formats
10 years ago
Min RK d3b9da68b1 Backport PR #1049: Nested svg
10 years ago
Min RK e4f5d040d1 Backport PR #931: Add simple iopub message rate limiter
10 years ago
Min RK 5fa6c34f40 Backport PR #1036: disable MathJax renderer selection menu
10 years ago
Min RK 8a3b21816d Backport PR #1032: bump MathJax to 2.6
10 years ago
Min RK 67e60d954f Backport PR #1020: only import ssl if it's used
10 years ago
Min RK 8f147f1dfc Backport PR #1017: Add `require` to list of modules to try fixing kernel.js loading.
10 years ago
Min RK 205bfcf220 Backport PR #992: use _.isEqual to check for metadata changes
10 years ago
Min RK f365612924 Backport PR #987: save before copy if notebook is dirty
10 years ago
Min RK eb31a3c652 Backport PR #976: Run notebook tests instead of IPython tests in Dockerfile
10 years ago
Min RK fb98f7e71f Backport PR #925: Be more explicit about deprecation.
10 years ago
Min RK 1faa5ee631 Backport PR #589: Docker purge unneeded files
10 years ago
Volker Braun ac6baa7d22 Use require.toUrl for help_links
10 years ago

@ -18,10 +18,11 @@ env:
- GROUP=js/services
- GROUP=js/tree
before_install:
- 'if [[ $GROUP == js* ]]; then npm install -g casperjs; fi'
- pip install --upgrade pip setuptools
- 'if [[ $GROUP == js* ]]; then npm install -g casperjs@1.1.1; fi'
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
install:
- pip install -f travis-wheels/wheelhouse --pre file://$PWD#egg=notebook[test] coveralls
- pip install -f travis-wheels/wheelhouse file://$PWD#egg=notebook[test]
script:
- 'if [[ $GROUP == js* ]]; then python -m notebook.jstest ${GROUP:3}; fi'
- 'if [[ $GROUP == python ]]; then nosetests --with-coverage --cover-package=notebook notebook; fi'
@ -32,5 +33,3 @@ matrix:
- python: 3.4
env: GROUP=python
after_success:
- coveralls

@ -59,14 +59,16 @@ RUN curl -O https://bootstrap.pypa.io/get-pip.py && \
python3 get-pip.py && \
rm get-pip.py && \
pip2 --no-cache-dir install requests[security] && \
pip3 --no-cache-dir install requests[security]
pip3 --no-cache-dir install requests[security] && \
rm -rf /root/.cache
# Install some dependencies.
RUN pip2 --no-cache-dir install ipykernel && \
pip3 --no-cache-dir install ipykernel && \
\
python2 -m ipykernel.kernelspec && \
python3 -m ipykernel.kernelspec
python3 -m ipykernel.kernelspec && \
rm -rf /root/.cache
# Move notebook contents into place.
ADD . /usr/src/jupyter-notebook
@ -76,21 +78,22 @@ RUN BUILD_DEPS="nodejs-legacy npm" && \
apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq $BUILD_DEPS && \
\
pip3 install --no-cache-dir --pre -e /usr/src/jupyter-notebook && \
pip3 install --no-cache-dir /usr/src/jupyter-notebook && \
pip3 install ipywidgets && \
\
npm cache clean && \
apt-get clean && \
rm -rf /root/.npm && \
rm -rf /root/.cache && \
rm -rf /root/.config && \
rm -rf /root/.local && \
rm -rf /root/tmp && \
rm -rf /var/lib/apt/lists/* && \
apt-get purge -y --auto-remove \
-o APT::AutoRemove::RecommendsImportant=false -o APT::AutoRemove::SuggestsImportant=false $BUILD_DEPS
# Run tests.
RUN pip2 install --no-cache-dir mock nose requests testpath && \
pip3 install --no-cache-dir nose requests testpath && \
\
iptest2 && iptest3 && \
\
pip2 uninstall -y funcsigs mock nose pbr requests six testpath && \
pip3 uninstall -y nose requests testpath
RUN pip3 install --no-cache-dir notebook[test] && nosetests notebook
# Add a notebook profile.
RUN mkdir -p -m 700 /root/.jupyter/ && \

@ -2,7 +2,7 @@
[![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter)
[![Build Status](https://travis-ci.org/jupyter/notebook.svg?branch=master)](https://travis-ci.org/jupyter/notebook)
[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](http://jupyter-notebook.readthedocs.org/en/latest/?badge=latest)
[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/latest/?badge=latest)
The Jupyter notebook is a web-based notebook environment for interactive
computing.
@ -24,12 +24,12 @@ discrete repos.
## Installation
You can find the installation documentation for the
[Jupyter platform, on ReadTheDocs](http://jupyter.readthedocs.org/en/latest/install.html).
[Jupyter platform, on ReadTheDocs](https://jupyter.readthedocs.io/en/latest/install.html).
The documentation for advanced usage of Jupyter notebook can be found
[here](http://jupyter-notebook.readthedocs.org/en/latest).
[here](https://jupyter-notebook.readthedocs.io/en/latest/).
For a local installation, make sure you have
[pip installed](https://pip.readthedocs.org/en/stable/installing/) and run:
[pip installed](https://pip.readthedocs.io/en/stable/installing/) and run:
$ pip install notebook
@ -122,7 +122,7 @@ jupyter notebook
## Resources
- [Project Jupyter website](https://jupyter.org)
- [Online Demo at try.jupyter.org](https://try.jupyter.org)
- [Documentation for Jupyter notebook](http://jupyter-notebook.readthedocs.org/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)]
- [Documentation for Project Jupyter](http://jupyter.readthedocs.org/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)]
- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)]
- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)]
- [Issues](https://github.com/jupyter/notebook/issues)
- [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter)

@ -5,17 +5,17 @@
"backbone": "components/backbone#~1.2",
"bootstrap": "components/bootstrap#~3.3",
"bootstrap-tour": "0.9.0",
"codemirror": "~5.8",
"codemirror": "components/codemirror#~5.22.2",
"es6-promise": "~1.0",
"font-awesome": "components/font-awesome#~4.2.0",
"google-caja": "5669",
"jquery": "components/jquery#~2.0",
"jquery-ui": "components/jqueryui#~1.10",
"marked": "~0.3",
"MathJax": "components/MathJax#~2.5",
"MathJax": "components/MathJax#~2.6",
"moment": "~2.8.4",
"requirejs": "~2.1",
"term.js": "chjj/term.js#~0.0.7",
"xterm.js": "sourcelair/xterm.js#~2.1.0",
"text-encoding": "~0.1",
"underscore": "components/underscore#~1.5",
"jquery-typeahead": "~2.0.0"

@ -6,6 +6,213 @@ Jupyter notebook changelog
A summary of changes in the Jupyter notebook.
For more detailed information, see `GitHub <https://github.com/jupyter/notebook>`__.
.. tip::
Use ``pip install notebook --upgrade`` or ``conda upgrade notebook`` to
upgrade to the latest release.
.. _release-4.4.0:
4.4.0
-----
- Allow override of output callbacks to redirect output messages. This is used to implement the ipywidgets Output widget, for example.
- Fix an async bug in message handling by allowing comm message handlers to return a promise which halts message processing until the promise resolves.
See the 4.4 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.4>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.4>`__ involved in this release.
.. _release-4.3.2:
4.3.2
-----
4.3.2 is a patch release with a bug fix for CodeMirror and improved handling of the "editable" cell metadata field.
- Monkey-patch for CodeMirror that resolves `#2037 <https://github.com/jupyter/notebook/issues/2037>`__ without breaking `#1967 <https://github.com/jupyter/notebook/issues/1967>`__
- Read-only (``"editable": false``) cells can be executed but cannot be split, merged, or deleted
See the 4.3.2 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3.2>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3.2>`__ involved in this release.
.. _release-4.3.1:
4.3.1
-----
4.3.1 is a patch release with a security patch, a couple bug fixes, and improvements to the newly-released token authentication.
**Security fix**:
- CVE-2016-9971. Fix CSRF vulnerability,
where malicious forms could create untitled files and start kernels
(no remote execution or modification of existing files)
for users of certain browsers (Firefox, Internet Explorer / Edge).
All previous notebook releases are affected.
Bug fixes:
- Fix carriage return handling
- Make the font size more robust against fickle browsers
- Ignore resize events that bubbled up and didn't come from window
- Add Authorization to allowed CORS headers
- Downgrade CodeMirror to 5.16 while we figure out issues in Safari
Other improvements:
- Better docs for token-based authentication
- Further highlight token info in log output when autogenerated
See the 4.3.1 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3.1>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3.1>`__ involved in this release.
.. _release-4.3:
4.3.0
-----
4.3 is a minor release with many bug fixes and improvements.
The biggest user-facing change is the addition of token authentication,
which is enabled by default.
A token is generated and used when your browser is opened automatically,
so you shouldn't have to enter anything in the default circumstances.
If you see a login page
(e.g. by switching browsers, or launching on a new port with ``--no-browser``),
you get a login URL with the token from the command ``jupyter notebook list``,
which you can paste into your browser.
Highlights:
- API for creating mime-type based renderer extensions using :code:`OutputArea.register_mime_type` and :code:`Notebook.render_cell_output` methods. See `mimerender-cookiecutter <https://github.com/jupyterlab/mimerender-cookiecutter>`__ for reference implementations and cookiecutter.
- Enable token authentication by default. See :ref:`server_security` for more details.
- Update security docs to reflect new signature system
- Switched from term.js to xterm.js
Bug fixes:
- Ensure variable is set if exc_info is falsey
- Catch and log handler exceptions in :code:`events.trigger`
- Add debug log for static file paths
- Don't check origin on token-authenticated requests
- Remove leftover print statement
- Fix highlighting of Python code blocks
- :code:`json_errors` should be outermost decorator on API handlers
- Fix remove old nbserver info files
- Fix notebook mime type on download links
- Fix carriage symbol bahvior
- Fix terminal styles
- Update dead links in docs
- If kernel is broken, start a new session
- Include cross-origin check when allowing login URL redirects
Other improvements:
- Allow JSON output data with mime type "application/*+json"
- Allow kernelspecs to have spaces in them for backward compat
- Allow websocket connections from scripts
- Allow :code:`None` for post_save_hook
- Upgrade CodeMirror to 5.21
- Upgrade xterm to 2.1.0
- Docs for using comms
- Set :code:`dirty` flag when output arrives
- Set :code:`ws-url` data attribute when accessing a notebook terminal
- Add base aliases for nbextensions
- Include :code:`@` operator in CodeMirror IPython mode
- Extend mathjax_url docstring
- Load nbextension in predictable order
- Improve the error messages for nbextensions
- Include cross-origin check when allowing login URL redirects
See the 4.3 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3%20>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3%20>`__ involved in this release.
.. _release-4.2.3:
4.2.3
-----
4.2.3 is a small bugfix release on 4.2.
Highlights:
- Fix regression in 4.2.2 that delayed loading custom.js
until after ``notebook_loaded`` and ``app_initialized`` events have fired.
- Fix some outdated docs and links.
.. seealso::
4.2.3 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.3>`__.
.. _release-4.2.2:
4.2.2
-----
4.2.2 is a small bugfix release on 4.2, with an important security fix.
All users are strongly encouraged to upgrade to 4.2.2.
Highlights:
- **Security fix**: CVE-2016-6524, where untrusted latex output
could be added to the page in a way that could execute javascript.
- Fix missing POST in OPTIONS responses.
- Fix for downloading non-ascii filenames.
- Avoid clobbering ssl_options, so that users can specify more detailed SSL configuration.
- Fix inverted load order in nbconfig, so user config has highest priority.
- Improved error messages here and there.
.. seealso::
4.2.2 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.2>`__.
.. _release-4.2.1:
4.2.1
-----
4.2.1 is a small bugfix release on 4.2. Highlights:
- Compatibility fixes for some versions of ipywidgets
- Fix for ignored CSS on Windows
- Fix specifying destination when installing nbextensions
.. seealso::
4.2.1 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.1>`__.
.. _release-4.2.0:
4.2.0
-----
Release 4.2 adds a new API for enabling and installing extensions.
Extensions can now be enabled at the system-level, rather than just per-user.
An API is defined for installing directly from a Python package, as well.
.. seealso::
:doc:`./examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages`
Highlighted changes:
- Upgrade MathJax to 2.6 to fix vertical-bar appearing on some equations.
- Restore ability for notebook directory to be root (4.1 regression)
- Large outputs are now throttled, reducing the ability of output floods to
kill the browser.
- Fix the notebook ignoring cell executions while a kernel is starting by queueing the messages.
- Fix handling of url prefixes (e.g. JupyterHub) in terminal and edit pages.
- Support nested SVGs in output.
And various other fixes and improvements.
.. _release-4.1.0:
4.1.0

@ -0,0 +1,98 @@
Comms
=====
*Comms* allow custom messages between the frontend and the kernel. They are used,
for instance, in `ipywidgets <http://ipywidgets.readthedocs.io/en/latest/>`__ to
update widget state.
A comm consists of a pair of objects, in the kernel and the frontend, with an
automatically assigned unique ID. When one side sends a message, a callback on
the other side is triggered with that message data. Either side, the frontend
or kernel, can open or close the comm.
.. seealso::
`Custom Messages <http://jupyter-client.readthedocs.io/en/latest/messaging.html#custom-messages>`__
The messaging specification section on comms
Opening a comm from the kernel
------------------------------
First, the function to accept the comm must be available on the frontend. This
can either be specified in a `requirejs` module, or registered in a registry, for
example when an :doc:`extension <extending/frontend_extensions>` is loaded.
This example shows a frontend comm target registered in a registry:
.. code-block:: javascript
Jupyter.notebook.kernel.comm_manager.register_target('my_comm_target',
function(comm, msg) {
// comm is the frontend comm instance
// msg is the comm_open message, which can carry data
// Register handlers for later messages:
comm.on_msg(function(msg) {...});
comm.on_close(function(msg) {...});
comm.send({'foo': 0});
});
Now that the frontend comm is registered, you can open the comm from the kernel:
.. code-block:: python
from ipykernel.comm import Comm
# Use comm to send a message from the kernel
my_comm = Comm(target_name='my_comm_target', data={'foo': 1})
my_comm.send({'foo': 2})
# Add a callback for received messages.
@my_comm.on_msg
def _recv(msg):
# Use msg['content']['data'] for the data in the message
This example uses the IPython kernel; it's up to each language kernel what API,
if any, it offers for using comms.
Opening a comm from the frontend
--------------------------------
This is very similar to above, but in reverse. First, a comm target must be
registered in the kernel. For instance, this may be done by code displaying
output: it will register a target in the kernel, and then display output
containing Javascript to connect to it.
.. code-block:: python
def target_func(comm, msg):
# comm is the kernel Comm instance
# msg is the comm_open message
# Register handler for later messages
@comm.on_msg
def _recv(msg):
# Use msg['content']['data'] for the data in the message
# Send data to the frontend
comm.send({'foo': 5})
get_ipython().kernel.comm_manager.register_target('my_comm_target', target_func)
This example uses the IPython kernel again; this example will be different in
other kernels that support comms. Refer to the specific language kernel's
documentation for comms support.
And then open the comm from the frontend:
.. code-block:: javascript
comm = Jupyter.notebook.kernel.comm_manager.new_comm('my_comm_target',
{'foo': 6})
// Send data
comm.send({'foo': 7})
// Register a handler
comm.on_msg(function(msg) {
console.log(msg.content.data.foo);
});

@ -328,7 +328,7 @@ texinfo_documents = [
intersphinx_mapping = {
'ipython': ('http://ipython.org/ipython-doc/dev/', None),
'nbconvert': ('http://nbconvert.readthedocs.org/en/latest/', None),
'nbformat': ('http://nbformat.readthedocs.org/en/latest/', None),
'jupyter': ('http://jupyter.readthedocs.org/en/latest/', None),
'nbconvert': ('https://nbconvert.readthedocs.io/en/latest/', None),
'nbformat': ('https://nbformat.readthedocs.io/en/latest/', None),
'jupyter': ('https://jupyter.readthedocs.io/en/latest/', None),
}

@ -0,0 +1,281 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Distributing Jupyter Extensions as Python Packages"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Overview\n",
"### How can the notebook be extended?\n",
"The Jupyter Notebook client and server application are both deeply customizable. Their behavior can be extended by creating, respectively:\n",
"\n",
"- nbextension: a notebook extension\n",
" - a single JS file, or directory of JavaScript, Cascading StyleSheets, etc. that contain at\n",
" minimum a JavaScript module packaged as an\n",
" [AMD modules](https://en.wikipedia.org/wiki/Asynchronous_module_definition)\n",
" that exports a function `load_ipython_extension`\n",
"- server extension: an importable Python module\n",
" - that implements `load_jupyter_server_extension`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Why create a Python package for Jupyter extensions?\n",
"Since it is rare to have a server extension that does not have any frontend components (an nbextension), for convenience and consistency, all these client and server extensions with their assets can be packaged and versioned together as a Python package with a few simple commands. This makes installing the package of extensions easier and less error-prone for the user. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Installation of Jupyter Extensions\n",
"### Install a Python package containing Jupyter Extensions\n",
"There are several ways that you may get a Python package containing Jupyter Extensions. Commonly, you will use a package manager for your system:\n",
"```shell\n",
"pip install helpful_package\n",
"# or\n",
"conda install helpful_package\n",
"# or\n",
"apt-get install helpful_package\n",
"\n",
"# where 'helpful_package' is a Python package containing one or more Jupyter Extensions\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Enable a Server Extension\n",
"\n",
"The simplest case would be to enable a server extension which has no frontend components. \n",
"\n",
"A `pip` user that wants their configuration stored in their home directory would type the following command:\n",
"```shell\n",
"jupyter serverextension enable --py helpful_package\n",
"```\n",
"\n",
"Alternatively, a `virtualenv` or `conda` user can pass `--sys-prefix` which keeps their environment isolated and reproducible. For example:\n",
"```shell\n",
"# Make sure that your virtualenv or conda environment is activated\n",
"[source] activate my-environment\n",
"\n",
"jupyter serverextension enable --py helpful_package --sys-prefix\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Install the nbextension assets"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If a package also has an nbextension with frontend assets that must be available (but not neccessarily enabled by default), install these assets with the following command:\n",
"```shell\n",
"jupyter nbextension install --py helpful_package # or --sys-prefix if using virtualenv or conda\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Enable nbextension assets\n",
"If a package has assets that should be loaded every time a Jupyter app (e.g. lab, notebook, dashboard, terminal) is loaded in the browser, the following command can be used to enable the nbextension:\n",
"```shell\n",
"jupyter nbextension enable --py helpful_package # or --sys-prefix if using virtualenv or conda\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Did it work? Check by listing Jupyter Extensions.\n",
"After running one or more extension installation steps, you can list what is presently known about nbextensions or server extension. The following commands will list which extensions are available, whether they are enabled, and other extension details:\n",
"\n",
"```shell\n",
"jupyter nbextension list\n",
"jupyter serverextension list\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Additional resources on creating and distributing packages \n",
"\n",
"> Of course, in addition to the files listed, there are number of other files one needs to build a proper package. Here are some good resources:\n",
"- [The Hitchhiker's Guide to Packaging](https://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/quickstart.html)\n",
"- [Repository Structure and Python](http://www.kennethreitz.org/essays/repository-structure-and-python) by Kenneth Reitz\n",
"\n",
"> How you distribute them, too, is important:\n",
"- [Packaging and Distributing Projects](https://python-packaging-user-guide.readthedocs.io/distributing/)\n",
"- [conda: Building packages](http://conda.pydata.org/docs/building/build.html)\n",
"\n",
"> Here are some tools to get you started:\n",
"- [generator-nbextension](https://github.com/Anaconda-Server/generator-nbextension)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example - Server extension"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Creating a Python package with a server extension\n",
"\n",
"Here is an example of a python module which contains a server extension directly on itself. It has this directory structure:\n",
"```\n",
"- setup.py\n",
"- MANIFEST.in\n",
"- my_module/\n",
" - __init__.py\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Defining the server extension\n",
"This example shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file.\n",
"\n",
"#### `my_module/__init__.py`\n",
"\n",
"```python\n",
"def _jupyter_server_extension_paths():\n",
" return [{\n",
" \"module\": \"my_module\"\n",
" }]\n",
"\n",
"\n",
"def load_jupyter_server_extension(nbapp):\n",
" nbapp.log.info(\"my module enabled!\")\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Install and enable the server extension\n",
"Which a user can install with:\n",
"```\n",
"jupyter serverextension enable --py my_module [--sys-prefix]\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example - Server extension and nbextension"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Creating a Python package with a server extension and nbextension\n",
"Here is another server extension, with a front-end module. It assumes this directory structure:\n",
"\n",
"```\n",
"- setup.py\n",
"- MANIFEST.in\n",
"- my_fancy_module/\n",
" - __init__.py\n",
" - static/\n",
" index.js\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"### Defining the server extension and nbextension\n",
"This example again shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file. This time, there is also a function `_jupyter_nbextension_path` for the nbextension.\n",
"\n",
"#### `my_fancy_module/__init__.py`\n",
"\n",
"```python\n",
"def _jupyter_server_extension_paths():\n",
" return [{\n",
" \"module\": \"my_fancy_module\"\n",
" }]\n",
"\n",
"# Jupyter Extension points\n",
"def _jupyter_nbextension_paths():\n",
" return [dict(\n",
" section=\"notebook\",\n",
" # the path is relative to the `my_fancy_module` directory\n",
" src=\"static\",\n",
" # directory in the `nbextension/` namespace\n",
" dest=\"my_fancy_module\",\n",
" # _also_ in the `nbextension/` namespace\n",
" require=\"my_fancy_module/index\")]\n",
"\n",
"def load_jupyter_server_extension(nbapp):\n",
" nbapp.log.info(\"my module enabled!\")\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Install and enable the server extension and nbextension\n",
"\n",
"The user can install and enable the extensions with the following set of commands:\n",
"```\n",
"jupyter nbextension install --py my_fancy_module [--sys-prefix|--user]\n",
"jupyter nbextension enable --py my_fancy_module [--sys-prefix|--system]\n",
"jupyter serverextension enable --py my_fancy_module [--sys-prefix|--system]\n",
"```"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.1"
}
},
"nbformat": 4,
"nbformat_minor": 0
}

@ -523,7 +523,7 @@
"```\n",
"\n",
"\n",
"See for example @damianavila [\"ZenMode\" plugin](https://github.com/ipython-contrib/IPython-notebook-extensions/blob/master/custom.example.js#L34) :\n",
"See for example @damianavila [\"ZenMode\" plugin](https://github.com/ipython-contrib/jupyter_contrib_nbextensions/blob/b29c698394239a6931fa4911440550df214812cb/src/jupyter_contrib_nbextensions/nbextensions/zenmode/main.js#L32) :\n",
"\n",
"```javascript\n",
"\n",

@ -7,66 +7,6 @@
"# Notebook Basics"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Running the Notebook Server"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The Jupyter notebook server is a custom web server that runs the notebook web application. Most of the time, users run the notebook server on their local computer using the command line interface."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Starting the notebook server using the command line"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can start the notebook server from the command line (Terminal on Mac/Linux, CMD prompt on Windows) by running the following command: \n",
"\n",
" jupyter notebook\n",
"\n",
"This will print some information about the notebook server in your terminal, including the URL of the web application (by default, `http://127.0.0.1:8888`). It will then open your default web browser to this URL.\n",
"\n",
"When the notebook opens, you will see the **notebook dashboard**, which will show a list of the notebooks, files, and subdirectories in the directory where the notebook server was started (as seen in the next section, below). Most of the time, you will want to start a notebook server in the highest directory in your filesystem where notebooks can be found. Often this will be your home directory."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Additional options"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By default, the notebook server starts on port 8888. If port 8888 is unavailable, the notebook server searchs the next available port.\n",
"\n",
"You can also specify the port manually:\n",
"\n",
" jupyter notebook --port 9999\n",
"\n",
"Or start notebook server without opening a web browser.\n",
"\n",
" jupyter notebook --no-browser\n",
"\n",
"The notebook server has a number of other command line arguments that can be displayed with the `--help` flag: \n",
"\n",
" jupyter notebook --help"
]
},
{
"cell_type": "markdown",
"metadata": {},
@ -291,21 +231,21 @@
],
"metadata": {
"kernelspec": {
"display_name": "IPython (Python 3)",
"display_name": "Python 2",
"language": "python",
"name": "python3"
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.1"
"pygments_lexer": "ipython2",
"version": "2.7.11"
}
},
"nbformat": 4,

@ -0,0 +1,245 @@
`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.ipynb>`__
Distributing Jupyter Extensions as Python Packages
==================================================
Overview
--------
How can the notebook be extended?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Jupyter Notebook client and server application are both deeply
customizable. Their behavior can be extended by creating, respectively:
- nbextension: a notebook extension
- a single JS file, or directory of JavaScript, Cascading
StyleSheets, etc. that contain at minimum a JavaScript module
packaged as an `AMD
modules <https://en.wikipedia.org/wiki/Asynchronous_module_definition>`__
that exports a function ``load_ipython_extension``
- server extension: an importable Python module
- that implements ``load_jupyter_server_extension``
Why create a Python package for Jupyter extensions?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Since it is rare to have a server extension that does not have any
frontend components (an nbextension), for convenience and consistency,
all these client and server extensions with their assets can be packaged
and versioned together as a Python package with a few simple commands.
This makes installing the package of extensions easier and less
error-prone for the user.
Installation of Jupyter Extensions
----------------------------------
Install a Python package containing Jupyter Extensions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are several ways that you may get a Python package containing
Jupyter Extensions. Commonly, you will use a package manager for your
system:
.. code:: shell
pip install helpful_package
# or
conda install helpful_package
# or
apt-get install helpful_package
# where 'helpful_package' is a Python package containing one or more Jupyter Extensions
Enable a Server Extension
~~~~~~~~~~~~~~~~~~~~~~~~~
The simplest case would be to enable a server extension which has no
frontend components.
A ``pip`` user that wants their configuration stored in their home
directory would type the following command:
.. code:: shell
jupyter serverextension enable --py helpful_package
Alternatively, a ``virtualenv`` or ``conda`` user can pass
``--sys-prefix`` which keeps their environment isolated and
reproducible. For example:
.. code:: shell
# Make sure that your virtualenv or conda environment is activated
[source] activate my-environment
jupyter serverextension enable --py helpful_package --sys-prefix
Install the nbextension assets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If a package also has an nbextension with frontend assets that must be
available (but not neccessarily enabled by default), install these
assets with the following command:
.. code:: shell
jupyter nbextension install --py helpful_package # or --sys-prefix if using virtualenv or conda
Enable nbextension assets
~~~~~~~~~~~~~~~~~~~~~~~~~
If a package has assets that should be loaded every time a Jupyter app
(e.g. lab, notebook, dashboard, terminal) is loaded in the browser, the
following command can be used to enable the nbextension:
.. code:: shell
jupyter nbextension enable --py helpful_package # or --sys-prefix if using virtualenv or conda
Did it work? Check by listing Jupyter Extensions.
-------------------------------------------------
After running one or more extension installation steps, you can list
what is presently known about nbextensions or server extension. The
following commands will list which extensions are available, whether
they are enabled, and other extension details:
.. code:: shell
jupyter nbextension list
jupyter serverextension list
Additional resources on creating and distributing packages
----------------------------------------------------------
Of course, in addition to the files listed, there are number of
other files one needs to build a proper package. Here are some good
resources: - `The Hitchhiker's Guide to
Packaging <http://the-hitchhikers-guide-to-packaging.readthedocs.org/en/latest/quickstart.html>`__
- `Repository Structure and
Python <http://www.kennethreitz.org/essays/repository-structure-and-python>`__
by Kenneth Reitz
How you distribute them, too, is important: - `Packaging and
Distributing
Projects <http://python-packaging-user-guide.readthedocs.org/en/latest/distributing/>`__
- `conda: Building
packages <http://conda.pydata.org/docs/building/build.html>`__
Here are some tools to get you started: -
`generator-nbextension <https://github.com/Anaconda-Server/generator-nbextension>`__
Example - Server extension
--------------------------
Creating a Python package with a server extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here is an example of a python module which contains a server extension
directly on itself. It has this directory structure:
::
- setup.py
- MANIFEST.in
- my_module/
- __init__.py
Defining the server extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This example shows that the server extension and its
``load_jupyter_server_extension`` function are defined in the
``__init__.py`` file.
``my_module/__init__.py``
^^^^^^^^^^^^^^^^^^^^^^^^^
.. code:: python
def _jupyter_server_extension_paths():
return [{
"module": "my_module"
}]
def load_jupyter_server_extension(nbapp):
nbapp.log.info("my module enabled!")
Install and enable the server extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Which a user can install with:
::
jupyter serverextension enable --py my_module [--sys-prefix]
Example - Server extension and nbextension
------------------------------------------
Creating a Python package with a server extension and nbextension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here is another server extension, with a front-end module. It assumes
this directory structure:
::
- setup.py
- MANIFEST.in
- my_fancy_module/
- __init__.py
- static/
index.js
Defining the server extension and nbextension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This example again shows that the server extension and its
``load_jupyter_server_extension`` function are defined in the
``__init__.py`` file. This time, there is also a function
``_jupyter_nbextension_path`` for the nbextension.
``my_fancy_module/__init__.py``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code:: python
def _jupyter_server_extension_paths():
return [{
"module": "my_fancy_module"
}]
# Jupyter Extension points
def _jupyter_nbextension_paths():
return [dict(
section="notebook",
# the path is relative to the `my_fancy_module` directory
src="static",
# directory in the `nbextension/` namespace
dest="my_fancy_module",
# _also_ in the `nbextension/` namespace
require="my_fancy_module/index")]
def load_jupyter_server_extension(nbapp):
nbapp.log.info("my module enabled!")
Install and enable the server extension and nbextension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The user can install and enable the extensions with the following set of
commands:
::
jupyter nbextension install --py my_fancy_module [--sys-prefix|--user]
jupyter nbextension enable --py my_fancy_module [--sys-prefix|--system]
jupyter serverextension enable --py my_fancy_module [--sys-prefix|--system]
`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.ipynb>`__

@ -318,7 +318,7 @@ Exercise:
^^^^^^^^^
Try to wrap the all code in a file, put this file in
``{profile}/static/custom/<a-name>.js``, and add
``{jupyter_dir}/custom/<a-name>.js``, and add
::

@ -4,60 +4,6 @@
Notebook Basics
===============
Running the Notebook Server
---------------------------
The Jupyter notebook server is a custom web server that runs the
notebook web application. Most of the time, users run the notebook
server on their local computer using the command line interface.
Starting the notebook server using the command line
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can start the notebook server from the command line (Terminal on
Mac/Linux, CMD prompt on Windows) by running the following command:
::
jupyter notebook
This will print some information about the notebook server in your
terminal, including the URL of the web application (by default,
``http://127.0.0.1:8888``). It will then open your default web browser
to this URL.
When the notebook opens, you will see the **notebook dashboard**, which
will show a list of the notebooks, files, and subdirectories in the
directory where the notebook server was started (as seen in the next
section, below). Most of the time, you will want to start a notebook
server in the highest directory in your filesystem where notebooks can
be found. Often this will be your home directory.
Additional options
~~~~~~~~~~~~~~~~~~
By default, the notebook server starts on port 8888. If port 8888 is
unavailable, the notebook server searchs the next available port.
You can also specify the port manually:
::
jupyter notebook --port 9999
Or start notebook server without opening a web browser.
::
jupyter notebook --no-browser
The notebook server has a number of other command line arguments that
can be displayed with the ``--help`` flag:
::
jupyter notebook --help
The Notebook dashboard
----------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

@ -214,6 +214,6 @@ ContentsManager.
directories as SQL relations. PGContents also provides an example of how to
re-use the notebook's tests.
.. _NBFormat: http://nbformat.readthedocs.org/en/latest/index.html
.. _NBFormat: https://nbformat.readthedocs.io/en/latest/index.html
.. _PGContents: https://github.com/quantopian/pgcontents
.. _PostgreSQL: http://www.postgresql.org/

@ -38,7 +38,7 @@ extension:
});
.. note::
Although for historical reasons the function is called
``load_ipython_extension``, it does apply to the Jupyter notebook in
general, and will work regardless of the kernel in use.
@ -112,7 +112,7 @@ place:
});
.. note::
The standard keybindings might not work correctly on non-US keyboards.
Unfortunately, this is a limitation of browser implementations and the
status of keyboard event handling on the web in general. We appreciate your
@ -184,6 +184,7 @@ actions defined in an extension, it makes sense to use the extension name as
the prefix. For the action name, the following guidelines should be considered:
.. adapted from notebook/static/notebook/js/actions.js
* First pick a noun and a verb for the action. For example, if the action is
"restart kernel," the verb is "restart" and the noun is "kernel".
* Omit terms like "selected" and "active" by default, so "delete-cell", rather
@ -202,10 +203,12 @@ the prefix. For the action name, the following guidelines should be considered:
Installing and enabling extensions
----------------------------------
You can install your nbextension with the command:
You can install your nbextension with the command::
jupyter nbextension install path/to/my_extension/
jupyter nbextension install path/to/my_extension/ [--user|--sys-prefix]
The default installation is system-wide. You can use ``--user`` to do a per-user installation,
or ``--sys-prefix`` to install to Python's prefix (e.g. in a virtual or conda environment).
Where my_extension is the directory containing the Javascript files.
This will copy it to a Jupyter data directory (the exact location is platform
dependent - see :ref:`jupyter_path`).
@ -214,11 +217,15 @@ For development, you can use the ``--symlink`` flag to symlink your extension
rather than copying it, so there's no need to reinstall after changes.
To use your extension, you'll also need to **enable** it, which tells the
notebook interface to load it. You can do that with another command:
notebook interface to load it. You can do that with another command::
jupyter nbextension enable my_extension/main
jupyter nbextension enable my_extension/main [--sys-prefix]
The argument refers to the Javascript module containing your
``load_ipython_extension`` function, which is ``my_extension/main.js`` in this
example. There is a corresponding ``disable`` command to stop using an
extension without uninstalling it.
.. versionchanged:: 4.2
Added ``--sys-prefix`` argument

@ -12,3 +12,4 @@ override the notebook's defaults with your own custom behavior.
contents
savehooks
handlers
frontend_extensions

@ -2,31 +2,16 @@
The Jupyter notebook
====================
.. sidebar:: What's New in Jupyter Notebook
:subtitle: Release :ref:`release-4.1.0`
`Release Announcement <https://blog.jupyter.org/2016/01/08/notebook-4-1-release/>`_
- Cell toolbar selector moved to View menu
- Restart & Run All Cells added to Kernel menu
- Multiple-cell selection and actions including cut, copy, paste and execute
- Command palette added for executing Jupyter actions
- Find and replace added to Edit menu
To upgrade to the release:
``pip install notebook --upgrade``
or
``conda upgrade notebook``
.. toctree::
:maxdepth: 1
:caption: User Documentation
notebook
Installation <https://jupyter.readthedocs.org/en/latest/install.html>
Running the Notebook <https://jupyter.readthedocs.org/en/latest/running.html>
Migrating from IPython <https://jupyter.readthedocs.org/en/latest/migrating.html>
Installation <https://jupyter.readthedocs.io/en/latest/install.html>
Running the Notebook <https://jupyter.readthedocs.io/en/latest/running.html>
Migrating from IPython <https://jupyter.readthedocs.io/en/latest/migrating.html>
ui_components
comms
.. toctree::
:maxdepth: 2
@ -49,6 +34,7 @@ The Jupyter notebook
:maxdepth: 1
:caption: Community documentation
examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages
examples/Notebook/rstversions/Examples and Tutorials Index
.. toctree::

@ -27,7 +27,7 @@
.. Other python projects
.. _matplotlib: http://matplotlib.org
.. _nbviewer: http://nbviewer.jupyter.org
.. _nbconvert: http://nbconvert.readthedocs.org/en/latest/
.. _nbconvert: https://nbconvert.readthedocs.io/en/latest/
.. Other tools and projects
.. _Markdown: http://daringfireball.net/projects/markdown/syntax

@ -414,7 +414,8 @@ You can generate a new notebook signing key with::
Browser Compatibility
---------------------
The Jupyter Notebook is officially supported the latest stable version the following browsers:
The Jupyter Notebook is officially supported by the latest stable versions of the
following browsers:
* Chrome
* Safari

@ -16,12 +16,21 @@ serving HTTP requests.
This document describes how you can
:ref:`secure a notebook server <notebook_server_security>` and how to
:ref:`run it on a public interface <notebook_public_server>`.
:ref:`run it on a public interface <notebook_public_server>`.
.. important::
**This is not the multi-user server you are looking for**. This document describes how you can run a public server with a single user. This should only be done by someone who wants remote access to their personal machine. Even so, doing this requires a thorough understanding of the set-ups limitations and security implications. If you allow multiple users to access a notebook server as it is described in this document, their commands may collide, clobber and overwrite each other.
If you want a multi-user server, the official solution is JupyterHub_. To use JupyterHub, you need a Unix server (typically Linux) running somewhere that is accessible to your users on a network. This may run over the public internet, but doing so introduces additional of `security concerns <https://jupyterhub.readthedocs.io/en/latest/getting-started.html#security>`_.
.. _ZeroMQ: http://zeromq.org
.. _Tornado: http://www.tornadoweb.org
.. _JupyterHub: https://jupyterhub.readthedocs.io/en/latest/
.. _notebook_server_security:

@ -1,11 +1,81 @@
.. _server_security:
Security in the Jupyter notebook server
=======================================
Since access to the Jupyter notebook server means access to running arbitrary code,
it is important to restrict access to the notebook server.
For this reason, notebook 4.3 introduces token-based authentication that is **on by default**.
.. note::
If you enable a password for your notebook server,
token authentication is not enabled by default,
and the behavior of the notebook server is unchanged from from versions earlier than 4.3.
When token authentication is enabled, the notebook uses a token to authenticate requests.
This token can be provided to login to the notebook server in three ways:
- in the ``Authorization`` header, e.g.::
Authorization: token abcdef...
- In a URL parameter, e.g.::
https://my-notebook/tree/?token=abcdef...
- In the password field of the login form that will be shown to you if you are not logged in.
When you start a notebook server with token authentication enabled (default),
a token is generated to use for authentication.
This token is logged to the terminal, so that you can copy/paste the URL into your browser::
[I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01
If the notebook server is going to open your browser automatically
(the default, unless ``--no-browser`` has been passed),
an *additional* token is generated for launching the browser.
This additional token can be used only once,
and is used to set a cookie for your browser once it connects.
After your browser has made its first request with this one-time-token,
the token is discarded and a cookie is set in your browser.
At any later time, you can see the tokens and URLs for all of your running servers with :command:`jupyter notebook list`::
$ jupyter notebook list
Currently running servers:
http://localhost:8888/?token=abc... :: /home/you/notebooks
https://0.0.0.0:9999/?token=123... :: /tmp/public
http://localhost:8889/ :: /tmp/has-password
For servers with token-authentication enabled, the URL in the above listing will include the token,
so you can copy and paste that URL into your browser to login.
If a server has no token (e.g. it has a password or has authentication disabled),
the URL will not include the token argument.
Once you have visited this URL,
a cookie will be set in your browser and you won't need to use the token again,
unless you switch browsers, clear your cookies, or start a notebook server on a new port.
You can disable authentication altogether by setting the token and password to empty strings,
but this is **NOT RECOMMENDED**, unless authentication or access restrictions are handled at a different layer in your web application:
.. sourcecode:: python
c.NotebookApp.token = ''
c.NotebookApp.password = ''
.. _notebook_security:
Security in Jupyter notebooks
=============================
Security in notebook documents
==============================
As Jupyter notebooks become more popular for sharing and collaboration,
the potential for malicious people to attempt to exploit the notebook
for their nefarious purposes increases. IPython 2.0 introduces a
for their nefarious purposes increases. IPython 2.0 introduced a
security model to prevent execution of untrusted code without explicit
user input.
@ -40,24 +110,19 @@ Our security model
The details of trust
--------------------
Jupyter notebooks store a signature in metadata, which is used to answer
the question "Did the current user do this?"
When a notebook is executed and saved, a signature is computed from a
digest of the notebook's contents plus a secret key. This is stored in a
database, writable only by the current user. By default, this is located at::
This signature is a digest of the notebooks contents plus a secret key,
known only to the user. The secret key is a user-only readable file in
the Jupyter profile's security directory. By default, this is::
~/.local/share/jupyter/nbsignatures.db # Linux
~/Library/Jupyter/nbsignatures.db # OS X
%APPDATA%/jupyter/nbsignatures.db # Windows
~/.jupyter/profile_default/security/notebook_secret
Each signature represents a series of outputs which were produced by code the
current user executed, and are therefore trusted.
.. note::
The notebook secret being stored in the profile means that
loading a notebook in another profile results in it being untrusted,
unless you copy or symlink the notebook secret to share it across profiles.
When a notebook is opened by a user, the server computes a signature
with the user's key, and compares it with the signature stored in the
notebook's metadata. If the signature matches, HTML and Javascript
When you open a notebook, the server computes its signature, and checks if it's
in the database. If a match is found, HTML and Javascript
output in the notebook will be trusted at load, otherwise it will be
untrusted.
@ -73,7 +138,7 @@ been removed (either via ``Clear Output`` or re-execution), then the
notebook will become trusted.
While trust is updated per output, this is only for the duration of a
single session. A notebook file on disk is either trusted or not in its
single session. A newly loaded notebook file is either trusted or not in its
entirety.
Explicit trust
@ -89,8 +154,8 @@ long time. Users can explicitly trust a notebook in two ways:
- After loading the untrusted notebook, with ``File / Trust Notebook``
These two methods simply load the notebook, compute a new signature with
the user's key, and then store the newly signed notebook.
These two methods simply load the notebook, compute a new signature, and add
that signature to the user's database.
Reporting security issues
-------------------------
@ -105,9 +170,9 @@ you can use :download:`this PGP public key <ipython_security.asc>`.
Affected use cases
------------------
Some use cases that work in Jupyter 1.0 will become less convenient in
Some use cases that work in Jupyter 1.0 became less convenient in
2.0 as a result of the security changes. We do our best to minimize
these annoyance, but security is always at odds with convenience.
these annoyances, but security is always at odds with convenience.
Javascript and CSS in Markdown cells
************************************
@ -135,20 +200,15 @@ in an untrusted state. There are three basic approaches to this:
- re-run notebooks when you get them (not always viable)
- explicitly trust notebooks via ``jupyter trust`` or the notebook menu
(annoying, but easy)
- share a notebook secret, and use a Jupyter profile dedicated to the
- share a notebook signatures database, and use configuration dedicated to the
collaboration while working on the project.
Multiple profiles or machines
*****************************
Since the notebook secret is stored in a profile directory by default,
opening a notebook with a different profile or on a different machine
will result in a different key, and thus be untrusted. The only current
way to address this is by sharing the notebook secret. This can be
facilitated by setting the configurable:
To share a signatures database among users, you can configure:
.. sourcecode:: python
c.NotebookApp.secret_file = "/path/to/notebook_secret"
c.NotebookNotary.data_dir = "/path/to/signature_dir"
in each profile, and only sharing the secret once per machine.
to specify a non-default path to the SQLite database (of notebook hashes,
essentially). We are aware that SQLite doesn't work well on NFS and we are
`working out better ways to do this <https://github.com/jupyter/notebook/issues/1782>`_.

@ -9,5 +9,5 @@ store the current version info of the notebook.
# Next beta/alpha/rc release: The version number for beta is X.Y.ZbN **without dots**.
version_info = (4, 2, 0, '.dev')
version_info = (4, 4, 1)
__version__ = '.'.join(map(str, version_info[:3])) + ''.join(version_info[3:])

@ -3,6 +3,12 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import re
try:
from urllib.parse import urlparse # Py 3
except ImportError:
from urlparse import urlparse # Py 2
import uuid
from tornado.escape import url_escape
@ -23,13 +29,37 @@ class LoginHandler(IPythonHandler):
message=message,
))
def _redirect_safe(self, url, default=None):
"""Redirect if url is on our PATH
Full-domain redirects are allowed if they pass our CORS origin checks.
Otherwise use default (self.base_url if unspecified).
"""
if default is None:
default = self.base_url
if not url.startswith(self.base_url):
# require that next_url be absolute path within our path
allow = False
# OR pass our cross-origin check
if '://' in url:
# if full URL, run our cross-origin check:
parsed = urlparse(url.lower())
origin = '%s://%s' % (parsed.scheme, parsed.netloc)
if self.allow_origin:
allow = self.allow_origin == origin
elif self.allow_origin_pat:
allow = bool(self.allow_origin_pat.match(origin))
if not allow:
# not allowed, use default
self.log.warn("Not allowing login redirect to %r" % url)
url = default
self.redirect(url)
def get(self):
if self.current_user:
next_url = self.get_argument('next', default=self.base_url)
if not next_url.startswith(self.base_url):
# require that next_url be absolute path within our path
next_url = self.base_url
self.redirect(next_url)
self._redirect_safe(next_url)
else:
self._render()
@ -39,25 +69,77 @@ class LoginHandler(IPythonHandler):
def post(self):
typed_password = self.get_argument('password', default=u'')
if self.login_available(self.settings):
if self.get_login_available(self.settings):
if passwd_check(self.hashed_password, typed_password):
# tornado <4.2 have a bug that consider secure==True as soon as
# 'secure' kwarg is passed to set_secure_cookie
if self.settings.get('secure_cookie', self.request.protocol == 'https'):
kwargs = {'secure': True}
else:
kwargs = {}
self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()), **kwargs)
self.set_login_cookie(self, uuid.uuid4().hex)
elif self.token and self.token == typed_password:
self.set_login_cookie(self, uuid.uuid4().hex)
else:
self.set_status(401)
self._render(message={'error': 'Invalid password'})
return
next_url = self.get_argument('next', default=self.base_url)
if not next_url.startswith(self.base_url):
# require that next_url be absolute path within our path
next_url = self.base_url
self.redirect(next_url)
self._redirect_safe(next_url)
@classmethod
def set_login_cookie(cls, handler, user_id=None):
"""Call this on handlers to set the login cookie for success"""
cookie_options = handler.settings.get('cookie_options', {})
cookie_options.setdefault('httponly', True)
# tornado <4.2 has a bug that considers secure==True as soon as
# 'secure' kwarg is passed to set_secure_cookie
if handler.settings.get('secure_cookie', handler.request.protocol == 'https'):
cookie_options.setdefault('secure', True)
handler.set_secure_cookie(handler.cookie_name, user_id, **cookie_options)
return user_id
auth_header_pat = re.compile('token\s+(.+)', re.IGNORECASE)
@classmethod
def get_token(cls, handler):
"""Get the user token from a request
Default:
- in URL parameters: ?token=<token>
- in header: Authorization: token <token>
"""
user_token = handler.get_argument('token', '')
if not user_token:
# get it from Authorization header
m = cls.auth_header_pat.match(handler.request.headers.get('Authorization', ''))
if m:
user_token = m.group(1)
return user_token
@classmethod
def should_check_origin(cls, handler):
"""Should the Handler check for CORS origin validation?
Origin check should be skipped for token-authenticated requests.
Returns:
- True, if Handler must check for valid CORS origin.
- False, if Handler should skip origin check since requests are token-authenticated.
"""
return not cls.is_token_authenticated(handler)
@classmethod
def is_token_authenticated(cls, handler):
"""Returns True if handler has been token authenticated. Otherwise, False.
Login with a token is used to signal certain things, such as:
- permit access to REST API
- xsrf protection
- skip origin-checks for scripts
"""
if getattr(handler, '_user_id', None) is None:
# ensure get_user has been called, so we know if we're token-authenticated
handler.get_current_user()
return getattr(handler, '_token_authenticated', False)
@classmethod
def get_user(cls, handler):
@ -67,18 +149,58 @@ class LoginHandler(IPythonHandler):
"""
# Can't call this get_current_user because it will collide when
# called on LoginHandler itself.
user_id = handler.get_secure_cookie(handler.cookie_name)
# For now the user_id should not return empty, but it could, eventually.
if user_id == '':
user_id = 'anonymous'
if getattr(handler, '_user_id', None):
return handler._user_id
user_id = cls.get_user_token(handler)
if user_id is None:
user_id = handler.get_secure_cookie(handler.cookie_name)
else:
cls.set_login_cookie(handler, user_id)
# Record that the current request has been authenticated with a token.
# Used in is_token_authenticated above.
handler._token_authenticated = True
if user_id is None:
# prevent extra Invalid cookie sig warnings:
handler.clear_login_cookie()
if not handler.login_available:
# Completely insecure! No authentication at all.
# No need to warn here, though; validate_security will have already done that.
user_id = 'anonymous'
# cache value for future retrievals on the same request
handler._user_id = user_id
return user_id
@classmethod
def get_user_token(cls, handler):
"""Identify the user based on a token in the URL or Authorization header
Returns:
- uuid if authenticated
- None if not
"""
token = handler.token
if not token:
return
# check login token from URL argument or Authorization header
user_token = cls.get_token(handler)
one_time_token = handler.one_time_token
authenticated = False
if user_token == token:
# token-authenticated, set the login cookie
handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
elif one_time_token and user_token == one_time_token:
# one-time-token-authenticated, only allow this token once
handler.settings.pop('one_time_token', None)
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
if authenticated:
return uuid.uuid4().hex
else:
return None
@classmethod
def validate_security(cls, app, ssl_options=None):
@ -91,9 +213,14 @@ class LoginHandler(IPythonHandler):
if ssl_options is None:
app.log.warning(warning + " and not using encryption. This "
"is not recommended.")
if not app.password:
if not app.password and not app.token:
app.log.warning(warning + " and not using authentication. "
"This is highly insecure and not recommended.")
else:
if not app.password and not app.token:
app.log.warning(
"All authentication is disabled."
" Anyone who can connect to this server will be able to run code.")
@classmethod
def password_from_settings(cls, settings):
@ -104,6 +231,6 @@ class LoginHandler(IPythonHandler):
return settings.get('password', u'')
@classmethod
def login_available(cls, settings):
def get_login_available(cls, settings):
"""Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
return bool(cls.password_from_settings(settings))
return bool(cls.password_from_settings(settings) or settings.get('token'))

@ -20,9 +20,7 @@ except ImportError:
from urlparse import urlparse # Py 2
from jinja2 import TemplateNotFound
from tornado import web
from tornado import gen
from tornado import web, gen, escape
from tornado.log import app_log
from notebook._sysinfo import get_sys_info
@ -42,9 +40,15 @@ non_alphanum = re.compile(r'[^A-Za-z0-9]')
sys_info = json.dumps(get_sys_info())
def log():
if Application.initialized():
return Application.instance().log
else:
return app_log
class AuthenticatedHandler(web.RequestHandler):
"""A RequestHandler with an authenticated user."""
@property
def content_security_policy(self):
"""The default Content-Security-Policy header
@ -81,6 +85,23 @@ class AuthenticatedHandler(web.RequestHandler):
return 'anonymous'
return self.login_handler.get_user(self)
def skip_check_origin(self):
"""Ask my login_handler if I should skip the origin_check
For example: in the default LoginHandler, if a request is token-authenticated,
origin checking should be skipped.
"""
if self.login_handler is None or not hasattr(self.login_handler, 'should_check_origin'):
return False
return not self.login_handler.should_check_origin(self)
@property
def token_authenticated(self):
"""Have I been authenticated with a token?"""
if self.login_handler is None or not hasattr(self.login_handler, 'is_token_authenticated'):
return False
return self.login_handler.is_token_authenticated(self)
@property
def cookie_name(self):
default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
@ -99,6 +120,16 @@ class AuthenticatedHandler(web.RequestHandler):
"""Return the login handler for this application, if any."""
return self.settings.get('login_handler_class', None)
@property
def token(self):
"""Return the login token for this application, if any."""
return self.settings.get('token', None)
@property
def one_time_token(self):
"""Return the one-time-use token for this application, if any."""
return self.settings.get('one_time_token', None)
@property
def login_available(self):
"""May a user proceed to log in?
@ -109,7 +140,7 @@ class AuthenticatedHandler(web.RequestHandler):
"""
if self.login_handler is None:
return False
return bool(self.login_handler.login_available(self.settings))
return bool(self.login_handler.get_login_available(self.settings))
class IPythonHandler(AuthenticatedHandler):
@ -134,10 +165,7 @@ class IPythonHandler(AuthenticatedHandler):
@property
def log(self):
"""use the IPython log by default, falling back on tornado's logger"""
if Application.initialized():
return Application.instance().log
else:
return app_log
return log()
@property
def jinja_template_vars(self):
@ -256,15 +284,20 @@ class IPythonHandler(AuthenticatedHandler):
Copied from WebSocket with changes:
- allow unspecified host/origin (e.g. scripts)
- allow token-authenticated requests
"""
if self.allow_origin == '*':
if self.allow_origin == '*' or self.skip_check_origin():
return True
host = self.request.headers.get("Host")
origin = self.request.headers.get("Origin")
# If no header is provided, assume it comes from a script/curl.
# We are only concerned with cross-site browser stuff here.
# If no header is provided, let the request through.
# Origin can be None for:
# - same-origin (IE, Firefox)
# - Cross-site POST form (IE, Firefox)
# - Scripts
# The cross-site POST (XSRF) case is handled by tornado's xsrf_token
if origin is None or host is None:
return True
@ -284,11 +317,19 @@ class IPythonHandler(AuthenticatedHandler):
# No CORS headers deny the request
allow = False
if not allow:
self.log.warn("Blocking Cross Origin API request. Origin: %s, Host: %s",
origin, host,
self.log.warning("Blocking Cross Origin API request for %s. Origin: %s, Host: %s",
self.request.path, origin, host,
)
return allow
def check_xsrf_cookie(self):
"""Bypass xsrf cookie checks when token-authenticated"""
if self.token_authenticated or self.settings.get('disable_check_xsrf', False):
# Token-authenticated requests do not need additional XSRF-check
# Servers without authentication are vulnerable to XSRF
return
return super(IPythonHandler, self).check_xsrf_cookie()
#---------------------------------------------------------------
# template rendering
#---------------------------------------------------------------
@ -310,11 +351,15 @@ class IPythonHandler(AuthenticatedHandler):
ws_url=self.ws_url,
logged_in=self.logged_in,
login_available=self.login_available,
token_available=bool(self.token or self.one_time_token),
static_url=self.static_url,
sys_info=sys_info,
contents_js_source=self.contents_js_source,
version_hash=self.version_hash,
ignore_minified_js=self.ignore_minified_js,
xsrf_form_html=self.xsrf_form_html,
token=self.token,
xsrf_token=self.xsrf_token.decode('utf8'),
**self.jinja_template_vars
)
@ -337,6 +382,7 @@ class IPythonHandler(AuthenticatedHandler):
exc_info = kwargs.get('exc_info')
message = ''
status_message = responses.get(status_code, 'Unknown HTTP Error')
exception = '(unknown)'
if exc_info:
exception = exc_info[1]
# get the custom message, if defined
@ -389,11 +435,10 @@ class APIHandler(IPythonHandler):
self.set_header('Content-Type', 'application/json')
return super(APIHandler, self).finish(*args, **kwargs)
@web.authenticated
def options(self, *args, **kwargs):
self.set_header('Access-Control-Allow-Headers', 'accept, content-type')
self.set_header('Access-Control-Allow-Headers', 'accept, content-type, authorization')
self.set_header('Access-Control-Allow-Methods',
'GET, PUT, PATCH, DELETE, OPTIONS')
'GET, PUT, POST, PATCH, DELETE, OPTIONS')
self.finish()
@ -411,7 +456,7 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
if os.path.splitext(path)[1] == '.ipynb':
name = path.rsplit('/', 1)[-1]
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
self.set_header('Content-Disposition','attachment; filename="%s"' % escape.url_escape(name))
return web.StaticFileHandler.get(self, path)
@ -528,6 +573,9 @@ class FileFindHandler(IPythonHandler, web.StaticFileHandler):
return ''
cls._static_paths[path] = abspath
log().debug("Path %s served from %s"%(path, abspath))
return abspath
def validate_absolute_path(self, root, absolute_path):
@ -598,6 +646,17 @@ class FilesRedirectHandler(IPythonHandler):
return self.redirect_to_files(self, path)
class RedirectWithParams(web.RequestHandler):
"""Sam as web.RedirectHandler, but preserves URL parameters"""
def initialize(self, url, permanent=True):
self._url = url
self._permanent = permanent
def get(self):
sep = '&' if '?' in self._url else '?'
url = sep.join([self._url, self.request.query])
self.redirect(url, permanent=self._permanent)
#-----------------------------------------------------------------------------
# URL pattern fragments for re-use
#-----------------------------------------------------------------------------

@ -126,21 +126,19 @@ class WebSocketMixin(object):
Tornado >= 4 calls this method automatically, raising 403 if it returns False.
"""
if self.allow_origin == '*':
if self.allow_origin == '*' or (
hasattr(self, 'skip_check_origin') and self.skip_check_origin()):
return True
host = self.request.headers.get("Host")
if origin is None:
origin = self.get_origin()
# If no header is provided, assume we can't verify origin
if origin is None:
self.log.warn("Missing Origin header, rejecting WebSocket connection.")
return False
if host is None:
self.log.warn("Missing Host header, rejecting WebSocket connection.")
return False
# If no origin or host header is provided, assume from script
if origin is None or host is None:
return True
origin = origin.lower()
origin_host = urlparse(origin).netloc
@ -218,16 +216,23 @@ class ZMQStreamHandler(WebSocketMixin, WebSocketHandler):
self.stream.close()
def _reserialize_reply(self, msg_list, channel=None):
def _reserialize_reply(self, msg_or_list, channel=None):
"""Reserialize a reply message using JSON.
This takes the msg list from the ZMQ socket, deserializes it using
self.session and then serializes the result using JSON. This method
should be used by self._on_zmq_reply to build messages that can
msg_or_list can be an already-deserialized msg dict or the zmq buffer list.
If it is the zmq list, it will be deserialized with self.session.
This takes the msg list from the ZMQ socket and serializes the result for the websocket.
This method should be used by self._on_zmq_reply to build messages that can
be sent back to the browser.
"""
idents, msg_list = self.session.feed_identities(msg_list)
msg = self.session.deserialize(msg_list)
if isinstance(msg_or_list, dict):
# already unpacked
msg = msg_or_list
else:
idents, msg_list = self.session.feed_identities(msg_or_list)
msg = self.session.deserialize(msg_list)
if channel:
msg['channel'] = channel
if msg['buffers']:

@ -8,7 +8,7 @@ import mimetypes
import json
import base64
from tornado import web
from tornado import web, escape
from notebook.base.handlers import IPythonHandler
@ -31,11 +31,11 @@ class FilesHandler(IPythonHandler):
model = cm.get(path, type='file')
if self.get_argument("download", False):
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
self.set_header('Content-Disposition','attachment; filename="%s"' % escape.url_escape(name))
# get mimetype from filename
if name.endswith('.ipynb'):
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Type', 'application/x-ipynb+json')
else:
cur_mime = mimetypes.guess_type(name)[0]
if cur_mime is not None:

@ -313,6 +313,7 @@ class JSController(TestController):
'-m', 'notebook',
'--no-browser',
'--notebook-dir', self.nbdir.name,
'--NotebookApp.token=',
'--NotebookApp.base_url=%s' % self.base_url,
]
# ipc doesn't work on Windows, and darwin has crazy-long temp paths,

@ -7,7 +7,8 @@ import io
import os
import zipfile
from tornado import web
from tornado import web, escape
from tornado.log import app_log
from ..base.handlers import (
IPythonHandler, FilesRedirectHandler,
@ -38,7 +39,7 @@ def respond_zip(handler, name, output, resources):
# Headers
zip_filename = os.path.splitext(name)[0] + '.zip'
handler.set_header('Content-Disposition',
'attachment; filename="%s"' % zip_filename)
'attachment; filename="%s"' % escape.url_escape(zip_filename))
handler.set_header('Content-Type', 'application/zip')
# Prepare the zip file
@ -70,6 +71,7 @@ def get_exporter(format, **kwargs):
try:
return Exporter(**kwargs)
except Exception as e:
app_log.exception("Could not construct Exporter: %s", Exporter)
raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
class NbconvertFileHandler(IPythonHandler):
@ -103,6 +105,7 @@ class NbconvertFileHandler(IPythonHandler):
}
)
except Exception as e:
self.log.exception("nbconvert failed: %s", e)
raise web.HTTPError(500, "nbconvert failed: %s" % e)
if respond_zip(self, name, output, resources):
@ -112,7 +115,7 @@ class NbconvertFileHandler(IPythonHandler):
if self.get_argument('download', 'false').lower() == 'true':
filename = os.path.splitext(name)[0] + resources['output_extension']
self.set_header('Content-Disposition',
'attachment; filename="%s"' % filename)
'attachment; filename="%s"' % escape.url_escape(filename))
# MIME type
if exporter.output_mimetype:

@ -20,12 +20,12 @@ from ipython_genutils.testing.decorators import onlyif_cmds_exist
class NbconvertAPI(object):
"""Wrapper for nbconvert API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'nbconvert', path),
response = self.request(verb,
url_path_join('nbconvert', path),
data=body, params=params,
)
response.raise_for_status()
@ -69,7 +69,7 @@ class APITest(NotebookTestBase):
encoding='utf-8') as f:
write(nb, f, version=4)
self.nbconvert_api = NbconvertAPI(self.base_url())
self.nbconvert_api = NbconvertAPI(self.request)
def tearDown(self):
nbdir = self.notebook_dir.name
@ -109,8 +109,7 @@ class APITest(NotebookTestBase):
@onlyif_cmds_exist('pandoc')
def test_from_post(self):
nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
nbmodel = requests.get(nbmodel_url).json()
nbmodel = self.request('GET', 'api/contents/foo/testnb.ipynb').json()
r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
self.assertEqual(r.status_code, 200)
@ -124,8 +123,7 @@ class APITest(NotebookTestBase):
@onlyif_cmds_exist('pandoc')
def test_from_post_zip(self):
nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
nbmodel = requests.get(nbmodel_url).json()
nbmodel = self.request('GET', 'api/contents/foo/testnb.ipynb').json()
r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
self.assertIn(u'application/zip', r.headers['Content-Type'])

File diff suppressed because it is too large Load Diff

@ -7,24 +7,24 @@
from __future__ import absolute_import, print_function
import base64
import binascii
import datetime
import errno
import importlib
import io
import json
import logging
import mimetypes
import os
import random
import re
import select
import signal
import socket
import ssl
import sys
import threading
import webbrowser
from jinja2 import Environment, FileSystemLoader
# Install the pyzmq ioloop. This has to be done before anything else from
@ -47,6 +47,7 @@ if version_info < (4,0):
from tornado import httpserver
from tornado import web
from tornado.httputil import url_concat
from tornado.log import LogFormatter, app_log, access_log, gen_log
from notebook import (
@ -54,7 +55,8 @@ from notebook import (
DEFAULT_TEMPLATE_PATH_LIST,
__version__,
)
from .base.handlers import Template404
from .base.handlers import Template404, RedirectWithParams
from .log import log_request
from .services.kernels.kernelmanager import MappingKernelManager
from .services.config import ConfigManager
@ -77,7 +79,7 @@ from jupyter_client.session import Session
from nbformat.sign import NotebookNotary
from traitlets import (
Dict, Unicode, Integer, List, Bool, Bytes, Instance,
TraitError, Type,
TraitError, Type, Float,
)
from ipython_genutils import py3compat
from jupyter_core.paths import jupyter_runtime_dir, jupyter_path
@ -116,15 +118,6 @@ def load_handlers(name):
return mod.default_handlers
class DeprecationHandler(IPythonHandler):
def get(self, url_path):
self.set_header("Content-Type", 'text/javascript')
self.finish("""
console.warn('`/static/widgets/js` is deprecated. Use `nbextensions/widgets/widgets/js` instead.');
define(['%s'], function(x) { return x; });
""" % url_path_join('nbextensions', 'widgets', 'widgets', url_path.rstrip('.js')))
self.log.warn('Deprecated widget Javascript path /static/widgets/js/*.js was used')
#-----------------------------------------------------------------------------
# The Tornado web application
#-----------------------------------------------------------------------------
@ -187,13 +180,20 @@ class NotebookWebApplication(web.Application):
},
version_hash=version_hash,
ignore_minified_js=ipython_app.ignore_minified_js,
# rate limits
iopub_msg_rate_limit=ipython_app.iopub_msg_rate_limit,
iopub_data_rate_limit=ipython_app.iopub_data_rate_limit,
rate_limit_window=ipython_app.rate_limit_window,
# authentication
cookie_secret=ipython_app.cookie_secret,
login_url=url_path_join(base_url,'/login'),
login_handler_class=ipython_app.login_handler_class,
logout_handler_class=ipython_app.logout_handler_class,
password=ipython_app.password,
xsrf_cookies=True,
disable_check_xsrf=ipython_app.disable_check_xsrf,
# managers
kernel_manager=kernel_manager,
@ -222,7 +222,6 @@ class NotebookWebApplication(web.Application):
# Order matters. The first handler to match the URL will handle the request.
handlers = []
handlers.append((r'/deprecatedwidgets/(.*)', DeprecationHandler))
handlers.extend(load_handlers('tree.handlers'))
handlers.extend([(r"/login", settings['login_handler_class'])])
handlers.extend([(r"/logout", settings['logout_handler_class'])])
@ -241,16 +240,20 @@ class NotebookWebApplication(web.Application):
handlers.extend(load_handlers('services.security.handlers'))
# BEGIN HARDCODED WIDGETS HACK
# TODO: Remove on notebook 5.0
try:
import ipywidgets
handlers.append(
(r"/nbextensions/widgets/(.*)", FileFindHandler, {
'path': ipywidgets.find_static_assets(),
'no_cache_paths': ['/'], # don't cache anything in nbextensions
}),
)
import widgetsnbextension
except:
app_log.warn('ipywidgets package not installed. Widgets are unavailable.')
try:
import ipywidgets as widgets
handlers.append(
(r"/nbextensions/widgets/(.*)", FileFindHandler, {
'path': widgets.find_static_assets(),
'no_cache_paths': ['/'], # don't cache anything in nbextensions
}),
)
except:
app_log.warning('Widgets are unavailable. Please install widgetsnbextension or ipywidgets 4.0')
# END HARDCODED WIDGETS HACK
handlers.append(
@ -269,7 +272,7 @@ class NotebookWebApplication(web.Application):
handlers.extend(load_handlers('base.handlers'))
# set the URL that will be redirected from `/`
handlers.append(
(r'/?', web.RedirectHandler, {
(r'/?', RedirectWithParams, {
'url' : settings['default_url'],
'permanent': False, # want 302, not 301
})
@ -306,7 +309,10 @@ class NbserverListApp(JupyterApp):
if self.json:
print(json.dumps(serverinfo))
else:
print(serverinfo['url'], "::", serverinfo['notebook_dir'])
url = serverinfo['url']
if serverinfo.get('token'):
url = url + '?token=%s' % serverinfo['token']
print(url, "::", serverinfo['notebook_dir'])
#-----------------------------------------------------------------------------
# Aliases and Flags
@ -333,6 +339,11 @@ flags['no-mathjax']=(
"""
)
flags['allow-root']=(
{'NotebookApp' : {'allow_root' : True}},
"Allow the notebook to be run from root user."
)
# Add notebook manager flags
flags.update(boolean_flag('script', 'FileContentsManager.save_script',
'DEPRECATED, IGNORED',
@ -432,6 +443,10 @@ class NotebookApp(JupyterApp):
help="Set the Access-Control-Allow-Credentials: true header"
)
allow_root = Bool(False, config=True,
help="Whether to allow the user to run the notebook as root."
)
default_url = Unicode('/tree', config=True,
help="The default URL to redirect to from `/`"
)
@ -473,7 +488,7 @@ class NotebookApp(JupyterApp):
)
client_ca = Unicode(u'', config=True,
help="""The full path to a certificate authority certifificate for SSL/TLS client authentication."""
help="""The full path to a certificate authority certificate for SSL/TLS client authentication."""
)
cookie_secret_file = Unicode(config=True,
@ -513,6 +528,37 @@ class NotebookApp(JupyterApp):
self.cookie_secret_file
)
token = Unicode('<generated>', config=True,
help="""Token used for authenticating first-time connections to the server.
When no password is enabled,
the default is to generate a new, random token.
Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.
"""
)
one_time_token = Unicode(
help="""One-time token used for opening a browser.
Once used, this token cannot be used again.
"""
)
_token_generated = True
def _token_default(self):
if self.password:
# no token if password is enabled
self._token_generated = False
return u''
else:
self._token_generated = True
return binascii.hexlify(os.urandom(24)).decode('ascii')
def _token_changed(self, name, old, new):
self._token_generated = False
password = Unicode(u'', config=True,
help="""Hashed password to use for web authentication.
@ -524,6 +570,22 @@ class NotebookApp(JupyterApp):
"""
)
disable_check_xsrf = Bool(False, config=True,
help="""Disable cross-site-request-forgery protection
Jupyter notebook 4.3.1 introduces protection from cross-site request forgeries,
requiring API requests to either:
- originate from pages served by this server (validated with XSRF cookie and token), or
- authenticate with a token
Some anonymous compute resources still desire the ability to run code,
completely without authentication.
These services can disable all authentication and security checks,
with the full knowledge of what that implies.
"""
)
open_browser = Bool(True, config=True,
help="""Whether to open in a browser after starting.
The specific browser used is platform dependent and
@ -551,6 +613,10 @@ class NotebookApp(JupyterApp):
help="Supply overrides for the tornado.web.Application that the "
"Jupyter notebook uses.")
cookie_options = Dict(config=True,
help="Extra keyword arguments to pass to `set_secure_cookie`."
" See tornado's set_secure_cookie docs for details."
)
ssl_options = Dict(config=True,
help="""Supply SSL options for the tornado HTTPServer.
See the tornado docs for details.""")
@ -653,7 +719,10 @@ class NotebookApp(JupyterApp):
"""
)
mathjax_url = Unicode("", config=True,
help="""The url for MathJax.js."""
help="""A custom url for MathJax.js.
Should be in the form of a case-sensitive url to MathJax,
for example: /static/components/MathJax/MathJax.js
"""
)
def _mathjax_url_default(self):
if not self.enable_mathjax:
@ -759,6 +828,11 @@ class NotebookApp(JupyterApp):
def _notebook_dir_validate(self, value, trait):
# Strip any trailing slashes
# *except* if it's root
_, path = os.path.splitdrive(value)
if path == os.sep:
return value
value = value.rstrip(os.sep)
if not os.path.isabs(value):
@ -774,9 +848,19 @@ class NotebookApp(JupyterApp):
self.config.FileContentsManager.root_dir = new
self.config.MappingKernelManager.root_dir = new
# TODO: Remove me in notebook 5.0
server_extensions = List(Unicode(), config=True,
help=("Python modules to load as notebook server extensions. "
"This is an experimental API, and may change in future releases.")
help=("DEPRECATED use the nbserver_extensions dict instead")
)
def _server_extensions_changed(self, name, old, new):
self.log.warning("server_extensions is deprecated, use nbserver_extensions")
self.server_extensions = new
nbserver_extensions = Dict({}, config=True,
help=("Dict of Python modules to load as notebook server extensions."
"Entry values can be used to enable and disable the loading of"
"the extensions. The extensions will be loaded in alphabetical "
"order.")
)
reraise_server_extension_failures = Bool(
@ -785,9 +869,20 @@ class NotebookApp(JupyterApp):
help="Reraise exceptions encountered loading server extensions?",
)
iopub_msg_rate_limit = Float(0, config=True, help="""(msg/sec)
Maximum rate at which messages can be sent on iopub before they are
limited.""")
iopub_data_rate_limit = Float(0, config=True, help="""(bytes/sec)
Maximum rate at which messages can be sent on iopub before they are
limited.""")
rate_limit_window = Float(1.0, config=True, help="""(sec) Time window used to
check the message and data rate limits.""")
def parse_command_line(self, argv=None):
super(NotebookApp, self).parse_command_line(argv)
if self.extra_args:
arg0 = self.extra_args[0]
f = os.path.abspath(arg0)
@ -852,6 +947,12 @@ class NotebookApp(JupyterApp):
if self.allow_origin_pat:
self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
self.tornado_settings['allow_credentials'] = self.allow_credentials
self.tornado_settings['cookie_options'] = self.cookie_options
self.tornado_settings['token'] = self.token
if (self.open_browser or self.file_to_run) and not self.password:
self.one_time_token = binascii.hexlify(os.urandom(24)).decode('ascii')
self.tornado_settings['one_time_token'] = self.one_time_token
# ensure default_url starts with base_url
if not self.default_url.startswith(self.base_url):
self.default_url = url_path_join(self.base_url, self.default_url)
@ -870,13 +971,17 @@ class NotebookApp(JupyterApp):
ssl_options['keyfile'] = self.keyfile
if self.client_ca:
ssl_options['ca_certs'] = self.client_ca
ssl_options['cert_reqs'] = ssl.CERT_REQUIRED
if not ssl_options:
# None indicates no SSL config
ssl_options = None
else:
# Disable SSLv3, since its use is discouraged.
ssl_options['ssl_version']=ssl.PROTOCOL_TLSv1
# SSL may be missing, so only import it if it's to be used
import ssl
# Disable SSLv3 by default, since its use is discouraged.
ssl_options.setdefault('ssl_version', ssl.PROTOCOL_TLSv1)
if ssl_options.get('ca_certs', False):
ssl_options.setdefault('cert_reqs', ssl.CERT_REQUIRED)
self.login_handler_class.validate_security(self, ssl_options=ssl_options)
self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
xheaders=self.trust_xheaders)
@ -887,7 +992,7 @@ class NotebookApp(JupyterApp):
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 random 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)):
self.log.warn("Permission to listen on port %i denied" % port)
@ -906,7 +1011,14 @@ class NotebookApp(JupyterApp):
@property
def display_url(self):
ip = self.ip if self.ip else '[all ip addresses on your system]'
return self._url(ip)
url = self._url(ip)
if self.token:
# Don't log full token if it came from config
token = self.token if self._token_generated else '...'
url = url_concat(url, {'token': token})
return url
query = '?token=%s' % self.token if self.token else ''
return self._url(ip) + query
@property
def connection_url(self):
@ -1000,18 +1112,34 @@ class NotebookApp(JupyterApp):
The extension API is experimental, and may change in future releases.
"""
# TODO: Remove me in notebook 5.0
for modulename in self.server_extensions:
try:
mod = importlib.import_module(modulename)
func = getattr(mod, 'load_jupyter_server_extension', None)
if func is not None:
func(self)
except Exception:
if self.reraise_server_extension_failures:
raise
self.log.warn("Error loading server extension %s", modulename,
exc_info=True)
# Don't override disable state of the extension if it already exist
# in the new traitlet
if not modulename in self.nbserver_extensions:
self.nbserver_extensions[modulename] = True
for modulename in sorted(self.nbserver_extensions):
if self.nbserver_extensions[modulename]:
try:
mod = importlib.import_module(modulename)
func = getattr(mod, 'load_jupyter_server_extension', None)
if func is not None:
func(self)
except Exception:
if self.reraise_server_extension_failures:
raise
self.log.warning("Error loading server extension %s", modulename,
exc_info=True)
def init_mime_overrides(self):
# On some Windows machines, an application has registered an incorrect
# mimetype for CSS in the registry. Tornado uses this when serving
# .css files, causing browsers to reject the stylesheet. We know the
# mimetype always needs to be text/css, so we override it here.
mimetypes.add_type('text/css', '.css')
@catch_config_error
def initialize(self, argv=None):
super(NotebookApp, self).initialize(argv)
@ -1024,6 +1152,7 @@ class NotebookApp(JupyterApp):
self.init_terminals()
self.init_signal()
self.init_server_extensions()
self.init_mime_overrides()
def cleanup_kernels(self):
"""Shutdown all kernels.
@ -1047,14 +1176,16 @@ class NotebookApp(JupyterApp):
'port': self.port,
'secure': bool(self.certfile),
'base_url': self.base_url,
'token': self.token,
'notebook_dir': os.path.abspath(self.notebook_dir),
'pid': os.getpid()
'password': bool(self.password),
'pid': os.getpid(),
}
def write_server_info_file(self):
"""Write the result of server_info() to the JSON file info_file."""
with open(self.info_file, 'w') as f:
json.dump(self.server_info(), f, indent=2)
json.dump(self.server_info(), f, indent=2, sort_keys=True)
def remove_server_info_file(self):
"""Remove the nbserver-<pid>.json file created for this server.
@ -1072,8 +1203,19 @@ class NotebookApp(JupyterApp):
This method takes no arguments so all configuration and initialization
must be done prior to calling this method."""
super(NotebookApp, self).start()
if not self.allow_root:
# check if we are running as root, and abort if it's not allowed
try:
uid = os.geteuid()
except AttributeError:
uid = -1 # anything nonzero here, since we can't check UID assume non-root
if uid == 0:
self.log.critical("Running as root is not recommended. Use --allow-root to bypass.")
self.exit(1)
info = self.log.info
for line in self.notebook_info().split("\n"):
info(line)
@ -1096,12 +1238,25 @@ class NotebookApp(JupyterApp):
relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))
else:
uri = self.default_url
# default_url contains base_url, but so does connection_url
uri = self.default_url[len(self.base_url):]
if self.one_time_token:
uri = url_concat(uri, {'token': self.one_time_token})
if browser:
b = lambda : browser.open(url_path_join(self.connection_url, uri),
new=2)
threading.Thread(target=b).start()
if self.token and self._token_generated:
# log full URL with generated token, so there's a copy/pasteable link
# with auth info.
self.log.critical('\n'.join([
'\n',
'Copy/paste this URL into your browser when you connect for the first time,',
'to login with a token:',
' %s' % url_concat(self.connection_url, {'token': self.token}),
]))
self.io_loop = ioloop.IOLoop.current()
if sys.platform.startswith('win'):
# add no-op to wake every 5s
@ -1113,9 +1268,9 @@ class NotebookApp(JupyterApp):
except KeyboardInterrupt:
info("Interrupted...")
finally:
self.cleanup_kernels()
self.remove_server_info_file()
self.cleanup_kernels()
def stop(self):
def _stop():
self.http_server.stop()
@ -1149,7 +1304,7 @@ def list_running_servers(runtime_dir=None):
else:
# If the process has died, try to delete its info file
try:
os.unlink(file)
os.unlink(os.path.join(runtime_dir, file))
except OSError:
pass # TODO: This should warn or log or something
#-----------------------------------------------------------------------------
@ -1157,4 +1312,3 @@ def list_running_servers(runtime_dir=None):
#-----------------------------------------------------------------------------
main = launch_new_instance = NotebookApp.launch_instance

@ -0,0 +1,340 @@
# coding: utf-8
"""Utilities for installing server extensions for the notebook"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import print_function
import importlib
import sys
from jupyter_core.paths import jupyter_config_path
from ._version import __version__
from .nbextensions import (
JupyterApp, BaseNBExtensionApp, _get_config_dir,
GREEN_ENABLED, RED_DISABLED,
GREEN_OK, RED_X,
)
from traitlets import Bool
from traitlets.utils.importstring import import_item
from traitlets.config.manager import BaseJSONConfigManager
# ------------------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------------------
class ArgumentConflict(ValueError):
pass
def toggle_serverextension_python(import_name, enabled=None, parent=None,
user=True, sys_prefix=False, logger=None):
"""Toggle a server extension.
By default, toggles the extension in the system-wide Jupyter configuration
location (e.g. /usr/local/etc/jupyter).
Parameters
----------
import_name : str
Importable Python module (dotted-notation) exposing the magic-named
`load_jupyter_server_extension` function
enabled : bool [default: None]
Toggle state for the extension. Set to None to toggle, True to enable,
and False to disable the extension.
parent : Configurable [default: None]
user : bool [default: True]
Toggle in the user's configuration location (e.g. ~/.jupyter).
sys_prefix : bool [default: False]
Toggle in the current Python environment's configuration location
(e.g. ~/.envs/my-env/etc/jupyter). Will override `user`.
logger : Jupyter logger [optional]
Logger instance to use
"""
user = False if sys_prefix else user
config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
cm = BaseJSONConfigManager(parent=parent, config_dir=config_dir)
cfg = cm.get("jupyter_notebook_config")
server_extensions = (
cfg.setdefault("NotebookApp", {})
.setdefault("nbserver_extensions", {})
)
old_enabled = server_extensions.get(import_name, None)
new_enabled = enabled if enabled is not None else not old_enabled
if logger:
if new_enabled:
logger.info(u"Enabling: %s" % (import_name))
else:
logger.info(u"Disabling: %s" % (import_name))
server_extensions[import_name] = new_enabled
if logger:
logger.info(u"- Writing config: {}".format(config_dir))
cm.update("jupyter_notebook_config", cfg)
if new_enabled:
validate_serverextension(import_name, logger)
def validate_serverextension(import_name, logger=None):
"""Assess the health of an installed server extension
Returns a list of validation warnings.
Parameters
----------
import_name : str
Importable Python module (dotted-notation) exposing the magic-named
`load_jupyter_server_extension` function
logger : Jupyter logger [optional]
Logger instance to use
"""
warnings = []
infos = []
func = None
if logger:
logger.info(" - Validating...")
try:
mod = importlib.import_module(import_name)
func = getattr(mod, 'load_jupyter_server_extension', None)
except Exception:
logger.warning("Error loading server extension %s", import_name)
import_msg = u" {} is {} importable?"
if func is not None:
infos.append(import_msg.format(GREEN_OK, import_name))
else:
warnings.append(import_msg.format(RED_X, import_name))
post_mortem = u" {} {} {}"
if logger:
if warnings:
[logger.info(info) for info in infos]
[logger.warn(warning) for warning in warnings]
else:
logger.info(post_mortem.format(import_name, "", GREEN_OK))
return warnings
# ----------------------------------------------------------------------
# Applications
# ----------------------------------------------------------------------
flags = {}
flags.update(JupyterApp.flags)
flags.pop("y", None)
flags.pop("generate-config", None)
flags.update({
"user" : ({
"ToggleServerExtensionApp" : {
"user" : True,
}}, "Perform the operation for the current user"
),
"system" : ({
"ToggleServerExtensionApp" : {
"user" : False,
"sys_prefix": False,
}}, "Perform the operation system-wide"
),
"sys-prefix" : ({
"ToggleServerExtensionApp" : {
"sys_prefix" : True,
}}, "Use sys.prefix as the prefix for installing server extensions"
),
"py" : ({
"ToggleServerExtensionApp" : {
"python" : True,
}}, "Install from a Python package"
),
})
flags['python'] = flags['py']
class ToggleServerExtensionApp(BaseNBExtensionApp):
"""A base class for enabling/disabling extensions"""
name = "jupyter serverextension enable/disable"
description = "Enable/disable a server extension using frontend configuration files."
flags = flags
user = Bool(True, config=True, help="Whether to do a user install")
sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix")
python = Bool(False, config=True, help="Install from a Python package")
def toggle_server_extension(self, import_name):
"""Change the status of a named server extension.
Uses the value of `self._toggle_value`.
Parameters
---------
import_name : str
Importable Python module (dotted-notation) exposing the magic-named
`load_jupyter_server_extension` function
"""
toggle_serverextension_python(
import_name, self._toggle_value, parent=self, user=self.user,
sys_prefix=self.sys_prefix, logger=self.log)
def toggle_server_extension_python(self, package):
"""Change the status of some server extensions in a Python package.
Uses the value of `self._toggle_value`.
Parameters
---------
package : str
Importable Python module exposing the
magic-named `_jupyter_server_extension_paths` function
"""
m, server_exts = _get_server_extension_metadata(package)
for server_ext in server_exts:
module = server_ext['module']
self.toggle_server_extension(module)
def start(self):
"""Perform the App's actions as configured"""
if not self.extra_args:
sys.exit('Please specify a server extension/package to enable or disable')
for arg in self.extra_args:
if self.python:
self.toggle_server_extension_python(arg)
else:
self.toggle_server_extension(arg)
class EnableServerExtensionApp(ToggleServerExtensionApp):
"""An App that enables (and validates) Server Extensions"""
name = "jupyter serverextension enable"
description = """
Enable a serverextension in configuration.
Usage
jupyter serverextension enable [--system|--sys-prefix]
"""
_toggle_value = True
class DisableServerExtensionApp(ToggleServerExtensionApp):
"""An App that disables Server Extensions"""
name = "jupyter serverextension disable"
description = """
Disable a serverextension in configuration.
Usage
jupyter serverextension disable [--system|--sys-prefix]
"""
_toggle_value = False
class ListServerExtensionsApp(BaseNBExtensionApp):
"""An App that lists (and validates) Server Extensions"""
name = "jupyter serverextension list"
version = __version__
description = "List all server extensions known by the configuration system"
def list_server_extensions(self):
"""List all enabled and disabled server extensions, by config path
Enabled extensions are validated, potentially generating warnings.
"""
config_dirs = jupyter_config_path()
for config_dir in config_dirs:
cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
data = cm.get("jupyter_notebook_config")
server_extensions = (
data.setdefault("NotebookApp", {})
.setdefault("nbserver_extensions", {})
)
if server_extensions:
print(u'config dir: {}'.format(config_dir))
for import_name, enabled in server_extensions.items():
print(u' {} {}'.format(
import_name,
GREEN_ENABLED if enabled else RED_DISABLED))
validate_serverextension(import_name, self.log)
def start(self):
"""Perform the App's actions as configured"""
self.list_server_extensions()
_examples = """
jupyter serverextension list # list all configured server extensions
jupyter serverextension enable --py <packagename> # enable all server extensions in a Python package
jupyter serverextension disable --py <packagename> # disable all server extensions in a Python package
"""
class ServerExtensionApp(BaseNBExtensionApp):
"""Root level server extension app"""
name = "jupyter serverextension"
version = __version__
description = "Work with Jupyter server extensions"
examples = _examples
subcommands = dict(
enable=(EnableServerExtensionApp, "Enable an server extension"),
disable=(DisableServerExtensionApp, "Disable an server extension"),
list=(ListServerExtensionsApp, "List server extensions")
)
def start(self):
"""Perform the App's actions as configured"""
super(ServerExtensionApp, self).start()
# The above should have called a subcommand and raised NoStart; if we
# get here, it didn't, so we should self.log.info a message.
subcmds = ", ".join(sorted(self.subcommands))
sys.exit("Please supply at least one subcommand: %s" % subcmds)
main = ServerExtensionApp.launch_instance
# ------------------------------------------------------------------------------
# Private API
# ------------------------------------------------------------------------------
def _get_server_extension_metadata(module):
"""Load server extension metadata from a module.
Returns a tuple of (
the package as loaded
a list of server extension specs: [
{
"module": "mockextension"
}
]
)
Parameters
----------
module : str
Importable Python module exposing the
magic-named `_jupyter_server_extension_paths` function
"""
m = import_item(module)
if not hasattr(m, '_jupyter_server_extension_paths'):
raise KeyError(u'The Python module {} does not include any valid server extensions'.format(module))
return m, m._jupyter_server_extension_paths()
if __name__ == '__main__':
main()

@ -42,7 +42,7 @@ paths:
schema:
$ref: '#/definitions/Session'
patch:
summary: This can be used to rename the notebook, or move it to a new directory.
summary: "This can be used to rename the session."
tags:
- sessions
parameters:
@ -50,15 +50,7 @@ paths:
in: body
required: true
schema:
type: object
properties:
notebook:
type: object
properties:
path:
type: string
format: path
description: new path for notebook
$ref: '#definitions/Session'
responses:
200:
description: Session
@ -88,29 +80,14 @@ paths:
items:
$ref: '#/definitions/Session'
post:
summary: Create a new session, or return an existing session if a session for the notebook path already exists
summary: "Create a new session, or return an existing session if a session of the same name already exists"
tags:
- sessions
parameters:
- name: session
in: body
schema:
type: object
properties:
notebook:
type: object
required:
- path
properties:
path:
type: string
description: path to notebook file
kernel:
type: object
properties:
name:
type: string
description: Kernel spec name, defaults to default kernel spec
$ref: '#definitions/Session'
responses:
201:
description: Session created or returned
@ -122,7 +99,7 @@ paths:
type: string
format: url
501:
description: Kernel not available
description: Session not available
schema:
type: object
description: error message
@ -351,12 +328,15 @@ definitions:
id:
type: string
format: uuid
notebook:
type: object
properties:
path:
type: string
description: path to notebook
path:
type: string
description: path to the session
name:
type: string
description: name of the session
type:
type: string;
description: session type
kernel:
$ref: '#/definitions/Kernel'

@ -13,21 +13,21 @@ from ...base.handlers import APIHandler, json_errors
class ConfigHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
def get(self, section_name):
self.set_header("Content-Type", 'application/json')
self.finish(json.dumps(self.config_manager.get(section_name)))
@web.authenticated
@json_errors
@web.authenticated
def put(self, section_name):
data = self.get_json_body() # Will raise 400 if content is not valid JSON
self.config_manager.set(section_name, data)
self.set_status(204)
@web.authenticated
@json_errors
@web.authenticated
def patch(self, section_name):
new_data = self.get_json_body()
section = self.config_manager.update(section_name, new_data)

@ -5,13 +5,47 @@
import os.path
from traitlets.config.manager import BaseJSONConfigManager
from jupyter_core.paths import jupyter_config_dir
from traitlets import Unicode
from traitlets.config.manager import BaseJSONConfigManager, recursive_update
from jupyter_core.paths import jupyter_config_dir, jupyter_config_path
from traitlets import Unicode, Instance, List
from traitlets.config import LoggingConfigurable
class ConfigManager(BaseJSONConfigManager):
class ConfigManager(LoggingConfigurable):
"""Config Manager used for storing notebook frontend config"""
config_dir = Unicode(config=True)
def _config_dir_default(self):
# Public API
def get(self, section_name):
"""Get the config from all config sections."""
config = {}
# step through back to front, to ensure front of the list is top priority
for p in self.read_config_path[::-1]:
cm = BaseJSONConfigManager(config_dir=p)
recursive_update(config, cm.get(section_name))
return config
def set(self, section_name, data):
"""Set the config only to the user's config."""
return self.write_config_manager.set(section_name, data)
def update(self, section_name, new_data):
"""Update the config only to the user's config."""
return self.write_config_manager.update(section_name, new_data)
# Private API
read_config_path = List(Unicode())
def _read_config_path_default(self):
return [os.path.join(p, 'nbconfig') for p in jupyter_config_path()]
write_config_dir = Unicode()
def _write_config_dir_default(self):
return os.path.join(jupyter_config_dir(), 'nbconfig')
write_config_manager = Instance(BaseJSONConfigManager)
def _write_config_manager_default(self):
return BaseJSONConfigManager(config_dir=self.write_config_dir)
def _write_config_dir_changed(self, name, old, new):
self.write_config_manager = BaseJSONConfigManager(config_dir=self.write_config_dir)

@ -11,12 +11,12 @@ from notebook.tests.launchnotebook import NotebookTestBase
class ConfigAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, section, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/config', section),
response = self.request(verb,
url_path_join('api/config', section),
data=body,
)
response.raise_for_status()
@ -34,7 +34,7 @@ class ConfigAPI(object):
class APITest(NotebookTestBase):
"""Test the config web service API"""
def setUp(self):
self.config_api = ConfigAPI(self.base_url())
self.config_api = ConfigAPI(self.request)
def test_create_retrieve_config(self):
sample = {'foo': 'bar', 'baz': 73}

@ -7,6 +7,7 @@
import io
import os
import shutil
import warnings
import mimetypes
import nbformat
@ -32,9 +33,10 @@ _script_exporter = None
def _post_save_script(model, os_path, contents_manager, **kwargs):
"""convert notebooks to Python script after save with nbconvert
replaces `ipython notebook --script`
replaces `jupyter notebook --script`
"""
from nbconvert.exporters.script import ScriptExporter
warnings.warn("`_post_save_script` is deprecated and will be removed in Notebook 5.0", DeprecationWarning)
if model['type'] != 'notebook':
return
@ -62,24 +64,26 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
except AttributeError:
return getcwd()
save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook. Will be removed in Notebook 5.0')
def _save_script_changed(self):
self.log.warn("""
`--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
`--script` is deprecated and will be removed in notebook 5.0.
You can trigger nbconvert via pre- or post-save hooks:
ContentsManager.pre_save_hook
FileContentsManager.post_save_hook
A post-save hook has been registered that calls:
ipython nbconvert --to script [notebook]
jupyter nbconvert --to script [notebook]
which behaves similarly to `--script`.
""")
self.post_save_hook = _post_save_script
post_save_hook = Any(None, config=True,
post_save_hook = Any(None, config=True, allow_none=True,
help="""Python callable or importstring thereof
to be called on the path of a file just saved.
@ -473,6 +477,8 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
def get_kernel_path(self, path, model=None):
"""Return the initial API path of a kernel associated with a given notebook"""
if self.dir_exists(path):
return path
if '/' in path:
parent_dir = path.rsplit('/', 1)[0]
else:

@ -98,8 +98,8 @@ class ContentsHandler(APIHandler):
self.set_header('Content-Type', 'application/json')
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def get(self, path=''):
"""Return a model for a file or directory.
@ -130,8 +130,8 @@ class ContentsHandler(APIHandler):
validate_model(model, expect_content=content)
self._finish_model(model, location=False)
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def patch(self, path=''):
"""PATCH renames a file or directory without re-uploading content."""
@ -181,8 +181,8 @@ class ContentsHandler(APIHandler):
validate_model(model, expect_content=False)
self._finish_model(model)
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def post(self, path=''):
"""Create a new file in the specified path.
@ -217,8 +217,8 @@ class ContentsHandler(APIHandler):
else:
yield self._new_untitled(path)
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def put(self, path=''):
"""Saves the file in the location specified by name and path.
@ -243,8 +243,8 @@ class ContentsHandler(APIHandler):
else:
yield gen.maybe_future(self._new_untitled(path))
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def delete(self, path=''):
"""delete a file in the given path"""
@ -257,8 +257,8 @@ class ContentsHandler(APIHandler):
class CheckpointsHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def get(self, path=''):
"""get lists checkpoints for a file"""
@ -267,8 +267,8 @@ class CheckpointsHandler(APIHandler):
data = json.dumps(checkpoints, default=date_default)
self.finish(data)
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def post(self, path=''):
"""post creates a new checkpoint"""
@ -284,8 +284,8 @@ class CheckpointsHandler(APIHandler):
class ModifyCheckpointsHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def post(self, path, checkpoint_id):
"""post restores a file from a checkpoint"""
@ -294,8 +294,8 @@ class ModifyCheckpointsHandler(APIHandler):
self.set_status(204)
self.finish()
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def delete(self, path, checkpoint_id):
"""delete clears a checkpoint for a given file"""

@ -72,7 +72,7 @@ class ContentsManager(LoggingConfigurable):
help="The base name used when creating untitled directories."
)
pre_save_hook = Any(None, config=True,
pre_save_hook = Any(None, config=True, allow_none=True,
help="""Python callable or importstring thereof
To be called on a contents model prior to save.

@ -44,12 +44,12 @@ def dirs_only(dir_model):
class API(object):
"""Wrapper for contents API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/contents', path),
response = self.request(verb,
url_path_join('api/contents', path),
data=body, params=params,
)
response.raise_for_status()
@ -209,7 +209,7 @@ class APITest(NotebookTestBase):
blob = self._blob_for_name(name)
self.make_blob(u'{}/{}.blob'.format(d, name), blob)
self.api = API(self.base_url())
self.api = API(self.request)
def tearDown(self):
for dname in (list(self.top_level_dirs) + self.hidden_dirs):

@ -16,23 +16,23 @@ from jupyter_client.jsonutil import date_default
from ipython_genutils.py3compat import cast_unicode
from notebook.utils import url_path_join, url_escape
from ...base.handlers import IPythonHandler, APIHandler, json_errors
from ...base.handlers import APIHandler, json_errors
from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
from jupyter_client import protocol_version as client_protocol_version
class MainKernelHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def get(self):
km = self.kernel_manager
kernels = yield gen.maybe_future(km.list_kernels())
self.finish(json.dumps(kernels))
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def post(self):
km = self.kernel_manager
@ -54,16 +54,16 @@ class MainKernelHandler(APIHandler):
class KernelHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
def get(self, kernel_id):
km = self.kernel_manager
km._check_kernel_id(kernel_id)
model = km.kernel_model(kernel_id)
self.finish(json.dumps(model))
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def delete(self, kernel_id):
km = self.kernel_manager
@ -74,8 +74,8 @@ class KernelHandler(APIHandler):
class KernelActionHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def post(self, kernel_id, action):
km = self.kernel_manager
@ -97,13 +97,30 @@ class KernelActionHandler(APIHandler):
class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
# class-level registry of open sessions
# allows checking for conflict on session-id,
# which is used as a zmq identity and must be unique.
_open_sessions = {}
@property
def kernel_info_timeout(self):
return self.settings.get('kernel_info_timeout', 10)
@property
def iopub_msg_rate_limit(self):
return self.settings.get('iopub_msg_rate_limit', None)
@property
def iopub_data_rate_limit(self):
return self.settings.get('iopub_data_rate_limit', None)
@property
def rate_limit_window(self):
return self.settings.get('rate_limit_window', 1.0)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
def create_stream(self):
km = self.kernel_manager
identity = self.session.bsession
@ -182,11 +199,25 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
self.kernel_id = None
self.kernel_info_channel = None
self._kernel_info_future = Future()
self._close_future = Future()
self.session_key = ''
# Rate limiting code
self._iopub_window_msg_count = 0
self._iopub_window_byte_count = 0
self._iopub_msgs_exceeded = False
self._iopub_data_exceeded = False
# Queue of (time stamp, byte count)
# Allows you to specify that the byte count should be lowered
# by a delta amount at some point in the future.
self._iopub_window_byte_queue = []
@gen.coroutine
def pre_get(self):
# authenticate first
super(ZMQChannelsHandler, self).pre_get()
# check session collision:
yield self._register_session()
# then request kernel info, waiting up to a certain time before giving up.
# We don't want to wait forever, because browsers don't take it well when
# servers never respond to websocket connection requests.
@ -210,6 +241,21 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
self.kernel_id = cast_unicode(kernel_id, 'ascii')
yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id)
@gen.coroutine
def _register_session(self):
"""Ensure we aren't creating a duplicate session.
If a previous identical session is still open, close it to avoid collisions.
This is likely due to a client reconnecting from a lost network connection,
where the socket on our side has not been cleaned up yet.
"""
self.session_key = '%s:%s' % (self.kernel_id, self.session.session)
stale_handler = self._open_sessions.get(self.session_key)
if stale_handler:
self.log.warning("Replacing stale connection: %s", self.session_key)
yield stale_handler.close()
self._open_sessions[self.session_key] = self
def open(self, kernel_id):
super(ZMQChannelsHandler, self).open()
try:
@ -244,8 +290,97 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
return
stream = self.channels[channel]
self.session.send(stream, msg)
def _on_zmq_reply(self, stream, msg_list):
idents, fed_msg_list = self.session.feed_identities(msg_list)
msg = self.session.deserialize(fed_msg_list)
parent = msg['parent_header']
def write_stderr(error_message):
self.log.warn(error_message)
msg = self.session.msg("stream",
content={"text": error_message, "name": "stderr"},
parent=parent
)
msg['channel'] = 'iopub'
self.write_message(json.dumps(msg, default=date_default))
channel = getattr(stream, 'channel', None)
msg_type = msg['header']['msg_type']
if channel == 'iopub' and msg_type not in {'status', 'comm_open', 'execute_input'}:
# Remove the counts queued for removal.
now = IOLoop.current().time()
while len(self._iopub_window_byte_queue) > 0:
queued = self._iopub_window_byte_queue[0]
if (now >= queued[0]):
self._iopub_window_byte_count -= queued[1]
self._iopub_window_msg_count -= 1
del self._iopub_window_byte_queue[0]
else:
# This part of the queue hasn't be reached yet, so we can
# abort the loop.
break
# Increment the bytes and message count
self._iopub_window_msg_count += 1
byte_count = sum([len(x) for x in msg_list])
self._iopub_window_byte_count += byte_count
# Queue a removal of the byte and message count for a time in the
# future, when we are no longer interested in it.
self._iopub_window_byte_queue.append((now + self.rate_limit_window, byte_count))
# Check the limits, set the limit flags, and reset the
# message and data counts.
msg_rate = float(self._iopub_window_msg_count) / self.rate_limit_window
data_rate = float(self._iopub_window_byte_count) / self.rate_limit_window
# Check the msg rate
if self.iopub_msg_rate_limit is not None and msg_rate > self.iopub_msg_rate_limit and self.iopub_msg_rate_limit > 0:
if not self._iopub_msgs_exceeded:
self._iopub_msgs_exceeded = True
write_stderr("""iopub message rate exceeded. The
notebook server will temporarily stop sending iopub
messages to the client in order to avoid crashing it.
To change this limit, set the config variable
`--NotebookApp.iopub_msg_rate_limit`.""")
return
else:
if self._iopub_msgs_exceeded:
self._iopub_msgs_exceeded = False
if not self._iopub_data_exceeded:
self.log.warn("iopub messages resumed")
# Check the data rate
if self.iopub_data_rate_limit is not None and data_rate > self.iopub_data_rate_limit and self.iopub_data_rate_limit > 0:
if not self._iopub_data_exceeded:
self._iopub_data_exceeded = True
write_stderr("""iopub data rate exceeded. The
notebook server will temporarily stop sending iopub
messages to the client in order to avoid crashing it.
To change this limit, set the config variable
`--NotebookApp.iopub_data_rate_limit`.""")
return
else:
if self._iopub_data_exceeded:
self._iopub_data_exceeded = False
if not self._iopub_msgs_exceeded:
self.log.warn("iopub messages resumed")
# If either of the limit flags are set, do not send the message.
if self._iopub_msgs_exceeded or self._iopub_data_exceeded:
return
super(ZMQChannelsHandler, self)._on_zmq_reply(stream, msg)
def close(self):
super(ZMQChannelsHandler, self).close()
return self._close_future
def on_close(self):
self.log.debug("Websocket closed %s", self.session_key)
# unregister myself as an open session (only if it's really me)
if self._open_sessions.get(self.session_key) is self:
self._open_sessions.pop(self.session_key)
km = self.kernel_manager
if self.kernel_id in km:
km.remove_restart_callback(
@ -266,6 +401,7 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
socket.close()
self.channels = {}
self._close_future.set_result(None)
def _send_status_message(self, status):
msg = self.session.msg("status",

@ -115,7 +115,7 @@ class MappingKernelManager(MultiKernelManager):
def finish():
"""Common cleanup when restart finishes/fails for any reason."""
if not channel.closed:
if not channel.closed():
channel.close()
loop.remove_timeout(timeout)
kernel.remove_restart_callback(on_restart_failed, 'dead')

@ -10,12 +10,12 @@ from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
class KernelAPI(object):
"""Wrapper for kernel REST API requests"""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/kernels', path), data=body)
response = self.request(verb,
url_path_join('api/kernels', path), data=body)
if 400 <= response.status_code < 600:
try:
@ -48,7 +48,7 @@ class KernelAPI(object):
class KernelAPITest(NotebookTestBase):
"""Test the kernels web service API"""
def setUp(self):
self.kern_api = KernelAPI(self.base_url())
self.kern_api = KernelAPI(self.request)
def tearDown(self):
for k in self.kern_api.list().json():

@ -14,7 +14,7 @@ pjoin = os.path.join
from tornado import web
from ...base.handlers import APIHandler, json_errors
from ...utils import url_path_join
from ...utils import url_path_join, url_unescape
def kernelspec_model(handler, name):
"""Load a KernelSpec by name and return the REST API model"""
@ -45,8 +45,8 @@ def kernelspec_model(handler, name):
class MainKernelSpecHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
def get(self):
ksm = self.kernel_spec_manager
km = self.kernel_manager
@ -66,11 +66,11 @@ class MainKernelSpecHandler(APIHandler):
class KernelSpecHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
def get(self, kernel_name):
try:
model = kernelspec_model(self, kernel_name)
model = kernelspec_model(self, url_unescape(kernel_name))
except KeyError:
raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name)
self.set_header("Content-Type", 'application/json')
@ -79,7 +79,7 @@ class KernelSpecHandler(APIHandler):
# URL to handler mappings
kernel_name_regex = r"(?P<kernel_name>[\w\.\-]+)"
kernel_name_regex = r"(?P<kernel_name>[\w\.\-%]+)"
default_handlers = [
(r"/api/kernelspecs", MainKernelSpecHandler),

@ -12,7 +12,7 @@ pjoin = os.path.join
import requests
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
from notebook.utils import url_path_join
from notebook.utils import url_path_join, url_escape
from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
# Copied from jupyter_client.tests.test_kernelspec so updating that doesn't
@ -26,12 +26,12 @@ some_resource = u"The very model of a modern major general"
class KernelSpecAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, path),
response = self.request(verb,
path,
data=body,
)
response.raise_for_status()
@ -46,10 +46,16 @@ class KernelSpecAPI(object):
def kernel_resource(self, name, path):
return self._req('GET', url_path_join('kernelspecs', name, path))
class APITest(NotebookTestBase):
"""Test the kernelspec web service API"""
def setUp(self):
sample_kernel_dir = pjoin(self.data_dir.name, 'kernels', 'sample')
self.create_spec('sample')
self.create_spec('sample 2')
self.ks_api = KernelSpecAPI(self.request)
def create_spec(self, name):
sample_kernel_dir = pjoin(self.data_dir.name, 'kernels', name)
try:
os.makedirs(sample_kernel_dir)
except OSError as e:
@ -63,8 +69,6 @@ class APITest(NotebookTestBase):
encoding='utf-8') as f:
f.write(some_resource)
self.ks_api = KernelSpecAPI(self.base_url())
def test_list_kernelspecs_bad(self):
"""Can list kernelspecs when one is invalid"""
bad_kernel_dir = pjoin(self.data_dir.name, 'kernels', 'bad')
@ -113,6 +117,10 @@ class APITest(NotebookTestBase):
self.assertEqual(model['spec']['display_name'], 'Test kernel')
self.assertIsInstance(model['resources'], dict)
def test_get_kernelspec_spaces(self):
model = self.ks_api.kernel_spec_info('sample%202').json()
self.assertEqual(model['name'].lower(), 'sample 2')
def test_get_nonexistant_kernelspec(self):
with assert_http_error(404):
self.ks_api.kernel_spec_info('nonexistant')

@ -6,8 +6,8 @@ from ...base.handlers import APIHandler, json_errors
class NbconvertRootHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
def get(self):
try:
from nbconvert.exporters.export import exporter_map

@ -5,12 +5,12 @@ from notebook.tests.launchnotebook import NotebookTestBase
class NbconvertAPI(object):
"""Wrapper for nbconvert API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/nbconvert', path),
response = self.request(verb,
url_path_join('api/nbconvert', path),
data=body, params=params,
)
response.raise_for_status()
@ -21,7 +21,7 @@ class NbconvertAPI(object):
class APITest(NotebookTestBase):
def setUp(self):
self.nbconvert_api = NbconvertAPI(self.base_url())
self.nbconvert_api = NbconvertAPI(self.request)
def test_list_formats(self):
formats = self.nbconvert_api.list_formats().json()

@ -3,19 +3,23 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from tornado import gen, web
from tornado import web
from ...base.handlers import APIHandler, json_errors
from . import csp_report_uri
class CSPReportHandler(APIHandler):
'''Accepts a content security policy violation report'''
@web.authenticated
def skip_origin_check(self):
"""Don't check origin when reporting origin-check violations!"""
return True
@json_errors
@web.authenticated
def post(self):
'''Log a content security policy violation report'''
csp_report = self.get_json_body()
self.log.warn("Content security violation: %s",
self.log.warning("Content security violation: %s",
self.request.body.decode('utf8', 'replace'))
default_handlers = [

@ -7,6 +7,7 @@ Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-16%3A-
# Distributed under the terms of the Modified BSD License.
import json
import os
from tornado import gen, web
@ -18,8 +19,8 @@ from jupyter_client.kernelspec import NoSuchKernel
class SessionRootHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def get(self):
# Return a list of running sessions
@ -27,35 +28,51 @@ class SessionRootHandler(APIHandler):
sessions = yield gen.maybe_future(sm.list_sessions())
self.finish(json.dumps(sessions, default=date_default))
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def post(self):
# Creates a new session
#(unless a session already exists for the named nb)
#(unless a session already exists for the named session)
sm = self.session_manager
model = self.get_json_body()
if model is None:
raise web.HTTPError(400, "No JSON data provided")
if 'notebook' in model and 'path' in model['notebook']:
self.log.warn('Sessions API changed, see updated swagger docs')
model['path'] = model['notebook']['path']
model['type'] = 'notebook'
try:
path = model['notebook']['path']
path = model['path']
except KeyError:
raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
raise web.HTTPError(400, "Missing field in JSON data: path")
try:
kernel_name = model['kernel']['name']
mtype = model['type']
except KeyError:
self.log.debug("No kernel name specified, using default kernel")
raise web.HTTPError(400, "Missing field in JSON data: type")
name = model.get('name', None)
kernel = model.get('kernel', {})
kernel_name = kernel.get('name', None)
kernel_id = kernel.get('id', None)
if not kernel_id and not kernel_name:
self.log.debug("No kernel specified, using default kernel")
kernel_name = None
# Check to see if session exists
exists = yield gen.maybe_future(sm.session_exists(path=path))
if exists:
model = yield gen.maybe_future(sm.get_session(path=path))
else:
try:
model = yield gen.maybe_future(
sm.create_session(path=path, kernel_name=kernel_name))
sm.create_session(path=path, kernel_name=kernel_name,
kernel_id=kernel_id, name=name,
type=mtype))
except NoSuchKernel:
msg = ("The '%s' kernel is not available. Please pick another "
"suitable kernel instead, or install that kernel." % kernel_name)
@ -73,8 +90,8 @@ class SessionRootHandler(APIHandler):
class SessionHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def get(self, session_id):
# Returns the JSON model for a single session
@ -82,27 +99,62 @@ class SessionHandler(APIHandler):
model = yield gen.maybe_future(sm.get_session(session_id=session_id))
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def patch(self, session_id):
# Currently, this handler is strictly for renaming notebooks
"""Patch updates sessions:
- path updates session to track renamed paths
- kernel.name starts a new kernel with a given kernelspec
"""
sm = self.session_manager
km = self.kernel_manager
model = self.get_json_body()
if model is None:
raise web.HTTPError(400, "No JSON data provided")
# get the previous session model
before = yield gen.maybe_future(sm.get_session(session_id=session_id))
changes = {}
if 'notebook' in model:
notebook = model['notebook']
if 'path' in notebook:
changes['path'] = notebook['path']
if 'notebook' in model and 'path' in model['notebook']:
self.log.warn('Sessions API changed, see updated swagger docs')
model['path'] = model['notebook']['path']
model['type'] = 'notebook'
if 'path' in model:
changes['path'] = model['path']
if 'name' in model:
changes['name'] = model['name']
if 'type' in model:
changes['type'] = model['type']
if 'kernel' in model:
# Kernel id takes precedence over name.
if model['kernel'].get('id') is not None:
kernel_id = model['kernel']['id']
if kernel_id not in km:
raise web.HTTPError(400, "No such kernel: %s" % kernel_id)
changes['kernel_id'] = kernel_id
elif model['kernel'].get('name') is not None:
kernel_name = model['kernel']['name']
kernel_id = yield sm.start_kernel_for_session(
session_id, kernel_name=kernel_name, name=before['name'],
path=before['path'], type=before['type'])
changes['kernel_id'] = kernel_id
yield gen.maybe_future(sm.update_session(session_id, **changes))
model = yield gen.maybe_future(sm.get_session(session_id=session_id))
if model['kernel']['id'] != before['kernel']['id']:
# kernel_id changed because we got a new kernel
# shutdown the old one
yield gen.maybe_future(
km.shutdown_kernel(before['kernel']['id'])
)
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def delete(self, session_id):
# Deletes the session with given session_id

@ -3,8 +3,14 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import uuid
import sqlite3
try:
import sqlite3
except ImportError:
# fallback on pysqlite2 if Python was build without sqlite
from pysqlite2 import dbapi2 as sqlite3
from tornado import gen, web
@ -21,7 +27,7 @@ class SessionManager(LoggingConfigurable):
# Session database initialized below
_cursor = None
_connection = None
_columns = {'session_id', 'path', 'kernel_id'}
_columns = {'session_id', 'path', 'name', 'type', 'kernel_id'}
@property
def cursor(self):
@ -29,7 +35,7 @@ class SessionManager(LoggingConfigurable):
if self._cursor is None:
self._cursor = self.connection.cursor()
self._cursor.execute("""CREATE TABLE session
(session_id, path, kernel_id)""")
(session_id, path, name, type, kernel_id)""")
return self._cursor
@property
@ -51,7 +57,7 @@ class SessionManager(LoggingConfigurable):
self.close()
def session_exists(self, path):
"""Check to see if the session for a given notebook exists"""
"""Check to see if the session of a given name exists"""
self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
reply = self.cursor.fetchone()
if reply is None:
@ -62,23 +68,33 @@ class SessionManager(LoggingConfigurable):
def new_session_id(self):
"Create a uuid for a new session"
return unicode_type(uuid.uuid4())
@gen.coroutine
def create_session(self, path=None, kernel_name=None):
def create_session(self, path=None, name=None, type=None, kernel_name=None, kernel_id=None):
"""Creates a session and returns its model"""
session_id = self.new_session_id()
# allow nbm to specify kernels cwd
if kernel_id is not None and kernel_id in self.kernel_manager:
pass
else:
kernel_id = yield self.start_kernel_for_session(session_id, path, name, type, kernel_name)
result = yield gen.maybe_future(
self.save_session(session_id, path=path, name=name, type=type, kernel_id=kernel_id)
)
# py2-compat
raise gen.Return(result)
@gen.coroutine
def start_kernel_for_session(self, session_id, path, name, type, kernel_name):
"""Start a new kernel for a given session."""
# allow contents manager to specify kernels cwd
kernel_path = self.contents_manager.get_kernel_path(path=path)
kernel_id = yield gen.maybe_future(
self.kernel_manager.start_kernel(path=kernel_path, kernel_name=kernel_name)
)
result = yield gen.maybe_future(
self.save_session(session_id, path=path, kernel_id=kernel_id)
)
# py2-compat
raise gen.Return(result)
def save_session(self, session_id, path=None, kernel_id=None):
raise gen.Return(kernel_id)
def save_session(self, session_id, path=None, name=None, type=None, kernel_id=None):
"""Saves the items for the session with the given session_id
Given a session_id (and any other of the arguments), this method
@ -90,7 +106,11 @@ class SessionManager(LoggingConfigurable):
session_id : str
uuid for the session; this method must be given a session_id
path : str
the path for the given notebook
the path for the given session
name: str
the name of the session
type: string
the type of the session
kernel_id : str
a uuid for the kernel associated with this session
@ -99,8 +119,8 @@ class SessionManager(LoggingConfigurable):
model : dict
a dictionary of the session model
"""
self.cursor.execute("INSERT INTO session VALUES (?,?,?)",
(session_id, path, kernel_id)
self.cursor.execute("INSERT INTO session VALUES (?,?,?,?,?)",
(session_id, path, name, type, kernel_id)
)
return self.get_session(session_id=session_id)
@ -114,7 +134,7 @@ class SessionManager(LoggingConfigurable):
----------
**kwargs : keyword argument
must be given one of the keywords and values from the session database
(i.e. session_id, path, kernel_id)
(i.e. session_id, path, name, type, kernel_id)
Returns
-------
@ -190,11 +210,14 @@ class SessionManager(LoggingConfigurable):
model = {
'id': row['session_id'],
'notebook': {
'path': row['path']
},
'path': row['path'],
'name': row['name'],
'type': row['type'],
'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
}
if row['type'] == 'notebook':
# Provide the deprecated API.
model['notebook'] = {'path': row['path'], 'name': row['name']}
return model
def list_sessions(self):
@ -211,9 +234,9 @@ class SessionManager(LoggingConfigurable):
pass
return result
@gen.coroutine
def delete_session(self, session_id):
"""Deletes the row in the session database with given session_id"""
# Check that session exists before deleting
session = self.get_session(session_id=session_id)
self.kernel_manager.shutdown_kernel(session['kernel']['id'])
yield gen.maybe_future(self.kernel_manager.shutdown_kernel(session['kernel']['id']))
self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))

@ -48,6 +48,7 @@ class TestSessionManager(TestCase):
def co_add():
sessions = []
for kwargs in kwarg_list:
kwargs.setdefault('type', 'notebook')
session = yield self.sm.create_session(**kwargs)
sessions.append(session)
raise gen.Return(sessions)
@ -61,7 +62,10 @@ class TestSessionManager(TestCase):
session_id = self.create_session(path='/path/to/test.ipynb', kernel_name='bar')['id']
model = sm.get_session(session_id=session_id)
expected = {'id':session_id,
'notebook':{'path': u'/path/to/test.ipynb'},
'path': u'/path/to/test.ipynb',
'notebook': {'path': u'/path/to/test.ipynb', 'name': None},
'type': 'notebook',
'name': None,
'kernel': {'id':u'A', 'name': 'bar'}}
self.assertEqual(model, expected)
@ -87,23 +91,30 @@ class TestSessionManager(TestCase):
sm = self.sm
sessions = self.create_sessions(
dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
dict(path='/path/to/3/test3.ipynb', kernel_name='python'),
dict(path='/path/to/2/test2.py', type='file', kernel_name='python'),
dict(path='/path/to/3', name='foo', type='console', kernel_name='python'),
)
sessions = sm.list_sessions()
expected = [
{
'id':sessions[0]['id'],
'notebook':{'path': u'/path/to/1/test1.ipynb'},
'path': u'/path/to/1/test1.ipynb',
'type': 'notebook',
'notebook': {'path': u'/path/to/1/test1.ipynb', 'name': None},
'name': None,
'kernel':{'id':u'A', 'name':'python'}
}, {
'id':sessions[1]['id'],
'notebook': {'path': u'/path/to/2/test2.ipynb'},
'path': u'/path/to/2/test2.py',
'type': 'file',
'name': None,
'kernel':{'id':u'B', 'name':'python'}
}, {
'id':sessions[2]['id'],
'notebook':{'path': u'/path/to/3/test3.ipynb'},
'path': u'/path/to/3',
'type': 'console',
'name': 'foo',
'kernel':{'id':u'C', 'name':'python'}
}
]
@ -121,9 +132,10 @@ class TestSessionManager(TestCase):
expected = [
{
'id': sessions[1]['id'],
'notebook': {
'path': u'/path/to/2/test2.ipynb',
},
'path': u'/path/to/2/test2.ipynb',
'type': 'notebook',
'name': None,
'notebook': {'path': u'/path/to/2/test2.ipynb', 'name': None},
'kernel': {
'id': u'B',
'name':'python',
@ -139,7 +151,10 @@ class TestSessionManager(TestCase):
sm.update_session(session_id, path='/path/to/new_name.ipynb')
model = sm.get_session(session_id=session_id)
expected = {'id':session_id,
'notebook':{'path': u'/path/to/new_name.ipynb'},
'path': u'/path/to/new_name.ipynb',
'type': 'notebook',
'name': None,
'notebook': {'path': u'/path/to/new_name.ipynb', 'name': None},
'kernel':{'id':u'A', 'name':'julia'}}
self.assertEqual(model, expected)
@ -155,17 +170,22 @@ class TestSessionManager(TestCase):
sessions = self.create_sessions(
dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
dict(path='/path/to/3/test3.ipynb', kernel_name='python'),
dict(path='/path/to/3', name='foo', type='console', kernel_name='python'),
)
sm.delete_session(sessions[1]['id'])
new_sessions = sm.list_sessions()
expected = [{
'id': sessions[0]['id'],
'notebook': {'path': u'/path/to/1/test1.ipynb'},
'path': u'/path/to/1/test1.ipynb',
'type': 'notebook',
'name': None,
'notebook': {'path': u'/path/to/1/test1.ipynb', 'name': None},
'kernel': {'id':u'A', 'name':'python'}
}, {
'id': sessions[2]['id'],
'notebook': {'path': u'/path/to/3/test3.ipynb'},
'type': 'console',
'path': u'/path/to/3',
'name': 'foo',
'kernel': {'id':u'C', 'name':'python'}
}
]
@ -175,6 +195,8 @@ class TestSessionManager(TestCase):
# try to delete a session that doesn't exist ~ raise error
sm = self.sm
self.create_session(path='/path/to/test.ipynb', kernel_name='python')
self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
with self.assertRaises(TypeError):
self.loop.run_sync(lambda : sm.delete_session(bad_kwarg='23424')) # Bad keyword
with self.assertRaises(web.HTTPError):
self.loop.run_sync(lambda : sm.delete_session(session_id='23424')) # nonexistent

@ -17,12 +17,12 @@ from nbformat import write
class SessionAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/sessions', path), data=body)
response = self.request(verb,
url_path_join('api/sessions', path), data=body)
if 400 <= response.status_code < 600:
try:
@ -39,13 +39,38 @@ class SessionAPI(object):
def get(self, id):
return self._req('GET', id)
def create(self, path, kernel_name='python'):
body = json.dumps({'notebook': {'path':path},
'kernel': {'name': kernel_name}})
def create(self, path, type='notebook', kernel_name='python', kernel_id=None):
body = json.dumps({'path': path,
'type': type,
'kernel': {'name': kernel_name,
'id': kernel_id}})
return self._req('POST', '', body)
def modify(self, id, path):
body = json.dumps({'notebook': {'path':path}})
def create_deprecated(self, path):
body = json.dumps({'notebook': {'path': path},
'kernel': {'name': 'python',
'id': 'foo'}})
return self._req('POST', '', body)
def modify_path(self, id, path):
body = json.dumps({'path': path})
return self._req('PATCH', id, body)
def modify_path_deprecated(self, id, path):
body = json.dumps({'notebook': {'path': path}})
return self._req('PATCH', id, body)
def modify_type(self, id, type):
body = json.dumps({'type': type})
return self._req('PATCH', id, body)
def modify_kernel_name(self, id, kernel_name):
body = json.dumps({'kernel': {'name': kernel_name}})
return self._req('PATCH', id, body)
def modify_kernel_id(self, id, kernel_id):
# Also send a dummy name to show that id takes precedence.
body = json.dumps({'kernel': {'id': kernel_id, 'name': 'foo'}})
return self._req('PATCH', id, body)
def delete(self, id):
@ -67,7 +92,7 @@ class SessionAPITest(NotebookTestBase):
nb = new_notebook()
write(nb, f, version=4)
self.sess_api = SessionAPI(self.base_url())
self.sess_api = SessionAPI(self.request)
def tearDown(self):
for session in self.sess_api.list().json():
@ -91,7 +116,52 @@ class SessionAPITest(NotebookTestBase):
self.assertEqual(resp.status_code, 201)
newsession = resp.json()
self.assertIn('id', newsession)
self.assertEqual(newsession['path'], 'foo/nb1.ipynb')
self.assertEqual(newsession['type'], 'notebook')
self.assertEqual(resp.headers['Location'], self.url_prefix + 'api/sessions/{0}'.format(newsession['id']))
sessions = self.sess_api.list().json()
self.assertEqual(sessions, [newsession])
# Retrieve it
sid = newsession['id']
got = self.sess_api.get(sid).json()
self.assertEqual(got, newsession)
def test_create_file_session(self):
resp = self.sess_api.create('foo/nb1.py', type='file')
self.assertEqual(resp.status_code, 201)
newsession = resp.json()
self.assertEqual(newsession['path'], 'foo/nb1.py')
self.assertEqual(newsession['type'], 'file')
def test_create_console_session(self):
resp = self.sess_api.create('foo/abc123', type='console')
self.assertEqual(resp.status_code, 201)
newsession = resp.json()
self.assertEqual(newsession['path'], 'foo/abc123')
self.assertEqual(newsession['type'], 'console')
def test_create_deprecated(self):
resp = self.sess_api.create_deprecated('foo/nb1.ipynb')
self.assertEqual(resp.status_code, 201)
newsession = resp.json()
self.assertEqual(newsession['path'], 'foo/nb1.ipynb')
self.assertEqual(newsession['type'], 'notebook')
self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
def test_create_with_kernel_id(self):
# create a new kernel
r = self.request('POST', 'api/kernels')
r.raise_for_status()
kernel = r.json()
resp = self.sess_api.create('foo/nb1.ipynb', kernel_id=kernel['id'])
self.assertEqual(resp.status_code, 201)
newsession = resp.json()
self.assertIn('id', newsession)
self.assertEqual(newsession['path'], 'foo/nb1.ipynb')
self.assertEqual(newsession['kernel']['id'], kernel['id'])
self.assertEqual(resp.headers['Location'], self.url_prefix + 'api/sessions/{0}'.format(newsession['id']))
sessions = self.sess_api.list().json()
@ -115,10 +185,65 @@ class SessionAPITest(NotebookTestBase):
with assert_http_error(404):
self.sess_api.get(sid)
def test_modify(self):
def test_modify_path(self):
newsession = self.sess_api.create('foo/nb1.ipynb').json()
sid = newsession['id']
changed = self.sess_api.modify(sid, 'nb2.ipynb').json()
changed = self.sess_api.modify_path(sid, 'nb2.ipynb').json()
self.assertEqual(changed['id'], sid)
self.assertEqual(changed['path'], 'nb2.ipynb')
def test_modify_path_deprecated(self):
newsession = self.sess_api.create('foo/nb1.ipynb').json()
sid = newsession['id']
changed = self.sess_api.modify_path_deprecated(sid, 'nb2.ipynb').json()
self.assertEqual(changed['id'], sid)
self.assertEqual(changed['notebook']['path'], 'nb2.ipynb')
def test_modify_type(self):
newsession = self.sess_api.create('foo/nb1.ipynb').json()
sid = newsession['id']
changed = self.sess_api.modify_type(sid, 'console').json()
self.assertEqual(changed['id'], sid)
self.assertEqual(changed['type'], 'console')
def test_modify_kernel_name(self):
before = self.sess_api.create('foo/nb1.ipynb').json()
sid = before['id']
after = self.sess_api.modify_kernel_name(sid, before['kernel']['name']).json()
self.assertEqual(after['id'], sid)
self.assertEqual(after['path'], before['path'])
self.assertEqual(after['type'], before['type'])
self.assertNotEqual(after['kernel']['id'], before['kernel']['id'])
# check kernel list, to be sure previous kernel was cleaned up
r = self.request('GET', 'api/kernels')
r.raise_for_status()
kernel_list = r.json()
self.assertEqual(kernel_list, [after['kernel']])
def test_modify_kernel_id(self):
before = self.sess_api.create('foo/nb1.ipynb').json()
sid = before['id']
# create a new kernel
r = self.request('POST', 'api/kernels')
r.raise_for_status()
kernel = r.json()
# Attach our session to the existing kernel
after = self.sess_api.modify_kernel_id(sid, kernel['id']).json()
self.assertEqual(after['id'], sid)
self.assertEqual(after['path'], before['path'])
self.assertEqual(after['type'], before['type'])
self.assertNotEqual(after['kernel']['id'], before['kernel']['id'])
self.assertEqual(after['kernel']['id'], kernel['id'])
# check kernel list, to be sure previous kernel was cleaned up
r = self.request('GET', 'api/kernels')
r.raise_for_status()
kernel_list = r.json()
self.assertEqual(kernel_list, [kernel])

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

@ -20,5 +20,16 @@ define(['base/js/namespace', 'jquery'], function(IPython, $) {
IPython.Events = Events;
IPython.events = events;
return $([events]);
var events = $([events]);
// catch and log errors in triggered events
events._original_trigger = events.trigger;
events.trigger = function (name, data) {
try {
this._original_trigger.apply(this, arguments);
} catch (e) {
console.error("Exception in event handler for " + name, e, arguments);
}
}
return events;
});

@ -73,7 +73,7 @@ define(function(){
// tree
jglobal('SessionList','tree/js/sessionlist');
Jupyter.version = "4.2.0.dev";
Jupyter.version = "4.4.1";
Jupyter._target = '_blank';
return Jupyter;
});

@ -53,9 +53,19 @@ define([
this._resize_site();
};
Page.prototype._resize_site = function() {
// Update the site's size.
$('div#site').height($(window).height() - $('#header').height());
Page.prototype._resize_site = function(e) {
/**
* Update the site's size.
*/
// In the case an event is passed in, only trigger if the event does
// *not* have a target DOM node (i.e., it is not bubbling up). See
// https://bugs.jquery.com/ticket/9841#comment:8
if (!(e && e.target && e.target.tagName)) {
$('div#site').height($(window).height() - $('#header').height());
}
};
return {'Page': Page};

@ -13,6 +13,16 @@ define([
// keep track of which extensions have been loaded already
var extensions_loaded = [];
/**
* Whether or not an extension has been loaded
* @param {string} extension - name of the extension
* @return {boolean} true if loaded already
*/
var is_loaded = function(extension) {
var ext_path = "nbextensions/" + extension;
return extensions_loaded.indexOf(ext_path) >= 0;
};
/**
* Load a single extension.
* @param {string} extension - extension path.
@ -21,17 +31,17 @@ define([
var load_extension = function (extension) {
return new Promise(function(resolve, reject) {
var ext_path = "nbextensions/" + extension;
require([ext_path], function(module) {
try {
if (extensions_loaded.indexOf(ext_path) < 0) {
console.log("Loading extension: " + extension);
module.load_ipython_extension();
extensions_loaded.push(ext_path);
requirejs([ext_path], function(module) {
if (!is_loaded(extension)) {
console.log("Loading extension: " + extension);
if (module.load_ipython_extension) {
Promise.resolve(module.load_ipython_extension()).then(function() {
resolve(module);
}).catch(reject);
}
else{
console.log("Loaded extension already: " + extension);
}
} finally {
extensions_loaded.push(ext_path);
} else {
console.log("Loaded extension already: " + extension);
resolve(module);
}
}, function(err) {
@ -46,23 +56,38 @@ define([
* @return {Promise} that resolves to a list of loaded module handles.
*/
var load_extensions = function () {
console.log("load_extensions", arguments);
return Promise.all(Array.prototype.map.call(arguments, load_extension)).catch(function(err) {
console.error("Failed to load extension" + (err.requireModules.length>1?'s':'') + ":", err.requireModules, err);
});
};
/**
* Return a list of extensions that should be active
* The config for nbextensions comes in as a dict where keys are
* nbextensions paths and the values are a bool indicating if it
* should be active. This returns a list of nbextension paths
* where the value is true
*/
function filter_extensions(nbext_config) {
var active = [];
Object.keys(nbext_config).forEach(function (nbext) {
if (nbext_config[nbext]) {active.push(nbext);}
});
return active;
}
/**
* Wait for a config section to load, and then load the extensions specified
* in a 'load_extensions' key inside it.
*/
function load_extensions_from_config(section) {
section.loaded.then(function() {
return section.loaded.then(function() {
if (section.data.load_extensions) {
var nbextension_paths = Object.getOwnPropertyNames(
section.data.load_extensions);
load_extensions.apply(this, nbextension_paths);
var active = filter_extensions(section.data.load_extensions);
return load_extensions.apply(this, active);
}
});
}).catch(utils.reject('Could not load nbextensions from ' + section.section_name + ' config file'));
}
//============================================================================
@ -359,12 +384,13 @@ define([
// Remove chunks that should be overridden by the effect of
// carriage return characters
function fixCarriageReturn(txt) {
var tmp = txt;
do {
txt = tmp;
tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
} while (tmp.length < txt.length);
txt = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
while (txt.search(/\r[^$]/g) > -1) {
var base = txt.match(/^(.*)\r+/m)[1];
var insert = txt.match(/\r+(.*)$/m)[1];
insert = insert + base.slice(insert.length, base.length);
txt = txt.replace(/\r+.*$/m, '\r').replace(/^.*\r/m, insert);
}
return txt;
}
@ -501,7 +527,7 @@ define([
var to_absolute_cursor_pos = function (cm, cursor) {
console.warn('`utils.to_absolute_cursor_pos(cm, pos)` is deprecated. Use `cm.indexFromPos(cursor)`');
return cm.indexFromPos(cusrsor);
return cm.indexFromPos(cursor);
};
var from_absolute_cursor_pos = function (cm, cursor_pos) {
@ -650,6 +676,35 @@ define([
return wrapped_error;
};
var ajax = function (url, settings) {
// like $.ajax, but ensure Authorization header is set
settings = _add_auth_header(settings);
return $.ajax(url, settings);
};
var _get_cookie = function (name) {
// from tornado docs: http://www.tornadoweb.org/en/stable/guide/security.html
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var _add_auth_header = function (settings) {
/**
* Adds auth header to jquery ajax settings
*/
settings = settings || {};
if (!settings.headers) {
settings.headers = {};
}
if (!settings.headers.Authorization) {
var xsrf_token = _get_cookie('_xsrf');
if (xsrf_token) {
settings.headers['X-XSRFToken'] = xsrf_token;
}
}
return settings;
};
var promising_ajax = function(url, settings) {
/**
* Like $.ajax, but returning an ES6 promise. success and error settings
@ -664,7 +719,7 @@ define([
log_ajax_error(jqXHR, status, error);
reject(wrap_ajax_error(jqXHR, status, error));
};
$.ajax(url, settings);
ajax(url, settings);
});
};
@ -826,8 +881,10 @@ define([
};
var utils = {
is_loaded: is_loaded,
load_extension: load_extension,
load_extensions: load_extensions,
filter_extensions: filter_extensions,
load_extensions_from_config: load_extensions_from_config,
regex_split : regex_split,
uuid : uuid,
@ -852,10 +909,11 @@ define([
is_or_has : is_or_has,
is_focused : is_focused,
mergeopt: mergeopt,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
requireCodeMirrorMode : requireCodeMirrorMode,
XHR_ERROR : XHR_ERROR,
ajax : ajax,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
wrap_ajax_error : wrap_ajax_error,
promising_ajax : promising_ajax,
WrappedError: WrappedError,

@ -63,6 +63,9 @@ body > #header {
}
[dir="rtl"] #ipython_notebook {
float: right !important;
}
#noscript {
width: auto;

@ -116,7 +116,8 @@ function($,
if (ext_idx > 0) {
// CodeMirror.findModeByExtension wants extension without '.'
modeinfo = CodeMirror.findModeByExtension(model.name.slice(ext_idx + 1));
modeinfo = CodeMirror.findModeByExtension(
model.name.slice(ext_idx + 1).toLowerCase());
}
}
if (modeinfo) {

@ -13,7 +13,7 @@ require([
'edit/js/menubar',
'edit/js/savewidget',
'edit/js/notificationarea',
'custom/custom',
'custom',
], function(
$,
IPython,
@ -28,6 +28,7 @@ require([
notificationarea
){
"use strict";
page = new page.Page();
var base_url = utils.get_body_data('baseUrl');

@ -114,6 +114,11 @@ define([
});
// View
this.element.find('#toggle_header').click(function (){
$("#header-container").toggle();
});
this.element.find('#menu-line-numbers').click(function () {
var current = editor.codemirror.getOption('lineNumbers');
var value = Boolean(1-current);

@ -9,19 +9,27 @@ require([
'use strict';
$('#notebook_about').click(function () {
// use underscore template to auto html escape
var text = 'You are using Jupyter notebook.<br/><br/>';
text = text + 'The version of the notebook server is ';
text = text + _.template('<b><%- version %></b>')({ version: sys_info.notebook_version });
if (sys_info.commit_hash) {
text = text + _.template('-<%- hash %>')({ hash: sys_info.commit_hash });
if (sys_info) {
var text = 'You are using Jupyter notebook.<br/><br/>';
text = text + 'The version of the notebook server is ';
text = text + _.template('<b><%- version %></b>')({ version: sys_info.notebook_version });
if (sys_info.commit_hash) {
text = text + _.template('-<%- hash %>')({ hash: sys_info.commit_hash });
}
text = text + _.template(' and is running on:<br/><pre>Python <%- pyver %></pre>')({
pyver: sys_info.sys_version });
var kinfo = $('<div/>').attr('id', '#about-kinfo').text('Waiting for kernel to be available...');
var body = $('<div/>');
body.append($('<h4/>').text('Server Information:'));
body.append($('<p/>').html(text));
body.append($('<h4/>').text('Current Kernel Information:'));
body.append(kinfo);
} else {
var text = 'Could not access sys_info variable for version information.';
var body = $('<div/>');
body.append($('<h4/>').text('Cannot find sys_info!'));
body.append($('<p/>').html(text));
}
text = text + _.template(' and is running on:<br/><pre>Python <%- pyver %></pre>')({ pyver: sys_info.sys_version });
var kinfo = $('<div/>').attr('id', '#about-kinfo').text('Waiting for kernel to be available...');
var body = $('<div/>');
body.append($('<h4/>').text('Server Information:'));
body.append($('<p/>').html(text));
body.append($('<h4/>').text('Current Kernel Information:'));
body.append(kinfo);
dialog.modal({
title: 'About Jupyter Notebook',
body: body,

@ -212,6 +212,7 @@ define(function(require){
}
},
'cut-cell' : {
help: 'cut selected cells',
icon: 'fa-cut',
help_index : 'ee',
handler : function (env) {
@ -221,6 +222,7 @@ define(function(require){
}
},
'copy-cell' : {
help: 'copy selected cells',
icon: 'fa-copy',
help_index : 'ef',
handler : function (env) {
@ -228,14 +230,14 @@ define(function(require){
}
},
'paste-cell-above' : {
help: 'paste cell above',
help: 'paste cells above',
help_index : 'eg',
handler : function (env) {
env.notebook.paste_cell_above();
}
},
'paste-cell-below' : {
help: 'paste cell below',
help: 'paste cells below',
icon: 'fa-paste',
help_index : 'eh',
handler : function (env) {
@ -345,6 +347,7 @@ define(function(require){
}
},
'move-cell-down' : {
help: 'move selected cells down',
icon: 'fa-arrow-down',
help_index : 'eb',
handler : function (env) {
@ -352,6 +355,7 @@ define(function(require){
}
},
'move-cell-up' : {
help: 'move selected cells up',
icon: 'fa-arrow-up',
help_index : 'ea',
handler : function (env) {
@ -372,7 +376,7 @@ define(function(require){
}
},
'delete-cell': {
help: 'delete selected cell',
help: 'delete selected cells',
help_index : 'ej',
handler : function (env) {
env.notebook.delete_cell();
@ -545,9 +549,6 @@ define(function(require){
'duplicate-notebook':{
help: "Create an open a copy of current notebook",
handler : function (env, event) {
if (env.notebook.dirty) {
env.notebook.save_notebook({async : false});
}
env.notebook.copy_notebook();
}
},

@ -479,11 +479,20 @@ define([
if (data.metadata !== undefined) {
this.metadata = data.metadata;
}
// upgrade cell's editable metadata if not defined
if (this.metadata.editable === undefined) {
this.metadata.editable = this.is_editable();
}
// upgrade cell's deletable metadata if not defined
if (this.metadata.deletable === undefined) {
this.metadata.deletable = this.is_deletable();
}
};
/**
* can the cell be split into two cells (false if not deletable)
*
* @method is_splittable
**/
Cell.prototype.is_splittable = function () {
@ -500,14 +509,28 @@ define([
};
/**
* is the cell deletable? only false (undeletable) if
* metadata.deletable is explicitly false -- everything else
* is the cell edtitable? only false (readonly) if
* metadata.editable is explicitly false -- everything else
* counts as true
*
* @method is_editable
**/
Cell.prototype.is_editable = function () {
if (this.metadata.editable === false) {
return false;
}
return true;
};
/**
* is the cell deletable? only false (undeletable) if
* metadata.deletable is explicitly false or if the cell is not
* editable -- everything else counts as true
*
* @method is_deletable
**/
Cell.prototype.is_deletable = function () {
if (this.metadata.deletable === false) {
if (this.metadata.deletable === false || !this.is_editable()) {
return false;
}
return true;

@ -177,6 +177,8 @@ define([
if (that.keyboard_manager) {
that.keyboard_manager.enable();
}
that.code_mirror.setOption('readOnly', !that.is_editable());
});
this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this));
$(this.code_mirror.getInputField()).attr("spellcheck", "false");
@ -305,24 +307,21 @@ define([
* @method execute
*/
CodeCell.prototype.execute = function (stop_on_error) {
if (!this.kernel || !this.kernel.is_connected()) {
console.log("Can't execute, kernel is not connected.");
if (!this.kernel) {
console.log("Can't execute cell since kernel is not set.");
return;
}
this.output_area.clear_output(false, true);
if (stop_on_error === undefined) {
stop_on_error = true;
}
this.output_area.clear_output(false, true);
var old_msg_id = this.last_msg_id;
if (old_msg_id) {
this.kernel.clear_callbacks_for_msg(old_msg_id);
if (old_msg_id) {
delete CodeCell.msg_cells[old_msg_id];
}
delete CodeCell.msg_cells[old_msg_id];
this.last_msg_id = null;
}
if (this.get_text().trim().length === 0) {
// nothing to do
@ -356,9 +355,11 @@ define([
},
iopub : {
output : function() {
that.events.trigger('set_dirty.Notebook', {value: true});
that.output_area.handle_output.apply(that.output_area, arguments);
},
clear_output : function() {
that.events.trigger('set_dirty.Notebook', {value: true});
that.output_area.handle_clear_output.apply(that.output_area, arguments);
},
},
@ -483,6 +484,7 @@ define([
var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
// This HTML call is okay because the user contents are escaped.
this.element.find('div.input_prompt').html(prompt_html);
this.events.trigger('set_dirty.Notebook', {value: true});
};

@ -25,7 +25,7 @@
}
}
pythonConf.name = 'python';
pythonConf.singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\\?]");
pythonConf.singleOperators = new RegExp("^[\\+\\-\\*/%&|@\\^~<>!\\?]");
if (pythonConf.version === 3) {
pythonConf.identifiers = new RegExp("^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*");
} else if (pythonConf.version === 2) {

@ -6,7 +6,8 @@ define([
'base/js/namespace',
'base/js/dialog',
'base/js/utils',
], function($, IPython, dialog, utils) {
'require',
], function($, IPython, dialog, utils, require) {
"use strict";
var KernelSelector = function(selector, notebook) {
@ -147,6 +148,14 @@ define([
// load kernel js
if (ks.resources['kernel.js']) {
// Debug added for Notebook 4.2, please remove at some point in the
// future if the following does not append anymore when kernels
// have kernel.js
//
// > Uncaught (in promise) TypeError: require is not a function
//
console.info('Dynamically requiring kernel.js, `require` is ', require);
require([ks.resources['kernel.js']],
function (kernel_mod) {
if (kernel_mod && kernel_mod.onload) {

@ -79,7 +79,6 @@ define([
'up' : 'jupyter-notebook:move-cursor-up',
'down' : 'jupyter-notebook:move-cursor-down',
'ctrl-shift--' : 'jupyter-notebook:split-cell-at-cursor',
'ctrl-shift-subtract' : 'jupyter-notebook:split-cell-at-cursor',
};
};

@ -24,8 +24,7 @@ require([
'notebook/js/about',
'typeahead',
'notebook/js/searchandreplace',
// only loaded, not used, please keep sure this is loaded last
'custom/custom'
'custom',
], function(
IPython,
$,
@ -48,18 +47,10 @@ require([
CodeMirror,
about,
typeahead,
searchandreplace,
// please keep sure that even if not used, this is loaded last
custom
searchandreplace
) {
"use strict";
// BEGIN HARDCODED WIDGETS HACK
utils.load_extension('widgets/notebook/js/extension').catch(function () {
console.warn('ipywidgets package not installed. Widgets are not available.');
});
// END HARDCODED WIDGETS HACK
// compat with old IPython, remove for IPython > 3.0
window.CodeMirror = CodeMirror;
@ -85,7 +76,7 @@ require([
var save_widget = new savewidget.SaveWidget('span#save_widget', {
events: events,
keyboard_manager: keyboard_manager});
acts.extend_env({save_widget:save_widget})
acts.extend_env({save_widget:save_widget});
var contents = new contents.Contents({
base_url: common_options.base_url,
common_config: common_config
@ -177,8 +168,28 @@ require([
configurable: false
});
utils.load_extensions_from_config(config_section);
utils.load_extensions_from_config(common_config);
// Now actually load nbextensions from config
Promise.all([
utils.load_extensions_from_config(config_section),
utils.load_extensions_from_config(common_config),
])
.catch(function(error) {
console.error('Could not load nbextensions from user config files', error);
})
// BEGIN HARDCODED WIDGETS HACK
.then(function() {
if (!utils.is_loaded('jupyter-js-widgets/extension')) {
// Fallback to the ipywidgets extension
utils.load_extension('widgets/notebook/js/extension').catch(function () {
console.warn('Widgets are not available. Please install widgetsnbextension or ipywidgets 4.0');
});
}
})
.catch(function(error) {
console.error('Could not load ipywidgets', error);
});
// END HARDCODED WIDGETS HACK
notebook.load_notebook(common_options.notebook_path);
});

@ -28,7 +28,7 @@ define([
webFont: "STIX-Web",
styles: {'.MathJax_Display': {"margin": 0}},
linebreaks: { automatic: true }
}
},
});
MathJax.Hub.Configured();
} else if (window.mathjax_url !== "") {

@ -79,7 +79,7 @@ define([
) + "?download=" + download.toString();
var w = window.open('', IPython._target);
if (this.notebook.dirty) {
if (this.notebook.dirty && this.notebook.writable) {
this.notebook.save_notebook().then(function() {
w.location = url;
});
@ -112,9 +112,6 @@ define([
), IPython._target);
});
this.element.find('#copy_notebook').click(function () {
if (that.notebook.dirty) {
that.notebook.save_notebook({async : false});
}
that.notebook.copy_notebook();
return false;
});
@ -125,7 +122,7 @@ define([
var url = utils.url_path_join(
base_url, 'files', notebook_path
) + '?download=1';
if (that.notebook.dirty) {
if (that.notebook.dirty && that.notebook.writable) {
that.notebook.save_notebook().then(function() {
w.location = url;
});
@ -402,7 +399,7 @@ define([
.append($("<a>")
.attr('target', '_blank')
.attr('title', 'Opens in a new window')
.attr('href', link.url)
.attr('href', require.toUrl(link.url))
.append($("<i>")
.addClass("fa fa-external-link menu-icon pull-right")
)

@ -8,6 +8,7 @@ define(function (require) {
"use strict";
var IPython = require('base/js/namespace');
var $ = require('jquery');
var _ = require('underscore');
var utils = require('base/js/utils');
var dialog = require('base/js/dialog');
var cellmod = require('notebook/js/cell');
@ -66,6 +67,8 @@ define(function (require) {
this.last_modified = null;
// debug 484
this._last_modified = 'init';
// Firefox workaround
this._ff_beforeunload_fired = false;
// Create default scroll manager.
this.scroll_manager = new scrollmanager.ScrollManager(this);
@ -110,7 +113,8 @@ define(function (require) {
}
}, function (err) {
console.log("No CodeMirror mode: " + lang);
callback(err, code);
console.log("Require CodeMirror mode error: " + err);
callback(null, code);
});
}
});
@ -245,13 +249,11 @@ define(function (require) {
display_name: data.spec.display_name,
language: data.spec.language,
};
if (!existing_spec || JSON.stringify(existing_spec) != JSON.stringify(that.metadata.kernelspec)) {
if (!existing_spec || ! _.isEqual(existing_spec, that.metadata.kernelspec)) {
that.set_dirty(true);
}
// start session if the current session isn't already correct
if (!(that.session && that.session.kernel && that.session.kernel.name === data.name)) {
that.start_session(data.name);
}
// start a new session
that.start_session(data.name);
});
this.events.on('kernel_ready.Kernel', function(event, data) {
@ -263,7 +265,7 @@ define(function (require) {
var existing_info = that.metadata.language_info;
var langinfo = kinfo.language_info;
that.metadata.language_info = langinfo;
if (!existing_info || JSON.stringify(existing_info) != JSON.stringify(langinfo)) {
if (!existing_info || ! _.isEqual(existing_info, langinfo)) {
that.set_dirty(true);
}
// Mode 'null' should be plain, unhighlighted text.
@ -313,6 +315,17 @@ define(function (require) {
if (kill_kernel) {
that.session.delete();
}
if ( utils.browser[0] === "Firefox") {
// Workaround ancient Firefox bug showing beforeunload twice: https://bugzilla.mozilla.org/show_bug.cgi?id=531199
if (that._ff_beforeunload_fired) {
return; // don't show twice on FF
}
that._ff_beforeunload_fired = true;
// unset flag immediately after dialog is dismissed
setTimeout(function () {
that._ff_beforeunload_fired = false;
}, 1);
}
// if we are autosaving, trigger an autosave on nav-away.
// still warn, because if we don't the autosave may fail.
if (that.dirty) {
@ -457,7 +470,8 @@ define(function (require) {
* @return {jQuery} A selector of all cell elements
*/
Notebook.prototype.get_cell_elements = function () {
return this.container.find(".cell").not('.cell .cell');
var container = this.container || $('#notebook-container')
return container.find(".cell").not('.cell .cell');
};
/**
@ -865,6 +879,7 @@ define(function (require) {
tomove.detach();
pivot.after(tomove);
this.get_cell(selected-1).focus_cell();
this.select(anchored - 1);
this.select(selected - 1, false);
};
@ -891,6 +906,7 @@ define(function (require) {
tomove.detach();
pivot.before(tomove);
this.get_cell(selected+1).focus_cell();
this.select(first);
this.select(anchored + 1);
this.select(selected + 1, false);
@ -1553,6 +1569,18 @@ define(function (require) {
first_inserted.focus_cell();
}
};
/**
* Re-render the output of a CodeCell.
*/
Notebook.prototype.render_cell_output = function (code_cell) {
var cell_data = code_cell.toJSON();
var cell_index = this.find_cell_index(code_cell);
var trusted = code_cell.output_area.trusted;
this.clear_output(cell_index);
code_cell.output_area.trusted = trusted;
code_cell.fromJSON(cell_data);
};
// Split/merge
@ -1936,7 +1964,7 @@ define(function (require) {
var success = $.proxy(this._session_started, this);
var failure = $.proxy(this._session_start_failed, this);
if (this.session !== null) {
if (this.session && this.session.kernel) {
this.session.restart(options, success, failure);
} else {
this.session = new session.Session(options);
@ -2542,7 +2570,7 @@ define(function (require) {
" Selecting trust will immediately reload this notebook in a trusted state."
).append(
" For more information, see the "
).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
).append($("<a>").attr("href", "https://jupyter-notebook.readthedocs.io/en/latest/security.html")
.text("Jupyter security documentation")
).append(".")
);
@ -2578,23 +2606,32 @@ define(function (require) {
/**
* Make a copy of the current notebook.
* If the notebook has unsaved changes, it is saved first.
*/
Notebook.prototype.copy_notebook = function () {
var that = this;
var base_url = this.base_url;
var w = window.open('', IPython._target);
var parent = utils.url_path_split(this.notebook_path)[0];
this.contents.copy(this.notebook_path, parent).then(
function (data) {
w.location = utils.url_path_join(
base_url, 'notebooks', utils.encode_uri_components(data.path)
);
},
function(error) {
w.close();
that.events.trigger('notebook_copy_failed', error);
}
);
var p;
if (this.dirty) {
p = this.save_notebook();
} else {
p = Promise.resolve();
}
return p.then(function () {
return that.contents.copy(that.notebook_path, parent).then(
function (data) {
w.location = utils.url_path_join(
base_url, 'notebooks', utils.encode_uri_components(data.path)
);
},
function(error) {
w.close();
that.events.trigger('notebook_copy_failed', error);
}
);
});
};
/**

@ -139,8 +139,7 @@ define([
if (info.attempt === 1) {
var msg = "A connection to the notebook server could not be established." +
" The notebook will continue trying to reconnect, but" +
" until it does, you will NOT be able to run code. Check your" +
" The notebook will continue trying to reconnect. Check your" +
" network connection or notebook server configuration.";
dialog.kernel_modal({

@ -91,8 +91,8 @@ define([
return false;
}
// line-height from http://stackoverflow.com/questions/1185151
var fontSize = this.element.css('font-size');
var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
var fontSize = this.element.css('font-size') || '14px';
var lineHeight = Math.floor((parseFloat(fontSize.replace('px','')) || 14) * 1.3);
return (this.element.height() > threshold * lineHeight);
};
@ -103,10 +103,6 @@ define([
this.prompt_overlay.click(function () { that.toggle_scroll(); });
this.element.resize(function () {
// FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
if ( utils.browser[0] === "Firefox" ) {
return;
}
// maybe scroll output,
// if it's grown large enough and hasn't already been scrolled.
if (!that.scrolled && that._should_scroll()) {
@ -258,7 +254,7 @@ define([
}
var data = bundle.data;
$.map(OutputArea.output_types, function(key){
if (key !== 'application/json' &&
if ((key.indexOf('application/') === -1 || key.indexOf('json') === -1) &&
data[key] !== undefined &&
typeof data[key] !== 'string'
) {
@ -650,8 +646,9 @@ define([
var type = 'image/svg+xml';
var toinsert = this.create_output_subarea(md, "output_svg", type);
// Get the svg element from within the HTML.
var svg = $('<div />').html(svg_html).find('svg');
// Get the svg element from within the HTML.
// One svg is supposed, but could embed other nested svgs
var svg = $($('<div \>').html(svg_html).find('svg')[0]);
var svg_area = $('<div />');
var width = svg.attr('width');
var height = svg.attr('height');
@ -753,7 +750,7 @@ define([
*/
var type = 'text/latex';
var toinsert = this.create_output_subarea(md, "output_latex", type);
toinsert.append(latex);
toinsert.text(latex);
element.append(toinsert);
return toinsert;
};
@ -960,6 +957,19 @@ define([
"application/javascript" : append_javascript,
"application/pdf" : append_pdf
};
OutputArea.prototype.mime_types = function () {
return OutputArea.display_order;
};
OutputArea.prototype.register_mime_type = function (mimetype, append, options) {
if (mimetype && typeof(append) === 'function') {
OutputArea.output_types.push(mimetype);
if (options.safe) OutputArea.safe_outputs[mimetype] = true;
OutputArea.display_order.splice(options.index || 0, 0, mimetype);
OutputArea.append_map[mimetype] = append;
}
};
return {'OutputArea': OutputArea};
});

@ -161,6 +161,14 @@ define([
this.pager_element.find(".container").append($('<pre/>').html(utils.fixCarriageReturn(utils.fixConsole(text))));
};
Pager.prototype.append = function (htm) {
/**
* The only user content injected with this HTML call is escaped by
* the fixConsole() method.
*/
this.pager_element.find(".container").append(htm);
};
Pager.prototype._resize = function() {
/**

@ -113,6 +113,7 @@ define([
'end':'End',
'space':'Space',
'backspace':'Backspace',
'-':'Minus'
};
var humanize_map;
@ -123,7 +124,7 @@ define([
humanize_map = default_humanize_map;
}
var special_case = { pageup: "PageUp", pagedown: "Page Down", 'minus': '-' };
var special_case = { pageup: "PageUp", pagedown: "Page Down" };
function humanize_key(key){
if (key.length === 1){

@ -104,6 +104,7 @@ define([
if (that.keyboard_manager) {
that.keyboard_manager.enable();
}
that.code_mirror.setOption('readOnly', !that.is_editable());
});
this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
// The tabindex=-1 makes this div focusable.

@ -97,18 +97,11 @@ div.cell {
}
div.inner_cell {
min-width: 0;
.vbox();
.box-flex1();
}
@-moz-document url-prefix() {
div.inner_cell {
// hack around FF bug causing cell to expand when lines are long
// instead of scrolling
overflow-x: hidden;
}
}
/* input_area and input_prompt must match in top border and margin for alignment */
div.input_area {
border: 1px solid @light_border_color;

@ -4,7 +4,7 @@
}
.shortcut_key {
display: inline-block;
width: 20ex;
width: 21ex;
text-align: right;
font-family: @font-family-monospace;
}

@ -129,12 +129,9 @@ define([
}
this.comms[content.comm_id] = this.comms[content.comm_id].then(function(comm) {
try {
comm.handle_msg(msg);
} catch (e) {
console.log("Exception handling comm msg: ", e, e.stack, msg);
}
return comm;
return (Promise.resolve(comm.handle_msg(msg))
.catch(utils.reject('Exception handling comm message'))
.then(function() {return comm;}));
});
return this.comms[content.comm_id];
};
@ -194,7 +191,7 @@ define([
var callback = this['_' + key + '_callback'];
if (callback) {
try {
callback(msg);
return callback(msg);
} catch (e) {
console.log("Exception in Comm callback", e, e.stack, msg);
}
@ -202,7 +199,7 @@ define([
};
Comm.prototype.handle_msg = function (msg) {
this._callback('msg', msg);
return this._callback('msg', msg);
};
Comm.prototype.handle_close = function (msg) {

@ -42,6 +42,7 @@ define([
this.username = "username";
this.session_id = utils.uuid();
this._msg_callbacks = {};
this._msg_callbacks_overrides = {};
this._msg_queue = Promise.resolve();
this.info_reply = {}; // kernel_info_reply stored here after starting
@ -63,6 +64,8 @@ define([
this._autorestart_attempt = 0;
this._reconnect_attempt = 0;
this.reconnect_limit = 7;
this._pending_messages = [];
};
/**
@ -149,7 +152,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Kernel.prototype.list = function (success, error) {
$.ajax(this.kernel_service_url, {
utils.ajax(this.kernel_service_url, {
processData: false,
cache: false,
type: "GET",
@ -191,7 +194,7 @@ define([
}
};
$.ajax(url, {
utils.ajax(url, {
processData: false,
cache: false,
type: "POST",
@ -215,7 +218,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Kernel.prototype.get_info = function (success, error) {
$.ajax(this.kernel_url, {
utils.ajax(this.kernel_url, {
processData: false,
cache: false,
type: "GET",
@ -241,7 +244,7 @@ define([
Kernel.prototype.kill = function (success, error) {
this.events.trigger('kernel_killed.Kernel', {kernel: this});
this._kernel_dead();
$.ajax(this.kernel_url, {
utils.ajax(this.kernel_url, {
processData: false,
cache: false,
type: "DELETE",
@ -275,7 +278,7 @@ define([
};
var url = utils.url_path_join(this.kernel_url, 'interrupt');
$.ajax(url, {
utils.ajax(url, {
processData: false,
cache: false,
type: "POST",
@ -299,6 +302,8 @@ define([
this.events.trigger('kernel_restarting.Kernel', {kernel: this});
this.stop_channels();
this._msg_callbacks = {};
this._msg_callbacks_overrides = {};
var that = this;
var on_success = function (data, status, xhr) {
that.events.trigger('kernel_created.Kernel', {kernel: that});
@ -317,7 +322,7 @@ define([
};
var url = utils.url_path_join(this.kernel_url, 'restart');
$.ajax(url, {
utils.ajax(url, {
processData: false,
cache: false,
type: "POST",
@ -337,7 +342,7 @@ define([
* @function reconnect
*/
if (this.is_connected()) {
return;
this.stop_channels();
}
this._reconnect_attempt = this._reconnect_attempt + 1;
this.events.trigger('kernel_reconnecting.Kernel', {
@ -409,6 +414,15 @@ define([
* @function _kernel_connected
*/
this.events.trigger('kernel_connected.Kernel', {kernel: this});
// Send pending messages. We shift the message off the queue
// after the message is sent so that if there is an exception,
// the message is still pending.
while (this._pending_messages.length > 0) {
this.ws.send(this._pending_messages[0]);
this._pending_messages.shift();
}
// get kernel info so we know what state the kernel is in
var that = this;
this.kernel_info(function (reply) {
@ -524,8 +538,13 @@ define([
this.events.trigger('kernel_disconnected.Kernel', {kernel: this});
if (error) {
console.log('WebSocket connection failed: ', ws_url);
this.events.trigger('kernel_connection_failed.Kernel', {kernel: this, ws_url: ws_url, attempt: this._reconnect_attempt});
console.log('WebSocket connection failed: ', ws_url, error);
this.events.trigger('kernel_connection_failed.Kernel', {
kernel: this,
ws_url: ws_url,
attempt: this._reconnect_attempt,
error: error,
});
}
this._schedule_reconnect();
};
@ -602,19 +621,34 @@ define([
return (this.ws === null);
};
Kernel.prototype._send = function(msg) {
/**
* Send a message (if the kernel is connected) or queue the message for future delivery
*
* Pending messages will automatically be sent when a kernel becomes connected.
*
* @function _send
* @param msg
*/
if (this.is_connected()) {
this.ws.send(msg);
} else {
this._pending_messages.push(msg);
}
}
Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) {
/**
* Send a message on the Kernel's shell channel
*
* If the kernel is not connected, the message will be buffered.
*
* @function send_shell_message
*/
if (!this.is_connected()) {
throw new Error("kernel is not connected");
}
var msg = this._get_msg(msg_type, content, metadata, buffers);
msg.channel = 'shell';
this.ws.send(serialize.serialize(msg));
this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
this._send(serialize.serialize(msg));
return msg.header.msg_id;
};
@ -627,7 +661,7 @@ define([
*
* When calling this method, pass a callback function that expects one argument.
* The callback will be passed the complete `kernel_info_reply` message documented
* [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
* [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#kernel-info)
*/
var callbacks;
if (callback) {
@ -645,7 +679,7 @@ define([
*
* When calling this method, pass a callback function that expects one argument.
* The callback will be passed the complete `comm_info_reply` message documented
* [here](http://ipython.org/ipython-doc/dev/development/messaging.html#comm_info)
* [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#comm_info)
*/
var callbacks;
if (callback) {
@ -663,7 +697,7 @@ define([
*
* When calling this method, pass a callback function that expects one argument.
* The callback will be passed the complete `inspect_reply` message documented
* [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
* [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#object-information)
*
* @function inspect
* @param code {string}
@ -754,7 +788,7 @@ define([
* `complete_reply` message as its only argument when it arrives.
*
* `complete_reply` is documented
* [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
* [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#complete)
*
* @function complete
* @param code {string}
@ -777,16 +811,13 @@ define([
* @function send_input_reply
*/
Kernel.prototype.send_input_reply = function (input) {
if (!this.is_connected()) {
throw new Error("kernel is not connected");
}
var content = {
value : input
};
this.events.trigger('input_reply.Kernel', {kernel: this, content: content});
var msg = this._get_msg("input_reply", content);
msg.channel = 'stdin';
this.ws.send(serialize.serialize(msg));
this._send(serialize.serialize(msg));
return msg.header.msg_id;
};
@ -819,6 +850,35 @@ define([
}
};
/**
* Get output callbacks for a specific message.
*
* @function get_output_callbacks_for_msg
*
* Since output callbacks can be overridden, we first check the override stack.
*/
Kernel.prototype.get_output_callbacks_for_msg = function (msg_id) {
return this.get_callbacks_for_msg(this.get_output_callback_id(msg_id));
};
/**
* Get the output callback id for a message
*
* Since output callbacks can be redirected, this may not be the same as
* the msg_id.
*
* @function get_output_callback_id
*/
Kernel.prototype.get_output_callback_id = function (msg_id) {
var callback_id = msg_id;
var overrides = this._msg_callbacks_overrides[msg_id];
if (overrides && overrides.length > 0) {
callback_id = overrides[overrides.length-1];
}
return callback_id
}
/**
* Clear callbacks for a specific message.
*
@ -863,10 +923,16 @@ define([
*
* }
*
* If the third parameter is truthy, the callback is set as the last
* callback registered.
*
* @function set_callbacks_for_msg
*/
Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
this.last_msg_id = msg_id;
Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks, save) {
var remember = save || true;
if (remember) {
this.last_msg_id = msg_id;
}
if (callbacks) {
// shallow-copy mapping, because we will modify it at the top level
var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
@ -875,11 +941,31 @@ define([
cbcopy.input = callbacks.input;
cbcopy.shell_done = (!callbacks.shell);
cbcopy.iopub_done = (!callbacks.iopub);
} else {
} else if (remember) {
this.last_msg_callbacks = {};
}
};
/**
* Override output callbacks for a particular msg_id
*/
Kernel.prototype.output_callback_overrides_push = function(msg_id, callback_id) {
var output_callbacks = this._msg_callbacks_overrides[msg_id];
if (output_callbacks === void 0) {
this._msg_callbacks_overrides[msg_id] = output_callbacks = [];
}
output_callbacks.push(callback_id);
}
Kernel.prototype.output_callback_overrides_pop = function(msg_id) {
var callback_ids = this._msg_callbacks_overrides[msg_id];
if (!callback_ids) {
console.error("Popping callback overrides, but none registered", msg_id);
return;
}
return callback_ids.pop();
}
Kernel.prototype._handle_ws_message = function (e) {
var that = this;
this._msg_queue = this._msg_queue.then(function() {
@ -1004,7 +1090,7 @@ define([
* @function _handle_clear_output
*/
Kernel.prototype._handle_clear_output = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
var callbacks = this.get_output_callbacks_for_msg(msg.parent_header.msg_id);
if (!callbacks || !callbacks.iopub) {
return;
}
@ -1020,7 +1106,8 @@ define([
* @function _handle_output_message
*/
Kernel.prototype._handle_output_message = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
var msg_id = msg.parent_header.msg_id;
var callbacks = this.get_output_callbacks_for_msg(msg_id);
if (!callbacks || !callbacks.iopub) {
// The message came from another client. Let the UI decide what to
// do with it.

@ -77,7 +77,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.list = function (success, error) {
$.ajax(this.session_service_url, {
utils.ajax(this.session_service_url, {
processData: false,
cache: false,
type: "GET",
@ -118,7 +118,7 @@ define([
}
};
$.ajax(this.session_service_url, {
utils.ajax(this.session_service_url, {
processData: false,
cache: false,
type: "POST",
@ -140,7 +140,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.get_info = function (success, error) {
$.ajax(this.session_url, {
utils.ajax(this.session_url, {
processData: false,
cache: false,
type: "GET",
@ -166,7 +166,7 @@ define([
this.notebook_model.path = path;
}
$.ajax(this.session_url, {
utils.ajax(this.session_url, {
processData: false,
cache: false,
type: "PATCH",
@ -193,7 +193,7 @@ define([
this.kernel._kernel_dead();
}
$.ajax(this.session_url, {
utils.ajax(this.session_url, {
processData: false,
cache: false,
type: "DELETE",
@ -242,7 +242,9 @@ define([
*/
Session.prototype._get_model = function () {
return {
notebook: this.notebook_model,
path: this.notebook_model.path,
type: 'notebook',
name: '',
kernel: this.kernel_model
};
};
@ -262,7 +264,7 @@ define([
this.session_url = utils.url_path_join(this.session_service_url, this.id);
}
if (data && data.notebook) {
this.notebook_model.path = data.notebook.path;
this.notebook_model.path = data.path;
}
if (data && data.kernel) {
this.kernel_model.name = data.kernel.name;

@ -8,7 +8,7 @@ require([
'base/js/page',
'services/config',
'terminal/js/terminado',
'custom/custom',
'custom',
], function(
$,
termjs,
@ -20,10 +20,12 @@ require([
"use strict";
page = new page.Page();
var config = new configmod.ConfigSection('terminal',
{base_url: utils.get_body_data('baseUrl')});
config.load();
var common_config = new configmod.ConfigSection('common',
{base_url: utils.get_body_data('baseUrl')});
common_config.load();
// Test size: 25x80
var termRowHeight = function(){ return 1.00 * $("#dummy-screen")[0].offsetHeight / 25;};
// 1.02 here arrived at by trial and error to make the spacing look right
@ -31,10 +33,14 @@ require([
var base_url = utils.get_body_data('baseUrl');
var ws_path = utils.get_body_data('wsPath');
var ws_url = location.protocol.replace('http', 'ws') + "//" + location.host
+ base_url + ws_path;
var ws_url = utils.get_body_data('wsUrl');
if (!ws_url) {
// trailing 's' in https will become wss for secure web sockets
ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
}
ws_url = ws_url + base_url + ws_path;
var header = $("#header")[0]
var header = $("#header")[0];
function calculate_size() {
var height = $(window).height() - header.offsetHeight;
var width = $('#terminado-container').width();
@ -51,6 +57,7 @@ require([
page.show_site();
utils.load_extensions_from_config(config);
utils.load_extensions_from_config(common_config);
window.onresize = function() {

@ -1,13 +1,12 @@
define ([], function() {
define ([
'termjs',
], function(Terminal) {
"use strict";
function make_terminal(element, size, ws_url) {
var ws = new WebSocket(ws_url);
Terminal.brokenBold = true;
var term = new Terminal({
cols: size.cols,
rows: size.rows,
screenKeys: false,
useStyle: false
rows: size.rows
});
ws.onopen = function(event) {
ws.send(JSON.stringify(["set_size", size.rows, size.cols,

@ -7,6 +7,7 @@
}
.terminal {
width: 100%;
float: left;
font-family: monospace;
color: white;
@ -19,6 +20,10 @@
line-height: 1em;
font-size: @notebook_font_size;
}
.xterm-rows {
padding: 10px;
}
}
.terminal-cursor {

@ -19,7 +19,7 @@ require([
// only loaded, not used:
'jquery-ui',
'bootstrap',
'custom/custom',
'custom',
], function(
$,
IPython,
@ -41,6 +41,14 @@ require([
page = new page.Page();
function isMirroringEnabled() {
return (new RegExp("^(ar|he)").test(navigator.language));
}
if (isMirroringEnabled()) {
$("body").attr("dir","rtl");
}
var common_options = {
base_url: utils.get_body_data("baseUrl"),
notebook_path: utils.get_body_data("notebookPath"),

@ -555,6 +555,7 @@ define([
var uri_prefix = NotebookList.uri_prefixes[model.type];
if (model.type === 'file' &&
model.mimetype && model.mimetype.substr(0,5) !== 'text/'
&& !model.mimetype.endsWith('javascript')
) {
// send text/unidentified files to editor, others go to raw viewer
uri_prefix = 'files';
@ -631,7 +632,7 @@ define([
'api/sessions',
encodeURIComponent(session.id)
);
$.ajax(url, settings);
utils.ajax(url, settings);
}
};

@ -63,7 +63,7 @@ define([
error : utils.log_ajax_error,
};
var url = utils.url_path_join(this.base_url, 'api/sessions');
$.ajax(url, settings);
utils.ajax(url, settings);
};
SesssionList.prototype.sessions_loaded = function(data){

@ -61,12 +61,12 @@ define([
this.base_url,
'api/terminals'
);
$.ajax(url, settings);
utils.ajax(url, settings);
};
TerminalList.prototype.load_terminals = function() {
var url = utils.url_path_join(this.base_url, 'api/terminals');
$.ajax(url, {
utils.ajax(url, {
type: "GET",
cache: false,
dataType: "json",
@ -114,7 +114,7 @@ define([
};
var url = utils.url_path_join(that.base_url, 'api/terminals',
utils.encode_uri_components(name));
$.ajax(url, settings);
utils.ajax(url, settings);
return false;
});
item.find(".item_buttons").text("").append(shutdown_button);

@ -16,10 +16,18 @@
// The left padding of the selector button's contents.
@dashboard-selectorbtn-lpad: 7px;
[dir="rtl"] #tabs li {
float: right;
}
ul#tabs {
margin-bottom: @dashboard_tb_pad;
}
[dir="rtl"] ul#tabs {
margin-right:0px;
}
ul#tabs a {
padding-top: @dashboard_tb_pad + 2px;
padding-bottom: @dashboard_tb_pad;
@ -48,6 +56,21 @@ ul.breadcrumb {
}
}
[dir="rtl"] .list_toolbar {
.tree-buttons {
float: left !important;
}
.pull-right {
padding-top: 1px;
float: left !important;
}
.pull-left {
float: right !important;
}
}
.dynamic-buttons {
padding-top: @dashboard_tb_pad - 1px;
display: inline-block;
@ -192,6 +215,10 @@ ul.breadcrumb {
padding-right: 0px;
}
[dir="rtl"] #tree-selector a {
float: right;
}
#button-select-all {
min-width: 50px;
}
@ -246,6 +273,10 @@ ul#new-menu {
right: 0;
}
[dir="rtl"] #new-menu {
text-align: right;
}
.kernel-menu-icon {
padding-right: 12px;
width: 24px;
@ -305,6 +336,12 @@ ul#new-menu {
}
}
[dir="rtl"] #running {
.col-sm-8 {
float: right !important;
}
}
.delete-button {
display: none;
}

@ -11,7 +11,7 @@
{% block bodyclasses %}edit_app {{super()}}{% endblock %}
{% block params %}
data-base-url="{{base_url}}"
data-base-url="{{base_url | urlencode}}"
data-file-path="{{file_path}}"
{{super()}}
{% endblock %}
@ -65,7 +65,9 @@ data-file-path="{{file_path}}"
</li>
<li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
<ul id="view-menu" class="dropdown-menu">
<li id="menu-line-numbers"><a href="#">Toggle Line Numbers</a></li>
<li id="toggle_header" title="Show/Hide the logo and notebook title (above menu bar)">
<a href="#">Toggle Header</a></li>
<li id="menu-line-numbers"><a href="#">Toggle Line Numbers</a></li>
</ul>
</li>
<li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Language</a>

@ -14,13 +14,15 @@
<div id="ipython-main-app" class="container">
{% if login_available %}
{# login_available means password-login is allowed. Show the form. #}
<div class="row">
<div class="navbar col-sm-8 col-sm-offset2">
<div class="navbar col-sm-8">
<div class="navbar-inner">
<div class="container">
<div class="center-nav">
<p class="navbar-text nav">Password:</p>
<p class="navbar-text nav">Password{% if token_available %} or token{% endif %}:</p>
<form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
{{ xsrf_form_html() | safe }}
<input type="password" name="password" id="password_input" class="form-control">
<button type="submit" id="login_submit">Log in</button>
</form>
@ -29,6 +31,8 @@
</div>
</div>
</div>
{% else %}
<p>No login available, you shouldn't be seeing this page.</p>
{% endif %}
{% if message %}
<div class="row">
@ -39,8 +43,34 @@
{% endfor %}
</div>
{% endif %}
{% if token_available %}
{% block token_message %}
<div class="col-sm-6 col-sm-offset-3 text-left">
<p class="warning">
Token authentication is enabled.
<div/>
You need to open the notebook server with its first-time login token in the URL,
or enable a password in order to gain access.
The command:
</p>
<pre>jupyter notebook list</pre>
<p>
will show you the URLs of running servers with their tokens,
which you can copy and paste into your browser. For example:
</p>
<pre>Currently running servers:
http://localhost:8888/?token=c8de56fa... :: /Users/you/notebooks
</pre>
<p>
Or you can paste just the token value into the password field on this page.
</p>
<p>
Cookies are required for authenticated access to notebooks.
</p>
</div>
{% endblock token_message %}
{% endif %}
</div>
{% endblock %}

@ -105,7 +105,7 @@ data-notebook-path="{{notebook_path | urlencode}}"
<li id="print_preview"><a href="#">Print Preview</a></li>
<li class="dropdown-submenu"><a href="#">Download as</a>
<ul class="dropdown-menu">
<li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
<li id="download_ipynb"><a href="#">Notebook (.ipynb)</a></li>
<li id="download_script"><a href="#">Script</a></li>
<li id="download_html"><a href="#">HTML (.html)</a></li>
<li id="download_markdown"><a href="#">Markdown (.md)</a></li>
@ -148,7 +148,7 @@ data-notebook-path="{{notebook_path | urlencode}}"
<li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
<ul id="view_menu" class="dropdown-menu">
<li id="toggle_header"
title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
title="Show/Hide the logo and notebook title (above menu bar)">
<a href="#">Toggle Header</a></li>
<li id="toggle_toolbar"
title="Show/Hide the action icons (below menu bar)">
@ -293,7 +293,7 @@ data-notebook-path="{{notebook_path | urlencode}}"
{% endif %}
{% endfor %}
<li class="divider"></li>
<li title="About IPython Notebook"><a id="notebook_about" href="#">About</a></li>
<li title="About Jupyter Notebook"><a id="notebook_about" href="#">About</a></li>
{% endblock %}
</ul>
</li>

@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<title>{% block title %}IPython Notebook{% endblock %}</title>
<title>{% block title %}Jupyter Notebook{% endblock %}</title>
{% block favicon %}<link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
@ -27,7 +27,6 @@
'auth/js/main': 'auth/js/main.min',
custom : '{{ base_url }}custom',
nbextensions : '{{ base_url }}nbextensions',
widgets : '{{ base_url }}deprecatedwidgets',
kernelspecs : '{{ base_url }}kernelspecs',
underscore : 'components/underscore/underscore-min',
backbone : 'components/backbone/backbone-min',
@ -37,8 +36,8 @@
'jquery-ui': 'components/jquery-ui/ui/minified/jquery-ui.min',
moment: 'components/moment/moment',
codemirror: 'components/codemirror',
termjs: 'components/term.js/src/term',
typeahead: 'components/jquery-typeahead/dist/jquery.typeahead'
termjs: 'components/xterm.js/dist/xterm',
typeahead: 'components/jquery-typeahead/dist/jquery.typeahead.min',
},
map: { // for backward compatibility
"*": {
@ -80,6 +79,33 @@
}
}
});
define("bootstrap", function () {
return window.$;
});
define("jquery", function () {
return window.$;
});
define("jqueryui", function () {
return window.$;
});
define("jquery-ui", function () {
return window.$;
});
// error-catching custom.js shim.
define("custom", function (require, exports, module) {
try {
var custom = require('custom/custom');
console.debug('loaded custom.js');
return custom;
} catch (e) {
console.error("error loading custom.js", e);
return {};
}
})
</script>
{% block meta %}
@ -87,11 +113,17 @@
</head>
<body class="{% block bodyclasses %}{% endblock %}" {% block params %}{% endblock %}>
<body class="{% block bodyclasses %}{% endblock %}"
{% block params %}
{% if logged_in and token %}
data-jupyter-api-token="{{token | urlencode}}"
{% endif %}
{% endblock params %}
>
<noscript>
<div id='noscript'>
IPython Notebook requires JavaScript.<br>
Jupyter Notebook requires JavaScript.<br>
Please enable it to proceed.
</div>
</noscript>

@ -6,7 +6,8 @@
{% block params %}
data-base-url="{{base_url}}"
data-base-url="{{base_url | urlencode}}"
data-ws-url="{{ws_url | urlencode}}"
data-ws-path="{{ws_path}}"
{% endblock %}
@ -15,6 +16,7 @@ data-ws-path="{{ws_path}}"
{{super()}}
<link rel="stylesheet" href="{{ static_url("terminal/css/override.css") }}" type="text/css" />
<link rel="stylesheet" href="{{static_url("components/xterm.js/dist/xterm.css")}}" type="text/css" />
{% endblock %}
{% block site %}

@ -4,15 +4,15 @@ from ..base.handlers import APIHandler, json_errors
from ..utils import url_path_join
class TerminalRootHandler(APIHandler):
@web.authenticated
@json_errors
@web.authenticated
def get(self):
tm = self.terminal_manager
terms = [{'name': name} for name in tm.terminals]
self.finish(json.dumps(terms))
@web.authenticated
@json_errors
@web.authenticated
def post(self):
"""POST /terminals creates a new terminal and redirects to it"""
name, _ = self.terminal_manager.new_named_terminal()
@ -22,8 +22,8 @@ class TerminalRootHandler(APIHandler):
class TerminalHandler(APIHandler):
SUPPORTED_METHODS = ('GET', 'DELETE')
@web.authenticated
@json_errors
@web.authenticated
def get(self, name):
tm = self.terminal_manager
if name in tm.terminals:
@ -31,8 +31,8 @@ class TerminalHandler(APIHandler):
else:
raise web.HTTPError(404, "Terminal not found: %r" % name)
@web.authenticated
@json_errors
@web.authenticated
@gen.coroutine
def delete(self, name):
tm = self.terminal_manager

@ -26,6 +26,31 @@ casper.notebook_test(function () {
this.test.assertEquals(result, output, "IPython.utils.fixConsole() handles [0m correctly");
var input = [
'hasrn\r\n',
'hasn\n',
'\n',
'abcdef\r',
'hello\n',
'ab3\r',
'x2\r\r',
'1\r',
].join('');
var output = [
'hasrn\n',
'hasn\n',
'\n',
'hellof\n',
'123\r'
].join('');
var result = this.evaluate(function (input) {
return IPython.utils.fixCarriageReturn(input);
}, input);
this.test.assertEquals(result, output, "IPython.utils.fixCarriageReturns works");
this.thenEvaluate(function() {
define('nbextensions/a', [], function() { window.a = true; });
define('nbextensions/c', [], function() { window.c = true; });

@ -2,8 +2,8 @@
from __future__ import print_function
from binascii import hexlify
import os
import sys
import time
import requests
from contextlib import contextmanager
@ -18,9 +18,11 @@ except ImportError:
from mock import patch #py2
from tornado.ioloop import IOLoop
import zmq
import jupyter_core.paths
from ..notebookapp import NotebookApp
from ..utils import url_path_join
from ipython_genutils.tempdir import TemporaryDirectory
MAX_WAITTIME = 30 # seconds to wait for notebook server to start
@ -66,6 +68,20 @@ class NotebookTestBase(TestCase):
cls.notebook_thread.join(timeout=MAX_WAITTIME)
if cls.notebook_thread.is_alive():
raise TimeoutError("Undead notebook server")
@classmethod
def request(self, verb, path, **kwargs):
"""Send a request to my server
with authentication and everything.
"""
headers = kwargs.setdefault('headers', {})
# kwargs.setdefault('allow_redirects', False)
headers.setdefault('Authorization', 'token %s' % self.token)
response = requests.request(verb,
url_path_join(self.base_url(), path),
**kwargs)
return response
@classmethod
def setup_class(cls):
@ -83,6 +99,7 @@ class NotebookTestBase(TestCase):
cls.data_dir = data_dir
cls.runtime_dir = TemporaryDirectory()
cls.notebook_dir = TemporaryDirectory()
cls.token = hexlify(os.urandom(4)).decode('ascii')
started = Event()
def start_thread():
@ -96,6 +113,7 @@ class NotebookTestBase(TestCase):
notebook_dir=cls.notebook_dir.name,
base_url=cls.url_prefix,
config=cls.config,
token=cls.token,
)
# don't register signal handler during tests
app.init_signal = lambda : None
@ -130,6 +148,16 @@ class NotebookTestBase(TestCase):
cls.notebook_dir.cleanup()
cls.env_patch.stop()
cls.path_patch.stop()
# cleanup global zmq Context, to ensure we aren't leaving dangling sockets
def cleanup_zmq():
zmq.Context.instance().term()
t = Thread(target=cleanup_zmq)
t.daemon = True
t.start()
t.join(5) # give it a few seconds to clean up (this should be immediate)
# if term never returned, there's zmq stuff still open somewhere, so shout about it.
if t.is_alive():
raise RuntimeError("Failed to teardown zmq Context, open sockets likely left lying around.")
@classmethod
def base_url(cls):

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save