|
@@
-1,718
+1,718
b''
|
|
1
|
# coding: utf-8
|
|
1
|
# coding: utf-8
|
|
2
|
"""A tornado based IPython notebook server.
|
|
2
|
"""A tornado based IPython notebook server.
|
|
3
|
|
|
3
|
|
|
4
|
Authors:
|
|
4
|
Authors:
|
|
5
|
|
|
5
|
|
|
6
|
* Brian Granger
|
|
6
|
* Brian Granger
|
|
7
|
"""
|
|
7
|
"""
|
|
8
|
#-----------------------------------------------------------------------------
|
|
8
|
#-----------------------------------------------------------------------------
|
|
9
|
# Copyright (C) 2008-2011 The IPython Development Team
|
|
9
|
# Copyright (C) 2008-2011 The IPython Development Team
|
|
10
|
#
|
|
10
|
#
|
|
11
|
# Distributed under the terms of the BSD License. The full license is in
|
|
11
|
# Distributed under the terms of the BSD License. The full license is in
|
|
12
|
# the file COPYING, distributed as part of this software.
|
|
12
|
# the file COPYING, distributed as part of this software.
|
|
13
|
#-----------------------------------------------------------------------------
|
|
13
|
#-----------------------------------------------------------------------------
|
|
14
|
|
|
14
|
|
|
15
|
#-----------------------------------------------------------------------------
|
|
15
|
#-----------------------------------------------------------------------------
|
|
16
|
# Imports
|
|
16
|
# Imports
|
|
17
|
#-----------------------------------------------------------------------------
|
|
17
|
#-----------------------------------------------------------------------------
|
|
18
|
|
|
18
|
|
|
19
|
# stdlib
|
|
19
|
# stdlib
|
|
20
|
import errno
|
|
20
|
import errno
|
|
21
|
import logging
|
|
21
|
import logging
|
|
22
|
import os
|
|
22
|
import os
|
|
23
|
import random
|
|
23
|
import random
|
|
24
|
import re
|
|
24
|
import re
|
|
25
|
import select
|
|
25
|
import select
|
|
26
|
import signal
|
|
26
|
import signal
|
|
27
|
import socket
|
|
27
|
import socket
|
|
28
|
import sys
|
|
28
|
import sys
|
|
29
|
import threading
|
|
29
|
import threading
|
|
30
|
import time
|
|
30
|
import time
|
|
31
|
import uuid
|
|
31
|
import uuid
|
|
32
|
import webbrowser
|
|
32
|
import webbrowser
|
|
33
|
|
|
33
|
|
|
34
|
|
|
34
|
|
|
35
|
# Third party
|
|
35
|
# Third party
|
|
36
|
# check for pyzmq 2.1.11 (this is actually redundant)
|
|
36
|
# check for pyzmq 2.1.11
|
|
37
|
from IPython.kernel.zmq import check_for_zmq
|
|
37
|
from IPython.utils.zmqrelated import check_for_zmq
|
|
38
|
check_for_zmq('2.1.11', 'IPython.frontend.html.notebook')
|
|
38
|
check_for_zmq('2.1.11', 'IPython.frontend.html.notebook')
|
|
39
|
|
|
39
|
|
|
40
|
import zmq
|
|
40
|
import zmq
|
|
41
|
from jinja2 import Environment, FileSystemLoader
|
|
41
|
from jinja2 import Environment, FileSystemLoader
|
|
42
|
|
|
42
|
|
|
43
|
# Install the pyzmq ioloop. This has to be done before anything else from
|
|
43
|
# Install the pyzmq ioloop. This has to be done before anything else from
|
|
44
|
# tornado is imported.
|
|
44
|
# tornado is imported.
|
|
45
|
from zmq.eventloop import ioloop
|
|
45
|
from zmq.eventloop import ioloop
|
|
46
|
ioloop.install()
|
|
46
|
ioloop.install()
|
|
47
|
|
|
47
|
|
|
48
|
# check for tornado 2.1.0
|
|
48
|
# check for tornado 2.1.0
|
|
49
|
msg = "The IPython Notebook requires tornado >= 2.1.0"
|
|
49
|
msg = "The IPython Notebook requires tornado >= 2.1.0"
|
|
50
|
try:
|
|
50
|
try:
|
|
51
|
import tornado
|
|
51
|
import tornado
|
|
52
|
except ImportError:
|
|
52
|
except ImportError:
|
|
53
|
raise ImportError(msg)
|
|
53
|
raise ImportError(msg)
|
|
54
|
try:
|
|
54
|
try:
|
|
55
|
version_info = tornado.version_info
|
|
55
|
version_info = tornado.version_info
|
|
56
|
except AttributeError:
|
|
56
|
except AttributeError:
|
|
57
|
raise ImportError(msg + ", but you have < 1.1.0")
|
|
57
|
raise ImportError(msg + ", but you have < 1.1.0")
|
|
58
|
if version_info < (2,1,0):
|
|
58
|
if version_info < (2,1,0):
|
|
59
|
raise ImportError(msg + ", but you have %s" % tornado.version)
|
|
59
|
raise ImportError(msg + ", but you have %s" % tornado.version)
|
|
60
|
|
|
60
|
|
|
61
|
from tornado import httpserver
|
|
61
|
from tornado import httpserver
|
|
62
|
from tornado import web
|
|
62
|
from tornado import web
|
|
63
|
|
|
63
|
|
|
64
|
# Our own libraries
|
|
64
|
# Our own libraries
|
|
65
|
from IPython.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
|
|
65
|
from IPython.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
|
|
66
|
from .kernelmanager import MappingKernelManager
|
|
66
|
from .kernelmanager import MappingKernelManager
|
|
67
|
from .handlers import (LoginHandler, LogoutHandler,
|
|
67
|
from .handlers import (LoginHandler, LogoutHandler,
|
|
68
|
ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
|
|
68
|
ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
|
|
69
|
MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
|
|
69
|
MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
|
|
70
|
ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
|
|
70
|
ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
|
|
71
|
RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
|
|
71
|
RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
|
|
72
|
MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
|
|
72
|
MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
|
|
73
|
FileFindHandler, NotebookRedirectHandler,
|
|
73
|
FileFindHandler, NotebookRedirectHandler,
|
|
74
|
)
|
|
74
|
)
|
|
75
|
from .nbmanager import NotebookManager
|
|
75
|
from .nbmanager import NotebookManager
|
|
76
|
from .filenbmanager import FileNotebookManager
|
|
76
|
from .filenbmanager import FileNotebookManager
|
|
77
|
from .clustermanager import ClusterManager
|
|
77
|
from .clustermanager import ClusterManager
|
|
78
|
|
|
78
|
|
|
79
|
from IPython.config.application import catch_config_error, boolean_flag
|
|
79
|
from IPython.config.application import catch_config_error, boolean_flag
|
|
80
|
from IPython.core.application import BaseIPythonApplication
|
|
80
|
from IPython.core.application import BaseIPythonApplication
|
|
81
|
from IPython.core.profiledir import ProfileDir
|
|
81
|
from IPython.core.profiledir import ProfileDir
|
|
82
|
from IPython.frontend.consoleapp import IPythonConsoleApp
|
|
82
|
from IPython.frontend.consoleapp import IPythonConsoleApp
|
|
83
|
from IPython.kernel import swallow_argv
|
|
83
|
from IPython.kernel import swallow_argv
|
|
84
|
from IPython.kernel.zmq.session import Session, default_secure
|
|
84
|
from IPython.kernel.zmq.session import Session, default_secure
|
|
85
|
from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
|
|
85
|
from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
|
|
86
|
from IPython.kernel.zmq.kernelapp import (
|
|
86
|
from IPython.kernel.zmq.kernelapp import (
|
|
87
|
kernel_flags,
|
|
87
|
kernel_flags,
|
|
88
|
kernel_aliases,
|
|
88
|
kernel_aliases,
|
|
89
|
IPKernelApp
|
|
89
|
IPKernelApp
|
|
90
|
)
|
|
90
|
)
|
|
91
|
from IPython.utils.importstring import import_item
|
|
91
|
from IPython.utils.importstring import import_item
|
|
92
|
from IPython.utils.localinterfaces import LOCALHOST
|
|
92
|
from IPython.utils.localinterfaces import LOCALHOST
|
|
93
|
from IPython.utils.traitlets import (
|
|
93
|
from IPython.utils.traitlets import (
|
|
94
|
Dict, Unicode, Integer, List, Enum, Bool,
|
|
94
|
Dict, Unicode, Integer, List, Enum, Bool,
|
|
95
|
DottedObjectName
|
|
95
|
DottedObjectName
|
|
96
|
)
|
|
96
|
)
|
|
97
|
from IPython.utils import py3compat
|
|
97
|
from IPython.utils import py3compat
|
|
98
|
from IPython.utils.path import filefind
|
|
98
|
from IPython.utils.path import filefind
|
|
99
|
|
|
99
|
|
|
100
|
#-----------------------------------------------------------------------------
|
|
100
|
#-----------------------------------------------------------------------------
|
|
101
|
# Module globals
|
|
101
|
# Module globals
|
|
102
|
#-----------------------------------------------------------------------------
|
|
102
|
#-----------------------------------------------------------------------------
|
|
103
|
|
|
103
|
|
|
104
|
_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
|
|
104
|
_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
|
|
105
|
_kernel_action_regex = r"(?P<action>restart|interrupt)"
|
|
105
|
_kernel_action_regex = r"(?P<action>restart|interrupt)"
|
|
106
|
_notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
|
|
106
|
_notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
|
|
107
|
_notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
|
|
107
|
_notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
|
|
108
|
_profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
|
|
108
|
_profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
|
|
109
|
_cluster_action_regex = r"(?P<action>start|stop)"
|
|
109
|
_cluster_action_regex = r"(?P<action>start|stop)"
|
|
110
|
|
|
110
|
|
|
111
|
_examples = """
|
|
111
|
_examples = """
|
|
112
|
ipython notebook # start the notebook
|
|
112
|
ipython notebook # start the notebook
|
|
113
|
ipython notebook --profile=sympy # use the sympy profile
|
|
113
|
ipython notebook --profile=sympy # use the sympy profile
|
|
114
|
ipython notebook --pylab=inline # pylab in inline plotting mode
|
|
114
|
ipython notebook --pylab=inline # pylab in inline plotting mode
|
|
115
|
ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
|
|
115
|
ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
|
|
116
|
ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
|
|
116
|
ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
|
|
117
|
"""
|
|
117
|
"""
|
|
118
|
|
|
118
|
|
|
119
|
#-----------------------------------------------------------------------------
|
|
119
|
#-----------------------------------------------------------------------------
|
|
120
|
# Helper functions
|
|
120
|
# Helper functions
|
|
121
|
#-----------------------------------------------------------------------------
|
|
121
|
#-----------------------------------------------------------------------------
|
|
122
|
|
|
122
|
|
|
123
|
def url_path_join(a,b):
|
|
123
|
def url_path_join(a,b):
|
|
124
|
if a.endswith('/') and b.startswith('/'):
|
|
124
|
if a.endswith('/') and b.startswith('/'):
|
|
125
|
return a[:-1]+b
|
|
125
|
return a[:-1]+b
|
|
126
|
else:
|
|
126
|
else:
|
|
127
|
return a+b
|
|
127
|
return a+b
|
|
128
|
|
|
128
|
|
|
129
|
def random_ports(port, n):
|
|
129
|
def random_ports(port, n):
|
|
130
|
"""Generate a list of n random ports near the given port.
|
|
130
|
"""Generate a list of n random ports near the given port.
|
|
131
|
|
|
131
|
|
|
132
|
The first 5 ports will be sequential, and the remaining n-5 will be
|
|
132
|
The first 5 ports will be sequential, and the remaining n-5 will be
|
|
133
|
randomly selected in the range [port-2*n, port+2*n].
|
|
133
|
randomly selected in the range [port-2*n, port+2*n].
|
|
134
|
"""
|
|
134
|
"""
|
|
135
|
for i in range(min(5, n)):
|
|
135
|
for i in range(min(5, n)):
|
|
136
|
yield port + i
|
|
136
|
yield port + i
|
|
137
|
for i in range(n-5):
|
|
137
|
for i in range(n-5):
|
|
138
|
yield port + random.randint(-2*n, 2*n)
|
|
138
|
yield port + random.randint(-2*n, 2*n)
|
|
139
|
|
|
139
|
|
|
140
|
#-----------------------------------------------------------------------------
|
|
140
|
#-----------------------------------------------------------------------------
|
|
141
|
# The Tornado web application
|
|
141
|
# The Tornado web application
|
|
142
|
#-----------------------------------------------------------------------------
|
|
142
|
#-----------------------------------------------------------------------------
|
|
143
|
|
|
143
|
|
|
144
|
class NotebookWebApplication(web.Application):
|
|
144
|
class NotebookWebApplication(web.Application):
|
|
145
|
|
|
145
|
|
|
146
|
def __init__(self, ipython_app, kernel_manager, notebook_manager,
|
|
146
|
def __init__(self, ipython_app, kernel_manager, notebook_manager,
|
|
147
|
cluster_manager, log,
|
|
147
|
cluster_manager, log,
|
|
148
|
base_project_url, settings_overrides):
|
|
148
|
base_project_url, settings_overrides):
|
|
149
|
handlers = [
|
|
149
|
handlers = [
|
|
150
|
(r"/", ProjectDashboardHandler),
|
|
150
|
(r"/", ProjectDashboardHandler),
|
|
151
|
(r"/login", LoginHandler),
|
|
151
|
(r"/login", LoginHandler),
|
|
152
|
(r"/logout", LogoutHandler),
|
|
152
|
(r"/logout", LogoutHandler),
|
|
153
|
(r"/new", NewHandler),
|
|
153
|
(r"/new", NewHandler),
|
|
154
|
(r"/%s" % _notebook_id_regex, NamedNotebookHandler),
|
|
154
|
(r"/%s" % _notebook_id_regex, NamedNotebookHandler),
|
|
155
|
(r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
|
|
155
|
(r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
|
|
156
|
(r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
|
|
156
|
(r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
|
|
157
|
(r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
|
|
157
|
(r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
|
|
158
|
(r"/kernels", MainKernelHandler),
|
|
158
|
(r"/kernels", MainKernelHandler),
|
|
159
|
(r"/kernels/%s" % _kernel_id_regex, KernelHandler),
|
|
159
|
(r"/kernels/%s" % _kernel_id_regex, KernelHandler),
|
|
160
|
(r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
|
|
160
|
(r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
|
|
161
|
(r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
|
|
161
|
(r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
|
|
162
|
(r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
|
|
162
|
(r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
|
|
163
|
(r"/notebooks", NotebookRootHandler),
|
|
163
|
(r"/notebooks", NotebookRootHandler),
|
|
164
|
(r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
|
|
164
|
(r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
|
|
165
|
(r"/rstservice/render", RSTHandler),
|
|
165
|
(r"/rstservice/render", RSTHandler),
|
|
166
|
(r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
|
|
166
|
(r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
|
|
167
|
(r"/clusters", MainClusterHandler),
|
|
167
|
(r"/clusters", MainClusterHandler),
|
|
168
|
(r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
|
|
168
|
(r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
|
|
169
|
(r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
|
|
169
|
(r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
|
|
170
|
]
|
|
170
|
]
|
|
171
|
|
|
171
|
|
|
172
|
# Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
|
|
172
|
# Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
|
|
173
|
# base_project_url will always be unicode, which will in turn
|
|
173
|
# base_project_url will always be unicode, which will in turn
|
|
174
|
# make the patterns unicode, and ultimately result in unicode
|
|
174
|
# make the patterns unicode, and ultimately result in unicode
|
|
175
|
# keys in kwargs to handler._execute(**kwargs) in tornado.
|
|
175
|
# keys in kwargs to handler._execute(**kwargs) in tornado.
|
|
176
|
# This enforces that base_project_url be ascii in that situation.
|
|
176
|
# This enforces that base_project_url be ascii in that situation.
|
|
177
|
#
|
|
177
|
#
|
|
178
|
# Note that the URLs these patterns check against are escaped,
|
|
178
|
# Note that the URLs these patterns check against are escaped,
|
|
179
|
# and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
|
|
179
|
# and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
|
|
180
|
base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
|
|
180
|
base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
|
|
181
|
|
|
181
|
|
|
182
|
settings = dict(
|
|
182
|
settings = dict(
|
|
183
|
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
|
183
|
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
|
184
|
static_path=ipython_app.static_file_path,
|
|
184
|
static_path=ipython_app.static_file_path,
|
|
185
|
static_handler_class = FileFindHandler,
|
|
185
|
static_handler_class = FileFindHandler,
|
|
186
|
static_url_prefix = url_path_join(base_project_url,'/static/'),
|
|
186
|
static_url_prefix = url_path_join(base_project_url,'/static/'),
|
|
187
|
cookie_secret=os.urandom(1024),
|
|
187
|
cookie_secret=os.urandom(1024),
|
|
188
|
login_url=url_path_join(base_project_url,'/login'),
|
|
188
|
login_url=url_path_join(base_project_url,'/login'),
|
|
189
|
cookie_name='username-%s' % uuid.uuid4(),
|
|
189
|
cookie_name='username-%s' % uuid.uuid4(),
|
|
190
|
base_project_url = base_project_url,
|
|
190
|
base_project_url = base_project_url,
|
|
191
|
)
|
|
191
|
)
|
|
192
|
|
|
192
|
|
|
193
|
# allow custom overrides for the tornado web app.
|
|
193
|
# allow custom overrides for the tornado web app.
|
|
194
|
settings.update(settings_overrides)
|
|
194
|
settings.update(settings_overrides)
|
|
195
|
|
|
195
|
|
|
196
|
# prepend base_project_url onto the patterns that we match
|
|
196
|
# prepend base_project_url onto the patterns that we match
|
|
197
|
new_handlers = []
|
|
197
|
new_handlers = []
|
|
198
|
for handler in handlers:
|
|
198
|
for handler in handlers:
|
|
199
|
pattern = url_path_join(base_project_url, handler[0])
|
|
199
|
pattern = url_path_join(base_project_url, handler[0])
|
|
200
|
new_handler = tuple([pattern]+list(handler[1:]))
|
|
200
|
new_handler = tuple([pattern]+list(handler[1:]))
|
|
201
|
new_handlers.append( new_handler )
|
|
201
|
new_handlers.append( new_handler )
|
|
202
|
|
|
202
|
|
|
203
|
super(NotebookWebApplication, self).__init__(new_handlers, **settings)
|
|
203
|
super(NotebookWebApplication, self).__init__(new_handlers, **settings)
|
|
204
|
|
|
204
|
|
|
205
|
self.kernel_manager = kernel_manager
|
|
205
|
self.kernel_manager = kernel_manager
|
|
206
|
self.notebook_manager = notebook_manager
|
|
206
|
self.notebook_manager = notebook_manager
|
|
207
|
self.cluster_manager = cluster_manager
|
|
207
|
self.cluster_manager = cluster_manager
|
|
208
|
self.ipython_app = ipython_app
|
|
208
|
self.ipython_app = ipython_app
|
|
209
|
self.read_only = self.ipython_app.read_only
|
|
209
|
self.read_only = self.ipython_app.read_only
|
|
210
|
self.config = self.ipython_app.config
|
|
210
|
self.config = self.ipython_app.config
|
|
211
|
self.use_less = self.ipython_app.use_less
|
|
211
|
self.use_less = self.ipython_app.use_less
|
|
212
|
self.log = log
|
|
212
|
self.log = log
|
|
213
|
self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
|
|
213
|
self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
|
|
214
|
|
|
214
|
|
|
215
|
|
|
215
|
|
|
216
|
|
|
216
|
|
|
217
|
#-----------------------------------------------------------------------------
|
|
217
|
#-----------------------------------------------------------------------------
|
|
218
|
# Aliases and Flags
|
|
218
|
# Aliases and Flags
|
|
219
|
#-----------------------------------------------------------------------------
|
|
219
|
#-----------------------------------------------------------------------------
|
|
220
|
|
|
220
|
|
|
221
|
flags = dict(kernel_flags)
|
|
221
|
flags = dict(kernel_flags)
|
|
222
|
flags['no-browser']=(
|
|
222
|
flags['no-browser']=(
|
|
223
|
{'NotebookApp' : {'open_browser' : False}},
|
|
223
|
{'NotebookApp' : {'open_browser' : False}},
|
|
224
|
"Don't open the notebook in a browser after startup."
|
|
224
|
"Don't open the notebook in a browser after startup."
|
|
225
|
)
|
|
225
|
)
|
|
226
|
flags['no-mathjax']=(
|
|
226
|
flags['no-mathjax']=(
|
|
227
|
{'NotebookApp' : {'enable_mathjax' : False}},
|
|
227
|
{'NotebookApp' : {'enable_mathjax' : False}},
|
|
228
|
"""Disable MathJax
|
|
228
|
"""Disable MathJax
|
|
229
|
|
|
229
|
|
|
230
|
MathJax is the javascript library IPython uses to render math/LaTeX. It is
|
|
230
|
MathJax is the javascript library IPython uses to render math/LaTeX. It is
|
|
231
|
very large, so you may want to disable it if you have a slow internet
|
|
231
|
very large, so you may want to disable it if you have a slow internet
|
|
232
|
connection, or for offline use of the notebook.
|
|
232
|
connection, or for offline use of the notebook.
|
|
233
|
|
|
233
|
|
|
234
|
When disabled, equations etc. will appear as their untransformed TeX source.
|
|
234
|
When disabled, equations etc. will appear as their untransformed TeX source.
|
|
235
|
"""
|
|
235
|
"""
|
|
236
|
)
|
|
236
|
)
|
|
237
|
flags['read-only'] = (
|
|
237
|
flags['read-only'] = (
|
|
238
|
{'NotebookApp' : {'read_only' : True}},
|
|
238
|
{'NotebookApp' : {'read_only' : True}},
|
|
239
|
"""Allow read-only access to notebooks.
|
|
239
|
"""Allow read-only access to notebooks.
|
|
240
|
|
|
240
|
|
|
241
|
When using a password to protect the notebook server, this flag
|
|
241
|
When using a password to protect the notebook server, this flag
|
|
242
|
allows unauthenticated clients to view the notebook list, and
|
|
242
|
allows unauthenticated clients to view the notebook list, and
|
|
243
|
individual notebooks, but not edit them, start kernels, or run
|
|
243
|
individual notebooks, but not edit them, start kernels, or run
|
|
244
|
code.
|
|
244
|
code.
|
|
245
|
|
|
245
|
|
|
246
|
If no password is set, the server will be entirely read-only.
|
|
246
|
If no password is set, the server will be entirely read-only.
|
|
247
|
"""
|
|
247
|
"""
|
|
248
|
)
|
|
248
|
)
|
|
249
|
|
|
249
|
|
|
250
|
# Add notebook manager flags
|
|
250
|
# Add notebook manager flags
|
|
251
|
flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
|
|
251
|
flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
|
|
252
|
'Auto-save a .py script everytime the .ipynb notebook is saved',
|
|
252
|
'Auto-save a .py script everytime the .ipynb notebook is saved',
|
|
253
|
'Do not auto-save .py scripts for every notebook'))
|
|
253
|
'Do not auto-save .py scripts for every notebook'))
|
|
254
|
|
|
254
|
|
|
255
|
# the flags that are specific to the frontend
|
|
255
|
# the flags that are specific to the frontend
|
|
256
|
# these must be scrubbed before being passed to the kernel,
|
|
256
|
# these must be scrubbed before being passed to the kernel,
|
|
257
|
# or it will raise an error on unrecognized flags
|
|
257
|
# or it will raise an error on unrecognized flags
|
|
258
|
notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
|
|
258
|
notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
|
|
259
|
|
|
259
|
|
|
260
|
aliases = dict(kernel_aliases)
|
|
260
|
aliases = dict(kernel_aliases)
|
|
261
|
|
|
261
|
|
|
262
|
aliases.update({
|
|
262
|
aliases.update({
|
|
263
|
'ip': 'NotebookApp.ip',
|
|
263
|
'ip': 'NotebookApp.ip',
|
|
264
|
'port': 'NotebookApp.port',
|
|
264
|
'port': 'NotebookApp.port',
|
|
265
|
'port-retries': 'NotebookApp.port_retries',
|
|
265
|
'port-retries': 'NotebookApp.port_retries',
|
|
266
|
'transport': 'KernelManager.transport',
|
|
266
|
'transport': 'KernelManager.transport',
|
|
267
|
'keyfile': 'NotebookApp.keyfile',
|
|
267
|
'keyfile': 'NotebookApp.keyfile',
|
|
268
|
'certfile': 'NotebookApp.certfile',
|
|
268
|
'certfile': 'NotebookApp.certfile',
|
|
269
|
'notebook-dir': 'NotebookManager.notebook_dir',
|
|
269
|
'notebook-dir': 'NotebookManager.notebook_dir',
|
|
270
|
'browser': 'NotebookApp.browser',
|
|
270
|
'browser': 'NotebookApp.browser',
|
|
271
|
})
|
|
271
|
})
|
|
272
|
|
|
272
|
|
|
273
|
# remove ipkernel flags that are singletons, and don't make sense in
|
|
273
|
# remove ipkernel flags that are singletons, and don't make sense in
|
|
274
|
# multi-kernel evironment:
|
|
274
|
# multi-kernel evironment:
|
|
275
|
aliases.pop('f', None)
|
|
275
|
aliases.pop('f', None)
|
|
276
|
|
|
276
|
|
|
277
|
notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
|
|
277
|
notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
|
|
278
|
u'notebook-dir']
|
|
278
|
u'notebook-dir']
|
|
279
|
|
|
279
|
|
|
280
|
#-----------------------------------------------------------------------------
|
|
280
|
#-----------------------------------------------------------------------------
|
|
281
|
# NotebookApp
|
|
281
|
# NotebookApp
|
|
282
|
#-----------------------------------------------------------------------------
|
|
282
|
#-----------------------------------------------------------------------------
|
|
283
|
|
|
283
|
|
|
284
|
class NotebookApp(BaseIPythonApplication):
|
|
284
|
class NotebookApp(BaseIPythonApplication):
|
|
285
|
|
|
285
|
|
|
286
|
name = 'ipython-notebook'
|
|
286
|
name = 'ipython-notebook'
|
|
287
|
default_config_file_name='ipython_notebook_config.py'
|
|
287
|
default_config_file_name='ipython_notebook_config.py'
|
|
288
|
|
|
288
|
|
|
289
|
description = """
|
|
289
|
description = """
|
|
290
|
The IPython HTML Notebook.
|
|
290
|
The IPython HTML Notebook.
|
|
291
|
|
|
291
|
|
|
292
|
This launches a Tornado based HTML Notebook Server that serves up an
|
|
292
|
This launches a Tornado based HTML Notebook Server that serves up an
|
|
293
|
HTML5/Javascript Notebook client.
|
|
293
|
HTML5/Javascript Notebook client.
|
|
294
|
"""
|
|
294
|
"""
|
|
295
|
examples = _examples
|
|
295
|
examples = _examples
|
|
296
|
|
|
296
|
|
|
297
|
classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
|
|
297
|
classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
|
|
298
|
FileNotebookManager]
|
|
298
|
FileNotebookManager]
|
|
299
|
flags = Dict(flags)
|
|
299
|
flags = Dict(flags)
|
|
300
|
aliases = Dict(aliases)
|
|
300
|
aliases = Dict(aliases)
|
|
301
|
|
|
301
|
|
|
302
|
kernel_argv = List(Unicode)
|
|
302
|
kernel_argv = List(Unicode)
|
|
303
|
|
|
303
|
|
|
304
|
log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
|
|
304
|
log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
|
|
305
|
default_value=logging.INFO,
|
|
305
|
default_value=logging.INFO,
|
|
306
|
config=True,
|
|
306
|
config=True,
|
|
307
|
help="Set the log level by value or name.")
|
|
307
|
help="Set the log level by value or name.")
|
|
308
|
|
|
308
|
|
|
309
|
# create requested profiles by default, if they don't exist:
|
|
309
|
# create requested profiles by default, if they don't exist:
|
|
310
|
auto_create = Bool(True)
|
|
310
|
auto_create = Bool(True)
|
|
311
|
|
|
311
|
|
|
312
|
# file to be opened in the notebook server
|
|
312
|
# file to be opened in the notebook server
|
|
313
|
file_to_run = Unicode('')
|
|
313
|
file_to_run = Unicode('')
|
|
314
|
|
|
314
|
|
|
315
|
# Network related information.
|
|
315
|
# Network related information.
|
|
316
|
|
|
316
|
|
|
317
|
ip = Unicode(LOCALHOST, config=True,
|
|
317
|
ip = Unicode(LOCALHOST, config=True,
|
|
318
|
help="The IP address the notebook server will listen on."
|
|
318
|
help="The IP address the notebook server will listen on."
|
|
319
|
)
|
|
319
|
)
|
|
320
|
|
|
320
|
|
|
321
|
def _ip_changed(self, name, old, new):
|
|
321
|
def _ip_changed(self, name, old, new):
|
|
322
|
if new == u'*': self.ip = u''
|
|
322
|
if new == u'*': self.ip = u''
|
|
323
|
|
|
323
|
|
|
324
|
port = Integer(8888, config=True,
|
|
324
|
port = Integer(8888, config=True,
|
|
325
|
help="The port the notebook server will listen on."
|
|
325
|
help="The port the notebook server will listen on."
|
|
326
|
)
|
|
326
|
)
|
|
327
|
port_retries = Integer(50, config=True,
|
|
327
|
port_retries = Integer(50, config=True,
|
|
328
|
help="The number of additional ports to try if the specified port is not available."
|
|
328
|
help="The number of additional ports to try if the specified port is not available."
|
|
329
|
)
|
|
329
|
)
|
|
330
|
|
|
330
|
|
|
331
|
certfile = Unicode(u'', config=True,
|
|
331
|
certfile = Unicode(u'', config=True,
|
|
332
|
help="""The full path to an SSL/TLS certificate file."""
|
|
332
|
help="""The full path to an SSL/TLS certificate file."""
|
|
333
|
)
|
|
333
|
)
|
|
334
|
|
|
334
|
|
|
335
|
keyfile = Unicode(u'', config=True,
|
|
335
|
keyfile = Unicode(u'', config=True,
|
|
336
|
help="""The full path to a private key file for usage with SSL/TLS."""
|
|
336
|
help="""The full path to a private key file for usage with SSL/TLS."""
|
|
337
|
)
|
|
337
|
)
|
|
338
|
|
|
338
|
|
|
339
|
password = Unicode(u'', config=True,
|
|
339
|
password = Unicode(u'', config=True,
|
|
340
|
help="""Hashed password to use for web authentication.
|
|
340
|
help="""Hashed password to use for web authentication.
|
|
341
|
|
|
341
|
|
|
342
|
To generate, type in a python/IPython shell:
|
|
342
|
To generate, type in a python/IPython shell:
|
|
343
|
|
|
343
|
|
|
344
|
from IPython.lib import passwd; passwd()
|
|
344
|
from IPython.lib import passwd; passwd()
|
|
345
|
|
|
345
|
|
|
346
|
The string should be of the form type:salt:hashed-password.
|
|
346
|
The string should be of the form type:salt:hashed-password.
|
|
347
|
"""
|
|
347
|
"""
|
|
348
|
)
|
|
348
|
)
|
|
349
|
|
|
349
|
|
|
350
|
open_browser = Bool(True, config=True,
|
|
350
|
open_browser = Bool(True, config=True,
|
|
351
|
help="""Whether to open in a browser after starting.
|
|
351
|
help="""Whether to open in a browser after starting.
|
|
352
|
The specific browser used is platform dependent and
|
|
352
|
The specific browser used is platform dependent and
|
|
353
|
determined by the python standard library `webbrowser`
|
|
353
|
determined by the python standard library `webbrowser`
|
|
354
|
module, unless it is overridden using the --browser
|
|
354
|
module, unless it is overridden using the --browser
|
|
355
|
(NotebookApp.browser) configuration option.
|
|
355
|
(NotebookApp.browser) configuration option.
|
|
356
|
""")
|
|
356
|
""")
|
|
357
|
|
|
357
|
|
|
358
|
browser = Unicode(u'', config=True,
|
|
358
|
browser = Unicode(u'', config=True,
|
|
359
|
help="""Specify what command to use to invoke a web
|
|
359
|
help="""Specify what command to use to invoke a web
|
|
360
|
browser when opening the notebook. If not specified, the
|
|
360
|
browser when opening the notebook. If not specified, the
|
|
361
|
default browser will be determined by the `webbrowser`
|
|
361
|
default browser will be determined by the `webbrowser`
|
|
362
|
standard library module, which allows setting of the
|
|
362
|
standard library module, which allows setting of the
|
|
363
|
BROWSER environment variable to override it.
|
|
363
|
BROWSER environment variable to override it.
|
|
364
|
""")
|
|
364
|
""")
|
|
365
|
|
|
365
|
|
|
366
|
read_only = Bool(False, config=True,
|
|
366
|
read_only = Bool(False, config=True,
|
|
367
|
help="Whether to prevent editing/execution of notebooks."
|
|
367
|
help="Whether to prevent editing/execution of notebooks."
|
|
368
|
)
|
|
368
|
)
|
|
369
|
|
|
369
|
|
|
370
|
use_less = Bool(False, config=True,
|
|
370
|
use_less = Bool(False, config=True,
|
|
371
|
help="""Wether to use Browser Side less-css parsing
|
|
371
|
help="""Wether to use Browser Side less-css parsing
|
|
372
|
instead of compiled css version in templates that allows
|
|
372
|
instead of compiled css version in templates that allows
|
|
373
|
it. This is mainly convenient when working on the less
|
|
373
|
it. This is mainly convenient when working on the less
|
|
374
|
file to avoid a build step, or if user want to overwrite
|
|
374
|
file to avoid a build step, or if user want to overwrite
|
|
375
|
some of the less variables without having to recompile
|
|
375
|
some of the less variables without having to recompile
|
|
376
|
everything.
|
|
376
|
everything.
|
|
377
|
|
|
377
|
|
|
378
|
You will need to install the less.js component in the static directory
|
|
378
|
You will need to install the less.js component in the static directory
|
|
379
|
either in the source tree or in your profile folder.
|
|
379
|
either in the source tree or in your profile folder.
|
|
380
|
""")
|
|
380
|
""")
|
|
381
|
|
|
381
|
|
|
382
|
webapp_settings = Dict(config=True,
|
|
382
|
webapp_settings = Dict(config=True,
|
|
383
|
help="Supply overrides for the tornado.web.Application that the "
|
|
383
|
help="Supply overrides for the tornado.web.Application that the "
|
|
384
|
"IPython notebook uses.")
|
|
384
|
"IPython notebook uses.")
|
|
385
|
|
|
385
|
|
|
386
|
enable_mathjax = Bool(True, config=True,
|
|
386
|
enable_mathjax = Bool(True, config=True,
|
|
387
|
help="""Whether to enable MathJax for typesetting math/TeX
|
|
387
|
help="""Whether to enable MathJax for typesetting math/TeX
|
|
388
|
|
|
388
|
|
|
389
|
MathJax is the javascript library IPython uses to render math/LaTeX. It is
|
|
389
|
MathJax is the javascript library IPython uses to render math/LaTeX. It is
|
|
390
|
very large, so you may want to disable it if you have a slow internet
|
|
390
|
very large, so you may want to disable it if you have a slow internet
|
|
391
|
connection, or for offline use of the notebook.
|
|
391
|
connection, or for offline use of the notebook.
|
|
392
|
|
|
392
|
|
|
393
|
When disabled, equations etc. will appear as their untransformed TeX source.
|
|
393
|
When disabled, equations etc. will appear as their untransformed TeX source.
|
|
394
|
"""
|
|
394
|
"""
|
|
395
|
)
|
|
395
|
)
|
|
396
|
def _enable_mathjax_changed(self, name, old, new):
|
|
396
|
def _enable_mathjax_changed(self, name, old, new):
|
|
397
|
"""set mathjax url to empty if mathjax is disabled"""
|
|
397
|
"""set mathjax url to empty if mathjax is disabled"""
|
|
398
|
if not new:
|
|
398
|
if not new:
|
|
399
|
self.mathjax_url = u''
|
|
399
|
self.mathjax_url = u''
|
|
400
|
|
|
400
|
|
|
401
|
base_project_url = Unicode('/', config=True,
|
|
401
|
base_project_url = Unicode('/', config=True,
|
|
402
|
help='''The base URL for the notebook server.
|
|
402
|
help='''The base URL for the notebook server.
|
|
403
|
|
|
403
|
|
|
404
|
Leading and trailing slashes can be omitted,
|
|
404
|
Leading and trailing slashes can be omitted,
|
|
405
|
and will automatically be added.
|
|
405
|
and will automatically be added.
|
|
406
|
''')
|
|
406
|
''')
|
|
407
|
def _base_project_url_changed(self, name, old, new):
|
|
407
|
def _base_project_url_changed(self, name, old, new):
|
|
408
|
if not new.startswith('/'):
|
|
408
|
if not new.startswith('/'):
|
|
409
|
self.base_project_url = '/'+new
|
|
409
|
self.base_project_url = '/'+new
|
|
410
|
elif not new.endswith('/'):
|
|
410
|
elif not new.endswith('/'):
|
|
411
|
self.base_project_url = new+'/'
|
|
411
|
self.base_project_url = new+'/'
|
|
412
|
|
|
412
|
|
|
413
|
base_kernel_url = Unicode('/', config=True,
|
|
413
|
base_kernel_url = Unicode('/', config=True,
|
|
414
|
help='''The base URL for the kernel server
|
|
414
|
help='''The base URL for the kernel server
|
|
415
|
|
|
415
|
|
|
416
|
Leading and trailing slashes can be omitted,
|
|
416
|
Leading and trailing slashes can be omitted,
|
|
417
|
and will automatically be added.
|
|
417
|
and will automatically be added.
|
|
418
|
''')
|
|
418
|
''')
|
|
419
|
def _base_kernel_url_changed(self, name, old, new):
|
|
419
|
def _base_kernel_url_changed(self, name, old, new):
|
|
420
|
if not new.startswith('/'):
|
|
420
|
if not new.startswith('/'):
|
|
421
|
self.base_kernel_url = '/'+new
|
|
421
|
self.base_kernel_url = '/'+new
|
|
422
|
elif not new.endswith('/'):
|
|
422
|
elif not new.endswith('/'):
|
|
423
|
self.base_kernel_url = new+'/'
|
|
423
|
self.base_kernel_url = new+'/'
|
|
424
|
|
|
424
|
|
|
425
|
websocket_host = Unicode("", config=True,
|
|
425
|
websocket_host = Unicode("", config=True,
|
|
426
|
help="""The hostname for the websocket server."""
|
|
426
|
help="""The hostname for the websocket server."""
|
|
427
|
)
|
|
427
|
)
|
|
428
|
|
|
428
|
|
|
429
|
extra_static_paths = List(Unicode, config=True,
|
|
429
|
extra_static_paths = List(Unicode, config=True,
|
|
430
|
help="""Extra paths to search for serving static files.
|
|
430
|
help="""Extra paths to search for serving static files.
|
|
431
|
|
|
431
|
|
|
432
|
This allows adding javascript/css to be available from the notebook server machine,
|
|
432
|
This allows adding javascript/css to be available from the notebook server machine,
|
|
433
|
or overriding individual files in the IPython"""
|
|
433
|
or overriding individual files in the IPython"""
|
|
434
|
)
|
|
434
|
)
|
|
435
|
def _extra_static_paths_default(self):
|
|
435
|
def _extra_static_paths_default(self):
|
|
436
|
return [os.path.join(self.profile_dir.location, 'static')]
|
|
436
|
return [os.path.join(self.profile_dir.location, 'static')]
|
|
437
|
|
|
437
|
|
|
438
|
@property
|
|
438
|
@property
|
|
439
|
def static_file_path(self):
|
|
439
|
def static_file_path(self):
|
|
440
|
"""return extra paths + the default location"""
|
|
440
|
"""return extra paths + the default location"""
|
|
441
|
return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
|
|
441
|
return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
|
|
442
|
|
|
442
|
|
|
443
|
mathjax_url = Unicode("", config=True,
|
|
443
|
mathjax_url = Unicode("", config=True,
|
|
444
|
help="""The url for MathJax.js."""
|
|
444
|
help="""The url for MathJax.js."""
|
|
445
|
)
|
|
445
|
)
|
|
446
|
def _mathjax_url_default(self):
|
|
446
|
def _mathjax_url_default(self):
|
|
447
|
if not self.enable_mathjax:
|
|
447
|
if not self.enable_mathjax:
|
|
448
|
return u''
|
|
448
|
return u''
|
|
449
|
static_url_prefix = self.webapp_settings.get("static_url_prefix",
|
|
449
|
static_url_prefix = self.webapp_settings.get("static_url_prefix",
|
|
450
|
"/static/")
|
|
450
|
"/static/")
|
|
451
|
try:
|
|
451
|
try:
|
|
452
|
mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
|
|
452
|
mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
|
|
453
|
except IOError:
|
|
453
|
except IOError:
|
|
454
|
if self.certfile:
|
|
454
|
if self.certfile:
|
|
455
|
# HTTPS: load from Rackspace CDN, because SSL certificate requires it
|
|
455
|
# HTTPS: load from Rackspace CDN, because SSL certificate requires it
|
|
456
|
base = u"https://c328740.ssl.cf1.rackcdn.com"
|
|
456
|
base = u"https://c328740.ssl.cf1.rackcdn.com"
|
|
457
|
else:
|
|
457
|
else:
|
|
458
|
base = u"http://cdn.mathjax.org"
|
|
458
|
base = u"http://cdn.mathjax.org"
|
|
459
|
|
|
459
|
|
|
460
|
url = base + u"/mathjax/latest/MathJax.js"
|
|
460
|
url = base + u"/mathjax/latest/MathJax.js"
|
|
461
|
self.log.info("Using MathJax from CDN: %s", url)
|
|
461
|
self.log.info("Using MathJax from CDN: %s", url)
|
|
462
|
return url
|
|
462
|
return url
|
|
463
|
else:
|
|
463
|
else:
|
|
464
|
self.log.info("Using local MathJax from %s" % mathjax)
|
|
464
|
self.log.info("Using local MathJax from %s" % mathjax)
|
|
465
|
return static_url_prefix+u"mathjax/MathJax.js"
|
|
465
|
return static_url_prefix+u"mathjax/MathJax.js"
|
|
466
|
|
|
466
|
|
|
467
|
def _mathjax_url_changed(self, name, old, new):
|
|
467
|
def _mathjax_url_changed(self, name, old, new):
|
|
468
|
if new and not self.enable_mathjax:
|
|
468
|
if new and not self.enable_mathjax:
|
|
469
|
# enable_mathjax=False overrides mathjax_url
|
|
469
|
# enable_mathjax=False overrides mathjax_url
|
|
470
|
self.mathjax_url = u''
|
|
470
|
self.mathjax_url = u''
|
|
471
|
else:
|
|
471
|
else:
|
|
472
|
self.log.info("Using MathJax: %s", new)
|
|
472
|
self.log.info("Using MathJax: %s", new)
|
|
473
|
|
|
473
|
|
|
474
|
notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
|
|
474
|
notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
|
|
475
|
config=True,
|
|
475
|
config=True,
|
|
476
|
help='The notebook manager class to use.')
|
|
476
|
help='The notebook manager class to use.')
|
|
477
|
|
|
477
|
|
|
478
|
trust_xheaders = Bool(False, config=True,
|
|
478
|
trust_xheaders = Bool(False, config=True,
|
|
479
|
help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
|
|
479
|
help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
|
|
480
|
"sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
|
|
480
|
"sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
|
|
481
|
)
|
|
481
|
)
|
|
482
|
|
|
482
|
|
|
483
|
def parse_command_line(self, argv=None):
|
|
483
|
def parse_command_line(self, argv=None):
|
|
484
|
super(NotebookApp, self).parse_command_line(argv)
|
|
484
|
super(NotebookApp, self).parse_command_line(argv)
|
|
485
|
if argv is None:
|
|
485
|
if argv is None:
|
|
486
|
argv = sys.argv[1:]
|
|
486
|
argv = sys.argv[1:]
|
|
487
|
|
|
487
|
|
|
488
|
# Scrub frontend-specific flags
|
|
488
|
# Scrub frontend-specific flags
|
|
489
|
self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
|
|
489
|
self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
|
|
490
|
# Kernel should inherit default config file from frontend
|
|
490
|
# Kernel should inherit default config file from frontend
|
|
491
|
self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
|
|
491
|
self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
|
|
492
|
|
|
492
|
|
|
493
|
if self.extra_args:
|
|
493
|
if self.extra_args:
|
|
494
|
f = os.path.abspath(self.extra_args[0])
|
|
494
|
f = os.path.abspath(self.extra_args[0])
|
|
495
|
if os.path.isdir(f):
|
|
495
|
if os.path.isdir(f):
|
|
496
|
nbdir = f
|
|
496
|
nbdir = f
|
|
497
|
else:
|
|
497
|
else:
|
|
498
|
self.file_to_run = f
|
|
498
|
self.file_to_run = f
|
|
499
|
nbdir = os.path.dirname(f)
|
|
499
|
nbdir = os.path.dirname(f)
|
|
500
|
self.config.NotebookManager.notebook_dir = nbdir
|
|
500
|
self.config.NotebookManager.notebook_dir = nbdir
|
|
501
|
|
|
501
|
|
|
502
|
def init_configurables(self):
|
|
502
|
def init_configurables(self):
|
|
503
|
# force Session default to be secure
|
|
503
|
# force Session default to be secure
|
|
504
|
default_secure(self.config)
|
|
504
|
default_secure(self.config)
|
|
505
|
self.kernel_manager = MappingKernelManager(
|
|
505
|
self.kernel_manager = MappingKernelManager(
|
|
506
|
config=self.config, log=self.log, kernel_argv=self.kernel_argv,
|
|
506
|
config=self.config, log=self.log, kernel_argv=self.kernel_argv,
|
|
507
|
connection_dir = self.profile_dir.security_dir,
|
|
507
|
connection_dir = self.profile_dir.security_dir,
|
|
508
|
)
|
|
508
|
)
|
|
509
|
kls = import_item(self.notebook_manager_class)
|
|
509
|
kls = import_item(self.notebook_manager_class)
|
|
510
|
self.notebook_manager = kls(config=self.config, log=self.log)
|
|
510
|
self.notebook_manager = kls(config=self.config, log=self.log)
|
|
511
|
self.notebook_manager.load_notebook_names()
|
|
511
|
self.notebook_manager.load_notebook_names()
|
|
512
|
self.cluster_manager = ClusterManager(config=self.config, log=self.log)
|
|
512
|
self.cluster_manager = ClusterManager(config=self.config, log=self.log)
|
|
513
|
self.cluster_manager.update_profiles()
|
|
513
|
self.cluster_manager.update_profiles()
|
|
514
|
|
|
514
|
|
|
515
|
def init_logging(self):
|
|
515
|
def init_logging(self):
|
|
516
|
# This prevents double log messages because tornado use a root logger that
|
|
516
|
# This prevents double log messages because tornado use a root logger that
|
|
517
|
# self.log is a child of. The logging module dipatches log messages to a log
|
|
517
|
# self.log is a child of. The logging module dipatches log messages to a log
|
|
518
|
# and all of its ancenstors until propagate is set to False.
|
|
518
|
# and all of its ancenstors until propagate is set to False.
|
|
519
|
self.log.propagate = False
|
|
519
|
self.log.propagate = False
|
|
520
|
|
|
520
|
|
|
521
|
def init_webapp(self):
|
|
521
|
def init_webapp(self):
|
|
522
|
"""initialize tornado webapp and httpserver"""
|
|
522
|
"""initialize tornado webapp and httpserver"""
|
|
523
|
self.web_app = NotebookWebApplication(
|
|
523
|
self.web_app = NotebookWebApplication(
|
|
524
|
self, self.kernel_manager, self.notebook_manager,
|
|
524
|
self, self.kernel_manager, self.notebook_manager,
|
|
525
|
self.cluster_manager, self.log,
|
|
525
|
self.cluster_manager, self.log,
|
|
526
|
self.base_project_url, self.webapp_settings
|
|
526
|
self.base_project_url, self.webapp_settings
|
|
527
|
)
|
|
527
|
)
|
|
528
|
if self.certfile:
|
|
528
|
if self.certfile:
|
|
529
|
ssl_options = dict(certfile=self.certfile)
|
|
529
|
ssl_options = dict(certfile=self.certfile)
|
|
530
|
if self.keyfile:
|
|
530
|
if self.keyfile:
|
|
531
|
ssl_options['keyfile'] = self.keyfile
|
|
531
|
ssl_options['keyfile'] = self.keyfile
|
|
532
|
else:
|
|
532
|
else:
|
|
533
|
ssl_options = None
|
|
533
|
ssl_options = None
|
|
534
|
self.web_app.password = self.password
|
|
534
|
self.web_app.password = self.password
|
|
535
|
self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
|
|
535
|
self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
|
|
536
|
xheaders=self.trust_xheaders)
|
|
536
|
xheaders=self.trust_xheaders)
|
|
537
|
if not self.ip:
|
|
537
|
if not self.ip:
|
|
538
|
warning = "WARNING: The notebook server is listening on all IP addresses"
|
|
538
|
warning = "WARNING: The notebook server is listening on all IP addresses"
|
|
539
|
if ssl_options is None:
|
|
539
|
if ssl_options is None:
|
|
540
|
self.log.critical(warning + " and not using encryption. This"
|
|
540
|
self.log.critical(warning + " and not using encryption. This"
|
|
541
|
"is not recommended.")
|
|
541
|
"is not recommended.")
|
|
542
|
if not self.password and not self.read_only:
|
|
542
|
if not self.password and not self.read_only:
|
|
543
|
self.log.critical(warning + "and not using authentication."
|
|
543
|
self.log.critical(warning + "and not using authentication."
|
|
544
|
"This is highly insecure and not recommended.")
|
|
544
|
"This is highly insecure and not recommended.")
|
|
545
|
success = None
|
|
545
|
success = None
|
|
546
|
for port in random_ports(self.port, self.port_retries+1):
|
|
546
|
for port in random_ports(self.port, self.port_retries+1):
|
|
547
|
try:
|
|
547
|
try:
|
|
548
|
self.http_server.listen(port, self.ip)
|
|
548
|
self.http_server.listen(port, self.ip)
|
|
549
|
except socket.error as e:
|
|
549
|
except socket.error as e:
|
|
550
|
# XXX: remove the e.errno == -9 block when we require
|
|
550
|
# XXX: remove the e.errno == -9 block when we require
|
|
551
|
# tornado >= 3.0
|
|
551
|
# tornado >= 3.0
|
|
552
|
if e.errno == -9 and tornado.version_info[0] < 3:
|
|
552
|
if e.errno == -9 and tornado.version_info[0] < 3:
|
|
553
|
# The flags passed to socket.getaddrinfo from
|
|
553
|
# The flags passed to socket.getaddrinfo from
|
|
554
|
# tornado.netutils.bind_sockets can cause "gaierror:
|
|
554
|
# tornado.netutils.bind_sockets can cause "gaierror:
|
|
555
|
# [Errno -9] Address family for hostname not supported"
|
|
555
|
# [Errno -9] Address family for hostname not supported"
|
|
556
|
# when the interface is not associated, for example.
|
|
556
|
# when the interface is not associated, for example.
|
|
557
|
# Changing the flags to exclude socket.AI_ADDRCONFIG does
|
|
557
|
# Changing the flags to exclude socket.AI_ADDRCONFIG does
|
|
558
|
# not cause this error, but the only way to do this is to
|
|
558
|
# not cause this error, but the only way to do this is to
|
|
559
|
# monkeypatch socket to remove the AI_ADDRCONFIG attribute
|
|
559
|
# monkeypatch socket to remove the AI_ADDRCONFIG attribute
|
|
560
|
saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
|
|
560
|
saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
|
|
561
|
self.log.warn('Monkeypatching socket to fix tornado bug')
|
|
561
|
self.log.warn('Monkeypatching socket to fix tornado bug')
|
|
562
|
del(socket.AI_ADDRCONFIG)
|
|
562
|
del(socket.AI_ADDRCONFIG)
|
|
563
|
try:
|
|
563
|
try:
|
|
564
|
# retry the tornado call without AI_ADDRCONFIG flags
|
|
564
|
# retry the tornado call without AI_ADDRCONFIG flags
|
|
565
|
self.http_server.listen(port, self.ip)
|
|
565
|
self.http_server.listen(port, self.ip)
|
|
566
|
except socket.error as e2:
|
|
566
|
except socket.error as e2:
|
|
567
|
e = e2
|
|
567
|
e = e2
|
|
568
|
else:
|
|
568
|
else:
|
|
569
|
self.port = port
|
|
569
|
self.port = port
|
|
570
|
success = True
|
|
570
|
success = True
|
|
571
|
break
|
|
571
|
break
|
|
572
|
# restore the monekypatch
|
|
572
|
# restore the monekypatch
|
|
573
|
socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
|
|
573
|
socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
|
|
574
|
if e.errno != errno.EADDRINUSE:
|
|
574
|
if e.errno != errno.EADDRINUSE:
|
|
575
|
raise
|
|
575
|
raise
|
|
576
|
self.log.info('The port %i is already in use, trying another random port.' % port)
|
|
576
|
self.log.info('The port %i is already in use, trying another random port.' % port)
|
|
577
|
else:
|
|
577
|
else:
|
|
578
|
self.port = port
|
|
578
|
self.port = port
|
|
579
|
success = True
|
|
579
|
success = True
|
|
580
|
break
|
|
580
|
break
|
|
581
|
if not success:
|
|
581
|
if not success:
|
|
582
|
self.log.critical('ERROR: the notebook server could not be started because '
|
|
582
|
self.log.critical('ERROR: the notebook server could not be started because '
|
|
583
|
'no available port could be found.')
|
|
583
|
'no available port could be found.')
|
|
584
|
self.exit(1)
|
|
584
|
self.exit(1)
|
|
585
|
|
|
585
|
|
|
586
|
def init_signal(self):
|
|
586
|
def init_signal(self):
|
|
587
|
if not sys.platform.startswith('win'):
|
|
587
|
if not sys.platform.startswith('win'):
|
|
588
|
signal.signal(signal.SIGINT, self._handle_sigint)
|
|
588
|
signal.signal(signal.SIGINT, self._handle_sigint)
|
|
589
|
signal.signal(signal.SIGTERM, self._signal_stop)
|
|
589
|
signal.signal(signal.SIGTERM, self._signal_stop)
|
|
590
|
if hasattr(signal, 'SIGUSR1'):
|
|
590
|
if hasattr(signal, 'SIGUSR1'):
|
|
591
|
# Windows doesn't support SIGUSR1
|
|
591
|
# Windows doesn't support SIGUSR1
|
|
592
|
signal.signal(signal.SIGUSR1, self._signal_info)
|
|
592
|
signal.signal(signal.SIGUSR1, self._signal_info)
|
|
593
|
if hasattr(signal, 'SIGINFO'):
|
|
593
|
if hasattr(signal, 'SIGINFO'):
|
|
594
|
# only on BSD-based systems
|
|
594
|
# only on BSD-based systems
|
|
595
|
signal.signal(signal.SIGINFO, self._signal_info)
|
|
595
|
signal.signal(signal.SIGINFO, self._signal_info)
|
|
596
|
|
|
596
|
|
|
597
|
def _handle_sigint(self, sig, frame):
|
|
597
|
def _handle_sigint(self, sig, frame):
|
|
598
|
"""SIGINT handler spawns confirmation dialog"""
|
|
598
|
"""SIGINT handler spawns confirmation dialog"""
|
|
599
|
# register more forceful signal handler for ^C^C case
|
|
599
|
# register more forceful signal handler for ^C^C case
|
|
600
|
signal.signal(signal.SIGINT, self._signal_stop)
|
|
600
|
signal.signal(signal.SIGINT, self._signal_stop)
|
|
601
|
# request confirmation dialog in bg thread, to avoid
|
|
601
|
# request confirmation dialog in bg thread, to avoid
|
|
602
|
# blocking the App
|
|
602
|
# blocking the App
|
|
603
|
thread = threading.Thread(target=self._confirm_exit)
|
|
603
|
thread = threading.Thread(target=self._confirm_exit)
|
|
604
|
thread.daemon = True
|
|
604
|
thread.daemon = True
|
|
605
|
thread.start()
|
|
605
|
thread.start()
|
|
606
|
|
|
606
|
|
|
607
|
def _restore_sigint_handler(self):
|
|
607
|
def _restore_sigint_handler(self):
|
|
608
|
"""callback for restoring original SIGINT handler"""
|
|
608
|
"""callback for restoring original SIGINT handler"""
|
|
609
|
signal.signal(signal.SIGINT, self._handle_sigint)
|
|
609
|
signal.signal(signal.SIGINT, self._handle_sigint)
|
|
610
|
|
|
610
|
|
|
611
|
def _confirm_exit(self):
|
|
611
|
def _confirm_exit(self):
|
|
612
|
"""confirm shutdown on ^C
|
|
612
|
"""confirm shutdown on ^C
|
|
613
|
|
|
613
|
|
|
614
|
A second ^C, or answering 'y' within 5s will cause shutdown,
|
|
614
|
A second ^C, or answering 'y' within 5s will cause shutdown,
|
|
615
|
otherwise original SIGINT handler will be restored.
|
|
615
|
otherwise original SIGINT handler will be restored.
|
|
616
|
|
|
616
|
|
|
617
|
This doesn't work on Windows.
|
|
617
|
This doesn't work on Windows.
|
|
618
|
"""
|
|
618
|
"""
|
|
619
|
# FIXME: remove this delay when pyzmq dependency is >= 2.1.11
|
|
619
|
# FIXME: remove this delay when pyzmq dependency is >= 2.1.11
|
|
620
|
time.sleep(0.1)
|
|
620
|
time.sleep(0.1)
|
|
621
|
info = self.log.info
|
|
621
|
info = self.log.info
|
|
622
|
info('interrupted')
|
|
622
|
info('interrupted')
|
|
623
|
print self.notebook_info()
|
|
623
|
print self.notebook_info()
|
|
624
|
sys.stdout.write("Shutdown this notebook server (y/[n])? ")
|
|
624
|
sys.stdout.write("Shutdown this notebook server (y/[n])? ")
|
|
625
|
sys.stdout.flush()
|
|
625
|
sys.stdout.flush()
|
|
626
|
r,w,x = select.select([sys.stdin], [], [], 5)
|
|
626
|
r,w,x = select.select([sys.stdin], [], [], 5)
|
|
627
|
if r:
|
|
627
|
if r:
|
|
628
|
line = sys.stdin.readline()
|
|
628
|
line = sys.stdin.readline()
|
|
629
|
if line.lower().startswith('y'):
|
|
629
|
if line.lower().startswith('y'):
|
|
630
|
self.log.critical("Shutdown confirmed")
|
|
630
|
self.log.critical("Shutdown confirmed")
|
|
631
|
ioloop.IOLoop.instance().stop()
|
|
631
|
ioloop.IOLoop.instance().stop()
|
|
632
|
return
|
|
632
|
return
|
|
633
|
else:
|
|
633
|
else:
|
|
634
|
print "No answer for 5s:",
|
|
634
|
print "No answer for 5s:",
|
|
635
|
print "resuming operation..."
|
|
635
|
print "resuming operation..."
|
|
636
|
# no answer, or answer is no:
|
|
636
|
# no answer, or answer is no:
|
|
637
|
# set it back to original SIGINT handler
|
|
637
|
# set it back to original SIGINT handler
|
|
638
|
# use IOLoop.add_callback because signal.signal must be called
|
|
638
|
# use IOLoop.add_callback because signal.signal must be called
|
|
639
|
# from main thread
|
|
639
|
# from main thread
|
|
640
|
ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
|
|
640
|
ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
|
|
641
|
|
|
641
|
|
|
642
|
def _signal_stop(self, sig, frame):
|
|
642
|
def _signal_stop(self, sig, frame):
|
|
643
|
self.log.critical("received signal %s, stopping", sig)
|
|
643
|
self.log.critical("received signal %s, stopping", sig)
|
|
644
|
ioloop.IOLoop.instance().stop()
|
|
644
|
ioloop.IOLoop.instance().stop()
|
|
645
|
|
|
645
|
|
|
646
|
def _signal_info(self, sig, frame):
|
|
646
|
def _signal_info(self, sig, frame):
|
|
647
|
print self.notebook_info()
|
|
647
|
print self.notebook_info()
|
|
648
|
|
|
648
|
|
|
649
|
@catch_config_error
|
|
649
|
@catch_config_error
|
|
650
|
def initialize(self, argv=None):
|
|
650
|
def initialize(self, argv=None):
|
|
651
|
self.init_logging()
|
|
651
|
self.init_logging()
|
|
652
|
super(NotebookApp, self).initialize(argv)
|
|
652
|
super(NotebookApp, self).initialize(argv)
|
|
653
|
self.init_configurables()
|
|
653
|
self.init_configurables()
|
|
654
|
self.init_webapp()
|
|
654
|
self.init_webapp()
|
|
655
|
self.init_signal()
|
|
655
|
self.init_signal()
|
|
656
|
|
|
656
|
|
|
657
|
def cleanup_kernels(self):
|
|
657
|
def cleanup_kernels(self):
|
|
658
|
"""Shutdown all kernels.
|
|
658
|
"""Shutdown all kernels.
|
|
659
|
|
|
659
|
|
|
660
|
The kernels will shutdown themselves when this process no longer exists,
|
|
660
|
The kernels will shutdown themselves when this process no longer exists,
|
|
661
|
but explicit shutdown allows the KernelManagers to cleanup the connection files.
|
|
661
|
but explicit shutdown allows the KernelManagers to cleanup the connection files.
|
|
662
|
"""
|
|
662
|
"""
|
|
663
|
self.log.info('Shutting down kernels')
|
|
663
|
self.log.info('Shutting down kernels')
|
|
664
|
self.kernel_manager.shutdown_all()
|
|
664
|
self.kernel_manager.shutdown_all()
|
|
665
|
|
|
665
|
|
|
666
|
def notebook_info(self):
|
|
666
|
def notebook_info(self):
|
|
667
|
"Return the current working directory and the server url information"
|
|
667
|
"Return the current working directory and the server url information"
|
|
668
|
mgr_info = self.notebook_manager.info_string() + "\n"
|
|
668
|
mgr_info = self.notebook_manager.info_string() + "\n"
|
|
669
|
return mgr_info +"The IPython Notebook is running at: %s" % self._url
|
|
669
|
return mgr_info +"The IPython Notebook is running at: %s" % self._url
|
|
670
|
|
|
670
|
|
|
671
|
def start(self):
|
|
671
|
def start(self):
|
|
672
|
""" Start the IPython Notebok server app, after initialization
|
|
672
|
""" Start the IPython Notebok server app, after initialization
|
|
673
|
|
|
673
|
|
|
674
|
This method takes no arguments so all configuration and initialization
|
|
674
|
This method takes no arguments so all configuration and initialization
|
|
675
|
must be done prior to calling this method."""
|
|
675
|
must be done prior to calling this method."""
|
|
676
|
ip = self.ip if self.ip else '[all ip addresses on your system]'
|
|
676
|
ip = self.ip if self.ip else '[all ip addresses on your system]'
|
|
677
|
proto = 'https' if self.certfile else 'http'
|
|
677
|
proto = 'https' if self.certfile else 'http'
|
|
678
|
info = self.log.info
|
|
678
|
info = self.log.info
|
|
679
|
self._url = "%s://%s:%i%s" % (proto, ip, self.port,
|
|
679
|
self._url = "%s://%s:%i%s" % (proto, ip, self.port,
|
|
680
|
self.base_project_url)
|
|
680
|
self.base_project_url)
|
|
681
|
for line in self.notebook_info().split("\n"):
|
|
681
|
for line in self.notebook_info().split("\n"):
|
|
682
|
info(line)
|
|
682
|
info(line)
|
|
683
|
info("Use Control-C to stop this server and shut down all kernels.")
|
|
683
|
info("Use Control-C to stop this server and shut down all kernels.")
|
|
684
|
|
|
684
|
|
|
685
|
if self.open_browser or self.file_to_run:
|
|
685
|
if self.open_browser or self.file_to_run:
|
|
686
|
ip = self.ip or LOCALHOST
|
|
686
|
ip = self.ip or LOCALHOST
|
|
687
|
try:
|
|
687
|
try:
|
|
688
|
browser = webbrowser.get(self.browser or None)
|
|
688
|
browser = webbrowser.get(self.browser or None)
|
|
689
|
except webbrowser.Error as e:
|
|
689
|
except webbrowser.Error as e:
|
|
690
|
self.log.warn('No web browser found: %s.' % e)
|
|
690
|
self.log.warn('No web browser found: %s.' % e)
|
|
691
|
browser = None
|
|
691
|
browser = None
|
|
692
|
|
|
692
|
|
|
693
|
if self.file_to_run:
|
|
693
|
if self.file_to_run:
|
|
694
|
name, _ = os.path.splitext(os.path.basename(self.file_to_run))
|
|
694
|
name, _ = os.path.splitext(os.path.basename(self.file_to_run))
|
|
695
|
url = self.notebook_manager.rev_mapping.get(name, '')
|
|
695
|
url = self.notebook_manager.rev_mapping.get(name, '')
|
|
696
|
else:
|
|
696
|
else:
|
|
697
|
url = ''
|
|
697
|
url = ''
|
|
698
|
if browser:
|
|
698
|
if browser:
|
|
699
|
b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
|
|
699
|
b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
|
|
700
|
self.port, self.base_project_url, url), new=2)
|
|
700
|
self.port, self.base_project_url, url), new=2)
|
|
701
|
threading.Thread(target=b).start()
|
|
701
|
threading.Thread(target=b).start()
|
|
702
|
try:
|
|
702
|
try:
|
|
703
|
ioloop.IOLoop.instance().start()
|
|
703
|
ioloop.IOLoop.instance().start()
|
|
704
|
except KeyboardInterrupt:
|
|
704
|
except KeyboardInterrupt:
|
|
705
|
info("Interrupted...")
|
|
705
|
info("Interrupted...")
|
|
706
|
finally:
|
|
706
|
finally:
|
|
707
|
self.cleanup_kernels()
|
|
707
|
self.cleanup_kernels()
|
|
708
|
|
|
708
|
|
|
709
|
|
|
709
|
|
|
710
|
#-----------------------------------------------------------------------------
|
|
710
|
#-----------------------------------------------------------------------------
|
|
711
|
# Main entry point
|
|
711
|
# Main entry point
|
|
712
|
#-----------------------------------------------------------------------------
|
|
712
|
#-----------------------------------------------------------------------------
|
|
713
|
|
|
713
|
|
|
714
|
def launch_new_instance():
|
|
714
|
def launch_new_instance():
|
|
715
|
app = NotebookApp.instance()
|
|
715
|
app = NotebookApp.instance()
|
|
716
|
app.initialize()
|
|
716
|
app.initialize()
|
|
717
|
app.start()
|
|
717
|
app.start()
|
|
718
|
|
|
718
|
|