Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

View file

@ -0,0 +1,28 @@
"""The Jupyter HTML Notebook"""
import os
# Packagers: modify this line if you store the notebook static files elsewhere
DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
# Packagers: modify the next line if you store the notebook template files
# elsewhere
# Include both notebook/ and notebook/templates/. This makes it
# possible for users to override a template with a file that inherits from that
# template.
#
# For example, if you want to override a specific block of notebook.html, you
# can create a file called notebook.html that inherits from
# templates/notebook.html, and the latter will resolve correctly to the base
# implementation.
DEFAULT_TEMPLATE_PATH_LIST = [
os.path.dirname(__file__),
os.path.join(os.path.dirname(__file__), "templates"),
]
DEFAULT_NOTEBOOK_PORT = 8888
del os
from .nbextensions import install_nbextension
from ._version import version_info, __version__

View file

@ -0,0 +1,5 @@
from __future__ import absolute_import
if __name__ == '__main__':
from notebook import notebookapp as app
app.launch_new_instance()

View file

@ -0,0 +1,99 @@
# encoding: utf-8
"""
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.
from __future__ import absolute_import
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)

View file

@ -0,0 +1,42 @@
# encoding: utf-8
"""
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')

View file

@ -0,0 +1,13 @@
"""
store the current version info of the notebook.
"""
# Downstream maintainer, when running `python.setup.py jsversion`,
# the version string is propagated to the JavaScript files, do not forget to
# patch the JavaScript files in `.postN` release done by distributions.
# Next beta/alpha/rc release: The version number for beta is X.Y.ZbN **without dots**.
version_info = (6, 1, 5)
__version__ = '.'.join(map(str, version_info[:3])) + ''.join(version_info[3:])

View file

@ -0,0 +1 @@
from .security import passwd

View file

@ -0,0 +1,42 @@
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)

View file

@ -0,0 +1,253 @@
"""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('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'))

View file

@ -0,0 +1,23 @@
"""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)]

View file

@ -0,0 +1,172 @@
"""
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')
'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

View file

@ -0,0 +1,48 @@
"""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)

View file

@ -0,0 +1,27 @@
# coding: utf-8
from ..security import passwd, passwd_check
import nose.tools as nt
def test_passwd_structure():
p = passwd('passphrase')
algorithm, hashed = p.split(':')
nt.assert_equal(algorithm, 'argon2')
nt.assert_true(hashed.startswith('$argon2id$'))
def test_roundtrip():
p = passwd('passphrase')
nt.assert_equal(passwd_check(p, 'passphrase'), True)
def test_bad():
p = passwd('passphrase')
nt.assert_equal(passwd_check(p, p), False)
nt.assert_equal(passwd_check(p, 'a:b:c:d'), False)
nt.assert_equal(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¶ŧ←↓→")

View file

@ -0,0 +1,947 @@
"""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(IPythonHandler, self).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).
"""
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(IPythonHandler, self).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(IPythonHandler, self).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(APIHandler, self).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(APIHandler, self).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(APIHandler, self).get_login_url()
@property
def content_security_policy(self):
csp = '; '.join([
super(APIHandler, self).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(APIHandler, self).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(AuthenticatedFileHandler, self).content_security_policy + \
"; sandbox allow-scripts"
@web.authenticated
def head(self, path):
self.check_xsrf_cookie()
return super(AuthenticatedFileHandler, self).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(AuthenticatedFileHandler, self).get_content_type()
def set_headers(self):
super(AuthenticatedFileHandler, self).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(AuthenticatedFileHandler, self).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(FileFindHandler, self).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(FileFindHandler, self).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
"""
@web.authenticated
def get(self):
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)
]

View file

@ -0,0 +1,300 @@
# coding: utf-8
"""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
from jupyter_client.jsonutil import date_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'))
if sys.version_info < (3, 4):
buffers = [x.tobytes() for x in buffers]
bmsg = json.dumps(msg, default=date_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=date_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(AuthenticatedZMQStreamHandler, self).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)

View file

@ -0,0 +1,7 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from .bundlerextensions import main
if __name__ == '__main__':
main()

View file

@ -0,0 +1,307 @@
# 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(BundlerExtensionApp, self).start()
# The above should have called a subcommand and raised NoStart; if we
# get here, it didn't, so we should self.log.info a message.
subcmds = ", ".join(sorted(self.subcommands))
sys.exit("Please supply at least one subcommand: %s" % subcmds)
main = BundlerExtensionApp.launch_instance
if __name__ == '__main__':
main()

View file

@ -0,0 +1,88 @@
"""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)
]

View file

@ -0,0 +1,47 @@
# 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())

View file

@ -0,0 +1 @@
Used to test globbing.

View file

@ -0,0 +1,6 @@
{
"nbformat_minor": 0,
"cells": [],
"nbformat": 4,
"metadata": {}
}

View file

@ -0,0 +1 @@
Used to test globbing.

View file

@ -0,0 +1,80 @@
"""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):
"""Shoudl 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)

View file

@ -0,0 +1,124 @@
"""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')))

View file

@ -0,0 +1,72 @@
"""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)

View file

@ -0,0 +1,230 @@
"""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))

View file

@ -0,0 +1,59 @@
# 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())

View file

@ -0,0 +1,133 @@
# coding: utf-8
"""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

View file

@ -0,0 +1,29 @@
#encoding: utf-8
"""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),
]

View file

@ -0,0 +1,104 @@
# coding: utf-8
"""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'

View file

@ -0,0 +1,85 @@
"""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(FilesHandler, self).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 = []

View file

@ -0,0 +1,253 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import logging
import mimetypes
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(WebSocketChannelsHandler, self).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(WebSocketChannelsHandler, self).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(WebSocketChannelsHandler, self).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(GatewayWebSocketClient, self).__init__(**kwargs)
self.kernel_id = None
self.ws = None
self.ws_future = Future()
self.disconnected = False
@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.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
if not self.disconnected: # if websocket is not disconnected by client, attept to reconnect to Gateway
self.log.info("Attempting to re-establish the connection to Gateway: {}".format(self.kernel_id))
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),
]

View file

@ -0,0 +1,626 @@
# 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 gen, web
from tornado.escape import json_encode, json_decode, url_escape
from tornado.httpclient import HTTPClient, AsyncHTTPClient, HTTPError
from ..services.kernels.kernelmanager import MappingKernelManager
from ..services.sessions.sessionmanager import SessionManager
from jupyter_client.kernelspec import KernelSpecManager
from ..utils import url_path_join
from traitlets import Instance, Unicode, 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(GatewayClient, self).__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)
@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()
kwargs.update(self._static_args)
return kwargs
@gen.coroutine
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 = yield 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
raise gen.Return(response)
class GatewayKernelManager(MappingKernelManager):
"""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(GatewayKernelManager, self).__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
@gen.coroutine
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 = yield gateway_request(kernel_url, method='POST', 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 = yield self.get_kernel(kernel_id)
kernel_id = kernel['id']
self.log.info("Using existing kernel: %s" % kernel_id)
self._kernels[kernel_id] = kernel
raise gen.Return(kernel_id)
@gen.coroutine
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 = yield 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)
self._kernels[kernel_id] = kernel
self.log.debug("Kernel retrieved: %s" % kernel)
raise gen.Return(kernel)
@gen.coroutine
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 = yield self.get_kernel(kernel_id)
raise gen.Return(model)
@gen.coroutine
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 = yield gateway_request(kernel_url, method='GET')
kernels = json_decode(response.body)
self._kernels = {x['id']: x for x in kernels}
raise gen.Return(kernels)
@gen.coroutine
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 = yield gateway_request(kernel_url, method='DELETE')
self.log.debug("Shutdown kernel response: %d %s", response.code, response.reason)
self.remove_kernel(kernel_id)
@gen.coroutine
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 = yield gateway_request(kernel_url, method='POST', body=json_encode({}))
self.log.debug("Restart kernel response: %d %s", response.code, response.reason)
@gen.coroutine
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 = yield gateway_request(kernel_url, method='POST', 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.keys():
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(GatewayKernelSpecManager, self).__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
@gen.coroutine
def get_all_specs(self):
fetched_kspecs = yield 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')
raise gen.Return(remote_kspecs)
@gen.coroutine
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 = yield gateway_request(kernel_spec_url, method='GET')
kernel_specs = json_decode(response.body)
raise gen.Return(kernel_specs)
@gen.coroutine
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 = yield 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)
raise gen.Return(kernel_spec)
@gen.coroutine
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 = yield 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
raise gen.Return(kernel_spec_resource)
class GatewaySessionManager(SessionManager):
kernel_manager = Instance('notebook.gateway.managers.GatewayKernelManager')
@gen.coroutine
def kernel_culled(self, kernel_id):
"""Checks if the kernel is still considered alive and returns true if its not found. """
kernel = yield self.kernel_manager.get_kernel(kernel_id)
raise gen.Return(kernel is None)

View file

@ -0,0 +1,103 @@
"""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
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,745 @@
# 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 &amp; Replace"
msgstr "Rechercher &amp; 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 &amp; Replace"
msgstr "Coller les cellules &amp; 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 &amp; Clear Output"
msgstr "Redémarrer &amp; 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 &amp; Run All"
msgstr "Redémarrer &amp; 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."

View file

@ -0,0 +1,480 @@
# 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

View file

@ -0,0 +1,740 @@
# 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>' を参照"

View file

@ -0,0 +1,527 @@
# 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 "Jupyter Notebook は tornade 4.0 以上が必要です"
#: notebook/notebookapp.py:57
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have < 1.1.0"
msgstr "Jupyter Notebook は tornade 4.0 以上が必要ですが 1.1.0 以下です"
#: notebook/notebookapp.py:59
#, python-format
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have %s"
msgstr "Jupyter Notebook は tornade 4.0 以上が必要ですが %s です"
#: notebook/notebookapp.py:209
msgid "The `ignore_minified_js` flag is deprecated and no longer works."
msgstr "`ignore_minified_js` フラグは非推奨であり既に動作していません。"
#: notebook/notebookapp.py:210
#, python-format
msgid "Alternatively use `%s` when working on the notebook's Javascript and LESS"
msgstr "ノートブックの Javascript と LESS で動作する場合には代わりに `%s` を使用してください。"
#: notebook/notebookapp.py:211
msgid "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"
msgstr "`ignore_minified_js` フラグは非推奨でありノートブック 6.0 では削除されます"
#: notebook/notebookapp.py:389
msgid "List currently running notebook servers."
msgstr "現在起動中のノートブックサーバの一覧"
#: notebook/notebookapp.py:393
msgid "Produce machine-readable JSON output."
msgstr "機械で読み込み可能な JSON 出力。"
#: 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 "True の場合、出力の各行はサーバ情報ファイルからの詳細情報を含む JSON オブジェクトになります。"
#: 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 "無効: matplotlib を有効にするにはノートブックで %pylab または %matplotlib を使用して下さい。"
#: notebook/notebookapp.py:439
msgid "Allow the notebook to be run from root user."
msgstr "ートブックをrootユーザーから実行できるようにする。"
#: 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 ""
"The Jupyter HTML Notebook.\n"
" \n"
" HTML5/Javascript Notebook クライアントを提供する Tornado ベースの HTML Notebook サーバを起動します。"
#: notebook/notebookapp.py:509
msgid "Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation"
msgstr "非推奨: 圧縮された JS ファイルを使用するかどうか。主に開発中に JS が再コンパイルされるのを回避するために使用します。"
#: notebook/notebookapp.py:540
msgid "Set the Access-Control-Allow-Credentials: true header"
msgstr "Access-Control-Allow-Credentials: true ヘッダーを設定します。"
#: notebook/notebookapp.py:544
msgid "Whether to allow the user to run the notebook as root."
msgstr "ユーザーがノートブックを root として実行できるようにするかどうか。"
#: notebook/notebookapp.py:548
msgid "The default URL to redirect to from `/`"
msgstr "`/` からリダイレクトされるデフォルトの URL"
#: notebook/notebookapp.py:552
msgid "The IP address the notebook server will listen on."
msgstr "ノートブックサーバが待ち受ける IP アドレス。"
#: notebook/notebookapp.py:565
#, python-format
msgid ""
"Cannot bind to localhost, using 127.0.0.1 as default ip\n"
"%s"
msgstr ""
"localhost でバインドできません。デフォルト IP アドレスとして 127.0.0.1 を使用します\n"
"%s"
#: 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 "SSL/TLS 証明書ファイルへの完全なパス。"
#: notebook/notebookapp.py:591
msgid "The full path to a private key file for usage with SSL/TLS."
msgstr "SSL/TLS で使用する秘密鍵ファイルへの完全なパス。"
#: notebook/notebookapp.py:595
msgid "The full path to a certificate authority certificate for SSL/TLS client authentication."
msgstr "SSL/TLS クライアント認証用の認証局証明書への完全なパス。"
#: notebook/notebookapp.py:599
msgid "The file where the cookie secret is stored."
msgstr "cookie secret を保存するファイル。"
#: notebook/notebookapp.py:628
#, python-format
msgid "Writing notebook server cookie secret to %s"
msgstr "ノートブックサーバは cookie secret を %s に書き込みます"
#: notebook/notebookapp.py:635
#, python-format
msgid "Could not set permissions on %s"
msgstr "%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 ""
"サーバに接続する際に初回の認証に使われるトークン。\n"
"\n"
" パスワード無しが有効になっている場合\n"
" デフォルト値はランダムなトークンが新しく生成されます。\n"
"\n"
" 空の文字列に設定すると認証が完全に無効になります。これは推奨されていません。\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 ""
"開いたブラウザが仕様するワンタイムトークン。\n"
" 1度使用されると再度使用する事が出来ません。\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 ""
"起動時にどこでノートブックを開くかを指定します。これは\n"
" 標準ライブラリのメソッド `webbrowser.open` の引数 `new` に渡されます。\n"
" 動作は保証されていませんがブラウザのサポートによって異なります。\n"
" 有効な値:\n"
" 2 新しいタブで開く\n"
" 1 新しいウィンドウで開く\n"
" 0 既にあるウィンドウで開く\n"
" 詳細は `webbrowser.open` のドキュメントを参照。\n"
" "
#: notebook/notebookapp.py:737
msgid "DEPRECATED, use tornado_settings"
msgstr "非推奨 tornado_settings の使用"
#: notebook/notebookapp.py:742
msgid ""
"\n"
" webapp_settings is deprecated, use tornado_settings.\n"
msgstr ""
"\n"
" webapp_settings は非推奨です。tornado_settings を使って下さい。\n"
#: notebook/notebookapp.py:746
msgid "Supply overrides for the tornado.web.Application that the Jupyter notebook uses."
msgstr "Jupyterートブックが使用する tornado.web.Application のオーバーライドを指定します。"
#: 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"
" tornado の websocket 接続の圧縮オプションを指定します。\n"
"\n"
" この値は :meth:`WebSocketHandler.get_compression_options` から返されます。\n"
" None (default) の場合は圧縮は無効になります。\n"
" 辞書 (空でも良い) の場合は圧縮が有効になります。\n"
"\n"
" 詳細は tornado の WebSocketHandler.get_compression_options のドキュメントを参照。\n"
" "
#: notebook/notebookapp.py:761
msgid "Supply overrides for terminado. Currently only supports \"shell_command\"."
msgstr "terminado のオーバーライドを指定します。現時は \"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 "`set_secure_cookie` に渡す追加のキーワード引数。詳細は tornado の set_secure_cookie のドキュメントを参照。"
#: notebook/notebookapp.py:768
msgid ""
"Supply SSL options for the tornado HTTPServer.\n"
" See the tornado docs for details."
msgstr ""
"tornado HTTPServer の SSL オプションを指定します。\n"
" 詳しくは tornado のドキュメントを参照。"
#: notebook/notebookapp.py:772
msgid "Supply extra arguments that will be passed to Jinja environment."
msgstr "Jinja environment に渡される追加の引数を指定します。"
#: notebook/notebookapp.py:776
msgid "Extra variables to supply to jinja templates when rendering."
msgstr "jinja テンプレートがレンダリングする際に渡される追加の変数。"
#: notebook/notebookapp.py:812
msgid "DEPRECATED use base_url"
msgstr "非推奨 base_url の使用"
#: notebook/notebookapp.py:816
msgid "base_project_url is deprecated, use base_url"
msgstr "base_project_url は非推奨です。base_url を使用して下さい。"
#: notebook/notebookapp.py:832
msgid "Path to search for custom.js, css"
msgstr "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 ""
"Jinja テンプレートを探す為の追加パス。\n"
"\n"
" notebook.templates を上書きする為に使う事が出来ます。"
#: notebook/notebookapp.py:855
msgid "extra paths to look for Javascript notebook extensions"
msgstr "Javascript ノートブック拡張への追加パス"
#: notebook/notebookapp.py:900
#, python-format
msgid "Using MathJax: %s"
msgstr "使用している MathJax: %s"
#: notebook/notebookapp.py:903
msgid "The MathJax.js configuration file that is to be used."
msgstr "使用される MathJax.js 設定ファイル。"
#: notebook/notebookapp.py:908
#, python-format
msgid "Using MathJax configuration file: %s"
msgstr "使用する MathJax 設定ファイル: %s"
#: 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 "X-Scheme/X-Forwarded-Proto および X-Real-Ip/X-Forwarded-For ヘッダーがアップストリームのリバースプロキシによって送信されたことを信頼するかどうか。プロキシが SSL を処理する場合に必要となります。"
#: notebook/notebookapp.py:976
msgid ""
"\n"
" DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.\n"
" "
msgstr ""
"\n"
" 非推奨: matplotlib を有効にするにはノートブックで %pylab または %matplotlib\n"
" を実行して下さい。"
#: notebook/notebookapp.py:988
msgid "Support for specifying --pylab on the command line has been removed."
msgstr "コマンドラインでの --pylab 指定はサポートされなくなりました。"
#: notebook/notebookapp.py:990
msgid "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself."
msgstr "ノートブックの中で `%pylab{0}` または `%matplotlib{0}` を使ってください。"
#: 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 "ノートブックディレクトリが見つかりません: '%r'"
#: notebook/notebookapp.py:1031
msgid "DEPRECATED use the nbserver_extensions dict instead"
msgstr "非推奨 nbserver_extensions 辞書を代わりに使用して下さい "
#: notebook/notebookapp.py:1036
msgid "server_extensions is deprecated, use nbserver_extensions"
msgstr "server_extensions が非推奨です。 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 "ノートブックサーバ拡張としてロードする Python モジュールの辞書。エントリー値を使用して拡張のロードを有効または無効にすることができます。 拡張子はアルファベット順にロードされます。"
#: 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 "メッセージが送信される前に iopub で送信可能な最大レート。"
#: 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"
" ストリーム出力が送信制限される前に iopub で送信可能な最大レート。"
#: notebook/notebookapp.py:1060
msgid ""
"(sec) Time window used to \n"
" check the message and data rate limits."
msgstr ""
"(sec) このウィンドウはメッセージとデータの帯域リミット\n"
" をチェックする為に使用されます。"
#: notebook/notebookapp.py:1071
#, python-format
msgid "No such file or directory: %s"
msgstr "その様なファイルまたはディレクトリは存在しません: %s"
#: 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 "ポート %i は既に使用されています。他のポートで試して下さい。"
#: notebook/notebookapp.py:1184
#, python-format
msgid "Permission to listen on port %i denied"
msgstr "ポート %i で待機する権限がありません"
#: 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 "[システム上の全ての IP アドレス]"
#: notebook/notebookapp.py:1223
#, python-format
msgid "Terminals not available (error was %s)"
msgstr "端末は存在しません (%s でエラー発生)"
#: 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 "このノートブックサーバをシャットダウンしますか? (%s/[%s])"
#: notebook/notebookapp.py:1269
msgid "Shutdown confirmed"
msgstr "シャットダウンの確認"
#: notebook/notebookapp.py:1273
msgid "No answer for 5s:"
msgstr "5秒間に応答がありません:"
#: notebook/notebookapp.py:1274
msgid "resuming operation..."
msgstr "操作を再開中..."
#: notebook/notebookapp.py:1282
#, python-format
msgid "received signal %s, stopping"
msgstr "シグナル %s を受信。停止します"
#: notebook/notebookapp.py:1338
#, python-format
msgid "Error loading server extension %s"
msgstr "サーバ拡張 %s の読み込みエラー"
#: notebook/notebookapp.py:1369
#, python-format
msgid "Shutting down %d kernels"
msgstr "%d 個のカーネルをシャットダウンしています"
#: notebook/notebookapp.py:1375
#, python-format
msgid "%d active kernel"
msgid_plural "%d active kernels"
msgstr[0] "%d 個のアクティブなカーネル"
msgstr[1] "%d 個のアクティブなカーネル"
#: notebook/notebookapp.py:1379
#, python-format
msgid ""
"The Jupyter Notebook is running at:\n"
"%s"
msgstr ""
"Jupyter Notebook は以下の URL 起動しています:\n"
"%s"
#: notebook/notebookapp.py:1426
msgid "Running as root is not recommended. Use --allow-root to bypass."
msgstr "root ユーザでの実行は推奨されません。バイパスするには --allow-root を使って下さい。"
#: notebook/notebookapp.py:1432
msgid "Use Control-C to stop this server and shut down all kernels (twice to skip confirmation)."
msgstr "サーバを停止し全てのカーネルをシャットダウンするには Control-C を使って下さい(確認をスキップするには2回)。"
#: 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 "Project Jupyter へようこそ! 利用可能な色々なツールとそれに対応するドキュメントを探索して下さい。プラットフォームへの貢献に興味がある場合は http://jupyter.org/community.html の communityresources セクションにアクセスしてください。"
#: notebook/notebookapp.py:1445
#, python-format
msgid "No web browser found: %s."
msgstr "ウェブブラウザが見つかりません: %s"
#: notebook/notebookapp.py:1450
#, python-format
msgid "%s does not exist"
msgstr "%s は存在しません"
#: notebook/notebookapp.py:1484
msgid "Interrupted..."
msgstr "中断..."
#: notebook/services/contents/filemanager.py:506
#, python-format
msgid "Serving notebooks from local directory: %s"
msgstr "ローカルディレクトリからノートブックをサーブ: %s"
#: 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

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