parent
c0f3dc02a0
commit
5b6ed99aec
@ -1,11 +0,0 @@
|
||||
name: Enforce PR label
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, edited, synchronize]
|
||||
jobs:
|
||||
enforce-label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: enforce-triage-label
|
||||
uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1
|
||||
@ -1,61 +0,0 @@
|
||||
name: Linux JS Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: '*'
|
||||
pull_request:
|
||||
branches: '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu, macos]
|
||||
group: [notebook, base, services]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache pip on Linux
|
||||
uses: actions/cache@v1
|
||||
if: startsWith(runner.os, 'Linux')
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python }}-${{ hashFiles('**/requirements.txt', 'setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-${{ matrix.python }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install --upgrade setuptools wheel
|
||||
npm install
|
||||
npm install -g casperjs@1.1.3 phantomjs-prebuilt@2.1.7
|
||||
pip install .[test]
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
python -m notebook.jstest ${{ matrix.group }}
|
||||
@ -1,53 +0,0 @@
|
||||
# The NBConvert Service requires pandoc. Instead of testing
|
||||
# Pandoc on every operating system (which should already be
|
||||
# done in nbconvert directly), we'll only test these services
|
||||
# on ubuntu where we can easily load Pandoc from a Github
|
||||
# Actions docker image (this docker image is not on other
|
||||
# operating systems).
|
||||
name: NBConvert Service Tests
|
||||
on:
|
||||
push:
|
||||
branches: '*'
|
||||
pull_request:
|
||||
branches: '*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [ '3.7', '3.8', '3.9', '3.10' ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Install Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: 'x64'
|
||||
- name: Setup Pandoc
|
||||
uses: r-lib/actions/setup-pandoc@v1
|
||||
- name: Upgrade packaging dependencies
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
- name: Get pip cache dir
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-${{ matrix.python-version }}-
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install the Python dependencies
|
||||
run: |
|
||||
pip install -e .[test]
|
||||
- name: Run NBConvert Tests
|
||||
run: |
|
||||
pytest notebook/nbconvert/tests/
|
||||
- name: Run NBConvert Service Tests
|
||||
run: |
|
||||
pytest notebook/services/nbconvert/tests/
|
||||
@ -1,53 +0,0 @@
|
||||
name: Python Tests
|
||||
on:
|
||||
push:
|
||||
branches: '*'
|
||||
pull_request:
|
||||
branches: '*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu, macos, windows]
|
||||
python-version: [ '3.7', '3.8', '3.9', '3.10' ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Install Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: 'x64'
|
||||
- name: Upgrade packaging dependencies
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel --user
|
||||
- name: Get pip cache dir
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-${{ matrix.python-version }}-
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install the Python dependencies
|
||||
run: |
|
||||
pip install -e .[test] codecov
|
||||
- name: List installed packages
|
||||
run: |
|
||||
pip freeze
|
||||
pip check
|
||||
- name: Run Server-side tests
|
||||
run: |
|
||||
pytest -vv --cov notebook --cov-branch --cov-report term-missing:skip-covered --ignore-glob=notebook/tests/selenium/* --ignore-glob=notebook/nbconvert/tests/* --ignore-glob=notebook/services/nbconvert/tests/*
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
pytest -v notebook/tests/test_notebookapp_integration.py --integration_tests
|
||||
- name: Coverage
|
||||
run: |
|
||||
codecov
|
||||
@ -1,46 +0,0 @@
|
||||
name: Selenium Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: '*'
|
||||
pull_request:
|
||||
branches: '*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu, macos]
|
||||
python-version: [ '3.7', '3.8', '3.9', '3.10' ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: 'x64'
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Install JS
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install -U pip setuptools wheel
|
||||
pip install --upgrade selenium
|
||||
pip install pytest
|
||||
pip install .[test]
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export JUPYTER_TEST_BROWSER=firefox
|
||||
export MOZ_HEADLESS=1
|
||||
pytest -sv notebook/tests/selenium
|
||||
@ -1,149 +0,0 @@
|
||||
A. J. Holyoake <a.j.holyoake@gmail.com> ajholyoake <a.j.holyoake@gmail.com>
|
||||
Aaron Culich <aculich@gmail.com> Aaron Culich <aculich@eecs.berkeley.edu>
|
||||
Aron Ahmadia <aron@ahmadia.net> ahmadia <aron@ahmadia.net>
|
||||
Benjamin Ragan-Kelley <benjaminrk@gmail.com> <minrk@Mercury.local>
|
||||
Benjamin Ragan-Kelley <benjaminrk@gmail.com> Min RK
|
||||
Benjamin Ragan-Kelley <benjaminrk@gmail.com> MinRK <benjaminrk@gmail.com>
|
||||
Barry Wark <barrywark@gmail.com> Barry Wark <barrywarkatgmaildotcom>
|
||||
Ben Edwards <bedwards@cs.unm.edu> Ben Edwards <bedwards@sausage.(none)>
|
||||
Bradley M. Froehle <brad.froehle@gmail.com> Bradley M. Froehle <bfroehle@math.berkeley.edu>
|
||||
Bradley M. Froehle <brad.froehle@gmail.com> Bradley Froehle <brad.froehle@gmail.com>
|
||||
Brandon Parsons <brandon@parsonstx.com> Brandon Parsons <brandon.parsons@hp.com>
|
||||
Brian E. Granger <ellisonbg@gmail.com> Brian Granger
|
||||
Brian E. Granger <ellisonbg@gmail.com> Brian Granger <>
|
||||
Brian E. Granger <ellisonbg@gmail.com> bgranger <>
|
||||
Brian E. Granger <ellisonbg@gmail.com> bgranger <bgranger@red>
|
||||
Christoph Gohlke <cgohlke@uci.edu> cgohlke <cgohlke@uci.edu>
|
||||
Cyrille Rossant <cyrille.rossant@gmail.com> rossant <rossant@github>
|
||||
Damián Avila <damianavila82@yahoo.com.ar> damianavila <damianavila82@yahoo.com.ar>
|
||||
Damián Avila <damianavila82@yahoo.com.ar> damianavila <damianavila@gmail.com>
|
||||
Damon Allen <damontallen@gmail.com> damontallen <damontallen@gmail.com>
|
||||
Darren Dale <dsdale24@gmail.com> darren.dale <>
|
||||
Darren Dale <dsdale24@gmail.com> Darren Dale <>
|
||||
Dav Clark <davclark@berkeley.edu> Dav Clark <>
|
||||
Dav Clark <davclark@berkeley.edu> Dav Clark <davclark@gmail.com>
|
||||
David Hirschfeld <david.hirschfeld@gazprom-mt.com> dhirschfeld <david.hirschfeld@gazprom-mt.com>
|
||||
David P. Sanders <dpsanders@gmail.com> David P. Sanders <dpsanders@ciencias.unam.mx>
|
||||
David Warde-Farley <wardefar@iro.umontreal.ca> David Warde-Farley <>
|
||||
Doug Blank <dblank@cs.brynmawr.edu> Doug Blank <doug.blank@gmail.com>
|
||||
Eugene Van den Bulke <eugene.van-den-bulke@gmail.com> Eugene Van den Bulke <eugene.vandenbulke@gmail.com>
|
||||
Evan Patterson <epatters@enthought.com> <epatters@EPattersons-MacBook-Pro.local>
|
||||
Evan Patterson <epatters@enthought.com> <epatters@evan-laptop.localdomain>
|
||||
Evan Patterson <epatters@enthought.com> <epatters@caltech.edu>
|
||||
Evan Patterson <epatters@enthought.com> <ejpatters@gmail.com>
|
||||
Evan Patterson <epatters@enthought.com> epatters <ejpatters@gmail.com>
|
||||
Evan Patterson <epatters@enthought.com> epatters <epatters@enthought.com>
|
||||
Ernie French <ernestfrench@gmail.com> Ernie French <ernie@gqpbj.com>
|
||||
Ernie French <ernestfrench@gmail.com> ernie french <ernestfrench@gmail.com>
|
||||
Ernie French <ernestfrench@gmail.com> ernop <ernestfrench@gmail.com>
|
||||
Fernando Perez <Fernando.Perez@berkeley.edu> <fperez.net@gmail.com>
|
||||
Fernando Perez <Fernando.Perez@berkeley.edu> Fernando Perez <fernando.perez@berkeley.edu>
|
||||
Fernando Perez <Fernando.Perez@berkeley.edu> fperez <>
|
||||
Fernando Perez <Fernando.Perez@berkeley.edu> fptest <>
|
||||
Fernando Perez <Fernando.Perez@berkeley.edu> fptest1 <>
|
||||
Fernando Perez <Fernando.Perez@berkeley.edu> Fernando Perez <fernando.perez@berkeley.edu>
|
||||
Fernando Perez <fernando.perez@berkeley.edu> Fernando Perez <>
|
||||
Fernando Perez <fernando.perez@berkeley.edu> Fernando Perez <fperez@maqroll>
|
||||
Frank Murphy <fpmurphy@mtu.edu> Frank Murphy <fmurphy@arbor.net>
|
||||
Gabriel Becker <gmbecker@ucdavis.edu> gmbecker <gmbecker@ucdavis.edu>
|
||||
Gael Varoquaux <gael.varoquaux@normalesup.org> gael.varoquaux <>
|
||||
Gael Varoquaux <gael.varoquaux@normalesup.org> gvaroquaux <gvaroquaux@gvaroquaux-desktop>
|
||||
Gael Varoquaux <gael.varoquaux@normalesup.org> Gael Varoquaux <>
|
||||
Ingolf Becker <ingolf.becker@googlemail.com> watercrossing <ingolf.becker@googlemail.com>
|
||||
Jake Vanderplas <jakevdp@gmail.com> Jake Vanderplas <vanderplas@astro.washington.edu>
|
||||
Jakob Gager <jakob.gager@gmail.com> jakobgager <jakob.gager@gmail.com>
|
||||
Jakob Gager <jakob.gager@gmail.com> jakobgager <gager@ilsb.tuwien.ac.at>
|
||||
Jakob Gager <jakob.gager@gmail.com> jakobgager <jakobgager@hotmail.com>
|
||||
Jason Grout <jgrout6@bloomberg.net> <jason.grout@drake.edu>
|
||||
Jason Grout <jgrout6@bloomberg.net> <jason-github@creativetrax.com>
|
||||
Jason Gors <jason.gors.work@gmail.com> jason gors <jason.gors.work@gmail.com>
|
||||
Jason Gors <jason.gors.work@gmail.com> jgors <jason.gors.work@gmail.com>
|
||||
Jens Hedegaard Nielsen <jenshnielsen@gmail.com> Jens Hedegaard Nielsen <jhn@jhn-Znote.(none)>
|
||||
Jens Hedegaard Nielsen <jenshnielsen@gmail.com> Jens H Nielsen <jenshnielsen@gmail.com>
|
||||
Jens Hedegaard Nielsen <jenshnielsen@gmail.com> Jens H. Nielsen <jenshnielsen@gmail.com>
|
||||
Jez Ng <jezreel@gmail.com> Jez Ng <me@jezng.com>
|
||||
Jonathan Frederic <jdfreder@calpoly.edu> Jonathan Frederic <jonathan@LifebookMint.(none)>
|
||||
Jonathan Frederic <jdfreder@calpoly.edu> Jonathan Frederic <jon.freder@gmail.com>
|
||||
Jonathan Frederic <jdfreder@calpoly.edu> Jonathan Frederic <xh3xx.goose@gmail.com>
|
||||
Jonathan Frederic <jdfreder@calpoly.edu> jon <jon.freder@gmail.com>
|
||||
Jonathan Frederic <jdfreder@calpoly.edu> U-Jon-PC\Jon <Jon@Jon-PC.(none)>
|
||||
Jonathan March <jmarch@enthought.com> Jonathan March <JDM@MarchRay.net>
|
||||
Jonathan March <jmarch@enthought.com> jdmarch <JDM@marchRay.net>
|
||||
Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jörgen Stenarson <jorgen.stenarson@bostream.nu>
|
||||
Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jorgen Stenarson <jorgen.stenarson@bostream.nu>
|
||||
Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jorgen Stenarson <>
|
||||
Jörgen Stenarson <jorgen.stenarson@kroywen.se> jstenar <jorgen.stenarson@bostream.nu>
|
||||
Jörgen Stenarson <jorgen.stenarson@kroywen.se> jstenar <>
|
||||
Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jörgen Stenarson <jorgen.stenarson@kroywen.se>
|
||||
Juergen Hasch <python@elbonia.de> juhasch <python@elbonia.de>
|
||||
Juergen Hasch <python@elbonia.de> juhasch <hasch@VMBOX.fritz.box>
|
||||
Julia Evans <julia@jvns.ca> Julia Evans <julia@stripe.com>
|
||||
Kester Tong <kestert@google.com> KesterTong <kestert@google.com>
|
||||
Kyle Kelley <rgbkrk@gmail.com> Kyle Kelley <kyle.kelley@rackspace.com>
|
||||
Kyle Kelley <rgbkrk@gmail.com> rgbkrk <rgbkrk@gmail.com>
|
||||
Laurent Dufréchou <laurent.dufrechou@gmail.com> <laurent.dufrechou@gmail.com>
|
||||
Laurent Dufréchou <laurent.dufrechou@gmail.com> <laurent@Pep>
|
||||
Laurent Dufréchou <laurent.dufrechou@gmail.com> laurent dufrechou <>
|
||||
Laurent Dufréchou <laurent.dufrechou@gmail.com> laurent.dufrechou <>
|
||||
Laurent Dufréchou <laurent.dufrechou@gmail.com> Laurent Dufrechou <>
|
||||
Laurent Dufréchou <laurent.dufrechou@gmail.com> laurent.dufrechou@gmail.com <>
|
||||
Laurent Dufréchou <laurent.dufrechou@gmail.com> ldufrechou <ldufrechou@PEP>
|
||||
Lorena Pantano <lorena.pantano@gmail.com> Lorena <lorena.pantano@gmail.com>
|
||||
Luis Pedro Coelho <luis@luispedro.org> Luis Pedro Coelho <lpc@cmu.edu>
|
||||
Marc Molla <marcmolla@gmail.com> marcmolla <marcmolla@gmail.com>
|
||||
Martín Gaitán <gaitan@gmail.com> Martín Gaitán <gaitan@phasety.com>
|
||||
Matthias Bussonnier <bussonniermatthias@gmail.com> Matthias BUSSONNIER <bussonniermatthias@gmail.com>
|
||||
Matthias Bussonnier <bussonniermatthias@gmail.com> Bussonnier Matthias <bussonniermatthias@gmail.com>
|
||||
Matthias Bussonnier <bussonniermatthias@gmail.com> Matthias BUSSONNIER <bussonniermatthias@umr168-curn-1-24x-6561.curie.fr>
|
||||
Matthias Bussonnier <bussonniermatthias@gmail.com> Matthias Bussonnier <carreau@Aspire.(none)>
|
||||
Michael Droettboom <mdboom@gmail.com> Michael Droettboom <mdroe@stsci.edu>
|
||||
Nicholas Bollweg <nick.bollweg@gmail.com> Nicholas Bollweg (Nick) <nick.bollweg@gmail.com>
|
||||
Nicolas Rougier <Nicolas.Rougier@inria.fr> <Nicolas.rougier@inria.fr>
|
||||
Nikolay Koldunov <koldunovn@gmail.com> Nikolay Koldunov <nikolay.koldunov@zmaw.de>
|
||||
Omar Andrés Zapata Mesa <andresete.chaos@gmail.com> Omar Andres Zapata Mesa <andresete.chaos@gmail.com>
|
||||
Omar Andrés Zapata Mesa <andresete.chaos@gmail.com> Omar Andres Zapata Mesa <omazapa@tuxhome>
|
||||
Pankaj Pandey <pankaj86@gmail.com> Pankaj Pandey <pankaj@enthought.com>
|
||||
Pascal Schetelat <pascal.schetelat@gmail.com> pascal-schetelat <pascal.schetelat@gmail.com>
|
||||
Paul Ivanov <pi@berkeley.edu> Paul Ivanov <pivanov314@gmail.com>
|
||||
Pauli Virtanen <pauli.virtanen@iki.fi> Pauli Virtanen <>
|
||||
Pauli Virtanen <pauli.virtanen@iki.fi> Pauli Virtanen <pav@iki.fi>
|
||||
Pierre Gerold <pierre.gerold@laposte.net> Pierre Gerold <gerold@crans.org>
|
||||
Pietro Berkes <pberkes@enthought.com> Pietro Berkes <pietro.berkes@googlemail.com>
|
||||
Piti Ongmongkolkul <piti118@gmail.com> piti118 <piti118@gmail.com>
|
||||
Prabhu Ramachandran <prabhu@enthought.com> Prabhu Ramachandran <>
|
||||
Puneeth Chaganti <punchagan@gmail.com> Puneeth Chaganti <punchagan@muse-amuse.in>
|
||||
Robert Kern <robert.kern@gmail.com> rkern <>
|
||||
Robert Kern <robert.kern@gmail.com> Robert Kern <rkern@enthought.com>
|
||||
Robert Kern <robert.kern@gmail.com> Robert Kern <rkern@Sacrilege.local>
|
||||
Robert Kern <robert.kern@gmail.com> Robert Kern <>
|
||||
Robert Marchman <bo.marchman@gmail.com> Robert Marchman <robert.l.marchman@dartmouth.edu>
|
||||
Satrajit Ghosh <satra@mit.edu> Satrajit Ghosh <satra@ba5.mit.edu>
|
||||
Satrajit Ghosh <satra@mit.edu> Satrajit Ghosh <satrajit.ghosh@gmail.com>
|
||||
Scott Sanderson <scoutoss@gmail.com> Scott Sanderson <ssanderson@quantopian.com>
|
||||
smithj1 <smithj1@LMC-022896.local> smithj1 <smithj1@LMC-022896.swisscom.com>
|
||||
smithj1 <smithj1@LMC-022896.local> smithj1 <smithj1@lmc-022896.local>
|
||||
Steven Johnson <steven.johnson@drake.edu> stevenJohnson <steven.johnson@drake.edu>
|
||||
Steven Silvester <steven.silvester@ieee.org> blink1073 <steven.silvester@ieee.org>
|
||||
S. Weber <s8weber@c4.usr.sh> s8weber <s8weber@c5.usr.sh>
|
||||
Stefan van der Walt <stefan@sun.ac.za> Stefan van der Walt <bzr@mentat.za.net>
|
||||
Silvia Vinyes <silvia.vinyes@gmail.com> Silvia <silvia@silvia-U44SG.(none)>
|
||||
Silvia Vinyes <silvia.vinyes@gmail.com> silviav12 <silvia.vinyes@gmail.com>
|
||||
Sylvain Corlay <scorlay@bloomberg.net> <sylvain.corlay@gmail.com>
|
||||
Sylvain Corlay <scorlay@bloomberg.net> sylvain.corlay <sylvain.corlay@gmail.com>
|
||||
Ted Drain <ted.drain@gmail.com> TD22057 <ted.drain@gmail.com>
|
||||
Théophile Studer <theo.studer@gmail.com> Théophile Studer <studer@users.noreply.github.com>
|
||||
Thomas Kluyver <takowl@gmail.com> Thomas <takowl@gmail.com>
|
||||
Thomas Spura <tomspur@fedoraproject.org> Thomas Spura <thomas.spura@gmail.com>
|
||||
Timo Paulssen <timonator@perpetuum-immobile.de> timo <timonator@perpetuum-immobile.de>
|
||||
vds <vds@VIVIAN> vds2212 <vds2212@VIVIAN>
|
||||
vds <vds@VIVIAN> vds <vds@vivian>
|
||||
Ville M. Vainio <vivainio@gmail.com> <vivainio2@WN-W0941>
|
||||
Ville M. Vainio <vivainio@gmail.com> ville <ville@VILLE-PC>
|
||||
Ville M. Vainio <vivainio@gmail.com> ville <ville@ville-desktop>
|
||||
Ville M. Vainio <vivainio@gmail.com> vivainio <>
|
||||
Ville M. Vainio <vivainio@gmail.com> Ville M. Vainio <vivainio@villev>
|
||||
Ville M. Vainio <vivainio@gmail.com> Ville M. Vainio <vivainio@ville_vmw>
|
||||
Walter Doerwald <walter@livinglogic.de> walter.doerwald <>
|
||||
Walter Doerwald <walter@livinglogic.de> Walter Doerwald <>
|
||||
W. Trevor King <wking@tremily.us> W. Trevor King <wking@drexel.edu>
|
||||
Yoval P. <yoval@gmx.com> y-p <yoval@gmx.com>
|
||||
@ -1,197 +0,0 @@
|
||||
Contributing to the Jupyter Notebook
|
||||
====================================
|
||||
|
||||
If you're reading this section, you're probably interested in contributing to
|
||||
Jupyter. Welcome and thanks for your interest in contributing!
|
||||
|
||||
Please take a look at the Contributor documentation, familiarize yourself with
|
||||
using the Jupyter Notebook, and introduce yourself on the mailing list and
|
||||
share what area of the project you are interested in working on.
|
||||
|
||||
General Guidelines
|
||||
------------------
|
||||
|
||||
For general documentation about contributing to Jupyter projects, see the
|
||||
`Project Jupyter Contributor Documentation`__.
|
||||
|
||||
__ https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html
|
||||
|
||||
|
||||
Setting Up a Development Environment
|
||||
------------------------------------
|
||||
|
||||
Installing Node.js and npm
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Building the Notebook from its GitHub source code requires some tools to
|
||||
create and minify JavaScript components and the CSS,
|
||||
specifically Node.js and Node's package manager, ``npm``.
|
||||
It should be node version ≥ 6.0.
|
||||
|
||||
If you use ``conda``, you can get them with::
|
||||
|
||||
conda install -c conda-forge nodejs
|
||||
|
||||
If you use `Homebrew <https://brew.sh/>`_ on Mac OS X::
|
||||
|
||||
brew install node
|
||||
|
||||
Installation on Linux may vary, but be aware that the `nodejs` or `npm` packages
|
||||
included in the system package repository may be too old to work properly.
|
||||
|
||||
You can also use the installer from the `Node.js website <https://nodejs.org>`_.
|
||||
|
||||
|
||||
Installing the Jupyter Notebook
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Once you have installed the dependencies mentioned above, use the following
|
||||
steps::
|
||||
|
||||
pip install --upgrade setuptools pip
|
||||
git clone https://github.com/jupyter/notebook
|
||||
cd notebook
|
||||
pip install -e .
|
||||
|
||||
If you are using a system-wide Python installation and you only want to install the notebook for you,
|
||||
you can add ``--user`` to the install commands.
|
||||
|
||||
Once you have done this, you can launch the master branch of Jupyter notebook
|
||||
from any directory in your system with::
|
||||
|
||||
jupyter notebook
|
||||
|
||||
Verification
|
||||
^^^^^^^^^^^^
|
||||
|
||||
While running the notebook, select one of your notebook files (the file will have the extension ``.ipynb``).
|
||||
In the top tab you will click on "Help" and then click on "About". In the pop window you will see information about the version of Jupyter that you are running. You will see "The version of the notebook server is:".
|
||||
If you are working in development mode, you will see that your version of Jupyter notebook will include the word "dev". If it does not include the word "dev", you are currently not working in development mode and should follow the steps below to uninstall and reinstall Jupyter.
|
||||
|
||||
Troubleshooting the Installation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you do not see that your Jupyter Notebook is not running on dev mode, it's possible that you are
|
||||
running other instances of Jupyter Notebook. You can try the following steps:
|
||||
|
||||
1. Uninstall all instances of the notebook package. These include any installations you made using
|
||||
pip or conda.
|
||||
2. Run ``python3 -m pip install -e .`` in the notebook repository to install the notebook from there.
|
||||
3. Run ``npm run build`` to make sure the Javascript and CSS are updated and compiled.
|
||||
4. Launch with ``python3 -m notebook --port 8989``, and check that the browser is pointing to ``localhost:8989``
|
||||
(rather than the default 8888). You don't necessarily have to launch with port 8989, as long as you use
|
||||
a port that is neither the default nor in use, then it should be fine.
|
||||
5. Verify the installation with the steps in the previous section.
|
||||
|
||||
|
||||
Rebuilding JavaScript and CSS
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There is a build step for the JavaScript and CSS in the notebook.
|
||||
To make sure that you are working with up-to-date code, you will need to run
|
||||
this command whenever there are changes to JavaScript or LESS sources::
|
||||
|
||||
npm run build
|
||||
|
||||
**IMPORTANT:** Don't forget to run ``npm run build`` after switching branches.
|
||||
When switching between branches of different versions (e.g. ``4.x`` and
|
||||
``master``), run ``pip install -e .``. If you have tried the above and still
|
||||
find that the notebook is not reflecting the current source code, try cleaning
|
||||
the repo with ``git clean -xfd`` and reinstalling with ``pip install -e .``.
|
||||
|
||||
Development Tip
|
||||
"""""""""""""""
|
||||
|
||||
When doing development, you can use this command to automatically rebuild
|
||||
JavaScript and LESS sources as they are modified::
|
||||
|
||||
npm run build:watch
|
||||
|
||||
Git Hooks
|
||||
"""""""""
|
||||
|
||||
If you want to automatically update dependencies and recompile JavaScript and
|
||||
CSS after checking out a new commit, you can install post-checkout and
|
||||
post-merge hooks which will do it for you::
|
||||
|
||||
git-hooks/install-hooks.sh
|
||||
|
||||
See ``git-hooks/README.md`` for more details.
|
||||
|
||||
|
||||
Running Tests
|
||||
-------------
|
||||
|
||||
Python Tests
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Install dependencies::
|
||||
|
||||
pip install -e '.[test]'
|
||||
|
||||
To run the Python tests, use::
|
||||
|
||||
pytest
|
||||
|
||||
If you want coverage statistics as well, you can run::
|
||||
|
||||
py.test --cov notebook -v --pyargs notebook
|
||||
|
||||
JavaScript Tests
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
To run the JavaScript tests, you will need to have PhantomJS and CasperJS
|
||||
installed::
|
||||
|
||||
npm install -g casperjs phantomjs-prebuilt
|
||||
|
||||
Then, to run the JavaScript tests::
|
||||
|
||||
python -m notebook.jstest [group]
|
||||
|
||||
where ``[group]`` is an optional argument that is a path relative to
|
||||
``notebook/tests/``.
|
||||
For example, to run all tests in ``notebook/tests/notebook``::
|
||||
|
||||
python -m notebook.jstest notebook
|
||||
|
||||
or to run just ``notebook/tests/notebook/deletecell.js``::
|
||||
|
||||
python -m notebook.jstest notebook/deletecell.js
|
||||
|
||||
|
||||
Building the Documentation
|
||||
--------------------------
|
||||
|
||||
To build the documentation you'll need `Sphinx <http://www.sphinx-doc.org/>`_,
|
||||
`pandoc <http://pandoc.org/>`_ and a few other packages.
|
||||
|
||||
To install (and activate) a conda environment named ``notebook_docs``
|
||||
containing all the necessary packages (except pandoc), use::
|
||||
|
||||
conda create -n notebook_docs pip
|
||||
conda activate notebook_docs # Linux and OS X
|
||||
activate notebook_docs # Windows
|
||||
pip install .[docs]
|
||||
|
||||
If you want to install the necessary packages with ``pip``, use the following instead::
|
||||
|
||||
pip install .[docs]
|
||||
|
||||
Once you have installed the required packages, you can build the docs with::
|
||||
|
||||
cd docs
|
||||
make html
|
||||
|
||||
After that, the generated HTML files will be available at
|
||||
``build/html/index.html``. You may view the docs in your browser.
|
||||
|
||||
You can automatically check if all hyperlinks are still valid::
|
||||
|
||||
make linkcheck
|
||||
|
||||
Windows users can find ``make.bat`` in the ``docs`` folder.
|
||||
|
||||
You should also have a look at the `Project Jupyter Documentation Guide`__.
|
||||
|
||||
__ https://jupyter.readthedocs.io/en/latest/contributing/docs-contributions/index.html
|
||||
File diff suppressed because one or more lines are too long
@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "jupyter-notebook-deps",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"backbone": "components/backbone#~1.2",
|
||||
"bootstrap": "bootstrap#~3.4",
|
||||
"bootstrap-tour": "0.9.0",
|
||||
"codemirror": "components/codemirror#5.56.0+components1",
|
||||
"create-react-class": "https://cdn.jsdelivr.net/npm/create-react-class@15.6.3/create-react-class.min.js",
|
||||
"es6-promise": "~1.0",
|
||||
"font-awesome": "components/font-awesome#~4.7.0",
|
||||
"jed": "~1.1.1",
|
||||
"jquery": "components/jquery#~3.5.0",
|
||||
"jquery-typeahead": "~2.10.6",
|
||||
"jquery-ui": "components/jqueryui#~1.12",
|
||||
"marked": "~0.7",
|
||||
"MathJax": "^2.7.4",
|
||||
"moment": "~2.19.3",
|
||||
"react": "~16.0.0",
|
||||
"requirejs": "~2.2",
|
||||
"requirejs-text": "~2.0.15",
|
||||
"requirejs-plugins": "~1.0.3",
|
||||
"text-encoding": "~0.1",
|
||||
"underscore": "components/underscore#~1.8.3",
|
||||
"xterm.js": "https://unpkg.com/xterm@~3.1.0/dist/xterm.js",
|
||||
"xterm.js-css": "https://unpkg.com/xterm@~3.1.0/dist/xterm.css",
|
||||
"xterm.js-fit": "https://unpkg.com/xterm@~3.1.0/dist/addons/fit/fit.js"
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
git hooks for Jupyter
|
||||
|
||||
add these to your `.git/hooks`
|
||||
|
||||
For now, we just have `post-checkout` and `post-merge`,
|
||||
both of which attempt to rebuild javascript and css sourcemaps,
|
||||
so make sure that you have a fully synced repo whenever you checkout or pull.
|
||||
|
||||
To use these hooks, run `./install-hooks.sh`.
|
||||
@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
DOTGIT=`git rev-parse --git-dir`
|
||||
TOPLEVEL=`git rev-parse --show-toplevel`
|
||||
TO=${DOTGIT}/hooks
|
||||
FROM=${TOPLEVEL}/git-hooks
|
||||
|
||||
ln -s ${FROM}/post-checkout ${TO}/post-checkout
|
||||
ln -s ${FROM}/post-merge ${TO}/post-merge
|
||||
@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$(basename $0)" == "post-merge" ]]; then
|
||||
PREVIOUS_HEAD=ORIG_HEAD
|
||||
else
|
||||
PREVIOUS_HEAD=$1
|
||||
fi
|
||||
|
||||
# if style changed (and less available), rebuild sourcemaps
|
||||
if [[
|
||||
! -z "$(git diff $PREVIOUS_HEAD notebook/static/*/js/**.js)"
|
||||
]]; then
|
||||
echo "rebuilding javascript"
|
||||
python setup.py js || echo "fail to rebuild javascript"
|
||||
fi
|
||||
|
||||
if [[
|
||||
! -z "$(git diff $PREVIOUS_HEAD notebook/static/*/less/**.less)"
|
||||
]]; then
|
||||
echo "rebuilding css sourcemaps"
|
||||
python setup.py css || echo "fail to recompile css"
|
||||
fi
|
||||
@ -1 +0,0 @@
|
||||
post-checkout
|
||||
@ -1,11 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=Jupyter Notebook
|
||||
Comment=Run Jupyter Notebook
|
||||
Exec=jupyter-notebook %f
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Icon=notebook
|
||||
StartupNotify=true
|
||||
MimeType=application/x-ipynb+json;
|
||||
Categories=Development;Education;
|
||||
Keywords=python;
|
||||
@ -1,96 +0,0 @@
|
||||
"""
|
||||
Utilities for getting information about Jupyter and the system it's running in.
|
||||
"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os
|
||||
import platform
|
||||
import pprint
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from ipython_genutils import py3compat, encoding
|
||||
|
||||
import notebook
|
||||
|
||||
def pkg_commit_hash(pkg_path):
|
||||
"""Get short form of commit hash given directory `pkg_path`
|
||||
|
||||
We get the commit hash from git if it's a repo.
|
||||
|
||||
If this fail, we return a not-found placeholder tuple
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pkg_path : str
|
||||
directory containing package
|
||||
only used for getting commit from active repo
|
||||
|
||||
Returns
|
||||
-------
|
||||
hash_from : str
|
||||
Where we got the hash from - description
|
||||
hash_str : str
|
||||
short form of hash
|
||||
"""
|
||||
|
||||
# maybe we are in a repository, check for a .git folder
|
||||
p = os.path
|
||||
cur_path = None
|
||||
par_path = pkg_path
|
||||
while cur_path != par_path:
|
||||
cur_path = par_path
|
||||
if p.exists(p.join(cur_path, '.git')):
|
||||
try:
|
||||
proc = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=pkg_path)
|
||||
repo_commit, _ = proc.communicate()
|
||||
except OSError:
|
||||
repo_commit = None
|
||||
|
||||
if repo_commit:
|
||||
return 'repository', repo_commit.strip().decode('ascii')
|
||||
else:
|
||||
return u'', u''
|
||||
par_path = p.dirname(par_path)
|
||||
|
||||
return u'', u''
|
||||
|
||||
|
||||
def pkg_info(pkg_path):
|
||||
"""Return dict describing the context of this package
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pkg_path : str
|
||||
path containing __init__.py for package
|
||||
|
||||
Returns
|
||||
-------
|
||||
context : dict
|
||||
with named parameters of interest
|
||||
"""
|
||||
src, hsh = pkg_commit_hash(pkg_path)
|
||||
return dict(
|
||||
notebook_version=notebook.__version__,
|
||||
notebook_path=pkg_path,
|
||||
commit_source=src,
|
||||
commit_hash=hsh,
|
||||
sys_version=sys.version,
|
||||
sys_executable=sys.executable,
|
||||
sys_platform=sys.platform,
|
||||
platform=platform.platform(),
|
||||
os_name=os.name,
|
||||
default_encoding=encoding.DEFAULT_ENCODING,
|
||||
)
|
||||
|
||||
def get_sys_info():
|
||||
"""Return useful information about the system as a dict."""
|
||||
p = os.path
|
||||
path = p.realpath(p.dirname(p.abspath(p.join(notebook.__file__))))
|
||||
return pkg_info(path)
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
"""
|
||||
Timezone utilities
|
||||
|
||||
Just UTC-awareness right now
|
||||
"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from datetime import tzinfo, timedelta, datetime
|
||||
|
||||
# constant for zero offset
|
||||
ZERO = timedelta(0)
|
||||
|
||||
class tzUTC(tzinfo):
|
||||
"""tzinfo object for UTC (zero offset)"""
|
||||
|
||||
def utcoffset(self, d):
|
||||
return ZERO
|
||||
|
||||
def dst(self, d):
|
||||
return ZERO
|
||||
|
||||
UTC = tzUTC()
|
||||
|
||||
def utc_aware(unaware):
|
||||
"""decorator for adding UTC tzinfo to datetime's utcfoo methods"""
|
||||
def utc_method(*args, **kwargs):
|
||||
dt = unaware(*args, **kwargs)
|
||||
return dt.replace(tzinfo=UTC)
|
||||
return utc_method
|
||||
|
||||
utcfromtimestamp = utc_aware(datetime.utcfromtimestamp)
|
||||
utcnow = utc_aware(datetime.utcnow)
|
||||
|
||||
def isoformat(dt):
|
||||
"""Return iso-formatted timestamp
|
||||
|
||||
Like .isoformat(), but uses Z for UTC instead of +00:00
|
||||
"""
|
||||
return dt.isoformat().replace('+00:00', 'Z')
|
||||
@ -1 +0,0 @@
|
||||
from .security import passwd
|
||||
@ -1,42 +0,0 @@
|
||||
from notebook.auth import passwd
|
||||
from getpass import getpass
|
||||
from notebook.config_manager import BaseJSONConfigManager
|
||||
from jupyter_core.paths import jupyter_config_dir
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
def set_password(args):
|
||||
password = args.password
|
||||
while not password :
|
||||
password1 = getpass("" if args.quiet else "Provide password: ")
|
||||
password_repeat = getpass("" if args.quiet else "Repeat password: ")
|
||||
if password1 != password_repeat:
|
||||
print("Passwords do not match, try again")
|
||||
elif len(password1) < 4:
|
||||
print("Please provide at least 4 characters")
|
||||
else:
|
||||
password = password1
|
||||
|
||||
password_hash = passwd(password)
|
||||
cfg = BaseJSONConfigManager(config_dir=jupyter_config_dir())
|
||||
cfg.update('jupyter_notebook_config', {
|
||||
'NotebookApp': {
|
||||
'password': password_hash,
|
||||
}
|
||||
})
|
||||
if not args.quiet:
|
||||
print("password stored in config dir: %s" % jupyter_config_dir())
|
||||
|
||||
def main(argv):
|
||||
parser = argparse.ArgumentParser(argv[0])
|
||||
subparsers = parser.add_subparsers()
|
||||
parser_password = subparsers.add_parser('password', help='sets a password for your notebook server')
|
||||
parser_password.add_argument("password", help="password to set, if not given, a password will be queried for (NOTE: this may not be safe)",
|
||||
nargs="?")
|
||||
parser_password.add_argument("--quiet", help="suppress messages", action="store_true")
|
||||
parser_password.set_defaults(function=set_password)
|
||||
args = parser.parse_args(argv[1:])
|
||||
args.function(args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
||||
@ -1,253 +0,0 @@
|
||||
"""Tornado handlers for logging into the notebook."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import re
|
||||
import os
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import uuid
|
||||
|
||||
from tornado.escape import url_escape
|
||||
|
||||
from .security import passwd_check, set_password
|
||||
|
||||
from ..base.handlers import IPythonHandler
|
||||
|
||||
|
||||
class LoginHandler(IPythonHandler):
|
||||
"""The basic tornado login handler
|
||||
|
||||
authenticates with a hashed password from the configuration.
|
||||
"""
|
||||
def _render(self, message=None):
|
||||
self.write(self.render_template('login.html',
|
||||
next=url_escape(self.get_argument('next', default=self.base_url)),
|
||||
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
|
||||
# protect chrome users from mishandling unescaped backslashes.
|
||||
# \ is not valid in urls, but some browsers treat it as /
|
||||
# instead of %5C, causing `\\` to behave as `//`
|
||||
url = url.replace("\\", "%5C")
|
||||
parsed = urlparse(url)
|
||||
if parsed.netloc or not (parsed.path + '/').startswith(self.base_url):
|
||||
# require that next_url be absolute path within our path
|
||||
allow = False
|
||||
# OR pass our cross-origin check
|
||||
if parsed.netloc:
|
||||
# if full URL, run our cross-origin check:
|
||||
origin = '%s://%s' % (parsed.scheme, parsed.netloc)
|
||||
origin = origin.lower()
|
||||
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.warning("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)
|
||||
self._redirect_safe(next_url)
|
||||
else:
|
||||
self._render()
|
||||
|
||||
@property
|
||||
def hashed_password(self):
|
||||
return self.password_from_settings(self.settings)
|
||||
|
||||
def passwd_check(self, a, b):
|
||||
return passwd_check(a, b)
|
||||
|
||||
def post(self):
|
||||
typed_password = self.get_argument('password', default=u'')
|
||||
new_password = self.get_argument('new_password', default=u'')
|
||||
|
||||
|
||||
|
||||
if self.get_login_available(self.settings):
|
||||
if self.passwd_check(self.hashed_password, typed_password) and not new_password:
|
||||
self.set_login_cookie(self, uuid.uuid4().hex)
|
||||
elif self.token and self.token == typed_password:
|
||||
self.set_login_cookie(self, uuid.uuid4().hex)
|
||||
if new_password and self.settings.get('allow_password_change'):
|
||||
config_dir = self.settings.get('config_dir')
|
||||
config_file = os.path.join(config_dir, 'jupyter_notebook_config.json')
|
||||
set_password(new_password, config_file=config_file)
|
||||
self.log.info("Wrote hashed password to %s" % config_file)
|
||||
else:
|
||||
self.set_status(401)
|
||||
self._render(message={'error': 'Invalid credentials'})
|
||||
return
|
||||
|
||||
|
||||
next_url = self.get_argument('next', default=self.base_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)
|
||||
cookie_options.setdefault('path', handler.base_url)
|
||||
handler.set_secure_cookie(handler.cookie_name, user_id, **cookie_options)
|
||||
return user_id
|
||||
|
||||
auth_header_pat = re.compile(r'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):
|
||||
"""Called by handlers.get_current_user for identifying the current user.
|
||||
|
||||
See tornado.web.RequestHandler.get_current_user for details.
|
||||
"""
|
||||
# Can't call this get_current_user because it will collide when
|
||||
# called on LoginHandler itself.
|
||||
if getattr(handler, '_user_id', None):
|
||||
return handler._user_id
|
||||
user_id = cls.get_user_token(handler)
|
||||
if user_id is None:
|
||||
get_secure_cookie_kwargs = handler.settings.get('get_secure_cookie_kwargs', {})
|
||||
user_id = handler.get_secure_cookie(handler.cookie_name, **get_secure_cookie_kwargs )
|
||||
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:
|
||||
# If an invalid cookie was sent, clear it to prevent unnecessary
|
||||
# extra warnings. But don't do this on a request with *no* cookie,
|
||||
# because that can erroneously log you out (see gh-3365)
|
||||
if handler.get_cookie(handler.cookie_name) is not None:
|
||||
handler.log.warning("Clearing invalid/expired login cookie %s", handler.cookie_name)
|
||||
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)
|
||||
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
|
||||
|
||||
if authenticated:
|
||||
return uuid.uuid4().hex
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
def validate_security(cls, app, ssl_options=None):
|
||||
"""Check the notebook application's security.
|
||||
|
||||
Show messages, or abort if necessary, based on the security configuration.
|
||||
"""
|
||||
if not app.ip:
|
||||
warning = "WARNING: The notebook server is listening on all IP addresses"
|
||||
if ssl_options is None:
|
||||
app.log.warning(warning + " and not using encryption. This "
|
||||
"is not recommended.")
|
||||
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):
|
||||
"""Return the hashed password from the tornado settings.
|
||||
|
||||
If there is no configured password, an empty string will be returned.
|
||||
"""
|
||||
return settings.get('password', u'')
|
||||
|
||||
@classmethod
|
||||
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) or settings.get('token'))
|
||||
@ -1,23 +0,0 @@
|
||||
"""Tornado handlers for logging out of the notebook.
|
||||
"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from ..base.handlers import IPythonHandler
|
||||
|
||||
|
||||
class LogoutHandler(IPythonHandler):
|
||||
|
||||
def get(self):
|
||||
self.clear_login_cookie()
|
||||
if self.login_available:
|
||||
message = {'info': 'Successfully logged out.'}
|
||||
else:
|
||||
message = {'warning': 'Cannot log out. Notebook authentication '
|
||||
'is disabled.'}
|
||||
self.write(self.render_template('logout.html',
|
||||
message=message))
|
||||
|
||||
|
||||
default_handlers = [(r"/logout", LogoutHandler)]
|
||||
@ -1,172 +0,0 @@
|
||||
"""
|
||||
Password generation for the Notebook.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import getpass
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode
|
||||
from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader
|
||||
from jupyter_core.paths import jupyter_config_dir
|
||||
|
||||
# Length of the salt in nr of hex chars, which implies salt_len * 4
|
||||
# bits of randomness.
|
||||
salt_len = 12
|
||||
|
||||
|
||||
def passwd(passphrase=None, algorithm='argon2'):
|
||||
"""Generate hashed password and salt for use in notebook configuration.
|
||||
|
||||
In the notebook configuration, set `c.NotebookApp.password` to
|
||||
the generated string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
passphrase : str
|
||||
Password to hash. If unspecified, the user is asked to input
|
||||
and verify a password.
|
||||
algorithm : str
|
||||
Hashing algorithm to use (e.g, 'sha1' or any argument supported
|
||||
by :func:`hashlib.new`, or 'argon2').
|
||||
|
||||
Returns
|
||||
-------
|
||||
hashed_passphrase : str
|
||||
Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> passwd('mypassword', algorithm='sha1')
|
||||
'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12'
|
||||
|
||||
"""
|
||||
if passphrase is None:
|
||||
for i in range(3):
|
||||
p0 = getpass.getpass('Enter password: ')
|
||||
p1 = getpass.getpass('Verify password: ')
|
||||
if p0 == p1:
|
||||
passphrase = p0
|
||||
break
|
||||
else:
|
||||
print('Passwords do not match.')
|
||||
else:
|
||||
raise ValueError('No matching passwords found. Giving up.')
|
||||
|
||||
if algorithm == 'argon2':
|
||||
from argon2 import PasswordHasher
|
||||
ph = PasswordHasher(
|
||||
memory_cost=10240,
|
||||
time_cost=10,
|
||||
parallelism=8,
|
||||
)
|
||||
h = ph.hash(passphrase)
|
||||
|
||||
return ':'.join((algorithm, cast_unicode(h, 'ascii')))
|
||||
else:
|
||||
h = hashlib.new(algorithm)
|
||||
salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len)
|
||||
h.update(cast_bytes(passphrase, 'utf-8') + str_to_bytes(salt, 'ascii'))
|
||||
|
||||
return ':'.join((algorithm, salt, h.hexdigest()))
|
||||
|
||||
|
||||
def passwd_check(hashed_passphrase, passphrase):
|
||||
"""Verify that a given passphrase matches its hashed version.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hashed_passphrase : str
|
||||
Hashed password, in the format returned by `passwd`.
|
||||
passphrase : str
|
||||
Passphrase to validate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
valid : bool
|
||||
True if the passphrase matches the hash.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from notebook.auth.security import passwd_check
|
||||
>>> passwd_check('argon2:...', 'mypassword')
|
||||
True
|
||||
|
||||
>>> passwd_check('argon2:...', 'otherpassword')
|
||||
False
|
||||
|
||||
>>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
|
||||
... 'mypassword')
|
||||
True
|
||||
"""
|
||||
if hashed_passphrase.startswith('argon2:'):
|
||||
import argon2
|
||||
import argon2.exceptions
|
||||
ph = argon2.PasswordHasher()
|
||||
try:
|
||||
return ph.verify(hashed_passphrase[7:], passphrase)
|
||||
except argon2.exceptions.VerificationError:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
try:
|
||||
h = hashlib.new(algorithm)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
if len(pw_digest) == 0:
|
||||
return False
|
||||
|
||||
h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))
|
||||
|
||||
return h.hexdigest() == pw_digest
|
||||
|
||||
@contextmanager
|
||||
def persist_config(config_file=None, mode=0o600):
|
||||
"""Context manager that can be used to modify a config object
|
||||
|
||||
On exit of the context manager, the config will be written back to disk,
|
||||
by default with user-only (600) permissions.
|
||||
"""
|
||||
|
||||
if config_file is None:
|
||||
config_file = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json')
|
||||
|
||||
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
||||
|
||||
loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file))
|
||||
try:
|
||||
config = loader.load_config()
|
||||
except ConfigFileNotFound:
|
||||
config = Config()
|
||||
|
||||
yield config
|
||||
|
||||
with io.open(config_file, 'w', encoding='utf8') as f:
|
||||
f.write(cast_unicode(json.dumps(config, indent=2)))
|
||||
|
||||
try:
|
||||
os.chmod(config_file, mode)
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb),
|
||||
RuntimeWarning)
|
||||
|
||||
|
||||
def set_password(password=None, config_file=None):
|
||||
"""Ask user for password, store it in notebook json configuration file"""
|
||||
|
||||
hashed_password = passwd(password)
|
||||
|
||||
with persist_config(config_file) as config:
|
||||
config.NotebookApp.password = hashed_password
|
||||
@ -1,48 +0,0 @@
|
||||
"""Tests for login redirects"""
|
||||
|
||||
import requests
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
from notebook.tests.launchnotebook import NotebookTestBase
|
||||
|
||||
|
||||
class LoginTest(NotebookTestBase):
|
||||
def login(self, next):
|
||||
first = requests.get(self.base_url() + "login")
|
||||
first.raise_for_status()
|
||||
resp = requests.post(
|
||||
url_concat(
|
||||
self.base_url() + "login",
|
||||
{'next': next},
|
||||
),
|
||||
allow_redirects=False,
|
||||
data={
|
||||
"password": self.token,
|
||||
"_xsrf": first.cookies.get("_xsrf", ""),
|
||||
},
|
||||
cookies=first.cookies,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.headers['Location']
|
||||
|
||||
def test_next_bad(self):
|
||||
for bad_next in (
|
||||
"//some-host",
|
||||
"//host" + self.url_prefix + "tree",
|
||||
"https://google.com",
|
||||
"/absolute/not/base_url",
|
||||
):
|
||||
url = self.login(next=bad_next)
|
||||
self.assertEqual(url, self.url_prefix)
|
||||
assert url
|
||||
|
||||
def test_next_ok(self):
|
||||
for next_path in (
|
||||
"tree/",
|
||||
"//" + self.url_prefix + "tree",
|
||||
"notebooks/notebook.ipynb",
|
||||
"tree//something",
|
||||
):
|
||||
expected = self.url_prefix + next_path
|
||||
actual = self.login(next=expected)
|
||||
self.assertEqual(actual, expected)
|
||||
@ -1,25 +0,0 @@
|
||||
from ..security import passwd, passwd_check
|
||||
|
||||
def test_passwd_structure():
|
||||
p = passwd('passphrase')
|
||||
algorithm, hashed = p.split(':')
|
||||
assert algorithm == 'argon2'
|
||||
assert hashed.startswith('$argon2id$')
|
||||
|
||||
def test_roundtrip():
|
||||
p = passwd('passphrase')
|
||||
assert passwd_check(p, 'passphrase') == True
|
||||
|
||||
def test_bad():
|
||||
p = passwd('passphrase')
|
||||
assert passwd_check(p, p) == False
|
||||
assert passwd_check(p, 'a:b:c:d') == False
|
||||
assert passwd_check(p, 'a:b') == False
|
||||
|
||||
def test_passwd_check_unicode():
|
||||
# GH issue #4524
|
||||
phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f'
|
||||
assert passwd_check(phash, u"łe¶ŧ←↓→")
|
||||
phash = (u'argon2:$argon2id$v=19$m=10240,t=10,p=8$'
|
||||
u'qjjDiZUofUVVnrVYxacnbA$l5pQq1bJ8zglGT2uXP6iOg')
|
||||
assert passwd_check(phash, u"łe¶ŧ←↓→")
|
||||
@ -1,952 +0,0 @@
|
||||
"""Base Tornado handlers for the notebook server."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import datetime
|
||||
import functools
|
||||
import ipaddress
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
import warnings
|
||||
from http.client import responses
|
||||
from http.cookies import Morsel
|
||||
|
||||
from urllib.parse import urlparse
|
||||
from jinja2 import TemplateNotFound
|
||||
from tornado import web, gen, escape, httputil
|
||||
from tornado.log import app_log
|
||||
import prometheus_client
|
||||
|
||||
from notebook._sysinfo import get_sys_info
|
||||
|
||||
from traitlets.config import Application
|
||||
from ipython_genutils.path import filefind
|
||||
from ipython_genutils.py3compat import string_types
|
||||
|
||||
import notebook
|
||||
from notebook._tz import utcnow
|
||||
from notebook.i18n import combine_translations
|
||||
from notebook.utils import is_hidden, url_path_join, url_is_absolute, url_escape, urldecode_unix_socket_path
|
||||
from notebook.services.security import csp_report_uri
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Top-level handlers
|
||||
#-----------------------------------------------------------------------------
|
||||
non_alphanum = re.compile(r'[^A-Za-z0-9]')
|
||||
|
||||
_sys_info_cache = None
|
||||
def json_sys_info():
|
||||
global _sys_info_cache
|
||||
if _sys_info_cache is None:
|
||||
_sys_info_cache = json.dumps(get_sys_info())
|
||||
return _sys_info_cache
|
||||
|
||||
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
|
||||
|
||||
Can be overridden by defining Content-Security-Policy in settings['headers']
|
||||
"""
|
||||
if 'Content-Security-Policy' in self.settings.get('headers', {}):
|
||||
# user-specified, don't override
|
||||
return self.settings['headers']['Content-Security-Policy']
|
||||
|
||||
return '; '.join([
|
||||
"frame-ancestors 'self'",
|
||||
# Make sure the report-uri is relative to the base_url
|
||||
"report-uri " + self.settings.get('csp_report_uri', url_path_join(self.base_url, csp_report_uri)),
|
||||
])
|
||||
|
||||
def set_default_headers(self):
|
||||
headers = {}
|
||||
headers["X-Content-Type-Options"] = "nosniff"
|
||||
headers.update(self.settings.get('headers', {}))
|
||||
|
||||
headers["Content-Security-Policy"] = self.content_security_policy
|
||||
|
||||
# Allow for overriding headers
|
||||
for header_name, value in headers.items():
|
||||
try:
|
||||
self.set_header(header_name, value)
|
||||
except Exception as e:
|
||||
# tornado raise Exception (not a subclass)
|
||||
# if method is unsupported (websocket and Access-Control-Allow-Origin
|
||||
# for example, so just ignore)
|
||||
self.log.debug(e)
|
||||
|
||||
def force_clear_cookie(self, name, path="/", domain=None):
|
||||
"""Deletes the cookie with the given name.
|
||||
|
||||
Tornado's cookie handling currently (Jan 2018) stores cookies in a dict
|
||||
keyed by name, so it can only modify one cookie with a given name per
|
||||
response. The browser can store multiple cookies with the same name
|
||||
but different domains and/or paths. This method lets us clear multiple
|
||||
cookies with the same name.
|
||||
|
||||
Due to limitations of the cookie protocol, you must pass the same
|
||||
path and domain to clear a cookie as were used when that cookie
|
||||
was set (but there is no way to find out on the server side
|
||||
which values were used for a given cookie).
|
||||
"""
|
||||
name = escape.native_str(name)
|
||||
expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
|
||||
|
||||
morsel = Morsel()
|
||||
morsel.set(name, '', '""')
|
||||
morsel['expires'] = httputil.format_timestamp(expires)
|
||||
morsel['path'] = path
|
||||
if domain:
|
||||
morsel['domain'] = domain
|
||||
self.add_header("Set-Cookie", morsel.OutputString())
|
||||
|
||||
def clear_login_cookie(self):
|
||||
cookie_options = self.settings.get('cookie_options', {})
|
||||
path = cookie_options.setdefault('path', self.base_url)
|
||||
self.clear_cookie(self.cookie_name, path=path)
|
||||
if path and path != '/':
|
||||
# also clear cookie on / to ensure old cookies are cleared
|
||||
# after the change in path behavior (changed in notebook 5.2.2).
|
||||
# N.B. This bypasses the normal cookie handling, which can't update
|
||||
# two cookies with the same name. See the method above.
|
||||
self.force_clear_cookie(self.cookie_name)
|
||||
|
||||
def get_current_user(self):
|
||||
if self.login_handler is None:
|
||||
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.request.method == 'OPTIONS':
|
||||
# no origin-check on options requests, which are used to check origins!
|
||||
return True
|
||||
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(
|
||||
self.request.host
|
||||
))
|
||||
return self.settings.get('cookie_name', default_cookie_name)
|
||||
|
||||
@property
|
||||
def logged_in(self):
|
||||
"""Is a user currently logged in?"""
|
||||
user = self.get_current_user()
|
||||
return (user and not user == 'anonymous')
|
||||
|
||||
@property
|
||||
def login_handler(self):
|
||||
"""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 login_available(self):
|
||||
"""May a user proceed to log in?
|
||||
|
||||
This returns True if login capability is available, irrespective of
|
||||
whether the user is already logged in or not.
|
||||
|
||||
"""
|
||||
if self.login_handler is None:
|
||||
return False
|
||||
return bool(self.login_handler.get_login_available(self.settings))
|
||||
|
||||
|
||||
class IPythonHandler(AuthenticatedHandler):
|
||||
"""IPython-specific extensions to authenticated handling
|
||||
|
||||
Mostly property shortcuts to IPython-specific settings.
|
||||
"""
|
||||
|
||||
@property
|
||||
def ignore_minified_js(self):
|
||||
"""Wether to user bundle in template. (*.min files)
|
||||
|
||||
Mainly use for development and avoid file recompilation
|
||||
"""
|
||||
return self.settings.get('ignore_minified_js', False)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.settings.get('config', None)
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
"""use the IPython log by default, falling back on tornado's logger"""
|
||||
return log()
|
||||
|
||||
@property
|
||||
def jinja_template_vars(self):
|
||||
"""User-supplied values to supply to jinja templates."""
|
||||
return self.settings.get('jinja_template_vars', {})
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# URLs
|
||||
#---------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def version_hash(self):
|
||||
"""The version hash to use for cache hints for static files"""
|
||||
return self.settings.get('version_hash', '')
|
||||
|
||||
@property
|
||||
def mathjax_url(self):
|
||||
url = self.settings.get('mathjax_url', '')
|
||||
if not url or url_is_absolute(url):
|
||||
return url
|
||||
return url_path_join(self.base_url, url)
|
||||
|
||||
@property
|
||||
def mathjax_config(self):
|
||||
return self.settings.get('mathjax_config', 'TeX-AMS-MML_HTMLorMML-full,Safe')
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
return self.settings.get('base_url', '/')
|
||||
|
||||
@property
|
||||
def default_url(self):
|
||||
return self.settings.get('default_url', '')
|
||||
|
||||
@property
|
||||
def ws_url(self):
|
||||
return self.settings.get('websocket_url', '')
|
||||
|
||||
@property
|
||||
def contents_js_source(self):
|
||||
self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
|
||||
'services/contents'))
|
||||
return self.settings.get('contents_js_source', 'services/contents')
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# Manager objects
|
||||
#---------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def kernel_manager(self):
|
||||
return self.settings['kernel_manager']
|
||||
|
||||
@property
|
||||
def contents_manager(self):
|
||||
return self.settings['contents_manager']
|
||||
|
||||
@property
|
||||
def session_manager(self):
|
||||
return self.settings['session_manager']
|
||||
|
||||
@property
|
||||
def terminal_manager(self):
|
||||
return self.settings['terminal_manager']
|
||||
|
||||
@property
|
||||
def kernel_spec_manager(self):
|
||||
return self.settings['kernel_spec_manager']
|
||||
|
||||
@property
|
||||
def config_manager(self):
|
||||
return self.settings['config_manager']
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# CORS
|
||||
#---------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def allow_origin(self):
|
||||
"""Normal Access-Control-Allow-Origin"""
|
||||
return self.settings.get('allow_origin', '')
|
||||
|
||||
@property
|
||||
def allow_origin_pat(self):
|
||||
"""Regular expression version of allow_origin"""
|
||||
return self.settings.get('allow_origin_pat', None)
|
||||
|
||||
@property
|
||||
def allow_credentials(self):
|
||||
"""Whether to set Access-Control-Allow-Credentials"""
|
||||
return self.settings.get('allow_credentials', False)
|
||||
|
||||
def set_default_headers(self):
|
||||
"""Add CORS headers, if defined"""
|
||||
super().set_default_headers()
|
||||
if self.allow_origin:
|
||||
self.set_header("Access-Control-Allow-Origin", self.allow_origin)
|
||||
elif self.allow_origin_pat:
|
||||
origin = self.get_origin()
|
||||
if origin and self.allow_origin_pat.match(origin):
|
||||
self.set_header("Access-Control-Allow-Origin", origin)
|
||||
elif (
|
||||
self.token_authenticated
|
||||
and "Access-Control-Allow-Origin" not in
|
||||
self.settings.get('headers', {})
|
||||
):
|
||||
# allow token-authenticated requests cross-origin by default.
|
||||
# only apply this exception if allow-origin has not been specified.
|
||||
self.set_header('Access-Control-Allow-Origin',
|
||||
self.request.headers.get('Origin', ''))
|
||||
|
||||
if self.allow_credentials:
|
||||
self.set_header("Access-Control-Allow-Credentials", 'true')
|
||||
|
||||
def set_attachment_header(self, filename):
|
||||
"""Set Content-Disposition: attachment header
|
||||
|
||||
As a method to ensure handling of filename encoding
|
||||
"""
|
||||
escaped_filename = url_escape(filename)
|
||||
self.set_header('Content-Disposition',
|
||||
'attachment;'
|
||||
" filename*=utf-8''{utf8}"
|
||||
.format(
|
||||
utf8=escaped_filename,
|
||||
)
|
||||
)
|
||||
|
||||
def get_origin(self):
|
||||
# Handle WebSocket Origin naming convention differences
|
||||
# The difference between version 8 and 13 is that in 8 the
|
||||
# client sends a "Sec-Websocket-Origin" header and in 13 it's
|
||||
# simply "Origin".
|
||||
if "Origin" in self.request.headers:
|
||||
origin = self.request.headers.get("Origin")
|
||||
else:
|
||||
origin = self.request.headers.get("Sec-Websocket-Origin", None)
|
||||
return origin
|
||||
|
||||
# origin_to_satisfy_tornado is present because tornado requires
|
||||
# check_origin to take an origin argument, but we don't use it
|
||||
def check_origin(self, origin_to_satisfy_tornado=""):
|
||||
"""Check Origin for cross-site API requests, including websockets
|
||||
|
||||
Copied from WebSocket with changes:
|
||||
|
||||
- allow unspecified host/origin (e.g. scripts)
|
||||
- allow token-authenticated requests
|
||||
"""
|
||||
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, 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
|
||||
|
||||
origin = origin.lower()
|
||||
origin_host = urlparse(origin).netloc
|
||||
|
||||
# OK if origin matches host
|
||||
if origin_host == host:
|
||||
return True
|
||||
|
||||
# Check CORS headers
|
||||
if self.allow_origin:
|
||||
allow = self.allow_origin == origin
|
||||
elif self.allow_origin_pat:
|
||||
allow = bool(self.allow_origin_pat.match(origin))
|
||||
else:
|
||||
# No CORS headers deny the request
|
||||
allow = False
|
||||
if not allow:
|
||||
self.log.warning("Blocking Cross Origin API request for %s. Origin: %s, Host: %s",
|
||||
self.request.path, origin, host,
|
||||
)
|
||||
return allow
|
||||
|
||||
def check_referer(self):
|
||||
"""Check Referer for cross-site requests.
|
||||
|
||||
Disables requests to certain endpoints with
|
||||
external or missing Referer.
|
||||
|
||||
If set, allow_origin settings are applied to the Referer
|
||||
to whitelist specific cross-origin sites.
|
||||
|
||||
Used on GET for api endpoints and /files/
|
||||
to block cross-site inclusion (XSSI).
|
||||
"""
|
||||
|
||||
if self.allow_origin == "*" or self.skip_check_origin():
|
||||
return True
|
||||
|
||||
host = self.request.headers.get("Host")
|
||||
referer = self.request.headers.get("Referer")
|
||||
|
||||
if not host:
|
||||
self.log.warning("Blocking request with no host")
|
||||
return False
|
||||
if not referer:
|
||||
self.log.warning("Blocking request with no referer")
|
||||
return False
|
||||
|
||||
referer_url = urlparse(referer)
|
||||
referer_host = referer_url.netloc
|
||||
if referer_host == host:
|
||||
return True
|
||||
|
||||
# apply cross-origin checks to Referer:
|
||||
origin = "{}://{}".format(referer_url.scheme, referer_url.netloc)
|
||||
if self.allow_origin:
|
||||
allow = self.allow_origin == origin
|
||||
elif self.allow_origin_pat:
|
||||
allow = bool(self.allow_origin_pat.match(origin))
|
||||
else:
|
||||
# No CORS settings, deny the request
|
||||
allow = False
|
||||
|
||||
if not allow:
|
||||
self.log.warning("Blocking Cross Origin request for %s. Referer: %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
|
||||
try:
|
||||
return super().check_xsrf_cookie()
|
||||
except web.HTTPError as e:
|
||||
if self.request.method in {'GET', 'HEAD'}:
|
||||
# Consider Referer a sufficient cross-origin check for GET requests
|
||||
if not self.check_referer():
|
||||
referer = self.request.headers.get('Referer')
|
||||
if referer:
|
||||
msg = "Blocking Cross Origin request from {}.".format(referer)
|
||||
else:
|
||||
msg = "Blocking request from unknown origin"
|
||||
raise web.HTTPError(403, msg) from e
|
||||
else:
|
||||
raise
|
||||
|
||||
def check_host(self):
|
||||
"""Check the host header if remote access disallowed.
|
||||
|
||||
Returns True if the request should continue, False otherwise.
|
||||
"""
|
||||
if self.settings.get('allow_remote_access', False):
|
||||
return True
|
||||
|
||||
# Remove port (e.g. ':8888') from host
|
||||
host = re.match(r'^(.*?)(:\d+)?$', self.request.host).group(1)
|
||||
|
||||
# Browsers format IPv6 addresses like [::1]; we need to remove the []
|
||||
if host.startswith('[') and host.endswith(']'):
|
||||
host = host[1:-1]
|
||||
|
||||
# UNIX socket handling
|
||||
check_host = urldecode_unix_socket_path(host)
|
||||
if check_host.startswith('/') and os.path.exists(check_host):
|
||||
allow = True
|
||||
else:
|
||||
try:
|
||||
addr = ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
# Not an IP address: check against hostnames
|
||||
allow = host in self.settings.get('local_hostnames', ['localhost'])
|
||||
else:
|
||||
allow = addr.is_loopback
|
||||
|
||||
if not allow:
|
||||
self.log.warning(
|
||||
("Blocking request with non-local 'Host' %s (%s). "
|
||||
"If the notebook should be accessible at that name, "
|
||||
"set NotebookApp.allow_remote_access to disable the check."),
|
||||
host, self.request.host
|
||||
)
|
||||
return allow
|
||||
|
||||
def prepare(self):
|
||||
if not self.check_host():
|
||||
raise web.HTTPError(403)
|
||||
return super().prepare()
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# template rendering
|
||||
#---------------------------------------------------------------
|
||||
|
||||
def get_template(self, name):
|
||||
"""Return the jinja template object for a given name"""
|
||||
return self.settings['jinja2_env'].get_template(name)
|
||||
|
||||
def render_template(self, name, **ns):
|
||||
ns.update(self.template_namespace)
|
||||
template = self.get_template(name)
|
||||
return template.render(**ns)
|
||||
|
||||
@property
|
||||
def template_namespace(self):
|
||||
return dict(
|
||||
base_url=self.base_url,
|
||||
default_url=self.default_url,
|
||||
ws_url=self.ws_url,
|
||||
logged_in=self.logged_in,
|
||||
allow_password_change=self.settings.get('allow_password_change'),
|
||||
login_available=self.login_available,
|
||||
token_available=bool(self.token),
|
||||
static_url=self.static_url,
|
||||
sys_info=json_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'),
|
||||
nbjs_translations=json.dumps(combine_translations(
|
||||
self.request.headers.get('Accept-Language', ''))),
|
||||
**self.jinja_template_vars
|
||||
)
|
||||
|
||||
def get_json_body(self):
|
||||
"""Return the body of the request as JSON data."""
|
||||
if not self.request.body:
|
||||
return None
|
||||
# Do we need to call body.decode('utf-8') here?
|
||||
body = self.request.body.strip().decode(u'utf-8')
|
||||
try:
|
||||
model = json.loads(body)
|
||||
except Exception as e:
|
||||
self.log.debug("Bad JSON: %r", body)
|
||||
self.log.error("Couldn't parse JSON", exc_info=True)
|
||||
raise web.HTTPError(400, u'Invalid JSON in body of request') from e
|
||||
return model
|
||||
|
||||
def write_error(self, status_code, **kwargs):
|
||||
"""render custom error pages"""
|
||||
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
|
||||
try:
|
||||
message = exception.log_message % exception.args
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# construct the custom reason, if defined
|
||||
reason = getattr(exception, 'reason', '')
|
||||
if reason:
|
||||
status_message = reason
|
||||
|
||||
# build template namespace
|
||||
ns = dict(
|
||||
status_code=status_code,
|
||||
status_message=status_message,
|
||||
message=message,
|
||||
exception=exception,
|
||||
)
|
||||
|
||||
self.set_header('Content-Type', 'text/html')
|
||||
# render the template
|
||||
try:
|
||||
html = self.render_template('%s.html' % status_code, **ns)
|
||||
except TemplateNotFound:
|
||||
html = self.render_template('error.html', **ns)
|
||||
|
||||
self.write(html)
|
||||
|
||||
|
||||
class APIHandler(IPythonHandler):
|
||||
"""Base class for API handlers"""
|
||||
|
||||
def prepare(self):
|
||||
if not self.check_origin():
|
||||
raise web.HTTPError(404)
|
||||
return super().prepare()
|
||||
|
||||
def write_error(self, status_code, **kwargs):
|
||||
"""APIHandler errors are JSON, not human pages"""
|
||||
self.set_header('Content-Type', 'application/json')
|
||||
message = responses.get(status_code, 'Unknown HTTP Error')
|
||||
reply = {
|
||||
'message': message,
|
||||
}
|
||||
exc_info = kwargs.get('exc_info')
|
||||
if exc_info:
|
||||
e = exc_info[1]
|
||||
if isinstance(e, HTTPError):
|
||||
reply['message'] = e.log_message or message
|
||||
reply['reason'] = e.reason
|
||||
else:
|
||||
reply['message'] = 'Unhandled error'
|
||||
reply['reason'] = None
|
||||
reply['traceback'] = ''.join(traceback.format_exception(*exc_info))
|
||||
self.log.warning(reply['message'])
|
||||
self.finish(json.dumps(reply))
|
||||
|
||||
def get_current_user(self):
|
||||
"""Raise 403 on API handlers instead of redirecting to human login page"""
|
||||
# preserve _user_cache so we don't raise more than once
|
||||
if hasattr(self, '_user_cache'):
|
||||
return self._user_cache
|
||||
self._user_cache = user = super().get_current_user()
|
||||
return user
|
||||
|
||||
def get_login_url(self):
|
||||
# if get_login_url is invoked in an API handler,
|
||||
# that means @web.authenticated is trying to trigger a redirect.
|
||||
# instead of redirecting, raise 403 instead.
|
||||
if not self.current_user:
|
||||
raise web.HTTPError(403)
|
||||
return super().get_login_url()
|
||||
|
||||
@property
|
||||
def content_security_policy(self):
|
||||
csp = '; '.join([
|
||||
super().content_security_policy,
|
||||
"default-src 'none'",
|
||||
])
|
||||
return csp
|
||||
|
||||
# set _track_activity = False on API handlers that shouldn't track activity
|
||||
_track_activity = True
|
||||
|
||||
def update_api_activity(self):
|
||||
"""Update last_activity of API requests"""
|
||||
# record activity of authenticated requests
|
||||
if (
|
||||
self._track_activity
|
||||
and getattr(self, '_user_cache', None)
|
||||
and self.get_argument('no_track_activity', None) is None
|
||||
):
|
||||
self.settings['api_last_activity'] = utcnow()
|
||||
|
||||
def finish(self, *args, **kwargs):
|
||||
self.update_api_activity()
|
||||
self.set_header('Content-Type', 'application/json')
|
||||
return super().finish(*args, **kwargs)
|
||||
|
||||
def options(self, *args, **kwargs):
|
||||
if 'Access-Control-Allow-Headers' in self.settings.get('headers', {}):
|
||||
self.set_header('Access-Control-Allow-Headers', self.settings['headers']['Access-Control-Allow-Headers'])
|
||||
else:
|
||||
self.set_header('Access-Control-Allow-Headers',
|
||||
'accept, content-type, authorization, x-xsrftoken')
|
||||
self.set_header('Access-Control-Allow-Methods',
|
||||
'GET, PUT, POST, PATCH, DELETE, OPTIONS')
|
||||
|
||||
# if authorization header is requested,
|
||||
# that means the request is token-authenticated.
|
||||
# avoid browser-side rejection of the preflight request.
|
||||
# only allow this exception if allow_origin has not been specified
|
||||
# and notebook authentication is enabled.
|
||||
# If the token is not valid, the 'real' request will still be rejected.
|
||||
requested_headers = self.request.headers.get('Access-Control-Request-Headers', '').split(',')
|
||||
if requested_headers and any(
|
||||
h.strip().lower() == 'authorization'
|
||||
for h in requested_headers
|
||||
) and (
|
||||
# FIXME: it would be even better to check specifically for token-auth,
|
||||
# but there is currently no API for this.
|
||||
self.login_available
|
||||
) and (
|
||||
self.allow_origin
|
||||
or self.allow_origin_pat
|
||||
or 'Access-Control-Allow-Origin' in self.settings.get('headers', {})
|
||||
):
|
||||
self.set_header('Access-Control-Allow-Origin',
|
||||
self.request.headers.get('Origin', ''))
|
||||
|
||||
|
||||
class Template404(IPythonHandler):
|
||||
"""Render our 404 template"""
|
||||
def prepare(self):
|
||||
raise web.HTTPError(404)
|
||||
|
||||
|
||||
class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
|
||||
"""static files should only be accessible when logged in"""
|
||||
|
||||
@property
|
||||
def content_security_policy(self):
|
||||
# In case we're serving HTML/SVG, confine any Javascript to a unique
|
||||
# origin so it can't interact with the notebook server.
|
||||
return super().content_security_policy + "; sandbox allow-scripts"
|
||||
|
||||
@web.authenticated
|
||||
def head(self, path):
|
||||
self.check_xsrf_cookie()
|
||||
return super().head(path)
|
||||
|
||||
@web.authenticated
|
||||
def get(self, path):
|
||||
self.check_xsrf_cookie()
|
||||
|
||||
if os.path.splitext(path)[1] == '.ipynb' or self.get_argument("download", False):
|
||||
name = path.rsplit('/', 1)[-1]
|
||||
self.set_attachment_header(name)
|
||||
|
||||
return web.StaticFileHandler.get(self, path)
|
||||
|
||||
def get_content_type(self):
|
||||
path = self.absolute_path.strip('/')
|
||||
if '/' in path:
|
||||
_, name = path.rsplit('/', 1)
|
||||
else:
|
||||
name = path
|
||||
if name.endswith('.ipynb'):
|
||||
return 'application/x-ipynb+json'
|
||||
else:
|
||||
cur_mime = mimetypes.guess_type(name)[0]
|
||||
if cur_mime == 'text/plain':
|
||||
return 'text/plain; charset=UTF-8'
|
||||
else:
|
||||
return super().get_content_type()
|
||||
|
||||
def set_headers(self):
|
||||
super().set_headers()
|
||||
# disable browser caching, rely on 304 replies for savings
|
||||
if "v" not in self.request.arguments:
|
||||
self.add_header("Cache-Control", "no-cache")
|
||||
|
||||
def compute_etag(self):
|
||||
return None
|
||||
|
||||
def validate_absolute_path(self, root, absolute_path):
|
||||
"""Validate and return the absolute path.
|
||||
|
||||
Requires tornado 3.1
|
||||
|
||||
Adding to tornado's own handling, forbids the serving of hidden files.
|
||||
"""
|
||||
abs_path = super().validate_absolute_path(root, absolute_path)
|
||||
abs_root = os.path.abspath(root)
|
||||
if is_hidden(abs_path, abs_root) and not self.contents_manager.allow_hidden:
|
||||
self.log.info("Refusing to serve hidden file, via 404 Error, use flag 'ContentsManager.allow_hidden' to enable")
|
||||
raise web.HTTPError(404)
|
||||
return abs_path
|
||||
|
||||
|
||||
def json_errors(method):
|
||||
"""Decorate methods with this to return GitHub style JSON errors.
|
||||
|
||||
This should be used on any JSON API on any handler method that can raise HTTPErrors.
|
||||
|
||||
This will grab the latest HTTPError exception using sys.exc_info
|
||||
and then:
|
||||
|
||||
1. Set the HTTP status code based on the HTTPError
|
||||
2. Create and return a JSON body with a message field describing
|
||||
the error in a human readable form.
|
||||
"""
|
||||
warnings.warn('@json_errors is deprecated in notebook 5.2.0. Subclass APIHandler instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
@functools.wraps(method)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
self.write_error = types.MethodType(APIHandler.write_error, self)
|
||||
return method(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# File handler
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
# to minimize subclass changes:
|
||||
HTTPError = web.HTTPError
|
||||
|
||||
class FileFindHandler(IPythonHandler, web.StaticFileHandler):
|
||||
"""subclass of StaticFileHandler for serving files from a search path"""
|
||||
|
||||
# cache search results, don't search for files more than once
|
||||
_static_paths = {}
|
||||
|
||||
def set_headers(self):
|
||||
super().set_headers()
|
||||
# disable browser caching, rely on 304 replies for savings
|
||||
if "v" not in self.request.arguments or \
|
||||
any(self.request.path.startswith(path) for path in self.no_cache_paths):
|
||||
self.set_header("Cache-Control", "no-cache")
|
||||
|
||||
def initialize(self, path, default_filename=None, no_cache_paths=None):
|
||||
self.no_cache_paths = no_cache_paths or []
|
||||
|
||||
if isinstance(path, string_types):
|
||||
path = [path]
|
||||
|
||||
self.root = tuple(
|
||||
os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
|
||||
)
|
||||
self.default_filename = default_filename
|
||||
|
||||
def compute_etag(self):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_absolute_path(cls, roots, path):
|
||||
"""locate a file to serve on our static file search path"""
|
||||
with cls._lock:
|
||||
if path in cls._static_paths:
|
||||
return cls._static_paths[path]
|
||||
try:
|
||||
abspath = os.path.abspath(filefind(path, roots))
|
||||
except IOError:
|
||||
# IOError means not found
|
||||
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):
|
||||
"""check if the file should be served (raises 404, 403, etc.)"""
|
||||
if absolute_path == '':
|
||||
raise web.HTTPError(404)
|
||||
|
||||
for root in self.root:
|
||||
if (absolute_path + os.sep).startswith(root):
|
||||
break
|
||||
|
||||
return super().validate_absolute_path(root, absolute_path)
|
||||
|
||||
|
||||
class APIVersionHandler(APIHandler):
|
||||
|
||||
def get(self):
|
||||
# not authenticated, so give as few info as possible
|
||||
self.finish(json.dumps({"version":notebook.__version__}))
|
||||
|
||||
|
||||
class TrailingSlashHandler(web.RequestHandler):
|
||||
"""Simple redirect handler that strips trailing slashes
|
||||
|
||||
This should be the first, highest priority handler.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
path, *rest = self.request.uri.partition("?")
|
||||
# trim trailing *and* leading /
|
||||
# to avoid misinterpreting repeated '//'
|
||||
path = "/" + path.strip("/")
|
||||
new_uri = "".join([path, *rest])
|
||||
self.redirect(new_uri)
|
||||
|
||||
post = put = get
|
||||
|
||||
|
||||
class FilesRedirectHandler(IPythonHandler):
|
||||
"""Handler for redirecting relative URLs to the /files/ handler"""
|
||||
|
||||
@staticmethod
|
||||
def redirect_to_files(self, path):
|
||||
"""make redirect logic a reusable static method
|
||||
|
||||
so it can be called from other handlers.
|
||||
"""
|
||||
cm = self.contents_manager
|
||||
if cm.dir_exists(path):
|
||||
# it's a *directory*, redirect to /tree
|
||||
url = url_path_join(self.base_url, 'tree', url_escape(path))
|
||||
else:
|
||||
orig_path = path
|
||||
# otherwise, redirect to /files
|
||||
parts = path.split('/')
|
||||
|
||||
if not cm.file_exists(path=path) and 'files' in parts:
|
||||
# redirect without files/ iff it would 404
|
||||
# this preserves pre-2.0-style 'files/' links
|
||||
self.log.warning("Deprecated files/ URL: %s", orig_path)
|
||||
parts.remove('files')
|
||||
path = '/'.join(parts)
|
||||
|
||||
if not cm.file_exists(path=path):
|
||||
raise web.HTTPError(404)
|
||||
|
||||
url = url_path_join(self.base_url, 'files', url_escape(path))
|
||||
self.log.debug("Redirecting %s to %s", self.request.path, url)
|
||||
self.redirect(url)
|
||||
|
||||
def get(self, path=''):
|
||||
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)
|
||||
|
||||
|
||||
class PrometheusMetricsHandler(IPythonHandler):
|
||||
"""
|
||||
Return prometheus metrics for this notebook server
|
||||
"""
|
||||
def get(self):
|
||||
if self.settings['authenticate_prometheus'] and not self.logged_in:
|
||||
raise web.HTTPError(403)
|
||||
|
||||
self.set_header('Content-Type', prometheus_client.CONTENT_TYPE_LATEST)
|
||||
self.write(prometheus_client.generate_latest(prometheus_client.REGISTRY))
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# URL pattern fragments for re-use
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
# path matches any number of `/foo[/bar...]` or just `/` or ''
|
||||
path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# URL to handler mappings
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
default_handlers = [
|
||||
(r".*/", TrailingSlashHandler),
|
||||
(r"api", APIVersionHandler),
|
||||
(r'/(robots\.txt|favicon\.ico)', web.StaticFileHandler),
|
||||
(r'/metrics', PrometheusMetricsHandler)
|
||||
]
|
||||
@ -1,302 +0,0 @@
|
||||
"""Tornado handlers for WebSocket <-> ZMQ sockets."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import tornado
|
||||
from tornado import gen, ioloop, web
|
||||
from tornado.iostream import StreamClosedError
|
||||
from tornado.websocket import WebSocketHandler, WebSocketClosedError
|
||||
|
||||
from jupyter_client.session import Session
|
||||
try:
|
||||
from jupyter_client.jsonutil import json_default, extract_dates
|
||||
except ImportError:
|
||||
from jupyter_client.jsonutil import (
|
||||
date_default as json_default, extract_dates
|
||||
)
|
||||
from ipython_genutils.py3compat import cast_unicode
|
||||
|
||||
from notebook.utils import maybe_future
|
||||
from .handlers import IPythonHandler
|
||||
|
||||
|
||||
def serialize_binary_message(msg):
|
||||
"""serialize a message as a binary blob
|
||||
|
||||
Header:
|
||||
|
||||
4 bytes: number of msg parts (nbufs) as 32b int
|
||||
4 * nbufs bytes: offset for each buffer as integer as 32b int
|
||||
|
||||
Offsets are from the start of the buffer, including the header.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
The message serialized to bytes.
|
||||
|
||||
"""
|
||||
# don't modify msg or buffer list in-place
|
||||
msg = msg.copy()
|
||||
buffers = list(msg.pop('buffers'))
|
||||
bmsg = json.dumps(msg, default=json_default).encode('utf8')
|
||||
buffers.insert(0, bmsg)
|
||||
nbufs = len(buffers)
|
||||
offsets = [4 * (nbufs + 1)]
|
||||
for buf in buffers[:-1]:
|
||||
offsets.append(offsets[-1] + len(buf))
|
||||
offsets_buf = struct.pack('!' + 'I' * (nbufs + 1), nbufs, *offsets)
|
||||
buffers.insert(0, offsets_buf)
|
||||
return b''.join(buffers)
|
||||
|
||||
|
||||
def deserialize_binary_message(bmsg):
|
||||
"""deserialize a message from a binary blog
|
||||
|
||||
Header:
|
||||
|
||||
4 bytes: number of msg parts (nbufs) as 32b int
|
||||
4 * nbufs bytes: offset for each buffer as integer as 32b int
|
||||
|
||||
Offsets are from the start of the buffer, including the header.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
message dictionary
|
||||
"""
|
||||
nbufs = struct.unpack('!i', bmsg[:4])[0]
|
||||
offsets = list(struct.unpack('!' + 'I' * nbufs, bmsg[4:4*(nbufs+1)]))
|
||||
offsets.append(None)
|
||||
bufs = []
|
||||
for start, stop in zip(offsets[:-1], offsets[1:]):
|
||||
bufs.append(bmsg[start:stop])
|
||||
msg = json.loads(bufs[0].decode('utf8'))
|
||||
msg['header'] = extract_dates(msg['header'])
|
||||
msg['parent_header'] = extract_dates(msg['parent_header'])
|
||||
msg['buffers'] = bufs[1:]
|
||||
return msg
|
||||
|
||||
# ping interval for keeping websockets alive (30 seconds)
|
||||
WS_PING_INTERVAL = 30000
|
||||
|
||||
|
||||
class WebSocketMixin(object):
|
||||
"""Mixin for common websocket options"""
|
||||
ping_callback = None
|
||||
last_ping = 0
|
||||
last_pong = 0
|
||||
stream = None
|
||||
|
||||
@property
|
||||
def ping_interval(self):
|
||||
"""The interval for websocket keep-alive pings.
|
||||
|
||||
Set ws_ping_interval = 0 to disable pings.
|
||||
"""
|
||||
return self.settings.get('ws_ping_interval', WS_PING_INTERVAL)
|
||||
|
||||
@property
|
||||
def ping_timeout(self):
|
||||
"""If no ping is received in this many milliseconds,
|
||||
close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
|
||||
Default is max of 3 pings or 30 seconds.
|
||||
"""
|
||||
return self.settings.get('ws_ping_timeout',
|
||||
max(3 * self.ping_interval, WS_PING_INTERVAL)
|
||||
)
|
||||
|
||||
def check_origin(self, origin=None):
|
||||
"""Check Origin == Host or Access-Control-Allow-Origin.
|
||||
|
||||
Tornado >= 4 calls this method automatically, raising 403 if it returns False.
|
||||
"""
|
||||
|
||||
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 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
|
||||
|
||||
# OK if origin matches host
|
||||
if origin_host == host:
|
||||
return True
|
||||
|
||||
# Check CORS headers
|
||||
if self.allow_origin:
|
||||
allow = self.allow_origin == origin
|
||||
elif self.allow_origin_pat:
|
||||
allow = bool(self.allow_origin_pat.match(origin))
|
||||
else:
|
||||
# No CORS headers deny the request
|
||||
allow = False
|
||||
if not allow:
|
||||
self.log.warning("Blocking Cross Origin WebSocket Attempt. Origin: %s, Host: %s",
|
||||
origin, host,
|
||||
)
|
||||
return allow
|
||||
|
||||
def clear_cookie(self, *args, **kwargs):
|
||||
"""meaningless for websockets"""
|
||||
pass
|
||||
|
||||
def open(self, *args, **kwargs):
|
||||
self.log.debug("Opening websocket %s", self.request.path)
|
||||
|
||||
# start the pinging
|
||||
if self.ping_interval > 0:
|
||||
loop = ioloop.IOLoop.current()
|
||||
self.last_ping = loop.time() # Remember time of last ping
|
||||
self.last_pong = self.last_ping
|
||||
self.ping_callback = ioloop.PeriodicCallback(
|
||||
self.send_ping, self.ping_interval,
|
||||
)
|
||||
self.ping_callback.start()
|
||||
return super(WebSocketMixin, self).open(*args, **kwargs)
|
||||
|
||||
def send_ping(self):
|
||||
"""send a ping to keep the websocket alive"""
|
||||
if self.ws_connection is None and self.ping_callback is not None:
|
||||
self.ping_callback.stop()
|
||||
return
|
||||
|
||||
# check for timeout on pong. Make sure that we really have sent a recent ping in
|
||||
# case the machine with both server and client has been suspended since the last ping.
|
||||
now = ioloop.IOLoop.current().time()
|
||||
since_last_pong = 1e3 * (now - self.last_pong)
|
||||
since_last_ping = 1e3 * (now - self.last_ping)
|
||||
if since_last_ping < 2*self.ping_interval and since_last_pong > self.ping_timeout:
|
||||
self.log.warning("WebSocket ping timeout after %i ms.", since_last_pong)
|
||||
self.close()
|
||||
return
|
||||
try:
|
||||
self.ping(b'')
|
||||
except (StreamClosedError, WebSocketClosedError):
|
||||
# websocket has been closed, stop pinging
|
||||
self.ping_callback.stop()
|
||||
return
|
||||
|
||||
self.last_ping = now
|
||||
|
||||
def on_pong(self, data):
|
||||
self.last_pong = ioloop.IOLoop.current().time()
|
||||
|
||||
|
||||
class ZMQStreamHandler(WebSocketMixin, WebSocketHandler):
|
||||
|
||||
if tornado.version_info < (4,1):
|
||||
"""Backport send_error from tornado 4.1 to 4.0"""
|
||||
def send_error(self, *args, **kwargs):
|
||||
if self.stream is None:
|
||||
super(WebSocketHandler, self).send_error(*args, **kwargs)
|
||||
else:
|
||||
# If we get an uncaught exception during the handshake,
|
||||
# we have no choice but to abruptly close the connection.
|
||||
# TODO: for uncaught exceptions after the handshake,
|
||||
# we can close the connection more gracefully.
|
||||
self.stream.close()
|
||||
|
||||
|
||||
def _reserialize_reply(self, msg_or_list, channel=None):
|
||||
"""Reserialize a reply message using JSON.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
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']:
|
||||
buf = serialize_binary_message(msg)
|
||||
return buf
|
||||
else:
|
||||
smsg = json.dumps(msg, default=json_default)
|
||||
return cast_unicode(smsg)
|
||||
|
||||
def _on_zmq_reply(self, stream, msg_list):
|
||||
# Sometimes this gets triggered when the on_close method is scheduled in the
|
||||
# eventloop but hasn't been called.
|
||||
if self.ws_connection is None or stream.closed():
|
||||
self.log.warning("zmq message arrived on closed channel")
|
||||
self.close()
|
||||
return
|
||||
channel = getattr(stream, 'channel', None)
|
||||
try:
|
||||
msg = self._reserialize_reply(msg_list, channel=channel)
|
||||
except Exception:
|
||||
self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
|
||||
return
|
||||
|
||||
try:
|
||||
self.write_message(msg, binary=isinstance(msg, bytes))
|
||||
except (StreamClosedError, WebSocketClosedError):
|
||||
self.log.warning("zmq message arrived on closed channel")
|
||||
self.close()
|
||||
return
|
||||
|
||||
|
||||
class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
|
||||
|
||||
def set_default_headers(self):
|
||||
"""Undo the set_default_headers in IPythonHandler
|
||||
|
||||
which doesn't make sense for websockets
|
||||
"""
|
||||
pass
|
||||
|
||||
def pre_get(self):
|
||||
"""Run before finishing the GET request
|
||||
|
||||
Extend this method to add logic that should fire before
|
||||
the websocket finishes completing.
|
||||
"""
|
||||
# authenticate the request before opening the websocket
|
||||
if self.get_current_user() is None:
|
||||
self.log.warning("Couldn't authenticate WebSocket connection")
|
||||
raise web.HTTPError(403)
|
||||
|
||||
if self.get_argument('session_id', False):
|
||||
self.session.session = cast_unicode(self.get_argument('session_id'))
|
||||
else:
|
||||
self.log.warning("No session ID specified")
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, *args, **kwargs):
|
||||
# pre_get can be a coroutine in subclasses
|
||||
# assign and yield in two step to avoid tornado 3 issues
|
||||
res = self.pre_get()
|
||||
yield maybe_future(res)
|
||||
res = super().get(*args, **kwargs)
|
||||
yield maybe_future(res)
|
||||
|
||||
def initialize(self):
|
||||
self.log.debug("Initializing websocket connection %s", self.request.path)
|
||||
self.session = Session(config=self.config)
|
||||
|
||||
def get_compression_options(self):
|
||||
return self.settings.get('websocket_compression_options', None)
|
||||
@ -1,7 +0,0 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from .bundlerextensions import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,307 +0,0 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import sys
|
||||
import os
|
||||
|
||||
from ..extensions import BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED
|
||||
from .._version import __version__
|
||||
from notebook.config_manager import BaseJSONConfigManager
|
||||
|
||||
from jupyter_core.paths import jupyter_config_path
|
||||
|
||||
from traitlets.utils.importstring import import_item
|
||||
from traitlets import Bool
|
||||
|
||||
BUNDLER_SECTION = "notebook"
|
||||
BUNDLER_SUBSECTION = "bundlerextensions"
|
||||
|
||||
def _get_bundler_metadata(module):
|
||||
"""Gets the list of bundlers associated with a Python package.
|
||||
|
||||
Returns a tuple of (the module, [{
|
||||
'name': 'unique name of the bundler',
|
||||
'label': 'file menu item label for the bundler',
|
||||
'module_name': 'dotted package/module name containing the bundler',
|
||||
'group': 'download or deploy parent menu item'
|
||||
}])
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
module : str
|
||||
Importable Python module exposing the
|
||||
magic-named `_jupyter_bundlerextension_paths` function
|
||||
"""
|
||||
m = import_item(module)
|
||||
if not hasattr(m, '_jupyter_bundlerextension_paths'):
|
||||
raise KeyError('The Python module {} does not contain a valid bundlerextension'.format(module))
|
||||
bundlers = m._jupyter_bundlerextension_paths()
|
||||
return m, bundlers
|
||||
|
||||
def _set_bundler_state(name, label, module_name, group, state,
|
||||
user=True, sys_prefix=False, logger=None):
|
||||
"""Set whether a bundler is enabled or disabled.
|
||||
|
||||
Returns True if the final state is the one requested.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
Unique name of the bundler
|
||||
label : string
|
||||
Human-readable label for the bundler menu item in the notebook UI
|
||||
module_name : string
|
||||
Dotted module/package name containing the bundler
|
||||
group : string
|
||||
'download' or 'deploy' indicating the parent menu containing the label
|
||||
state : bool
|
||||
The state in which to leave the extension
|
||||
user : bool [default: True]
|
||||
Whether to update the user's .jupyter/nbconfig directory
|
||||
sys_prefix : bool [default: False]
|
||||
Whether to update the sys.prefix, i.e. environment. Will override
|
||||
`user`.
|
||||
logger : Jupyter logger [optional]
|
||||
Logger instance to use
|
||||
"""
|
||||
user = False if sys_prefix else user
|
||||
config_dir = os.path.join(
|
||||
_get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig')
|
||||
cm = BaseJSONConfigManager(config_dir=config_dir)
|
||||
|
||||
if logger:
|
||||
logger.info("{} {} bundler {}...".format(
|
||||
"Enabling" if state else "Disabling",
|
||||
name,
|
||||
module_name
|
||||
))
|
||||
|
||||
if state:
|
||||
cm.update(BUNDLER_SECTION, {
|
||||
BUNDLER_SUBSECTION: {
|
||||
name: {
|
||||
"label": label,
|
||||
"module_name": module_name,
|
||||
"group" : group
|
||||
}
|
||||
}
|
||||
})
|
||||
else:
|
||||
cm.update(BUNDLER_SECTION, {
|
||||
BUNDLER_SUBSECTION: {
|
||||
name: None
|
||||
}
|
||||
})
|
||||
|
||||
return (cm.get(BUNDLER_SECTION)
|
||||
.get(BUNDLER_SUBSECTION, {})
|
||||
.get(name) is not None) == state
|
||||
|
||||
|
||||
def _set_bundler_state_python(state, module, user, sys_prefix, logger=None):
|
||||
"""Enables or disables bundlers defined in a Python package.
|
||||
|
||||
Returns a list of whether the state was achieved for each bundler.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : Bool
|
||||
Whether the extensions should be enabled
|
||||
module : str
|
||||
Importable Python module exposing the
|
||||
magic-named `_jupyter_bundlerextension_paths` function
|
||||
user : bool
|
||||
Whether to enable in the user's nbconfig directory.
|
||||
sys_prefix : bool
|
||||
Enable/disable in the sys.prefix, i.e. environment
|
||||
logger : Jupyter logger [optional]
|
||||
Logger instance to use
|
||||
"""
|
||||
m, bundlers = _get_bundler_metadata(module)
|
||||
return [_set_bundler_state(name=bundler["name"],
|
||||
label=bundler["label"],
|
||||
module_name=bundler["module_name"],
|
||||
group=bundler["group"],
|
||||
state=state,
|
||||
user=user, sys_prefix=sys_prefix,
|
||||
logger=logger)
|
||||
for bundler in bundlers]
|
||||
|
||||
def enable_bundler_python(module, user=True, sys_prefix=False, logger=None):
|
||||
"""Enables bundlers defined in a Python package.
|
||||
|
||||
Returns whether each bundle defined in the packaged was enabled or not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module : str
|
||||
Importable Python module exposing the
|
||||
magic-named `_jupyter_bundlerextension_paths` function
|
||||
user : bool [default: True]
|
||||
Whether to enable in the user's nbconfig directory.
|
||||
sys_prefix : bool [default: False]
|
||||
Whether to enable in the sys.prefix, i.e. environment. Will override
|
||||
`user`
|
||||
logger : Jupyter logger [optional]
|
||||
Logger instance to use
|
||||
"""
|
||||
return _set_bundler_state_python(True, module, user, sys_prefix,
|
||||
logger=logger)
|
||||
|
||||
def disable_bundler_python(module, user=True, sys_prefix=False, logger=None):
|
||||
"""Disables bundlers defined in a Python package.
|
||||
|
||||
Returns whether each bundle defined in the packaged was enabled or not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module : str
|
||||
Importable Python module exposing the
|
||||
magic-named `_jupyter_bundlerextension_paths` function
|
||||
user : bool [default: True]
|
||||
Whether to enable in the user's nbconfig directory.
|
||||
sys_prefix : bool [default: False]
|
||||
Whether to enable in the sys.prefix, i.e. environment. Will override
|
||||
`user`
|
||||
logger : Jupyter logger [optional]
|
||||
Logger instance to use
|
||||
"""
|
||||
return _set_bundler_state_python(False, module, user, sys_prefix,
|
||||
logger=logger)
|
||||
|
||||
class ToggleBundlerExtensionApp(BaseExtensionApp):
|
||||
"""A base class for apps that enable/disable bundlerextensions"""
|
||||
name = "jupyter bundlerextension enable/disable"
|
||||
version = __version__
|
||||
description = "Enable/disable a bundlerextension in configuration."
|
||||
|
||||
user = Bool(True, config=True, help="Apply the configuration only for the current user (default)")
|
||||
|
||||
_toggle_value = None
|
||||
|
||||
def _config_file_name_default(self):
|
||||
"""The default config file name."""
|
||||
return 'jupyter_notebook_config'
|
||||
|
||||
def toggle_bundler_python(self, module):
|
||||
"""Toggle some extensions in an importable Python module.
|
||||
|
||||
Returns a list of booleans indicating whether the state was changed as
|
||||
requested.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module : str
|
||||
Importable Python module exposing the
|
||||
magic-named `_jupyter_bundlerextension_paths` function
|
||||
"""
|
||||
toggle = (enable_bundler_python if self._toggle_value
|
||||
else disable_bundler_python)
|
||||
return toggle(module,
|
||||
user=self.user,
|
||||
sys_prefix=self.sys_prefix,
|
||||
logger=self.log)
|
||||
|
||||
def start(self):
|
||||
if not self.extra_args:
|
||||
sys.exit('Please specify an bundlerextension/package to enable or disable')
|
||||
elif len(self.extra_args) > 1:
|
||||
sys.exit('Please specify one bundlerextension/package at a time')
|
||||
if self.python:
|
||||
self.toggle_bundler_python(self.extra_args[0])
|
||||
else:
|
||||
raise NotImplementedError('Cannot install bundlers from non-Python packages')
|
||||
|
||||
class EnableBundlerExtensionApp(ToggleBundlerExtensionApp):
|
||||
"""An App that enables bundlerextensions"""
|
||||
name = "jupyter bundlerextension enable"
|
||||
description = """
|
||||
Enable a bundlerextension in frontend configuration.
|
||||
|
||||
Usage
|
||||
jupyter bundlerextension enable [--system|--sys-prefix]
|
||||
"""
|
||||
_toggle_value = True
|
||||
|
||||
class DisableBundlerExtensionApp(ToggleBundlerExtensionApp):
|
||||
"""An App that disables bundlerextensions"""
|
||||
name = "jupyter bundlerextension disable"
|
||||
description = """
|
||||
Disable a bundlerextension in frontend configuration.
|
||||
|
||||
Usage
|
||||
jupyter bundlerextension disable [--system|--sys-prefix]
|
||||
"""
|
||||
_toggle_value = None
|
||||
|
||||
|
||||
class ListBundlerExtensionApp(BaseExtensionApp):
|
||||
"""An App that lists and validates nbextensions"""
|
||||
name = "jupyter nbextension list"
|
||||
version = __version__
|
||||
description = "List all nbextensions known by the configuration system"
|
||||
|
||||
def list_nbextensions(self):
|
||||
"""List all the nbextensions"""
|
||||
config_dirs = [os.path.join(p, 'nbconfig') for p in jupyter_config_path()]
|
||||
|
||||
print("Known bundlerextensions:")
|
||||
|
||||
for config_dir in config_dirs:
|
||||
head = u' config dir: {}'.format(config_dir)
|
||||
head_shown = False
|
||||
|
||||
cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
|
||||
data = cm.get('notebook')
|
||||
if 'bundlerextensions' in data:
|
||||
if not head_shown:
|
||||
# only show heading if there is an nbextension here
|
||||
print(head)
|
||||
head_shown = True
|
||||
|
||||
for bundler_id, info in data['bundlerextensions'].items():
|
||||
label = info.get('label')
|
||||
module = info.get('module_name')
|
||||
if label is None or module is None:
|
||||
msg = u' {} {}'.format(bundler_id, RED_DISABLED)
|
||||
else:
|
||||
msg = u' "{}" from {} {}'.format(
|
||||
label, module, GREEN_ENABLED
|
||||
)
|
||||
print(msg)
|
||||
|
||||
def start(self):
|
||||
"""Perform the App's functions as configured"""
|
||||
self.list_nbextensions()
|
||||
|
||||
|
||||
class BundlerExtensionApp(BaseExtensionApp):
|
||||
"""Base jupyter bundlerextension command entry point"""
|
||||
name = "jupyter bundlerextension"
|
||||
version = __version__
|
||||
description = "Work with Jupyter bundler extensions"
|
||||
examples = """
|
||||
jupyter bundlerextension list # list all configured bundlers
|
||||
jupyter bundlerextension enable --py <packagename> # enable all bundlers in a Python package
|
||||
jupyter bundlerextension disable --py <packagename> # disable all bundlers in a Python package
|
||||
"""
|
||||
|
||||
subcommands = dict(
|
||||
enable=(EnableBundlerExtensionApp, "Enable a bundler extension"),
|
||||
disable=(DisableBundlerExtensionApp, "Disable a bundler extension"),
|
||||
list=(ListBundlerExtensionApp, "List bundler extensions")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Perform the App's functions as configured"""
|
||||
super().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 = BundlerExtensionApp.launch_instance
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,88 +0,0 @@
|
||||
"""Tornado handler for bundling notebooks."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from ipython_genutils.importstring import import_item
|
||||
from tornado import web, gen
|
||||
|
||||
from notebook.utils import maybe_future, url2path
|
||||
from notebook.base.handlers import IPythonHandler
|
||||
from notebook.services.config import ConfigManager
|
||||
|
||||
from . import tools
|
||||
|
||||
|
||||
class BundlerHandler(IPythonHandler):
|
||||
def initialize(self):
|
||||
"""Make tools module available on the handler instance for compatibility
|
||||
with existing bundler API and ease of reference."""
|
||||
self.tools = tools
|
||||
|
||||
def get_bundler(self, bundler_id):
|
||||
"""
|
||||
Get bundler metadata from config given a bundler ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bundler_id: str
|
||||
Unique bundler ID within the notebook/bundlerextensions config section
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Bundler metadata with label, group, and module_name attributes
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If the bundler ID is unknown
|
||||
"""
|
||||
cm = ConfigManager()
|
||||
return cm.get('notebook').get('bundlerextensions', {})[bundler_id]
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def get(self, path):
|
||||
"""Bundle the given notebook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path: str
|
||||
Path to the notebook (path parameter)
|
||||
bundler: str
|
||||
Bundler ID to use (query parameter)
|
||||
"""
|
||||
bundler_id = self.get_query_argument('bundler')
|
||||
model = self.contents_manager.get(path=url2path(path))
|
||||
|
||||
try:
|
||||
bundler = self.get_bundler(bundler_id)
|
||||
except KeyError as e:
|
||||
raise web.HTTPError(400, 'Bundler %s not enabled' %
|
||||
bundler_id) from e
|
||||
|
||||
module_name = bundler['module_name']
|
||||
try:
|
||||
# no-op in python3, decode error in python2
|
||||
module_name = str(module_name)
|
||||
except UnicodeEncodeError:
|
||||
# Encode unicode as utf-8 in python2 else import_item fails
|
||||
module_name = module_name.encode('utf-8')
|
||||
|
||||
try:
|
||||
bundler_mod = import_item(module_name)
|
||||
except ImportError as e:
|
||||
raise web.HTTPError(500, 'Could not import bundler %s ' %
|
||||
bundler_id) from e
|
||||
|
||||
# Let the bundler respond in any way it sees fit and assume it will
|
||||
# finish the request
|
||||
yield maybe_future(bundler_mod.bundle(self, model))
|
||||
|
||||
_bundler_id_regex = r'(?P<bundler_id>[A-Za-z0-9_]+)'
|
||||
|
||||
default_handlers = [
|
||||
(r"/bundle/(.*)", BundlerHandler)
|
||||
]
|
||||
@ -1,47 +0,0 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import os
|
||||
import io
|
||||
import tarfile
|
||||
import nbformat
|
||||
|
||||
def _jupyter_bundlerextension_paths():
|
||||
"""Metadata for notebook bundlerextension"""
|
||||
return [{
|
||||
# unique bundler name
|
||||
"name": "tarball_bundler",
|
||||
# module containing bundle function
|
||||
"module_name": "notebook.bundler.tarball_bundler",
|
||||
# human-readable menu item label
|
||||
"label" : "Notebook Tarball (tar.gz)",
|
||||
# group under 'deploy' or 'download' menu
|
||||
"group" : "download",
|
||||
}]
|
||||
|
||||
def bundle(handler, model):
|
||||
"""Create a compressed tarball containing the notebook document.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
handler : tornado.web.RequestHandler
|
||||
Handler that serviced the bundle request
|
||||
model : dict
|
||||
Notebook model from the configured ContentManager
|
||||
"""
|
||||
notebook_filename = model['name']
|
||||
notebook_content = nbformat.writes(model['content']).encode('utf-8')
|
||||
notebook_name = os.path.splitext(notebook_filename)[0]
|
||||
tar_filename = '{}.tar.gz'.format(notebook_name)
|
||||
|
||||
info = tarfile.TarInfo(notebook_filename)
|
||||
info.size = len(notebook_content)
|
||||
|
||||
with io.BytesIO() as tar_buffer:
|
||||
with tarfile.open(tar_filename, "w:gz", fileobj=tar_buffer) as tar:
|
||||
tar.addfile(info, io.BytesIO(notebook_content))
|
||||
|
||||
handler.set_attachment_header(tar_filename)
|
||||
handler.set_header('Content-Type', 'application/gzip')
|
||||
|
||||
# Return the buffer value as the response
|
||||
handler.finish(tar_buffer.getvalue())
|
||||
@ -1 +0,0 @@
|
||||
Used to test globbing.
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"nbformat_minor": 0,
|
||||
"cells": [],
|
||||
"nbformat": 4,
|
||||
"metadata": {}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
Used to test globbing.
|
||||
@ -1,80 +0,0 @@
|
||||
"""Test the bundlers API."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import io
|
||||
from os.path import join as pjoin
|
||||
|
||||
from notebook.tests.launchnotebook import NotebookTestBase
|
||||
from nbformat import write
|
||||
from nbformat.v4 import (
|
||||
new_notebook, new_markdown_cell, new_code_cell, new_output,
|
||||
)
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def bundle(handler, model):
|
||||
"""Bundler test stub. Echo the notebook path."""
|
||||
handler.finish(model['path'])
|
||||
|
||||
class BundleAPITest(NotebookTestBase):
|
||||
"""Test the bundlers web service API"""
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Make a test notebook. Borrowed from nbconvert test. Assumes the class
|
||||
teardown will clean it up in the end."""
|
||||
super(BundleAPITest, cls).setup_class()
|
||||
nbdir = cls.notebook_dir
|
||||
|
||||
nb = new_notebook()
|
||||
|
||||
nb.cells.append(new_markdown_cell(u'Created by test'))
|
||||
cc1 = new_code_cell(source=u'print(2*6)')
|
||||
cc1.outputs.append(new_output(output_type="stream", text=u'12'))
|
||||
nb.cells.append(cc1)
|
||||
|
||||
with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
|
||||
encoding='utf-8') as f:
|
||||
write(nb, f, version=4)
|
||||
|
||||
def test_missing_bundler_arg(self):
|
||||
"""Should respond with 400 error about missing bundler arg"""
|
||||
resp = self.request('GET', 'bundle/fake.ipynb')
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn('Missing argument bundler', resp.text)
|
||||
|
||||
def test_notebook_not_found(self):
|
||||
"""Should respond with 404 error about missing notebook"""
|
||||
resp = self.request('GET', 'bundle/fake.ipynb',
|
||||
params={'bundler': 'fake_bundler'})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertIn('Not Found', resp.text)
|
||||
|
||||
def test_bundler_not_enabled(self):
|
||||
"""Should respond with 400 error about disabled bundler"""
|
||||
resp = self.request('GET', 'bundle/testnb.ipynb',
|
||||
params={'bundler': 'fake_bundler'})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn('Bundler fake_bundler not enabled', resp.text)
|
||||
|
||||
def test_bundler_import_error(self):
|
||||
"""Should respond with 500 error about failure to load bundler module"""
|
||||
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
|
||||
mock.return_value = {'module_name': 'fake_module'}
|
||||
resp = self.request('GET', 'bundle/testnb.ipynb',
|
||||
params={'bundler': 'fake_bundler'})
|
||||
mock.assert_called_with('fake_bundler')
|
||||
self.assertEqual(resp.status_code, 500)
|
||||
self.assertIn('Could not import bundler fake_bundler', resp.text)
|
||||
|
||||
def test_bundler_invoke(self):
|
||||
"""Should respond with 200 and output from test bundler stub"""
|
||||
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
|
||||
mock.return_value = {'module_name': 'notebook.bundler.tests.test_bundler_api'}
|
||||
resp = self.request('GET', 'bundle/testnb.ipynb',
|
||||
params={'bundler': 'stub_bundler'})
|
||||
mock.assert_called_with('stub_bundler')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn('testnb.ipynb', resp.text)
|
||||
@ -1,124 +0,0 @@
|
||||
"""Test the bundler tools."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import notebook.bundler.tools as tools
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
class TestBundlerTools(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_get_no_cell_references(self):
|
||||
'''Should find no references in a regular HTML comment.'''
|
||||
no_references = tools.get_cell_reference_patterns({'source':'''!<--
|
||||
a
|
||||
b
|
||||
c
|
||||
-->''', 'cell_type':'markdown'})
|
||||
self.assertEqual(len(no_references), 0)
|
||||
|
||||
def test_get_cell_reference_patterns_comment_multiline(self):
|
||||
'''Should find two references and ignore a comment within an HTML comment.'''
|
||||
cell = {'cell_type':'markdown', 'source':'''<!--associate:
|
||||
a
|
||||
b/
|
||||
#comment
|
||||
-->'''}
|
||||
references = tools.get_cell_reference_patterns(cell)
|
||||
self.assertTrue('a' in references and 'b/' in references, str(references))
|
||||
self.assertEqual(len(references), 2, str(references))
|
||||
|
||||
def test_get_cell_reference_patterns_comment_trailing_filename(self):
|
||||
'''Should find three references within an HTML comment.'''
|
||||
cell = {'cell_type':'markdown', 'source':'''<!--associate:c
|
||||
a
|
||||
b/
|
||||
#comment
|
||||
-->'''}
|
||||
references = tools.get_cell_reference_patterns(cell)
|
||||
self.assertTrue('a' in references and 'b/' in references and 'c' in references, str(references))
|
||||
self.assertEqual(len(references), 3, str(references))
|
||||
|
||||
def test_get_cell_reference_patterns_precode(self):
|
||||
'''Should find no references in a fenced code block in a *code* cell.'''
|
||||
self.assertTrue(tools.get_cell_reference_patterns)
|
||||
no_references = tools.get_cell_reference_patterns({'source':'''```
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
```
|
||||
''', 'cell_type':'code'})
|
||||
self.assertEqual(len(no_references), 0)
|
||||
|
||||
def test_get_cell_reference_patterns_precode_mdcomment(self):
|
||||
'''Should find two references and ignore a comment in a fenced code block.'''
|
||||
cell = {'cell_type':'markdown', 'source':'''```
|
||||
a
|
||||
b/
|
||||
#comment
|
||||
```'''}
|
||||
references = tools.get_cell_reference_patterns(cell)
|
||||
self.assertTrue('a' in references and 'b/' in references, str(references))
|
||||
self.assertEqual(len(references), 2, str(references))
|
||||
|
||||
def test_get_cell_reference_patterns_precode_backticks(self):
|
||||
'''Should find three references in a fenced code block.'''
|
||||
cell = {'cell_type':'markdown', 'source':'''```c
|
||||
a
|
||||
b/
|
||||
#comment
|
||||
```'''}
|
||||
references = tools.get_cell_reference_patterns(cell)
|
||||
self.assertTrue('a' in references and 'b/' in references and 'c' in references, str(references))
|
||||
self.assertEqual(len(references), 3, str(references))
|
||||
|
||||
def test_glob_dir(self):
|
||||
'''Should expand to single file in the resources/ subfolder.'''
|
||||
self.assertIn(os.path.join('resources', 'empty.ipynb'),
|
||||
tools.expand_references(HERE, ['resources/empty.ipynb']))
|
||||
|
||||
def test_glob_subdir(self):
|
||||
'''Should expand to all files in the resources/ subfolder.'''
|
||||
self.assertIn(os.path.join('resources', 'empty.ipynb'),
|
||||
tools.expand_references(HERE, ['resources/']))
|
||||
|
||||
def test_glob_splat(self):
|
||||
'''Should expand to all contents under this test/ directory.'''
|
||||
globs = tools.expand_references(HERE, ['*'])
|
||||
self.assertIn('test_bundler_tools.py', globs, globs)
|
||||
self.assertIn('resources', globs, globs)
|
||||
|
||||
def test_glob_splatsplat_in_middle(self):
|
||||
'''Should expand to test_file.txt deep under this test/ directory.'''
|
||||
globs = tools.expand_references(HERE, ['resources/**/test_file.txt'])
|
||||
self.assertIn(os.path.join('resources', 'subdir', 'test_file.txt'), globs, globs)
|
||||
|
||||
def test_glob_splatsplat_trailing(self):
|
||||
'''Should expand to all descendants of this test/ directory.'''
|
||||
globs = tools.expand_references(HERE, ['resources/**'])
|
||||
self.assertIn(os.path.join('resources', 'empty.ipynb'), globs, globs)
|
||||
self.assertIn(os.path.join('resources', 'subdir', 'test_file.txt'), globs, globs)
|
||||
|
||||
def test_glob_splatsplat_leading(self):
|
||||
'''Should expand to test_file.txt under any path.'''
|
||||
globs = tools.expand_references(HERE, ['**/test_file.txt'])
|
||||
self.assertIn(os.path.join('resources', 'subdir', 'test_file.txt'), globs, globs)
|
||||
self.assertIn(os.path.join('resources', 'another_subdir', 'test_file.txt'), globs, globs)
|
||||
|
||||
def test_copy_filelist(self):
|
||||
'''Should copy select files from source to destination'''
|
||||
globs = tools.expand_references(HERE, ['**/test_file.txt'])
|
||||
tools.copy_filelist(HERE, self.tmp, globs)
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'resources', 'subdir', 'test_file.txt')))
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'resources', 'another_subdir', 'test_file.txt')))
|
||||
self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'resources', 'empty.ipynb')))
|
||||
@ -1,72 +0,0 @@
|
||||
"""Test the bundlerextension CLI."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
from unittest.mock import patch
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
from ipython_genutils import py3compat
|
||||
|
||||
from traitlets.tests.utils import check_help_all_output
|
||||
|
||||
import notebook.nbextensions as nbextensions
|
||||
from notebook.config_manager import BaseJSONConfigManager
|
||||
from ..bundlerextensions import (_get_config_dir, enable_bundler_python,
|
||||
disable_bundler_python)
|
||||
|
||||
def test_help_output():
|
||||
check_help_all_output('notebook.bundler.bundlerextensions')
|
||||
check_help_all_output('notebook.bundler.bundlerextensions', ['enable'])
|
||||
check_help_all_output('notebook.bundler.bundlerextensions', ['disable'])
|
||||
|
||||
class TestBundlerExtensionCLI(unittest.TestCase):
|
||||
"""Tests the bundlerextension CLI against the example zip_bundler."""
|
||||
def setUp(self):
|
||||
"""Build an isolated config environment."""
|
||||
td = TemporaryDirectory()
|
||||
|
||||
self.test_dir = py3compat.cast_unicode(td.name)
|
||||
self.data_dir = os.path.join(self.test_dir, 'data')
|
||||
self.config_dir = os.path.join(self.test_dir, 'config')
|
||||
self.system_data_dir = os.path.join(self.test_dir, 'system_data')
|
||||
self.system_path = [self.system_data_dir]
|
||||
|
||||
# Use temp directory, not real user or system config paths
|
||||
self.patch_env = patch.dict('os.environ', {
|
||||
'JUPYTER_CONFIG_DIR': self.config_dir,
|
||||
'JUPYTER_DATA_DIR': self.data_dir,
|
||||
})
|
||||
self.patch_env.start()
|
||||
self.patch_system_path = patch.object(nbextensions,
|
||||
'SYSTEM_JUPYTER_PATH', self.system_path)
|
||||
self.patch_system_path.start()
|
||||
|
||||
def tearDown(self):
|
||||
"""Remove the test config environment."""
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
self.patch_env.stop()
|
||||
self.patch_system_path.stop()
|
||||
|
||||
def test_enable(self):
|
||||
"""Should add the bundler to the notebook configuration."""
|
||||
enable_bundler_python('notebook.bundler.zip_bundler')
|
||||
|
||||
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
|
||||
cm = BaseJSONConfigManager(config_dir=config_dir)
|
||||
bundlers = cm.get('notebook').get('bundlerextensions', {})
|
||||
self.assertEqual(len(bundlers), 1)
|
||||
self.assertIn('notebook_zip_download', bundlers)
|
||||
|
||||
def test_disable(self):
|
||||
"""Should remove the bundler from the notebook configuration."""
|
||||
self.test_enable()
|
||||
disable_bundler_python('notebook.bundler.zip_bundler')
|
||||
|
||||
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
|
||||
cm = BaseJSONConfigManager(config_dir=config_dir)
|
||||
bundlers = cm.get('notebook').get('bundlerextensions', {})
|
||||
self.assertEqual(len(bundlers), 0)
|
||||
@ -1,230 +0,0 @@
|
||||
"""Set of common tools to aid bundler implementations."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import os
|
||||
import shutil
|
||||
import errno
|
||||
import nbformat
|
||||
import fnmatch
|
||||
import glob
|
||||
|
||||
def get_file_references(abs_nb_path, version):
|
||||
"""Gets a list of files referenced either in Markdown fenced code blocks
|
||||
or in HTML comments from the notebook. Expands patterns expressed in
|
||||
gitignore syntax (https://git-scm.com/docs/gitignore). Returns the
|
||||
fully expanded list of filenames relative to the notebook dirname.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
abs_nb_path: str
|
||||
Absolute path of the notebook on disk
|
||||
version: int
|
||||
Version of the notebook document format to use
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
Filename strings relative to the notebook path
|
||||
"""
|
||||
ref_patterns = get_reference_patterns(abs_nb_path, version)
|
||||
expanded = expand_references(os.path.dirname(abs_nb_path), ref_patterns)
|
||||
return expanded
|
||||
|
||||
def get_reference_patterns(abs_nb_path, version):
|
||||
"""Gets a list of reference patterns either in Markdown fenced code blocks
|
||||
or in HTML comments from the notebook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
abs_nb_path: str
|
||||
Absolute path of the notebook on disk
|
||||
version: int
|
||||
Version of the notebook document format to use
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
Pattern strings from the notebook
|
||||
"""
|
||||
notebook = nbformat.read(abs_nb_path, version)
|
||||
referenced_list = []
|
||||
for cell in notebook.cells:
|
||||
references = get_cell_reference_patterns(cell)
|
||||
if references:
|
||||
referenced_list = referenced_list + references
|
||||
return referenced_list
|
||||
|
||||
def get_cell_reference_patterns(cell):
|
||||
'''
|
||||
Retrieves the list of references from a single notebook cell. Looks for
|
||||
fenced code blocks or HTML comments in Markdown cells, e.g.,
|
||||
|
||||
```
|
||||
some.csv
|
||||
foo/
|
||||
!foo/bar
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
<!--associate:
|
||||
some.csv
|
||||
foo/
|
||||
!foo/bar
|
||||
-->
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cell: dict
|
||||
Notebook cell object
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
Reference patterns found in the cell
|
||||
'''
|
||||
referenced = []
|
||||
# invisible after execution: unrendered HTML comment
|
||||
if cell.get('cell_type').startswith('markdown') and cell.get('source').startswith('<!--associate:'):
|
||||
lines = cell.get('source')[len('<!--associate:'):].splitlines()
|
||||
for line in lines:
|
||||
if line.startswith('-->'):
|
||||
break
|
||||
# Trying to go out of the current directory leads to
|
||||
# trouble when deploying
|
||||
if line.find('../') < 0 and not line.startswith('#'):
|
||||
referenced.append(line)
|
||||
# visible after execution: rendered as a code element within a pre element
|
||||
elif cell.get('cell_type').startswith('markdown') and cell.get('source').find('```') >= 0:
|
||||
source = cell.get('source')
|
||||
offset = source.find('```')
|
||||
lines = source[offset + len('```'):].splitlines()
|
||||
for line in lines:
|
||||
if line.startswith('```'):
|
||||
break
|
||||
# Trying to go out of the current directory leads to
|
||||
# trouble when deploying
|
||||
if line.find('../') < 0 and not line.startswith('#'):
|
||||
referenced.append(line)
|
||||
|
||||
# Clean out blank references
|
||||
return [ref for ref in referenced if ref.strip()]
|
||||
|
||||
def expand_references(root_path, references):
|
||||
"""Expands a set of reference patterns by evaluating them against the
|
||||
given root directory. Expansions are performed against patterns
|
||||
expressed in the same manner as in gitignore
|
||||
(https://git-scm.com/docs/gitignore).
|
||||
|
||||
NOTE: Temporarily changes the current working directory when called.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
root_path: str
|
||||
Assumed root directory for the patterns
|
||||
references: list
|
||||
Reference patterns from get_reference_patterns expressed with
|
||||
forward-slash directory separators
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
Filename strings relative to the root path
|
||||
"""
|
||||
# Use normpath to convert to platform specific slashes, but be sure
|
||||
# to retain a trailing slash which normpath pulls off
|
||||
normalized_references = []
|
||||
for ref in references:
|
||||
normalized_ref = os.path.normpath(ref)
|
||||
# un-normalized separator
|
||||
if ref.endswith('/'):
|
||||
normalized_ref += os.sep
|
||||
normalized_references.append(normalized_ref)
|
||||
references = normalized_references
|
||||
|
||||
globbed = []
|
||||
negations = []
|
||||
must_walk = []
|
||||
for pattern in references:
|
||||
if pattern and pattern.find(os.sep) < 0:
|
||||
# simple shell glob
|
||||
cwd = os.getcwd()
|
||||
os.chdir(root_path)
|
||||
if pattern.startswith('!'):
|
||||
negations = negations + glob.glob(pattern[1:])
|
||||
else:
|
||||
globbed = globbed + glob.glob(pattern)
|
||||
os.chdir(cwd)
|
||||
elif pattern:
|
||||
must_walk.append(pattern)
|
||||
|
||||
for pattern in must_walk:
|
||||
pattern_is_negation = pattern.startswith('!')
|
||||
if pattern_is_negation:
|
||||
testpattern = pattern[1:]
|
||||
else:
|
||||
testpattern = pattern
|
||||
for root, _, filenames in os.walk(root_path):
|
||||
for filename in filenames:
|
||||
joined = os.path.join(root[len(root_path) + 1:], filename)
|
||||
if testpattern.endswith(os.sep):
|
||||
if joined.startswith(testpattern):
|
||||
if pattern_is_negation:
|
||||
negations.append(joined)
|
||||
else:
|
||||
globbed.append(joined)
|
||||
elif testpattern.find('**') >= 0:
|
||||
# path wildcard
|
||||
ends = testpattern.split('**')
|
||||
if len(ends) == 2:
|
||||
if joined.startswith(ends[0]) and joined.endswith(ends[1]):
|
||||
if pattern_is_negation:
|
||||
negations.append(joined)
|
||||
else:
|
||||
globbed.append(joined)
|
||||
else:
|
||||
# segments should be respected
|
||||
if fnmatch.fnmatch(joined, testpattern):
|
||||
if pattern_is_negation:
|
||||
negations.append(joined)
|
||||
else:
|
||||
globbed.append(joined)
|
||||
|
||||
for negated in negations:
|
||||
try:
|
||||
globbed.remove(negated)
|
||||
except ValueError as err:
|
||||
pass
|
||||
return set(globbed)
|
||||
|
||||
def copy_filelist(src, dst, src_relative_filenames):
|
||||
"""Copies the given list of files, relative to src, into dst, creating
|
||||
directories along the way as needed and ignore existence errors.
|
||||
Skips any files that do not exist. Does not create empty directories
|
||||
from src in dst.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src: str
|
||||
Root of the source directory
|
||||
dst: str
|
||||
Root of the destination directory
|
||||
src_relative_filenames: list
|
||||
Filenames relative to src
|
||||
"""
|
||||
for filename in src_relative_filenames:
|
||||
# Only consider the file if it exists in src
|
||||
if os.path.isfile(os.path.join(src, filename)):
|
||||
parent_relative = os.path.dirname(filename)
|
||||
if parent_relative:
|
||||
# Make sure the parent directory exists
|
||||
parent_dst = os.path.join(dst, parent_relative)
|
||||
try:
|
||||
os.makedirs(parent_dst)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
raise exc
|
||||
shutil.copy2(os.path.join(src, filename), os.path.join(dst, filename))
|
||||
@ -1,59 +0,0 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import os
|
||||
import io
|
||||
import zipfile
|
||||
import notebook.bundler.tools as tools
|
||||
|
||||
def _jupyter_bundlerextension_paths():
|
||||
"""Metadata for notebook bundlerextension"""
|
||||
return [{
|
||||
'name': 'notebook_zip_download',
|
||||
'label': 'IPython Notebook bundle (.zip)',
|
||||
'module_name': 'notebook.bundler.zip_bundler',
|
||||
'group': 'download'
|
||||
}]
|
||||
|
||||
def bundle(handler, model):
|
||||
"""Create a zip file containing the original notebook and files referenced
|
||||
from it. Retain the referenced files in paths relative to the notebook.
|
||||
Return the zip as a file download.
|
||||
|
||||
Assumes the notebook and other files are all on local disk.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
handler : tornado.web.RequestHandler
|
||||
Handler that serviced the bundle request
|
||||
model : dict
|
||||
Notebook model from the configured ContentManager
|
||||
"""
|
||||
abs_nb_path = os.path.join(handler.settings['contents_manager'].root_dir,
|
||||
model['path'])
|
||||
notebook_filename = model['name']
|
||||
notebook_name = os.path.splitext(notebook_filename)[0]
|
||||
|
||||
# Headers
|
||||
zip_filename = os.path.splitext(notebook_name)[0] + '.zip'
|
||||
handler.set_attachment_header(zip_filename)
|
||||
handler.set_header('Content-Type', 'application/zip')
|
||||
|
||||
# Get associated files
|
||||
ref_filenames = tools.get_file_references(abs_nb_path, 4)
|
||||
|
||||
# Prepare the zip file
|
||||
zip_buffer = io.BytesIO()
|
||||
zipf = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
|
||||
zipf.write(abs_nb_path, notebook_filename)
|
||||
|
||||
notebook_dir = os.path.dirname(abs_nb_path)
|
||||
for nb_relative_filename in ref_filenames:
|
||||
# Build absolute path to file on disk
|
||||
abs_fn = os.path.join(notebook_dir, nb_relative_filename)
|
||||
# Store file under path relative to notebook
|
||||
zipf.write(abs_fn, nb_relative_filename)
|
||||
|
||||
zipf.close()
|
||||
|
||||
# Return the buffer value as the response
|
||||
handler.finish(zip_buffer.getvalue())
|
||||
@ -1,132 +0,0 @@
|
||||
"""Manager to read and modify config data in JSON files."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import errno
|
||||
import glob
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import copy
|
||||
|
||||
from traitlets.config import LoggingConfigurable
|
||||
from traitlets.traitlets import Unicode, Bool
|
||||
|
||||
|
||||
def recursive_update(target, new):
|
||||
"""Recursively update one dictionary using another.
|
||||
|
||||
None values will delete their keys.
|
||||
"""
|
||||
for k, v in new.items():
|
||||
if isinstance(v, dict):
|
||||
if k not in target:
|
||||
target[k] = {}
|
||||
recursive_update(target[k], v)
|
||||
if not target[k]:
|
||||
# Prune empty subdicts
|
||||
del target[k]
|
||||
|
||||
elif v is None:
|
||||
target.pop(k, None)
|
||||
|
||||
else:
|
||||
target[k] = v
|
||||
|
||||
|
||||
def remove_defaults(data, defaults):
|
||||
"""Recursively remove items from dict that are already in defaults"""
|
||||
# copy the iterator, since data will be modified
|
||||
for key, value in list(data.items()):
|
||||
if key in defaults:
|
||||
if isinstance(value, dict):
|
||||
remove_defaults(data[key], defaults[key])
|
||||
if not data[key]: # prune empty subdicts
|
||||
del data[key]
|
||||
else:
|
||||
if value == defaults[key]:
|
||||
del data[key]
|
||||
|
||||
|
||||
class BaseJSONConfigManager(LoggingConfigurable):
|
||||
"""General JSON config manager
|
||||
|
||||
Deals with persisting/storing config in a json file with optionally
|
||||
default values in a {section_name}.d directory.
|
||||
"""
|
||||
|
||||
config_dir = Unicode('.')
|
||||
read_directory = Bool(True)
|
||||
|
||||
def ensure_config_dir_exists(self):
|
||||
"""Will try to create the config_dir directory."""
|
||||
try:
|
||||
os.makedirs(self.config_dir, 0o755)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
def file_name(self, section_name):
|
||||
"""Returns the json filename for the section_name: {config_dir}/{section_name}.json"""
|
||||
return os.path.join(self.config_dir, section_name+'.json')
|
||||
|
||||
def directory(self, section_name):
|
||||
"""Returns the directory name for the section name: {config_dir}/{section_name}.d"""
|
||||
return os.path.join(self.config_dir, section_name+'.d')
|
||||
|
||||
def get(self, section_name, include_root=True):
|
||||
"""Retrieve the config data for the specified section.
|
||||
|
||||
Returns the data as a dictionary, or an empty dictionary if the file
|
||||
doesn't exist.
|
||||
|
||||
When include_root is False, it will not read the root .json file,
|
||||
effectively returning the default values.
|
||||
"""
|
||||
paths = [self.file_name(section_name)] if include_root else []
|
||||
if self.read_directory:
|
||||
pattern = os.path.join(self.directory(section_name), '*.json')
|
||||
# These json files should be processed first so that the
|
||||
# {section_name}.json take precedence.
|
||||
# The idea behind this is that installing a Python package may
|
||||
# put a json file somewhere in the a .d directory, while the
|
||||
# .json file is probably a user configuration.
|
||||
paths = sorted(glob.glob(pattern)) + paths
|
||||
self.log.debug('Paths used for configuration of %s: \n\t%s', section_name, '\n\t'.join(paths))
|
||||
data = {}
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
with io.open(path, encoding='utf-8') as f:
|
||||
recursive_update(data, json.load(f))
|
||||
return data
|
||||
|
||||
def set(self, section_name, data):
|
||||
"""Store the given config data.
|
||||
"""
|
||||
filename = self.file_name(section_name)
|
||||
self.ensure_config_dir_exists()
|
||||
|
||||
if self.read_directory:
|
||||
# we will modify data in place, so make a copy
|
||||
data = copy.deepcopy(data)
|
||||
defaults = self.get(section_name, include_root=False)
|
||||
remove_defaults(data, defaults)
|
||||
|
||||
# Generate the JSON up front, since it could raise an exception,
|
||||
# in order to avoid writing half-finished corrupted data to disk.
|
||||
json_content = json.dumps(data, indent=2)
|
||||
|
||||
f = io.open(filename, 'w', encoding='utf-8')
|
||||
with f:
|
||||
f.write(json_content)
|
||||
|
||||
def update(self, section_name, new_data):
|
||||
"""Modify the config section by recursively updating it with new_data.
|
||||
|
||||
Returns the modified config data as a dictionary.
|
||||
"""
|
||||
data = self.get(section_name)
|
||||
recursive_update(data, new_data)
|
||||
self.set(section_name, data)
|
||||
return data
|
||||
@ -1,28 +0,0 @@
|
||||
"""Tornado handlers for the terminal emulator."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from tornado import web
|
||||
from ..base.handlers import IPythonHandler, path_regex
|
||||
from ..utils import url_escape
|
||||
|
||||
class EditorHandler(IPythonHandler):
|
||||
"""Render the text editor interface."""
|
||||
@web.authenticated
|
||||
def get(self, path):
|
||||
path = path.strip('/')
|
||||
if not self.contents_manager.file_exists(path):
|
||||
raise web.HTTPError(404, u'File does not exist: %s' % path)
|
||||
|
||||
basename = path.rsplit('/', 1)[-1]
|
||||
self.write(self.render_template('edit.html',
|
||||
file_path=url_escape(path),
|
||||
basename=basename,
|
||||
page_title=basename + " (editing)",
|
||||
)
|
||||
)
|
||||
|
||||
default_handlers = [
|
||||
(r"/edit%s" % path_regex, EditorHandler),
|
||||
]
|
||||
@ -1,103 +0,0 @@
|
||||
"""Utilities for installing extensions"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os
|
||||
from tornado.log import LogFormatter
|
||||
from traitlets import Bool, Any
|
||||
from jupyter_core.application import JupyterApp
|
||||
from jupyter_core.paths import (
|
||||
jupyter_config_dir, ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH
|
||||
)
|
||||
from ._version import __version__
|
||||
|
||||
class ArgumentConflict(ValueError):
|
||||
pass
|
||||
|
||||
_base_flags = {}
|
||||
_base_flags.update(JupyterApp.flags)
|
||||
_base_flags.pop("y", None)
|
||||
_base_flags.pop("generate-config", None)
|
||||
_base_flags.update({
|
||||
"user" : ({
|
||||
"BaseExtensionApp" : {
|
||||
"user" : True,
|
||||
}}, "Apply the operation only for the given user"
|
||||
),
|
||||
"system" : ({
|
||||
"BaseExtensionApp" : {
|
||||
"user" : False,
|
||||
"sys_prefix": False,
|
||||
}}, "Apply the operation system-wide"
|
||||
),
|
||||
"sys-prefix" : ({
|
||||
"BaseExtensionApp" : {
|
||||
"sys_prefix" : True,
|
||||
}}, "Use sys.prefix as the prefix for installing nbextensions (for environments, packaging)"
|
||||
),
|
||||
"py" : ({
|
||||
"BaseExtensionApp" : {
|
||||
"python" : True,
|
||||
}}, "Install from a Python package"
|
||||
)
|
||||
})
|
||||
_base_flags['python'] = _base_flags['py']
|
||||
|
||||
_base_aliases = {}
|
||||
_base_aliases.update(JupyterApp.aliases)
|
||||
|
||||
|
||||
class BaseExtensionApp(JupyterApp):
|
||||
"""Base nbextension installer app"""
|
||||
_log_formatter_cls = LogFormatter
|
||||
flags = _base_flags
|
||||
aliases = _base_aliases
|
||||
version = __version__
|
||||
|
||||
user = Bool(False, 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")
|
||||
|
||||
# Remove for 5.0...
|
||||
verbose = Any(None, config=True, help="DEPRECATED: Verbosity level")
|
||||
|
||||
def _verbose_changed(self):
|
||||
"""Warn about verbosity changes"""
|
||||
import warnings
|
||||
warnings.warn("`verbose` traits of `{}` has been deprecated, has no effects and will be removed in notebook 5.0.".format(type(self).__name__), DeprecationWarning)
|
||||
|
||||
def _log_format_default(self):
|
||||
"""A default format for messages"""
|
||||
return "%(message)s"
|
||||
|
||||
def _get_config_dir(user=False, sys_prefix=False):
|
||||
"""Get the location of config files for the current context
|
||||
|
||||
Returns the string to the environment
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
user : bool [default: False]
|
||||
Get the user's .jupyter config directory
|
||||
sys_prefix : bool [default: False]
|
||||
Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
|
||||
"""
|
||||
user = False if sys_prefix else user
|
||||
if user and sys_prefix:
|
||||
raise ArgumentConflict("Cannot specify more than one of user or sys_prefix")
|
||||
if user:
|
||||
nbext = jupyter_config_dir()
|
||||
elif sys_prefix:
|
||||
nbext = ENV_CONFIG_PATH[0]
|
||||
else:
|
||||
nbext = SYSTEM_CONFIG_PATH[0]
|
||||
return nbext
|
||||
|
||||
# Constants for pretty print extension listing function.
|
||||
# Window doesn't support coloring in the commandline
|
||||
GREEN_ENABLED = '\033[32m enabled \033[0m' if os.name != 'nt' else 'enabled '
|
||||
RED_DISABLED = '\033[31mdisabled\033[0m' if os.name != 'nt' else 'disabled'
|
||||
GREEN_OK = '\033[32mOK\033[0m' if os.name != 'nt' else 'ok'
|
||||
RED_X = '\033[31m X\033[0m' if os.name != 'nt' else ' X'
|
||||
@ -1,84 +0,0 @@
|
||||
"""Serve files directly from the ContentsManager."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import mimetypes
|
||||
import json
|
||||
from base64 import decodebytes
|
||||
|
||||
from tornado import gen, web
|
||||
|
||||
from notebook.base.handlers import IPythonHandler
|
||||
from notebook.utils import maybe_future
|
||||
|
||||
|
||||
class FilesHandler(IPythonHandler):
|
||||
"""serve files via ContentsManager
|
||||
|
||||
Normally used when ContentsManager is not a FileContentsManager.
|
||||
|
||||
FileContentsManager subclasses use AuthenticatedFilesHandler by default,
|
||||
a subclass of StaticFileHandler.
|
||||
"""
|
||||
|
||||
@property
|
||||
def content_security_policy(self):
|
||||
# In case we're serving HTML/SVG, confine any Javascript to a unique
|
||||
# origin so it can't interact with the notebook server.
|
||||
return super().content_security_policy + "; sandbox allow-scripts"
|
||||
|
||||
@web.authenticated
|
||||
def head(self, path):
|
||||
self.check_xsrf_cookie()
|
||||
return self.get(path, include_body=False)
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def get(self, path, include_body=True):
|
||||
# /files/ requests must originate from the same site
|
||||
self.check_xsrf_cookie()
|
||||
cm = self.contents_manager
|
||||
|
||||
if cm.is_hidden(path) and not cm.allow_hidden:
|
||||
self.log.info("Refusing to serve hidden file, via 404 Error")
|
||||
raise web.HTTPError(404)
|
||||
|
||||
path = path.strip('/')
|
||||
if '/' in path:
|
||||
_, name = path.rsplit('/', 1)
|
||||
else:
|
||||
name = path
|
||||
|
||||
model = yield maybe_future(cm.get(path, type='file', content=include_body))
|
||||
|
||||
if self.get_argument("download", False):
|
||||
self.set_attachment_header(name)
|
||||
|
||||
# get mimetype from filename
|
||||
if name.lower().endswith('.ipynb'):
|
||||
self.set_header('Content-Type', 'application/x-ipynb+json')
|
||||
else:
|
||||
cur_mime = mimetypes.guess_type(name)[0]
|
||||
if cur_mime == 'text/plain':
|
||||
self.set_header('Content-Type', 'text/plain; charset=UTF-8')
|
||||
elif cur_mime is not None:
|
||||
self.set_header('Content-Type', cur_mime)
|
||||
else:
|
||||
if model['format'] == 'base64':
|
||||
self.set_header('Content-Type', 'application/octet-stream')
|
||||
else:
|
||||
self.set_header('Content-Type', 'text/plain; charset=UTF-8')
|
||||
|
||||
if include_body:
|
||||
if model['format'] == 'base64':
|
||||
b64_bytes = model['content'].encode('ascii')
|
||||
self.write(decodebytes(b64_bytes))
|
||||
elif model['format'] == 'json':
|
||||
self.write(json.dumps(model['content']))
|
||||
else:
|
||||
self.write(model['content'])
|
||||
self.flush()
|
||||
|
||||
|
||||
default_handlers = []
|
||||
@ -1,263 +0,0 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os
|
||||
import logging
|
||||
import mimetypes
|
||||
import random
|
||||
|
||||
from ..base.handlers import APIHandler, IPythonHandler
|
||||
from ..utils import url_path_join
|
||||
|
||||
from tornado import gen, web
|
||||
from tornado.concurrent import Future
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
from tornado.websocket import WebSocketHandler, websocket_connect
|
||||
from tornado.httpclient import HTTPRequest
|
||||
from tornado.escape import url_escape, json_decode, utf8
|
||||
|
||||
from ipython_genutils.py3compat import cast_unicode
|
||||
from jupyter_client.session import Session
|
||||
from traitlets.config.configurable import LoggingConfigurable
|
||||
|
||||
from .managers import GatewayClient
|
||||
|
||||
# Keepalive ping interval (default: 30 seconds)
|
||||
GATEWAY_WS_PING_INTERVAL_SECS = int(os.getenv('GATEWAY_WS_PING_INTERVAL_SECS', 30))
|
||||
|
||||
|
||||
class WebSocketChannelsHandler(WebSocketHandler, IPythonHandler):
|
||||
|
||||
session = None
|
||||
gateway = None
|
||||
kernel_id = None
|
||||
ping_callback = None
|
||||
|
||||
def check_origin(self, origin=None):
|
||||
return IPythonHandler.check_origin(self, origin)
|
||||
|
||||
def set_default_headers(self):
|
||||
"""Undo the set_default_headers in IPythonHandler which doesn't make sense for websockets"""
|
||||
pass
|
||||
|
||||
def get_compression_options(self):
|
||||
# use deflate compress websocket
|
||||
return {}
|
||||
|
||||
def authenticate(self):
|
||||
"""Run before finishing the GET request
|
||||
|
||||
Extend this method to add logic that should fire before
|
||||
the websocket finishes completing.
|
||||
"""
|
||||
# authenticate the request before opening the websocket
|
||||
if self.get_current_user() is None:
|
||||
self.log.warning("Couldn't authenticate WebSocket connection")
|
||||
raise web.HTTPError(403)
|
||||
|
||||
if self.get_argument('session_id', False):
|
||||
self.session.session = cast_unicode(self.get_argument('session_id'))
|
||||
else:
|
||||
self.log.warning("No session ID specified")
|
||||
|
||||
def initialize(self):
|
||||
self.log.debug("Initializing websocket connection %s", self.request.path)
|
||||
self.session = Session(config=self.config)
|
||||
self.gateway = GatewayWebSocketClient(gateway_url=GatewayClient.instance().url)
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, kernel_id, *args, **kwargs):
|
||||
self.authenticate()
|
||||
self.kernel_id = cast_unicode(kernel_id, 'ascii')
|
||||
yield super().get(kernel_id=kernel_id, *args, **kwargs)
|
||||
|
||||
def send_ping(self):
|
||||
if self.ws_connection is None and self.ping_callback is not None:
|
||||
self.ping_callback.stop()
|
||||
return
|
||||
|
||||
self.ping(b'')
|
||||
|
||||
def open(self, kernel_id, *args, **kwargs):
|
||||
"""Handle web socket connection open to notebook server and delegate to gateway web socket handler """
|
||||
self.ping_callback = PeriodicCallback(self.send_ping, GATEWAY_WS_PING_INTERVAL_SECS * 1000)
|
||||
self.ping_callback.start()
|
||||
|
||||
self.gateway.on_open(
|
||||
kernel_id=kernel_id,
|
||||
message_callback=self.write_message,
|
||||
compression_options=self.get_compression_options()
|
||||
)
|
||||
|
||||
def on_message(self, message):
|
||||
"""Forward message to gateway web socket handler."""
|
||||
self.gateway.on_message(message)
|
||||
|
||||
def write_message(self, message, binary=False):
|
||||
"""Send message back to notebook client. This is called via callback from self.gateway._read_messages."""
|
||||
if self.ws_connection: # prevent WebSocketClosedError
|
||||
if isinstance(message, bytes):
|
||||
binary = True
|
||||
super().write_message(message, binary=binary)
|
||||
elif self.log.isEnabledFor(logging.DEBUG):
|
||||
msg_summary = WebSocketChannelsHandler._get_message_summary(json_decode(utf8(message)))
|
||||
self.log.debug("Notebook client closed websocket connection - message dropped: {}".format(msg_summary))
|
||||
|
||||
def on_close(self):
|
||||
self.log.debug("Closing websocket connection %s", self.request.path)
|
||||
self.gateway.on_close()
|
||||
super().on_close()
|
||||
|
||||
@staticmethod
|
||||
def _get_message_summary(message):
|
||||
summary = []
|
||||
message_type = message['msg_type']
|
||||
summary.append('type: {}'.format(message_type))
|
||||
|
||||
if message_type == 'status':
|
||||
summary.append(', state: {}'.format(message['content']['execution_state']))
|
||||
elif message_type == 'error':
|
||||
summary.append(', {}:{}:{}'.format(message['content']['ename'],
|
||||
message['content']['evalue'],
|
||||
message['content']['traceback']))
|
||||
else:
|
||||
summary.append(', ...') # don't display potentially sensitive data
|
||||
|
||||
return ''.join(summary)
|
||||
|
||||
|
||||
class GatewayWebSocketClient(LoggingConfigurable):
|
||||
"""Proxy web socket connection to a kernel/enterprise gateway."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.kernel_id = None
|
||||
self.ws = None
|
||||
self.ws_future = Future()
|
||||
self.disconnected = False
|
||||
self.retry = 0
|
||||
|
||||
@gen.coroutine
|
||||
def _connect(self, kernel_id):
|
||||
# websocket is initialized before connection
|
||||
self.ws = None
|
||||
self.kernel_id = kernel_id
|
||||
ws_url = url_path_join(
|
||||
GatewayClient.instance().ws_url,
|
||||
GatewayClient.instance().kernels_endpoint, url_escape(kernel_id), 'channels'
|
||||
)
|
||||
self.log.info('Connecting to {}'.format(ws_url))
|
||||
kwargs = {}
|
||||
kwargs = GatewayClient.instance().load_connection_args(**kwargs)
|
||||
|
||||
request = HTTPRequest(ws_url, **kwargs)
|
||||
self.ws_future = websocket_connect(request)
|
||||
self.ws_future.add_done_callback(self._connection_done)
|
||||
|
||||
def _connection_done(self, fut):
|
||||
if not self.disconnected and fut.exception() is None: # prevent concurrent.futures._base.CancelledError
|
||||
self.ws = fut.result()
|
||||
self.retry = 0
|
||||
self.log.debug("Connection is ready: ws: {}".format(self.ws))
|
||||
else:
|
||||
self.log.warning("Websocket connection has been closed via client disconnect or due to error. "
|
||||
"Kernel with ID '{}' may not be terminated on GatewayClient: {}".
|
||||
format(self.kernel_id, GatewayClient.instance().url))
|
||||
|
||||
def _disconnect(self):
|
||||
self.disconnected = True
|
||||
if self.ws is not None:
|
||||
# Close connection
|
||||
self.ws.close()
|
||||
elif not self.ws_future.done():
|
||||
# Cancel pending connection. Since future.cancel() is a noop on tornado, we'll track cancellation locally
|
||||
self.ws_future.cancel()
|
||||
self.log.debug("_disconnect: future cancelled, disconnected: {}".format(self.disconnected))
|
||||
|
||||
@gen.coroutine
|
||||
def _read_messages(self, callback):
|
||||
"""Read messages from gateway server."""
|
||||
while self.ws is not None:
|
||||
message = None
|
||||
if not self.disconnected:
|
||||
try:
|
||||
message = yield self.ws.read_message()
|
||||
except Exception as e:
|
||||
self.log.error("Exception reading message from websocket: {}".format(e)) # , exc_info=True)
|
||||
if message is None:
|
||||
if not self.disconnected:
|
||||
self.log.warning("Lost connection to Gateway: {}".format(self.kernel_id))
|
||||
break
|
||||
callback(message) # pass back to notebook client (see self.on_open and WebSocketChannelsHandler.open)
|
||||
else: # ws cancelled - stop reading
|
||||
break
|
||||
|
||||
# NOTE(esevan): if websocket is not disconnected by client, try to reconnect.
|
||||
if not self.disconnected and self.retry < GatewayClient.instance().gateway_retry_max:
|
||||
jitter = random.randint(10, 100) * 0.01
|
||||
retry_interval = min(GatewayClient.instance().gateway_retry_interval * (2 ** self.retry),
|
||||
GatewayClient.instance().gateway_retry_interval_max) + jitter
|
||||
self.retry += 1
|
||||
self.log.info("Attempting to re-establish the connection to Gateway in %s secs (%s/%s): %s",
|
||||
retry_interval, self.retry, GatewayClient.instance().gateway_retry_max, self.kernel_id)
|
||||
yield gen.sleep(retry_interval)
|
||||
self._connect(self.kernel_id)
|
||||
loop = IOLoop.current()
|
||||
loop.add_future(self.ws_future, lambda future: self._read_messages(callback))
|
||||
|
||||
def on_open(self, kernel_id, message_callback, **kwargs):
|
||||
"""Web socket connection open against gateway server."""
|
||||
self._connect(kernel_id)
|
||||
loop = IOLoop.current()
|
||||
loop.add_future(
|
||||
self.ws_future,
|
||||
lambda future: self._read_messages(message_callback)
|
||||
)
|
||||
|
||||
def on_message(self, message):
|
||||
"""Send message to gateway server."""
|
||||
if self.ws is None:
|
||||
loop = IOLoop.current()
|
||||
loop.add_future(
|
||||
self.ws_future,
|
||||
lambda future: self._write_message(message)
|
||||
)
|
||||
else:
|
||||
self._write_message(message)
|
||||
|
||||
def _write_message(self, message):
|
||||
"""Send message to gateway server."""
|
||||
try:
|
||||
if not self.disconnected and self.ws is not None:
|
||||
self.ws.write_message(message)
|
||||
except Exception as e:
|
||||
self.log.error("Exception writing message to websocket: {}".format(e)) # , exc_info=True)
|
||||
|
||||
def on_close(self):
|
||||
"""Web socket closed event."""
|
||||
self._disconnect()
|
||||
|
||||
|
||||
class GatewayResourceHandler(APIHandler):
|
||||
"""Retrieves resources for specific kernelspec definitions from kernel/enterprise gateway."""
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def get(self, kernel_name, path, include_body=True):
|
||||
ksm = self.kernel_spec_manager
|
||||
kernel_spec_res = yield ksm.get_kernel_spec_resource(kernel_name, path)
|
||||
if kernel_spec_res is None:
|
||||
self.log.warning("Kernelspec resource '{}' for '{}' not found. Gateway may not support"
|
||||
" resource serving.".format(path, kernel_name))
|
||||
else:
|
||||
self.set_header("Content-Type", mimetypes.guess_type(path)[0])
|
||||
self.finish(kernel_spec_res)
|
||||
|
||||
|
||||
from ..services.kernels.handlers import _kernel_id_regex
|
||||
from ..services.kernelspecs.handlers import kernel_name_regex
|
||||
|
||||
default_handlers = [
|
||||
(r"/api/kernels/%s/channels" % _kernel_id_regex, WebSocketChannelsHandler),
|
||||
(r"/kernelspecs/%s/(?P<path>.*)" % kernel_name_regex, GatewayResourceHandler),
|
||||
]
|
||||
@ -1,662 +0,0 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
from socket import gaierror
|
||||
from tornado import web
|
||||
from tornado.escape import json_encode, json_decode, url_escape
|
||||
from tornado.httpclient import HTTPClient, AsyncHTTPClient, HTTPError
|
||||
|
||||
from ..services.kernels.kernelmanager import AsyncMappingKernelManager
|
||||
from ..services.sessions.sessionmanager import SessionManager
|
||||
|
||||
from jupyter_client.kernelspec import KernelSpecManager
|
||||
from ..utils import url_path_join
|
||||
|
||||
from traitlets import Instance, Unicode, Int, Float, Bool, default, validate, TraitError
|
||||
from traitlets.config import SingletonConfigurable
|
||||
|
||||
|
||||
class GatewayClient(SingletonConfigurable):
|
||||
"""This class manages the configuration. It's its own singleton class so that we
|
||||
can share these values across all objects. It also contains some helper methods
|
||||
to build request arguments out of the various config options.
|
||||
|
||||
"""
|
||||
|
||||
url = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The url of the Kernel or Enterprise Gateway server where
|
||||
kernel specifications are defined and kernel management takes place.
|
||||
If defined, this Notebook server acts as a proxy for all kernel
|
||||
management and kernel specification retrieval. (JUPYTER_GATEWAY_URL env var)
|
||||
"""
|
||||
)
|
||||
|
||||
url_env = 'JUPYTER_GATEWAY_URL'
|
||||
|
||||
@default('url')
|
||||
def _url_default(self):
|
||||
return os.environ.get(self.url_env)
|
||||
|
||||
@validate('url')
|
||||
def _url_validate(self, proposal):
|
||||
value = proposal['value']
|
||||
# Ensure value, if present, starts with 'http'
|
||||
if value is not None and len(value) > 0:
|
||||
if not str(value).lower().startswith('http'):
|
||||
raise TraitError("GatewayClient url must start with 'http': '%r'" % value)
|
||||
return value
|
||||
|
||||
ws_url = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The websocket url of the Kernel or Enterprise Gateway server. If not provided, this value
|
||||
will correspond to the value of the Gateway url with 'ws' in place of 'http'. (JUPYTER_GATEWAY_WS_URL env var)
|
||||
"""
|
||||
)
|
||||
|
||||
ws_url_env = 'JUPYTER_GATEWAY_WS_URL'
|
||||
|
||||
@default('ws_url')
|
||||
def _ws_url_default(self):
|
||||
default_value = os.environ.get(self.ws_url_env)
|
||||
if default_value is None:
|
||||
if self.gateway_enabled:
|
||||
default_value = self.url.lower().replace('http', 'ws')
|
||||
return default_value
|
||||
|
||||
@validate('ws_url')
|
||||
def _ws_url_validate(self, proposal):
|
||||
value = proposal['value']
|
||||
# Ensure value, if present, starts with 'ws'
|
||||
if value is not None and len(value) > 0:
|
||||
if not str(value).lower().startswith('ws'):
|
||||
raise TraitError("GatewayClient ws_url must start with 'ws': '%r'" % value)
|
||||
return value
|
||||
|
||||
kernels_endpoint_default_value = '/api/kernels'
|
||||
kernels_endpoint_env = 'JUPYTER_GATEWAY_KERNELS_ENDPOINT'
|
||||
kernels_endpoint = Unicode(default_value=kernels_endpoint_default_value, config=True,
|
||||
help="""The gateway API endpoint for accessing kernel resources (JUPYTER_GATEWAY_KERNELS_ENDPOINT env var)""")
|
||||
|
||||
@default('kernels_endpoint')
|
||||
def _kernels_endpoint_default(self):
|
||||
return os.environ.get(self.kernels_endpoint_env, self.kernels_endpoint_default_value)
|
||||
|
||||
kernelspecs_endpoint_default_value = '/api/kernelspecs'
|
||||
kernelspecs_endpoint_env = 'JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT'
|
||||
kernelspecs_endpoint = Unicode(default_value=kernelspecs_endpoint_default_value, config=True,
|
||||
help="""The gateway API endpoint for accessing kernelspecs (JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT env var)""")
|
||||
|
||||
@default('kernelspecs_endpoint')
|
||||
def _kernelspecs_endpoint_default(self):
|
||||
return os.environ.get(self.kernelspecs_endpoint_env, self.kernelspecs_endpoint_default_value)
|
||||
|
||||
kernelspecs_resource_endpoint_default_value = '/kernelspecs'
|
||||
kernelspecs_resource_endpoint_env = 'JUPYTER_GATEWAY_KERNELSPECS_RESOURCE_ENDPOINT'
|
||||
kernelspecs_resource_endpoint = Unicode(default_value=kernelspecs_resource_endpoint_default_value, config=True,
|
||||
help="""The gateway endpoint for accessing kernelspecs resources
|
||||
(JUPYTER_GATEWAY_KERNELSPECS_RESOURCE_ENDPOINT env var)""")
|
||||
|
||||
@default('kernelspecs_resource_endpoint')
|
||||
def _kernelspecs_resource_endpoint_default(self):
|
||||
return os.environ.get(self.kernelspecs_resource_endpoint_env, self.kernelspecs_resource_endpoint_default_value)
|
||||
|
||||
connect_timeout_default_value = 40.0
|
||||
connect_timeout_env = 'JUPYTER_GATEWAY_CONNECT_TIMEOUT'
|
||||
connect_timeout = Float(default_value=connect_timeout_default_value, config=True,
|
||||
help="""The time allowed for HTTP connection establishment with the Gateway server.
|
||||
(JUPYTER_GATEWAY_CONNECT_TIMEOUT env var)""")
|
||||
|
||||
@default('connect_timeout')
|
||||
def connect_timeout_default(self):
|
||||
return float(os.environ.get('JUPYTER_GATEWAY_CONNECT_TIMEOUT', self.connect_timeout_default_value))
|
||||
|
||||
request_timeout_default_value = 40.0
|
||||
request_timeout_env = 'JUPYTER_GATEWAY_REQUEST_TIMEOUT'
|
||||
request_timeout = Float(default_value=request_timeout_default_value, config=True,
|
||||
help="""The time allowed for HTTP request completion. (JUPYTER_GATEWAY_REQUEST_TIMEOUT env var)""")
|
||||
|
||||
@default('request_timeout')
|
||||
def request_timeout_default(self):
|
||||
return float(os.environ.get('JUPYTER_GATEWAY_REQUEST_TIMEOUT', self.request_timeout_default_value))
|
||||
|
||||
client_key = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The filename for client SSL key, if any. (JUPYTER_GATEWAY_CLIENT_KEY env var)
|
||||
"""
|
||||
)
|
||||
client_key_env = 'JUPYTER_GATEWAY_CLIENT_KEY'
|
||||
|
||||
@default('client_key')
|
||||
def _client_key_default(self):
|
||||
return os.environ.get(self.client_key_env)
|
||||
|
||||
client_cert = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The filename for client SSL certificate, if any. (JUPYTER_GATEWAY_CLIENT_CERT env var)
|
||||
"""
|
||||
)
|
||||
client_cert_env = 'JUPYTER_GATEWAY_CLIENT_CERT'
|
||||
|
||||
@default('client_cert')
|
||||
def _client_cert_default(self):
|
||||
return os.environ.get(self.client_cert_env)
|
||||
|
||||
ca_certs = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The filename of CA certificates or None to use defaults. (JUPYTER_GATEWAY_CA_CERTS env var)
|
||||
"""
|
||||
)
|
||||
ca_certs_env = 'JUPYTER_GATEWAY_CA_CERTS'
|
||||
|
||||
@default('ca_certs')
|
||||
def _ca_certs_default(self):
|
||||
return os.environ.get(self.ca_certs_env)
|
||||
|
||||
http_user = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The username for HTTP authentication. (JUPYTER_GATEWAY_HTTP_USER env var)
|
||||
"""
|
||||
)
|
||||
http_user_env = 'JUPYTER_GATEWAY_HTTP_USER'
|
||||
|
||||
@default('http_user')
|
||||
def _http_user_default(self):
|
||||
return os.environ.get(self.http_user_env)
|
||||
|
||||
http_pwd = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The password for HTTP authentication. (JUPYTER_GATEWAY_HTTP_PWD env var)
|
||||
"""
|
||||
)
|
||||
http_pwd_env = 'JUPYTER_GATEWAY_HTTP_PWD'
|
||||
|
||||
@default('http_pwd')
|
||||
def _http_pwd_default(self):
|
||||
return os.environ.get(self.http_pwd_env)
|
||||
|
||||
headers_default_value = '{}'
|
||||
headers_env = 'JUPYTER_GATEWAY_HEADERS'
|
||||
headers = Unicode(default_value=headers_default_value, allow_none=True, config=True,
|
||||
help="""Additional HTTP headers to pass on the request. This value will be converted to a dict.
|
||||
(JUPYTER_GATEWAY_HEADERS env var)
|
||||
"""
|
||||
)
|
||||
|
||||
@default('headers')
|
||||
def _headers_default(self):
|
||||
return os.environ.get(self.headers_env, self.headers_default_value)
|
||||
|
||||
auth_token = Unicode(default_value=None, allow_none=True, config=True,
|
||||
help="""The authorization token used in the HTTP headers. (JUPYTER_GATEWAY_AUTH_TOKEN env var)
|
||||
"""
|
||||
)
|
||||
auth_token_env = 'JUPYTER_GATEWAY_AUTH_TOKEN'
|
||||
|
||||
@default('auth_token')
|
||||
def _auth_token_default(self):
|
||||
return os.environ.get(self.auth_token_env, '')
|
||||
|
||||
validate_cert_default_value = True
|
||||
validate_cert_env = 'JUPYTER_GATEWAY_VALIDATE_CERT'
|
||||
validate_cert = Bool(default_value=validate_cert_default_value, config=True,
|
||||
help="""For HTTPS requests, determines if server's certificate should be validated or not.
|
||||
(JUPYTER_GATEWAY_VALIDATE_CERT env var)"""
|
||||
)
|
||||
|
||||
@default('validate_cert')
|
||||
def validate_cert_default(self):
|
||||
return bool(os.environ.get(self.validate_cert_env, str(self.validate_cert_default_value)) not in ['no', 'false'])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._static_args = {} # initialized on first use
|
||||
|
||||
env_whitelist_default_value = ''
|
||||
env_whitelist_env = 'JUPYTER_GATEWAY_ENV_WHITELIST'
|
||||
env_whitelist = Unicode(default_value=env_whitelist_default_value, config=True,
|
||||
help="""A comma-separated list of environment variable names that will be included, along with
|
||||
their values, in the kernel startup request. The corresponding `env_whitelist` configuration
|
||||
value must also be set on the Gateway server - since that configuration value indicates which
|
||||
environmental values to make available to the kernel. (JUPYTER_GATEWAY_ENV_WHITELIST env var)""")
|
||||
|
||||
@default('env_whitelist')
|
||||
def _env_whitelist_default(self):
|
||||
return os.environ.get(self.env_whitelist_env, self.env_whitelist_default_value)
|
||||
|
||||
gateway_retry_interval_default_value = 1.0
|
||||
gateway_retry_interval_env = 'JUPYTER_GATEWAY_RETRY_INTERVAL'
|
||||
gateway_retry_interval = Float(default_value=gateway_retry_interval_default_value, config=True,
|
||||
help="""The time allowed for HTTP reconnection with the Gateway server for the first time.
|
||||
Next will be JUPYTER_GATEWAY_RETRY_INTERVAL multiplied by two in factor of numbers of retries
|
||||
but less than JUPYTER_GATEWAY_RETRY_INTERVAL_MAX.
|
||||
(JUPYTER_GATEWAY_RETRY_INTERVAL env var)""")
|
||||
|
||||
@default('gateway_retry_interval')
|
||||
def gateway_retry_interval_default(self):
|
||||
return float(os.environ.get('JUPYTER_GATEWAY_RETRY_INTERVAL', self.gateway_retry_interval_default_value))
|
||||
|
||||
gateway_retry_interval_max_default_value = 30.0
|
||||
gateway_retry_interval_max_env = 'JUPYTER_GATEWAY_RETRY_INTERVAL_MAX'
|
||||
gateway_retry_interval_max = Float(default_value=gateway_retry_interval_max_default_value, config=True,
|
||||
help="""The maximum time allowed for HTTP reconnection retry with the Gateway server.
|
||||
(JUPYTER_GATEWAY_RETRY_INTERVAL_MAX env var)""")
|
||||
|
||||
@default('gateway_retry_interval_max')
|
||||
def gateway_retry_interval_max_default(self):
|
||||
return float(os.environ.get('JUPYTER_GATEWAY_RETRY_INTERVAL_MAX', self.gateway_retry_interval_max_default_value))
|
||||
|
||||
gateway_retry_max_default_value = 5
|
||||
gateway_retry_max_env = 'JUPYTER_GATEWAY_RETRY_MAX'
|
||||
gateway_retry_max = Int(default_value=gateway_retry_max_default_value, config=True,
|
||||
help="""The maximum retries allowed for HTTP reconnection with the Gateway server.
|
||||
(JUPYTER_GATEWAY_RETRY_MAX env var)""")
|
||||
|
||||
@default('gateway_retry_max')
|
||||
def gateway_retry_max_default(self):
|
||||
return int(os.environ.get('JUPYTER_GATEWAY_RETRY_MAX', self.gateway_retry_max_default_value))
|
||||
|
||||
@property
|
||||
def gateway_enabled(self):
|
||||
return bool(self.url is not None and len(self.url) > 0)
|
||||
|
||||
# Ensure KERNEL_LAUNCH_TIMEOUT has a default value.
|
||||
KERNEL_LAUNCH_TIMEOUT = int(os.environ.get('KERNEL_LAUNCH_TIMEOUT', 40))
|
||||
|
||||
def init_static_args(self):
|
||||
"""Initialize arguments used on every request. Since these are static values, we'll
|
||||
perform this operation once.
|
||||
|
||||
"""
|
||||
# Ensure that request timeout and KERNEL_LAUNCH_TIMEOUT are the same, taking the
|
||||
# greater value of the two.
|
||||
if self.request_timeout < float(GatewayClient.KERNEL_LAUNCH_TIMEOUT):
|
||||
self.request_timeout = float(GatewayClient.KERNEL_LAUNCH_TIMEOUT)
|
||||
elif self.request_timeout > float(GatewayClient.KERNEL_LAUNCH_TIMEOUT):
|
||||
GatewayClient.KERNEL_LAUNCH_TIMEOUT = int(self.request_timeout)
|
||||
# Ensure any adjustments are reflected in env.
|
||||
os.environ['KERNEL_LAUNCH_TIMEOUT'] = str(GatewayClient.KERNEL_LAUNCH_TIMEOUT)
|
||||
|
||||
self._static_args['headers'] = json.loads(self.headers)
|
||||
if 'Authorization' not in self._static_args['headers'].keys():
|
||||
self._static_args['headers'].update({
|
||||
'Authorization': 'token {}'.format(self.auth_token)
|
||||
})
|
||||
self._static_args['connect_timeout'] = self.connect_timeout
|
||||
self._static_args['request_timeout'] = self.request_timeout
|
||||
self._static_args['validate_cert'] = self.validate_cert
|
||||
if self.client_cert:
|
||||
self._static_args['client_cert'] = self.client_cert
|
||||
self._static_args['client_key'] = self.client_key
|
||||
if self.ca_certs:
|
||||
self._static_args['ca_certs'] = self.ca_certs
|
||||
if self.http_user:
|
||||
self._static_args['auth_username'] = self.http_user
|
||||
if self.http_pwd:
|
||||
self._static_args['auth_password'] = self.http_pwd
|
||||
|
||||
def load_connection_args(self, **kwargs):
|
||||
"""Merges the static args relative to the connection, with the given keyword arguments. If statics
|
||||
have yet to be initialized, we'll do that here.
|
||||
|
||||
"""
|
||||
if len(self._static_args) == 0:
|
||||
self.init_static_args()
|
||||
|
||||
for arg, static_value in self._static_args.items():
|
||||
if arg == 'headers':
|
||||
given_value = kwargs.setdefault(arg, {})
|
||||
if isinstance(given_value, dict):
|
||||
given_value.update(static_value)
|
||||
else:
|
||||
kwargs[arg] = static_value
|
||||
return kwargs
|
||||
|
||||
|
||||
async def gateway_request(endpoint, **kwargs):
|
||||
"""Make an async request to kernel gateway endpoint, returns a response """
|
||||
client = AsyncHTTPClient()
|
||||
kwargs = GatewayClient.instance().load_connection_args(**kwargs)
|
||||
try:
|
||||
response = await client.fetch(endpoint, **kwargs)
|
||||
# Trap a set of common exceptions so that we can inform the user that their Gateway url is incorrect
|
||||
# or the server is not running.
|
||||
# NOTE: We do this here since this handler is called during the Notebook's startup and subsequent refreshes
|
||||
# of the tree view.
|
||||
except ConnectionRefusedError as e:
|
||||
raise web.HTTPError(
|
||||
503,
|
||||
"Connection refused from Gateway server url '{}'. Check to be sure the"
|
||||
" Gateway instance is running.".format(GatewayClient.instance().url)
|
||||
) from e
|
||||
except HTTPError as e:
|
||||
# This can occur if the host is valid (e.g., foo.com) but there's nothing there.
|
||||
raise web.HTTPError(e.code, "Error attempting to connect to Gateway server url '{}'. "
|
||||
"Ensure gateway url is valid and the Gateway instance is running.".
|
||||
format(GatewayClient.instance().url)) from e
|
||||
except gaierror as e:
|
||||
raise web.HTTPError(
|
||||
404,
|
||||
"The Gateway server specified in the gateway_url '{}' doesn't appear to be valid. Ensure gateway "
|
||||
"url is valid and the Gateway instance is running.".format(GatewayClient.instance().url)
|
||||
) from e
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class GatewayKernelManager(AsyncMappingKernelManager):
|
||||
"""Kernel manager that supports remote kernels hosted by Jupyter Kernel or Enterprise Gateway."""
|
||||
|
||||
# We'll maintain our own set of kernel ids
|
||||
_kernels = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.base_endpoint = url_path_join(GatewayClient.instance().url, GatewayClient.instance().kernels_endpoint)
|
||||
|
||||
def __contains__(self, kernel_id):
|
||||
return kernel_id in self._kernels
|
||||
|
||||
def remove_kernel(self, kernel_id):
|
||||
"""Complete override since we want to be more tolerant of missing keys """
|
||||
try:
|
||||
return self._kernels.pop(kernel_id)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _get_kernel_endpoint_url(self, kernel_id=None):
|
||||
"""Builds a url for the kernels endpoint
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_id: kernel UUID (optional)
|
||||
"""
|
||||
if kernel_id:
|
||||
return url_path_join(self.base_endpoint, url_escape(str(kernel_id)))
|
||||
|
||||
return self.base_endpoint
|
||||
|
||||
async def start_kernel(self, kernel_id=None, path=None, **kwargs):
|
||||
"""Start a kernel for a session and return its kernel_id.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_id : uuid
|
||||
The uuid to associate the new kernel with. If this
|
||||
is not None, this kernel will be persistent whenever it is
|
||||
requested.
|
||||
path : API path
|
||||
The API path (unicode, '/' delimited) for the cwd.
|
||||
Will be transformed to an OS path relative to root_dir.
|
||||
"""
|
||||
self.log.info('Request start kernel: kernel_id=%s, path="%s"', kernel_id, path)
|
||||
|
||||
if kernel_id is None:
|
||||
if path is not None:
|
||||
kwargs['cwd'] = self.cwd_for_path(path)
|
||||
kernel_name = kwargs.get('kernel_name', 'python3')
|
||||
kernel_url = self._get_kernel_endpoint_url()
|
||||
self.log.debug("Request new kernel at: %s" % kernel_url)
|
||||
|
||||
# Let KERNEL_USERNAME take precedent over http_user config option.
|
||||
if os.environ.get('KERNEL_USERNAME') is None and GatewayClient.instance().http_user:
|
||||
os.environ['KERNEL_USERNAME'] = GatewayClient.instance().http_user
|
||||
|
||||
kernel_env = {k: v for (k, v) in dict(os.environ).items() if k.startswith('KERNEL_')
|
||||
or k in GatewayClient.instance().env_whitelist.split(",")}
|
||||
|
||||
# Convey the full path to where this notebook file is located.
|
||||
if path is not None and kernel_env.get('KERNEL_WORKING_DIR') is None:
|
||||
kernel_env['KERNEL_WORKING_DIR'] = kwargs['cwd']
|
||||
|
||||
json_body = json_encode({'name': kernel_name, 'env': kernel_env})
|
||||
|
||||
response = await gateway_request(
|
||||
kernel_url, method='POST', headers={'Content-Type': 'application/json'}, body=json_body
|
||||
)
|
||||
kernel = json_decode(response.body)
|
||||
kernel_id = kernel['id']
|
||||
self.log.info("Kernel started: %s" % kernel_id)
|
||||
self.log.debug("Kernel args: %r" % kwargs)
|
||||
else:
|
||||
kernel = await self.get_kernel(kernel_id)
|
||||
kernel_id = kernel['id']
|
||||
self.log.info("Using existing kernel: %s" % kernel_id)
|
||||
|
||||
self._kernels[kernel_id] = kernel
|
||||
return kernel_id
|
||||
|
||||
async def get_kernel(self, kernel_id=None, **kwargs):
|
||||
"""Get kernel for kernel_id.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_id : uuid
|
||||
The uuid of the kernel.
|
||||
"""
|
||||
kernel_url = self._get_kernel_endpoint_url(kernel_id)
|
||||
self.log.debug("Request kernel at: %s" % kernel_url)
|
||||
try:
|
||||
response = await gateway_request(kernel_url, method='GET')
|
||||
except web.HTTPError as error:
|
||||
if error.status_code == 404:
|
||||
self.log.warn("Kernel not found at: %s" % kernel_url)
|
||||
self.remove_kernel(kernel_id)
|
||||
kernel = None
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
kernel = json_decode(response.body)
|
||||
# Only update our models if we already know about this kernel
|
||||
if kernel_id in self._kernels:
|
||||
self._kernels[kernel_id] = kernel
|
||||
self.log.debug("Kernel retrieved: %s", kernel)
|
||||
else:
|
||||
self.log.warning("Kernel '%s' is not managed by this instance.", kernel_id)
|
||||
kernel = None
|
||||
return kernel
|
||||
|
||||
async def kernel_model(self, kernel_id):
|
||||
"""Return a dictionary of kernel information described in the
|
||||
JSON standard model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_id : uuid
|
||||
The uuid of the kernel.
|
||||
"""
|
||||
self.log.debug("RemoteKernelManager.kernel_model: %s", kernel_id)
|
||||
model = await self.get_kernel(kernel_id)
|
||||
return model
|
||||
|
||||
async def list_kernels(self, **kwargs):
|
||||
"""Get a list of kernels."""
|
||||
kernel_url = self._get_kernel_endpoint_url()
|
||||
self.log.debug("Request list kernels: %s", kernel_url)
|
||||
response = await gateway_request(kernel_url, method='GET')
|
||||
kernels = json_decode(response.body)
|
||||
# Only update our models if we already know about the kernels
|
||||
self._kernels = {x['id']: x for x in kernels if x['id'] in self._kernels}
|
||||
return list(self._kernels.values())
|
||||
|
||||
async def shutdown_kernel(self, kernel_id, now=False, restart=False):
|
||||
"""Shutdown a kernel by its kernel uuid.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
kernel_id : uuid
|
||||
The id of the kernel to shutdown.
|
||||
now : bool
|
||||
Shutdown the kernel immediately (True) or gracefully (False)
|
||||
restart : bool
|
||||
The purpose of this shutdown is to restart the kernel (True)
|
||||
"""
|
||||
kernel_url = self._get_kernel_endpoint_url(kernel_id)
|
||||
self.log.debug("Request shutdown kernel at: %s", kernel_url)
|
||||
response = await gateway_request(kernel_url, method='DELETE')
|
||||
self.log.debug("Shutdown kernel response: %d %s", response.code, response.reason)
|
||||
self.remove_kernel(kernel_id)
|
||||
|
||||
async def restart_kernel(self, kernel_id, now=False, **kwargs):
|
||||
"""Restart a kernel by its kernel uuid.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
kernel_id : uuid
|
||||
The id of the kernel to restart.
|
||||
"""
|
||||
kernel_url = self._get_kernel_endpoint_url(kernel_id) + '/restart'
|
||||
self.log.debug("Request restart kernel at: %s", kernel_url)
|
||||
response = await gateway_request(
|
||||
kernel_url, method='POST', headers={'Content-Type': 'application/json'}, body=json_encode({})
|
||||
)
|
||||
self.log.debug("Restart kernel response: %d %s", response.code, response.reason)
|
||||
|
||||
async def interrupt_kernel(self, kernel_id, **kwargs):
|
||||
"""Interrupt a kernel by its kernel uuid.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
kernel_id : uuid
|
||||
The id of the kernel to interrupt.
|
||||
"""
|
||||
kernel_url = self._get_kernel_endpoint_url(kernel_id) + '/interrupt'
|
||||
self.log.debug("Request interrupt kernel at: %s", kernel_url)
|
||||
response = await gateway_request(
|
||||
kernel_url, method='POST', headers={'Content-Type': 'application/json'}, body=json_encode({})
|
||||
)
|
||||
self.log.debug("Interrupt kernel response: %d %s", response.code, response.reason)
|
||||
|
||||
def shutdown_all(self, now=False):
|
||||
"""Shutdown all kernels."""
|
||||
# Note: We have to make this sync because the NotebookApp does not wait for async.
|
||||
shutdown_kernels = []
|
||||
kwargs = {'method': 'DELETE'}
|
||||
kwargs = GatewayClient.instance().load_connection_args(**kwargs)
|
||||
client = HTTPClient()
|
||||
for kernel_id in self._kernels:
|
||||
kernel_url = self._get_kernel_endpoint_url(kernel_id)
|
||||
self.log.debug("Request delete kernel at: %s", kernel_url)
|
||||
try:
|
||||
response = client.fetch(kernel_url, **kwargs)
|
||||
except HTTPError:
|
||||
pass
|
||||
else:
|
||||
self.log.debug("Delete kernel response: %d %s", response.code, response.reason)
|
||||
shutdown_kernels.append(kernel_id) # avoid changing dict size during iteration
|
||||
client.close()
|
||||
for kernel_id in shutdown_kernels:
|
||||
self.remove_kernel(kernel_id)
|
||||
|
||||
|
||||
class GatewayKernelSpecManager(KernelSpecManager):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
base_endpoint = url_path_join(GatewayClient.instance().url,
|
||||
GatewayClient.instance().kernelspecs_endpoint)
|
||||
|
||||
self.base_endpoint = GatewayKernelSpecManager._get_endpoint_for_user_filter(base_endpoint)
|
||||
self.base_resource_endpoint = url_path_join(GatewayClient.instance().url,
|
||||
GatewayClient.instance().kernelspecs_resource_endpoint)
|
||||
|
||||
@staticmethod
|
||||
def _get_endpoint_for_user_filter(default_endpoint):
|
||||
kernel_user = os.environ.get('KERNEL_USERNAME')
|
||||
if kernel_user:
|
||||
return '?user='.join([default_endpoint, kernel_user])
|
||||
return default_endpoint
|
||||
|
||||
def _get_kernelspecs_endpoint_url(self, kernel_name=None):
|
||||
"""Builds a url for the kernels endpoint
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_name: kernel name (optional)
|
||||
"""
|
||||
if kernel_name:
|
||||
return url_path_join(self.base_endpoint, url_escape(kernel_name))
|
||||
|
||||
return self.base_endpoint
|
||||
|
||||
async def get_all_specs(self):
|
||||
fetched_kspecs = await self.list_kernel_specs()
|
||||
|
||||
# get the default kernel name and compare to that of this server.
|
||||
# If different log a warning and reset the default. However, the
|
||||
# caller of this method will still return this server's value until
|
||||
# the next fetch of kernelspecs - at which time they'll match.
|
||||
km = self.parent.kernel_manager
|
||||
remote_default_kernel_name = fetched_kspecs.get('default')
|
||||
if remote_default_kernel_name != km.default_kernel_name:
|
||||
self.log.info("Default kernel name on Gateway server ({gateway_default}) differs from "
|
||||
"Notebook server ({notebook_default}). Updating to Gateway server's value.".
|
||||
format(gateway_default=remote_default_kernel_name,
|
||||
notebook_default=km.default_kernel_name))
|
||||
km.default_kernel_name = remote_default_kernel_name
|
||||
|
||||
remote_kspecs = fetched_kspecs.get('kernelspecs')
|
||||
return remote_kspecs
|
||||
|
||||
async def list_kernel_specs(self):
|
||||
"""Get a list of kernel specs."""
|
||||
kernel_spec_url = self._get_kernelspecs_endpoint_url()
|
||||
self.log.debug("Request list kernel specs at: %s", kernel_spec_url)
|
||||
response = await gateway_request(kernel_spec_url, method='GET')
|
||||
kernel_specs = json_decode(response.body)
|
||||
return kernel_specs
|
||||
|
||||
async def get_kernel_spec(self, kernel_name, **kwargs):
|
||||
"""Get kernel spec for kernel_name.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_name : str
|
||||
The name of the kernel.
|
||||
"""
|
||||
kernel_spec_url = self._get_kernelspecs_endpoint_url(kernel_name=str(kernel_name))
|
||||
self.log.debug("Request kernel spec at: %s" % kernel_spec_url)
|
||||
try:
|
||||
response = await gateway_request(kernel_spec_url, method='GET')
|
||||
except web.HTTPError as error:
|
||||
if error.status_code == 404:
|
||||
# Convert not found to KeyError since that's what the Notebook handler expects
|
||||
# message is not used, but might as well make it useful for troubleshooting
|
||||
raise KeyError(
|
||||
'kernelspec {kernel_name} not found on Gateway server at: {gateway_url}'.
|
||||
format(kernel_name=kernel_name, gateway_url=GatewayClient.instance().url)
|
||||
) from error
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
kernel_spec = json_decode(response.body)
|
||||
|
||||
return kernel_spec
|
||||
|
||||
async def get_kernel_spec_resource(self, kernel_name, path):
|
||||
"""Get kernel spec for kernel_name.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_name : str
|
||||
The name of the kernel.
|
||||
path : str
|
||||
The name of the desired resource
|
||||
"""
|
||||
kernel_spec_resource_url = url_path_join(self.base_resource_endpoint, str(kernel_name), str(path))
|
||||
self.log.debug("Request kernel spec resource '{}' at: {}".format(path, kernel_spec_resource_url))
|
||||
try:
|
||||
response = await gateway_request(kernel_spec_resource_url, method='GET')
|
||||
except web.HTTPError as error:
|
||||
if error.status_code == 404:
|
||||
kernel_spec_resource = None
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
kernel_spec_resource = response.body
|
||||
return kernel_spec_resource
|
||||
|
||||
|
||||
class GatewaySessionManager(SessionManager):
|
||||
kernel_manager = Instance('notebook.gateway.managers.GatewayKernelManager')
|
||||
|
||||
async def kernel_culled(self, kernel_id):
|
||||
"""Checks if the kernel is still considered alive and returns true if its not found. """
|
||||
kernel = await self.kernel_manager.get_kernel(kernel_id)
|
||||
return kernel is None
|
||||
@ -1,134 +0,0 @@
|
||||
# Implementation Notes for Internationalization of Jupyter Notebook
|
||||
|
||||
The implementation of i18n features for jupyter notebook is still a work-in-progress:
|
||||
|
||||
- User interface strings are (mostly) handled
|
||||
- Console messages are not handled (their usefulness in a translated environment is questionable)
|
||||
- Tooling has to be refined
|
||||
|
||||
However…
|
||||
|
||||
## How the language is selected ?
|
||||
|
||||
1. `jupyter notebook` command reads the `LANG` environment variable at startup,
|
||||
(`xx_XX` or just `xx` form, where `xx` is the language code you're wanting to
|
||||
run in).
|
||||
|
||||
Hint: if running Windows, you can set it in PowerShell with `${Env:LANG} = "xx_XX"`.
|
||||
if running Ubuntu 14, you should set environment variable `LANGUAGE="xx_XX"`.
|
||||
|
||||
2. The preferred language for web pages in your browser settings (`xx`) is
|
||||
also used. At the moment, it has to be first in the list.
|
||||
|
||||
## Contributing and managing translations
|
||||
|
||||
Finding and translating the `.pot` files could be (partially) done with a translation API, see the repo [Jupyter Notebook Azure Translator](https://github.com/berendjan/Jupyter-Notebook-Azure-Translator.git) for a possible starting point. (Not affiliated with Jupyter)
|
||||
|
||||
### Requirements
|
||||
|
||||
- *pybabel* (could be installed `pip install babel`)
|
||||
- *po2json* (could be installed with `npm install -g po2json`)
|
||||
|
||||
**All i18n-related commands are done from the related directory :**
|
||||
|
||||
cd notebook/i18n/
|
||||
|
||||
### Message extraction
|
||||
|
||||
The translatable material for notebook is split into 3 `.pot` files, as follows:
|
||||
|
||||
- *notebook/i18n/notebook.pot* - Console and startup messages, basically anything that is
|
||||
produced by Python code.
|
||||
- *notebook/i18n/nbui.pot* - User interface strings, as extracted from the Jinja2 templates
|
||||
in *notebook/templates/\*.html*
|
||||
- *noteook/i18n/nbjs.pot* - JavaScript strings and dialogs, which contain much of the visible
|
||||
user interface for Jupyter notebook.
|
||||
|
||||
To extract the messages from the source code whenever new material is added, use the
|
||||
`pybabel` command:
|
||||
|
||||
```shell
|
||||
pybabel extract -F babel_notebook.cfg -o notebook.pot --no-wrap --project Jupyter .
|
||||
pybabel extract -F babel_nbui.cfg -o nbui.pot --no-wrap --project Jupyter .
|
||||
pybabel extract -F babel_nbjs.cfg -o nbjs.pot --no-wrap --project Jupyter .
|
||||
```
|
||||
|
||||
After this is complete you have 3 `.pot` files that you can give to a translator for your favorite language.
|
||||
|
||||
Finding and translating the `.pot` files could be (partially done with a translation API, see the repo [Jupyter Notebook Azure Translator](https://github.com/berendjan/Jupyter-Notebook-Azure-Translator.git) for a possible starting point. (Not affiliated with Jupyter)
|
||||
|
||||
### Messages compilation
|
||||
|
||||
After the source material has been translated, you should have 3 `.po` files with the same base names
|
||||
as the `.pot` files above. Put them in `notebook/i18n/${LANG}/LC_MESSAGES`, where `${LANG}` is the language
|
||||
code for your desired language ( i.e. German = "de", Japanese = "ja", etc. ).
|
||||
|
||||
*notebook.po* and *nbui.po* need to be converted from `.po` to `.mo` format for
|
||||
use at runtime.
|
||||
|
||||
```shell
|
||||
pybabel compile -D notebook -f -l ${LANG} -i ${LANG}/LC_MESSAGES/notebook.po -o ${LANG}/LC_MESSAGES/notebook.mo
|
||||
pybabel compile -D nbui -f -l ${LANG} -i ${LANG}/LC_MESSAGES/nbui.po -o ${LANG}/LC_MESSAGES/nbui.mo
|
||||
```
|
||||
|
||||
*nbjs.po* needs to be converted to JSON for use within the JavaScript code, with *po2json*, as follows:
|
||||
|
||||
po2json -p -F -f jed1.x -d nbjs ${LANG}/LC_MESSAGES/nbjs.po ${LANG}/LC_MESSAGES/nbjs.json
|
||||
|
||||
When new languages get added, their language codes should be added to *notebook/i18n/nbjs.json*
|
||||
under the `supported_languages` element.
|
||||
|
||||
### Tips for Jupyter developers
|
||||
|
||||
The biggest "mistake" I found while doing i18n enablement was the habit of constructing UI messages
|
||||
from English "piece parts". For example, code like:
|
||||
|
||||
```javascript
|
||||
var msg = "Enter a new " + type + "name:"
|
||||
```
|
||||
|
||||
where `type` is either "file", "directory", or "notebook"....
|
||||
|
||||
is problematic when doing translations, because the surrounding text may need to vary
|
||||
depending on the inserted word. In this case, you need to switch it and use complete phrases,
|
||||
as follows:
|
||||
|
||||
```javascript
|
||||
var rename_msg = function (type) {
|
||||
switch(type) {
|
||||
case 'file': return _("Enter a new file name:");
|
||||
case 'directory': return _("Enter a new directory name:");
|
||||
case 'notebook': return _("Enter a new notebook name:");
|
||||
default: return _("Enter a new name:");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also you need to remember that adding an "s" or "es" to an English word to
|
||||
create the plural form doesn't translate well. Some languages have as many as 5 or 6 different
|
||||
plural forms for differing numbers, so using an API such as ngettext() is necessary in order
|
||||
to handle these cases properly.
|
||||
|
||||
### Known issues and future evolutions
|
||||
|
||||
1. Right now there are two different places where the desired language is set. At startup time, the Jupyter console's messages pay attention to the setting of the `${LANG}` environment variable
|
||||
as set in the shell at startup time. Unfortunately, this is also the time where the Jinja2
|
||||
environment is set up, which means that the template stuff will always come from this setting.
|
||||
We really want to be paying attention to the browser's settings for the stuff that happens in the
|
||||
browser, so we need to be able to retrieve this information after the browser is started and somehow
|
||||
communicate this back to Jinja2. So far, I haven't yet figured out how to do this, which means that if the ${LANG} at startup doesn't match the browser's settings, you could potentially get a mix
|
||||
of languages in the UI ( never a good thing ).
|
||||
|
||||
2. We will need to decide if console messages should be translatable, and enable them if desired.
|
||||
3. The keyboard shortcut editor was implemented after the i18n work was completed, so that portion
|
||||
does not have translation support at this time.
|
||||
4. Babel's documentation has instructions on how to integrate messages extraction
|
||||
into your *setup.py* so that eventually we can just do:
|
||||
|
||||
./setup.py extract_messages
|
||||
|
||||
I hope to get this working at some point in the near future.
|
||||
5. The conversions from `.po` to `.mo` probably can and should be done using `setup.py install`.
|
||||
|
||||
|
||||
Any questions or comments please let me know @JCEmmons on github (emmo@us.ibm.com)
|
||||
@ -1,103 +0,0 @@
|
||||
"""Server functions for loading translations
|
||||
"""
|
||||
from collections import defaultdict
|
||||
import errno
|
||||
import io
|
||||
import json
|
||||
from os.path import dirname, join as pjoin
|
||||
import re
|
||||
|
||||
I18N_DIR = dirname(__file__)
|
||||
# Cache structure:
|
||||
# {'nbjs': { # Domain
|
||||
# 'zh-CN': { # Language code
|
||||
# <english string>: <translated string>
|
||||
# ...
|
||||
# }
|
||||
# }}
|
||||
TRANSLATIONS_CACHE = {'nbjs': {}}
|
||||
|
||||
|
||||
_accept_lang_re = re.compile(r'''
|
||||
(?P<lang>[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?)
|
||||
(\s*;\s*q\s*=\s*
|
||||
(?P<qvalue>[01](.\d+)?)
|
||||
)?''', re.VERBOSE)
|
||||
|
||||
def parse_accept_lang_header(accept_lang):
|
||||
"""Parses the 'Accept-Language' HTTP header.
|
||||
|
||||
Returns a list of language codes in *ascending* order of preference
|
||||
(with the most preferred language last).
|
||||
"""
|
||||
by_q = defaultdict(list)
|
||||
for part in accept_lang.split(','):
|
||||
m = _accept_lang_re.match(part.strip())
|
||||
if not m:
|
||||
continue
|
||||
lang, qvalue = m.group('lang', 'qvalue')
|
||||
# Browser header format is zh-CN, gettext uses zh_CN
|
||||
lang = lang.replace('-', '_')
|
||||
if qvalue is None:
|
||||
qvalue = 1.
|
||||
else:
|
||||
qvalue = float(qvalue)
|
||||
if qvalue == 0:
|
||||
continue # 0 means not accepted
|
||||
by_q[qvalue].append(lang)
|
||||
if '_' in lang:
|
||||
short = lang.split('_')[0]
|
||||
if short != 'en':
|
||||
by_q[qvalue].append(short)
|
||||
|
||||
res = []
|
||||
for qvalue, langs in sorted(by_q.items()):
|
||||
res.extend(sorted(langs))
|
||||
return res
|
||||
|
||||
def load(language, domain='nbjs'):
|
||||
"""Load translations from an nbjs.json file"""
|
||||
try:
|
||||
f = io.open(pjoin(I18N_DIR, language, 'LC_MESSAGES', 'nbjs.json'),
|
||||
encoding='utf-8')
|
||||
except IOError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
return {}
|
||||
|
||||
with f:
|
||||
data = json.load(f)
|
||||
return data["locale_data"][domain]
|
||||
|
||||
def cached_load(language, domain='nbjs'):
|
||||
"""Load translations for one language, using in-memory cache if available"""
|
||||
domain_cache = TRANSLATIONS_CACHE[domain]
|
||||
try:
|
||||
return domain_cache[language]
|
||||
except KeyError:
|
||||
data = load(language, domain)
|
||||
domain_cache[language] = data
|
||||
return data
|
||||
|
||||
def combine_translations(accept_language, domain='nbjs'):
|
||||
"""Combine translations for multiple accepted languages.
|
||||
|
||||
Returns data re-packaged in jed1.x format.
|
||||
"""
|
||||
lang_codes = parse_accept_lang_header(accept_language)
|
||||
combined = {}
|
||||
for language in lang_codes:
|
||||
if language == 'en':
|
||||
# en is default, all translations are in frontend.
|
||||
combined.clear()
|
||||
else:
|
||||
combined.update(cached_load(language, domain))
|
||||
|
||||
combined[''] = {"domain":"nbjs"}
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"locale_data": {
|
||||
domain: combined
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
[javascript: notebook/static/base/js/*.js]
|
||||
extract_messages = $._, i18n.msg._
|
||||
|
||||
[javascript: notebook/static/notebook/js/*.js]
|
||||
extract_messages = $._, i18n.msg._
|
||||
|
||||
[javascript: notebook/static/notebook/js/celltoolbarpresets/*.js]
|
||||
extract_messages = $._, i18n.msg._
|
||||
|
||||
[javascript: notebook/static/tree/js/*.js]
|
||||
extract_messages = $._, i18n.msg._
|
||||
@ -1,4 +0,0 @@
|
||||
[jinja2: notebook/templates/**.html]
|
||||
encoding = utf-8
|
||||
[extractors]
|
||||
jinja2 = jinja2.ext:babel_extract
|
||||
@ -1,2 +0,0 @@
|
||||
[python: notebook/*.py]
|
||||
[python: notebook/services/contents/*.py]
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,745 +0,0 @@
|
||||
# Translations template for Jupyter.
|
||||
# Copyright (C) 2017 ORGANIZATION
|
||||
# This file is distributed under the same license as the Jupyter project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Jupyter VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2018-08-29 17:49+0200\n"
|
||||
"PO-Revision-Date: 2018-09-15 17:55+0200\n"
|
||||
"Last-Translator: Jocelyn Delalande <jocelyn@delalande.fr>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
"X-Generator: Poedit 1.8.11\n"
|
||||
|
||||
#: notebook/templates/404.html:3
|
||||
msgid "You are requesting a page that does not exist!"
|
||||
msgstr "Vous demandez une page qui n'existe pas !"
|
||||
|
||||
#: notebook/templates/edit.html:37
|
||||
msgid "current mode"
|
||||
msgstr "mode actuel"
|
||||
|
||||
#: notebook/templates/edit.html:48 notebook/templates/notebook.html:78
|
||||
msgid "File"
|
||||
msgstr "Fichier"
|
||||
|
||||
#: notebook/templates/edit.html:50 notebook/templates/tree.html:57
|
||||
msgid "New"
|
||||
msgstr "Nouveau"
|
||||
|
||||
#: notebook/templates/edit.html:51
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: notebook/templates/edit.html:52 notebook/templates/tree.html:36
|
||||
msgid "Rename"
|
||||
msgstr "Renommer"
|
||||
|
||||
#: notebook/templates/edit.html:53 notebook/templates/tree.html:38
|
||||
msgid "Download"
|
||||
msgstr "Télécharger"
|
||||
|
||||
#: notebook/templates/edit.html:56 notebook/templates/notebook.html:131
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit"
|
||||
msgstr "Édition"
|
||||
|
||||
#: notebook/templates/edit.html:58
|
||||
msgid "Find"
|
||||
msgstr "Rechercher"
|
||||
|
||||
#: notebook/templates/edit.html:59
|
||||
msgid "Find & Replace"
|
||||
msgstr "Rechercher & Remplacer"
|
||||
|
||||
#: notebook/templates/edit.html:61
|
||||
msgid "Key Map"
|
||||
msgstr "Raccourcis clavier"
|
||||
|
||||
#: notebook/templates/edit.html:62
|
||||
msgid "Default"
|
||||
msgstr "Par Défaut"
|
||||
|
||||
#: notebook/templates/edit.html:63
|
||||
msgid "Sublime Text"
|
||||
msgstr "Sublime Text"
|
||||
|
||||
#: notebook/templates/edit.html:68 notebook/templates/notebook.html:159
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View"
|
||||
msgstr "Affichage"
|
||||
|
||||
#: notebook/templates/edit.html:70 notebook/templates/notebook.html:162
|
||||
msgid "Show/Hide the logo and notebook title (above menu bar)"
|
||||
msgstr "Afficher/Masquer le logo et le titre du notebook (au-dessus de la "
|
||||
"barre de menu)"
|
||||
|
||||
#: notebook/templates/edit.html:71 notebook/templates/notebook.html:163
|
||||
msgid "Toggle Header"
|
||||
msgstr "Afficher/Masquer l'en-tête"
|
||||
|
||||
#: notebook/templates/edit.html:72 notebook/templates/notebook.html:171
|
||||
msgid "Toggle Line Numbers"
|
||||
msgstr "Afficher/Masquer les numéros de ligne"
|
||||
|
||||
#: notebook/templates/edit.html:75
|
||||
msgid "Language"
|
||||
msgstr "Langage"
|
||||
|
||||
#: notebook/templates/error.html:23
|
||||
msgid "The error was:"
|
||||
msgstr "L'erreur était :"
|
||||
|
||||
#: notebook/templates/login.html:24
|
||||
msgid "Password or token:"
|
||||
msgstr "Mot de passe ou jeton:"
|
||||
|
||||
#: notebook/templates/login.html:26
|
||||
msgid "Password:"
|
||||
msgstr "Mot de passe :"
|
||||
|
||||
#: notebook/templates/login.html:31
|
||||
msgid "Log in"
|
||||
msgstr "Se connecter"
|
||||
|
||||
#: notebook/templates/login.html:39
|
||||
msgid "No login available, you shouldn't be seeing this page."
|
||||
msgstr "Connexion non disponible, vous ne devriez pas voir cette page."
|
||||
|
||||
#: notebook/templates/logout.html:24
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)s\">dashboard"
|
||||
msgstr "Continuer vers le <a href=\"%(base_url)s\">tableau de bord"
|
||||
|
||||
#: notebook/templates/logout.html:26
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)slogin\">login page"
|
||||
msgstr "Continuer vers la <a href=\"%(base_url)slogin\">page de connexion"
|
||||
|
||||
#: notebook/templates/notebook.html:62
|
||||
msgid "Menu"
|
||||
msgstr "Menu"
|
||||
|
||||
#: notebook/templates/notebook.html:65 notebook/templates/notebook.html:254
|
||||
msgid "Kernel"
|
||||
msgstr "Noyau"
|
||||
|
||||
#: notebook/templates/notebook.html:68
|
||||
msgid "This notebook is read-only"
|
||||
msgstr "Ce notebook est en lecture seule"
|
||||
|
||||
#: notebook/templates/notebook.html:81
|
||||
msgid "New Notebook"
|
||||
msgstr "Nouveau Notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:85
|
||||
msgid "Opens a new window with the Dashboard view"
|
||||
msgstr "Ouvre une nouvelle fenêtre de tableau de bord"
|
||||
|
||||
#: notebook/templates/notebook.html:86
|
||||
msgid "Open..."
|
||||
msgstr "Ouvrir..."
|
||||
|
||||
#: notebook/templates/notebook.html:90
|
||||
msgid "Open a copy of this notebook's contents and start a new kernel"
|
||||
msgstr "Ouvrir une copie du contenu de ce notebook et démarrer un nouveau noyau"
|
||||
|
||||
#: notebook/templates/notebook.html:91
|
||||
msgid "Make a Copy..."
|
||||
msgstr "Faire une copie..."
|
||||
|
||||
#: notebook/templates/notebook.html:92
|
||||
msgid "Rename..."
|
||||
msgstr "Renommer..."
|
||||
|
||||
#: notebook/templates/notebook.html:93
|
||||
msgid "Save and Checkpoint"
|
||||
msgstr "Créer une nouvelle sauvegarde"
|
||||
|
||||
#: notebook/templates/notebook.html:96
|
||||
msgid "Revert to Checkpoint"
|
||||
msgstr "Restaurer la sauvegarde"
|
||||
|
||||
#: notebook/templates/notebook.html:106
|
||||
msgid "Print Preview"
|
||||
msgstr "Imprimer l'aperçu"
|
||||
|
||||
#: notebook/templates/notebook.html:107
|
||||
msgid "Download as"
|
||||
msgstr "Télécharger au format"
|
||||
|
||||
#: notebook/templates/notebook.html:109
|
||||
msgid "Notebook (.ipynb)"
|
||||
msgstr "Notebook (.ipynb)"
|
||||
|
||||
#: notebook/templates/notebook.html:110
|
||||
msgid "Script"
|
||||
msgstr "Script"
|
||||
|
||||
#: notebook/templates/notebook.html:111
|
||||
msgid "HTML (.html)"
|
||||
msgstr "HTML (.html)"
|
||||
|
||||
#: notebook/templates/notebook.html:112
|
||||
msgid "Markdown (.md)"
|
||||
msgstr "Markdown (.md)"
|
||||
|
||||
#: notebook/templates/notebook.html:113
|
||||
msgid "reST (.rst)"
|
||||
msgstr "reST (.rst)"
|
||||
|
||||
#: notebook/templates/notebook.html:114
|
||||
msgid "LaTeX (.tex)"
|
||||
msgstr "LaTeX (.tex)"
|
||||
|
||||
#: notebook/templates/notebook.html:115
|
||||
msgid "PDF via LaTeX (.pdf)"
|
||||
msgstr "PDF via LaTeX (.pdf)"
|
||||
|
||||
#: notebook/templates/notebook.html:118
|
||||
msgid "Deploy as"
|
||||
msgstr "Déployer en tant que"
|
||||
|
||||
#: notebook/templates/notebook.html:123
|
||||
msgid "Trust the output of this notebook"
|
||||
msgstr "Faire confiance à la sortie de ce notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:124
|
||||
msgid "Trust Notebook"
|
||||
msgstr "Faire confiance au notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:127
|
||||
msgid "Shutdown this notebook's kernel, and close this window"
|
||||
msgstr "Arrêter le noyau de ce notebook et fermer cette fenêtre"
|
||||
|
||||
#: notebook/templates/notebook.html:128
|
||||
msgid "Close and Halt"
|
||||
msgstr "Fermer et arrêter"
|
||||
|
||||
#: notebook/templates/notebook.html:133
|
||||
msgid "Cut Cells"
|
||||
msgstr "Couper les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:134
|
||||
msgid "Copy Cells"
|
||||
msgstr "Copier les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:135
|
||||
msgid "Paste Cells Above"
|
||||
msgstr "Coller les cellules avant"
|
||||
|
||||
#: notebook/templates/notebook.html:136
|
||||
msgid "Paste Cells Below"
|
||||
msgstr "Coller les cellules après"
|
||||
|
||||
#: notebook/templates/notebook.html:137
|
||||
msgid "Paste Cells & Replace"
|
||||
msgstr "Coller les cellules & remplacer"
|
||||
|
||||
#: notebook/templates/notebook.html:138
|
||||
msgid "Delete Cells"
|
||||
msgstr "Supprimer les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:139
|
||||
msgid "Undo Delete Cells"
|
||||
msgstr "Annuler la suppression des cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:141
|
||||
msgid "Split Cell"
|
||||
msgstr "Diviser la cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:142
|
||||
msgid "Merge Cell Above"
|
||||
msgstr "Fusionner avec la cellule précédente"
|
||||
|
||||
#: notebook/templates/notebook.html:143
|
||||
msgid "Merge Cell Below"
|
||||
msgstr "Fusionner avec la cellule suivante"
|
||||
|
||||
#: notebook/templates/notebook.html:145
|
||||
msgid "Move Cell Up"
|
||||
msgstr "Déplacer la cellule vers le haut"
|
||||
|
||||
#: notebook/templates/notebook.html:146
|
||||
msgid "Move Cell Down"
|
||||
msgstr "Déplacer la cellule vers le bas"
|
||||
|
||||
#: notebook/templates/notebook.html:148
|
||||
msgid "Edit Notebook Metadata"
|
||||
msgstr "Éditer les méta-données du notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:150
|
||||
msgid "Find and Replace"
|
||||
msgstr "Rechercher et remplacer"
|
||||
|
||||
#: notebook/templates/notebook.html:152
|
||||
msgid "Cut Cell Attachments"
|
||||
msgstr "Couper les pièces-Jointes de la cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:153
|
||||
msgid "Copy Cell Attachments"
|
||||
msgstr "Copier les pièces-jointes de la cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:154
|
||||
msgid "Paste Cell Attachments"
|
||||
msgstr "Coller les pièces-jointes de la cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:156
|
||||
msgid "Insert Image"
|
||||
msgstr "Insérer une image"
|
||||
|
||||
#: notebook/templates/notebook.html:166
|
||||
msgid "Show/Hide the action icons (below menu bar)"
|
||||
msgstr "Afficher/Masquer les icônes d'action (en-dessous de la barre de menu)"
|
||||
|
||||
#: notebook/templates/notebook.html:167
|
||||
msgid "Toggle Toolbar"
|
||||
msgstr "Afficher/Masquer la barre d'outils"
|
||||
|
||||
#: notebook/templates/notebook.html:170
|
||||
msgid "Show/Hide line numbers in cells"
|
||||
msgstr "Afficher/Masquer les numéros de ligne dans les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:174
|
||||
msgid "Cell Toolbar"
|
||||
msgstr "Barre d'outil de cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:179
|
||||
msgid "Insert"
|
||||
msgstr "Insérer"
|
||||
|
||||
#: notebook/templates/notebook.html:182
|
||||
msgid "Insert an empty Code cell above the currently active cell"
|
||||
msgstr "Insérer une cellule de code vide avant de la cellule active"
|
||||
|
||||
#: notebook/templates/notebook.html:183
|
||||
msgid "Insert Cell Above"
|
||||
msgstr "Insérer une cellule avant"
|
||||
|
||||
#: notebook/templates/notebook.html:185
|
||||
msgid "Insert an empty Code cell below the currently active cell"
|
||||
msgstr "Insérer une cellule de code vide après la cellule active"
|
||||
|
||||
#: notebook/templates/notebook.html:186
|
||||
msgid "Insert Cell Below"
|
||||
msgstr "Insérer une cellule après"
|
||||
|
||||
#: notebook/templates/notebook.html:189
|
||||
msgid "Cell"
|
||||
msgstr "Cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:191
|
||||
msgid "Run this cell, and move cursor to the next one"
|
||||
msgstr "Exécuter cette cellule, et déplacer le curseur à la suivante"
|
||||
|
||||
#: notebook/templates/notebook.html:192
|
||||
msgid "Run Cells"
|
||||
msgstr "Exécuter les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:193
|
||||
msgid "Run this cell, select below"
|
||||
msgstr "Exécuter cette cellule, sélectionner la suivante"
|
||||
|
||||
#: notebook/templates/notebook.html:194
|
||||
msgid "Run Cells and Select Below"
|
||||
msgstr "Exécuter les cellules et sélectionner la suivante"
|
||||
|
||||
#: notebook/templates/notebook.html:195
|
||||
msgid "Run this cell, insert below"
|
||||
msgstr "Exécuter la cellule et insérer à la suite"
|
||||
|
||||
#: notebook/templates/notebook.html:196
|
||||
msgid "Run Cells and Insert Below"
|
||||
msgstr "Exécuter les cellules et insérer après"
|
||||
|
||||
#: notebook/templates/notebook.html:197
|
||||
msgid "Run all cells in the notebook"
|
||||
msgstr "Exécuter toutes les cellules du notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:198
|
||||
msgid "Run All"
|
||||
msgstr "Exécuter tout"
|
||||
|
||||
#: notebook/templates/notebook.html:199
|
||||
msgid "Run all cells above (but not including) this cell"
|
||||
msgstr "Exécuter toutes les cellules avant celle-ci (non incluse)"
|
||||
|
||||
#: notebook/templates/notebook.html:200
|
||||
msgid "Run All Above"
|
||||
msgstr "Exécuter toutes les précédentes"
|
||||
|
||||
#: notebook/templates/notebook.html:201
|
||||
msgid "Run this cell and all cells below it"
|
||||
msgstr "Exécuter cette cellule et toutes les suivantes"
|
||||
|
||||
#: notebook/templates/notebook.html:202
|
||||
msgid "Run All Below"
|
||||
msgstr "Exécuter toutes les suivantes"
|
||||
|
||||
#: notebook/templates/notebook.html:205
|
||||
msgid ""
|
||||
"All cells in the notebook have a cell type. By default, new cells are "
|
||||
"created as 'Code' cells"
|
||||
msgstr ""
|
||||
"Toutes les cellules dans le notebook ont un type de "
|
||||
"cellule. Par défaut, les nouvelles cellules sont de type 'Code'"
|
||||
|
||||
#: notebook/templates/notebook.html:206
|
||||
msgid "Cell Type"
|
||||
msgstr "Type de cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:209
|
||||
msgid ""
|
||||
"Contents will be sent to the kernel for execution, and output will display "
|
||||
"in the footer of cell"
|
||||
msgstr ""
|
||||
"Le contenu sera envoyé au noyau pour exécution, et la sortie sera affichée "
|
||||
"dans le pied de cellule"
|
||||
|
||||
#: notebook/templates/notebook.html:212
|
||||
msgid "Contents will be rendered as HTML and serve as explanatory text"
|
||||
msgstr ""
|
||||
"Le contenu sera rendu en tant que HTML afin de servir de texte explicatif"
|
||||
|
||||
#: notebook/templates/notebook.html:213 notebook/templates/notebook.html:298
|
||||
msgid "Markdown"
|
||||
msgstr "Markdown"
|
||||
|
||||
#: notebook/templates/notebook.html:215
|
||||
msgid "Contents will pass through nbconvert unmodified"
|
||||
msgstr "Le contenu passera par nbconvert qui ne l'altèrera pas"
|
||||
|
||||
#: notebook/templates/notebook.html:216
|
||||
msgid "Raw NBConvert"
|
||||
msgstr "Texte Brut (pour NBConvert)"
|
||||
|
||||
#: notebook/templates/notebook.html:220
|
||||
msgid "Current Outputs"
|
||||
msgstr "Sorties actuelles"
|
||||
|
||||
#: notebook/templates/notebook.html:223
|
||||
msgid "Hide/Show the output of the current cell"
|
||||
msgstr "Masquer/Afficher la sortie de la cellule actuelle"
|
||||
|
||||
#: notebook/templates/notebook.html:224 notebook/templates/notebook.html:240
|
||||
msgid "Toggle"
|
||||
msgstr "Afficher/Masquer"
|
||||
|
||||
#: notebook/templates/notebook.html:227
|
||||
msgid "Scroll the output of the current cell"
|
||||
msgstr "Faire défiler la sortie de la cellule actuelle"
|
||||
|
||||
#: notebook/templates/notebook.html:228 notebook/templates/notebook.html:244
|
||||
msgid "Toggle Scrolling"
|
||||
msgstr "Activer/Désactiver le défilement"
|
||||
|
||||
#: notebook/templates/notebook.html:231
|
||||
msgid "Clear the output of the current cell"
|
||||
msgstr "Effacer la sortie de la cellule actuelle"
|
||||
|
||||
#: notebook/templates/notebook.html:232 notebook/templates/notebook.html:248
|
||||
msgid "Clear"
|
||||
msgstr "Effacer"
|
||||
|
||||
#: notebook/templates/notebook.html:236
|
||||
msgid "All Output"
|
||||
msgstr "Toute la sortie"
|
||||
|
||||
#: notebook/templates/notebook.html:239
|
||||
msgid "Hide/Show the output of all cells"
|
||||
msgstr "Afficher/Masquer la sortie de toutes les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:243
|
||||
msgid "Scroll the output of all cells"
|
||||
msgstr "Faire défiler la sortie de toutes les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:247
|
||||
msgid "Clear the output of all cells"
|
||||
msgstr "Effacer la sortie de toutes les cellules"
|
||||
|
||||
#: notebook/templates/notebook.html:257
|
||||
msgid "Send Keyboard Interrupt (CTRL-C) to the Kernel"
|
||||
msgstr "Envoyer l'interruption clavier (CTRL-C) au noyau"
|
||||
|
||||
#: notebook/templates/notebook.html:258
|
||||
msgid "Interrupt"
|
||||
msgstr "Interrompre"
|
||||
|
||||
#: notebook/templates/notebook.html:261
|
||||
msgid "Restart the Kernel"
|
||||
msgstr "Redémarrer le noyau"
|
||||
|
||||
#: notebook/templates/notebook.html:262
|
||||
msgid "Restart"
|
||||
msgstr "Redémarrer"
|
||||
|
||||
#: notebook/templates/notebook.html:265
|
||||
msgid "Restart the Kernel and clear all output"
|
||||
msgstr "Redémarrer le noyau et effacer toutes les sorties"
|
||||
|
||||
#: notebook/templates/notebook.html:266
|
||||
msgid "Restart & Clear Output"
|
||||
msgstr "Redémarrer & effacer les sorties"
|
||||
|
||||
#: notebook/templates/notebook.html:269
|
||||
msgid "Restart the Kernel and re-run the notebook"
|
||||
msgstr "Redémarrer le noyau et ré-exécuter le noteboook"
|
||||
|
||||
#: notebook/templates/notebook.html:270
|
||||
msgid "Restart & Run All"
|
||||
msgstr "Redémarrer & tout exécuter"
|
||||
|
||||
#: notebook/templates/notebook.html:273
|
||||
msgid "Reconnect to the Kernel"
|
||||
msgstr "Reconnecter au noyau"
|
||||
|
||||
#: notebook/templates/notebook.html:274
|
||||
msgid "Reconnect"
|
||||
msgstr "Reconnecter"
|
||||
|
||||
#: notebook/templates/notebook.html:282
|
||||
msgid "Change kernel"
|
||||
msgstr "Changer de noyau"
|
||||
|
||||
#: notebook/templates/notebook.html:287
|
||||
msgid "Help"
|
||||
msgstr "Aide"
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "A quick tour of the notebook user interface"
|
||||
msgstr "Une rapide visite de l'interface utilisateur du notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "User Interface Tour"
|
||||
msgstr "Visite de l'interface utilisateur"
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Opens a tooltip with all keyboard shortcuts"
|
||||
msgstr "Ouvre une infobulle listant tous les raccourcis clavier"
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr "Raccourcis clavier"
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Opens a dialog allowing you to edit Keyboard shortcuts"
|
||||
msgstr "Ouvre une boîte de dialogue permettant de modifier les raccourcis clavier"
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Edit Keyboard Shortcuts"
|
||||
msgstr "Editer les raccourcis clavier"
|
||||
|
||||
#: notebook/templates/notebook.html:297
|
||||
msgid "Notebook Help"
|
||||
msgstr "Aide notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:303
|
||||
msgid "Opens in a new window"
|
||||
msgstr "S'ouvre dans une nouvelle fenêtre"
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About Jupyter Notebook"
|
||||
msgstr "À propos de Jupyter Notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About"
|
||||
msgstr "À propos"
|
||||
|
||||
#: notebook/templates/page.html:114
|
||||
msgid "Jupyter Notebook requires JavaScript."
|
||||
msgstr "Jupyter Notebook nécessite JavaScript"
|
||||
|
||||
#: notebook/templates/page.html:115
|
||||
msgid "Please enable it to proceed. "
|
||||
msgstr "Merci de l'activer pour continuer."
|
||||
|
||||
#: notebook/templates/page.html:121
|
||||
msgid "dashboard"
|
||||
msgstr "tableau de bord"
|
||||
|
||||
#: notebook/templates/page.html:132
|
||||
msgid "Logout"
|
||||
msgstr "Se déconnecter"
|
||||
|
||||
#: notebook/templates/page.html:134
|
||||
msgid "Login"
|
||||
msgstr "Se connecter"
|
||||
|
||||
#: notebook/templates/tree.html:23
|
||||
msgid "Files"
|
||||
msgstr "Fichiers"
|
||||
|
||||
#: notebook/templates/tree.html:24
|
||||
msgid "Running"
|
||||
msgstr "Actifs"
|
||||
|
||||
#: notebook/templates/tree.html:25
|
||||
msgid "Clusters"
|
||||
msgstr "Grappes"
|
||||
|
||||
#: notebook/templates/tree.html:32
|
||||
msgid "Select items to perform actions on them."
|
||||
msgstr "Sélectionner des éléments pour leur appliquer des actions."
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate selected"
|
||||
msgstr "Dupliquer la sélection"
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate"
|
||||
msgstr "Dupliquer"
|
||||
|
||||
#: notebook/templates/tree.html:36
|
||||
msgid "Rename selected"
|
||||
msgstr "Renommer la sélection"
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move selected"
|
||||
msgstr "Déplacer la sélection"
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move"
|
||||
msgstr "Déplacer"
|
||||
|
||||
#: notebook/templates/tree.html:38
|
||||
msgid "Download selected"
|
||||
msgstr "Télécharger la sélection"
|
||||
|
||||
#: notebook/templates/tree.html:39
|
||||
msgid "Shutdown selected notebook(s)"
|
||||
msgstr "Arrêter le(s) notebook(s) sélectionné(s)"
|
||||
|
||||
#: notebook/templates/notebook.html:278 notebook/templates/tree.html:39
|
||||
msgid "Shutdown"
|
||||
msgstr "Arrêter"
|
||||
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View selected"
|
||||
msgstr "Voir la sélection"
|
||||
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit selected"
|
||||
msgstr "Éditer la sélection"
|
||||
|
||||
#: notebook/templates/tree.html:42
|
||||
msgid "Delete selected"
|
||||
msgstr "Supprimer la sélection"
|
||||
|
||||
#: notebook/templates/tree.html:50
|
||||
msgid "Click to browse for a file to upload."
|
||||
msgstr "Cliquer pour choisir un fichier à téléverser"
|
||||
|
||||
#: notebook/templates/tree.html:51
|
||||
msgid "Upload"
|
||||
msgstr "Téléverser"
|
||||
|
||||
#: notebook/templates/tree.html:65
|
||||
msgid "Text File"
|
||||
msgstr "Fichier texte"
|
||||
|
||||
#: notebook/templates/tree.html:68
|
||||
msgid "Folder"
|
||||
msgstr "Répertoire"
|
||||
|
||||
#: notebook/templates/tree.html:72
|
||||
msgid "Terminal"
|
||||
msgstr "Terminal"
|
||||
|
||||
#: notebook/templates/tree.html:76
|
||||
msgid "Terminals Unavailable"
|
||||
msgstr "Terminaux non disponibles"
|
||||
|
||||
#: notebook/templates/tree.html:82
|
||||
msgid "Refresh notebook list"
|
||||
msgstr "Rafraîchir la liste des notebooks"
|
||||
|
||||
#: notebook/templates/tree.html:90
|
||||
msgid "Select All / None"
|
||||
msgstr "Sélectionner tout / aucun"
|
||||
|
||||
#: notebook/templates/tree.html:93
|
||||
msgid "Select..."
|
||||
msgstr "Sélectionner..."
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Select All Folders"
|
||||
msgstr "Sélectionner tous les répertoires"
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Folders"
|
||||
msgstr "Répertoires"
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "Select All Notebooks"
|
||||
msgstr "Sélectionner tous les notebooks"
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "All Notebooks"
|
||||
msgstr "Tous les notebooks"
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Select Running Notebooks"
|
||||
msgstr "Sélectionner les notebooks en cours d'exécution"
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Running"
|
||||
msgstr "Actifs"
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Select All Files"
|
||||
msgstr "Sélectionner tous les fichiers"
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Files"
|
||||
msgstr "Fichiers"
|
||||
|
||||
#: notebook/templates/tree.html:114
|
||||
msgid "Last Modified"
|
||||
msgstr "Dernière modification"
|
||||
|
||||
#: notebook/templates/tree.html:120
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: notebook/templates/tree.html:130
|
||||
msgid "Currently running Jupyter processes"
|
||||
msgstr "Processus Jupyter en cours d'exécution"
|
||||
|
||||
#: notebook/templates/tree.html:134
|
||||
msgid "Refresh running list"
|
||||
msgstr "Rafraîchir la liste des actifs"
|
||||
|
||||
#: notebook/templates/tree.html:150
|
||||
msgid "There are no terminals running."
|
||||
msgstr "Il n'y a aucun terminal en cours d'exécution."
|
||||
|
||||
#: notebook/templates/tree.html:152
|
||||
msgid "Terminals are unavailable."
|
||||
msgstr "Les terminaux sont indisponibles."
|
||||
|
||||
#: notebook/templates/tree.html:162
|
||||
msgid "Notebooks"
|
||||
msgstr "Notebooks"
|
||||
|
||||
#: notebook/templates/tree.html:169
|
||||
msgid "There are no notebooks running."
|
||||
msgstr "Il n'y a aucun notebook en cours d'exécution."
|
||||
|
||||
#: notebook/templates/tree.html:178
|
||||
msgid "Clusters tab is now provided by IPython parallel."
|
||||
msgstr "L'onglet des grappes est désormais fourni par IPython parallel."
|
||||
|
||||
#: notebook/templates/tree.html:179
|
||||
msgid ""
|
||||
"See '<a href=\"https://github.com/ipython/ipyparallel\">IPython parallel</"
|
||||
"a>' for installation details."
|
||||
msgstr ""
|
||||
"Voir '<a href=\"https://github.com/ipython/ipyparallel\">IPython parallel</"
|
||||
"a>' pour les détails d'installation."
|
||||
@ -1,480 +0,0 @@
|
||||
# Translations template for Jupyter.
|
||||
# Copyright (C) 2017 ORGANIZATION
|
||||
# This file is distributed under the same license as the Jupyter project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Jupyter VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2017-07-08 21:52-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: notebook/notebookapp.py:53
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:57
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have < 1.1.0"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:59
|
||||
#, python-format
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:209
|
||||
msgid "The `ignore_minified_js` flag is deprecated and no longer works."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:210
|
||||
#, python-format
|
||||
msgid "Alternatively use `%s` when working on the notebook's Javascript and LESS"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:211
|
||||
msgid "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:389
|
||||
msgid "List currently running notebook servers."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:393
|
||||
msgid "Produce machine-readable JSON output."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:397
|
||||
msgid "If True, each line of output will be a JSON object with the details from the server info file."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:402
|
||||
msgid "Currently running servers:"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:419
|
||||
msgid "Don't open the notebook in a browser after startup."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:423
|
||||
msgid "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:439
|
||||
msgid "Allow the notebook to be run from root user."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:470
|
||||
msgid ""
|
||||
"The Jupyter HTML Notebook.\n"
|
||||
" \n"
|
||||
" This launches a Tornado based HTML Notebook Server that serves up an HTML5/Javascript Notebook client."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:509
|
||||
msgid "Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:540
|
||||
msgid "Set the Access-Control-Allow-Credentials: true header"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:544
|
||||
msgid "Whether to allow the user to run the notebook as root."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:548
|
||||
msgid "The default URL to redirect to from `/`"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:552
|
||||
msgid "The IP address the notebook server will listen on."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:565
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Cannot bind to localhost, using 127.0.0.1 as default ip\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:579
|
||||
msgid "The port the notebook server will listen on."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:583
|
||||
msgid "The number of additional ports to try if the specified port is not available."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:587
|
||||
msgid "The full path to an SSL/TLS certificate file."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:591
|
||||
msgid "The full path to a private key file for usage with SSL/TLS."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:595
|
||||
msgid "The full path to a certificate authority certificate for SSL/TLS client authentication."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:599
|
||||
msgid "The file where the cookie secret is stored."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:628
|
||||
#, python-format
|
||||
msgid "Writing notebook server cookie secret to %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:635
|
||||
#, python-format
|
||||
msgid "Could not set permissions on %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:640
|
||||
msgid ""
|
||||
"Token used for authenticating first-time connections to the server.\n"
|
||||
"\n"
|
||||
" When no password is enabled,\n"
|
||||
" the default is to generate a new, random token.\n"
|
||||
"\n"
|
||||
" Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:650
|
||||
msgid ""
|
||||
"One-time token used for opening a browser.\n"
|
||||
" Once used, this token cannot be used again.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:726
|
||||
msgid ""
|
||||
"Specify Where to open the notebook on startup. This is the\n"
|
||||
" `new` argument passed to the standard library method `webbrowser.open`.\n"
|
||||
" The behaviour is not guaranteed, but depends on browser support. Valid\n"
|
||||
" values are:\n"
|
||||
" 2 opens a new tab,\n"
|
||||
" 1 opens a new window,\n"
|
||||
" 0 opens in an existing window.\n"
|
||||
" See the `webbrowser.open` documentation for details.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:737
|
||||
msgid "DEPRECATED, use tornado_settings"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:742
|
||||
msgid ""
|
||||
"\n"
|
||||
" webapp_settings is deprecated, use tornado_settings.\n"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:746
|
||||
msgid "Supply overrides for the tornado.web.Application that the Jupyter notebook uses."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:750
|
||||
msgid ""
|
||||
"\n"
|
||||
" Set the tornado compression options for websocket connections.\n"
|
||||
"\n"
|
||||
" This value will be returned from :meth:`WebSocketHandler.get_compression_options`.\n"
|
||||
" None (default) will disable compression.\n"
|
||||
" A dict (even an empty one) will enable compression.\n"
|
||||
"\n"
|
||||
" See the tornado docs for WebSocketHandler.get_compression_options for details.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:761
|
||||
msgid "Supply overrides for terminado. Currently only supports \"shell_command\"."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:764
|
||||
msgid "Extra keyword arguments to pass to `set_secure_cookie`. See tornado's set_secure_cookie docs for details."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:768
|
||||
msgid ""
|
||||
"Supply SSL options for the tornado HTTPServer.\n"
|
||||
" See the tornado docs for details."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:772
|
||||
msgid "Supply extra arguments that will be passed to Jinja environment."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:776
|
||||
msgid "Extra variables to supply to jinja templates when rendering."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:812
|
||||
msgid "DEPRECATED use base_url"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:816
|
||||
msgid "base_project_url is deprecated, use base_url"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:832
|
||||
msgid "Path to search for custom.js, css"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:844
|
||||
msgid ""
|
||||
"Extra paths to search for serving jinja templates.\n"
|
||||
"\n"
|
||||
" Can be used to override templates from notebook.templates."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:855
|
||||
msgid "extra paths to look for Javascript notebook extensions"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:900
|
||||
#, python-format
|
||||
msgid "Using MathJax: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:903
|
||||
msgid "The MathJax.js configuration file that is to be used."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:908
|
||||
#, python-format
|
||||
msgid "Using MathJax configuration file: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:914
|
||||
msgid "The notebook manager class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:920
|
||||
msgid "The kernel manager class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:926
|
||||
msgid "The session manager class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:932
|
||||
msgid "The config manager class to use"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:953
|
||||
msgid "The login handler class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:960
|
||||
msgid "The logout handler class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:964
|
||||
msgid "Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headerssent by the upstream reverse proxy. Necessary if the proxy handles SSL"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:976
|
||||
msgid ""
|
||||
"\n"
|
||||
" DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:988
|
||||
msgid "Support for specifying --pylab on the command line has been removed."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:990
|
||||
msgid "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:995
|
||||
msgid "The directory to use for notebooks and kernels."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1018
|
||||
#, python-format
|
||||
msgid "No such notebook dir: '%r'"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1031
|
||||
msgid "DEPRECATED use the nbserver_extensions dict instead"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1036
|
||||
msgid "server_extensions is deprecated, use nbserver_extensions"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1040
|
||||
msgid "Dict of Python modules to load as notebook server extensions.Entry values can be used to enable and disable the loading ofthe extensions. The extensions will be loaded in alphabetical order."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1049
|
||||
msgid "Reraise exceptions encountered loading server extensions?"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1052
|
||||
msgid ""
|
||||
"(msgs/sec)\n"
|
||||
" Maximum rate at which messages can be sent on iopub before they are\n"
|
||||
" limited."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1056
|
||||
msgid ""
|
||||
"(bytes/sec)\n"
|
||||
" Maximum rate at which stream output can be sent on iopub before they are\n"
|
||||
" limited."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1060
|
||||
msgid ""
|
||||
"(sec) Time window used to \n"
|
||||
" check the message and data rate limits."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1071
|
||||
#, python-format
|
||||
msgid "No such file or directory: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1141
|
||||
msgid "Notebook servers are configured to only be run with a password."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1142
|
||||
msgid "Hint: run the following command to set a password"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1143
|
||||
msgid "\t$ python -m notebook.auth password"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1181
|
||||
#, python-format
|
||||
msgid "The port %i is already in use, trying another port."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1184
|
||||
#, python-format
|
||||
msgid "Permission to listen on port %i denied"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1193
|
||||
msgid "ERROR: the notebook server could not be started because no available port could be found."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1199
|
||||
msgid "[all ip addresses on your system]"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1223
|
||||
#, python-format
|
||||
msgid "Terminals not available (error was %s)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1259
|
||||
msgid "interrupted"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1261
|
||||
msgid "y"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1262
|
||||
msgid "n"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1263
|
||||
#, python-format
|
||||
msgid "Shutdown this notebook server (%s/[%s])? "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1269
|
||||
msgid "Shutdown confirmed"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1273
|
||||
msgid "No answer for 5s:"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1274
|
||||
msgid "resuming operation..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1282
|
||||
#, python-format
|
||||
msgid "received signal %s, stopping"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1338
|
||||
#, python-format
|
||||
msgid "Error loading server extension %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1369
|
||||
#, python-format
|
||||
msgid "Shutting down %d kernels"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1375
|
||||
#, python-format
|
||||
msgid "%d active kernel"
|
||||
msgid_plural "%d active kernels"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: notebook/notebookapp.py:1379
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The Jupyter Notebook is running at:\n"
|
||||
"\r"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1426
|
||||
msgid "Running as root is not recommended. Use --allow-root to bypass."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1432
|
||||
msgid "Use Control-C to stop this server and shut down all kernels (twice to skip confirmation)."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1434
|
||||
msgid "Welcome to Project Jupyter! Explore the various tools available and their corresponding documentation. If you are interested in contributing to the platform, please visit the communityresources section at http://jupyter.org/community.html."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1445
|
||||
#, python-format
|
||||
msgid "No web browser found: %s."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1450
|
||||
#, python-format
|
||||
msgid "%s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1484
|
||||
msgid "Interrupted..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/services/contents/filemanager.py:506
|
||||
#, python-format
|
||||
msgid "Serving notebooks from local directory: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/services/contents/manager.py:68
|
||||
msgid "Untitled"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,740 +0,0 @@
|
||||
# Translations template for Jupyter.
|
||||
# Copyright (C) 2017 ORGANIZATION
|
||||
# This file is distributed under the same license as the Jupyter project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Jupyter VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2017-07-07 12:48-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: notebook/templates/404.html:3
|
||||
msgid "You are requesting a page that does not exist!"
|
||||
msgstr "要求したページは存在しません!"
|
||||
|
||||
#: notebook/templates/edit.html:37
|
||||
msgid "current mode"
|
||||
msgstr "現在のモード"
|
||||
|
||||
#: notebook/templates/edit.html:48 notebook/templates/notebook.html:78
|
||||
msgid "File"
|
||||
msgstr "ファイル"
|
||||
|
||||
#: notebook/templates/edit.html:50 notebook/templates/tree.html:57
|
||||
msgid "New"
|
||||
msgstr "新規"
|
||||
|
||||
#: notebook/templates/edit.html:51
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
#: notebook/templates/edit.html:52 notebook/templates/tree.html:36
|
||||
msgid "Rename"
|
||||
msgstr "リネーム"
|
||||
|
||||
#: notebook/templates/edit.html:53 notebook/templates/tree.html:38
|
||||
msgid "Download"
|
||||
msgstr "ダウンロード"
|
||||
|
||||
#: notebook/templates/edit.html:56 notebook/templates/notebook.html:131
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit"
|
||||
msgstr "編集"
|
||||
|
||||
#: notebook/templates/edit.html:58
|
||||
msgid "Find"
|
||||
msgstr "検索"
|
||||
|
||||
#: notebook/templates/edit.html:59
|
||||
msgid "Find & Replace"
|
||||
msgstr "検索と置換"
|
||||
|
||||
#: notebook/templates/edit.html:61
|
||||
msgid "Key Map"
|
||||
msgstr "キーマッピング"
|
||||
|
||||
#: notebook/templates/edit.html:62
|
||||
msgid "Default"
|
||||
msgstr "デフォルト"
|
||||
|
||||
#: notebook/templates/edit.html:63
|
||||
msgid "Sublime Text"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:68 notebook/templates/notebook.html:159
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View"
|
||||
msgstr "表示"
|
||||
|
||||
#: notebook/templates/edit.html:70 notebook/templates/notebook.html:162
|
||||
msgid "Show/Hide the logo and notebook title (above menu bar)"
|
||||
msgstr "ロゴとノートブックのタイトルを表示/非表示 (メニューバーの上)"
|
||||
|
||||
#: notebook/templates/edit.html:71 notebook/templates/notebook.html:163
|
||||
msgid "Toggle Header"
|
||||
msgstr "ヘッダをトグル"
|
||||
|
||||
#: notebook/templates/edit.html:72 notebook/templates/notebook.html:171
|
||||
msgid "Toggle Line Numbers"
|
||||
msgstr "行番号をトグル"
|
||||
|
||||
#: notebook/templates/edit.html:75
|
||||
msgid "Language"
|
||||
msgstr "言語"
|
||||
|
||||
#: notebook/templates/error.html:23
|
||||
msgid "The error was:"
|
||||
msgstr "エラー内容:"
|
||||
|
||||
#: notebook/templates/login.html:24
|
||||
msgid "Password or token:"
|
||||
msgstr "パスワードまたはトークン:"
|
||||
|
||||
#: notebook/templates/login.html:26
|
||||
msgid "Password:"
|
||||
msgstr "パスワード:"
|
||||
|
||||
#: notebook/templates/login.html:31
|
||||
msgid "Log in"
|
||||
msgstr "ログイン"
|
||||
|
||||
#: notebook/templates/login.html:39
|
||||
msgid "No login available, you shouldn't be seeing this page."
|
||||
msgstr "ログインしていないのでこのページを見る事はできません。"
|
||||
|
||||
#: notebook/templates/logout.html:24
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)s\">dashboard"
|
||||
msgstr "<a href=\"%(base_url)s\">ダッシュボード"
|
||||
|
||||
#: notebook/templates/logout.html:26
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)slogin\">login page"
|
||||
msgstr "<a href=\"%(base_url)slogin\">ログインページ"
|
||||
|
||||
#: notebook/templates/notebook.html:62
|
||||
msgid "Menu"
|
||||
msgstr "メニュー"
|
||||
|
||||
#: notebook/templates/notebook.html:65 notebook/templates/notebook.html:254
|
||||
msgid "Kernel"
|
||||
msgstr "カーネル"
|
||||
|
||||
#: notebook/templates/notebook.html:68
|
||||
msgid "This notebook is read-only"
|
||||
msgstr "このノートブックは読み取り専用です"
|
||||
|
||||
#: notebook/templates/notebook.html:81
|
||||
msgid "New Notebook"
|
||||
msgstr "新しいノートブック"
|
||||
|
||||
#: notebook/templates/notebook.html:85
|
||||
msgid "Opens a new window with the Dashboard view"
|
||||
msgstr "ダッシュボードで新しいウィンドウを開く"
|
||||
|
||||
#: notebook/templates/notebook.html:86
|
||||
msgid "Open..."
|
||||
msgstr "開く..."
|
||||
|
||||
#: notebook/templates/notebook.html:90
|
||||
msgid "Open a copy of this notebook's contents and start a new kernel"
|
||||
msgstr "このノートブックの内容の複製を開き新しいカーネルを起動する"
|
||||
|
||||
#: notebook/templates/notebook.html:91
|
||||
msgid "Make a Copy..."
|
||||
msgstr "コピーを作る..."
|
||||
|
||||
#: notebook/templates/notebook.html:92
|
||||
msgid "Rename..."
|
||||
msgstr "リネーム..."
|
||||
|
||||
msgid "Save as..."
|
||||
msgstr "名前を付けて保存..."
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "終了"
|
||||
|
||||
#: notebook/templates/notebook.html:93
|
||||
msgid "Save and Checkpoint"
|
||||
msgstr "保存とチェックポイント"
|
||||
|
||||
#: notebook/templates/notebook.html:96
|
||||
msgid "Revert to Checkpoint"
|
||||
msgstr "チェックポイントを元に戻す"
|
||||
|
||||
#: notebook/templates/notebook.html:106
|
||||
msgid "Print Preview"
|
||||
msgstr "印刷プレビュー"
|
||||
|
||||
#: notebook/templates/notebook.html:107
|
||||
msgid "Download as"
|
||||
msgstr "名前を付けてダウンロード"
|
||||
|
||||
#: notebook/templates/notebook.html:109
|
||||
msgid "Notebook (.ipynb)"
|
||||
msgstr "ノートブック (.ipynb)"
|
||||
|
||||
#: notebook/templates/notebook.html:110
|
||||
msgid "Script"
|
||||
msgstr "スクリプト"
|
||||
|
||||
#: notebook/templates/notebook.html:111
|
||||
msgid "HTML (.html)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:112
|
||||
msgid "Markdown (.md)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:113
|
||||
msgid "reST (.rst)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:114
|
||||
msgid "LaTeX (.tex)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:115
|
||||
msgid "PDF via LaTeX (.pdf)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:118
|
||||
msgid "Deploy as"
|
||||
msgstr "名前を付けてデプロイ"
|
||||
|
||||
#: notebook/templates/notebook.html:123
|
||||
msgid "Trust the output of this notebook"
|
||||
msgstr "このノートブックの出力を信頼する"
|
||||
|
||||
#: notebook/templates/notebook.html:124
|
||||
msgid "Trust Notebook"
|
||||
msgstr "ノートブックを信頼する"
|
||||
|
||||
#: notebook/templates/notebook.html:127
|
||||
msgid "Shutdown this notebook's kernel, and close this window"
|
||||
msgstr "このノートブックのカーネルをシャットダウンし、このウィンドウを閉じる"
|
||||
|
||||
#: notebook/templates/notebook.html:128
|
||||
msgid "Close and Halt"
|
||||
msgstr "閉じて終了"
|
||||
|
||||
#: notebook/templates/notebook.html:133
|
||||
msgid "Cut Cells"
|
||||
msgstr "セルを切り取り"
|
||||
|
||||
#: notebook/templates/notebook.html:134
|
||||
msgid "Copy Cells"
|
||||
msgstr "セルをコピー"
|
||||
|
||||
#: notebook/templates/notebook.html:135
|
||||
msgid "Paste Cells Above"
|
||||
msgstr "上にセルをペースト"
|
||||
|
||||
#: notebook/templates/notebook.html:136
|
||||
msgid "Paste Cells Below"
|
||||
msgstr "下にセルをペースト"
|
||||
|
||||
#: notebook/templates/notebook.html:137
|
||||
msgid "Paste Cells & Replace"
|
||||
msgstr "セルをペーストして入れ替え(&A)"
|
||||
|
||||
#: notebook/templates/notebook.html:138
|
||||
msgid "Delete Cells"
|
||||
msgstr "セルを削除"
|
||||
|
||||
#: notebook/templates/notebook.html:139
|
||||
msgid "Undo Delete Cells"
|
||||
msgstr "セルの削除を取り消し"
|
||||
|
||||
#: notebook/templates/notebook.html:141
|
||||
msgid "Split Cell"
|
||||
msgstr "セルを分割"
|
||||
|
||||
#: notebook/templates/notebook.html:142
|
||||
msgid "Merge Cell Above"
|
||||
msgstr "上のセルをマージ"
|
||||
|
||||
#: notebook/templates/notebook.html:143
|
||||
msgid "Merge Cell Below"
|
||||
msgstr "下のセルをマージ"
|
||||
|
||||
#: notebook/templates/notebook.html:145
|
||||
msgid "Move Cell Up"
|
||||
msgstr "セルを上に移動"
|
||||
|
||||
#: notebook/templates/notebook.html:146
|
||||
msgid "Move Cell Down"
|
||||
msgstr "セルを下に移動"
|
||||
|
||||
#: notebook/templates/notebook.html:148
|
||||
msgid "Edit Notebook Metadata"
|
||||
msgstr "ノートブックのメタデータを編集"
|
||||
|
||||
#: notebook/templates/notebook.html:150
|
||||
msgid "Find and Replace"
|
||||
msgstr "検索と置換"
|
||||
|
||||
#: notebook/templates/notebook.html:152
|
||||
msgid "Cut Cell Attachments"
|
||||
msgstr "セルのアタッチメントを切り取り"
|
||||
|
||||
#: notebook/templates/notebook.html:153
|
||||
msgid "Copy Cell Attachments"
|
||||
msgstr "セルのアタッチメントをコピー"
|
||||
|
||||
#: notebook/templates/notebook.html:154
|
||||
msgid "Paste Cell Attachments"
|
||||
msgstr "セルのアタッチメントをペースト"
|
||||
|
||||
#: notebook/templates/notebook.html:156
|
||||
msgid "Insert Image"
|
||||
msgstr "画像を挿入"
|
||||
|
||||
#: notebook/templates/notebook.html:166
|
||||
msgid "Show/Hide the action icons (below menu bar)"
|
||||
msgstr "アクションアイコンを表示/非表示 (メニューバーの下)"
|
||||
|
||||
#: notebook/templates/notebook.html:167
|
||||
msgid "Toggle Toolbar"
|
||||
msgstr "ツールバーをトグル"
|
||||
|
||||
#: notebook/templates/notebook.html:170
|
||||
msgid "Show/Hide line numbers in cells"
|
||||
msgstr "セル内の行番号を表示/非表示"
|
||||
|
||||
#: notebook/templates/notebook.html:174
|
||||
msgid "Cell Toolbar"
|
||||
msgstr "セルツールバー"
|
||||
|
||||
#: notebook/templates/notebook.html:179
|
||||
msgid "Insert"
|
||||
msgstr "挿入"
|
||||
|
||||
#: notebook/templates/notebook.html:182
|
||||
msgid "Insert an empty Code cell above the currently active cell"
|
||||
msgstr "現在アクティブなセルの上に空のコードセルを挿入する"
|
||||
|
||||
#: notebook/templates/notebook.html:183
|
||||
msgid "Insert Cell Above"
|
||||
msgstr "上にセルを挿入"
|
||||
|
||||
#: notebook/templates/notebook.html:185
|
||||
msgid "Insert an empty Code cell below the currently active cell"
|
||||
msgstr "現在アクティブなセルの下に空のコードセルを挿入する"
|
||||
|
||||
#: notebook/templates/notebook.html:186
|
||||
msgid "Insert Cell Below"
|
||||
msgstr "下にセルを挿入"
|
||||
|
||||
#: notebook/templates/notebook.html:189
|
||||
msgid "Cell"
|
||||
msgstr "セル"
|
||||
|
||||
#: notebook/templates/notebook.html:191
|
||||
msgid "Run this cell, and move cursor to the next one"
|
||||
msgstr "このセルを実行しカーソルを一つ次に移動する"
|
||||
|
||||
#: notebook/templates/notebook.html:192
|
||||
msgid "Run Cells"
|
||||
msgstr "セルを実行"
|
||||
|
||||
#: notebook/templates/notebook.html:193
|
||||
msgid "Run this cell, select below"
|
||||
msgstr "このセルを実行し下を選択する"
|
||||
|
||||
#: notebook/templates/notebook.html:194
|
||||
msgid "Run Cells and Select Below"
|
||||
msgstr "ここまでのセルを実行し下を選択する"
|
||||
|
||||
#: notebook/templates/notebook.html:195
|
||||
msgid "Run this cell, insert below"
|
||||
msgstr "このセルを実行し下に挿入"
|
||||
|
||||
#: notebook/templates/notebook.html:196
|
||||
msgid "Run Cells and Insert Below"
|
||||
msgstr "ここまでのセルを実行し下に挿入"
|
||||
|
||||
#: notebook/templates/notebook.html:197
|
||||
msgid "Run all cells in the notebook"
|
||||
msgstr "ノートブックの全てのセルを実行"
|
||||
|
||||
#: notebook/templates/notebook.html:198
|
||||
msgid "Run All"
|
||||
msgstr "全てを実行"
|
||||
|
||||
#: notebook/templates/notebook.html:199
|
||||
msgid "Run all cells above (but not including) this cell"
|
||||
msgstr "このセルの上にある (このセルは含まない) すべてのセルを実行する"
|
||||
|
||||
#: notebook/templates/notebook.html:200
|
||||
msgid "Run All Above"
|
||||
msgstr "ここまでのセルの全てを実行"
|
||||
|
||||
#: notebook/templates/notebook.html:201
|
||||
msgid "Run this cell and all cells below it"
|
||||
msgstr "このセルと以下のすべてのセルを実行"
|
||||
|
||||
#: notebook/templates/notebook.html:202
|
||||
msgid "Run All Below"
|
||||
msgstr "以下を全て実行"
|
||||
|
||||
#: notebook/templates/notebook.html:205
|
||||
msgid "All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells"
|
||||
msgstr "ノートブック上のすべてのセルには種別があります。デフォルトでは新しいセルは 'コード' セルとして作成されます"
|
||||
|
||||
#: notebook/templates/notebook.html:206
|
||||
msgid "Cell Type"
|
||||
msgstr "セルの種別"
|
||||
|
||||
#: notebook/templates/notebook.html:209
|
||||
msgid "Contents will be sent to the kernel for execution, and output will display in the footer of cell"
|
||||
msgstr "実行のために内容がカーネルに送られ、セルのフッターに出力が表示されます"
|
||||
|
||||
#: notebook/templates/notebook.html:212
|
||||
msgid "Contents will be rendered as HTML and serve as explanatory text"
|
||||
msgstr "内容は HTML としてレンダリングされ説明のテキストとしてサーブされます"
|
||||
|
||||
#: notebook/templates/notebook.html:213 notebook/templates/notebook.html:298
|
||||
msgid "Markdown"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:215
|
||||
msgid "Contents will pass through nbconvert unmodified"
|
||||
msgstr "内容は変更されずに nbconvert に渡されます"
|
||||
|
||||
#: notebook/templates/notebook.html:216
|
||||
msgid "Raw NBConvert"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:220
|
||||
msgid "Current Outputs"
|
||||
msgstr "現在の出力"
|
||||
|
||||
#: notebook/templates/notebook.html:223
|
||||
msgid "Hide/Show the output of the current cell"
|
||||
msgstr "現在のセルの出力を表示/非表示"
|
||||
|
||||
#: notebook/templates/notebook.html:224 notebook/templates/notebook.html:240
|
||||
msgid "Toggle"
|
||||
msgstr "トグル"
|
||||
|
||||
#: notebook/templates/notebook.html:227
|
||||
msgid "Scroll the output of the current cell"
|
||||
msgstr "現在のセルの出力をスクロール"
|
||||
|
||||
#: notebook/templates/notebook.html:228 notebook/templates/notebook.html:244
|
||||
msgid "Toggle Scrolling"
|
||||
msgstr "スクロールをトグル"
|
||||
|
||||
#: notebook/templates/notebook.html:231
|
||||
msgid "Clear the output of the current cell"
|
||||
msgstr "現在のセルの出力をクリア"
|
||||
|
||||
#: notebook/templates/notebook.html:232 notebook/templates/notebook.html:248
|
||||
msgid "Clear"
|
||||
msgstr "クリア"
|
||||
|
||||
#: notebook/templates/notebook.html:236
|
||||
msgid "All Output"
|
||||
msgstr "全ての出力"
|
||||
|
||||
#: notebook/templates/notebook.html:239
|
||||
msgid "Hide/Show the output of all cells"
|
||||
msgstr "全てのセルの出力を表示/非表示"
|
||||
|
||||
#: notebook/templates/notebook.html:243
|
||||
msgid "Scroll the output of all cells"
|
||||
msgstr "全てのセルの出力をスクロール"
|
||||
|
||||
#: notebook/templates/notebook.html:247
|
||||
msgid "Clear the output of all cells"
|
||||
msgstr "全てのセルの出力をクリア"
|
||||
|
||||
#: notebook/templates/notebook.html:257
|
||||
msgid "Send Keyboard Interrupt (CTRL-C) to the Kernel"
|
||||
msgstr "キーボードの中断(CTRL-C)をカーネルに送る"
|
||||
|
||||
#: notebook/templates/notebook.html:258
|
||||
msgid "Interrupt"
|
||||
msgstr "中断"
|
||||
|
||||
#: notebook/templates/notebook.html:261
|
||||
msgid "Restart the Kernel"
|
||||
msgstr "カーネルを再起動"
|
||||
|
||||
#: notebook/templates/notebook.html:262
|
||||
msgid "Restart"
|
||||
msgstr "再起動"
|
||||
|
||||
#: notebook/templates/notebook.html:265
|
||||
msgid "Restart the Kernel and clear all output"
|
||||
msgstr "カーネルを再起動し全ての出力をクリアする"
|
||||
|
||||
#: notebook/templates/notebook.html:266
|
||||
msgid "Restart & Clear Output"
|
||||
msgstr "再起動し出力をクリアする"
|
||||
|
||||
#: notebook/templates/notebook.html:269
|
||||
msgid "Restart the Kernel and re-run the notebook"
|
||||
msgstr "カーネルを再起動しノートブックを再実行する"
|
||||
|
||||
#: notebook/templates/notebook.html:270
|
||||
msgid "Restart & Run All"
|
||||
msgstr "再起動し全てを実行"
|
||||
|
||||
#: notebook/templates/notebook.html:273
|
||||
msgid "Reconnect to the Kernel"
|
||||
msgstr "カーネルに再接続する"
|
||||
|
||||
#: notebook/templates/notebook.html:274
|
||||
msgid "Reconnect"
|
||||
msgstr "再接続"
|
||||
|
||||
#: notebook/templates/notebook.html:282
|
||||
msgid "Change kernel"
|
||||
msgstr "カーネルの変更"
|
||||
|
||||
#: notebook/templates/notebook.html:287
|
||||
msgid "Help"
|
||||
msgstr "ヘルプ"
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "A quick tour of the notebook user interface"
|
||||
msgstr "ノートブックユーザーインターフェースのクイックツアー"
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "User Interface Tour"
|
||||
msgstr "ユーザーインタフェースツアー"
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Opens a tooltip with all keyboard shortcuts"
|
||||
msgstr "全てのキーボードショートカットのツールチップを表示する"
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr "キーボードショートカット"
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Opens a dialog allowing you to edit Keyboard shortcuts"
|
||||
msgstr "キーボードショートカットの編集ダイアログを開く"
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Edit Keyboard Shortcuts"
|
||||
msgstr "キーボードショートカットの編集"
|
||||
|
||||
#: notebook/templates/notebook.html:297
|
||||
msgid "Notebook Help"
|
||||
msgstr "ノートブックのヘルプ"
|
||||
|
||||
#: notebook/templates/notebook.html:303
|
||||
msgid "Opens in a new window"
|
||||
msgstr "新しいウィンドウで開く"
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About Jupyter Notebook"
|
||||
msgstr "Jupyter Notebook について"
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About"
|
||||
msgstr "詳細"
|
||||
|
||||
#: notebook/templates/page.html:114
|
||||
msgid "Jupyter Notebook requires JavaScript."
|
||||
msgstr "Jupyter Notebook には JavaScript が必要です。"
|
||||
|
||||
#: notebook/templates/page.html:115
|
||||
msgid "Please enable it to proceed. "
|
||||
msgstr "続行するには有効にして下さい。 "
|
||||
|
||||
#: notebook/templates/page.html:121
|
||||
msgid "dashboard"
|
||||
msgstr "ダッシュボード"
|
||||
|
||||
#: notebook/templates/page.html:132
|
||||
msgid "Logout"
|
||||
msgstr "ログアウト"
|
||||
|
||||
#: notebook/templates/page.html:134
|
||||
msgid "Login"
|
||||
msgstr "ログイン"
|
||||
|
||||
#: notebook/templates/tree.html:23
|
||||
msgid "Files"
|
||||
msgstr "ファイル"
|
||||
|
||||
#: notebook/templates/tree.html:24
|
||||
msgid "Running"
|
||||
msgstr "実行中"
|
||||
|
||||
#: notebook/templates/tree.html:25
|
||||
msgid "Clusters"
|
||||
msgstr "クラスタ"
|
||||
|
||||
#: notebook/templates/tree.html:32
|
||||
msgid "Select items to perform actions on them."
|
||||
msgstr "アクションを実行する為のアイテムを選択して下さい。"
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate selected"
|
||||
msgstr "選択アイテムを複製する"
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate"
|
||||
msgstr "複製"
|
||||
|
||||
#: notebook/templates/tree.html:36
|
||||
msgid "Rename selected"
|
||||
msgstr "選択アイテムをリネームする"
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move selected"
|
||||
msgstr "選択アイテムを移動する"
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move"
|
||||
msgstr "移動"
|
||||
|
||||
#: notebook/templates/tree.html:38
|
||||
msgid "Download selected"
|
||||
msgstr "選択アイテムをダウンロードする"
|
||||
|
||||
#: notebook/templates/tree.html:39
|
||||
msgid "Shutdown selected notebook(s)"
|
||||
msgstr "選択されているノートブックをシャットダウンする"
|
||||
|
||||
#: notebook/templates/notebook.html:278
|
||||
#: notebook/templates/tree.html:39
|
||||
msgid "Shutdown"
|
||||
msgstr "シャットダウン"
|
||||
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View selected"
|
||||
msgstr "選択されているアイテムを表示する"
|
||||
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit selected"
|
||||
msgstr "選択されているアイテムを編集する"
|
||||
|
||||
#: notebook/templates/tree.html:42
|
||||
msgid "Delete selected"
|
||||
msgstr "選択されているアイテムを削除する"
|
||||
|
||||
#: notebook/templates/tree.html:50
|
||||
msgid "Click to browse for a file to upload."
|
||||
msgstr "クリックしてアップロードするファイルを選択して下さい。"
|
||||
|
||||
#: notebook/templates/tree.html:51
|
||||
msgid "Upload"
|
||||
msgstr "アップロード"
|
||||
|
||||
#: notebook/templates/tree.html:65
|
||||
msgid "Text File"
|
||||
msgstr "テキストファイル"
|
||||
|
||||
#: notebook/templates/tree.html:68
|
||||
msgid "Folder"
|
||||
msgstr "フォルダ"
|
||||
|
||||
#: notebook/templates/tree.html:72
|
||||
msgid "Terminal"
|
||||
msgstr "端末"
|
||||
|
||||
#: notebook/templates/tree.html:76
|
||||
msgid "Terminals Unavailable"
|
||||
msgstr "端末が存在しません"
|
||||
|
||||
#: notebook/templates/tree.html:82
|
||||
msgid "Refresh notebook list"
|
||||
msgstr "ノートブックの一覧を再読み込み"
|
||||
|
||||
#: notebook/templates/tree.html:90
|
||||
msgid "Select All / None"
|
||||
msgstr "全てを選択 / 解除"
|
||||
|
||||
#: notebook/templates/tree.html:93
|
||||
msgid "Select..."
|
||||
msgstr "選択..."
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Select All Folders"
|
||||
msgstr "全てのフォルダを選択..."
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Folders"
|
||||
msgstr "フォルダ"
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "Select All Notebooks"
|
||||
msgstr "全てのノートブックを選択"
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "All Notebooks"
|
||||
msgstr "全てのノートブック"
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Select Running Notebooks"
|
||||
msgstr "実行中のノートブックを選択"
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Running"
|
||||
msgstr "実行中"
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Select All Files"
|
||||
msgstr "全てのファイルを選択"
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Files"
|
||||
msgstr "ファイル"
|
||||
|
||||
#: notebook/templates/tree.html:114
|
||||
msgid "Last Modified"
|
||||
msgstr "最終変更時刻"
|
||||
|
||||
#: notebook/templates/tree.html:120
|
||||
msgid "Name"
|
||||
msgstr "名前"
|
||||
|
||||
msgid "File size"
|
||||
msgstr "ファイルサイズ"
|
||||
|
||||
#: notebook/templates/tree.html:130
|
||||
msgid "Currently running Jupyter processes"
|
||||
msgstr "現在実行中の Jupyter プロセス一覧"
|
||||
|
||||
#: notebook/templates/tree.html:134
|
||||
msgid "Refresh running list"
|
||||
msgstr "実行中の一覧を再読み込み"
|
||||
|
||||
#: notebook/templates/tree.html:150
|
||||
msgid "There are no terminals running."
|
||||
msgstr "実行中の端末はありません。"
|
||||
|
||||
#: notebook/templates/tree.html:152
|
||||
msgid "Terminals are unavailable."
|
||||
msgstr "端末はありません。"
|
||||
|
||||
#: notebook/templates/tree.html:162
|
||||
msgid "Notebooks"
|
||||
msgstr "ノートブック"
|
||||
|
||||
#: notebook/templates/tree.html:169
|
||||
msgid "There are no notebooks running."
|
||||
msgstr "実行中のノートブックはありません。"
|
||||
|
||||
#: notebook/templates/tree.html:178
|
||||
msgid "Clusters tab is now provided by IPython parallel."
|
||||
msgstr "Clousters タブが IPython parallel によって提供される様になりました。"
|
||||
|
||||
#: notebook/templates/tree.html:179
|
||||
msgid "See '<a href=\"https://github.com/ipython/ipyparallel\">IPython parallel</a>' for installation details."
|
||||
msgstr "詳しいインストール方法は '<a href=\"https://github.com/ipython/ipyparallel\">IPython parallel</a>' を参照"
|
||||
@ -1,16 +0,0 @@
|
||||
{
|
||||
"domain": "nbjs",
|
||||
"supported_languages": [
|
||||
"fr-FR",
|
||||
"zh-CN",
|
||||
"nl",
|
||||
"ja_JP"
|
||||
],
|
||||
"locale_data": {
|
||||
"nbjs": {
|
||||
"": {
|
||||
"domain": "nbjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,732 +0,0 @@
|
||||
# Translations template for Jupyter.
|
||||
# Copyright (C) 2017 ORGANIZATION
|
||||
# This file is distributed under the same license as the Jupyter project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Jupyter VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2017-07-07 12:48-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: notebook/templates/404.html:3
|
||||
msgid "You are requesting a page that does not exist!"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:37
|
||||
msgid "current mode"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:48 notebook/templates/notebook.html:78
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:50 notebook/templates/tree.html:57
|
||||
msgid "New"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:51
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:52 notebook/templates/tree.html:36
|
||||
msgid "Rename"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:53 notebook/templates/tree.html:38
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:56 notebook/templates/notebook.html:131
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:58
|
||||
msgid "Find"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:59
|
||||
msgid "Find & Replace"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:61
|
||||
msgid "Key Map"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:62
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:63
|
||||
msgid "Sublime Text"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:68 notebook/templates/notebook.html:159
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:70 notebook/templates/notebook.html:162
|
||||
msgid "Show/Hide the logo and notebook title (above menu bar)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:71 notebook/templates/notebook.html:163
|
||||
msgid "Toggle Header"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:72 notebook/templates/notebook.html:171
|
||||
msgid "Toggle Line Numbers"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/edit.html:75
|
||||
msgid "Language"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/error.html:23
|
||||
msgid "The error was:"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/login.html:24
|
||||
msgid "Password or token:"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/login.html:26
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/login.html:31
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/login.html:39
|
||||
msgid "No login available, you shouldn't be seeing this page."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/logout.html:24
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)s\">dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/logout.html:26
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)slogin\">login page"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:62
|
||||
msgid "Menu"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:65 notebook/templates/notebook.html:254
|
||||
msgid "Kernel"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:68
|
||||
msgid "This notebook is read-only"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:81
|
||||
msgid "New Notebook"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:85
|
||||
msgid "Opens a new window with the Dashboard view"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:86
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:90
|
||||
msgid "Open a copy of this notebook's contents and start a new kernel"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:91
|
||||
msgid "Make a Copy..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:92
|
||||
msgid "Rename..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:93
|
||||
msgid "Save and Checkpoint"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:96
|
||||
msgid "Revert to Checkpoint"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:106
|
||||
msgid "Print Preview"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:107
|
||||
msgid "Download as"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:109
|
||||
msgid "Notebook (.ipynb)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:110
|
||||
msgid "Script"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:111
|
||||
msgid "HTML (.html)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:112
|
||||
msgid "Markdown (.md)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:113
|
||||
msgid "reST (.rst)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:114
|
||||
msgid "LaTeX (.tex)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:115
|
||||
msgid "PDF via LaTeX (.pdf)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:118
|
||||
msgid "Deploy as"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:123
|
||||
msgid "Trust the output of this notebook"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:124
|
||||
msgid "Trust Notebook"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:127
|
||||
msgid "Shutdown this notebook's kernel, and close this window"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:128
|
||||
msgid "Close and Halt"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:133
|
||||
msgid "Cut Cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:134
|
||||
msgid "Copy Cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:135
|
||||
msgid "Paste Cells Above"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:136
|
||||
msgid "Paste Cells Below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:137
|
||||
msgid "Paste Cells & Replace"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:138
|
||||
msgid "Delete Cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:139
|
||||
msgid "Undo Delete Cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:141
|
||||
msgid "Split Cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:142
|
||||
msgid "Merge Cell Above"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:143
|
||||
msgid "Merge Cell Below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:145
|
||||
msgid "Move Cell Up"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:146
|
||||
msgid "Move Cell Down"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:148
|
||||
msgid "Edit Notebook Metadata"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:150
|
||||
msgid "Find and Replace"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:152
|
||||
msgid "Cut Cell Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:153
|
||||
msgid "Copy Cell Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:154
|
||||
msgid "Paste Cell Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:156
|
||||
msgid "Insert Image"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:166
|
||||
msgid "Show/Hide the action icons (below menu bar)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:167
|
||||
msgid "Toggle Toolbar"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:170
|
||||
msgid "Show/Hide line numbers in cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:174
|
||||
msgid "Cell Toolbar"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:179
|
||||
msgid "Insert"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:182
|
||||
msgid "Insert an empty Code cell above the currently active cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:183
|
||||
msgid "Insert Cell Above"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:185
|
||||
msgid "Insert an empty Code cell below the currently active cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:186
|
||||
msgid "Insert Cell Below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:189
|
||||
msgid "Cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:191
|
||||
msgid "Run this cell, and move cursor to the next one"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:192
|
||||
msgid "Run Cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:193
|
||||
msgid "Run this cell, select below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:194
|
||||
msgid "Run Cells and Select Below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:195
|
||||
msgid "Run this cell, insert below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:196
|
||||
msgid "Run Cells and Insert Below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:197
|
||||
msgid "Run all cells in the notebook"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:198
|
||||
msgid "Run All"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:199
|
||||
msgid "Run all cells above (but not including) this cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:200
|
||||
msgid "Run All Above"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:201
|
||||
msgid "Run this cell and all cells below it"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:202
|
||||
msgid "Run All Below"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:205
|
||||
msgid "All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:206
|
||||
msgid "Cell Type"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:209
|
||||
msgid "Contents will be sent to the kernel for execution, and output will display in the footer of cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:212
|
||||
msgid "Contents will be rendered as HTML and serve as explanatory text"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:213 notebook/templates/notebook.html:298
|
||||
msgid "Markdown"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:215
|
||||
msgid "Contents will pass through nbconvert unmodified"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:216
|
||||
msgid "Raw NBConvert"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:220
|
||||
msgid "Current Outputs"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:223
|
||||
msgid "Hide/Show the output of the current cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:224 notebook/templates/notebook.html:240
|
||||
msgid "Toggle"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:227
|
||||
msgid "Scroll the output of the current cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:228 notebook/templates/notebook.html:244
|
||||
msgid "Toggle Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:231
|
||||
msgid "Clear the output of the current cell"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:232 notebook/templates/notebook.html:248
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:236
|
||||
msgid "All Output"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:239
|
||||
msgid "Hide/Show the output of all cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:243
|
||||
msgid "Scroll the output of all cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:247
|
||||
msgid "Clear the output of all cells"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:257
|
||||
msgid "Send Keyboard Interrupt (CTRL-C) to the Kernel"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:258
|
||||
msgid "Interrupt"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:261
|
||||
msgid "Restart the Kernel"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:262
|
||||
msgid "Restart"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:265
|
||||
msgid "Restart the Kernel and clear all output"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:266
|
||||
msgid "Restart & Clear Output"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:269
|
||||
msgid "Restart the Kernel and re-run the notebook"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:270
|
||||
msgid "Restart & Run All"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:273
|
||||
msgid "Reconnect to the Kernel"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:274
|
||||
msgid "Reconnect"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:282
|
||||
msgid "Change kernel"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:287
|
||||
msgid "Help"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "A quick tour of the notebook user interface"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "User Interface Tour"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Opens a tooltip with all keyboard shortcuts"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Opens a dialog allowing you to edit Keyboard shortcuts"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Edit Keyboard Shortcuts"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:297
|
||||
msgid "Notebook Help"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:303
|
||||
msgid "Opens in a new window"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About Jupyter Notebook"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/page.html:114
|
||||
msgid "Jupyter Notebook requires JavaScript."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/page.html:115
|
||||
msgid "Please enable it to proceed. "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/page.html:121
|
||||
msgid "dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/page.html:132
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/page.html:134
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:23
|
||||
msgid "Files"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:24
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:25
|
||||
msgid "Clusters"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:32
|
||||
msgid "Select items to perform actions on them."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate selected"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:36
|
||||
msgid "Rename selected"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move selected"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:38
|
||||
msgid "Download selected"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:39
|
||||
msgid "Shutdown selected notebook(s)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/notebook.html:278
|
||||
#: notebook/templates/tree.html:39
|
||||
msgid "Shutdown"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View selected"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit selected"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:42
|
||||
msgid "Delete selected"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:50
|
||||
msgid "Click to browse for a file to upload."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:51
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:65
|
||||
msgid "Text File"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:68
|
||||
msgid "Folder"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:72
|
||||
msgid "Terminal"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:76
|
||||
msgid "Terminals Unavailable"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:82
|
||||
msgid "Refresh notebook list"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:90
|
||||
msgid "Select All / None"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:93
|
||||
msgid "Select..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Select All Folders"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Folders"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "Select All Notebooks"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "All Notebooks"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Select Running Notebooks"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Select All Files"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Files"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:114
|
||||
msgid "Last Modified"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:120
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:130
|
||||
msgid "Currently running Jupyter processes"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:134
|
||||
msgid "Refresh running list"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:150
|
||||
msgid "There are no terminals running."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:152
|
||||
msgid "Terminals are unavailable."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:162
|
||||
msgid "Notebooks"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:169
|
||||
msgid "There are no notebooks running."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:178
|
||||
msgid "Clusters tab is now provided by IPython parallel."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/templates/tree.html:179
|
||||
msgid "See '<a href=\"https://github.com/ipython/ipyparallel\">IPython parallel</a>' for installation details."
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,746 +0,0 @@
|
||||
# Translations template for Jupyter.
|
||||
# Copyright (C) 2017 ORGANIZATION
|
||||
# This file is distributed under the same license as the Jupyter project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Jupyter VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2017-07-07 12:48-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: notebook/templates/404.html:3
|
||||
msgid "You are requesting a page that does not exist!"
|
||||
msgstr "U vraagt een pagina die niet bestaat!"
|
||||
|
||||
#: notebook/templates/edit.html:37
|
||||
msgid "current mode"
|
||||
msgstr "huidige modus"
|
||||
|
||||
#: notebook/templates/edit.html:48 notebook/templates/notebook.html:78
|
||||
msgid "File"
|
||||
msgstr "Bestand"
|
||||
|
||||
#: notebook/templates/edit.html:50 notebook/templates/tree.html:57
|
||||
msgid "New"
|
||||
msgstr "Nieuw"
|
||||
|
||||
#: notebook/templates/edit.html:51
|
||||
msgid "Save"
|
||||
msgstr "Opslaan"
|
||||
|
||||
#: notebook/templates/edit.html:52 notebook/templates/tree.html:36
|
||||
msgid "Rename"
|
||||
msgstr "Hernoemen"
|
||||
|
||||
#: notebook/templates/edit.html:53 notebook/templates/tree.html:38
|
||||
msgid "Download"
|
||||
msgstr "Downloaden"
|
||||
|
||||
#: notebook/templates/edit.html:56 notebook/templates/notebook.html:131
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit"
|
||||
msgstr "Bewerken"
|
||||
|
||||
#: notebook/templates/edit.html:58
|
||||
msgid "Find"
|
||||
msgstr "Vinden"
|
||||
|
||||
#: notebook/templates/edit.html:59
|
||||
msgid "Find & Replace"
|
||||
msgstr "Zoeken en vervangen"
|
||||
|
||||
#: notebook/templates/edit.html:61
|
||||
msgid "Key Map"
|
||||
msgstr "Sleutelkaart"
|
||||
|
||||
#: notebook/templates/edit.html:62
|
||||
msgid "Default"
|
||||
msgstr "Standaard"
|
||||
|
||||
#: notebook/templates/edit.html:63
|
||||
msgid "Sublime Text"
|
||||
msgstr "Sublime Tekst"
|
||||
|
||||
#: notebook/templates/edit.html:68 notebook/templates/notebook.html:159
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View"
|
||||
msgstr "Bekijken"
|
||||
|
||||
#: notebook/templates/edit.html:70 notebook/templates/notebook.html:162
|
||||
msgid "Show/Hide the logo and notebook title (above menu bar)"
|
||||
msgstr ""
|
||||
"Het logo en de titel van het notebook weergeven/verbergen (boven "
|
||||
"menubalk)"
|
||||
|
||||
#: notebook/templates/edit.html:71 notebook/templates/notebook.html:163
|
||||
msgid "Toggle Header"
|
||||
msgstr "Koptekst in- of uitschakelen"
|
||||
|
||||
#: notebook/templates/edit.html:72 notebook/templates/notebook.html:171
|
||||
msgid "Toggle Line Numbers"
|
||||
msgstr "Regelnummers in- of uitschakelen"
|
||||
|
||||
#: notebook/templates/edit.html:75
|
||||
msgid "Language"
|
||||
msgstr "Taal"
|
||||
|
||||
#: notebook/templates/error.html:23
|
||||
msgid "The error was:"
|
||||
msgstr "De fout was:"
|
||||
|
||||
#: notebook/templates/login.html:24
|
||||
msgid "Password or token:"
|
||||
msgstr "Wachtwoord of token:"
|
||||
|
||||
#: notebook/templates/login.html:26
|
||||
msgid "Password:"
|
||||
msgstr "Wachtwoord:"
|
||||
|
||||
#: notebook/templates/login.html:31
|
||||
msgid "Log in"
|
||||
msgstr "Aanmelden"
|
||||
|
||||
#: notebook/templates/login.html:39
|
||||
msgid "No login available, you shouldn't be seeing this page."
|
||||
msgstr "Geen login beschikbaar, u zou deze pagina niet moeten zien."
|
||||
|
||||
#: notebook/templates/logout.html:24
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)s\">dashboard"
|
||||
msgstr "Ga naar het dashboard <a href=\"%(base_url)s\">"
|
||||
|
||||
#: notebook/templates/logout.html:26
|
||||
#, python-format
|
||||
msgid "Proceed to the <a href=\"%(base_url)slogin\">login page"
|
||||
msgstr "Ga naar de <a href=\"%(base_url)slogin\">login pagina"
|
||||
|
||||
#: notebook/templates/notebook.html:62
|
||||
msgid "Menu"
|
||||
msgstr "Menu"
|
||||
|
||||
#: notebook/templates/notebook.html:65 notebook/templates/notebook.html:254
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#: notebook/templates/notebook.html:68
|
||||
msgid "This notebook is read-only"
|
||||
msgstr "Dit notebook is alleen-lezen"
|
||||
|
||||
#: notebook/templates/notebook.html:81
|
||||
msgid "New Notebook"
|
||||
msgstr "Nieuw notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:85
|
||||
msgid "Opens a new window with the Dashboard view"
|
||||
msgstr "Opent een nieuw venster met de dashboardweergave"
|
||||
|
||||
#: notebook/templates/notebook.html:86
|
||||
msgid "Open..."
|
||||
msgstr "Open..."
|
||||
|
||||
#: notebook/templates/notebook.html:90
|
||||
msgid "Open a copy of this notebook's contents and start a new kernel"
|
||||
msgstr ""
|
||||
"Een kopie van de inhoud van dit notebook openen en een nieuwe kernel "
|
||||
"starten"
|
||||
|
||||
#: notebook/templates/notebook.html:91
|
||||
msgid "Make a Copy..."
|
||||
msgstr "Maak een kopie..."
|
||||
|
||||
#: notebook/templates/notebook.html:92
|
||||
msgid "Rename..."
|
||||
msgstr "Hernoemen..."
|
||||
|
||||
#: notebook/templates/notebook.html:93
|
||||
msgid "Save and Checkpoint"
|
||||
msgstr "Opslaan en Checkpoint"
|
||||
|
||||
#: notebook/templates/notebook.html:96
|
||||
msgid "Revert to Checkpoint"
|
||||
msgstr "Terugkeren naar Checkpoint"
|
||||
|
||||
#: notebook/templates/notebook.html:106
|
||||
msgid "Print Preview"
|
||||
msgstr "Afdrukvoorbeeld"
|
||||
|
||||
#: notebook/templates/notebook.html:107
|
||||
msgid "Download as"
|
||||
msgstr "Download als"
|
||||
|
||||
#: notebook/templates/notebook.html:109
|
||||
msgid "Notebook (.ipynb)"
|
||||
msgstr "Notebook (.ipynb)"
|
||||
|
||||
#: notebook/templates/notebook.html:110
|
||||
msgid "Script"
|
||||
msgstr "Script"
|
||||
|
||||
#: notebook/templates/notebook.html:111
|
||||
msgid "HTML (.html)"
|
||||
msgstr "HTML (.html)"
|
||||
|
||||
#: notebook/templates/notebook.html:112
|
||||
msgid "Markdown (.md)"
|
||||
msgstr "Markdown (.md)"
|
||||
|
||||
#: notebook/templates/notebook.html:113
|
||||
msgid "reST (.rst)"
|
||||
msgstr "reST (.rst)"
|
||||
|
||||
#: notebook/templates/notebook.html:114
|
||||
msgid "LaTeX (.tex)"
|
||||
msgstr "LaTeX (.tex)"
|
||||
|
||||
#: notebook/templates/notebook.html:115
|
||||
msgid "PDF via LaTeX (.pdf)"
|
||||
msgstr "PDF via LaTeX (.pdf)"
|
||||
|
||||
#: notebook/templates/notebook.html:118
|
||||
msgid "Deploy as"
|
||||
msgstr "Implementeren als"
|
||||
|
||||
#: notebook/templates/notebook.html:123
|
||||
msgid "Trust the output of this notebook"
|
||||
msgstr "Vertrouwen op de uitvoer van dit notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:124
|
||||
msgid "Trust Notebook"
|
||||
msgstr "Notebook vertrouwen"
|
||||
|
||||
#: notebook/templates/notebook.html:127
|
||||
msgid "Shutdown this notebook's kernel, and close this window"
|
||||
msgstr "De kernel van deze notebook afsluiten en dit venster sluiten"
|
||||
|
||||
#: notebook/templates/notebook.html:128
|
||||
msgid "Close and Halt"
|
||||
msgstr "Sluiten en Halt"
|
||||
|
||||
#: notebook/templates/notebook.html:133
|
||||
msgid "Cut Cells"
|
||||
msgstr "Cellen knippen"
|
||||
|
||||
#: notebook/templates/notebook.html:134
|
||||
msgid "Copy Cells"
|
||||
msgstr "Cellen kopiëren"
|
||||
|
||||
#: notebook/templates/notebook.html:135
|
||||
msgid "Paste Cells Above"
|
||||
msgstr "Cellen boven plakken"
|
||||
|
||||
#: notebook/templates/notebook.html:136
|
||||
msgid "Paste Cells Below"
|
||||
msgstr "Cellen eronder plakken"
|
||||
|
||||
#: notebook/templates/notebook.html:137
|
||||
msgid "Paste Cells & Replace"
|
||||
msgstr "Cellen plakken en vervangen"
|
||||
|
||||
#: notebook/templates/notebook.html:138
|
||||
msgid "Delete Cells"
|
||||
msgstr "Cellen verwijderen"
|
||||
|
||||
#: notebook/templates/notebook.html:139
|
||||
msgid "Undo Delete Cells"
|
||||
msgstr "Verwijdercellen ongedaan maken"
|
||||
|
||||
#: notebook/templates/notebook.html:141
|
||||
msgid "Split Cell"
|
||||
msgstr "Cel splitsen"
|
||||
|
||||
#: notebook/templates/notebook.html:142
|
||||
msgid "Merge Cell Above"
|
||||
msgstr "Cel boven samenvoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:143
|
||||
msgid "Merge Cell Below"
|
||||
msgstr "Cel eronder samenvoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:145
|
||||
msgid "Move Cell Up"
|
||||
msgstr "Cel omhoog verplaatsen"
|
||||
|
||||
#: notebook/templates/notebook.html:146
|
||||
msgid "Move Cell Down"
|
||||
msgstr "Cel omlaag verplaatsen"
|
||||
|
||||
#: notebook/templates/notebook.html:148
|
||||
msgid "Edit Notebook Metadata"
|
||||
msgstr "Metagegevens van notebook bewerken"
|
||||
|
||||
#: notebook/templates/notebook.html:150
|
||||
msgid "Find and Replace"
|
||||
msgstr "Zoeken en vervangen"
|
||||
|
||||
#: notebook/templates/notebook.html:152
|
||||
msgid "Cut Cell Attachments"
|
||||
msgstr "Celbijlagen knippen"
|
||||
|
||||
#: notebook/templates/notebook.html:153
|
||||
msgid "Copy Cell Attachments"
|
||||
msgstr "Celbijlagen kopiëren"
|
||||
|
||||
#: notebook/templates/notebook.html:154
|
||||
msgid "Paste Cell Attachments"
|
||||
msgstr "Celbijlagen plakken"
|
||||
|
||||
#: notebook/templates/notebook.html:156
|
||||
msgid "Insert Image"
|
||||
msgstr "Afbeelding invoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:166
|
||||
msgid "Show/Hide the action icons (below menu bar)"
|
||||
msgstr "De actiepictogrammen weergeven/verbergen (onder menubalk)"
|
||||
|
||||
#: notebook/templates/notebook.html:167
|
||||
msgid "Toggle Toolbar"
|
||||
msgstr "Werkbalk in- en uitschakelen"
|
||||
|
||||
#: notebook/templates/notebook.html:170
|
||||
msgid "Show/Hide line numbers in cells"
|
||||
msgstr "Lijnnummers weergeven/verbergen in cellen"
|
||||
|
||||
#: notebook/templates/notebook.html:174
|
||||
msgid "Cell Toolbar"
|
||||
msgstr "Celwerkbalk"
|
||||
|
||||
#: notebook/templates/notebook.html:179
|
||||
msgid "Insert"
|
||||
msgstr "Invoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:182
|
||||
msgid "Insert an empty Code cell above the currently active cell"
|
||||
msgstr "Een lege codecel boven de actieve cel invoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:183
|
||||
msgid "Insert Cell Above"
|
||||
msgstr "Cel boven invoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:185
|
||||
msgid "Insert an empty Code cell below the currently active cell"
|
||||
msgstr "Een lege codecel onder de actieve cel invoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:186
|
||||
msgid "Insert Cell Below"
|
||||
msgstr "Cel eronder invoegen"
|
||||
|
||||
#: notebook/templates/notebook.html:189
|
||||
msgid "Cell"
|
||||
msgstr "Cel"
|
||||
|
||||
#: notebook/templates/notebook.html:191
|
||||
msgid "Run this cell, and move cursor to the next one"
|
||||
msgstr "Deze cel uitvoeren en cursor naar de volgende cel verplaatsen"
|
||||
|
||||
#: notebook/templates/notebook.html:192
|
||||
msgid "Run Cells"
|
||||
msgstr "Cellen uitvoeren"
|
||||
|
||||
#: notebook/templates/notebook.html:193
|
||||
msgid "Run this cell, select below"
|
||||
msgstr "Voer deze cel uit en selecteer de cel eronder"
|
||||
|
||||
#: notebook/templates/notebook.html:194
|
||||
msgid "Run Cells and Select Below"
|
||||
msgstr "Cellen uitvoeren en de cel eronder selecteren"
|
||||
|
||||
#: notebook/templates/notebook.html:195
|
||||
msgid "Run this cell, insert below"
|
||||
msgstr "Voer deze cel uit en voeg een cel toe"
|
||||
|
||||
#: notebook/templates/notebook.html:196
|
||||
msgid "Run Cells and Insert Below"
|
||||
msgstr "Cellen uitvoeren en voeg een cel toe"
|
||||
|
||||
#: notebook/templates/notebook.html:197
|
||||
msgid "Run all cells in the notebook"
|
||||
msgstr "Alle cellen in het notebook uitvoeren"
|
||||
|
||||
#: notebook/templates/notebook.html:198
|
||||
msgid "Run All"
|
||||
msgstr "Alles uitvoeren"
|
||||
|
||||
#: notebook/templates/notebook.html:199
|
||||
msgid "Run all cells above (but not including) this cell"
|
||||
msgstr "Alle cellen boven (maar niet inclusief) deze cel uitvoeren"
|
||||
|
||||
#: notebook/templates/notebook.html:200
|
||||
msgid "Run All Above"
|
||||
msgstr "All cellen hierboven uitvoeren"
|
||||
|
||||
#: notebook/templates/notebook.html:201
|
||||
msgid "Run this cell and all cells below it"
|
||||
msgstr "Voer deze cel en alle cellen eronder uit."
|
||||
|
||||
#: notebook/templates/notebook.html:202
|
||||
msgid "Run All Below"
|
||||
msgstr "Voer alles eronder uit"
|
||||
|
||||
#: notebook/templates/notebook.html:205
|
||||
msgid ""
|
||||
"All cells in the notebook have a cell type. By default, new cells are "
|
||||
"created as 'Code' cells"
|
||||
msgstr ""
|
||||
"Alle cellen in het notebook hebben een celtype. Standaard worden nieuwe "
|
||||
"cellen gemaakt als 'Code'-cellen"
|
||||
|
||||
#: notebook/templates/notebook.html:206
|
||||
msgid "Cell Type"
|
||||
msgstr "Celtype"
|
||||
|
||||
#: notebook/templates/notebook.html:209
|
||||
msgid ""
|
||||
"Contents will be sent to the kernel for execution, and output will display "
|
||||
"in the footer of cell"
|
||||
msgstr ""
|
||||
"Inhoud wordt verzonden naar de kernel voor uitvoering, en de uitvoer wordt "
|
||||
"weergegeven in de voettekst van de cel"
|
||||
|
||||
#: notebook/templates/notebook.html:212
|
||||
msgid "Contents will be rendered as HTML and serve as explanatory text"
|
||||
msgstr "Inhoud wordt weergegeven als HTML en dient als verklarende tekst"
|
||||
|
||||
#: notebook/templates/notebook.html:213 notebook/templates/notebook.html:298
|
||||
msgid "Markdown"
|
||||
msgstr "Markdown"
|
||||
|
||||
#: notebook/templates/notebook.html:215
|
||||
msgid "Contents will pass through nbconvert unmodified"
|
||||
msgstr "De inhoud zal niet worden gewijzigd door nbconvert"
|
||||
|
||||
#: notebook/templates/notebook.html:216
|
||||
msgid "Raw NBConvert"
|
||||
msgstr "Raw NBConvert"
|
||||
|
||||
#: notebook/templates/notebook.html:220
|
||||
msgid "Current Outputs"
|
||||
msgstr "Huidige uitvoer"
|
||||
|
||||
#: notebook/templates/notebook.html:223
|
||||
msgid "Hide/Show the output of the current cell"
|
||||
msgstr "De uitvoer van de huidige cel verbergen/weergeven"
|
||||
|
||||
#: notebook/templates/notebook.html:224 notebook/templates/notebook.html:240
|
||||
msgid "Toggle"
|
||||
msgstr "In- en uitschakelen"
|
||||
|
||||
#: notebook/templates/notebook.html:227
|
||||
msgid "Scroll the output of the current cell"
|
||||
msgstr "De uitvoer van de huidige cel scrollen"
|
||||
|
||||
#: notebook/templates/notebook.html:228 notebook/templates/notebook.html:244
|
||||
msgid "Toggle Scrolling"
|
||||
msgstr "Scrollen in- en uitschakelen"
|
||||
|
||||
#: notebook/templates/notebook.html:231
|
||||
msgid "Clear the output of the current cell"
|
||||
msgstr "De uitvoer van de huidige cel wissen"
|
||||
|
||||
#: notebook/templates/notebook.html:232 notebook/templates/notebook.html:248
|
||||
msgid "Clear"
|
||||
msgstr "Wissen"
|
||||
|
||||
#: notebook/templates/notebook.html:236
|
||||
msgid "All Output"
|
||||
msgstr "Alle uitvoer"
|
||||
|
||||
#: notebook/templates/notebook.html:239
|
||||
msgid "Hide/Show the output of all cells"
|
||||
msgstr "De uitvoer van alle cellen verbergen/weergeven"
|
||||
|
||||
#: notebook/templates/notebook.html:243
|
||||
msgid "Scroll the output of all cells"
|
||||
msgstr "Door de uitvoer van alle cellen scrollen"
|
||||
|
||||
#: notebook/templates/notebook.html:247
|
||||
msgid "Clear the output of all cells"
|
||||
msgstr "De uitvoer van alle cellen wissen"
|
||||
|
||||
#: notebook/templates/notebook.html:257
|
||||
msgid "Send Keyboard Interrupt (CTRL-C) to the Kernel"
|
||||
msgstr "Toetsenbordinterruptie (Ctrl-C) naar de kernel verzenden"
|
||||
|
||||
#: notebook/templates/notebook.html:258
|
||||
msgid "Interrupt"
|
||||
msgstr "Onderbreken"
|
||||
|
||||
#: notebook/templates/notebook.html:261
|
||||
msgid "Restart the Kernel"
|
||||
msgstr "De kernel opnieuw starten"
|
||||
|
||||
#: notebook/templates/notebook.html:262
|
||||
msgid "Restart"
|
||||
msgstr "Opnieuw starten"
|
||||
|
||||
#: notebook/templates/notebook.html:265
|
||||
msgid "Restart the Kernel and clear all output"
|
||||
msgstr "Start de kernel opnieuw en schakel alle uitvoer uit"
|
||||
|
||||
#: notebook/templates/notebook.html:266
|
||||
msgid "Restart & Clear Output"
|
||||
msgstr "Uitvoer opnieuw starten en wissen"
|
||||
|
||||
#: notebook/templates/notebook.html:269
|
||||
msgid "Restart the Kernel and re-run the notebook"
|
||||
msgstr "Start de kernel opnieuw en voer notebook opnieuw uit"
|
||||
|
||||
#: notebook/templates/notebook.html:270
|
||||
msgid "Restart & Run All"
|
||||
msgstr "Alles opnieuw starten en uitvoeren"
|
||||
|
||||
#: notebook/templates/notebook.html:273
|
||||
msgid "Reconnect to the Kernel"
|
||||
msgstr "Opnieuw verbinding maken met de kernel"
|
||||
|
||||
#: notebook/templates/notebook.html:274
|
||||
msgid "Reconnect"
|
||||
msgstr "Sluit"
|
||||
|
||||
#: notebook/templates/notebook.html:282
|
||||
msgid "Change kernel"
|
||||
msgstr "Kernel wijzigen"
|
||||
|
||||
#: notebook/templates/notebook.html:287
|
||||
msgid "Help"
|
||||
msgstr "Help"
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "A quick tour of the notebook user interface"
|
||||
msgstr "Een snelle rondleiding door de gebruikersinterface van de notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:290
|
||||
msgid "User Interface Tour"
|
||||
msgstr "Gebruikersinterfacetour"
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Opens a tooltip with all keyboard shortcuts"
|
||||
msgstr "Hiermee opent u een tooltop met alle sneltoetsen"
|
||||
|
||||
#: notebook/templates/notebook.html:291
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr "Sneltoetsen"
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Opens a dialog allowing you to edit Keyboard shortcuts"
|
||||
msgstr "Hiermee opent u een dialoogvenster waarmee u sneltoetsen bewerken"
|
||||
|
||||
#: notebook/templates/notebook.html:292
|
||||
msgid "Edit Keyboard Shortcuts"
|
||||
msgstr "Sneltoetsen bewerken"
|
||||
|
||||
#: notebook/templates/notebook.html:297
|
||||
msgid "Notebook Help"
|
||||
msgstr "Help voor notebooks"
|
||||
|
||||
#: notebook/templates/notebook.html:303
|
||||
msgid "Opens in a new window"
|
||||
msgstr "Opent in een nieuw venster"
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About Jupyter Notebook"
|
||||
msgstr "Over Jupyter Notebook"
|
||||
|
||||
#: notebook/templates/notebook.html:319
|
||||
msgid "About"
|
||||
msgstr "Over"
|
||||
|
||||
#: notebook/templates/page.html:114
|
||||
msgid "Jupyter Notebook requires JavaScript."
|
||||
msgstr "Jupyter Notebook vereist JavaScript."
|
||||
|
||||
#: notebook/templates/page.html:115
|
||||
msgid "Please enable it to proceed. "
|
||||
msgstr "Schakel het in om door te gaan. "
|
||||
|
||||
#: notebook/templates/page.html:121
|
||||
msgid "dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
#: notebook/templates/page.html:132
|
||||
msgid "Logout"
|
||||
msgstr "Logout"
|
||||
|
||||
#: notebook/templates/page.html:134
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: notebook/templates/tree.html:23
|
||||
msgid "Files"
|
||||
msgstr "Bestanden"
|
||||
|
||||
#: notebook/templates/tree.html:24
|
||||
msgid "Running"
|
||||
msgstr "Actieve processen"
|
||||
|
||||
#: notebook/templates/tree.html:25
|
||||
msgid "Clusters"
|
||||
msgstr "Clusters"
|
||||
|
||||
#: notebook/templates/tree.html:32
|
||||
msgid "Select items to perform actions on them."
|
||||
msgstr "Selecteer items om acties op uit te voeren."
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate selected"
|
||||
msgstr "Duplicaat geselecteerd"
|
||||
|
||||
#: notebook/templates/tree.html:35
|
||||
msgid "Duplicate"
|
||||
msgstr "Dupliceer"
|
||||
|
||||
#: notebook/templates/tree.html:36
|
||||
msgid "Rename selected"
|
||||
msgstr "Naam wijzigen van geselecteerde"
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move selected"
|
||||
msgstr "Verplaats geselecteerde"
|
||||
|
||||
#: notebook/templates/tree.html:37
|
||||
msgid "Move"
|
||||
msgstr "Verplaatsen"
|
||||
|
||||
#: notebook/templates/tree.html:38
|
||||
msgid "Download selected"
|
||||
msgstr "Download geselecteerde"
|
||||
|
||||
#: notebook/templates/tree.html:39
|
||||
msgid "Shutdown selected notebook(s)"
|
||||
msgstr "Geselecteerde notebook(s) afsluiten"
|
||||
|
||||
#: notebook/templates/notebook.html:278 notebook/templates/tree.html:39
|
||||
msgid "Shutdown"
|
||||
msgstr "Afsluiten"
|
||||
|
||||
#: notebook/templates/tree.html:40
|
||||
msgid "View selected"
|
||||
msgstr "Geef geselecteerde weer"
|
||||
|
||||
#: notebook/templates/tree.html:41
|
||||
msgid "Edit selected"
|
||||
msgstr "Bewerk geselecteerde"
|
||||
|
||||
#: notebook/templates/tree.html:42
|
||||
msgid "Delete selected"
|
||||
msgstr "Verwijder geselecteerde"
|
||||
|
||||
#: notebook/templates/tree.html:50
|
||||
msgid "Click to browse for a file to upload."
|
||||
msgstr "Klik hier om te zoeken naar een bestand dat u wilt uploaden."
|
||||
|
||||
#: notebook/templates/tree.html:51
|
||||
msgid "Upload"
|
||||
msgstr "Uploaden"
|
||||
|
||||
#: notebook/templates/tree.html:65
|
||||
msgid "Text File"
|
||||
msgstr "Tekstbestand"
|
||||
|
||||
#: notebook/templates/tree.html:68
|
||||
msgid "Folder"
|
||||
msgstr "Map"
|
||||
|
||||
#: notebook/templates/tree.html:72
|
||||
msgid "Terminal"
|
||||
msgstr "Terminal"
|
||||
|
||||
#: notebook/templates/tree.html:76
|
||||
msgid "Terminals Unavailable"
|
||||
msgstr "Terminals Niet Beschikbaar"
|
||||
|
||||
#: notebook/templates/tree.html:82
|
||||
msgid "Refresh notebook list"
|
||||
msgstr "Notebook-lijst vernieuwen"
|
||||
|
||||
#: notebook/templates/tree.html:90
|
||||
msgid "Select All / None"
|
||||
msgstr "Selecteer Alles / Geen"
|
||||
|
||||
#: notebook/templates/tree.html:93
|
||||
msgid "Select..."
|
||||
msgstr "Selecteer..."
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Select All Folders"
|
||||
msgstr "Alle Mappen Selecteren"
|
||||
|
||||
#: notebook/templates/tree.html:98
|
||||
msgid "Folders"
|
||||
msgstr "Mappen"
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "Select All Notebooks"
|
||||
msgstr "Alle Notebooks Selecteren"
|
||||
|
||||
#: notebook/templates/tree.html:99
|
||||
msgid "All Notebooks"
|
||||
msgstr "Alle notebooks"
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Select Running Notebooks"
|
||||
msgstr "Actieve Notebooks Selecteren"
|
||||
|
||||
#: notebook/templates/tree.html:100
|
||||
msgid "Running"
|
||||
msgstr "Actieve Processen"
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Select All Files"
|
||||
msgstr "Alle Bestanden Selecteren"
|
||||
|
||||
#: notebook/templates/tree.html:101
|
||||
msgid "Files"
|
||||
msgstr "Bestanden"
|
||||
|
||||
#: notebook/templates/tree.html:114
|
||||
msgid "Last Modified"
|
||||
msgstr "Laatst gewijzigd"
|
||||
|
||||
#: notebook/templates/tree.html:120
|
||||
msgid "Name"
|
||||
msgstr "Naam"
|
||||
|
||||
#: notebook/templates/tree.html:130
|
||||
msgid "Currently running Jupyter processes"
|
||||
msgstr "Momenteel actieve Jupyter processen"
|
||||
|
||||
#: notebook/templates/tree.html:134
|
||||
msgid "Refresh running list"
|
||||
msgstr "Actieve processen lijst vernieuwen"
|
||||
|
||||
#: notebook/templates/tree.html:150
|
||||
msgid "There are no terminals running."
|
||||
msgstr "Er zijn geen terminals actief."
|
||||
|
||||
#: notebook/templates/tree.html:152
|
||||
msgid "Terminals are unavailable."
|
||||
msgstr "Terminals zijn niet beschikbaar."
|
||||
|
||||
#: notebook/templates/tree.html:162
|
||||
msgid "Notebooks"
|
||||
msgstr "Notebooks"
|
||||
|
||||
#: notebook/templates/tree.html:169
|
||||
msgid "There are no notebooks running."
|
||||
msgstr "Er worden geen notebooks uitgevoerd."
|
||||
|
||||
#: notebook/templates/tree.html:178
|
||||
msgid "Clusters tab is now provided by IPython parallel."
|
||||
msgstr "Clusters tabblad wordt nu geleverd door IPython parallel."
|
||||
|
||||
#: notebook/templates/tree.html:179
|
||||
msgid ""
|
||||
"See '<a href=\"https://github.com/ipython/ipyparallel\">IPython "
|
||||
"parallel</a>' for installation details."
|
||||
msgstr ""
|
||||
"Zie '<a href=\"https://github.com/ipython/ipyparallel\">IPython "
|
||||
"parallel</a>' voor installatiegegevens."
|
||||
Binary file not shown.
@ -1,607 +0,0 @@
|
||||
# Translations template for Jupyter.
|
||||
# Copyright (C) 2017 ORGANIZATION
|
||||
# This file is distributed under the same license as the Jupyter project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Jupyter VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2017-07-08 21:52-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: notebook/notebookapp.py:53
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0"
|
||||
msgstr "De Jupyter Notebook vereist tornado >= 4.0"
|
||||
|
||||
#: notebook/notebookapp.py:57
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have < 1.1.0"
|
||||
msgstr "De Jupyter Notebook vereist tornado >= 4.0, maar je hebt < 1.1.0"
|
||||
|
||||
#: notebook/notebookapp.py:59
|
||||
#, python-format
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have %s"
|
||||
msgstr "De Jupyter Notebook vereist tornado >= 4.0, maar je hebt %s"
|
||||
|
||||
#: notebook/notebookapp.py:209
|
||||
msgid "The `ignore_minified_js` flag is deprecated and no longer works."
|
||||
msgstr "De vlag 'ignore_minified_js' is afgeschaft en werkt niet meer."
|
||||
|
||||
#: notebook/notebookapp.py:210
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Alternatively use `%s` when working on the notebook's Javascript and LESS"
|
||||
msgstr ""
|
||||
"U ook '%s' gebruiken bij het werken aan de Javascript van de notebook en "
|
||||
"LESS"
|
||||
|
||||
#: notebook/notebookapp.py:211
|
||||
msgid ""
|
||||
"The `ignore_minified_js` flag is deprecated and will be removed in Notebook "
|
||||
"6.0"
|
||||
msgstr ""
|
||||
"De vlag 'ignore_minified_js' wordt afgeschaft en wordt verwijderd in "
|
||||
"Notebook 6.0"
|
||||
|
||||
#: notebook/notebookapp.py:389
|
||||
msgid "List currently running notebook servers."
|
||||
msgstr "Lijst met momenteel draaiende notebookservers."
|
||||
|
||||
#: notebook/notebookapp.py:393
|
||||
msgid "Produce machine-readable JSON output."
|
||||
msgstr "Productie computer-leesbare JSON-uitvoer."
|
||||
|
||||
#: notebook/notebookapp.py:397
|
||||
msgid ""
|
||||
"If True, each line of output will be a JSON object with the details from the"
|
||||
" server info file."
|
||||
msgstr ""
|
||||
"Als dit True is, zal elke uitvoerregel een JSON-object worden met de details uit het "
|
||||
"serverinfobestand."
|
||||
|
||||
#: notebook/notebookapp.py:402
|
||||
msgid "Currently running servers:"
|
||||
msgstr "Momenteel draaiende servers:"
|
||||
|
||||
#: notebook/notebookapp.py:419
|
||||
msgid "Don't open the notebook in a browser after startup."
|
||||
msgstr "Open het notebook niet in een browser na het opstarten."
|
||||
|
||||
#: notebook/notebookapp.py:423
|
||||
msgid ""
|
||||
"DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
|
||||
msgstr ""
|
||||
"UITGESCHAKELD: gebruik %pylab of %matplotlib in het notebook om "
|
||||
"matplotlib in te schakelen."
|
||||
|
||||
#: notebook/notebookapp.py:439
|
||||
msgid "Allow the notebook to be run from root user."
|
||||
msgstr "Sta toe dat het notebook vanaf de root user kan worden uitgevoerd."
|
||||
|
||||
#: notebook/notebookapp.py:470
|
||||
msgid ""
|
||||
"The Jupyter HTML Notebook.\n"
|
||||
" \n"
|
||||
" This launches a Tornado based HTML Notebook Server that serves up an HTML5/Javascript Notebook client."
|
||||
msgstr ""
|
||||
"De Jupyter HTML Notebook.\n"
|
||||
" \n"
|
||||
"Hiermee wordt een op Tornado gebaseerde HTML-notebookserver gelanceerd die een HTML5/Javascript-laptopclient bedient."
|
||||
|
||||
#: notebook/notebookapp.py:509
|
||||
msgid ""
|
||||
"Deprecated: Use minified JS file or not, mainly use during dev to avoid JS "
|
||||
"recompilation"
|
||||
msgstr ""
|
||||
"Afgeschaft: Gebruik minified JS-bestand of niet, voornamelijk gebruiken "
|
||||
"tijdens dev om JS recompilatie te voorkomen"
|
||||
|
||||
#: notebook/notebookapp.py:540
|
||||
msgid "Set the Access-Control-Allow-Credentials: true header"
|
||||
msgstr "De access-control-allow-credentials instellen: true header"
|
||||
|
||||
#: notebook/notebookapp.py:544
|
||||
msgid "Whether to allow the user to run the notebook as root."
|
||||
msgstr "Of de gebruiker het notebook als root mag activeren."
|
||||
|
||||
#: notebook/notebookapp.py:548
|
||||
msgid "The default URL to redirect to from `/`"
|
||||
msgstr "De standaard-URL om naar '/' te leiden"
|
||||
|
||||
#: notebook/notebookapp.py:552
|
||||
msgid "The IP address the notebook server will listen on."
|
||||
msgstr "Het IP-adres waar de notebookserver op geactiveerd wordt."
|
||||
|
||||
#: notebook/notebookapp.py:565
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Cannot bind to localhost, using 127.0.0.1 as default ip\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"Kan niet binden aan localhost, met 127.0.0.1 als standaardip\n"
|
||||
"%s"
|
||||
|
||||
#: notebook/notebookapp.py:579
|
||||
msgid "The port the notebook server will listen on."
|
||||
msgstr "De port waarop de notebookserver geactiveerd wordt."
|
||||
|
||||
#: notebook/notebookapp.py:583
|
||||
msgid ""
|
||||
"The number of additional ports to try if the specified port is not "
|
||||
"available."
|
||||
msgstr ""
|
||||
"Het aantal extra ports dat moet worden geprobeerd als de opgegeven port "
|
||||
"niet beschikbaar is."
|
||||
|
||||
#: notebook/notebookapp.py:587
|
||||
msgid "The full path to an SSL/TLS certificate file."
|
||||
msgstr "Het volledige pad naar een SSL/TLS-certificaatbestand."
|
||||
|
||||
#: notebook/notebookapp.py:591
|
||||
msgid "The full path to a private key file for usage with SSL/TLS."
|
||||
msgstr ""
|
||||
"Het volledige pad naar een privésleutelbestand voor gebruik met SSL/TLS."
|
||||
|
||||
#: notebook/notebookapp.py:595
|
||||
msgid ""
|
||||
"The full path to a certificate authority certificate for SSL/TLS client "
|
||||
"authentication."
|
||||
msgstr ""
|
||||
"Het volledige pad naar een certificaat van certificaatautoriteit voor "
|
||||
"SSL/TLS-clientverificatie."
|
||||
|
||||
#: notebook/notebookapp.py:599
|
||||
msgid "The file where the cookie secret is stored."
|
||||
msgstr "Het bestand waarin het cookiegeheim wordt opgeslagen."
|
||||
|
||||
#: notebook/notebookapp.py:628
|
||||
#, python-format
|
||||
msgid "Writing notebook server cookie secret to %s"
|
||||
msgstr "Cookiegeheim voor notebookserver schrijven naar %s"
|
||||
|
||||
#: notebook/notebookapp.py:635
|
||||
#, python-format
|
||||
msgid "Could not set permissions on %s"
|
||||
msgstr "Kan geen machtigingen instellen op %s"
|
||||
|
||||
#: notebook/notebookapp.py:640
|
||||
msgid ""
|
||||
"Token used for authenticating first-time connections to the server.\n"
|
||||
"\n"
|
||||
" When no password is enabled,\n"
|
||||
" the default is to generate a new, random token.\n"
|
||||
"\n"
|
||||
" Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Token wordt gebruikt voor het verifiëren van eerste verbindingen met de server.\n"
|
||||
"\n"
|
||||
"Wanneer er geen wachtwoord is ingeschakeld,\n"
|
||||
" de standaardinstelling is het genereren van een nieuwe, willekeurige token.\n"
|
||||
"\n"
|
||||
"Als u een lege tekenreeks instelt, wordt de verificatie helemaal uitgeschakeld, wat niet wordt aanbevolen.\n"
|
||||
" "
|
||||
|
||||
#: notebook/notebookapp.py:650
|
||||
msgid ""
|
||||
"One-time token used for opening a browser.\n"
|
||||
" Once used, this token cannot be used again.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Eenmalige token die wordt gebruikt voor het openen van een browser.\n"
|
||||
" Eenmaal gebruikt, kan dit token niet opnieuw worden gebruikt.\n"
|
||||
" "
|
||||
|
||||
#: notebook/notebookapp.py:726
|
||||
msgid ""
|
||||
"Specify Where to open the notebook on startup. This is the\n"
|
||||
" `new` argument passed to the standard library method `webbrowser.open`.\n"
|
||||
" The behaviour is not guaranteed, but depends on browser support. Valid\n"
|
||||
" values are:\n"
|
||||
" 2 opens a new tab,\n"
|
||||
" 1 opens a new window,\n"
|
||||
" 0 opens in an existing window.\n"
|
||||
" See the `webbrowser.open` documentation for details.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Geef op waar u het notebook moet openen bij het opstarten. Dit is de\n"
|
||||
" 'nieuw' argument doorgegeven aan de standaard bibliotheek methode 'webbrowser.open'.\n"
|
||||
" Het gedrag is niet gegarandeerd, maar is afhankelijk van browserondersteuning. Geldig\n"
|
||||
" waarden zijn:\n"
|
||||
" 2 opent een nieuw tabblad,\n"
|
||||
" 1 opent een nieuw venster,\n"
|
||||
" 0 wordt geopend in een bestaand venster.\n"
|
||||
" Zie de documentatie 'webbrowser.open' voor meer informatie.\n"
|
||||
" "
|
||||
|
||||
#: notebook/notebookapp.py:737
|
||||
msgid "DEPRECATED, use tornado_settings"
|
||||
msgstr "DEPRECATED, gebruik tornado_settings"
|
||||
|
||||
#: notebook/notebookapp.py:742
|
||||
msgid ""
|
||||
"\n"
|
||||
" webapp_settings is deprecated, use tornado_settings.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"webapp_settings is deprecated, gebruik tornado_settings.\n"
|
||||
|
||||
#: notebook/notebookapp.py:746
|
||||
msgid ""
|
||||
"Supply overrides for the tornado.web.Application that the Jupyter notebook "
|
||||
"uses."
|
||||
msgstr ""
|
||||
"Geef extra instellingen voor de tornado.web.Application die gebruikt wordt door de "
|
||||
" Jupyter notebook."
|
||||
|
||||
#: notebook/notebookapp.py:750
|
||||
msgid ""
|
||||
"\n"
|
||||
" Set the tornado compression options for websocket connections.\n"
|
||||
"\n"
|
||||
" This value will be returned from :meth:`WebSocketHandler.get_compression_options`.\n"
|
||||
" None (default) will disable compression.\n"
|
||||
" A dict (even an empty one) will enable compression.\n"
|
||||
"\n"
|
||||
" See the tornado docs for WebSocketHandler.get_compression_options for details.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Stel de tornadocompressieopties in voor websocketverbindingen.\n"
|
||||
"\n"
|
||||
"Deze waarde wordt geretourneerd van :meth:'WebSocketHandler.get_compression_options'.\n"
|
||||
" Geen (standaard) schakelt compressie uit.\n"
|
||||
" Een dict (zelfs een lege) zal compressie mogelijk maken.\n"
|
||||
"\n"
|
||||
"Zie de tornadodocumenten voor WebSocketHandler.get_compression_options voor meer informatie.\n"
|
||||
" "
|
||||
|
||||
#: notebook/notebookapp.py:761
|
||||
msgid "Supply overrides for terminado. Currently only supports \"shell_command\"."
|
||||
msgstr ""
|
||||
"Supply overrides voor terminado. Ondersteunt momenteel alleen een "
|
||||
"\"shell_command\"."
|
||||
|
||||
#: notebook/notebookapp.py:764
|
||||
msgid ""
|
||||
"Extra keyword arguments to pass to `set_secure_cookie`. See tornado's "
|
||||
"set_secure_cookie docs for details."
|
||||
msgstr ""
|
||||
"Extra trefwoordargumenten om door te geven aan 'set_secure_cookie'. Zie "
|
||||
"tornado's set_secure_cookie documenten voor meer informatie."
|
||||
|
||||
#: notebook/notebookapp.py:768
|
||||
msgid ""
|
||||
"Supply SSL options for the tornado HTTPServer.\n"
|
||||
" See the tornado docs for details."
|
||||
msgstr ""
|
||||
"SSL-opties leveren voor de tornado HTTPServer.\n"
|
||||
" Zie de tornado docs voor meer informatie."
|
||||
|
||||
#: notebook/notebookapp.py:772
|
||||
msgid "Supply extra arguments that will be passed to Jinja environment."
|
||||
msgstr ""
|
||||
"Vul extra argumenten aan die zullen worden doorgegeven aan de Jinja environment."
|
||||
|
||||
#: notebook/notebookapp.py:776
|
||||
msgid "Extra variables to supply to jinja templates when rendering."
|
||||
msgstr "Extra variabelen om aan te vullen aan de jinja-sjablonen bij het renderen."
|
||||
|
||||
#: notebook/notebookapp.py:812
|
||||
msgid "DEPRECATED use base_url"
|
||||
msgstr "DEPRECATED gebruik base_url"
|
||||
|
||||
#: notebook/notebookapp.py:816
|
||||
msgid "base_project_url is deprecated, use base_url"
|
||||
msgstr "base_project_url is deprecated, gebruik base_url"
|
||||
|
||||
#: notebook/notebookapp.py:832
|
||||
msgid "Path to search for custom.js, css"
|
||||
msgstr "Pad om te zoeken naar custom.js, css"
|
||||
|
||||
#: notebook/notebookapp.py:844
|
||||
msgid ""
|
||||
"Extra paths to search for serving jinja templates.\n"
|
||||
"\n"
|
||||
" Can be used to override templates from notebook.templates."
|
||||
msgstr ""
|
||||
"Extra paden om te zoeken voor het activeren van jinja-sjablonen.\n"
|
||||
"\n"
|
||||
"Kan worden gebruikt om sjablonen van notebook.templates te overschrijven."
|
||||
|
||||
#: notebook/notebookapp.py:855
|
||||
msgid "extra paths to look for Javascript notebook extensions"
|
||||
msgstr "extra paden om te zoeken naar Javascript-notebookextensies"
|
||||
|
||||
#: notebook/notebookapp.py:900
|
||||
#, python-format
|
||||
msgid "Using MathJax: %s"
|
||||
msgstr "MathJax gebruiken: %s"
|
||||
|
||||
#: notebook/notebookapp.py:903
|
||||
msgid "The MathJax.js configuration file that is to be used."
|
||||
msgstr "Het configuratiebestand MathJax.js dat moet worden gebruikt."
|
||||
|
||||
#: notebook/notebookapp.py:908
|
||||
#, python-format
|
||||
msgid "Using MathJax configuration file: %s"
|
||||
msgstr "MathJax-configuratiebestand gebruiken: %s"
|
||||
|
||||
#: notebook/notebookapp.py:914
|
||||
msgid "The notebook manager class to use."
|
||||
msgstr "De notebook manager klasse te gebruiken."
|
||||
|
||||
#: notebook/notebookapp.py:920
|
||||
msgid "The kernel manager class to use."
|
||||
msgstr "De kernel manager klasse om te gebruiken."
|
||||
|
||||
#: notebook/notebookapp.py:926
|
||||
msgid "The session manager class to use."
|
||||
msgstr "De sessie manager klasse die u gebruiken."
|
||||
|
||||
#: notebook/notebookapp.py:932
|
||||
msgid "The config manager class to use"
|
||||
msgstr "De config manager klasse te gebruiken"
|
||||
|
||||
#: notebook/notebookapp.py:953
|
||||
msgid "The login handler class to use."
|
||||
msgstr "De login handler klasse te gebruiken."
|
||||
|
||||
#: notebook/notebookapp.py:960
|
||||
msgid "The logout handler class to use."
|
||||
msgstr "De afmeld handler klasse die u wilt gebruiken."
|
||||
|
||||
#: notebook/notebookapp.py:964
|
||||
msgid ""
|
||||
"Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-"
|
||||
"Ip/X-Forwarded-For headerssent by the upstream reverse proxy. Necessary if "
|
||||
"the proxy handles SSL"
|
||||
msgstr ""
|
||||
"X-Scheme/X-Forwarded-Proto en X-Real-Ip/X-Forwarded-For headerssent door de "
|
||||
"upstream reverse proxy al dan niet vertrouwen. Noodzakelijk als de proxy SSL"
|
||||
" verwerkt"
|
||||
|
||||
#: notebook/notebookapp.py:976
|
||||
msgid ""
|
||||
"\n"
|
||||
" DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"UITGESCHAKELD: gebruik %pylab of %matplotlib in het notebook om matplotlib in te schakelen.\n"
|
||||
" "
|
||||
|
||||
#: notebook/notebookapp.py:988
|
||||
msgid "Support for specifying --pylab on the command line has been removed."
|
||||
msgstr ""
|
||||
"Ondersteuning voor het opgeven van --pylab op de opdrachtregel is "
|
||||
"verwijderd."
|
||||
|
||||
#: notebook/notebookapp.py:990
|
||||
msgid "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself."
|
||||
msgstr "Gebruik '%pylab{0}' of '%matplotlib{0}' in het notebook zelf."
|
||||
|
||||
#: notebook/notebookapp.py:995
|
||||
msgid "The directory to use for notebooks and kernels."
|
||||
msgstr "De map die u wilt gebruiken voor notebooks en kernels."
|
||||
|
||||
#: notebook/notebookapp.py:1018
|
||||
#, python-format
|
||||
msgid "No such notebook dir: '%r'"
|
||||
msgstr "Geen dergelijke notebook dir: '%r'"
|
||||
|
||||
#: notebook/notebookapp.py:1031
|
||||
msgid "DEPRECATED use the nbserver_extensions dict instead"
|
||||
msgstr "DEPRECATED gebruikt in plaats daarvan de nbserver_extensions dict"
|
||||
|
||||
#: notebook/notebookapp.py:1036
|
||||
msgid "server_extensions is deprecated, use nbserver_extensions"
|
||||
msgstr "server_extensions is afgeschaft, gebruik nbserver_extensions"
|
||||
|
||||
#: notebook/notebookapp.py:1040
|
||||
msgid ""
|
||||
"Dict of Python modules to load as notebook server extensions.Entry values "
|
||||
"can be used to enable and disable the loading ofthe extensions. The "
|
||||
"extensions will be loaded in alphabetical order."
|
||||
msgstr ""
|
||||
"Dict van Python-modules te laden als notebook server extensies. "
|
||||
"Invoerwaarden kunnen worden gebruikt om het laden van de extensies in en uit"
|
||||
" te schakelen. De extensies worden in alfabetische volgorde geladen."
|
||||
|
||||
#: notebook/notebookapp.py:1049
|
||||
msgid "Reraise exceptions encountered loading server extensions?"
|
||||
msgstr "Exceptions opnieuw weergeven die geraised waren tijdens het laden van"
|
||||
" de server-extensies?"
|
||||
|
||||
#: notebook/notebookapp.py:1052
|
||||
msgid ""
|
||||
"(msgs/sec)\n"
|
||||
" Maximum rate at which messages can be sent on iopub before they are\n"
|
||||
" limited."
|
||||
msgstr ""
|
||||
"(msgs/sec)\n"
|
||||
" Maximale ratio waarmee berichten op iopub kunnen worden verzonden voordat ze\n"
|
||||
" worden beperkt."
|
||||
|
||||
#: notebook/notebookapp.py:1056
|
||||
msgid ""
|
||||
"(bytes/sec)\n"
|
||||
" Maximum rate at which stream output can be sent on iopub before they are\n"
|
||||
" limited."
|
||||
msgstr ""
|
||||
"(bytes/sec)\n"
|
||||
" Maximale ratio waarmee streamoutput op iopub kan worden verzonden voordat ze\n"
|
||||
" worden beperkt."
|
||||
|
||||
#: notebook/notebookapp.py:1060
|
||||
msgid ""
|
||||
"(sec) Time window used to \n"
|
||||
" check the message and data rate limits."
|
||||
msgstr ""
|
||||
"(sec) Tijdvenster gebruikt om \n"
|
||||
" de limieten voor het verzenden van berichten en de gegevenssnelheiden te"
|
||||
" controleren."
|
||||
|
||||
#: notebook/notebookapp.py:1071
|
||||
#, python-format
|
||||
msgid "No such file or directory: %s"
|
||||
msgstr "Geen dergelijk bestand of map: %s"
|
||||
|
||||
#: notebook/notebookapp.py:1141
|
||||
msgid "Notebook servers are configured to only be run with a password."
|
||||
msgstr ""
|
||||
"Notebookservers zijn geconfigureerd om alleen met een wachtwoord te worden "
|
||||
"uitgevoerd."
|
||||
|
||||
#: notebook/notebookapp.py:1142
|
||||
msgid "Hint: run the following command to set a password"
|
||||
msgstr "Tip: voer de volgende opdracht uit om een wachtwoord in te stellen"
|
||||
|
||||
#: notebook/notebookapp.py:1143
|
||||
msgid "\t$ python -m notebook.auth password"
|
||||
msgstr "\t$ python -m notebook.auth wachtwoord"
|
||||
|
||||
#: notebook/notebookapp.py:1181
|
||||
#, python-format
|
||||
msgid "The port %i is already in use, trying another port."
|
||||
msgstr "De port %i is al in gebruik, proberen een andere port."
|
||||
|
||||
#: notebook/notebookapp.py:1184
|
||||
#, python-format
|
||||
msgid "Permission to listen on port %i denied"
|
||||
msgstr "Toestemming om te luisteren op port %i geweigerd"
|
||||
|
||||
#: notebook/notebookapp.py:1193
|
||||
msgid ""
|
||||
"ERROR: the notebook server could not be started because no available port "
|
||||
"could be found."
|
||||
msgstr ""
|
||||
"FOUT: de notebookserver kan niet worden gestart omdat er geen beschikbare "
|
||||
"port kon worden gevonden."
|
||||
|
||||
#: notebook/notebookapp.py:1199
|
||||
msgid "[all ip addresses on your system]"
|
||||
msgstr "[alle IP-adressen op uw systeem]"
|
||||
|
||||
#: notebook/notebookapp.py:1223
|
||||
#, python-format
|
||||
msgid "Terminals not available (error was %s)"
|
||||
msgstr "Terminals niet beschikbaar (fout was %s)"
|
||||
|
||||
#: notebook/notebookapp.py:1259
|
||||
msgid "interrupted"
|
||||
msgstr "onderbroken"
|
||||
|
||||
#: notebook/notebookapp.py:1261
|
||||
msgid "y"
|
||||
msgstr "y"
|
||||
|
||||
#: notebook/notebookapp.py:1262
|
||||
msgid "n"
|
||||
msgstr "n"
|
||||
|
||||
#: notebook/notebookapp.py:1263
|
||||
#, python-format
|
||||
msgid "Shutdown this notebook server (%s/[%s])? "
|
||||
msgstr "Deze notebookserver afsluiten (%s/[%s])? "
|
||||
|
||||
#: notebook/notebookapp.py:1269
|
||||
msgid "Shutdown confirmed"
|
||||
msgstr "Afsluiten bevestigd"
|
||||
|
||||
#: notebook/notebookapp.py:1273
|
||||
msgid "No answer for 5s:"
|
||||
msgstr "Geen antwoord voor 5s:"
|
||||
|
||||
#: notebook/notebookapp.py:1274
|
||||
msgid "resuming operation..."
|
||||
msgstr "hervatting van de werking..."
|
||||
|
||||
#: notebook/notebookapp.py:1282
|
||||
#, python-format
|
||||
msgid "received signal %s, stopping"
|
||||
msgstr "ontvangen signaal %s, stoppen"
|
||||
|
||||
#: notebook/notebookapp.py:1338
|
||||
#, python-format
|
||||
msgid "Error loading server extension %s"
|
||||
msgstr "Foutladen serverextensie %s"
|
||||
|
||||
#: notebook/notebookapp.py:1369
|
||||
#, python-format
|
||||
msgid "Shutting down %d kernels"
|
||||
msgstr "%d-kernels afsluiten"
|
||||
|
||||
#: notebook/notebookapp.py:1375
|
||||
#, python-format
|
||||
msgid "%d active kernel"
|
||||
msgid_plural "%d active kernels"
|
||||
msgstr[0] "%d actieve kernel"
|
||||
msgstr[1] "%d actieve kernel"
|
||||
|
||||
#: notebook/notebookapp.py:1379
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The Jupyter Notebook is running at:\n"
|
||||
"\r"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"De Jupyter Notebook draait op:\n"
|
||||
"\r"
|
||||
"%s"
|
||||
|
||||
#: notebook/notebookapp.py:1426
|
||||
msgid "Running as root is not recommended. Use --allow-root to bypass."
|
||||
msgstr ""
|
||||
"Hardlopen als root wordt niet aanbevolen. Gebruik --allow-root te "
|
||||
"omzeilen."
|
||||
|
||||
#: notebook/notebookapp.py:1432
|
||||
msgid ""
|
||||
"Use Control-C to stop this server and shut down all kernels (twice to skip "
|
||||
"confirmation)."
|
||||
msgstr ""
|
||||
"Gebruik Control-C om deze server te stoppen en sluit alle kernels af (twee "
|
||||
"keer om bevestiging over te slaan)."
|
||||
|
||||
#: notebook/notebookapp.py:1434
|
||||
msgid ""
|
||||
"Welcome to Project Jupyter! Explore the various tools available and their "
|
||||
"corresponding documentation. If you are interested in contributing to the "
|
||||
"platform, please visit the communityresources section at "
|
||||
"http://jupyter.org/community.html."
|
||||
msgstr ""
|
||||
"Welkom bij Project Jupyter! Bekijk de verschillende tools die beschikbaar "
|
||||
"zijn en de bijbehorende documentatie. Als je geïnteresseerd bent om bij te "
|
||||
"dragen aan het platform, ga dan naar de communityresources sectie op "
|
||||
"http://jupyter.org/community.html."
|
||||
|
||||
#: notebook/notebookapp.py:1445
|
||||
#, python-format
|
||||
msgid "No web browser found: %s."
|
||||
msgstr "Geen webbrowser gevonden: %s."
|
||||
|
||||
#: notebook/notebookapp.py:1450
|
||||
#, python-format
|
||||
msgid "%s does not exist"
|
||||
msgstr "%s bestaat niet"
|
||||
|
||||
#: notebook/notebookapp.py:1484
|
||||
msgid "Interrupted..."
|
||||
msgstr "Onderbroken..."
|
||||
|
||||
#: notebook/services/contents/filemanager.py:506
|
||||
#, python-format
|
||||
msgid "Serving notebooks from local directory: %s"
|
||||
msgstr "Notebooks uit lokale map activeren: %s"
|
||||
|
||||
#: notebook/services/contents/manager.py:68
|
||||
msgid "Untitled"
|
||||
msgstr "Naamloos"
|
||||
@ -1,480 +0,0 @@
|
||||
# Translations template for Jupyter.
|
||||
# Copyright (C) 2017 ORGANIZATION
|
||||
# This file is distributed under the same license as the Jupyter project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Jupyter VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2017-07-08 21:52-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: notebook/notebookapp.py:53
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:57
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have < 1.1.0"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:59
|
||||
#, python-format
|
||||
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:209
|
||||
msgid "The `ignore_minified_js` flag is deprecated and no longer works."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:210
|
||||
#, python-format
|
||||
msgid "Alternatively use `%s` when working on the notebook's Javascript and LESS"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:211
|
||||
msgid "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:389
|
||||
msgid "List currently running notebook servers."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:393
|
||||
msgid "Produce machine-readable JSON output."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:397
|
||||
msgid "If True, each line of output will be a JSON object with the details from the server info file."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:402
|
||||
msgid "Currently running servers:"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:419
|
||||
msgid "Don't open the notebook in a browser after startup."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:423
|
||||
msgid "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:439
|
||||
msgid "Allow the notebook to be run from root user."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:470
|
||||
msgid ""
|
||||
"The Jupyter HTML Notebook.\n"
|
||||
" \n"
|
||||
" This launches a Tornado based HTML Notebook Server that serves up an HTML5/Javascript Notebook client."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:509
|
||||
msgid "Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:540
|
||||
msgid "Set the Access-Control-Allow-Credentials: true header"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:544
|
||||
msgid "Whether to allow the user to run the notebook as root."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:548
|
||||
msgid "The default URL to redirect to from `/`"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:552
|
||||
msgid "The IP address the notebook server will listen on."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:565
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Cannot bind to localhost, using 127.0.0.1 as default ip\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:579
|
||||
msgid "The port the notebook server will listen on."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:583
|
||||
msgid "The number of additional ports to try if the specified port is not available."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:587
|
||||
msgid "The full path to an SSL/TLS certificate file."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:591
|
||||
msgid "The full path to a private key file for usage with SSL/TLS."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:595
|
||||
msgid "The full path to a certificate authority certificate for SSL/TLS client authentication."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:599
|
||||
msgid "The file where the cookie secret is stored."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:628
|
||||
#, python-format
|
||||
msgid "Writing notebook server cookie secret to %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:635
|
||||
#, python-format
|
||||
msgid "Could not set permissions on %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:640
|
||||
msgid ""
|
||||
"Token used for authenticating first-time connections to the server.\n"
|
||||
"\n"
|
||||
" When no password is enabled,\n"
|
||||
" the default is to generate a new, random token.\n"
|
||||
"\n"
|
||||
" Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:650
|
||||
msgid ""
|
||||
"One-time token used for opening a browser.\n"
|
||||
" Once used, this token cannot be used again.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:726
|
||||
msgid ""
|
||||
"Specify Where to open the notebook on startup. This is the\n"
|
||||
" `new` argument passed to the standard library method `webbrowser.open`.\n"
|
||||
" The behaviour is not guaranteed, but depends on browser support. Valid\n"
|
||||
" values are:\n"
|
||||
" 2 opens a new tab,\n"
|
||||
" 1 opens a new window,\n"
|
||||
" 0 opens in an existing window.\n"
|
||||
" See the `webbrowser.open` documentation for details.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:737
|
||||
msgid "DEPRECATED, use tornado_settings"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:742
|
||||
msgid ""
|
||||
"\n"
|
||||
" webapp_settings is deprecated, use tornado_settings.\n"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:746
|
||||
msgid "Supply overrides for the tornado.web.Application that the Jupyter notebook uses."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:750
|
||||
msgid ""
|
||||
"\n"
|
||||
" Set the tornado compression options for websocket connections.\n"
|
||||
"\n"
|
||||
" This value will be returned from :meth:`WebSocketHandler.get_compression_options`.\n"
|
||||
" None (default) will disable compression.\n"
|
||||
" A dict (even an empty one) will enable compression.\n"
|
||||
"\n"
|
||||
" See the tornado docs for WebSocketHandler.get_compression_options for details.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:761
|
||||
msgid "Supply overrides for terminado. Currently only supports \"shell_command\"."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:764
|
||||
msgid "Extra keyword arguments to pass to `set_secure_cookie`. See tornado's set_secure_cookie docs for details."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:768
|
||||
msgid ""
|
||||
"Supply SSL options for the tornado HTTPServer.\n"
|
||||
" See the tornado docs for details."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:772
|
||||
msgid "Supply extra arguments that will be passed to Jinja environment."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:776
|
||||
msgid "Extra variables to supply to jinja templates when rendering."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:812
|
||||
msgid "DEPRECATED use base_url"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:816
|
||||
msgid "base_project_url is deprecated, use base_url"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:832
|
||||
msgid "Path to search for custom.js, css"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:844
|
||||
msgid ""
|
||||
"Extra paths to search for serving jinja templates.\n"
|
||||
"\n"
|
||||
" Can be used to override templates from notebook.templates."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:855
|
||||
msgid "extra paths to look for Javascript notebook extensions"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:900
|
||||
#, python-format
|
||||
msgid "Using MathJax: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:903
|
||||
msgid "The MathJax.js configuration file that is to be used."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:908
|
||||
#, python-format
|
||||
msgid "Using MathJax configuration file: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:914
|
||||
msgid "The notebook manager class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:920
|
||||
msgid "The kernel manager class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:926
|
||||
msgid "The session manager class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:932
|
||||
msgid "The config manager class to use"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:953
|
||||
msgid "The login handler class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:960
|
||||
msgid "The logout handler class to use."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:964
|
||||
msgid "Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headerssent by the upstream reverse proxy. Necessary if the proxy handles SSL"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:976
|
||||
msgid ""
|
||||
"\n"
|
||||
" DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:988
|
||||
msgid "Support for specifying --pylab on the command line has been removed."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:990
|
||||
msgid "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:995
|
||||
msgid "The directory to use for notebooks and kernels."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1018
|
||||
#, python-format
|
||||
msgid "No such notebook dir: '%r'"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1031
|
||||
msgid "DEPRECATED use the nbserver_extensions dict instead"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1036
|
||||
msgid "server_extensions is deprecated, use nbserver_extensions"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1040
|
||||
msgid "Dict of Python modules to load as notebook server extensions.Entry values can be used to enable and disable the loading ofthe extensions. The extensions will be loaded in alphabetical order."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1049
|
||||
msgid "Reraise exceptions encountered loading server extensions?"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1052
|
||||
msgid ""
|
||||
"(msgs/sec)\n"
|
||||
" Maximum rate at which messages can be sent on iopub before they are\n"
|
||||
" limited."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1056
|
||||
msgid ""
|
||||
"(bytes/sec)\n"
|
||||
" Maximum rate at which stream output can be sent on iopub before they are\n"
|
||||
" limited."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1060
|
||||
msgid ""
|
||||
"(sec) Time window used to \n"
|
||||
" check the message and data rate limits."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1071
|
||||
#, python-format
|
||||
msgid "No such file or directory: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1141
|
||||
msgid "Notebook servers are configured to only be run with a password."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1142
|
||||
msgid "Hint: run the following command to set a password"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1143
|
||||
msgid "\t$ python -m notebook.auth password"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1181
|
||||
#, python-format
|
||||
msgid "The port %i is already in use, trying another port."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1184
|
||||
#, python-format
|
||||
msgid "Permission to listen on port %i denied"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1193
|
||||
msgid "ERROR: the notebook server could not be started because no available port could be found."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1199
|
||||
msgid "[all ip addresses on your system]"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1223
|
||||
#, python-format
|
||||
msgid "Terminals not available (error was %s)"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1259
|
||||
msgid "interrupted"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1261
|
||||
msgid "y"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1262
|
||||
msgid "n"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1263
|
||||
#, python-format
|
||||
msgid "Shutdown this notebook server (%s/[%s])? "
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1269
|
||||
msgid "Shutdown confirmed"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1273
|
||||
msgid "No answer for 5s:"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1274
|
||||
msgid "resuming operation..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1282
|
||||
#, python-format
|
||||
msgid "received signal %s, stopping"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1338
|
||||
#, python-format
|
||||
msgid "Error loading server extension %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1369
|
||||
#, python-format
|
||||
msgid "Shutting down %d kernels"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1375
|
||||
#, python-format
|
||||
msgid "%d active kernel"
|
||||
msgid_plural "%d active kernels"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: notebook/notebookapp.py:1379
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The Jupyter Notebook is running at:\n"
|
||||
"\r"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1426
|
||||
msgid "Running as root is not recommended. Use --allow-root to bypass."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1432
|
||||
msgid "Use Control-C to stop this server and shut down all kernels (twice to skip confirmation)."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1434
|
||||
msgid "Welcome to Project Jupyter! Explore the various tools available and their corresponding documentation. If you are interested in contributing to the platform, please visit the communityresources section at http://jupyter.org/community.html."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1445
|
||||
#, python-format
|
||||
msgid "No web browser found: %s."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1450
|
||||
#, python-format
|
||||
msgid "%s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/notebookapp.py:1484
|
||||
msgid "Interrupted..."
|
||||
msgstr ""
|
||||
|
||||
#: notebook/services/contents/filemanager.py:506
|
||||
#, python-format
|
||||
msgid "Serving notebooks from local directory: %s"
|
||||
msgstr ""
|
||||
|
||||
#: notebook/services/contents/manager.py:68
|
||||
msgid "Untitled"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,615 +0,0 @@
|
||||
"""Notebook Javascript Test Controller
|
||||
|
||||
This module runs one or more subprocesses which will actually run the Javascript
|
||||
test suite.
|
||||
"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import multiprocessing.pool
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import signal
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
from io import BytesIO
|
||||
from threading import Thread, Lock, Event
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from jupyter_core.paths import jupyter_runtime_dir
|
||||
from ipython_genutils.py3compat import bytes_to_str, which
|
||||
from notebook._sysinfo import get_sys_info
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
|
||||
from subprocess import TimeoutExpired
|
||||
def popen_wait(p, timeout):
|
||||
return p.wait(timeout)
|
||||
|
||||
NOTEBOOK_SHUTDOWN_TIMEOUT = 10
|
||||
|
||||
have = {}
|
||||
have['casperjs'] = bool(which('casperjs'))
|
||||
have['phantomjs'] = bool(which('phantomjs'))
|
||||
have['slimerjs'] = bool(which('slimerjs'))
|
||||
|
||||
class StreamCapturer(Thread):
|
||||
daemon = True # Don't hang if main thread crashes
|
||||
started = False
|
||||
def __init__(self, echo=False):
|
||||
super().__init__()
|
||||
self.echo = echo
|
||||
self.streams = []
|
||||
self.buffer = BytesIO()
|
||||
self.readfd, self.writefd = os.pipe()
|
||||
self.buffer_lock = Lock()
|
||||
self.stop = Event()
|
||||
|
||||
def run(self):
|
||||
self.started = True
|
||||
|
||||
while not self.stop.is_set():
|
||||
chunk = os.read(self.readfd, 1024)
|
||||
|
||||
with self.buffer_lock:
|
||||
self.buffer.write(chunk)
|
||||
if self.echo:
|
||||
sys.stdout.write(bytes_to_str(chunk))
|
||||
|
||||
os.close(self.readfd)
|
||||
os.close(self.writefd)
|
||||
|
||||
def reset_buffer(self):
|
||||
with self.buffer_lock:
|
||||
self.buffer.truncate(0)
|
||||
self.buffer.seek(0)
|
||||
|
||||
def get_buffer(self):
|
||||
with self.buffer_lock:
|
||||
return self.buffer.getvalue()
|
||||
|
||||
def ensure_started(self):
|
||||
if not self.started:
|
||||
self.start()
|
||||
|
||||
def halt(self):
|
||||
"""Safely stop the thread."""
|
||||
if not self.started:
|
||||
return
|
||||
|
||||
self.stop.set()
|
||||
os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
|
||||
self.join()
|
||||
|
||||
|
||||
class TestController(object):
|
||||
"""Run tests in a subprocess
|
||||
"""
|
||||
#: str, test group to be executed.
|
||||
section = None
|
||||
#: list, command line arguments to be executed
|
||||
cmd = None
|
||||
#: dict, extra environment variables to set for the subprocess
|
||||
env = None
|
||||
#: list, TemporaryDirectory instances to clear up when the process finishes
|
||||
dirs = None
|
||||
#: subprocess.Popen instance
|
||||
process = None
|
||||
#: str, process stdout+stderr
|
||||
stdout = None
|
||||
|
||||
def __init__(self):
|
||||
self.cmd = []
|
||||
self.env = {}
|
||||
self.dirs = []
|
||||
|
||||
def setup(self):
|
||||
"""Create temporary directories etc.
|
||||
|
||||
This is only called when we know the test group will be run. Things
|
||||
created here may be cleaned up by self.cleanup().
|
||||
"""
|
||||
pass
|
||||
|
||||
def launch(self, buffer_output=False, capture_output=False):
|
||||
# print('*** ENV:', self.env) # dbg
|
||||
# print('*** CMD:', self.cmd) # dbg
|
||||
env = os.environ.copy()
|
||||
env.update(self.env)
|
||||
if buffer_output:
|
||||
capture_output = True
|
||||
self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
|
||||
c.start()
|
||||
stdout = c.writefd if capture_output else None
|
||||
stderr = subprocess.STDOUT if capture_output else None
|
||||
self.process = subprocess.Popen(self.cmd, stdout=stdout,
|
||||
stderr=stderr, env=env)
|
||||
|
||||
def wait(self):
|
||||
self.process.wait()
|
||||
self.stdout_capturer.halt()
|
||||
self.stdout = self.stdout_capturer.get_buffer()
|
||||
return self.process.returncode
|
||||
|
||||
def print_extra_info(self):
|
||||
"""Print extra information about this test run.
|
||||
|
||||
If we're running in parallel and showing the concise view, this is only
|
||||
called if the test group fails. Otherwise, it's called before the test
|
||||
group is started.
|
||||
|
||||
The base implementation does nothing, but it can be overridden by
|
||||
subclasses.
|
||||
"""
|
||||
return
|
||||
|
||||
def cleanup_process(self):
|
||||
"""Cleanup on exit by killing any leftover processes."""
|
||||
subp = self.process
|
||||
if subp is None or (subp.poll() is not None):
|
||||
return # Process doesn't exist, or is already dead.
|
||||
|
||||
try:
|
||||
print('Cleaning up stale PID: %d' % subp.pid)
|
||||
subp.kill()
|
||||
except: # (OSError, WindowsError) ?
|
||||
# This is just a best effort, if we fail or the process was
|
||||
# really gone, ignore it.
|
||||
pass
|
||||
else:
|
||||
for i in range(10):
|
||||
if subp.poll() is None:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
break
|
||||
|
||||
if subp.poll() is None:
|
||||
# The process did not die...
|
||||
print('... failed. Manual cleanup may be required.')
|
||||
|
||||
def cleanup(self):
|
||||
"Kill process if it's still alive, and clean up temporary directories"
|
||||
self.cleanup_process()
|
||||
for td in self.dirs:
|
||||
td.cleanup()
|
||||
|
||||
__del__ = cleanup
|
||||
|
||||
|
||||
def get_js_test_dir():
|
||||
import notebook.tests as t
|
||||
return os.path.join(os.path.dirname(t.__file__), '')
|
||||
|
||||
def all_js_groups():
|
||||
import glob
|
||||
test_dir = get_js_test_dir()
|
||||
all_subdirs = glob.glob(test_dir + '[!_]*/')
|
||||
return [os.path.relpath(x, test_dir) for x in all_subdirs]
|
||||
|
||||
class JSController(TestController):
|
||||
"""Run CasperJS tests """
|
||||
|
||||
requirements = ['casperjs']
|
||||
|
||||
def __init__(self, section, xunit=True, engine='phantomjs', url=None):
|
||||
"""Create new test runner."""
|
||||
TestController.__init__(self)
|
||||
self.engine = engine
|
||||
self.section = section
|
||||
self.xunit = xunit
|
||||
self.url = url
|
||||
# run with a base URL that would be escaped,
|
||||
# to test that we don't double-escape URLs
|
||||
self.base_url = '/a@b/'
|
||||
self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
|
||||
js_test_dir = get_js_test_dir()
|
||||
includes = '--includes=' + os.path.join(js_test_dir,'util.js')
|
||||
test_cases = os.path.join(js_test_dir, self.section)
|
||||
self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
|
||||
|
||||
def setup(self):
|
||||
self.ipydir = TemporaryDirectory()
|
||||
self.config_dir = TemporaryDirectory()
|
||||
self.nbdir = TemporaryDirectory()
|
||||
self.home = TemporaryDirectory()
|
||||
self.env = {
|
||||
'HOME': self.home.name,
|
||||
'JUPYTER_CONFIG_DIR': self.config_dir.name,
|
||||
'IPYTHONDIR': self.ipydir.name,
|
||||
}
|
||||
self.dirs.append(self.ipydir)
|
||||
self.dirs.append(self.home)
|
||||
self.dirs.append(self.config_dir)
|
||||
self.dirs.append(self.nbdir)
|
||||
os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir1', u'sub ∂ir 1a')))
|
||||
os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir2', u'sub ∂ir 1b')))
|
||||
|
||||
if self.xunit:
|
||||
self.add_xunit()
|
||||
|
||||
# If a url was specified, use that for the testing.
|
||||
if self.url:
|
||||
try:
|
||||
alive = requests.get(self.url).status_code == 200
|
||||
except:
|
||||
alive = False
|
||||
|
||||
if alive:
|
||||
self.cmd.append("--url=%s" % self.url)
|
||||
else:
|
||||
raise Exception('Could not reach "%s".' % self.url)
|
||||
else:
|
||||
# start the ipython notebook, so we get the port number
|
||||
self.server_port = 0
|
||||
self._init_server()
|
||||
if self.server_port:
|
||||
self.cmd.append('--url=http://localhost:%i%s' % (self.server_port, self.base_url))
|
||||
else:
|
||||
# don't launch tests if the server didn't start
|
||||
self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
|
||||
|
||||
def add_xunit(self):
|
||||
xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
|
||||
self.cmd.append('--xunit=%s' % xunit_file)
|
||||
|
||||
def launch(self, buffer_output):
|
||||
# If the engine is SlimerJS, we need to buffer the output because
|
||||
# SlimerJS does not support exit codes, so CasperJS always returns 0.
|
||||
if self.engine == 'slimerjs' and not buffer_output:
|
||||
return super().launch(capture_output=True)
|
||||
|
||||
else:
|
||||
return super().launch(buffer_output=buffer_output)
|
||||
|
||||
def wait(self, *pargs, **kwargs):
|
||||
"""Wait for the JSController to finish"""
|
||||
ret = super().wait(*pargs, **kwargs)
|
||||
# If this is a SlimerJS controller, check the captured stdout for
|
||||
# errors. Otherwise, just return the return code.
|
||||
if self.engine == 'slimerjs':
|
||||
stdout = bytes_to_str(self.stdout)
|
||||
if ret != 0:
|
||||
# This could still happen e.g. if it's stopped by SIGINT
|
||||
return ret
|
||||
return bool(self.slimer_failure.search(stdout))
|
||||
else:
|
||||
return ret
|
||||
|
||||
def print_extra_info(self):
|
||||
print("Running tests with notebook directory %r" % self.nbdir.name)
|
||||
|
||||
@property
|
||||
def will_run(self):
|
||||
should_run = all(have[a] for a in self.requirements + [self.engine])
|
||||
return should_run
|
||||
|
||||
def _init_server(self):
|
||||
"Start the notebook server in a separate process"
|
||||
self.server_command = command = [sys.executable,
|
||||
'-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,
|
||||
# which run afoul of ipc's maximum path length.
|
||||
if sys.platform.startswith('linux'):
|
||||
command.append('--KernelManager.transport=ipc')
|
||||
self.stream_capturer = c = StreamCapturer()
|
||||
c.start()
|
||||
env = os.environ.copy()
|
||||
env.update(self.env)
|
||||
self.server = subprocess.Popen(command,
|
||||
stdout = c.writefd,
|
||||
stderr = subprocess.STDOUT,
|
||||
cwd=self.nbdir.name,
|
||||
env=env,
|
||||
)
|
||||
with patch.dict('os.environ', {'HOME': self.home.name}):
|
||||
runtime_dir = jupyter_runtime_dir()
|
||||
self.server_info_file = os.path.join(runtime_dir,
|
||||
'nbserver-%i.json' % self.server.pid
|
||||
)
|
||||
self._wait_for_server()
|
||||
|
||||
def _wait_for_server(self):
|
||||
"""Wait 30 seconds for the notebook server to start"""
|
||||
for i in range(300):
|
||||
if self.server.poll() is not None:
|
||||
return self._failed_to_start()
|
||||
if os.path.exists(self.server_info_file):
|
||||
try:
|
||||
self._load_server_info()
|
||||
except ValueError:
|
||||
# If the server is halfway through writing the file, we may
|
||||
# get invalid JSON; it should be ready next iteration.
|
||||
pass
|
||||
else:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
print("Notebook server-info file never arrived: %s" % self.server_info_file,
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
def _failed_to_start(self):
|
||||
"""Notebook server exited prematurely"""
|
||||
captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
|
||||
print("Notebook failed to start: ", file=sys.stderr)
|
||||
print(self.server_command)
|
||||
print(captured, file=sys.stderr)
|
||||
|
||||
def _load_server_info(self):
|
||||
"""Notebook server started, load connection info from JSON"""
|
||||
with open(self.server_info_file) as f:
|
||||
info = json.load(f)
|
||||
self.server_port = info['port']
|
||||
|
||||
def cleanup(self):
|
||||
if hasattr(self, 'server'):
|
||||
try:
|
||||
self.server.terminate()
|
||||
except OSError:
|
||||
# already dead
|
||||
pass
|
||||
# wait 10s for the server to shutdown
|
||||
try:
|
||||
popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
|
||||
except TimeoutExpired:
|
||||
# server didn't terminate, kill it
|
||||
try:
|
||||
print("Failed to terminate notebook server, killing it.",
|
||||
file=sys.stderr
|
||||
)
|
||||
self.server.kill()
|
||||
except OSError:
|
||||
# already dead
|
||||
pass
|
||||
# wait another 10s
|
||||
try:
|
||||
popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
|
||||
except TimeoutExpired:
|
||||
print("Notebook server still running (%s)" % self.server_info_file,
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
self.stream_capturer.halt()
|
||||
TestController.cleanup(self)
|
||||
|
||||
|
||||
def prepare_controllers(options):
|
||||
"""Returns two lists of TestController instances, those to run, and those
|
||||
not to run."""
|
||||
testgroups = options.testgroups
|
||||
if not testgroups:
|
||||
testgroups = all_js_groups()
|
||||
|
||||
engine = 'slimerjs' if options.slimerjs else 'phantomjs'
|
||||
c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in testgroups]
|
||||
|
||||
controllers = c_js
|
||||
to_run = [c for c in controllers if c.will_run]
|
||||
not_run = [c for c in controllers if not c.will_run]
|
||||
return to_run, not_run
|
||||
|
||||
def do_run(controller, buffer_output=True):
|
||||
"""Setup and run a test controller.
|
||||
|
||||
If buffer_output is True, no output is displayed, to avoid it appearing
|
||||
interleaved. In this case, the caller is responsible for displaying test
|
||||
output on failure.
|
||||
|
||||
Returns
|
||||
-------
|
||||
controller : TestController
|
||||
The same controller as passed in, as a convenience for using map() type
|
||||
APIs.
|
||||
exitcode : int
|
||||
The exit code of the test subprocess. Non-zero indicates failure.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
controller.setup()
|
||||
if not buffer_output:
|
||||
controller.print_extra_info()
|
||||
controller.launch(buffer_output=buffer_output)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return controller, 1 # signal failure
|
||||
|
||||
exitcode = controller.wait()
|
||||
return controller, exitcode
|
||||
|
||||
except KeyboardInterrupt:
|
||||
return controller, -signal.SIGINT
|
||||
finally:
|
||||
controller.cleanup()
|
||||
|
||||
def report():
|
||||
"""Return a string with a summary report of test-related variables."""
|
||||
inf = get_sys_info()
|
||||
out = []
|
||||
def _add(name, value):
|
||||
out.append((name, value))
|
||||
|
||||
_add('Python version', inf['sys_version'].replace('\n',''))
|
||||
_add('sys.executable', inf['sys_executable'])
|
||||
_add('Platform', inf['platform'])
|
||||
|
||||
width = max(len(n) for (n,v) in out)
|
||||
out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
|
||||
|
||||
avail = []
|
||||
not_avail = []
|
||||
|
||||
for k, is_avail in have.items():
|
||||
if is_avail:
|
||||
avail.append(k)
|
||||
else:
|
||||
not_avail.append(k)
|
||||
|
||||
if avail:
|
||||
out.append('\nTools and libraries available at test time:\n')
|
||||
avail.sort()
|
||||
out.append(' ' + ' '.join(avail)+'\n')
|
||||
|
||||
if not_avail:
|
||||
out.append('\nTools and libraries NOT available at test time:\n')
|
||||
not_avail.sort()
|
||||
out.append(' ' + ' '.join(not_avail)+'\n')
|
||||
|
||||
return ''.join(out)
|
||||
|
||||
def run_jstestall(options):
|
||||
"""Run the entire Javascript test suite.
|
||||
|
||||
This function constructs TestControllers and runs them in subprocesses.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
All parameters are passed as attributes of the options object.
|
||||
|
||||
testgroups : list of str
|
||||
Run only these sections of the test suite. If empty, run all the available
|
||||
sections.
|
||||
|
||||
fast : int or None
|
||||
Run the test suite in parallel, using n simultaneous processes. If None
|
||||
is passed, one process is used per CPU core. Default 1 (i.e. sequential)
|
||||
|
||||
inc_slow : bool
|
||||
Include slow tests. By default, these tests aren't run.
|
||||
|
||||
slimerjs : bool
|
||||
Use slimerjs if it's installed instead of phantomjs for casperjs tests.
|
||||
|
||||
url : unicode
|
||||
Address:port to use when running the JS tests.
|
||||
|
||||
xunit : bool
|
||||
Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
|
||||
|
||||
extra_args : list
|
||||
Extra arguments to pass to the test subprocesses, e.g. '-v'
|
||||
"""
|
||||
to_run, not_run = prepare_controllers(options)
|
||||
|
||||
def justify(ltext, rtext, width=70, fill='-'):
|
||||
ltext += ' '
|
||||
rtext = (' ' + rtext).rjust(width - len(ltext), fill)
|
||||
return ltext + rtext
|
||||
|
||||
# Run all test runners, tracking execution time
|
||||
failed = []
|
||||
t_start = time.time()
|
||||
|
||||
print()
|
||||
if options.fast == 1:
|
||||
# This actually means sequential, i.e. with 1 job
|
||||
for controller in to_run:
|
||||
print('Test group:', controller.section)
|
||||
sys.stdout.flush() # Show in correct order when output is piped
|
||||
controller, res = do_run(controller, buffer_output=False)
|
||||
if res:
|
||||
failed.append(controller)
|
||||
if res == -signal.SIGINT:
|
||||
print("Interrupted")
|
||||
break
|
||||
print()
|
||||
|
||||
else:
|
||||
# Run tests concurrently
|
||||
try:
|
||||
pool = multiprocessing.pool.ThreadPool(options.fast)
|
||||
for (controller, res) in pool.imap_unordered(do_run, to_run):
|
||||
res_string = 'OK' if res == 0 else 'FAILED'
|
||||
print(justify('Test group: ' + controller.section, res_string))
|
||||
if res:
|
||||
controller.print_extra_info()
|
||||
print(bytes_to_str(controller.stdout))
|
||||
failed.append(controller)
|
||||
if res == -signal.SIGINT:
|
||||
print("Interrupted")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
for controller in not_run:
|
||||
print(justify('Test group: ' + controller.section, 'NOT RUN'))
|
||||
|
||||
t_end = time.time()
|
||||
t_tests = t_end - t_start
|
||||
nrunners = len(to_run)
|
||||
nfail = len(failed)
|
||||
# summarize results
|
||||
print('_'*70)
|
||||
print('Test suite completed for system with the following information:')
|
||||
print(report())
|
||||
took = "Took %.3fs." % t_tests
|
||||
print('Status: ', end='')
|
||||
if not failed:
|
||||
print('OK (%d test groups).' % nrunners, took)
|
||||
else:
|
||||
# If anything went wrong, point out what command to rerun manually to
|
||||
# see the actual errors and individual summary
|
||||
failed_sections = [c.section for c in failed]
|
||||
print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
|
||||
nrunners, ', '.join(failed_sections)), took)
|
||||
print()
|
||||
print('You may wish to rerun these, with:')
|
||||
print(' python -m notebook.jstest', *failed_sections)
|
||||
print()
|
||||
|
||||
if failed:
|
||||
# Ensure that our exit code indicates failure
|
||||
sys.exit(1)
|
||||
|
||||
argparser = argparse.ArgumentParser(description='Run Jupyter Notebook Javascript tests')
|
||||
argparser.add_argument('testgroups', nargs='*',
|
||||
help='Run specified groups of tests. If omitted, run '
|
||||
'all tests.')
|
||||
argparser.add_argument('--slimerjs', action='store_true',
|
||||
help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
|
||||
argparser.add_argument('--url', help="URL to use for the JS tests.")
|
||||
argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
|
||||
help='Run test sections in parallel. This starts as many '
|
||||
'processes as you have cores, or you can specify a number.')
|
||||
argparser.add_argument('--xunit', action='store_true',
|
||||
help='Produce Xunit XML results')
|
||||
argparser.add_argument('--subproc-streams', default='capture',
|
||||
help="What to do with stdout/stderr from subprocesses. "
|
||||
"'capture' (default), 'show' and 'discard' are the options.")
|
||||
|
||||
def default_options():
|
||||
"""Get an argparse Namespace object with the default arguments, to pass to
|
||||
:func:`run_iptestall`.
|
||||
"""
|
||||
options = argparser.parse_args([])
|
||||
options.extra_args = []
|
||||
return options
|
||||
|
||||
def main():
|
||||
try:
|
||||
ix = sys.argv.index('--')
|
||||
except ValueError:
|
||||
to_parse = sys.argv[1:]
|
||||
extra_args = []
|
||||
else:
|
||||
to_parse = sys.argv[1:ix]
|
||||
extra_args = sys.argv[ix+1:]
|
||||
|
||||
options = argparser.parse_args(to_parse)
|
||||
options.extra_args = extra_args
|
||||
|
||||
run_jstestall(options)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,28 +0,0 @@
|
||||
from tornado import web
|
||||
from ..base.handlers import IPythonHandler
|
||||
from ..services.kernelspecs.handlers import kernel_name_regex
|
||||
|
||||
class KernelSpecResourceHandler(web.StaticFileHandler, IPythonHandler):
|
||||
SUPPORTED_METHODS = ('GET', 'HEAD')
|
||||
|
||||
def initialize(self):
|
||||
web.StaticFileHandler.initialize(self, path='')
|
||||
|
||||
@web.authenticated
|
||||
def get(self, kernel_name, path, include_body=True):
|
||||
ksm = self.kernel_spec_manager
|
||||
try:
|
||||
self.root = ksm.get_kernel_spec(kernel_name).resource_dir
|
||||
except KeyError as e:
|
||||
raise web.HTTPError(404,
|
||||
u'Kernel spec %s not found' % kernel_name) from e
|
||||
self.log.debug("Serving kernel resource from: %s", self.root)
|
||||
return web.StaticFileHandler.get(self, path, include_body=include_body)
|
||||
|
||||
@web.authenticated
|
||||
def head(self, kernel_name, path):
|
||||
return self.get(kernel_name, path, include_body=False)
|
||||
|
||||
default_handlers = [
|
||||
(r"/kernelspecs/%s/(?P<path>.*)" % kernel_name_regex, KernelSpecResourceHandler),
|
||||
]
|
||||
@ -1,56 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (c) Jupyter Development Team
|
||||
#
|
||||
# Distributed under the terms of the BSD License. The full license is in
|
||||
# the file LICENSE, distributed as part of this software.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import json
|
||||
from tornado.log import access_log
|
||||
from .prometheus.log_functions import prometheus_log_method
|
||||
|
||||
|
||||
def log_request(handler, log=access_log, log_json=False):
|
||||
"""log a bit more information about each request than tornado's default
|
||||
|
||||
- move static file get success to debug-level (reduces noise)
|
||||
- get proxied IP instead of proxy IP
|
||||
- log referer for redirect and failed requests
|
||||
- log user-agent for failed requests
|
||||
"""
|
||||
status = handler.get_status()
|
||||
request = handler.request
|
||||
if status < 300 or status == 304:
|
||||
# Successes (or 304 FOUND) are debug-level
|
||||
log_method = log.debug
|
||||
elif status < 400:
|
||||
log_method = log.info
|
||||
elif status < 500:
|
||||
log_method = log.warning
|
||||
else:
|
||||
log_method = log.error
|
||||
|
||||
request_time = 1000.0 * request.request_time()
|
||||
ns = dict(
|
||||
status=status,
|
||||
method=request.method,
|
||||
ip=request.remote_ip,
|
||||
uri=request.uri,
|
||||
request_time=float('%.2f' % request_time),
|
||||
)
|
||||
msg = "{status} {method} {uri} ({ip}) {request_time:f}ms"
|
||||
if status >= 400:
|
||||
# log bad referers
|
||||
ns['referer'] = request.headers.get('Referer', 'None')
|
||||
msg = msg + ' referer={referer}'
|
||||
if status >= 500 and status != 502:
|
||||
# log all headers if it caused an error
|
||||
if log_json:
|
||||
log_method("", extra=dict(props=dict(request.headers)))
|
||||
else:
|
||||
log_method(json.dumps(dict(request.headers), indent=2))
|
||||
if log_json:
|
||||
log_method("", extra=dict(props=ns))
|
||||
else:
|
||||
log_method(msg.format(**ns))
|
||||
prometheus_log_method(handler)
|
||||
@ -1,200 +0,0 @@
|
||||
"""Tornado handlers for nbconvert."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import io
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
from tornado import gen, web, escape
|
||||
from tornado.log import app_log
|
||||
|
||||
from ..base.handlers import (
|
||||
IPythonHandler, FilesRedirectHandler,
|
||||
path_regex,
|
||||
)
|
||||
from ..utils import maybe_future
|
||||
from nbformat import from_dict
|
||||
|
||||
from ipython_genutils.py3compat import cast_bytes
|
||||
from ipython_genutils import text
|
||||
|
||||
def find_resource_files(output_files_dir):
|
||||
files = []
|
||||
for dirpath, dirnames, filenames in os.walk(output_files_dir):
|
||||
files.extend([os.path.join(dirpath, f) for f in filenames])
|
||||
return files
|
||||
|
||||
def respond_zip(handler, name, output, resources):
|
||||
"""Zip up the output and resource files and respond with the zip file.
|
||||
|
||||
Returns True if it has served a zip file, False if there are no resource
|
||||
files, in which case we serve the plain output file.
|
||||
"""
|
||||
# Check if we have resource files we need to zip
|
||||
output_files = resources.get('outputs', None)
|
||||
if not output_files:
|
||||
return False
|
||||
|
||||
# Headers
|
||||
zip_filename = os.path.splitext(name)[0] + '.zip'
|
||||
handler.set_attachment_header(zip_filename)
|
||||
handler.set_header('Content-Type', 'application/zip')
|
||||
handler.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
|
||||
# Prepare the zip file
|
||||
buffer = io.BytesIO()
|
||||
zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
|
||||
output_filename = os.path.splitext(name)[0] + resources['output_extension']
|
||||
zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
|
||||
for filename, data in output_files.items():
|
||||
zipf.writestr(os.path.basename(filename), data)
|
||||
zipf.close()
|
||||
|
||||
handler.finish(buffer.getvalue())
|
||||
return True
|
||||
|
||||
def get_exporter(format, **kwargs):
|
||||
"""get an exporter, raising appropriate errors"""
|
||||
# if this fails, will raise 500
|
||||
try:
|
||||
from nbconvert.exporters.base import get_exporter
|
||||
except ImportError as e:
|
||||
raise web.HTTPError(500, "Could not import nbconvert: %s" % e) from e
|
||||
|
||||
try:
|
||||
Exporter = get_exporter(format)
|
||||
except KeyError as e:
|
||||
# should this be 400?
|
||||
raise web.HTTPError(404, u"No exporter for format: %s" % format) from e
|
||||
|
||||
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) from e
|
||||
|
||||
class NbconvertFileHandler(IPythonHandler):
|
||||
|
||||
SUPPORTED_METHODS = ('GET',)
|
||||
|
||||
@property
|
||||
def content_security_policy(self):
|
||||
# In case we're serving HTML/SVG, confine any Javascript to a unique
|
||||
# origin so it can't interact with the notebook server.
|
||||
return super().content_security_policy + "; sandbox allow-scripts"
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def get(self, format, path):
|
||||
|
||||
exporter = get_exporter(format, config=self.config, log=self.log)
|
||||
|
||||
path = path.strip('/')
|
||||
# If the notebook relates to a real file (default contents manager),
|
||||
# give its path to nbconvert.
|
||||
if hasattr(self.contents_manager, '_get_os_path'):
|
||||
os_path = self.contents_manager._get_os_path(path)
|
||||
ext_resources_dir, basename = os.path.split(os_path)
|
||||
else:
|
||||
ext_resources_dir = None
|
||||
|
||||
model = yield maybe_future(self.contents_manager.get(path=path))
|
||||
name = model['name']
|
||||
if model['type'] != 'notebook':
|
||||
# not a notebook, redirect to files
|
||||
return FilesRedirectHandler.redirect_to_files(self, path)
|
||||
|
||||
nb = model['content']
|
||||
|
||||
self.set_header('Last-Modified', model['last_modified'])
|
||||
|
||||
# create resources dictionary
|
||||
mod_date = model['last_modified'].strftime(text.date_format)
|
||||
nb_title = os.path.splitext(name)[0]
|
||||
|
||||
resource_dict = {
|
||||
"metadata": {
|
||||
"name": nb_title,
|
||||
"modified_date": mod_date
|
||||
},
|
||||
"config_dir": self.application.settings['config_dir']
|
||||
}
|
||||
|
||||
if ext_resources_dir:
|
||||
resource_dict['metadata']['path'] = ext_resources_dir
|
||||
|
||||
try:
|
||||
output, resources = exporter.from_notebook_node(
|
||||
nb,
|
||||
resources=resource_dict
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.exception("nbconvert failed: %s", e)
|
||||
raise web.HTTPError(500, "nbconvert failed: %s" % e) from e
|
||||
|
||||
if respond_zip(self, name, output, resources):
|
||||
return
|
||||
|
||||
# Force download if requested
|
||||
if self.get_argument('download', 'false').lower() == 'true':
|
||||
filename = os.path.splitext(name)[0] + resources['output_extension']
|
||||
self.set_attachment_header(filename)
|
||||
|
||||
# MIME type
|
||||
if exporter.output_mimetype:
|
||||
self.set_header('Content-Type',
|
||||
'%s; charset=utf-8' % exporter.output_mimetype)
|
||||
|
||||
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
self.finish(output)
|
||||
|
||||
class NbconvertPostHandler(IPythonHandler):
|
||||
SUPPORTED_METHODS = ('POST',)
|
||||
|
||||
@property
|
||||
def content_security_policy(self):
|
||||
# In case we're serving HTML/SVG, confine any Javascript to a unique
|
||||
# origin so it can't interact with the notebook server.
|
||||
return super().content_security_policy + "; sandbox allow-scripts"
|
||||
|
||||
@web.authenticated
|
||||
def post(self, format):
|
||||
exporter = get_exporter(format, config=self.config)
|
||||
|
||||
model = self.get_json_body()
|
||||
name = model.get('name', 'notebook.ipynb')
|
||||
nbnode = from_dict(model['content'])
|
||||
|
||||
try:
|
||||
output, resources = exporter.from_notebook_node(nbnode, resources={
|
||||
"metadata": {"name": name[:name.rfind('.')],},
|
||||
"config_dir": self.application.settings['config_dir'],
|
||||
})
|
||||
except Exception as e:
|
||||
raise web.HTTPError(500, "nbconvert failed: %s" % e) from e
|
||||
|
||||
if respond_zip(self, name, output, resources):
|
||||
return
|
||||
|
||||
# MIME type
|
||||
if exporter.output_mimetype:
|
||||
self.set_header('Content-Type',
|
||||
'%s; charset=utf-8' % exporter.output_mimetype)
|
||||
|
||||
self.finish(output)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# URL to handler mappings
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
_format_regex = r"(?P<format>\w+)"
|
||||
|
||||
|
||||
default_handlers = [
|
||||
(r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
|
||||
(r"/nbconvert/%s%s" % (_format_regex, path_regex),
|
||||
NbconvertFileHandler),
|
||||
]
|
||||
@ -1,158 +0,0 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from os.path import join as pjoin
|
||||
import shutil
|
||||
|
||||
import requests
|
||||
import pytest
|
||||
from notebook.utils import url_path_join
|
||||
from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
|
||||
from nbformat import write
|
||||
from nbformat.v4 import (
|
||||
new_notebook, new_markdown_cell, new_code_cell, new_output,
|
||||
)
|
||||
|
||||
from ipython_genutils.testing.decorators import onlyif_cmds_exist
|
||||
|
||||
from base64 import encodebytes
|
||||
|
||||
|
||||
def cmd_exists(cmd):
|
||||
"""Check is a command exists."""
|
||||
if shutil.which(cmd) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class NbconvertAPI(object):
|
||||
"""Wrapper for nbconvert API calls."""
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def _req(self, verb, path, body=None, params=None):
|
||||
response = self.request(verb,
|
||||
url_path_join('nbconvert', path),
|
||||
data=body, params=params,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def from_file(self, format, path, name, download=False):
|
||||
return self._req('GET', url_path_join(format, path, name),
|
||||
params={'download':download})
|
||||
|
||||
def from_post(self, format, nbmodel):
|
||||
body = json.dumps(nbmodel)
|
||||
return self._req('POST', format, body)
|
||||
|
||||
def list_formats(self):
|
||||
return self._req('GET', '')
|
||||
|
||||
png_green_pixel = encodebytes(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
|
||||
b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
|
||||
b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82'
|
||||
).decode('ascii')
|
||||
|
||||
class APITest(NotebookTestBase):
|
||||
def setUp(self):
|
||||
nbdir = self.notebook_dir
|
||||
|
||||
if not os.path.isdir(pjoin(nbdir, 'foo')):
|
||||
subdir = pjoin(nbdir, 'foo')
|
||||
|
||||
os.mkdir(subdir)
|
||||
|
||||
# Make sure that we clean this up when we're done.
|
||||
# By using addCleanup this will happen correctly even if we fail
|
||||
# later in setUp.
|
||||
@self.addCleanup
|
||||
def cleanup_dir():
|
||||
shutil.rmtree(subdir, ignore_errors=True)
|
||||
|
||||
nb = new_notebook()
|
||||
|
||||
nb.cells.append(new_markdown_cell(u'Created by test ³'))
|
||||
cc1 = new_code_cell(source=u'print(2*6)')
|
||||
cc1.outputs.append(new_output(output_type="stream", text=u'12'))
|
||||
cc1.outputs.append(new_output(output_type="execute_result",
|
||||
data={'image/png' : png_green_pixel},
|
||||
execution_count=1,
|
||||
))
|
||||
nb.cells.append(cc1)
|
||||
|
||||
with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
|
||||
encoding='utf-8') as f:
|
||||
write(nb, f, version=4)
|
||||
|
||||
self.nbconvert_api = NbconvertAPI(self.request)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not cmd_exists('pandoc'),
|
||||
reason="Pandoc wasn't found. Skipping this test."
|
||||
)
|
||||
def test_from_file(self):
|
||||
r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(u'text/html', r.headers['Content-Type'])
|
||||
self.assertIn(u'Created by test', r.text)
|
||||
self.assertIn(u'print', r.text)
|
||||
|
||||
r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
|
||||
self.assertIn(u'text/x-python', r.headers['Content-Type'])
|
||||
self.assertIn(u'print(2*6)', r.text)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not cmd_exists('pandoc'),
|
||||
reason="Pandoc wasn't found. Skipping this test."
|
||||
)
|
||||
def test_from_file_404(self):
|
||||
with assert_http_error(404):
|
||||
self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not cmd_exists('pandoc'),
|
||||
reason="Pandoc wasn't found. Skipping this test."
|
||||
)
|
||||
def test_from_file_download(self):
|
||||
r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
|
||||
content_disposition = r.headers['Content-Disposition']
|
||||
self.assertIn('attachment', content_disposition)
|
||||
self.assertIn('testnb.py', content_disposition)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not cmd_exists('pandoc'),
|
||||
reason="Pandoc wasn't found. Skipping this test."
|
||||
)
|
||||
def test_from_file_zip(self):
|
||||
r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
|
||||
self.assertIn(u'application/zip', r.headers['Content-Type'])
|
||||
self.assertIn(u'.zip', r.headers['Content-Disposition'])
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not cmd_exists('pandoc'),
|
||||
reason="Pandoc wasn't found. Skipping this test."
|
||||
)
|
||||
def test_from_post(self):
|
||||
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)
|
||||
self.assertIn(u'text/html', r.headers['Content-Type'])
|
||||
self.assertIn(u'Created by test', r.text)
|
||||
self.assertIn(u'print', r.text)
|
||||
|
||||
r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
|
||||
self.assertIn(u'text/x-python', r.headers['Content-Type'])
|
||||
self.assertIn(u'print(2*6)', r.text)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not cmd_exists('pandoc'),
|
||||
reason="Pandoc wasn't found. Skipping this test."
|
||||
)
|
||||
def test_from_post_zip(self):
|
||||
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'])
|
||||
self.assertIn(u'.zip', r.headers['Content-Disposition'])
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,114 +0,0 @@
|
||||
"""Tornado handlers for the live notebook view."""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from collections import namedtuple
|
||||
import os
|
||||
from tornado import (
|
||||
gen, web,
|
||||
)
|
||||
HTTPError = web.HTTPError
|
||||
|
||||
from ..base.handlers import (
|
||||
IPythonHandler, FilesRedirectHandler, path_regex,
|
||||
)
|
||||
from ..utils import (
|
||||
maybe_future, url_escape,
|
||||
)
|
||||
from ..transutils import _
|
||||
|
||||
|
||||
def get_frontend_exporters():
|
||||
from nbconvert.exporters.base import get_export_names, get_exporter
|
||||
|
||||
# name=exporter_name, display=export_from_notebook+extension
|
||||
ExporterInfo = namedtuple('ExporterInfo', ['name', 'display'])
|
||||
|
||||
default_exporters = [
|
||||
ExporterInfo(name='html', display='HTML (.html)'),
|
||||
ExporterInfo(name='latex', display='LaTeX (.tex)'),
|
||||
ExporterInfo(name='markdown', display='Markdown (.md)'),
|
||||
ExporterInfo(name='notebook', display='Notebook (.ipynb)'),
|
||||
ExporterInfo(name='pdf', display='PDF via LaTeX (.pdf)'),
|
||||
ExporterInfo(name='rst', display='reST (.rst)'),
|
||||
ExporterInfo(name='script', display='Script (.txt)'),
|
||||
ExporterInfo(name='slides', display='Reveal.js slides (.slides.html)')
|
||||
]
|
||||
|
||||
frontend_exporters = []
|
||||
for name in get_export_names():
|
||||
exporter_class = get_exporter(name)
|
||||
exporter_instance = exporter_class()
|
||||
ux_name = getattr(exporter_instance, 'export_from_notebook', None)
|
||||
super_uxname = getattr(super(exporter_class, exporter_instance),
|
||||
'export_from_notebook', None)
|
||||
|
||||
# Ensure export_from_notebook is explicitly defined & not inherited
|
||||
if ux_name is not None and ux_name != super_uxname:
|
||||
display = _('{} ({})'.format(ux_name,
|
||||
exporter_instance.file_extension))
|
||||
frontend_exporters.append(ExporterInfo(name, display))
|
||||
|
||||
# Ensure default_exporters are in frontend_exporters if not already
|
||||
# This protects against nbconvert versions lower than 5.5
|
||||
names = set(exporter.name.lower() for exporter in frontend_exporters)
|
||||
for exporter in default_exporters:
|
||||
if exporter.name not in names:
|
||||
frontend_exporters.append(exporter)
|
||||
|
||||
# Protect against nbconvert 5.5.0
|
||||
python_exporter = ExporterInfo(name='python', display='python (.py)')
|
||||
if python_exporter in frontend_exporters:
|
||||
frontend_exporters.remove(python_exporter)
|
||||
|
||||
# Protect against nbconvert 5.4.x
|
||||
template_exporter = ExporterInfo(name='custom', display='custom (.txt)')
|
||||
if template_exporter in frontend_exporters:
|
||||
frontend_exporters.remove(template_exporter)
|
||||
return sorted(frontend_exporters)
|
||||
|
||||
|
||||
class NotebookHandler(IPythonHandler):
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def get(self, path):
|
||||
"""get renders the notebook template if a name is given, or
|
||||
redirects to the '/files/' handler if the name is not given."""
|
||||
path = path.strip('/')
|
||||
cm = self.contents_manager
|
||||
|
||||
# will raise 404 on not found
|
||||
try:
|
||||
model = yield maybe_future(cm.get(path, content=False))
|
||||
except web.HTTPError as e:
|
||||
if e.status_code == 404 and 'files' in path.split('/'):
|
||||
# 404, but '/files/' in URL, let FilesRedirect take care of it
|
||||
return FilesRedirectHandler.redirect_to_files(self, path)
|
||||
else:
|
||||
raise
|
||||
if model['type'] != 'notebook':
|
||||
# not a notebook, redirect to files
|
||||
return FilesRedirectHandler.redirect_to_files(self, path)
|
||||
name = path.rsplit('/', 1)[-1]
|
||||
self.write(self.render_template('notebook.html',
|
||||
notebook_path=path,
|
||||
notebook_name=name,
|
||||
kill_kernel=False,
|
||||
mathjax_url=self.mathjax_url,
|
||||
mathjax_config=self.mathjax_config,
|
||||
get_frontend_exporters=get_frontend_exporters
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# URL to handler mappings
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
default_handlers = [
|
||||
(r"/notebooks%s" % path_regex, NotebookHandler),
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +0,0 @@
|
||||
"""
|
||||
A package containing all the functionality and
|
||||
configuration connected to the prometheus metrics
|
||||
"""
|
||||
@ -1,24 +0,0 @@
|
||||
from ..prometheus.metrics import HTTP_REQUEST_DURATION_SECONDS
|
||||
|
||||
|
||||
def prometheus_log_method(handler):
|
||||
"""
|
||||
Tornado log handler for recording RED metrics.
|
||||
|
||||
We record the following metrics:
|
||||
Rate - the number of requests, per second, your services are serving.
|
||||
Errors - the number of failed requests per second.
|
||||
Duration - The amount of time each request takes expressed as a time interval.
|
||||
|
||||
We use a fully qualified name of the handler as a label,
|
||||
rather than every url path to reduce cardinality.
|
||||
|
||||
This function should be either the value of or called from a function
|
||||
that is the 'log_function' tornado setting. This makes it get called
|
||||
at the end of every request, allowing us to record the metrics we need.
|
||||
"""
|
||||
HTTP_REQUEST_DURATION_SECONDS.labels(
|
||||
method=handler.request.method,
|
||||
handler='{}.{}'.format(handler.__class__.__module__, type(handler).__name__),
|
||||
status_code=handler.get_status()
|
||||
).observe(handler.request.request_time())
|
||||
@ -1,27 +0,0 @@
|
||||
"""
|
||||
Prometheus metrics exported by Jupyter Notebook Server
|
||||
|
||||
Read https://prometheus.io/docs/practices/naming/ for naming
|
||||
conventions for metrics & labels.
|
||||
"""
|
||||
|
||||
|
||||
from prometheus_client import Histogram, Gauge
|
||||
|
||||
|
||||
HTTP_REQUEST_DURATION_SECONDS = Histogram(
|
||||
'http_request_duration_seconds',
|
||||
'duration in seconds for all HTTP requests',
|
||||
['method', 'handler', 'status_code'],
|
||||
)
|
||||
|
||||
TERMINAL_CURRENTLY_RUNNING_TOTAL = Gauge(
|
||||
'terminal_currently_running_total',
|
||||
'counter for how many terminals are running',
|
||||
)
|
||||
|
||||
KERNEL_CURRENTLY_RUNNING_TOTAL = Gauge(
|
||||
'kernel_currently_running_total',
|
||||
'counter for how many kernels are running labeled by type',
|
||||
['type']
|
||||
)
|
||||
@ -1,331 +0,0 @@
|
||||
"""Utilities for installing server extensions for the notebook"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
from jupyter_core.paths import jupyter_config_path
|
||||
from ._version import __version__
|
||||
from .config_manager import BaseJSONConfigManager
|
||||
from .extensions import (
|
||||
BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED, GREEN_OK, RED_X
|
||||
)
|
||||
from traitlets import Bool
|
||||
from traitlets.utils.importstring import import_item
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
version = getattr(mod, '__version__', '')
|
||||
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, version, GREEN_OK))
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Applications
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
flags = {}
|
||||
flags.update(BaseExtensionApp.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(BaseExtensionApp):
|
||||
"""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(BaseExtensionApp):
|
||||
"""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(BaseExtensionApp):
|
||||
"""Root level server extension app"""
|
||||
name = "jupyter serverextension"
|
||||
version = __version__
|
||||
description = "Work with Jupyter server extensions"
|
||||
examples = _examples
|
||||
|
||||
subcommands = dict(
|
||||
enable=(EnableServerExtensionApp, "Enable a server extension"),
|
||||
disable=(DisableServerExtensionApp, "Disable a server extension"),
|
||||
list=(ListServerExtensionsApp, "List server extensions")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Perform the App's actions as configured"""
|
||||
super().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()
|
||||
@ -1,857 +0,0 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: Jupyter Notebook API
|
||||
description: Notebook API
|
||||
version: "5"
|
||||
contact:
|
||||
name: Jupyter Project
|
||||
url: https://jupyter.org
|
||||
# will be prefixed to all paths
|
||||
basePath: /
|
||||
produces:
|
||||
- application/json
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
kernel:
|
||||
name: kernel_id
|
||||
required: true
|
||||
in: path
|
||||
description: kernel uuid
|
||||
type: string
|
||||
format: uuid
|
||||
session:
|
||||
name: session
|
||||
required: true
|
||||
in: path
|
||||
description: session uuid
|
||||
type: string
|
||||
format: uuid
|
||||
path:
|
||||
name: path
|
||||
required: true
|
||||
in: path
|
||||
description: file path
|
||||
type: string
|
||||
checkpoint_id:
|
||||
name: checkpoint_id
|
||||
required: true
|
||||
in: path
|
||||
description: Checkpoint id for a file
|
||||
type: string
|
||||
section_name:
|
||||
name: section_name
|
||||
required: true
|
||||
in: path
|
||||
description: Name of config section
|
||||
type: string
|
||||
terminal_id:
|
||||
name: terminal_id
|
||||
required: true
|
||||
in: path
|
||||
description: ID of terminal session
|
||||
type: string
|
||||
|
||||
paths:
|
||||
|
||||
|
||||
/api/contents/{path}:
|
||||
parameters:
|
||||
- $ref: '#/parameters/path'
|
||||
get:
|
||||
summary: Get contents of file or directory
|
||||
description: "A client can optionally specify a type and/or format argument via URL parameter. When given, the Contents service shall return a model in the requested type and/or format. If the request cannot be satisfied, e.g. type=text is requested, but the file is binary, then the request shall fail with 400 and have a JSON response containing a 'reason' field, with the value 'bad format' or 'bad type', depending on what was requested."
|
||||
tags:
|
||||
- contents
|
||||
parameters:
|
||||
- name: type
|
||||
in: query
|
||||
description: File type ('file', 'directory')
|
||||
type: string
|
||||
enum:
|
||||
- file
|
||||
- directory
|
||||
- name: format
|
||||
in: query
|
||||
description: "How file content should be returned ('text', 'base64')"
|
||||
type: string
|
||||
enum:
|
||||
- text
|
||||
- base64
|
||||
- name: content
|
||||
in: query
|
||||
description: "Return content (0 for no content, 1 for return content)"
|
||||
type: integer
|
||||
responses:
|
||||
404:
|
||||
description: No item found
|
||||
400:
|
||||
description: Bad request
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error condition
|
||||
reason:
|
||||
type: string
|
||||
description: Explanation of error reason
|
||||
200:
|
||||
description: Contents of file or directory
|
||||
headers:
|
||||
Last-Modified:
|
||||
description: Last modified date for file
|
||||
type: string
|
||||
format: dateTime
|
||||
schema:
|
||||
$ref: '#/definitions/Contents'
|
||||
500:
|
||||
description: Model key error
|
||||
post:
|
||||
summary: Create a new file in the specified path
|
||||
description: "A POST to /api/contents/path creates a New untitled, empty file or directory. A POST to /api/contents/path with body {'copy_from': '/path/to/OtherNotebook.ipynb'} creates a new copy of OtherNotebook in path."
|
||||
tags:
|
||||
- contents
|
||||
parameters:
|
||||
- name: model
|
||||
in: body
|
||||
description: Path of file to copy
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
copy_from:
|
||||
type: string
|
||||
ext:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
responses:
|
||||
201:
|
||||
description: File created
|
||||
headers:
|
||||
Location:
|
||||
description: URL for the new file
|
||||
type: string
|
||||
format: url
|
||||
schema:
|
||||
$ref: '#/definitions/Contents'
|
||||
404:
|
||||
description: No item found
|
||||
400:
|
||||
description: Bad request
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error condition
|
||||
reason:
|
||||
type: string
|
||||
description: Explanation of error reason
|
||||
patch:
|
||||
summary: Rename a file or directory without re-uploading content
|
||||
tags:
|
||||
- contents
|
||||
parameters:
|
||||
- name: path
|
||||
in: body
|
||||
required: true
|
||||
description: New path for file or directory.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
format: path
|
||||
description: New path for file or directory
|
||||
responses:
|
||||
200:
|
||||
description: Path updated
|
||||
headers:
|
||||
Location:
|
||||
description: Updated URL for the file or directory
|
||||
type: string
|
||||
format: url
|
||||
schema:
|
||||
$ref: '#/definitions/Contents'
|
||||
400:
|
||||
description: No data provided
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error condition
|
||||
reason:
|
||||
type: string
|
||||
description: Explanation of error reason
|
||||
put:
|
||||
summary: Save or upload file.
|
||||
description: "Saves the file in the location specified by name and path. PUT is very similar to POST, but the requester specifies the name, whereas with POST, the server picks the name."
|
||||
tags:
|
||||
- contents
|
||||
parameters:
|
||||
- name: model
|
||||
in: body
|
||||
description: New path for file or directory
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The new filename if changed
|
||||
path:
|
||||
type: string
|
||||
description: New path for file or directory
|
||||
type:
|
||||
type: string
|
||||
description: Path dtype ('notebook', 'file', 'directory')
|
||||
format:
|
||||
type: string
|
||||
description: File format ('json', 'text', 'base64')
|
||||
content:
|
||||
type: string
|
||||
description: The actual body of the document excluding directory type
|
||||
responses:
|
||||
200:
|
||||
description: File saved
|
||||
headers:
|
||||
Location:
|
||||
description: Updated URL for the file or directory
|
||||
type: string
|
||||
format: url
|
||||
schema:
|
||||
$ref: '#/definitions/Contents'
|
||||
201:
|
||||
description: Path created
|
||||
headers:
|
||||
Location:
|
||||
description: URL for the file or directory
|
||||
type: string
|
||||
format: url
|
||||
schema:
|
||||
$ref: '#/definitions/Contents'
|
||||
400:
|
||||
description: No data provided
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error condition
|
||||
reason:
|
||||
type: string
|
||||
description: Explanation of error reason
|
||||
delete:
|
||||
summary: Delete a file in the given path
|
||||
tags:
|
||||
- contents
|
||||
responses:
|
||||
204:
|
||||
description: File deleted
|
||||
headers:
|
||||
Location:
|
||||
description: URL for the removed file
|
||||
type: string
|
||||
format: url
|
||||
/api/contents/{path}/checkpoints:
|
||||
parameters:
|
||||
- $ref: '#/parameters/path'
|
||||
get:
|
||||
summary: Get a list of checkpoints for a file
|
||||
description: List checkpoints for a given file. There will typically be zero or one results.
|
||||
tags:
|
||||
- contents
|
||||
responses:
|
||||
404:
|
||||
description: No item found
|
||||
400:
|
||||
description: Bad request
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error condition
|
||||
reason:
|
||||
type: string
|
||||
description: Explanation of error reason
|
||||
200:
|
||||
description: List of checkpoints for a file
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Checkpoints'
|
||||
500:
|
||||
description: Model key error
|
||||
post:
|
||||
summary: Create a new checkpoint for a file
|
||||
description: "Create a new checkpoint with the current state of a file. With the default FileContentsManager, only one checkpoint is supported, so creating new checkpoints clobbers existing ones."
|
||||
tags:
|
||||
- contents
|
||||
responses:
|
||||
201:
|
||||
description: Checkpoint created
|
||||
headers:
|
||||
Location:
|
||||
description: URL for the checkpoint
|
||||
type: string
|
||||
format: url
|
||||
schema:
|
||||
$ref: '#/definitions/Checkpoints'
|
||||
404:
|
||||
description: No item found
|
||||
400:
|
||||
description: Bad request
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error condition
|
||||
reason:
|
||||
type: string
|
||||
description: Explanation of error reason
|
||||
/api/contents/{path}/checkpoints/{checkpoint_id}:
|
||||
post:
|
||||
summary: Restore a file to a particular checkpointed state
|
||||
parameters:
|
||||
- $ref: "#/parameters/path"
|
||||
- $ref: "#/parameters/checkpoint_id"
|
||||
tags:
|
||||
- contents
|
||||
responses:
|
||||
204:
|
||||
description: Checkpoint restored
|
||||
400:
|
||||
description: Bad request
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error condition
|
||||
reason:
|
||||
type: string
|
||||
description: Explanation of error reason
|
||||
delete:
|
||||
summary: Delete a checkpoint
|
||||
parameters:
|
||||
- $ref: "#/parameters/path"
|
||||
- $ref: "#/parameters/checkpoint_id"
|
||||
tags:
|
||||
- contents
|
||||
responses:
|
||||
204:
|
||||
description: Checkpoint deleted
|
||||
/api/sessions/{session}:
|
||||
parameters:
|
||||
- $ref: '#/parameters/session'
|
||||
get:
|
||||
summary: Get session
|
||||
tags:
|
||||
- sessions
|
||||
responses:
|
||||
200:
|
||||
description: Session
|
||||
schema:
|
||||
$ref: '#/definitions/Session'
|
||||
patch:
|
||||
summary: "This can be used to rename the session."
|
||||
tags:
|
||||
- sessions
|
||||
parameters:
|
||||
- name: model
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Session'
|
||||
responses:
|
||||
200:
|
||||
description: Session
|
||||
schema:
|
||||
$ref: '#/definitions/Session'
|
||||
400:
|
||||
description: No data provided
|
||||
delete:
|
||||
summary: Delete a session
|
||||
tags:
|
||||
- sessions
|
||||
responses:
|
||||
204:
|
||||
description: Session (and kernel) were deleted
|
||||
410:
|
||||
description: "Kernel was deleted before the session, and the session was *not* deleted (TODO - check to make sure session wasn't deleted)"
|
||||
/api/sessions:
|
||||
get:
|
||||
summary: List available sessions
|
||||
tags:
|
||||
- sessions
|
||||
responses:
|
||||
200:
|
||||
description: List of current sessions
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Session'
|
||||
post:
|
||||
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:
|
||||
$ref: '#/definitions/Session'
|
||||
responses:
|
||||
201:
|
||||
description: Session created or returned
|
||||
schema:
|
||||
$ref: '#/definitions/Session'
|
||||
headers:
|
||||
Location:
|
||||
description: URL for session commands
|
||||
type: string
|
||||
format: url
|
||||
501:
|
||||
description: Session not available
|
||||
schema:
|
||||
type: object
|
||||
description: error message
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
short_message:
|
||||
type: string
|
||||
|
||||
/api/kernels:
|
||||
get:
|
||||
summary: List the JSON data for all kernels that are currently running
|
||||
tags:
|
||||
- kernels
|
||||
responses:
|
||||
200:
|
||||
description: List of currently-running kernel uuids
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Kernel'
|
||||
post:
|
||||
summary: Start a kernel and return the uuid
|
||||
tags:
|
||||
- kernels
|
||||
parameters:
|
||||
- name: name
|
||||
in: body
|
||||
description: Kernel spec name (defaults to default kernel spec for server)
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
responses:
|
||||
201:
|
||||
description: Kernel started
|
||||
schema:
|
||||
$ref: '#/definitions/Kernel'
|
||||
headers:
|
||||
Location:
|
||||
description: Model for started kernel
|
||||
type: string
|
||||
format: url
|
||||
/api/kernels/{kernel_id}:
|
||||
parameters:
|
||||
- $ref: '#/parameters/kernel'
|
||||
get:
|
||||
summary: Get kernel information
|
||||
tags:
|
||||
- kernels
|
||||
responses:
|
||||
200:
|
||||
description: Kernel information
|
||||
schema:
|
||||
$ref: '#/definitions/Kernel'
|
||||
delete:
|
||||
summary: Kill a kernel and delete the kernel id
|
||||
tags:
|
||||
- kernels
|
||||
responses:
|
||||
204:
|
||||
description: Kernel deleted
|
||||
/api/kernels/{kernel_id}/interrupt:
|
||||
parameters:
|
||||
- $ref: '#/parameters/kernel'
|
||||
post:
|
||||
summary: Interrupt a kernel
|
||||
tags:
|
||||
- kernels
|
||||
responses:
|
||||
204:
|
||||
description: Kernel interrupted
|
||||
/api/kernels/{kernel_id}/restart:
|
||||
parameters:
|
||||
- $ref: '#/parameters/kernel'
|
||||
post:
|
||||
summary: Restart a kernel
|
||||
tags:
|
||||
- kernels
|
||||
responses:
|
||||
200:
|
||||
description: Kernel interrupted
|
||||
headers:
|
||||
Location:
|
||||
description: URL for kernel commands
|
||||
type: string
|
||||
format: url
|
||||
schema:
|
||||
$ref: '#/definitions/Kernel'
|
||||
|
||||
/api/kernelspecs:
|
||||
get:
|
||||
summary: Get kernel specs
|
||||
tags:
|
||||
- kernelspecs
|
||||
responses:
|
||||
200:
|
||||
description: Kernel specs
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
default:
|
||||
type: string
|
||||
description: Default kernel name
|
||||
kernelspecs:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/KernelSpec'
|
||||
/api/config/{section_name}:
|
||||
get:
|
||||
summary: Get a configuration section by name
|
||||
parameters:
|
||||
- $ref: "#/parameters/section_name"
|
||||
tags:
|
||||
- config
|
||||
responses:
|
||||
200:
|
||||
description: Configuration object
|
||||
schema:
|
||||
type: object
|
||||
patch:
|
||||
summary: Update a configuration section by name
|
||||
tags:
|
||||
- config
|
||||
parameters:
|
||||
- $ref: "#/parameters/section_name"
|
||||
- name: configuration
|
||||
in: body
|
||||
schema:
|
||||
type: object
|
||||
responses:
|
||||
200:
|
||||
description: Configuration object
|
||||
schema:
|
||||
type: object
|
||||
|
||||
/api/terminals:
|
||||
get:
|
||||
summary: Get available terminals
|
||||
tags:
|
||||
- terminals
|
||||
responses:
|
||||
200:
|
||||
description: A list of all available terminal ids.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Terminal'
|
||||
403:
|
||||
description: Forbidden to access
|
||||
404:
|
||||
description: Not found
|
||||
|
||||
post:
|
||||
summary: Create a new terminal
|
||||
tags:
|
||||
- terminals
|
||||
responses:
|
||||
200:
|
||||
description: Succesfully created a new terminal
|
||||
schema:
|
||||
$ref: '#/definitions/Terminal'
|
||||
403:
|
||||
description: Forbidden to access
|
||||
404:
|
||||
description: Not found
|
||||
|
||||
/api/terminals/{terminal_id}:
|
||||
get:
|
||||
summary: Get a terminal session corresponding to an id.
|
||||
tags:
|
||||
- terminals
|
||||
parameters:
|
||||
- $ref: '#/parameters/terminal_id'
|
||||
responses:
|
||||
200:
|
||||
description: Terminal session with given id
|
||||
schema:
|
||||
$ref: '#/definitions/Terminal'
|
||||
403:
|
||||
description: Forbidden to access
|
||||
404:
|
||||
description: Not found
|
||||
|
||||
delete:
|
||||
summary: Delete a terminal session corresponding to an id.
|
||||
tags:
|
||||
- terminals
|
||||
parameters:
|
||||
- $ref: '#/parameters/terminal_id'
|
||||
responses:
|
||||
204:
|
||||
description: Succesfully deleted terminal session
|
||||
403:
|
||||
description: Forbidden to access
|
||||
404:
|
||||
description: Not found
|
||||
|
||||
|
||||
|
||||
|
||||
/api/status:
|
||||
get:
|
||||
summary: Get the current status/activity of the server.
|
||||
tags:
|
||||
- status
|
||||
responses:
|
||||
200:
|
||||
description: The current status of the server
|
||||
schema:
|
||||
$ref: '#/definitions/APIStatus'
|
||||
|
||||
/api/spec.yaml:
|
||||
get:
|
||||
summary: Get the current spec for the notebook server's APIs.
|
||||
tags:
|
||||
- api-spec
|
||||
produces:
|
||||
- text/x-yaml
|
||||
responses:
|
||||
200:
|
||||
description: The current spec for the notebook server's APIs.
|
||||
schema:
|
||||
type: file
|
||||
definitions:
|
||||
APIStatus:
|
||||
description: |
|
||||
Notebook server API status.
|
||||
Added in notebook 5.0.
|
||||
properties:
|
||||
started:
|
||||
type: string
|
||||
description: |
|
||||
ISO8601 timestamp indicating when the notebook server started.
|
||||
last_activity:
|
||||
type: string
|
||||
description: |
|
||||
ISO8601 timestamp indicating the last activity on the server,
|
||||
either on the REST API or kernel activity.
|
||||
connections:
|
||||
type: number
|
||||
description: |
|
||||
The total number of currently open connections to kernels.
|
||||
kernels:
|
||||
type: number
|
||||
description: |
|
||||
The total number of running kernels.
|
||||
KernelSpec:
|
||||
description: Kernel spec (contents of kernel.json)
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Unique name for kernel
|
||||
KernelSpecFile:
|
||||
$ref: '#/definitions/KernelSpecFile'
|
||||
resources:
|
||||
type: object
|
||||
properties:
|
||||
kernel.js:
|
||||
type: string
|
||||
format: filename
|
||||
description: path for kernel.js file
|
||||
kernel.css:
|
||||
type: string
|
||||
format: filename
|
||||
description: path for kernel.css file
|
||||
logo-*:
|
||||
type: string
|
||||
format: filename
|
||||
description: path for logo file. Logo filenames are of the form `logo-widthxheight`
|
||||
KernelSpecFile:
|
||||
description: Kernel spec json file
|
||||
required:
|
||||
- argv
|
||||
- display_name
|
||||
- language
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
description: The programming language which this kernel runs. This will be stored in notebook metadata.
|
||||
argv:
|
||||
type: array
|
||||
description: "A list of command line arguments used to start the kernel. The text `{connection_file}` in any argument will be replaced with the path to the connection file."
|
||||
items:
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
description: "The kernel's name as it should be displayed in the UI. Unlike the kernel name used in the API, this can contain arbitrary unicode characters."
|
||||
codemirror_mode:
|
||||
type: string
|
||||
description: Codemirror mode. Can be a string *or* an valid Codemirror mode object. This defaults to the string from the `language` property.
|
||||
env:
|
||||
type: object
|
||||
description: A dictionary of environment variables to set for the kernel. These will be added to the current environment variables.
|
||||
additionalProperties:
|
||||
type: string
|
||||
help_links:
|
||||
type: array
|
||||
description: Help items to be displayed in the help menu in the notebook UI.
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
- url
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: menu item link text
|
||||
url:
|
||||
type: string
|
||||
format: URL
|
||||
description: menu item link url
|
||||
Kernel:
|
||||
description: Kernel information
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: uuid of kernel
|
||||
name:
|
||||
type: string
|
||||
description: kernel spec name
|
||||
last_activity:
|
||||
type: string
|
||||
description: |
|
||||
ISO 8601 timestamp for the last-seen activity on this kernel.
|
||||
Use this in combination with execution_state == 'idle' to identify
|
||||
which kernels have been idle since a given time.
|
||||
Timestamps will be UTC, indicated 'Z' suffix.
|
||||
Added in notebook server 5.0.
|
||||
connections:
|
||||
type: number
|
||||
description: |
|
||||
The number of active connections to this kernel.
|
||||
execution_state:
|
||||
type: string
|
||||
description: |
|
||||
Current execution state of the kernel (typically 'idle' or 'busy', but may be other values, such as 'starting').
|
||||
Added in notebook server 5.0.
|
||||
Session:
|
||||
description: A session
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
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'
|
||||
Contents:
|
||||
description: "A contents object. The content and format keys may be null if content is not contained. If type is 'file', then the mimetype will be null."
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
- path
|
||||
- writable
|
||||
- created
|
||||
- last_modified
|
||||
- mimetype
|
||||
- format
|
||||
- content
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Name of file or directory, equivalent to the last part of the path"
|
||||
path:
|
||||
type: string
|
||||
description: Full path for file or directory
|
||||
type:
|
||||
type: string
|
||||
description: Type of content
|
||||
enum:
|
||||
- directory
|
||||
- file
|
||||
- notebook
|
||||
writable:
|
||||
type: boolean
|
||||
description: indicates whether the requester has permission to edit the file
|
||||
created:
|
||||
type: string
|
||||
description: Creation timestamp
|
||||
format: dateTime
|
||||
last_modified:
|
||||
type: string
|
||||
description: Last modified timestamp
|
||||
format: dateTime
|
||||
size:
|
||||
type: integer
|
||||
description: "The size of the file or notebook in bytes. If no size is provided, defaults to null."
|
||||
mimetype:
|
||||
type: string
|
||||
description: "The mimetype of a file. If content is not null, and type is 'file', this will contain the mimetype of the file, otherwise this will be null."
|
||||
content:
|
||||
type: string
|
||||
description: "The content, if requested (otherwise null). Will be an array if type is 'directory'"
|
||||
format:
|
||||
type: string
|
||||
description: Format of content (one of null, 'text', 'base64', 'json')
|
||||
Checkpoints:
|
||||
description: A checkpoint object.
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- last_modified
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique id for the checkpoint.
|
||||
last_modified:
|
||||
type: string
|
||||
description: Last modified timestamp
|
||||
format: dateTime
|
||||
Terminal:
|
||||
description: A Terminal object
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: name of terminal
|
||||
last_activity:
|
||||
type: string
|
||||
description: |
|
||||
ISO 8601 timestamp for the last-seen activity on this terminal. Use
|
||||
this to identify which terminals have been inactive since a given time.
|
||||
Timestamps will be UTC, indicated 'Z' suffix.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue