##// END OF EJS Templates
Merge pull request #4303 from ipython/multidir...
Min RK -
r13184:1274dc14 merge
parent child Browse files
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,124 b''
1 """Test the kernels service API."""
2
3
4 import os
5 import sys
6 import json
7
8 import requests
9
10 from IPython.html.utils import url_path_join
11 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
12
13 class KernelAPI(object):
14 """Wrapper for kernel REST API requests"""
15 def __init__(self, base_url):
16 self.base_url = base_url
17
18 def _req(self, verb, path, body=None):
19 response = requests.request(verb,
20 url_path_join(self.base_url, 'api/kernels', path), data=body)
21
22 if 400 <= response.status_code < 600:
23 try:
24 response.reason = response.json()['message']
25 except:
26 pass
27 response.raise_for_status()
28
29 return response
30
31 def list(self):
32 return self._req('GET', '')
33
34 def get(self, id):
35 return self._req('GET', id)
36
37 def start(self):
38 return self._req('POST', '')
39
40 def shutdown(self, id):
41 return self._req('DELETE', id)
42
43 def interrupt(self, id):
44 return self._req('POST', url_path_join(id, 'interrupt'))
45
46 def restart(self, id):
47 return self._req('POST', url_path_join(id, 'restart'))
48
49 class KernelAPITest(NotebookTestBase):
50 """Test the kernels web service API"""
51 def setUp(self):
52 self.kern_api = KernelAPI(self.base_url())
53
54 def tearDown(self):
55 for k in self.kern_api.list().json():
56 self.kern_api.shutdown(k['id'])
57
58 def test__no_kernels(self):
59 """Make sure there are no kernels running at the start"""
60 kernels = self.kern_api.list().json()
61 self.assertEqual(kernels, [])
62
63 def test_main_kernel_handler(self):
64 # POST request
65 r = self.kern_api.start()
66 kern1 = r.json()
67 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
68 self.assertEqual(r.status_code, 201)
69 self.assertIsInstance(kern1, dict)
70
71 # GET request
72 r = self.kern_api.list()
73 self.assertEqual(r.status_code, 200)
74 assert isinstance(r.json(), list)
75 self.assertEqual(r.json()[0]['id'], kern1['id'])
76
77 # create another kernel and check that they both are added to the
78 # list of kernels from a GET request
79 kern2 = self.kern_api.start().json()
80 assert isinstance(kern2, dict)
81 r = self.kern_api.list()
82 kernels = r.json()
83 self.assertEqual(r.status_code, 200)
84 assert isinstance(kernels, list)
85 self.assertEqual(len(kernels), 2)
86
87 # Interrupt a kernel
88 r = self.kern_api.interrupt(kern2['id'])
89 self.assertEqual(r.status_code, 204)
90
91 # Restart a kernel
92 r = self.kern_api.restart(kern2['id'])
93 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
94 rekern = r.json()
95 self.assertEqual(rekern['id'], kern2['id'])
96 self.assertIn('ws_url', rekern)
97
98 def test_kernel_handler(self):
99 # GET kernel with given id
100 kid = self.kern_api.start().json()['id']
101 r = self.kern_api.get(kid)
102 kern1 = r.json()
103 self.assertEqual(r.status_code, 200)
104 assert isinstance(kern1, dict)
105 self.assertIn('id', kern1)
106 self.assertIn('ws_url', kern1)
107 self.assertEqual(kern1['id'], kid)
108
109 # Request a bad kernel id and check that a JSON
110 # message is returned!
111 bad_id = '111-111-111-111-111'
112 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
113 self.kern_api.get(bad_id)
114
115 # DELETE kernel with id
116 r = self.kern_api.shutdown(kid)
117 self.assertEqual(r.status_code, 204)
118 kernels = self.kern_api.list().json()
119 self.assertEqual(kernels, [])
120
121 # Request to delete a non-existent kernel id
122 bad_id = '111-111-111-111-111'
123 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
124 self.kern_api.shutdown(bad_id)
@@ -0,0 +1,318 b''
1 # coding: utf-8
2 """Test the notebooks webservice API."""
3
4 import io
5 import json
6 import os
7 import shutil
8 from unicodedata import normalize
9
10 pjoin = os.path.join
11
12 import requests
13
14 from IPython.html.utils import url_path_join, url_escape
15 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 from IPython.nbformat import current
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 new_heading_cell, to_notebook_json)
19 from IPython.nbformat import v2
20 from IPython.utils import py3compat
21 from IPython.utils.data import uniq_stable
22
23
24 class NBAPI(object):
25 """Wrapper for notebook API calls."""
26 def __init__(self, base_url):
27 self.base_url = base_url
28
29 def _req(self, verb, path, body=None):
30 response = requests.request(verb,
31 url_path_join(self.base_url, 'api/notebooks', path),
32 data=body,
33 )
34 response.raise_for_status()
35 return response
36
37 def list(self, path='/'):
38 return self._req('GET', path)
39
40 def read(self, name, path='/'):
41 return self._req('GET', url_path_join(path, name))
42
43 def create_untitled(self, path='/'):
44 return self._req('POST', path)
45
46 def upload_untitled(self, body, path='/'):
47 return self._req('POST', path, body)
48
49 def copy_untitled(self, copy_from, path='/'):
50 body = json.dumps({'copy_from':copy_from})
51 return self._req('POST', path, body)
52
53 def create(self, name, path='/'):
54 return self._req('PUT', url_path_join(path, name))
55
56 def upload(self, name, body, path='/'):
57 return self._req('PUT', url_path_join(path, name), body)
58
59 def copy(self, copy_from, copy_to, path='/'):
60 body = json.dumps({'copy_from':copy_from})
61 return self._req('PUT', url_path_join(path, copy_to), body)
62
63 def save(self, name, body, path='/'):
64 return self._req('PUT', url_path_join(path, name), body)
65
66 def delete(self, name, path='/'):
67 return self._req('DELETE', url_path_join(path, name))
68
69 def rename(self, name, path, new_name):
70 body = json.dumps({'name': new_name})
71 return self._req('PATCH', url_path_join(path, name), body)
72
73 def get_checkpoints(self, name, path):
74 return self._req('GET', url_path_join(path, name, 'checkpoints'))
75
76 def new_checkpoint(self, name, path):
77 return self._req('POST', url_path_join(path, name, 'checkpoints'))
78
79 def restore_checkpoint(self, name, path, checkpoint_id):
80 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
81
82 def delete_checkpoint(self, name, path, checkpoint_id):
83 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
84
85 class APITest(NotebookTestBase):
86 """Test the kernels web service API"""
87 dirs_nbs = [('', 'inroot'),
88 ('Directory with spaces in', 'inspace'),
89 (u'unicodΓ©', 'innonascii'),
90 ('foo', 'a'),
91 ('foo', 'b'),
92 ('foo', 'name with spaces'),
93 ('foo', u'unicodΓ©'),
94 ('foo/bar', 'baz'),
95 (u'Γ₯ b', u'Γ§ d')
96 ]
97
98 dirs = uniq_stable([d for (d,n) in dirs_nbs])
99 del dirs[0] # remove ''
100
101 def setUp(self):
102 nbdir = self.notebook_dir.name
103
104 for d in self.dirs:
105 d.replace('/', os.sep)
106 if not os.path.isdir(pjoin(nbdir, d)):
107 os.mkdir(pjoin(nbdir, d))
108
109 for d, name in self.dirs_nbs:
110 d = d.replace('/', os.sep)
111 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
112 nb = new_notebook(name=name)
113 write(nb, f, format='ipynb')
114
115 self.nb_api = NBAPI(self.base_url())
116
117 def tearDown(self):
118 nbdir = self.notebook_dir.name
119
120 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©', u'Γ₯ b']:
121 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
122
123 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
124 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
125
126 def test_list_notebooks(self):
127 nbs = self.nb_api.list().json()
128 self.assertEqual(len(nbs), 1)
129 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
130
131 nbs = self.nb_api.list('/Directory with spaces in/').json()
132 self.assertEqual(len(nbs), 1)
133 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
134
135 nbs = self.nb_api.list(u'/unicodΓ©/').json()
136 self.assertEqual(len(nbs), 1)
137 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
138 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
139
140 nbs = self.nb_api.list('/foo/bar/').json()
141 self.assertEqual(len(nbs), 1)
142 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
143 self.assertEqual(nbs[0]['path'], 'foo/bar')
144
145 nbs = self.nb_api.list('foo').json()
146 self.assertEqual(len(nbs), 4)
147 nbnames = { normalize('NFC', n['name']) for n in nbs }
148 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
149 expected = { normalize('NFC', name) for name in expected }
150 self.assertEqual(nbnames, expected)
151
152 def test_list_nonexistant_dir(self):
153 with assert_http_error(404):
154 self.nb_api.list('nonexistant')
155
156 def test_get_contents(self):
157 for d, name in self.dirs_nbs:
158 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
159 self.assertEqual(nb['name'], u'%s.ipynb' % name)
160 self.assertIn('content', nb)
161 self.assertIn('metadata', nb['content'])
162 self.assertIsInstance(nb['content']['metadata'], dict)
163
164 # Name that doesn't exist - should be a 404
165 with assert_http_error(404):
166 self.nb_api.read('q.ipynb', 'foo')
167
168 def _check_nb_created(self, resp, name, path):
169 self.assertEqual(resp.status_code, 201)
170 location_header = py3compat.str_to_unicode(resp.headers['Location'])
171 self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
172 self.assertEqual(resp.json()['name'], name)
173 assert os.path.isfile(pjoin(
174 self.notebook_dir.name,
175 path.replace('/', os.sep),
176 name,
177 ))
178
179 def test_create_untitled(self):
180 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
181 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
182
183 # Second time
184 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
185 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
186
187 # And two directories down
188 resp = self.nb_api.create_untitled(path='foo/bar')
189 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
190
191 def test_upload_untitled(self):
192 nb = new_notebook(name='Upload test')
193 nbmodel = {'content': nb}
194 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
195 body=json.dumps(nbmodel))
196 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
197
198 def test_upload(self):
199 nb = new_notebook(name=u'ignored')
200 nbmodel = {'content': nb}
201 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
202 body=json.dumps(nbmodel))
203 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
204
205 def test_upload_v2(self):
206 nb = v2.new_notebook()
207 ws = v2.new_worksheet()
208 nb.worksheets.append(ws)
209 ws.cells.append(v2.new_code_cell(input='print("hi")'))
210 nbmodel = {'content': nb}
211 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
212 body=json.dumps(nbmodel))
213 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
214 resp = self.nb_api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
215 data = resp.json()
216 self.assertEqual(data['content']['nbformat'], current.nbformat)
217 self.assertEqual(data['content']['orig_nbformat'], 2)
218
219 def test_copy_untitled(self):
220 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
221 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
222
223 def test_copy(self):
224 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
225 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
226
227 def test_delete(self):
228 for d, name in self.dirs_nbs:
229 resp = self.nb_api.delete('%s.ipynb' % name, d)
230 self.assertEqual(resp.status_code, 204)
231
232 for d in self.dirs + ['/']:
233 nbs = self.nb_api.list(d).json()
234 self.assertEqual(len(nbs), 0)
235
236 def test_rename(self):
237 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
238 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
239 self.assertEqual(resp.json()['name'], 'z.ipynb')
240 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
241
242 nbs = self.nb_api.list('foo').json()
243 nbnames = set(n['name'] for n in nbs)
244 self.assertIn('z.ipynb', nbnames)
245 self.assertNotIn('a.ipynb', nbnames)
246
247 def test_save(self):
248 resp = self.nb_api.read('a.ipynb', 'foo')
249 nbcontent = json.loads(resp.text)['content']
250 nb = to_notebook_json(nbcontent)
251 ws = new_worksheet()
252 nb.worksheets = [ws]
253 ws.cells.append(new_heading_cell(u'Created by test Β³'))
254
255 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
256 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
257
258 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
259 with io.open(nbfile, 'r', encoding='utf-8') as f:
260 newnb = read(f, format='ipynb')
261 self.assertEqual(newnb.worksheets[0].cells[0].source,
262 u'Created by test Β³')
263 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
264 newnb = to_notebook_json(nbcontent)
265 self.assertEqual(newnb.worksheets[0].cells[0].source,
266 u'Created by test Β³')
267
268 # Save and rename
269 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
270 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
271 saved = resp.json()
272 self.assertEqual(saved['name'], 'a2.ipynb')
273 self.assertEqual(saved['path'], 'foo/bar')
274 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
275 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
276 with assert_http_error(404):
277 self.nb_api.read('a.ipynb', 'foo')
278
279 def test_checkpoints(self):
280 resp = self.nb_api.read('a.ipynb', 'foo')
281 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
282 self.assertEqual(r.status_code, 201)
283 cp1 = r.json()
284 self.assertEqual(set(cp1), {'id', 'last_modified'})
285 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
286
287 # Modify it
288 nbcontent = json.loads(resp.text)['content']
289 nb = to_notebook_json(nbcontent)
290 ws = new_worksheet()
291 nb.worksheets = [ws]
292 hcell = new_heading_cell('Created by test')
293 ws.cells.append(hcell)
294 # Save
295 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
296 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
297
298 # List checkpoints
299 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
300 self.assertEqual(cps, [cp1])
301
302 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
303 nb = to_notebook_json(nbcontent)
304 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
305
306 # Restore cp1
307 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
308 self.assertEqual(r.status_code, 204)
309 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
310 nb = to_notebook_json(nbcontent)
311 self.assertEqual(nb.worksheets, [])
312
313 # Delete cp1
314 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
315 self.assertEqual(r.status_code, 204)
316 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
317 self.assertEqual(cps, [])
318
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,127 b''
1 """Tornado handlers for the sessions web service.
2
3 Authors:
4
5 * Zach Sailer
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import json
20
21 from tornado import web
22
23 from ...base.handlers import IPythonHandler, json_errors
24 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join, url_escape
26
27 #-----------------------------------------------------------------------------
28 # Session web service handlers
29 #-----------------------------------------------------------------------------
30
31
32 class SessionRootHandler(IPythonHandler):
33
34 @web.authenticated
35 @json_errors
36 def get(self):
37 # Return a list of running sessions
38 sm = self.session_manager
39 sessions = sm.list_sessions()
40 self.finish(json.dumps(sessions, default=date_default))
41
42 @web.authenticated
43 @json_errors
44 def post(self):
45 # Creates a new session
46 #(unless a session already exists for the named nb)
47 sm = self.session_manager
48 nbm = self.notebook_manager
49 km = self.kernel_manager
50 model = self.get_json_body()
51 if model is None:
52 raise web.HTTPError(400, "No JSON data provided")
53 try:
54 name = model['notebook']['name']
55 except KeyError:
56 raise web.HTTPError(400, "Missing field in JSON data: name")
57 try:
58 path = model['notebook']['path']
59 except KeyError:
60 raise web.HTTPError(400, "Missing field in JSON data: path")
61 # Check to see if session exists
62 if sm.session_exists(name=name, path=path):
63 model = sm.get_session(name=name, path=path)
64 else:
65 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
66 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
67 location = url_path_join(self.base_kernel_url, 'api', 'sessions', model['id'])
68 self.set_header('Location', url_escape(location))
69 self.set_status(201)
70 self.finish(json.dumps(model, default=date_default))
71
72 class SessionHandler(IPythonHandler):
73
74 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
75
76 @web.authenticated
77 @json_errors
78 def get(self, session_id):
79 # Returns the JSON model for a single session
80 sm = self.session_manager
81 model = sm.get_session(session_id=session_id)
82 self.finish(json.dumps(model, default=date_default))
83
84 @web.authenticated
85 @json_errors
86 def patch(self, session_id):
87 # Currently, this handler is strictly for renaming notebooks
88 sm = self.session_manager
89 model = self.get_json_body()
90 if model is None:
91 raise web.HTTPError(400, "No JSON data provided")
92 changes = {}
93 if 'notebook' in model:
94 notebook = model['notebook']
95 if 'name' in notebook:
96 changes['name'] = notebook['name']
97 if 'path' in notebook:
98 changes['path'] = notebook['path']
99
100 sm.update_session(session_id, **changes)
101 model = sm.get_session(session_id=session_id)
102 self.finish(json.dumps(model, default=date_default))
103
104 @web.authenticated
105 @json_errors
106 def delete(self, session_id):
107 # Deletes the session with given session_id
108 sm = self.session_manager
109 km = self.kernel_manager
110 session = sm.get_session(session_id=session_id)
111 sm.delete_session(session_id)
112 km.shutdown_kernel(session['kernel']['id'])
113 self.set_status(204)
114 self.finish()
115
116
117 #-----------------------------------------------------------------------------
118 # URL to handler mappings
119 #-----------------------------------------------------------------------------
120
121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
122
123 default_handlers = [
124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
125 (r"/api/sessions", SessionRootHandler)
126 ]
127
@@ -0,0 +1,201 b''
1 """A base class session manager.
2
3 Authors:
4
5 * Zach Sailer
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import uuid
20 import sqlite3
21
22 from tornado import web
23
24 from IPython.config.configurable import LoggingConfigurable
25
26 #-----------------------------------------------------------------------------
27 # Classes
28 #-----------------------------------------------------------------------------
29
30 class SessionManager(LoggingConfigurable):
31
32 # Session database initialized below
33 _cursor = None
34 _connection = None
35 _columns = {'session_id', 'name', 'path', 'kernel_id', 'ws_url'}
36
37 @property
38 def cursor(self):
39 """Start a cursor and create a database called 'session'"""
40 if self._cursor is None:
41 self._cursor = self.connection.cursor()
42 self._cursor.execute("""CREATE TABLE session
43 (session_id, name, path, kernel_id, ws_url)""")
44 return self._cursor
45
46 @property
47 def connection(self):
48 """Start a database connection"""
49 if self._connection is None:
50 self._connection = sqlite3.connect(':memory:')
51 self._connection.row_factory = self.row_factory
52 return self._connection
53
54 def __del__(self):
55 """Close connection once SessionManager closes"""
56 self.cursor.close()
57
58 def session_exists(self, name, path):
59 """Check to see if the session for a given notebook exists"""
60 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
61 reply = self.cursor.fetchone()
62 if reply is None:
63 return False
64 else:
65 return True
66
67 def new_session_id(self):
68 "Create a uuid for a new session"
69 return unicode(uuid.uuid4())
70
71 def create_session(self, name=None, path=None, kernel_id=None, ws_url=None):
72 """Creates a session and returns its model"""
73 session_id = self.new_session_id()
74 return self.save_session(session_id, name=name, path=path, kernel_id=kernel_id, ws_url=ws_url)
75
76 def save_session(self, session_id, name=None, path=None, kernel_id=None, ws_url=None):
77 """Saves the items for the session with the given session_id
78
79 Given a session_id (and any other of the arguments), this method
80 creates a row in the sqlite session database that holds the information
81 for a session.
82
83 Parameters
84 ----------
85 session_id : str
86 uuid for the session; this method must be given a session_id
87 name : str
88 the .ipynb notebook name that started the session
89 path : str
90 the path to the named notebook
91 kernel_id : str
92 a uuid for the kernel associated with this session
93 ws_url : str
94 the websocket url
95
96 Returns
97 -------
98 model : dict
99 a dictionary of the session model
100 """
101 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?,?)",
102 (session_id, name, path, kernel_id, ws_url)
103 )
104 return self.get_session(session_id=session_id)
105
106 def get_session(self, **kwargs):
107 """Returns the model for a particular session.
108
109 Takes a keyword argument and searches for the value in the session
110 database, then returns the rest of the session's info.
111
112 Parameters
113 ----------
114 **kwargs : keyword argument
115 must be given one of the keywords and values from the session database
116 (i.e. session_id, name, path, kernel_id, ws_url)
117
118 Returns
119 -------
120 model : dict
121 returns a dictionary that includes all the information from the
122 session described by the kwarg.
123 """
124 if not kwargs:
125 raise TypeError("must specify a column to query")
126
127 conditions = []
128 for column in kwargs.keys():
129 if column not in self._columns:
130 raise TypeError("No such column: %r", column)
131 conditions.append("%s=?" % column)
132
133 query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
134
135 self.cursor.execute(query, kwargs.values())
136 model = self.cursor.fetchone()
137 if model is None:
138 q = []
139 for key, value in kwargs.items():
140 q.append("%s=%r" % (key, value))
141
142 raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
143 return model
144
145 def update_session(self, session_id, **kwargs):
146 """Updates the values in the session database.
147
148 Changes the values of the session with the given session_id
149 with the values from the keyword arguments.
150
151 Parameters
152 ----------
153 session_id : str
154 a uuid that identifies a session in the sqlite3 database
155 **kwargs : str
156 the key must correspond to a column title in session database,
157 and the value replaces the current value in the session
158 with session_id.
159 """
160 self.get_session(session_id=session_id)
161
162 if not kwargs:
163 # no changes
164 return
165
166 sets = []
167 for column in kwargs.keys():
168 if column not in self._columns:
169 raise TypeError("No such column: %r" % column)
170 sets.append("%s=?" % column)
171 query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
172 self.cursor.execute(query, kwargs.values() + [session_id])
173
174 @staticmethod
175 def row_factory(cursor, row):
176 """Takes sqlite database session row and turns it into a dictionary"""
177 row = sqlite3.Row(cursor, row)
178 model = {
179 'id': row['session_id'],
180 'notebook': {
181 'name': row['name'],
182 'path': row['path']
183 },
184 'kernel': {
185 'id': row['kernel_id'],
186 'ws_url': row['ws_url']
187 }
188 }
189 return model
190
191 def list_sessions(self):
192 """Returns a list of dictionaries containing all the information from
193 the session database"""
194 c = self.cursor.execute("SELECT * FROM session")
195 return list(c.fetchall())
196
197 def delete_session(self, session_id):
198 """Deletes the row in the session database with given session_id"""
199 # Check that session exists before deleting
200 self.get_session(session_id=session_id)
201 self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,83 b''
1 """Tests for the session manager."""
2
3 from unittest import TestCase
4
5 from tornado import web
6
7 from ..sessionmanager import SessionManager
8
9 class TestSessionManager(TestCase):
10
11 def test_get_session(self):
12 sm = SessionManager()
13 session_id = sm.new_session_id()
14 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
15 model = sm.get_session(session_id=session_id)
16 expected = {'id':session_id, 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678', 'ws_url':u'ws_url'}}
17 self.assertEqual(model, expected)
18
19 def test_bad_get_session(self):
20 # Should raise error if a bad key is passed to the database.
21 sm = SessionManager()
22 session_id = sm.new_session_id()
23 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
24 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
25
26 def test_list_sessions(self):
27 sm = SessionManager()
28 session_id1 = sm.new_session_id()
29 session_id2 = sm.new_session_id()
30 session_id3 = sm.new_session_id()
31 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678', ws_url='ws_url')
32 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678', ws_url='ws_url')
33 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678', ws_url='ws_url')
34 sessions = sm.list_sessions()
35 expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
36 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}},
37 {'id':session_id2, 'notebook': {'name':u'test2.ipynb',
38 'path': u'/path/to/2/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}},
39 {'id':session_id3, 'notebook':{'name':u'test3.ipynb',
40 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}]
41 self.assertEqual(sessions, expected)
42
43 def test_update_session(self):
44 sm = SessionManager()
45 session_id = sm.new_session_id()
46 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id=None, ws_url='ws_url')
47 sm.update_session(session_id, kernel_id='5678')
48 sm.update_session(session_id, name='new_name.ipynb')
49 model = sm.get_session(session_id=session_id)
50 expected = {'id':session_id, 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}
51 self.assertEqual(model, expected)
52
53 def test_bad_update_session(self):
54 # try to update a session with a bad keyword ~ raise error
55 sm = SessionManager()
56 session_id = sm.new_session_id()
57 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
58 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
59
60 def test_delete_session(self):
61 sm = SessionManager()
62 session_id1 = sm.new_session_id()
63 session_id2 = sm.new_session_id()
64 session_id3 = sm.new_session_id()
65 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678', ws_url='ws_url')
66 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678', ws_url='ws_url')
67 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678', ws_url='ws_url')
68 sm.delete_session(session_id2)
69 sessions = sm.list_sessions()
70 expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
71 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}},
72 {'id':session_id3, 'notebook':{'name':u'test3.ipynb',
73 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}]
74 self.assertEqual(sessions, expected)
75
76 def test_bad_delete_session(self):
77 # try to delete a session that doesn't exist ~ raise error
78 sm = SessionManager()
79 session_id = sm.new_session_id()
80 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
81 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
82 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
83
@@ -0,0 +1,107 b''
1 """Test the sessions web service API."""
2
3 import io
4 import os
5 import json
6 import requests
7 import shutil
8
9 pjoin = os.path.join
10
11 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from IPython.nbformat.current import new_notebook, write
14
15 class SessionAPI(object):
16 """Wrapper for notebook API calls."""
17 def __init__(self, base_url):
18 self.base_url = base_url
19
20 def _req(self, verb, path, body=None):
21 response = requests.request(verb,
22 url_path_join(self.base_url, 'api/sessions', path), data=body)
23
24 if 400 <= response.status_code < 600:
25 try:
26 response.reason = response.json()['message']
27 except:
28 pass
29 response.raise_for_status()
30
31 return response
32
33 def list(self):
34 return self._req('GET', '')
35
36 def get(self, id):
37 return self._req('GET', id)
38
39 def create(self, name, path):
40 body = json.dumps({'notebook': {'name':name, 'path':path}})
41 return self._req('POST', '', body)
42
43 def modify(self, id, name, path):
44 body = json.dumps({'notebook': {'name':name, 'path':path}})
45 return self._req('PATCH', id, body)
46
47 def delete(self, id):
48 return self._req('DELETE', id)
49
50 class SessionAPITest(NotebookTestBase):
51 """Test the sessions web service API"""
52 def setUp(self):
53 nbdir = self.notebook_dir.name
54 os.mkdir(pjoin(nbdir, 'foo'))
55
56 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w') as f:
57 nb = new_notebook(name='nb1')
58 write(nb, f, format='ipynb')
59
60 self.sess_api = SessionAPI(self.base_url())
61
62 def tearDown(self):
63 for session in self.sess_api.list().json():
64 self.sess_api.delete(session['id'])
65 shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'))
66
67 def test_create(self):
68 sessions = self.sess_api.list().json()
69 self.assertEqual(len(sessions), 0)
70
71 resp = self.sess_api.create('nb1.ipynb', 'foo')
72 self.assertEqual(resp.status_code, 201)
73 newsession = resp.json()
74 self.assertIn('id', newsession)
75 self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
76 self.assertEqual(newsession['notebook']['path'], 'foo')
77 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
78
79 sessions = self.sess_api.list().json()
80 self.assertEqual(sessions, [newsession])
81
82 # Retrieve it
83 sid = newsession['id']
84 got = self.sess_api.get(sid).json()
85 self.assertEqual(got, newsession)
86
87 def test_delete(self):
88 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
89 sid = newsession['id']
90
91 resp = self.sess_api.delete(sid)
92 self.assertEqual(resp.status_code, 204)
93
94 sessions = self.sess_api.list().json()
95 self.assertEqual(sessions, [])
96
97 with assert_http_error(404):
98 self.sess_api.get(sid)
99
100 def test_modify(self):
101 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
102 sid = newsession['id']
103
104 changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
105 self.assertEqual(changed['id'], sid)
106 self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
107 self.assertEqual(changed['notebook']['path'], '')
@@ -0,0 +1,117 b''
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
7
8 //============================================================================
9 // Notebook
10 //============================================================================
11
12 var IPython = (function (IPython) {
13 "use strict";
14
15 var utils = IPython.utils;
16
17 var Session = function(notebook_name, notebook_path, notebook){
18 this.kernel = null;
19 this.id = null;
20 this.name = notebook_name;
21 this.path = notebook_path;
22 this.notebook = notebook;
23 this._baseProjectUrl = notebook.baseProjectUrl();
24 };
25
26 Session.prototype.start = function(callback) {
27 var that = this;
28 var model = {
29 notebook : {
30 name : this.name,
31 path : this.path
32 }
33 };
34 var settings = {
35 processData : false,
36 cache : false,
37 type : "POST",
38 data: JSON.stringify(model),
39 dataType : "json",
40 success : function (data, status, xhr) {
41 that._handle_start_success(data);
42 if (callback) {
43 callback(data, status, xhr);
44 }
45 },
46 };
47 var url = utils.url_path_join(this._baseProjectUrl, 'api/sessions');
48 $.ajax(url, settings);
49 };
50
51 Session.prototype.rename_notebook = function (name, path) {
52 this.name = name;
53 this.path = path;
54 var model = {
55 notebook : {
56 name : this.name,
57 path : this.path
58 }
59 };
60 var settings = {
61 processData : false,
62 cache : false,
63 type : "PATCH",
64 data: JSON.stringify(model),
65 dataType : "json",
66 };
67 var url = utils.url_path_join(this._baseProjectUrl, 'api/sessions', this.id);
68 $.ajax(url, settings);
69 };
70
71 Session.prototype.delete = function() {
72 var settings = {
73 processData : false,
74 cache : false,
75 type : "DELETE",
76 dataType : "json",
77 };
78 var url = utils.url_path_join(this._baseProjectUrl, 'api/sessions', this.id);
79 $.ajax(url, settings);
80 };
81
82 // Kernel related things
83 /**
84 * Create the Kernel object associated with this Session.
85 *
86 * @method _handle_start_success
87 */
88 Session.prototype._handle_start_success = function (data, status, xhr) {
89 this.id = data.id;
90 var base_url = utils.url_path_join($('body').data('baseKernelUrl'), "api/kernels");
91 this.kernel = new IPython.Kernel(base_url);
92 this.kernel._kernel_started(data.kernel);
93 };
94
95 /**
96 * Prompt the user to restart the IPython kernel.
97 *
98 * @method restart_kernel
99 */
100 Session.prototype.restart_kernel = function () {
101 this.kernel.restart();
102 };
103
104 Session.prototype.interrupt_kernel = function() {
105 this.kernel.interrupt();
106 };
107
108
109 Session.prototype.kill_kernel = function() {
110 this.kernel.kill();
111 };
112
113 IPython.Session = Session;
114
115 return IPython;
116
117 }(IPython));
@@ -0,0 +1,88 b''
1 """Base class for notebook tests."""
2
3 import os
4 import sys
5 import time
6 import requests
7 from contextlib import contextmanager
8 from subprocess import Popen, PIPE
9 from unittest import TestCase
10
11 from IPython.utils.tempdir import TemporaryDirectory
12
13 class NotebookTestBase(TestCase):
14 """A base class for tests that need a running notebook.
15
16 This creates an empty profile in a temp ipython_dir
17 and then starts the notebook server with a separate temp notebook_dir.
18 """
19
20 port = 12341
21
22 @classmethod
23 def wait_until_alive(cls):
24 """Wait for the server to be alive"""
25 url = 'http://localhost:%i/api/notebooks' % cls.port
26 while True:
27 try:
28 requests.get(url)
29 except requests.exceptions.ConnectionError:
30 time.sleep(.1)
31 else:
32 break
33
34 @classmethod
35 def wait_until_dead(cls):
36 """Wait for the server to stop getting requests after shutdown"""
37 url = 'http://localhost:%i/api/notebooks' % cls.port
38 while True:
39 try:
40 requests.get(url)
41 except requests.exceptions.ConnectionError:
42 break
43 else:
44 time.sleep(.1)
45
46 @classmethod
47 def setup_class(cls):
48 cls.ipython_dir = TemporaryDirectory()
49 cls.notebook_dir = TemporaryDirectory()
50 notebook_args = [
51 sys.executable, '-c',
52 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
53 '--port=%d' % cls.port,
54 '--no-browser',
55 '--ipython-dir=%s' % cls.ipython_dir.name,
56 '--notebook-dir=%s' % cls.notebook_dir.name,
57 ]
58 devnull = open(os.devnull, 'w')
59 cls.notebook = Popen(notebook_args,
60 stdout=devnull,
61 stderr=devnull,
62 )
63 cls.wait_until_alive()
64
65 @classmethod
66 def teardown_class(cls):
67 cls.notebook.terminate()
68 cls.ipython_dir.cleanup()
69 cls.notebook_dir.cleanup()
70 cls.wait_until_dead()
71
72 @classmethod
73 def base_url(cls):
74 return 'http://localhost:%i/' % cls.port
75
76
77 @contextmanager
78 def assert_http_error(status, msg=None):
79 try:
80 yield
81 except requests.HTTPError as e:
82 real_status = e.response.status_code
83 assert real_status == status, \
84 "Expected status %d, got %d" % (real_status, status)
85 if msg:
86 assert msg in str(e), e
87 else:
88 assert False, "Expected HTTP error status" No newline at end of file
@@ -0,0 +1,51 b''
1 # coding: utf-8
2 """Test the /files/ handler."""
3
4 import io
5 import os
6 from unicodedata import normalize
7
8 pjoin = os.path.join
9
10 import requests
11
12 from IPython.html.utils import url_path_join
13 from .launchnotebook import NotebookTestBase
14 from IPython.utils import py3compat
15
16 class FilesTest(NotebookTestBase):
17 def test_hidden_files(self):
18 not_hidden = [
19 u'Γ₯ b',
20 pjoin(u'Γ₯ b/Γ§. d')
21 ]
22 hidden = [
23 u'.Γ₯ b',
24 pjoin(u'Γ₯ b/.Γ§ d')
25 ]
26 dirs = not_hidden + hidden
27
28 nbdir = self.notebook_dir.name
29 for d in dirs:
30 path = pjoin(nbdir, d.replace('/', os.sep))
31 if not os.path.exists(path):
32 os.mkdir(path)
33 with open(pjoin(path, 'foo'), 'w') as f:
34 f.write('foo')
35 with open(pjoin(path, '.foo'), 'w') as f:
36 f.write('.foo')
37 url = self.base_url()
38
39 for d in not_hidden:
40 path = pjoin(nbdir, d.replace('/', os.sep))
41 r = requests.get(url_path_join(url, 'files', d, 'foo'))
42 r.raise_for_status()
43 self.assertEqual(r.content, b'foo')
44 r = requests.get(url_path_join(url, 'files', d, '.foo'))
45 self.assertEqual(r.status_code, 403)
46
47 for d in hidden:
48 path = pjoin(nbdir, d.replace('/', os.sep))
49 for foo in ('foo', '.foo'):
50 r = requests.get(url_path_join(url, 'files', d, foo))
51 self.assertEqual(r.status_code, 403)
@@ -0,0 +1,61 b''
1 """Test HTML utils"""
2
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2013 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
9
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
13
14 import nose.tools as nt
15
16 import IPython.testing.tools as tt
17 from IPython.html.utils import url_escape, url_unescape
18
19 #-----------------------------------------------------------------------------
20 # Test functions
21 #-----------------------------------------------------------------------------
22
23 def test_help_output():
24 """ipython notebook --help-all works"""
25 tt.help_all_output_test('notebook')
26
27
28 def test_url_escape():
29
30 # changes path or notebook name with special characters to url encoding
31 # these tests specifically encode paths with spaces
32 path = url_escape('/this is a test/for spaces/')
33 nt.assert_equal(path, '/this%20is%20a%20test/for%20spaces/')
34
35 path = url_escape('notebook with space.ipynb')
36 nt.assert_equal(path, 'notebook%20with%20space.ipynb')
37
38 path = url_escape('/path with a/notebook and space.ipynb')
39 nt.assert_equal(path, '/path%20with%20a/notebook%20and%20space.ipynb')
40
41 path = url_escape('/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
42 nt.assert_equal(path,
43 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
44
45 def test_url_unescape():
46
47 # decodes a url string to a plain string
48 # these tests decode paths with spaces
49 path = url_unescape('/this%20is%20a%20test/for%20spaces/')
50 nt.assert_equal(path, '/this is a test/for spaces/')
51
52 path = url_unescape('notebook%20with%20space.ipynb')
53 nt.assert_equal(path, 'notebook with space.ipynb')
54
55 path = url_unescape('/path%20with%20a/notebook%20and%20space.ipynb')
56 nt.assert_equal(path, '/path with a/notebook and space.ipynb')
57
58 path = url_unescape(
59 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
60 nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
61
@@ -0,0 +1,85 b''
1 """Configurable for configuring the IPython inline backend
2
3 This module does not import anything from matplotlib.
4 """
5 #-----------------------------------------------------------------------------
6 # Copyright (C) 2011 The IPython Development Team
7 #
8 # Distributed under the terms of the BSD License. The full license is in
9 # the file COPYING, distributed as part of this software.
10 #-----------------------------------------------------------------------------
11
12 #-----------------------------------------------------------------------------
13 # Imports
14 #-----------------------------------------------------------------------------
15
16 from IPython.config.configurable import SingletonConfigurable
17 from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, Bool
18 from IPython.utils.warn import warn
19
20 #-----------------------------------------------------------------------------
21 # Configurable for inline backend options
22 #-----------------------------------------------------------------------------
23
24 # inherit from InlineBackendConfig for deprecation purposes
25 class InlineBackendConfig(SingletonConfigurable):
26 pass
27
28 class InlineBackend(InlineBackendConfig):
29 """An object to store configuration of the inline backend."""
30
31 def _config_changed(self, name, old, new):
32 # warn on change of renamed config section
33 if new.InlineBackendConfig != old.InlineBackendConfig:
34 warn("InlineBackendConfig has been renamed to InlineBackend")
35 super(InlineBackend, self)._config_changed(name, old, new)
36
37 # The typical default figure size is too large for inline use,
38 # so we shrink the figure size to 6x4, and tweak fonts to
39 # make that fit.
40 rc = Dict({'figure.figsize': (6.0,4.0),
41 # play nicely with white background in the Qt and notebook frontend
42 'figure.facecolor': 'white',
43 'figure.edgecolor': 'white',
44 # 12pt labels get cutoff on 6x4 logplots, so use 10pt.
45 'font.size': 10,
46 # 72 dpi matches SVG/qtconsole
47 # this only affects PNG export, as SVG has no dpi setting
48 'savefig.dpi': 72,
49 # 10pt still needs a little more room on the xlabel:
50 'figure.subplot.bottom' : .125
51 }, config=True,
52 help="""Subset of matplotlib rcParams that should be different for the
53 inline backend."""
54 )
55
56 figure_format = CaselessStrEnum(['svg', 'png', 'retina'], default_value='png', config=True,
57 help="The image format for figures with the inline backend.")
58
59 def _figure_format_changed(self, name, old, new):
60 from IPython.core.pylabtools import select_figure_format
61 if self.shell is None:
62 return
63 else:
64 select_figure_format(self.shell, new)
65
66 close_figures = Bool(True, config=True,
67 help="""Close all figures at the end of each cell.
68
69 When True, ensures that each cell starts with no active figures, but it
70 also means that one must keep track of references in order to edit or
71 redraw figures in subsequent cells. This mode is ideal for the notebook,
72 where residual plots from other cells might be surprising.
73
74 When False, one must call figure() to create new figures. This means
75 that gcf() and getfigs() can reference figures created in other cells,
76 and the active figure can continue to be edited with pylab/pyplot
77 methods that reference the current active figure. This mode facilitates
78 iterative editing of figures, and behaves most consistently with
79 other matplotlib backends, but figure barriers between cells must
80 be explicit.
81 """)
82
83 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
84
85
@@ -5,7 +5,7 b' python:'
5 - 3.3
5 - 3.3
6 before_install:
6 before_install:
7 - easy_install -q pyzmq
7 - easy_install -q pyzmq
8 - pip install jinja2 sphinx pygments tornado
8 - pip install jinja2 sphinx pygments tornado requests
9 - sudo apt-get install pandoc
9 - sudo apt-get install pandoc
10 install:
10 install:
11 - python setup.py install -q
11 - python setup.py install -q
@@ -38,6 +38,7 b' from IPython.utils.traitlets import ('
38 )
38 )
39 from IPython.utils.importstring import import_item
39 from IPython.utils.importstring import import_item
40 from IPython.utils.text import indent, wrap_paragraphs, dedent
40 from IPython.utils.text import indent, wrap_paragraphs, dedent
41 from IPython.utils import py3compat
41
42
42 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
43 # function for re-wrapping a helpstring
44 # function for re-wrapping a helpstring
@@ -457,7 +458,7 b' class Application(SingletonConfigurable):'
457 def parse_command_line(self, argv=None):
458 def parse_command_line(self, argv=None):
458 """Parse the command line arguments."""
459 """Parse the command line arguments."""
459 argv = sys.argv[1:] if argv is None else argv
460 argv = sys.argv[1:] if argv is None else argv
460 self.argv = list(argv)
461 self.argv = [ py3compat.cast_unicode(arg) for arg in argv ]
461
462
462 if argv and argv[0] == 'help':
463 if argv and argv[0] == 'help':
463 # turn `ipython help notebook` into `ipython notebook -h`
464 # turn `ipython help notebook` into `ipython notebook -h`
@@ -45,6 +45,7 b' from IPython.kernel.zmq.kernelapp import ('
45 kernel_aliases,
45 kernel_aliases,
46 IPKernelApp
46 IPKernelApp
47 )
47 )
48 from IPython.kernel.zmq.pylab.config import InlineBackend
48 from IPython.kernel.zmq.session import Session, default_secure
49 from IPython.kernel.zmq.session import Session, default_secure
49 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.kernel.connect import ConnectionFileMixin
51 from IPython.kernel.connect import ConnectionFileMixin
@@ -110,14 +111,7 b' aliases.update(app_aliases)'
110 # IPythonConsole
111 # IPythonConsole
111 #-----------------------------------------------------------------------------
112 #-----------------------------------------------------------------------------
112
113
113 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session]
114 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend]
114
115 try:
116 from IPython.kernel.zmq.pylab.backend_inline import InlineBackend
117 except ImportError:
118 pass
119 else:
120 classes.append(InlineBackend)
121
115
122 class IPythonConsoleApp(ConnectionFileMixin):
116 class IPythonConsoleApp(ConnectionFileMixin):
123 name = 'ipython-console-mixin'
117 name = 'ipython-console-mixin'
@@ -19,12 +19,16 b' Authors:'
19
19
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import functools
22 import hashlib
23 import hashlib
24 import json
23 import logging
25 import logging
24 import mimetypes
26 import mimetypes
25 import os
27 import os
26 import stat
28 import stat
29 import sys
27 import threading
30 import threading
31 import traceback
28
32
29 from tornado import web
33 from tornado import web
30 from tornado import websocket
34 from tornado import websocket
@@ -37,6 +41,11 b' except ImportError:'
37 from IPython.config import Application
41 from IPython.config import Application
38 from IPython.external.decorator import decorator
42 from IPython.external.decorator import decorator
39 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44 from IPython.utils.jsonutil import date_default
45
46 # UF_HIDDEN is a stat flag not defined in the stat module.
47 # It is used by BSD to indicate hidden files.
48 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
40
49
41 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
42 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
@@ -214,7 +223,11 b' class IPythonHandler(AuthenticatedHandler):'
214 return self.settings['cluster_manager']
223 return self.settings['cluster_manager']
215
224
216 @property
225 @property
217 def project(self):
226 def session_manager(self):
227 return self.settings['session_manager']
228
229 @property
230 def project_dir(self):
218 return self.notebook_manager.notebook_dir
231 return self.notebook_manager.notebook_dir
219
232
220 #---------------------------------------------------------------
233 #---------------------------------------------------------------
@@ -240,12 +253,100 b' class IPythonHandler(AuthenticatedHandler):'
240 use_less=self.use_less,
253 use_less=self.use_less,
241 )
254 )
242
255
256 def get_json_body(self):
257 """Return the body of the request as JSON data."""
258 if not self.request.body:
259 return None
260 # Do we need to call body.decode('utf-8') here?
261 body = self.request.body.strip().decode(u'utf-8')
262 try:
263 model = json.loads(body)
264 except Exception:
265 self.log.debug("Bad JSON: %r", body)
266 self.log.error("Couldn't parse JSON", exc_info=True)
267 raise web.HTTPError(400, u'Invalid JSON in body of request')
268 return model
269
270
243 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
271 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
244 """static files should only be accessible when logged in"""
272 """static files should only be accessible when logged in"""
245
273
246 @web.authenticated
274 @web.authenticated
247 def get(self, path):
275 def get(self, path):
276 if os.path.splitext(path)[1] == '.ipynb':
277 name = os.path.basename(path)
278 self.set_header('Content-Type', 'application/json')
279 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
280
248 return web.StaticFileHandler.get(self, path)
281 return web.StaticFileHandler.get(self, path)
282
283 def validate_absolute_path(self, root, absolute_path):
284 """Validate and return the absolute path.
285
286 Requires tornado 3.1
287
288 Adding to tornado's own handling, forbids the serving of hidden files.
289 """
290 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
291 abs_root = os.path.abspath(root)
292 self.forbid_hidden(abs_root, abs_path)
293 return abs_path
294
295 def forbid_hidden(self, absolute_root, absolute_path):
296 """Raise 403 if a file is hidden or contained in a hidden directory.
297
298 Hidden is determined by either name starting with '.'
299 or the UF_HIDDEN flag as reported by stat
300 """
301 inside_root = absolute_path[len(absolute_root):]
302 if any(part.startswith('.') for part in inside_root.split(os.sep)):
303 raise web.HTTPError(403)
304
305 # check UF_HIDDEN on any location up to root
306 path = absolute_path
307 while path and path.startswith(absolute_root) and path != absolute_root:
308 st = os.stat(path)
309 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
310 raise web.HTTPError(403)
311 path = os.path.dirname(path)
312
313 return absolute_path
314
315
316 def json_errors(method):
317 """Decorate methods with this to return GitHub style JSON errors.
318
319 This should be used on any JSON API on any handler method that can raise HTTPErrors.
320
321 This will grab the latest HTTPError exception using sys.exc_info
322 and then:
323
324 1. Set the HTTP status code based on the HTTPError
325 2. Create and return a JSON body with a message field describing
326 the error in a human readable form.
327 """
328 @functools.wraps(method)
329 def wrapper(self, *args, **kwargs):
330 try:
331 result = method(self, *args, **kwargs)
332 except web.HTTPError as e:
333 status = e.status_code
334 message = e.log_message
335 self.set_status(e.status_code)
336 self.finish(json.dumps(dict(message=message)))
337 except Exception:
338 self.log.error("Unhandled error in API request", exc_info=True)
339 status = 500
340 message = "Unknown server error"
341 t, value, tb = sys.exc_info()
342 self.set_status(status)
343 tb_text = ''.join(traceback.format_exception(t, value, tb))
344 reply = dict(message=message, traceback=tb_text)
345 self.finish(json.dumps(reply))
346 else:
347 return result
348 return wrapper
349
249
350
250
351
251 #-----------------------------------------------------------------------------
352 #-----------------------------------------------------------------------------
@@ -266,7 +367,7 b' class FileFindHandler(web.StaticFileHandler):'
266 if isinstance(path, basestring):
367 if isinstance(path, basestring):
267 path = [path]
368 path = [path]
268 self.roots = tuple(
369 self.roots = tuple(
269 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
370 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
270 )
371 )
271 self.default_filename = default_filename
372 self.default_filename = default_filename
272
373
@@ -284,7 +385,7 b' class FileFindHandler(web.StaticFileHandler):'
284
385
285 # os.path.abspath strips a trailing /
386 # os.path.abspath strips a trailing /
286 # it needs to be temporarily added back for requests to root/
387 # it needs to be temporarily added back for requests to root/
287 if not (abspath + os.path.sep).startswith(roots):
388 if not (abspath + os.sep).startswith(roots):
288 raise HTTPError(403, "%s is not in root static directory", path)
389 raise HTTPError(403, "%s is not in root static directory", path)
289
390
290 cls._static_paths[path] = abspath
391 cls._static_paths[path] = abspath
@@ -339,7 +440,7 b' class FileFindHandler(web.StaticFileHandler):'
339 if if_since >= modified:
440 if if_since >= modified:
340 self.set_status(304)
441 self.set_status(304)
341 return
442 return
342
443
343 with open(abspath, "rb") as file:
444 with open(abspath, "rb") as file:
344 data = file.read()
445 data = file.read()
345 hasher = hashlib.sha1()
446 hasher = hashlib.sha1()
@@ -369,7 +470,7 b' class FileFindHandler(web.StaticFileHandler):'
369 if isinstance(static_paths, basestring):
470 if isinstance(static_paths, basestring):
370 static_paths = [static_paths]
471 static_paths = [static_paths]
371 roots = tuple(
472 roots = tuple(
372 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
473 os.path.abspath(os.path.expanduser(p)) + os.sep for p in static_paths
373 )
474 )
374
475
375 try:
476 try:
@@ -403,13 +504,26 b' class FileFindHandler(web.StaticFileHandler):'
403 ``static_url_prefix`` removed. The return value should be
504 ``static_url_prefix`` removed. The return value should be
404 filesystem path relative to ``static_path``.
505 filesystem path relative to ``static_path``.
405 """
506 """
406 if os.path.sep != "/":
507 if os.sep != "/":
407 url_path = url_path.replace("/", os.path.sep)
508 url_path = url_path.replace("/", os.sep)
408 return url_path
509 return url_path
409
510
511 class TrailingSlashHandler(web.RequestHandler):
512 """Simple redirect handler that strips trailing slashes
513
514 This should be the first, highest priority handler.
515 """
516
517 SUPPORTED_METHODS = ['GET']
518
519 def get(self):
520 self.redirect(self.request.uri.rstrip('/'))
521
410 #-----------------------------------------------------------------------------
522 #-----------------------------------------------------------------------------
411 # URL to handler mappings
523 # URL to handler mappings
412 #-----------------------------------------------------------------------------
524 #-----------------------------------------------------------------------------
413
525
414
526
415 default_handlers = []
527 default_handlers = [
528 (r".*/", TrailingSlashHandler)
529 ]
@@ -17,75 +17,67 b' Authors:'
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 import json
21
20 from tornado import web
22 from tornado import web
21 HTTPError = web.HTTPError
23 HTTPError = web.HTTPError
22
24
23 from ..base.handlers import IPythonHandler
25 from ..base.handlers import IPythonHandler
24 from ..utils import url_path_join
26 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
27 from ..utils import url_path_join, url_escape, url_unescape
28 from urllib import quote
25
29
26 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
27 # Handlers
31 # Handlers
28 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
29
33
30
34
31 class NewHandler(IPythonHandler):
35 class NotebookHandler(IPythonHandler):
32
33 @web.authenticated
34 def get(self):
35 notebook_id = self.notebook_manager.new_notebook()
36 self.redirect(url_path_join(self.base_project_url, notebook_id))
37
38
39 class NamedNotebookHandler(IPythonHandler):
40
36
41 @web.authenticated
37 @web.authenticated
42 def get(self, notebook_id):
38 def get(self, path='', name=None):
39 """get renders the notebook template if a name is given, or
40 redirects to the '/files/' handler if the name is not given."""
41 path = path.strip('/')
43 nbm = self.notebook_manager
42 nbm = self.notebook_manager
44 if not nbm.notebook_exists(notebook_id):
43 if name is None:
45 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
44 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
45
46 # a .ipynb filename was given
47 if not nbm.notebook_exists(name, path):
48 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
49 name = url_escape(name)
50 path = url_escape(path)
46 self.write(self.render_template('notebook.html',
51 self.write(self.render_template('notebook.html',
47 project=self.project,
52 project=self.project_dir,
48 notebook_id=notebook_id,
53 notebook_path=path,
54 notebook_name=name,
49 kill_kernel=False,
55 kill_kernel=False,
50 mathjax_url=self.mathjax_url,
56 mathjax_url=self.mathjax_url,
51 )
57 )
52 )
58 )
53
59
54
55 class NotebookRedirectHandler(IPythonHandler):
60 class NotebookRedirectHandler(IPythonHandler):
56
61 def get(self, path=''):
57 @web.authenticated
62 nbm = self.notebook_manager
58 def get(self, notebook_name):
63 if nbm.path_exists(path):
59 # strip trailing .ipynb:
64 # it's a *directory*, redirect to /tree
60 notebook_name = os.path.splitext(notebook_name)[0]
65 url = url_path_join(self.base_project_url, 'tree', path)
61 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
62 if notebook_id:
63 url = url_path_join(self.settings.get('base_project_url', '/'), notebook_id)
64 return self.redirect(url)
65 else:
66 else:
66 raise HTTPError(404)
67 # otherwise, redirect to /files
67
68 # TODO: This should check if it's actually a file
68
69 url = url_path_join(self.base_project_url, 'files', path)
69 class NotebookCopyHandler(IPythonHandler):
70 url = url_escape(url)
70
71 self.log.debug("Redirecting %s to %s", self.request.path, url)
71 @web.authenticated
72 self.redirect(url)
72 def get(self, notebook_id):
73 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
74 self.redirect(url_path_join(self.base_project_url, notebook_id))
75
76
73
77 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
78 # URL to handler mappings
75 # URL to handler mappings
79 #-----------------------------------------------------------------------------
76 #-----------------------------------------------------------------------------
80
77
81
78
82 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
83 _notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
84
85 default_handlers = [
79 default_handlers = [
86 (r"/new", NewHandler),
80 (r"/notebooks%s" % _notebook_path_regex, NotebookHandler),
87 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
81 (r"/notebooks%s" % _path_regex, NotebookRedirectHandler),
88 (r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
89 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
90
91 ]
82 ]
83
@@ -65,6 +65,7 b' from .services.kernels.kernelmanager import MappingKernelManager'
65 from .services.notebooks.nbmanager import NotebookManager
65 from .services.notebooks.nbmanager import NotebookManager
66 from .services.notebooks.filenbmanager import FileNotebookManager
66 from .services.notebooks.filenbmanager import FileNotebookManager
67 from .services.clusters.clustermanager import ClusterManager
67 from .services.clusters.clustermanager import ClusterManager
68 from .services.sessions.sessionmanager import SessionManager
68
69
69 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
70 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
70
71
@@ -127,19 +128,19 b' def load_handlers(name):'
127 class NotebookWebApplication(web.Application):
128 class NotebookWebApplication(web.Application):
128
129
129 def __init__(self, ipython_app, kernel_manager, notebook_manager,
130 def __init__(self, ipython_app, kernel_manager, notebook_manager,
130 cluster_manager, log,
131 cluster_manager, session_manager, log, base_project_url,
131 base_project_url, settings_overrides):
132 settings_overrides):
132
133
133 settings = self.init_settings(
134 settings = self.init_settings(
134 ipython_app, kernel_manager, notebook_manager, cluster_manager,
135 ipython_app, kernel_manager, notebook_manager, cluster_manager,
135 log, base_project_url, settings_overrides)
136 session_manager, log, base_project_url, settings_overrides)
136 handlers = self.init_handlers(settings)
137 handlers = self.init_handlers(settings)
137
138
138 super(NotebookWebApplication, self).__init__(handlers, **settings)
139 super(NotebookWebApplication, self).__init__(handlers, **settings)
139
140
140 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
141 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
141 cluster_manager, log,
142 cluster_manager, session_manager, log, base_project_url,
142 base_project_url, settings_overrides):
143 settings_overrides):
143 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
144 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
144 # base_project_url will always be unicode, which will in turn
145 # base_project_url will always be unicode, which will in turn
145 # make the patterns unicode, and ultimately result in unicode
146 # make the patterns unicode, and ultimately result in unicode
@@ -168,7 +169,8 b' class NotebookWebApplication(web.Application):'
168 kernel_manager=kernel_manager,
169 kernel_manager=kernel_manager,
169 notebook_manager=notebook_manager,
170 notebook_manager=notebook_manager,
170 cluster_manager=cluster_manager,
171 cluster_manager=cluster_manager,
171
172 session_manager=session_manager,
173
172 # IPython stuff
174 # IPython stuff
173 nbextensions_path = ipython_app.nbextensions_path,
175 nbextensions_path = ipython_app.nbextensions_path,
174 mathjax_url=ipython_app.mathjax_url,
176 mathjax_url=ipython_app.mathjax_url,
@@ -192,6 +194,7 b' class NotebookWebApplication(web.Application):'
192 handlers.extend(load_handlers('services.kernels.handlers'))
194 handlers.extend(load_handlers('services.kernels.handlers'))
193 handlers.extend(load_handlers('services.notebooks.handlers'))
195 handlers.extend(load_handlers('services.notebooks.handlers'))
194 handlers.extend(load_handlers('services.clusters.handlers'))
196 handlers.extend(load_handlers('services.clusters.handlers'))
197 handlers.extend(load_handlers('services.sessions.handlers'))
195 handlers.extend([
198 handlers.extend([
196 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
199 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
197 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
200 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
@@ -497,13 +500,16 b' class NotebookApp(BaseIPythonApplication):'
497 super(NotebookApp, self).parse_command_line(argv)
500 super(NotebookApp, self).parse_command_line(argv)
498
501
499 if self.extra_args:
502 if self.extra_args:
500 f = os.path.abspath(self.extra_args[0])
503 arg0 = self.extra_args[0]
504 f = os.path.abspath(arg0)
505 self.argv.remove(arg0)
506 if not os.path.exists(f):
507 self.log.critical("No such file or directory: %s", f)
508 self.exit(1)
501 if os.path.isdir(f):
509 if os.path.isdir(f):
502 nbdir = f
510 self.config.FileNotebookManager.notebook_dir = f
503 else:
511 elif os.path.isfile(f):
504 self.file_to_run = f
512 self.file_to_run = f
505 nbdir = os.path.dirname(f)
506 self.config.NotebookManager.notebook_dir = nbdir
507
513
508 def init_kernel_argv(self):
514 def init_kernel_argv(self):
509 """construct the kernel arguments"""
515 """construct the kernel arguments"""
@@ -523,7 +529,7 b' class NotebookApp(BaseIPythonApplication):'
523 )
529 )
524 kls = import_item(self.notebook_manager_class)
530 kls = import_item(self.notebook_manager_class)
525 self.notebook_manager = kls(parent=self, log=self.log)
531 self.notebook_manager = kls(parent=self, log=self.log)
526 self.notebook_manager.load_notebook_names()
532 self.session_manager = SessionManager(parent=self, log=self.log)
527 self.cluster_manager = ClusterManager(parent=self, log=self.log)
533 self.cluster_manager = ClusterManager(parent=self, log=self.log)
528 self.cluster_manager.update_profiles()
534 self.cluster_manager.update_profiles()
529
535
@@ -535,14 +541,17 b' class NotebookApp(BaseIPythonApplication):'
535
541
536 # hook up tornado 3's loggers to our app handlers
542 # hook up tornado 3's loggers to our app handlers
537 for name in ('access', 'application', 'general'):
543 for name in ('access', 'application', 'general'):
538 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
544 logger = logging.getLogger('tornado.%s' % name)
545 logger.propagate = False
546 logger.setLevel(self.log.level)
547 logger.handlers = self.log.handlers
539
548
540 def init_webapp(self):
549 def init_webapp(self):
541 """initialize tornado webapp and httpserver"""
550 """initialize tornado webapp and httpserver"""
542 self.web_app = NotebookWebApplication(
551 self.web_app = NotebookWebApplication(
543 self, self.kernel_manager, self.notebook_manager,
552 self, self.kernel_manager, self.notebook_manager,
544 self.cluster_manager, self.log,
553 self.cluster_manager, self.session_manager,
545 self.base_project_url, self.webapp_settings,
554 self.log, self.base_project_url, self.webapp_settings
546 )
555 )
547 if self.certfile:
556 if self.certfile:
548 ssl_options = dict(certfile=self.certfile)
557 ssl_options = dict(certfile=self.certfile)
@@ -726,12 +735,22 b' class NotebookApp(BaseIPythonApplication):'
726 except webbrowser.Error as e:
735 except webbrowser.Error as e:
727 self.log.warn('No web browser found: %s.' % e)
736 self.log.warn('No web browser found: %s.' % e)
728 browser = None
737 browser = None
729
738
730 if self.file_to_run:
739 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
731 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
740 f = self.file_to_run
732 url = self.notebook_manager.rev_mapping.get(name, '')
741 if f and f.startswith(nbdir):
742 f = f[len(nbdir):]
743 else:
744 self.log.warn(
745 "Probably won't be able to open notebook %s "
746 "because it is not in notebook_dir %s",
747 f, nbdir,
748 )
749
750 if os.path.isfile(self.file_to_run):
751 url = url_path_join('notebooks', f)
733 else:
752 else:
734 url = ''
753 url = url_path_join('tree', f)
735 if browser:
754 if browser:
736 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
755 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
737 self.port, self.base_project_url, url), new=2)
756 self.port, self.base_project_url, url), new=2)
@@ -22,8 +22,9 b' from tornado import web'
22 from zmq.utils import jsonapi
22 from zmq.utils import jsonapi
23
23
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join, url_escape
25
26
26 from ...base.handlers import IPythonHandler
27 from ...base.handlers import IPythonHandler, json_errors
27 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
28 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
28
29
29 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
@@ -34,26 +35,37 b' from ...base.zmqhandlers import AuthenticatedZMQStreamHandler'
34 class MainKernelHandler(IPythonHandler):
35 class MainKernelHandler(IPythonHandler):
35
36
36 @web.authenticated
37 @web.authenticated
38 @json_errors
37 def get(self):
39 def get(self):
38 km = self.kernel_manager
40 km = self.kernel_manager
39 self.finish(jsonapi.dumps(km.list_kernel_ids()))
41 self.finish(jsonapi.dumps(km.list_kernels(self.ws_url)))
40
42
41 @web.authenticated
43 @web.authenticated
44 @json_errors
42 def post(self):
45 def post(self):
43 km = self.kernel_manager
46 km = self.kernel_manager
44 nbm = self.notebook_manager
47 kernel_id = km.start_kernel()
45 notebook_id = self.get_argument('notebook', default=None)
48 model = km.kernel_model(kernel_id, self.ws_url)
46 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
49 location = url_path_join(self.base_kernel_url, 'api', 'kernels', kernel_id)
47 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
50 self.set_header('Location', url_escape(location))
48 self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
51 self.set_status(201)
49 self.finish(jsonapi.dumps(data))
52 self.finish(jsonapi.dumps(model))
50
53
51
54
52 class KernelHandler(IPythonHandler):
55 class KernelHandler(IPythonHandler):
53
56
54 SUPPORTED_METHODS = ('DELETE')
57 SUPPORTED_METHODS = ('DELETE', 'GET')
55
58
56 @web.authenticated
59 @web.authenticated
60 @json_errors
61 def get(self, kernel_id):
62 km = self.kernel_manager
63 km._check_kernel_id(kernel_id)
64 model = km.kernel_model(kernel_id, self.ws_url)
65 self.finish(jsonapi.dumps(model))
66
67 @web.authenticated
68 @json_errors
57 def delete(self, kernel_id):
69 def delete(self, kernel_id):
58 km = self.kernel_manager
70 km = self.kernel_manager
59 km.shutdown_kernel(kernel_id)
71 km.shutdown_kernel(kernel_id)
@@ -64,6 +76,7 b' class KernelHandler(IPythonHandler):'
64 class KernelActionHandler(IPythonHandler):
76 class KernelActionHandler(IPythonHandler):
65
77
66 @web.authenticated
78 @web.authenticated
79 @json_errors
67 def post(self, kernel_id, action):
80 def post(self, kernel_id, action):
68 km = self.kernel_manager
81 km = self.kernel_manager
69 if action == 'interrupt':
82 if action == 'interrupt':
@@ -71,9 +84,9 b' class KernelActionHandler(IPythonHandler):'
71 self.set_status(204)
84 self.set_status(204)
72 if action == 'restart':
85 if action == 'restart':
73 km.restart_kernel(kernel_id)
86 km.restart_kernel(kernel_id)
74 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
87 model = km.kernel_model(kernel_id, self.ws_url)
75 self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
76 self.write(jsonapi.dumps(data))
89 self.write(jsonapi.dumps(model))
77 self.finish()
90 self.finish()
78
91
79
92
@@ -173,10 +186,10 b' _kernel_id_regex = r"(?P<kernel_id>\\w+-\\w+-\\w+-\\w+-\\w+)"'
173 _kernel_action_regex = r"(?P<action>restart|interrupt)"
186 _kernel_action_regex = r"(?P<action>restart|interrupt)"
174
187
175 default_handlers = [
188 default_handlers = [
176 (r"/kernels", MainKernelHandler),
189 (r"/api/kernels", MainKernelHandler),
177 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
190 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
178 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
191 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
179 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
192 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
180 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
193 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
181 (r"/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
194 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
182 ]
195 ]
@@ -35,56 +35,29 b' class MappingKernelManager(MultiKernelManager):'
35 return "IPython.kernel.ioloop.IOLoopKernelManager"
35 return "IPython.kernel.ioloop.IOLoopKernelManager"
36
36
37 kernel_argv = List(Unicode)
37 kernel_argv = List(Unicode)
38
39 _notebook_mapping = Dict()
40
38
41 #-------------------------------------------------------------------------
39 #-------------------------------------------------------------------------
42 # Methods for managing kernels and sessions
40 # Methods for managing kernels and sessions
43 #-------------------------------------------------------------------------
41 #-------------------------------------------------------------------------
44
42
45 def kernel_for_notebook(self, notebook_id):
46 """Return the kernel_id for a notebook_id or None."""
47 return self._notebook_mapping.get(notebook_id)
48
49 def set_kernel_for_notebook(self, notebook_id, kernel_id):
50 """Associate a notebook with a kernel."""
51 if notebook_id is not None:
52 self._notebook_mapping[notebook_id] = kernel_id
53
54 def notebook_for_kernel(self, kernel_id):
55 """Return the notebook_id for a kernel_id or None."""
56 for notebook_id, kid in self._notebook_mapping.iteritems():
57 if kernel_id == kid:
58 return notebook_id
59 return None
60
61 def delete_mapping_for_kernel(self, kernel_id):
62 """Remove the kernel/notebook mapping for kernel_id."""
63 notebook_id = self.notebook_for_kernel(kernel_id)
64 if notebook_id is not None:
65 del self._notebook_mapping[notebook_id]
66
67 def _handle_kernel_died(self, kernel_id):
43 def _handle_kernel_died(self, kernel_id):
68 """notice that a kernel died"""
44 """notice that a kernel died"""
69 self.log.warn("Kernel %s died, removing from map.", kernel_id)
45 self.log.warn("Kernel %s died, removing from map.", kernel_id)
70 self.delete_mapping_for_kernel(kernel_id)
71 self.remove_kernel(kernel_id)
46 self.remove_kernel(kernel_id)
72
47
73 def start_kernel(self, notebook_id=None, **kwargs):
48 def start_kernel(self, kernel_id=None, **kwargs):
74 """Start a kernel for a notebook an return its kernel_id.
49 """Start a kernel for a session an return its kernel_id.
75
50
76 Parameters
51 Parameters
77 ----------
52 ----------
78 notebook_id : uuid
53 kernel_id : uuid
79 The uuid of the notebook to associate the new kernel with. If this
54 The uuid to associate the new kernel with. If this
80 is not None, this kernel will be persistent whenever the notebook
55 is not None, this kernel will be persistent whenever it is
81 requests a kernel.
56 requested.
82 """
57 """
83 kernel_id = self.kernel_for_notebook(notebook_id)
84 if kernel_id is None:
58 if kernel_id is None:
85 kwargs['extra_arguments'] = self.kernel_argv
59 kwargs['extra_arguments'] = self.kernel_argv
86 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
60 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
87 self.set_kernel_for_notebook(notebook_id, kernel_id)
88 self.log.info("Kernel started: %s" % kernel_id)
61 self.log.info("Kernel started: %s" % kernel_id)
89 self.log.debug("Kernel args: %r" % kwargs)
62 self.log.debug("Kernel args: %r" % kwargs)
90 # register callback for failed auto-restart
63 # register callback for failed auto-restart
@@ -93,18 +66,33 b' class MappingKernelManager(MultiKernelManager):'
93 'dead',
66 'dead',
94 )
67 )
95 else:
68 else:
69 self._check_kernel_id(kernel_id)
96 self.log.info("Using existing kernel: %s" % kernel_id)
70 self.log.info("Using existing kernel: %s" % kernel_id)
97
98 return kernel_id
71 return kernel_id
99
72
100 def shutdown_kernel(self, kernel_id, now=False):
73 def shutdown_kernel(self, kernel_id, now=False):
101 """Shutdown a kernel by kernel_id"""
74 """Shutdown a kernel by kernel_id"""
75 self._check_kernel_id(kernel_id)
102 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
76 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
103 self.delete_mapping_for_kernel(kernel_id)
77
78 def kernel_model(self, kernel_id, ws_url):
79 """Return a dictionary of kernel information described in the
80 JSON standard model."""
81 self._check_kernel_id(kernel_id)
82 model = {"id":kernel_id, "ws_url": ws_url}
83 return model
84
85 def list_kernels(self, ws_url):
86 """Returns a list of kernel_id's of kernels running."""
87 kernels = []
88 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
89 for kernel_id in kernel_ids:
90 model = self.kernel_model(kernel_id, ws_url)
91 kernels.append(model)
92 return kernels
104
93
105 # override _check_kernel_id to raise 404 instead of KeyError
94 # override _check_kernel_id to raise 404 instead of KeyError
106 def _check_kernel_id(self, kernel_id):
95 def _check_kernel_id(self, kernel_id):
107 """Check a that a kernel_id exists and raise 404 if not."""
96 """Check a that a kernel_id exists and raise 404 if not."""
108 if kernel_id not in self:
97 if kernel_id not in self:
109 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
98 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
110
@@ -3,6 +3,7 b''
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 """
7 """
7
8
8 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
@@ -16,12 +17,11 b' Authors:'
16 # Imports
17 # Imports
17 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
18
19
19 import datetime
20 import io
20 import io
21 import itertools
21 import os
22 import os
22 import glob
23 import glob
23 import shutil
24 import shutil
24 from unicodedata import normalize
25
25
26 from tornado import web
26 from tornado import web
27
27
@@ -70,290 +70,340 b' class FileNotebookManager(NotebookManager):'
70 os.mkdir(new)
70 os.mkdir(new)
71 except:
71 except:
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73
74 filename_ext = Unicode(u'.ipynb')
75
73
76 # Map notebook names to notebook_ids
74 def get_notebook_names(self, path=''):
77 rev_mapping = Dict()
75 """List all notebook names in the notebook dir and path."""
78
76 path = path.strip('/')
79 def get_notebook_names(self):
77 if not os.path.isdir(self.get_os_path(path=path)):
80 """List all notebook names in the notebook dir."""
78 raise web.HTTPError(404, 'Directory not found: ' + path)
81 names = glob.glob(os.path.join(self.notebook_dir,
79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
82 '*' + self.filename_ext))
80 names = [os.path.basename(name)
83 names = [normalize('NFC', os.path.splitext(os.path.basename(name))[0])
84 for name in names]
81 for name in names]
85 return names
82 return names
86
83
87 def list_notebooks(self):
84 def increment_filename(self, basename, path='', ext='.ipynb'):
88 """List all notebooks in the notebook dir."""
85 """Return a non-used filename of the form basename<int>."""
89 names = self.get_notebook_names()
86 path = path.strip('/')
90
87 for i in itertools.count():
91 data = []
88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
92 for name in names:
89 os_path = self.get_os_path(name, path)
93 if name not in self.rev_mapping:
90 if not os.path.isfile(os_path):
94 notebook_id = self.new_notebook_id(name)
91 break
95 else:
96 notebook_id = self.rev_mapping[name]
97 data.append(dict(notebook_id=notebook_id,name=name))
98 data = sorted(data, key=lambda item: item['name'])
99 return data
100
101 def new_notebook_id(self, name):
102 """Generate a new notebook_id for a name and store its mappings."""
103 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
104 self.rev_mapping[name] = notebook_id
105 return notebook_id
106
107 def delete_notebook_id(self, notebook_id):
108 """Delete a notebook's id in the mapping."""
109 name = self.mapping[notebook_id]
110 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
111 del self.rev_mapping[name]
112
113 def notebook_exists(self, notebook_id):
114 """Does a notebook exist?"""
115 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
116 if not exists:
117 return False
118 path = self.get_path_by_name(self.mapping[notebook_id])
119 return os.path.isfile(path)
120
121 def get_name(self, notebook_id):
122 """get a notebook name, raising 404 if not found"""
123 try:
124 name = self.mapping[notebook_id]
125 except KeyError:
126 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
127 return name
92 return name
128
93
129 def get_path(self, notebook_id):
94 def path_exists(self, path):
130 """Return a full path to a notebook given its notebook_id."""
95 """Does the API-style path (directory) actually exist?
131 name = self.get_name(notebook_id)
96
132 return self.get_path_by_name(name)
97 Parameters
98 ----------
99 path : string
100 The path to check. This is an API path (`/` separated,
101 relative to base notebook-dir).
102
103 Returns
104 -------
105 exists : bool
106 Whether the path is indeed a directory.
107 """
108 path = path.strip('/')
109 os_path = self.get_os_path(path=path)
110 return os.path.isdir(os_path)
111
112 def get_os_path(self, name=None, path=''):
113 """Given a notebook name and a URL path, return its file system
114 path.
115
116 Parameters
117 ----------
118 name : string
119 The name of a notebook file with the .ipynb extension
120 path : string
121 The relative URL path (with '/' as separator) to the named
122 notebook.
133
123
134 def get_path_by_name(self, name):
124 Returns
135 """Return a full path to a notebook given its name."""
125 -------
136 filename = name + self.filename_ext
126 path : string
137 path = os.path.join(self.notebook_dir, filename)
127 A file system path that combines notebook_dir (location where
128 server started), the relative path, and the filename with the
129 current operating system's url.
130 """
131 parts = path.strip('/').split('/')
132 parts = [p for p in parts if p != ''] # remove duplicate splits
133 if name is not None:
134 parts.append(name)
135 path = os.path.join(self.notebook_dir, *parts)
138 return path
136 return path
139
137
140 def read_notebook_object_from_path(self, path):
138 def notebook_exists(self, name, path=''):
141 """read a notebook object from a path"""
139 """Returns a True if the notebook exists. Else, returns False.
142 info = os.stat(path)
140
141 Parameters
142 ----------
143 name : string
144 The name of the notebook you are checking.
145 path : string
146 The relative path to the notebook (with '/' as separator)
147
148 Returns
149 -------
150 bool
151 """
152 path = path.strip('/')
153 nbpath = self.get_os_path(name, path=path)
154 return os.path.isfile(nbpath)
155
156 def list_notebooks(self, path):
157 """Returns a list of dictionaries that are the standard model
158 for all notebooks in the relative 'path'.
159
160 Parameters
161 ----------
162 path : str
163 the URL path that describes the relative path for the
164 listed notebooks
165
166 Returns
167 -------
168 notebooks : list of dicts
169 a list of the notebook models without 'content'
170 """
171 path = path.strip('/')
172 notebook_names = self.get_notebook_names(path)
173 notebooks = []
174 for name in notebook_names:
175 model = self.get_notebook_model(name, path, content=False)
176 notebooks.append(model)
177 notebooks = sorted(notebooks, key=lambda item: item['name'])
178 return notebooks
179
180 def get_notebook_model(self, name, path='', content=True):
181 """ Takes a path and name for a notebook and returns it's model
182
183 Parameters
184 ----------
185 name : str
186 the name of the notebook
187 path : str
188 the URL path that describes the relative path for
189 the notebook
190
191 Returns
192 -------
193 model : dict
194 the notebook model. If contents=True, returns the 'contents'
195 dict in the model as well.
196 """
197 path = path.strip('/')
198 if not self.notebook_exists(name=name, path=path):
199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
200 os_path = self.get_os_path(name, path)
201 info = os.stat(os_path)
143 last_modified = tz.utcfromtimestamp(info.st_mtime)
202 last_modified = tz.utcfromtimestamp(info.st_mtime)
144 with open(path,'r') as f:
203 created = tz.utcfromtimestamp(info.st_ctime)
145 s = f.read()
204 # Create the notebook model.
146 try:
205 model ={}
147 # v1 and v2 and json in the .ipynb files.
206 model['name'] = name
148 nb = current.reads(s, u'json')
207 model['path'] = path
149 except ValueError as e:
208 model['last_modified'] = last_modified
150 msg = u"Unreadable Notebook: %s" % e
209 model['created'] = created
151 raise web.HTTPError(400, msg, reason=msg)
210 if content is True:
152 return last_modified, nb
211 with io.open(os_path, 'r', encoding='utf-8') as f:
153
212 try:
154 def read_notebook_object(self, notebook_id):
213 nb = current.read(f, u'json')
155 """Get the Notebook representation of a notebook by notebook_id."""
214 except Exception as e:
156 path = self.get_path(notebook_id)
215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
157 if not os.path.isfile(path):
216 model['content'] = nb
158 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
217 return model
159 last_modified, nb = self.read_notebook_object_from_path(path)
160 # Always use the filename as the notebook name.
161 # Eventually we will get rid of the notebook name in the metadata
162 # but for now, that name is just an empty string. Until the notebooks
163 # web service knows about names in URLs we still pass the name
164 # back to the web app using the metadata though.
165 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
166 return last_modified, nb
167
168 def write_notebook_object(self, nb, notebook_id=None):
169 """Save an existing notebook object by notebook_id."""
170 try:
171 new_name = normalize('NFC', nb.metadata.name)
172 except AttributeError:
173 raise web.HTTPError(400, u'Missing notebook name')
174
218
175 if notebook_id is None:
219 def save_notebook_model(self, model, name='', path=''):
176 notebook_id = self.new_notebook_id(new_name)
220 """Save the notebook model and return the model with no content."""
221 path = path.strip('/')
177
222
178 if notebook_id not in self.mapping:
223 if 'content' not in model:
179 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
224 raise web.HTTPError(400, u'No notebook JSON data provided')
180
225
181 old_name = self.mapping[notebook_id]
226 new_path = model.get('path', path).strip('/')
182 old_checkpoints = self.list_checkpoints(notebook_id)
227 new_name = model.get('name', name)
183 path = self.get_path_by_name(new_name)
184
228
185 # Right before we save the notebook, we write an empty string as the
229 if path != new_path or name != new_name:
186 # notebook name in the metadata. This is to prepare for removing
230 self.rename_notebook(name, path, new_name, new_path)
187 # this attribute entirely post 1.0. The web app still uses the metadata
188 # name for now.
189 nb.metadata.name = u''
190
231
232 # Save the notebook file
233 os_path = self.get_os_path(new_name, new_path)
234 nb = current.to_notebook_json(model['content'])
235 if 'name' in nb['metadata']:
236 nb['metadata']['name'] = u''
191 try:
237 try:
192 self.log.debug("Autosaving notebook %s", path)
238 self.log.debug("Autosaving notebook %s", os_path)
193 with open(path,'w') as f:
239 with io.open(os_path, 'w', encoding='utf-8') as f:
194 current.write(nb, f, u'json')
240 current.write(nb, f, u'json')
195 except Exception as e:
241 except Exception as e:
196 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
242 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
197
243
198 # save .py script as well
244 # Save .py script as well
199 if self.save_script:
245 if self.save_script:
200 pypath = os.path.splitext(path)[0] + '.py'
246 py_path = os.path.splitext(os_path)[0] + '.py'
201 self.log.debug("Writing script %s", pypath)
247 self.log.debug("Writing script %s", py_path)
202 try:
248 try:
203 with io.open(pypath,'w', encoding='utf-8') as f:
249 with io.open(py_path, 'w', encoding='utf-8') as f:
204 current.write(nb, f, u'py')
250 current.write(model, f, u'py')
205 except Exception as e:
251 except Exception as e:
206 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
252 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
207
253
208 # remove old files if the name changed
254 model = self.get_notebook_model(new_name, new_path, content=False)
209 if old_name != new_name:
255 return model
210 # update mapping
211 self.mapping[notebook_id] = new_name
212 self.rev_mapping[new_name] = notebook_id
213 del self.rev_mapping[old_name]
214
215 # remove renamed original, if it exists
216 old_path = self.get_path_by_name(old_name)
217 if os.path.isfile(old_path):
218 self.log.debug("unlinking notebook %s", old_path)
219 os.unlink(old_path)
220
221 # cleanup old script, if it exists
222 if self.save_script:
223 old_pypath = os.path.splitext(old_path)[0] + '.py'
224 if os.path.isfile(old_pypath):
225 self.log.debug("unlinking script %s", old_pypath)
226 os.unlink(old_pypath)
227
228 # rename checkpoints to follow file
229 for cp in old_checkpoints:
230 checkpoint_id = cp['checkpoint_id']
231 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
232 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
233 if os.path.isfile(old_cp_path):
234 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
235 os.rename(old_cp_path, new_cp_path)
236
237 return notebook_id
238
256
239 def delete_notebook(self, notebook_id):
257 def update_notebook_model(self, model, name, path=''):
240 """Delete notebook by notebook_id."""
258 """Update the notebook's path and/or name"""
241 nb_path = self.get_path(notebook_id)
259 path = path.strip('/')
242 if not os.path.isfile(nb_path):
260 new_name = model.get('name', name)
243 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
261 new_path = model.get('path', path).strip('/')
262 if path != new_path or name != new_name:
263 self.rename_notebook(name, path, new_name, new_path)
264 model = self.get_notebook_model(new_name, new_path, content=False)
265 return model
266
267 def delete_notebook_model(self, name, path=''):
268 """Delete notebook by name and path."""
269 path = path.strip('/')
270 os_path = self.get_os_path(name, path)
271 if not os.path.isfile(os_path):
272 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
244
273
245 # clear checkpoints
274 # clear checkpoints
246 for checkpoint in self.list_checkpoints(notebook_id):
275 for checkpoint in self.list_checkpoints(name, path):
247 checkpoint_id = checkpoint['checkpoint_id']
276 checkpoint_id = checkpoint['id']
248 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
277 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
249 self.log.debug(path)
278 if os.path.isfile(cp_path):
250 if os.path.isfile(path):
279 self.log.debug("Unlinking checkpoint %s", cp_path)
251 self.log.debug("unlinking checkpoint %s", path)
280 os.unlink(cp_path)
252 os.unlink(path)
253
281
254 self.log.debug("unlinking notebook %s", nb_path)
282 self.log.debug("Unlinking notebook %s", os_path)
255 os.unlink(nb_path)
283 os.unlink(os_path)
256 self.delete_notebook_id(notebook_id)
257
284
258 def increment_filename(self, basename):
285 def rename_notebook(self, old_name, old_path, new_name, new_path):
259 """Return a non-used filename of the form basename<int>.
286 """Rename a notebook."""
287 old_path = old_path.strip('/')
288 new_path = new_path.strip('/')
289 if new_name == old_name and new_path == old_path:
290 return
260
291
261 This searches through the filenames (basename0, basename1, ...)
292 new_os_path = self.get_os_path(new_name, new_path)
262 until is find one that is not already being used. It is used to
293 old_os_path = self.get_os_path(old_name, old_path)
263 create Untitled and Copy names that are unique.
294
264 """
295 # Should we proceed with the move?
265 i = 0
296 if os.path.isfile(new_os_path):
266 while True:
297 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
267 name = u'%s%i' % (basename,i)
298 if self.save_script:
268 path = self.get_path_by_name(name)
299 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
269 if not os.path.isfile(path):
300 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
270 break
301 if os.path.isfile(new_py_path):
271 else:
302 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
272 i = i+1
303
273 return name
304 # Move the notebook file
274
305 try:
306 os.rename(old_os_path, new_os_path)
307 except Exception as e:
308 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
309
310 # Move the checkpoints
311 old_checkpoints = self.list_checkpoints(old_name, old_path)
312 for cp in old_checkpoints:
313 checkpoint_id = cp['id']
314 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
315 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
316 if os.path.isfile(old_cp_path):
317 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
318 os.rename(old_cp_path, new_cp_path)
319
320 # Move the .py script
321 if self.save_script:
322 os.rename(old_py_path, new_py_path)
323
275 # Checkpoint-related utilities
324 # Checkpoint-related utilities
276
325
277 def get_checkpoint_path_by_name(self, name, checkpoint_id):
326 def get_checkpoint_path(self, checkpoint_id, name, path=''):
278 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
327 """find the path to a checkpoint"""
328 path = path.strip('/')
279 filename = u"{name}-{checkpoint_id}{ext}".format(
329 filename = u"{name}-{checkpoint_id}{ext}".format(
280 name=name,
330 name=name,
281 checkpoint_id=checkpoint_id,
331 checkpoint_id=checkpoint_id,
282 ext=self.filename_ext,
332 ext=self.filename_ext,
283 )
333 )
284 path = os.path.join(self.checkpoint_dir, filename)
334 cp_path = os.path.join(path, self.checkpoint_dir, filename)
285 return path
335 return cp_path
286
336
287 def get_checkpoint_path(self, notebook_id, checkpoint_id):
337 def get_checkpoint_model(self, checkpoint_id, name, path=''):
288 """find the path to a checkpoint"""
289 name = self.get_name(notebook_id)
290 return self.get_checkpoint_path_by_name(name, checkpoint_id)
291
292 def get_checkpoint_info(self, notebook_id, checkpoint_id):
293 """construct the info dict for a given checkpoint"""
338 """construct the info dict for a given checkpoint"""
294 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
339 path = path.strip('/')
295 stats = os.stat(path)
340 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
341 stats = os.stat(cp_path)
296 last_modified = tz.utcfromtimestamp(stats.st_mtime)
342 last_modified = tz.utcfromtimestamp(stats.st_mtime)
297 info = dict(
343 info = dict(
298 checkpoint_id = checkpoint_id,
344 id = checkpoint_id,
299 last_modified = last_modified,
345 last_modified = last_modified,
300 )
346 )
301
302 return info
347 return info
303
348
304 # public checkpoint API
349 # public checkpoint API
305
350
306 def create_checkpoint(self, notebook_id):
351 def create_checkpoint(self, name, path=''):
307 """Create a checkpoint from the current state of a notebook"""
352 """Create a checkpoint from the current state of a notebook"""
308 nb_path = self.get_path(notebook_id)
353 path = path.strip('/')
354 nb_path = self.get_os_path(name, path)
309 # only the one checkpoint ID:
355 # only the one checkpoint ID:
310 checkpoint_id = u"checkpoint"
356 checkpoint_id = u"checkpoint"
311 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
357 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
312 self.log.debug("creating checkpoint for notebook %s", notebook_id)
358 self.log.debug("creating checkpoint for notebook %s", name)
313 if not os.path.exists(self.checkpoint_dir):
359 if not os.path.exists(self.checkpoint_dir):
314 os.mkdir(self.checkpoint_dir)
360 os.mkdir(self.checkpoint_dir)
315 shutil.copy2(nb_path, cp_path)
361 shutil.copy2(nb_path, cp_path)
316
362
317 # return the checkpoint info
363 # return the checkpoint info
318 return self.get_checkpoint_info(notebook_id, checkpoint_id)
364 return self.get_checkpoint_model(checkpoint_id, name, path)
319
365
320 def list_checkpoints(self, notebook_id):
366 def list_checkpoints(self, name, path=''):
321 """list the checkpoints for a given notebook
367 """list the checkpoints for a given notebook
322
368
323 This notebook manager currently only supports one checkpoint per notebook.
369 This notebook manager currently only supports one checkpoint per notebook.
324 """
370 """
325 checkpoint_id = u"checkpoint"
371 path = path.strip('/')
326 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
372 checkpoint_id = "checkpoint"
373 path = self.get_checkpoint_path(checkpoint_id, name, path)
327 if not os.path.exists(path):
374 if not os.path.exists(path):
328 return []
375 return []
329 else:
376 else:
330 return [self.get_checkpoint_info(notebook_id, checkpoint_id)]
377 return [self.get_checkpoint_model(checkpoint_id, name, path)]
331
378
332
379
333 def restore_checkpoint(self, notebook_id, checkpoint_id):
380 def restore_checkpoint(self, checkpoint_id, name, path=''):
334 """restore a notebook to a checkpointed state"""
381 """restore a notebook to a checkpointed state"""
335 self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
382 path = path.strip('/')
336 nb_path = self.get_path(notebook_id)
383 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
337 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
384 nb_path = self.get_os_path(name, path)
385 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
338 if not os.path.isfile(cp_path):
386 if not os.path.isfile(cp_path):
339 self.log.debug("checkpoint file does not exist: %s", cp_path)
387 self.log.debug("checkpoint file does not exist: %s", cp_path)
340 raise web.HTTPError(404,
388 raise web.HTTPError(404,
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
389 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
342 )
390 )
343 # ensure notebook is readable (never restore from an unreadable notebook)
391 # ensure notebook is readable (never restore from an unreadable notebook)
344 last_modified, nb = self.read_notebook_object_from_path(cp_path)
392 with io.open(cp_path, 'r', encoding='utf-8') as f:
393 nb = current.read(f, u'json')
345 shutil.copy2(cp_path, nb_path)
394 shutil.copy2(cp_path, nb_path)
346 self.log.debug("copying %s -> %s", cp_path, nb_path)
395 self.log.debug("copying %s -> %s", cp_path, nb_path)
347
396
348 def delete_checkpoint(self, notebook_id, checkpoint_id):
397 def delete_checkpoint(self, checkpoint_id, name, path=''):
349 """delete a notebook's checkpoint"""
398 """delete a notebook's checkpoint"""
350 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
399 path = path.strip('/')
351 if not os.path.isfile(path):
400 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
401 if not os.path.isfile(cp_path):
352 raise web.HTTPError(404,
402 raise web.HTTPError(404,
353 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
403 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
354 )
404 )
355 self.log.debug("unlinking %s", path)
405 self.log.debug("unlinking %s", cp_path)
356 os.unlink(path)
406 os.unlink(cp_path)
357
407
358 def info_string(self):
408 def info_string(self):
359 return "Serving notebooks from local directory: %s" % self.notebook_dir
409 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -6,7 +6,7 b' Authors:'
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 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.
@@ -16,74 +16,193 b' Authors:'
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 from tornado import web
19 import json
20
20
21 from zmq.utils import jsonapi
21 from tornado import web
22
22
23 from IPython.html.utils import url_path_join, url_escape
23 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
24
25
25 from ...base.handlers import IPythonHandler
26 from IPython.html.base.handlers import IPythonHandler, json_errors
26
27
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28 # Notebook web service handlers
29 # Notebook web service handlers
29 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
30
31
31 class NotebookRootHandler(IPythonHandler):
32
32
33 @web.authenticated
33 class NotebookHandler(IPythonHandler):
34 def get(self):
35 nbm = self.notebook_manager
36 km = self.kernel_manager
37 files = nbm.list_notebooks()
38 for f in files :
39 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
40 self.finish(jsonapi.dumps(files))
41
34
42 @web.authenticated
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
43 def post(self):
44 nbm = self.notebook_manager
45 body = self.request.body.strip()
46 format = self.get_argument('format', default='json')
47 name = self.get_argument('name', default=None)
48 if body:
49 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
50 else:
51 notebook_id = nbm.new_notebook()
52 self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
53 self.finish(jsonapi.dumps(notebook_id))
54
36
37 def notebook_location(self, name, path=''):
38 """Return the full URL location of a notebook based.
39
40 Parameters
41 ----------
42 name : unicode
43 The base name of the notebook, such as "foo.ipynb".
44 path : unicode
45 The URL path of the notebook.
46 """
47 return url_escape(url_path_join(
48 self.base_project_url, 'api', 'notebooks', path, name
49 ))
55
50
56 class NotebookHandler(IPythonHandler):
51 def _finish_model(self, model, location=True):
52 """Finish a JSON request with a model, setting relevant headers, etc."""
53 if location:
54 location = self.notebook_location(model['name'], model['path'])
55 self.set_header('Location', location)
56 self.set_header('Last-Modified', model['last_modified'])
57 self.finish(json.dumps(model, default=date_default))
58
59 @web.authenticated
60 @json_errors
61 def get(self, path='', name=None):
62 """Return a Notebook or list of notebooks.
57
63
58 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
64 * GET with path and no notebook name lists notebooks in a directory
65 * GET with path and notebook name returns notebook JSON
66 """
67 nbm = self.notebook_manager
68 # Check to see if a notebook name was given
69 if name is None:
70 # List notebooks in 'path'
71 notebooks = nbm.list_notebooks(path)
72 self.finish(json.dumps(notebooks, default=date_default))
73 return
74 # get and return notebook representation
75 model = nbm.get_notebook_model(name, path)
76 self._finish_model(model, location=False)
59
77
60 @web.authenticated
78 @web.authenticated
61 def get(self, notebook_id):
79 @json_errors
80 def patch(self, path='', name=None):
81 """PATCH renames a notebook without re-uploading content."""
62 nbm = self.notebook_manager
82 nbm = self.notebook_manager
63 format = self.get_argument('format', default='json')
83 if name is None:
64 last_mod, name, data = nbm.get_notebook(notebook_id, format)
84 raise web.HTTPError(400, u'Notebook name missing')
85 model = self.get_json_body()
86 if model is None:
87 raise web.HTTPError(400, u'JSON body missing')
88 model = nbm.update_notebook_model(model, name, path)
89 self._finish_model(model)
90
91 def _copy_notebook(self, copy_from, path, copy_to=None):
92 """Copy a notebook in path, optionally specifying the new name.
65
93
66 if format == u'json':
94 Only support copying within the same directory.
67 self.set_header('Content-Type', 'application/json')
95 """
68 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
96 self.log.info(u"Copying notebook from %s/%s to %s/%s",
69 elif format == u'py':
97 path, copy_from,
70 self.set_header('Content-Type', 'application/x-python')
98 path, copy_to or '',
71 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
99 )
72 self.set_header('Last-Modified', last_mod)
100 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
73 self.finish(data)
101 self.set_status(201)
102 self._finish_model(model)
103
104 def _upload_notebook(self, model, path, name=None):
105 """Upload a notebook
106
107 If name specified, create it in path/name.
108 """
109 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
110 if name:
111 model['name'] = name
112
113 model = self.notebook_manager.create_notebook_model(model, path)
114 self.set_status(201)
115 self._finish_model(model)
116
117 def _create_empty_notebook(self, path, name=None):
118 """Create an empty notebook in path
119
120 If name specified, create it in path/name.
121 """
122 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
123 model = {}
124 if name:
125 model['name'] = name
126 model = self.notebook_manager.create_notebook_model(model, path=path)
127 self.set_status(201)
128 self._finish_model(model)
129
130 def _save_notebook(self, model, path, name):
131 """Save an existing notebook."""
132 self.log.info(u"Saving notebook at %s/%s", path, name)
133 model = self.notebook_manager.save_notebook_model(model, name, path)
134 if model['path'] != path.strip('/') or model['name'] != name:
135 # a rename happened, set Location header
136 location = True
137 else:
138 location = False
139 self._finish_model(model, location)
140
141 @web.authenticated
142 @json_errors
143 def post(self, path='', name=None):
144 """Create a new notebook in the specified path.
145
146 POST creates new notebooks. The server always decides on the notebook name.
147
148 POST /api/notebooks/path : new untitled notebook in path
149 If content specified, upload a notebook, otherwise start empty.
150 POST /api/notebooks/path?copy=OtherNotebook.ipynb : new copy of OtherNotebook in path
151 """
152
153 if name is not None:
154 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
155
156 model = self.get_json_body()
157
158 if model is not None:
159 copy_from = model.get('copy_from')
160 if copy_from:
161 if model.get('content'):
162 raise web.HTTPError(400, "Can't upload and copy at the same time.")
163 self._copy_notebook(copy_from, path)
164 else:
165 self._upload_notebook(model, path)
166 else:
167 self._create_empty_notebook(path)
74
168
75 @web.authenticated
169 @web.authenticated
76 def put(self, notebook_id):
170 @json_errors
77 nbm = self.notebook_manager
171 def put(self, path='', name=None):
78 format = self.get_argument('format', default='json')
172 """Saves the notebook in the location specified by name and path.
79 name = self.get_argument('name', default=None)
173
80 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
174 PUT /api/notebooks/path/Name.ipynb : Save notebook at path/Name.ipynb
81 self.set_status(204)
175 Notebook structure is specified in `content` key of JSON request body.
82 self.finish()
176 If content is not specified, create a new empty notebook.
177 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb : copy OtherNotebook to Name
178
179 POST and PUT are basically the same. The only difference:
180
181 - with POST, server always picks the name, with PUT the requester does
182 """
183 if name is None:
184 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
185
186 model = self.get_json_body()
187 if model:
188 copy_from = model.get('copy_from')
189 if copy_from:
190 if model.get('content'):
191 raise web.HTTPError(400, "Can't upload and copy at the same time.")
192 self._copy_notebook(copy_from, path, name)
193 elif self.notebook_manager.notebook_exists(name, path):
194 self._save_notebook(model, path, name)
195 else:
196 self._upload_notebook(model, path, name)
197 else:
198 self._create_empty_notebook(path, name)
83
199
84 @web.authenticated
200 @web.authenticated
85 def delete(self, notebook_id):
201 @json_errors
86 self.notebook_manager.delete_notebook(notebook_id)
202 def delete(self, path='', name=None):
203 """delete the notebook in the given notebook path"""
204 nbm = self.notebook_manager
205 nbm.delete_notebook_model(name, path)
87 self.set_status(204)
206 self.set_status(204)
88 self.finish()
207 self.finish()
89
208
@@ -93,23 +212,25 b' class NotebookCheckpointsHandler(IPythonHandler):'
93 SUPPORTED_METHODS = ('GET', 'POST')
212 SUPPORTED_METHODS = ('GET', 'POST')
94
213
95 @web.authenticated
214 @web.authenticated
96 def get(self, notebook_id):
215 @json_errors
216 def get(self, path='', name=None):
97 """get lists checkpoints for a notebook"""
217 """get lists checkpoints for a notebook"""
98 nbm = self.notebook_manager
218 nbm = self.notebook_manager
99 checkpoints = nbm.list_checkpoints(notebook_id)
219 checkpoints = nbm.list_checkpoints(name, path)
100 data = jsonapi.dumps(checkpoints, default=date_default)
220 data = json.dumps(checkpoints, default=date_default)
101 self.finish(data)
221 self.finish(data)
102
222
103 @web.authenticated
223 @web.authenticated
104 def post(self, notebook_id):
224 @json_errors
225 def post(self, path='', name=None):
105 """post creates a new checkpoint"""
226 """post creates a new checkpoint"""
106 nbm = self.notebook_manager
227 nbm = self.notebook_manager
107 checkpoint = nbm.create_checkpoint(notebook_id)
228 checkpoint = nbm.create_checkpoint(name, path)
108 data = jsonapi.dumps(checkpoint, default=date_default)
229 data = json.dumps(checkpoint, default=date_default)
109 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
230 location = url_path_join(self.base_project_url, 'api/notebooks',
110 self.base_project_url, notebook_id, checkpoint['checkpoint_id']
231 path, name, 'checkpoints', checkpoint['id'])
111 ))
232 self.set_header('Location', url_escape(location))
112
233 self.set_status(201)
113 self.finish(data)
234 self.finish(data)
114
235
115
236
@@ -118,39 +239,40 b' class ModifyNotebookCheckpointsHandler(IPythonHandler):'
118 SUPPORTED_METHODS = ('POST', 'DELETE')
239 SUPPORTED_METHODS = ('POST', 'DELETE')
119
240
120 @web.authenticated
241 @web.authenticated
121 def post(self, notebook_id, checkpoint_id):
242 @json_errors
243 def post(self, path, name, checkpoint_id):
122 """post restores a notebook from a checkpoint"""
244 """post restores a notebook from a checkpoint"""
123 nbm = self.notebook_manager
245 nbm = self.notebook_manager
124 nbm.restore_checkpoint(notebook_id, checkpoint_id)
246 nbm.restore_checkpoint(checkpoint_id, name, path)
125 self.set_status(204)
247 self.set_status(204)
126 self.finish()
248 self.finish()
127
249
128 @web.authenticated
250 @web.authenticated
129 def delete(self, notebook_id, checkpoint_id):
251 @json_errors
252 def delete(self, path, name, checkpoint_id):
130 """delete clears a checkpoint for a given notebook"""
253 """delete clears a checkpoint for a given notebook"""
131 nbm = self.notebook_manager
254 nbm = self.notebook_manager
132 nbm.delte_checkpoint(notebook_id, checkpoint_id)
255 nbm.delete_checkpoint(checkpoint_id, name, path)
133 self.set_status(204)
256 self.set_status(204)
134 self.finish()
257 self.finish()
135
258
136
137 #-----------------------------------------------------------------------------
259 #-----------------------------------------------------------------------------
138 # URL to handler mappings
260 # URL to handler mappings
139 #-----------------------------------------------------------------------------
261 #-----------------------------------------------------------------------------
140
262
141
263
142 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
264 _path_regex = r"(?P<path>(?:/.*)*)"
143 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
265 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
266 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
267 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
144
268
145 default_handlers = [
269 default_handlers = [
146 (r"/notebooks", NotebookRootHandler),
270 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
147 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
271 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
148 (r"/notebooks/%s/checkpoints" % _notebook_id_regex, NotebookCheckpointsHandler),
272 ModifyNotebookCheckpointsHandler),
149 (r"/notebooks/%s/checkpoints/%s" % (_notebook_id_regex, _checkpoint_id_regex),
273 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
150 ModifyNotebookCheckpointsHandler
274 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
151 ),
152 ]
275 ]
153
276
154
277
155
278
156
@@ -3,6 +3,7 b''
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 """
7 """
7
8
8 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
@@ -17,9 +18,6 b' Authors:'
17 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
18
19
19 import os
20 import os
20 import uuid
21
22 from tornado import web
23
21
24 from IPython.config.configurable import LoggingConfigurable
22 from IPython.config.configurable import LoggingConfigurable
25 from IPython.nbformat import current
23 from IPython.nbformat import current
@@ -38,14 +36,33 b' class NotebookManager(LoggingConfigurable):'
38 # Right now we use this attribute in a number of different places and
36 # Right now we use this attribute in a number of different places and
39 # we are going to have to disentangle all of this.
37 # we are going to have to disentangle all of this.
40 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
38 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 The directory to use for notebooks.
39 The directory to use for notebooks.
42 """)
40 """)
41
42 filename_ext = Unicode(u'.ipynb')
43
44 def path_exists(self, path):
45 """Does the API-style path (directory) actually exist?
46
47 Override this method in subclasses.
48
49 Parameters
50 ----------
51 path : string
52 The
53
54 Returns
55 -------
56 exists : bool
57 Whether the path does indeed exist.
58 """
59 raise NotImplementedError
60
43 def _notebook_dir_changed(self, name, old, new):
61 def _notebook_dir_changed(self, name, old, new):
44 """do a bit of validation of the notebook dir"""
62 """Do a bit of validation of the notebook dir."""
45 if not os.path.isabs(new):
63 if not os.path.isabs(new):
46 # If we receive a non-absolute path, make it absolute.
64 # If we receive a non-absolute path, make it absolute.
47 abs_new = os.path.abspath(new)
65 self.notebook_dir = os.path.abspath(new)
48 self.notebook_dir = abs_new
49 return
66 return
50 if os.path.exists(new) and not os.path.isdir(new):
67 if os.path.exists(new) and not os.path.isdir(new):
51 raise TraitError("notebook dir %r is not a directory" % new)
68 raise TraitError("notebook dir %r is not a directory" % new)
@@ -56,22 +73,22 b' class NotebookManager(LoggingConfigurable):'
56 except:
73 except:
57 raise TraitError("Couldn't create notebook dir %r" % new)
74 raise TraitError("Couldn't create notebook dir %r" % new)
58
75
59 allowed_formats = List([u'json',u'py'])
76 # Main notebook API
60
61 # Map notebook_ids to notebook names
62 mapping = Dict()
63
64 def load_notebook_names(self):
65 """Load the notebook names into memory.
66
77
67 This should be called once immediately after the notebook manager
78 def increment_filename(self, basename, path=''):
68 is created to load the existing notebooks into the mapping in
79 """Increment a notebook filename without the .ipynb to make it unique.
69 memory.
80
81 Parameters
82 ----------
83 basename : unicode
84 The name of a notebook without the ``.ipynb`` file extension.
85 path : unicode
86 The URL path of the notebooks directory
70 """
87 """
71 self.list_notebooks()
88 return basename
72
89
73 def list_notebooks(self):
90 def list_notebooks(self, path=''):
74 """List all notebooks.
91 """Return a list of notebook dicts without content.
75
92
76 This returns a list of dicts, each of the form::
93 This returns a list of dicts, each of the form::
77
94
@@ -83,147 +100,69 b' class NotebookManager(LoggingConfigurable):'
83 """
100 """
84 raise NotImplementedError('must be implemented in a subclass')
101 raise NotImplementedError('must be implemented in a subclass')
85
102
86
103 def get_notebook_model(self, name, path='', content=True):
87 def new_notebook_id(self, name):
104 """Get the notebook model with or without content."""
88 """Generate a new notebook_id for a name and store its mapping."""
89 # TODO: the following will give stable urls for notebooks, but unless
90 # the notebooks are immediately redirected to their new urls when their
91 # filemname changes, nasty inconsistencies result. So for now it's
92 # disabled and instead we use a random uuid4() call. But we leave the
93 # logic here so that we can later reactivate it, whhen the necessary
94 # url redirection code is written.
95 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
96 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
97
98 notebook_id = unicode(uuid.uuid4())
99 self.mapping[notebook_id] = name
100 return notebook_id
101
102 def delete_notebook_id(self, notebook_id):
103 """Delete a notebook's id in the mapping.
104
105 This doesn't delete the actual notebook, only its entry in the mapping.
106 """
107 del self.mapping[notebook_id]
108
109 def notebook_exists(self, notebook_id):
110 """Does a notebook exist?"""
111 return notebook_id in self.mapping
112
113 def get_notebook(self, notebook_id, format=u'json'):
114 """Get the representation of a notebook in format by notebook_id."""
115 format = unicode(format)
116 if format not in self.allowed_formats:
117 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
118 last_modified, nb = self.read_notebook_object(notebook_id)
119 kwargs = {}
120 if format == 'json':
121 # don't split lines for sending over the wire, because it
122 # should match the Python in-memory format.
123 kwargs['split_lines'] = False
124 data = current.writes(nb, format, **kwargs)
125 name = nb.metadata.get('name','notebook')
126 return last_modified, name, data
127
128 def read_notebook_object(self, notebook_id):
129 """Get the object representation of a notebook by notebook_id."""
130 raise NotImplementedError('must be implemented in a subclass')
105 raise NotImplementedError('must be implemented in a subclass')
131
106
132 def save_new_notebook(self, data, name=None, format=u'json'):
107 def save_notebook_model(self, model, name, path=''):
133 """Save a new notebook and return its notebook_id.
108 """Save the notebook model and return the model with no content."""
134
135 If a name is passed in, it overrides any values in the notebook data
136 and the value in the data is updated to use that value.
137 """
138 if format not in self.allowed_formats:
139 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
140
141 try:
142 nb = current.reads(data.decode('utf-8'), format)
143 except:
144 raise web.HTTPError(400, u'Invalid JSON data')
145
146 if name is None:
147 try:
148 name = nb.metadata.name
149 except AttributeError:
150 raise web.HTTPError(400, u'Missing notebook name')
151 nb.metadata.name = name
152
153 notebook_id = self.write_notebook_object(nb)
154 return notebook_id
155
156 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
157 """Save an existing notebook by notebook_id."""
158 if format not in self.allowed_formats:
159 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
160
161 try:
162 nb = current.reads(data.decode('utf-8'), format)
163 except:
164 raise web.HTTPError(400, u'Invalid JSON data')
165
166 if name is not None:
167 nb.metadata.name = name
168 self.write_notebook_object(nb, notebook_id)
169
170 def write_notebook_object(self, nb, notebook_id=None):
171 """Write a notebook object and return its notebook_id.
172
173 If notebook_id is None, this method should create a new notebook_id.
174 If notebook_id is not None, this method should check to make sure it
175 exists and is valid.
176 """
177 raise NotImplementedError('must be implemented in a subclass')
109 raise NotImplementedError('must be implemented in a subclass')
178
110
179 def delete_notebook(self, notebook_id):
111 def update_notebook_model(self, model, name, path=''):
180 """Delete notebook by notebook_id."""
112 """Update the notebook model and return the model with no content."""
181 raise NotImplementedError('must be implemented in a subclass')
113 raise NotImplementedError('must be implemented in a subclass')
182
114
183 def increment_filename(self, name):
115 def delete_notebook_model(self, name, path=''):
184 """Increment a filename to make it unique.
116 """Delete notebook by name and path."""
117 raise NotImplementedError('must be implemented in a subclass')
185
118
186 This exists for notebook stores that must have unique names. When a notebook
119 def create_notebook_model(self, model=None, path=''):
187 is created or copied this method constructs a unique filename, typically
120 """Create a new untitled notebook and return its model with no content."""
188 by appending an integer to the name.
121 path = path.strip('/')
122 if model is None:
123 model = {}
124 if 'content' not in model:
125 metadata = current.new_metadata(name=u'')
126 model['content'] = current.new_notebook(metadata=metadata)
127 if 'name' not in model:
128 model['name'] = self.increment_filename('Untitled', path)
129
130 model['path'] = path
131 model = self.save_notebook_model(model, model['name'], model['path'])
132 return model
133
134 def copy_notebook(self, from_name, to_name=None, path=''):
135 """Copy an existing notebook and return its new model.
136
137 If to_name not specified, increment `from_name-Copy#.ipynb`.
189 """
138 """
190 return name
139 path = path.strip('/')
191
140 model = self.get_notebook_model(from_name, path)
192 def new_notebook(self):
141 if not to_name:
193 """Create a new notebook and return its notebook_id."""
142 base = os.path.splitext(from_name)[0] + '-Copy'
194 name = self.increment_filename('Untitled')
143 to_name = self.increment_filename(base, path)
195 metadata = current.new_metadata(name=name)
144 model['name'] = to_name
196 nb = current.new_notebook(metadata=metadata)
145 model = self.save_notebook_model(model, to_name, path)
197 notebook_id = self.write_notebook_object(nb)
146 return model
198 return notebook_id
199
200 def copy_notebook(self, notebook_id):
201 """Copy an existing notebook and return its notebook_id."""
202 last_mod, nb = self.read_notebook_object(notebook_id)
203 name = nb.metadata.name + '-Copy'
204 name = self.increment_filename(name)
205 nb.metadata.name = name
206 notebook_id = self.write_notebook_object(nb)
207 return notebook_id
208
147
209 # Checkpoint-related
148 # Checkpoint-related
210
149
211 def create_checkpoint(self, notebook_id):
150 def create_checkpoint(self, name, path=''):
212 """Create a checkpoint of the current state of a notebook
151 """Create a checkpoint of the current state of a notebook
213
152
214 Returns a checkpoint_id for the new checkpoint.
153 Returns a checkpoint_id for the new checkpoint.
215 """
154 """
216 raise NotImplementedError("must be implemented in a subclass")
155 raise NotImplementedError("must be implemented in a subclass")
217
156
218 def list_checkpoints(self, notebook_id):
157 def list_checkpoints(self, name, path=''):
219 """Return a list of checkpoints for a given notebook"""
158 """Return a list of checkpoints for a given notebook"""
220 return []
159 return []
221
160
222 def restore_checkpoint(self, notebook_id, checkpoint_id):
161 def restore_checkpoint(self, checkpoint_id, name, path=''):
223 """Restore a notebook from one of its checkpoints"""
162 """Restore a notebook from one of its checkpoints"""
224 raise NotImplementedError("must be implemented in a subclass")
163 raise NotImplementedError("must be implemented in a subclass")
225
164
226 def delete_checkpoint(self, notebook_id, checkpoint_id):
165 def delete_checkpoint(self, checkpoint_id, name, path=''):
227 """delete a checkpoint for a notebook"""
166 """delete a checkpoint for a notebook"""
228 raise NotImplementedError("must be implemented in a subclass")
167 raise NotImplementedError("must be implemented in a subclass")
229
168
@@ -232,4 +171,3 b' class NotebookManager(LoggingConfigurable):'
232
171
233 def info_string(self):
172 def info_string(self):
234 return "Serving notebooks"
173 return "Serving notebooks"
235
@@ -1,26 +1,31 b''
1 # coding: utf-8
1 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
2
3
3 import os
4 import os
5
6 from tornado.web import HTTPError
4 from unittest import TestCase
7 from unittest import TestCase
5 from tempfile import NamedTemporaryFile
8 from tempfile import NamedTemporaryFile
6
9
7 from IPython.utils.tempdir import TemporaryDirectory
10 from IPython.utils.tempdir import TemporaryDirectory
8 from IPython.utils.traitlets import TraitError
11 from IPython.utils.traitlets import TraitError
12 from IPython.html.utils import url_path_join
9
13
10 from ..filenbmanager import FileNotebookManager
14 from ..filenbmanager import FileNotebookManager
15 from ..nbmanager import NotebookManager
11
16
12 class TestNotebookManager(TestCase):
17 class TestFileNotebookManager(TestCase):
13
18
14 def test_nb_dir(self):
19 def test_nb_dir(self):
15 with TemporaryDirectory() as td:
20 with TemporaryDirectory() as td:
16 km = FileNotebookManager(notebook_dir=td)
21 fm = FileNotebookManager(notebook_dir=td)
17 self.assertEqual(km.notebook_dir, td)
22 self.assertEqual(fm.notebook_dir, td)
18
23
19 def test_create_nb_dir(self):
24 def test_create_nb_dir(self):
20 with TemporaryDirectory() as td:
25 with TemporaryDirectory() as td:
21 nbdir = os.path.join(td, 'notebooks')
26 nbdir = os.path.join(td, 'notebooks')
22 km = FileNotebookManager(notebook_dir=nbdir)
27 fm = FileNotebookManager(notebook_dir=nbdir)
23 self.assertEqual(km.notebook_dir, nbdir)
28 self.assertEqual(fm.notebook_dir, nbdir)
24
29
25 def test_missing_nb_dir(self):
30 def test_missing_nb_dir(self):
26 with TemporaryDirectory() as td:
31 with TemporaryDirectory() as td:
@@ -31,4 +36,195 b' class TestNotebookManager(TestCase):'
31 with NamedTemporaryFile() as tf:
36 with NamedTemporaryFile() as tf:
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
37 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
33
38
39 def test_get_os_path(self):
40 # full filesystem path should be returned with correct operating system
41 # separators.
42 with TemporaryDirectory() as td:
43 nbdir = os.path.join(td, 'notebooks')
44 fm = FileNotebookManager(notebook_dir=nbdir)
45 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
47 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
48 self.assertEqual(path, fs_path)
49
50 fm = FileNotebookManager(notebook_dir=nbdir)
51 path = fm.get_os_path('test.ipynb')
52 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
53 self.assertEqual(path, fs_path)
54
55 fm = FileNotebookManager(notebook_dir=nbdir)
56 path = fm.get_os_path('test.ipynb', '////')
57 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
58 self.assertEqual(path, fs_path)
59
60 class TestNotebookManager(TestCase):
61
62 def make_dir(self, abs_path, rel_path):
63 """make subdirectory, rel_path is the relative path
64 to that directory from the location where the server started"""
65 os_path = os.path.join(abs_path, rel_path)
66 try:
67 os.makedirs(os_path)
68 except OSError:
69 print "Directory already exists."
70
71 def test_create_notebook_model(self):
72 with TemporaryDirectory() as td:
73 # Test in root directory
74 nm = FileNotebookManager(notebook_dir=td)
75 model = nm.create_notebook_model()
76 assert isinstance(model, dict)
77 self.assertIn('name', model)
78 self.assertIn('path', model)
79 self.assertEqual(model['name'], 'Untitled0.ipynb')
80 self.assertEqual(model['path'], '')
81
82 # Test in sub-directory
83 sub_dir = '/foo/'
84 self.make_dir(nm.notebook_dir, 'foo')
85 model = nm.create_notebook_model(None, sub_dir)
86 assert isinstance(model, dict)
87 self.assertIn('name', model)
88 self.assertIn('path', model)
89 self.assertEqual(model['name'], 'Untitled0.ipynb')
90 self.assertEqual(model['path'], sub_dir.strip('/'))
91
92 def test_get_notebook_model(self):
93 with TemporaryDirectory() as td:
94 # Test in root directory
95 # Create a notebook
96 nm = FileNotebookManager(notebook_dir=td)
97 model = nm.create_notebook_model()
98 name = model['name']
99 path = model['path']
100
101 # Check that we 'get' on the notebook we just created
102 model2 = nm.get_notebook_model(name, path)
103 assert isinstance(model2, dict)
104 self.assertIn('name', model2)
105 self.assertIn('path', model2)
106 self.assertEqual(model['name'], name)
107 self.assertEqual(model['path'], path)
34
108
109 # Test in sub-directory
110 sub_dir = '/foo/'
111 self.make_dir(nm.notebook_dir, 'foo')
112 model = nm.create_notebook_model(None, sub_dir)
113 model2 = nm.get_notebook_model(name, sub_dir)
114 assert isinstance(model2, dict)
115 self.assertIn('name', model2)
116 self.assertIn('path', model2)
117 self.assertIn('content', model2)
118 self.assertEqual(model2['name'], 'Untitled0.ipynb')
119 self.assertEqual(model2['path'], sub_dir.strip('/'))
120
121 def test_update_notebook_model(self):
122 with TemporaryDirectory() as td:
123 # Test in root directory
124 # Create a notebook
125 nm = FileNotebookManager(notebook_dir=td)
126 model = nm.create_notebook_model()
127 name = model['name']
128 path = model['path']
129
130 # Change the name in the model for rename
131 model['name'] = 'test.ipynb'
132 model = nm.update_notebook_model(model, name, path)
133 assert isinstance(model, dict)
134 self.assertIn('name', model)
135 self.assertIn('path', model)
136 self.assertEqual(model['name'], 'test.ipynb')
137
138 # Make sure the old name is gone
139 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
140
141 # Test in sub-directory
142 # Create a directory and notebook in that directory
143 sub_dir = '/foo/'
144 self.make_dir(nm.notebook_dir, 'foo')
145 model = nm.create_notebook_model(None, sub_dir)
146 name = model['name']
147 path = model['path']
148
149 # Change the name in the model for rename
150 model['name'] = 'test_in_sub.ipynb'
151 model = nm.update_notebook_model(model, name, path)
152 assert isinstance(model, dict)
153 self.assertIn('name', model)
154 self.assertIn('path', model)
155 self.assertEqual(model['name'], 'test_in_sub.ipynb')
156 self.assertEqual(model['path'], sub_dir.strip('/'))
157
158 # Make sure the old name is gone
159 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
160
161 def test_save_notebook_model(self):
162 with TemporaryDirectory() as td:
163 # Test in the root directory
164 # Create a notebook
165 nm = FileNotebookManager(notebook_dir=td)
166 model = nm.create_notebook_model()
167 name = model['name']
168 path = model['path']
169
170 # Get the model with 'content'
171 full_model = nm.get_notebook_model(name, path)
172
173 # Save the notebook
174 model = nm.save_notebook_model(full_model, name, path)
175 assert isinstance(model, dict)
176 self.assertIn('name', model)
177 self.assertIn('path', model)
178 self.assertEqual(model['name'], name)
179 self.assertEqual(model['path'], path)
180
181 # Test in sub-directory
182 # Create a directory and notebook in that directory
183 sub_dir = '/foo/'
184 self.make_dir(nm.notebook_dir, 'foo')
185 model = nm.create_notebook_model(None, sub_dir)
186 name = model['name']
187 path = model['path']
188 model = nm.get_notebook_model(name, path)
189
190 # Change the name in the model for rename
191 model = nm.save_notebook_model(model, name, path)
192 assert isinstance(model, dict)
193 self.assertIn('name', model)
194 self.assertIn('path', model)
195 self.assertEqual(model['name'], 'Untitled0.ipynb')
196 self.assertEqual(model['path'], sub_dir.strip('/'))
197
198 def test_delete_notebook_model(self):
199 with TemporaryDirectory() as td:
200 # Test in the root directory
201 # Create a notebook
202 nm = FileNotebookManager(notebook_dir=td)
203 model = nm.create_notebook_model()
204 name = model['name']
205 path = model['path']
206
207 # Delete the notebook
208 nm.delete_notebook_model(name, path)
209
210 # Check that a 'get' on the deleted notebook raises and error
211 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
212
213 def test_copy_notebook(self):
214 with TemporaryDirectory() as td:
215 # Test in the root directory
216 # Create a notebook
217 nm = FileNotebookManager(notebook_dir=td)
218 path = u'Γ₯ b'
219 name = u'nb √.ipynb'
220 os.mkdir(os.path.join(td, path))
221 orig = nm.create_notebook_model({'name' : name}, path=path)
222
223 # copy with unspecified name
224 copy = nm.copy_notebook(name, path=path)
225 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
226
227 # copy with specified name
228 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
229 self.assertEqual(copy2['name'], u'copy 2.ipynb')
230
@@ -366,6 +366,36 b' IPython.utils = (function (IPython) {'
366 return Math.floor(points*pixel_per_point);
366 return Math.floor(points*pixel_per_point);
367 };
367 };
368
368
369
370 var url_path_join = function () {
371 // join a sequence of url components with '/'
372 var url = '';
373 for (var i = 0; i < arguments.length; i++) {
374 if (arguments[i] === '') {
375 continue;
376 }
377 if (url.length > 0 && url[url.length-1] != '/') {
378 url = url + '/' + arguments[i];
379 } else {
380 url = url + arguments[i];
381 }
382 }
383 return url;
384 };
385
386
387 var splitext = function (filename) {
388 // mimic Python os.path.splitext
389 // Returns ['base', '.ext']
390 var idx = filename.lastIndexOf('.');
391 if (idx > 0) {
392 return [filename.slice(0, idx), filename.slice(idx)];
393 } else {
394 return [filename, ''];
395 }
396 }
397
398
369 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
399 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
370 var browser = (function() {
400 var browser = (function() {
371 var N= navigator.appName, ua= navigator.userAgent, tem;
401 var N= navigator.appName, ua= navigator.userAgent, tem;
@@ -384,7 +414,9 b' IPython.utils = (function (IPython) {'
384 fixCarriageReturn : fixCarriageReturn,
414 fixCarriageReturn : fixCarriageReturn,
385 autoLinkUrls : autoLinkUrls,
415 autoLinkUrls : autoLinkUrls,
386 points_to_pixels : points_to_pixels,
416 points_to_pixels : points_to_pixels,
387 browser : browser
417 url_path_join : url_path_join,
418 splitext : splitext,
419 browser : browser
388 };
420 };
389
421
390 }(IPython));
422 }(IPython));
@@ -66,7 +66,6 b' var IPython = (function (IPython) {'
66 this.input_prompt_number = null;
66 this.input_prompt_number = null;
67 this.collapsed = false;
67 this.collapsed = false;
68 this.cell_type = "code";
68 this.cell_type = "code";
69 this.last_msg_id = null;
70
69
71
70
72 var cm_overwrite_options = {
71 var cm_overwrite_options = {
@@ -244,9 +243,6 b' var IPython = (function (IPython) {'
244 this.output_area.clear_output();
243 this.output_area.clear_output();
245 this.set_input_prompt('*');
244 this.set_input_prompt('*');
246 this.element.addClass("running");
245 this.element.addClass("running");
247 if (this.last_msg_id) {
248 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
249 }
250 var callbacks = {
246 var callbacks = {
251 'execute_reply': $.proxy(this._handle_execute_reply, this),
247 'execute_reply': $.proxy(this._handle_execute_reply, this),
252 'output': $.proxy(this.output_area.handle_output, this.output_area),
248 'output': $.proxy(this.output_area.handle_output, this.output_area),
@@ -442,4 +438,4 b' var IPython = (function (IPython) {'
442 IPython.CodeCell = CodeCell;
438 IPython.CodeCell = CodeCell;
443
439
444 return IPython;
440 return IPython;
445 }(IPython));
441 }(IPython)); No newline at end of file
@@ -46,16 +46,24 b' function (marked) {'
46 $('#ipython-main-app').addClass('border-box-sizing');
46 $('#ipython-main-app').addClass('border-box-sizing');
47 $('div#notebook_panel').addClass('border-box-sizing');
47 $('div#notebook_panel').addClass('border-box-sizing');
48
48
49 var baseProjectUrl = $('body').data('baseProjectUrl')
49 var baseProjectUrl = $('body').data('baseProjectUrl');
50 var notebookPath = $('body').data('notebookPath');
51 var notebookName = $('body').data('notebookName');
52 notebookName = decodeURIComponent(notebookName);
53 notebookPath = decodeURIComponent(notebookPath);
54 console.log(notebookName);
55 if (notebookPath == 'None'){
56 notebookPath = "";
57 }
50
58
51 IPython.page = new IPython.Page();
59 IPython.page = new IPython.Page();
52 IPython.layout_manager = new IPython.LayoutManager();
60 IPython.layout_manager = new IPython.LayoutManager();
53 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
61 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
54 IPython.quick_help = new IPython.QuickHelp();
62 IPython.quick_help = new IPython.QuickHelp();
55 IPython.login_widget = new IPython.LoginWidget('span#login_widget',{baseProjectUrl:baseProjectUrl});
63 IPython.login_widget = new IPython.LoginWidget('span#login_widget',{baseProjectUrl:baseProjectUrl});
56 IPython.notebook = new IPython.Notebook('div#notebook',{baseProjectUrl:baseProjectUrl});
64 IPython.notebook = new IPython.Notebook('div#notebook',{baseProjectUrl:baseProjectUrl, notebookPath:notebookPath, notebookName:notebookName});
57 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
65 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
58 IPython.menubar = new IPython.MenuBar('#menubar',{baseProjectUrl:baseProjectUrl})
66 IPython.menubar = new IPython.MenuBar('#menubar',{baseProjectUrl:baseProjectUrl, notebookPath: notebookPath})
59 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container')
67 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container')
60 IPython.tooltip = new IPython.Tooltip()
68 IPython.tooltip = new IPython.Tooltip()
61 IPython.notification_area = new IPython.NotificationArea('#notification_area')
69 IPython.notification_area = new IPython.NotificationArea('#notification_area')
@@ -91,7 +99,7 b' function (marked) {'
91
99
92 $([IPython.events]).on('notebook_loaded.Notebook', first_load);
100 $([IPython.events]).on('notebook_loaded.Notebook', first_load);
93 $([IPython.events]).trigger('app_initialized.NotebookApp');
101 $([IPython.events]).trigger('app_initialized.NotebookApp');
94 IPython.notebook.load_notebook($('body').data('notebookId'));
102 IPython.notebook.load_notebook(notebookName, notebookPath);
95
103
96 if (marked) {
104 if (marked) {
97 marked.setOptions({
105 marked.setOptions({
@@ -112,7 +112,7 b' var IPython = (function (IPython) {'
112 label : 'Interrupt',
112 label : 'Interrupt',
113 icon : 'icon-stop',
113 icon : 'icon-stop',
114 callback : function () {
114 callback : function () {
115 IPython.notebook.kernel.interrupt();
115 IPython.notebook.session.interrupt_kernel();
116 }
116 }
117 }
117 }
118 ],'run_int');
118 ],'run_int');
@@ -18,9 +18,11 b''
18
18
19 var IPython = (function (IPython) {
19 var IPython = (function (IPython) {
20 "use strict";
20 "use strict";
21
22 var utils = IPython.utils;
21
23
22 /**
24 /**
23 * A MenuBar Class to generate the menubar of IPython noteboko
25 * A MenuBar Class to generate the menubar of IPython notebook
24 * @Class MenuBar
26 * @Class MenuBar
25 *
27 *
26 * @constructor
28 * @constructor
@@ -34,8 +36,8 b' var IPython = (function (IPython) {'
34 * does not support change for now is set through this option
36 * does not support change for now is set through this option
35 */
37 */
36 var MenuBar = function (selector, options) {
38 var MenuBar = function (selector, options) {
37 var options = options || {};
39 options = options || {};
38 if(options.baseProjectUrl!= undefined){
40 if (options.baseProjectUrl !== undefined) {
39 this._baseProjectUrl = options.baseProjectUrl;
41 this._baseProjectUrl = options.baseProjectUrl;
40 }
42 }
41 this.selector = selector;
43 this.selector = selector;
@@ -50,7 +52,12 b' var IPython = (function (IPython) {'
50 return this._baseProjectUrl || $('body').data('baseProjectUrl');
52 return this._baseProjectUrl || $('body').data('baseProjectUrl');
51 };
53 };
52
54
53
55 MenuBar.prototype.notebookPath = function() {
56 var path = $('body').data('notebookPath');
57 path = decodeURIComponent(path);
58 return path;
59 };
60
54 MenuBar.prototype.style = function () {
61 MenuBar.prototype.style = function () {
55 this.element.addClass('border-box-sizing');
62 this.element.addClass('border-box-sizing');
56 this.element.find("li").click(function (event, ui) {
63 this.element.find("li").click(function (event, ui) {
@@ -67,40 +74,64 b' var IPython = (function (IPython) {'
67 // File
74 // File
68 var that = this;
75 var that = this;
69 this.element.find('#new_notebook').click(function () {
76 this.element.find('#new_notebook').click(function () {
70 window.open(that.baseProjectUrl()+'new');
77 IPython.notebook.new_notebook();
71 });
78 });
72 this.element.find('#open_notebook').click(function () {
79 this.element.find('#open_notebook').click(function () {
73 window.open(that.baseProjectUrl());
80 window.open(utils.url_path_join(
74 });
81 that.baseProjectUrl(),
75 this.element.find('#rename_notebook').click(function () {
82 'tree',
76 IPython.save_widget.rename_notebook();
83 that.notebookPath()
84 ));
77 });
85 });
78 this.element.find('#copy_notebook').click(function () {
86 this.element.find('#copy_notebook').click(function () {
79 var notebook_id = IPython.notebook.get_notebook_id();
87 IPython.notebook.copy_notebook();
80 var url = that.baseProjectUrl() + notebook_id + '/copy';
81 window.open(url,'_blank');
82 return false;
88 return false;
83 });
89 });
84 this.element.find('#save_checkpoint').click(function () {
85 IPython.notebook.save_checkpoint();
86 });
87 this.element.find('#restore_checkpoint').click(function () {
88 });
89 this.element.find('#download_ipynb').click(function () {
90 this.element.find('#download_ipynb').click(function () {
90 var notebook_id = IPython.notebook.get_notebook_id();
91 var notebook_name = IPython.notebook.get_notebook_name();
91 var url = that.baseProjectUrl() + 'notebooks/' +
92 if (IPython.notebook.dirty) {
92 notebook_id + '?format=json';
93 IPython.notebook.save_notebook({async : false});
94 }
95
96 var url = utils.url_path_join(
97 that.baseProjectUrl(),
98 'files',
99 that.notebookPath(),
100 notebook_name + '.ipynb'
101 );
93 window.location.assign(url);
102 window.location.assign(url);
94 });
103 });
104
105 /* FIXME: download-as-py doesn't work right now
106 * We will need nbconvert hooked up to get this back
107
95 this.element.find('#download_py').click(function () {
108 this.element.find('#download_py').click(function () {
96 var notebook_id = IPython.notebook.get_notebook_id();
109 var notebook_name = IPython.notebook.get_notebook_name();
97 var url = that.baseProjectUrl() + 'notebooks/' +
110 if (IPython.notebook.dirty) {
98 notebook_id + '?format=py';
111 IPython.notebook.save_notebook({async : false});
112 }
113 var url = utils.url_path_join(
114 that.baseProjectUrl(),
115 'api/notebooks',
116 that.notebookPath(),
117 notebook_name + '.ipynb?format=py&download=True'
118 );
99 window.location.assign(url);
119 window.location.assign(url);
100 });
120 });
121
122 */
123
124 this.element.find('#rename_notebook').click(function () {
125 IPython.save_widget.rename_notebook();
126 });
127 this.element.find('#save_checkpoint').click(function () {
128 IPython.notebook.save_checkpoint();
129 });
130 this.element.find('#restore_checkpoint').click(function () {
131 });
101 this.element.find('#kill_and_exit').click(function () {
132 this.element.find('#kill_and_exit').click(function () {
102 IPython.notebook.kernel.kill();
133 IPython.notebook.session.delete();
103 setTimeout(function(){window.close();}, 200);
134 setTimeout(function(){window.close();}, 500);
104 });
135 });
105 // Edit
136 // Edit
106 this.element.find('#cut_cell').click(function () {
137 this.element.find('#cut_cell').click(function () {
@@ -216,7 +247,7 b' var IPython = (function (IPython) {'
216 });
247 });
217 // Kernel
248 // Kernel
218 this.element.find('#int_kernel').click(function () {
249 this.element.find('#int_kernel').click(function () {
219 IPython.notebook.kernel.interrupt();
250 IPython.notebook.session.interrupt_kernel();
220 });
251 });
221 this.element.find('#restart_kernel').click(function () {
252 this.element.find('#restart_kernel').click(function () {
222 IPython.notebook.restart_kernel();
253 IPython.notebook.restart_kernel();
@@ -240,7 +271,7 b' var IPython = (function (IPython) {'
240 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
271 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
241 var ul = this.element.find("#restore_checkpoint").find("ul");
272 var ul = this.element.find("#restore_checkpoint").find("ul");
242 ul.empty();
273 ul.empty();
243 if (! checkpoints || checkpoints.length == 0) {
274 if (!checkpoints || checkpoints.length === 0) {
244 ul.append(
275 ul.append(
245 $("<li/>")
276 $("<li/>")
246 .addClass("disabled")
277 .addClass("disabled")
@@ -250,7 +281,7 b' var IPython = (function (IPython) {'
250 )
281 )
251 );
282 );
252 return;
283 return;
253 };
284 }
254
285
255 checkpoints.map(function (checkpoint) {
286 checkpoints.map(function (checkpoint) {
256 var d = new Date(checkpoint.last_modified);
287 var d = new Date(checkpoint.last_modified);
@@ -1,5 +1,5 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
@@ -26,11 +26,13 b' var IPython = (function (IPython) {'
26 var Notebook = function (selector, options) {
26 var Notebook = function (selector, options) {
27 var options = options || {};
27 var options = options || {};
28 this._baseProjectUrl = options.baseProjectUrl;
28 this._baseProjectUrl = options.baseProjectUrl;
29
29 this.notebook_path = options.notebookPath;
30 this.notebook_name = options.notebookName;
30 this.element = $(selector);
31 this.element = $(selector);
31 this.element.scroll();
32 this.element.scroll();
32 this.element.data("notebook", this);
33 this.element.data("notebook", this);
33 this.next_prompt_number = 1;
34 this.next_prompt_number = 1;
35 this.session = null;
34 this.kernel = null;
36 this.kernel = null;
35 this.clipboard = null;
37 this.clipboard = null;
36 this.undelete_backup = null;
38 this.undelete_backup = null;
@@ -49,8 +51,6 b' var IPython = (function (IPython) {'
49 // single worksheet for now
51 // single worksheet for now
50 this.worksheet_metadata = {};
52 this.worksheet_metadata = {};
51 this.control_key_active = false;
53 this.control_key_active = false;
52 this.notebook_id = null;
53 this.notebook_name = null;
54 this.notebook_name_blacklist_re = /[\/\\:]/;
54 this.notebook_name_blacklist_re = /[\/\\:]/;
55 this.nbformat = 3 // Increment this when changing the nbformat
55 this.nbformat = 3 // Increment this when changing the nbformat
56 this.nbformat_minor = 0 // Increment this when changing the nbformat
56 this.nbformat_minor = 0 // Increment this when changing the nbformat
@@ -78,6 +78,18 b' var IPython = (function (IPython) {'
78 return this._baseProjectUrl || $('body').data('baseProjectUrl');
78 return this._baseProjectUrl || $('body').data('baseProjectUrl');
79 };
79 };
80
80
81 Notebook.prototype.notebookName = function() {
82 var name = $('body').data('notebookName');
83 name = decodeURIComponent(name);
84 return name;
85 };
86
87 Notebook.prototype.notebookPath = function() {
88 var path = $('body').data('notebookPath');
89 path = decodeURIComponent(path);
90 return path
91 };
92
81 /**
93 /**
82 * Create an HTML and CSS representation of the notebook.
94 * Create an HTML and CSS representation of the notebook.
83 *
95 *
@@ -299,7 +311,7 b' var IPython = (function (IPython) {'
299 return false;
311 return false;
300 } else if (event.which === 73 && that.control_key_active) {
312 } else if (event.which === 73 && that.control_key_active) {
301 // Interrupt kernel = i
313 // Interrupt kernel = i
302 that.kernel.interrupt();
314 that.session.interrupt_kernel();
303 that.control_key_active = false;
315 that.control_key_active = false;
304 return false;
316 return false;
305 } else if (event.which === 190 && that.control_key_active) {
317 } else if (event.which === 190 && that.control_key_active) {
@@ -362,7 +374,7 b' var IPython = (function (IPython) {'
362 // TODO: Make killing the kernel configurable.
374 // TODO: Make killing the kernel configurable.
363 var kill_kernel = false;
375 var kill_kernel = false;
364 if (kill_kernel) {
376 if (kill_kernel) {
365 that.kernel.kill();
377 that.session.kill_kernel();
366 }
378 }
367 // if we are autosaving, trigger an autosave on nav-away.
379 // if we are autosaving, trigger an autosave on nav-away.
368 // still warn, because if we don't the autosave may fail.
380 // still warn, because if we don't the autosave may fail.
@@ -1372,27 +1384,34 b' var IPython = (function (IPython) {'
1372 this.get_selected_cell().toggle_line_numbers();
1384 this.get_selected_cell().toggle_line_numbers();
1373 };
1385 };
1374
1386
1375 // Kernel related things
1387 // Session related things
1376
1388
1377 /**
1389 /**
1378 * Start a new kernel and set it on each code cell.
1390 * Start a new session and set it on each code cell.
1379 *
1391 *
1380 * @method start_kernel
1392 * @method start_session
1393 */
1394 Notebook.prototype.start_session = function () {
1395 this.session = new IPython.Session(this.notebook_name, this.notebook_path, this);
1396 this.session.start($.proxy(this._session_started, this));
1397 };
1398
1399
1400 /**
1401 * Once a session is started, link the code cells to the kernel
1402 *
1381 */
1403 */
1382 Notebook.prototype.start_kernel = function () {
1404 Notebook.prototype._session_started = function(){
1383 var base_url = $('body').data('baseKernelUrl') + "kernels";
1405 this.kernel = this.session.kernel;
1384 this.kernel = new IPython.Kernel(base_url);
1385 this.kernel.start({notebook: this.notebook_id});
1386 // Now that the kernel has been created, tell the CodeCells about it.
1387 var ncells = this.ncells();
1406 var ncells = this.ncells();
1388 for (var i=0; i<ncells; i++) {
1407 for (var i=0; i<ncells; i++) {
1389 var cell = this.get_cell(i);
1408 var cell = this.get_cell(i);
1390 if (cell instanceof IPython.CodeCell) {
1409 if (cell instanceof IPython.CodeCell) {
1391 cell.set_kernel(this.kernel)
1410 cell.set_kernel(this.session.kernel);
1392 };
1411 };
1393 };
1412 };
1394 };
1413 };
1395
1414
1396 /**
1415 /**
1397 * Prompt the user to restart the IPython kernel.
1416 * Prompt the user to restart the IPython kernel.
1398 *
1417 *
@@ -1410,13 +1429,13 b' var IPython = (function (IPython) {'
1410 "Restart" : {
1429 "Restart" : {
1411 "class" : "btn-danger",
1430 "class" : "btn-danger",
1412 "click" : function() {
1431 "click" : function() {
1413 that.kernel.restart();
1432 that.session.restart_kernel();
1414 }
1433 }
1415 }
1434 }
1416 }
1435 }
1417 });
1436 });
1418 };
1437 };
1419
1438
1420 /**
1439 /**
1421 * Run the selected cell.
1440 * Run the selected cell.
1422 *
1441 *
@@ -1496,23 +1515,14 b' var IPython = (function (IPython) {'
1496 // Persistance and loading
1515 // Persistance and loading
1497
1516
1498 /**
1517 /**
1499 * Getter method for this notebook's ID.
1500 *
1501 * @method get_notebook_id
1502 * @return {String} This notebook's ID
1503 */
1504 Notebook.prototype.get_notebook_id = function () {
1505 return this.notebook_id;
1506 };
1507
1508 /**
1509 * Getter method for this notebook's name.
1518 * Getter method for this notebook's name.
1510 *
1519 *
1511 * @method get_notebook_name
1520 * @method get_notebook_name
1512 * @return {String} This notebook's name
1521 * @return {String} This notebook's name
1513 */
1522 */
1514 Notebook.prototype.get_notebook_name = function () {
1523 Notebook.prototype.get_notebook_name = function () {
1515 return this.notebook_name;
1524 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1525 return nbname;
1516 };
1526 };
1517
1527
1518 /**
1528 /**
@@ -1550,6 +1560,7 b' var IPython = (function (IPython) {'
1550 * @param {Object} data JSON representation of a notebook
1560 * @param {Object} data JSON representation of a notebook
1551 */
1561 */
1552 Notebook.prototype.fromJSON = function (data) {
1562 Notebook.prototype.fromJSON = function (data) {
1563 var content = data.content;
1553 var ncells = this.ncells();
1564 var ncells = this.ncells();
1554 var i;
1565 var i;
1555 for (i=0; i<ncells; i++) {
1566 for (i=0; i<ncells; i++) {
@@ -1557,10 +1568,10 b' var IPython = (function (IPython) {'
1557 this.delete_cell(0);
1568 this.delete_cell(0);
1558 };
1569 };
1559 // Save the metadata and name.
1570 // Save the metadata and name.
1560 this.metadata = data.metadata;
1571 this.metadata = content.metadata;
1561 this.notebook_name = data.metadata.name;
1572 this.notebook_name = data.name;
1562 // Only handle 1 worksheet for now.
1573 // Only handle 1 worksheet for now.
1563 var worksheet = data.worksheets[0];
1574 var worksheet = content.worksheets[0];
1564 if (worksheet !== undefined) {
1575 if (worksheet !== undefined) {
1565 if (worksheet.metadata) {
1576 if (worksheet.metadata) {
1566 this.worksheet_metadata = worksheet.metadata;
1577 this.worksheet_metadata = worksheet.metadata;
@@ -1581,7 +1592,7 b' var IPython = (function (IPython) {'
1581 new_cell.fromJSON(cell_data);
1592 new_cell.fromJSON(cell_data);
1582 };
1593 };
1583 };
1594 };
1584 if (data.worksheets.length > 1) {
1595 if (content.worksheets.length > 1) {
1585 IPython.dialog.modal({
1596 IPython.dialog.modal({
1586 title : "Multiple worksheets",
1597 title : "Multiple worksheets",
1587 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1598 body : "This notebook has " + data.worksheets.length + " worksheets, " +
@@ -1652,28 +1663,38 b' var IPython = (function (IPython) {'
1652 *
1663 *
1653 * @method save_notebook
1664 * @method save_notebook
1654 */
1665 */
1655 Notebook.prototype.save_notebook = function () {
1666 Notebook.prototype.save_notebook = function (extra_settings) {
1656 // We may want to move the name/id/nbformat logic inside toJSON?
1667 // Create a JSON model to be sent to the server.
1657 var data = this.toJSON();
1668 var model = {};
1658 data.metadata.name = this.notebook_name;
1669 model.name = this.notebook_name;
1659 data.nbformat = this.nbformat;
1670 model.path = this.notebook_path;
1660 data.nbformat_minor = this.nbformat_minor;
1671 model.content = this.toJSON();
1661
1672 model.content.nbformat = this.nbformat;
1673 model.content.nbformat_minor = this.nbformat_minor;
1662 // time the ajax call for autosave tuning purposes.
1674 // time the ajax call for autosave tuning purposes.
1663 var start = new Date().getTime();
1675 var start = new Date().getTime();
1664
1665 // We do the call with settings so we can set cache to false.
1676 // We do the call with settings so we can set cache to false.
1666 var settings = {
1677 var settings = {
1667 processData : false,
1678 processData : false,
1668 cache : false,
1679 cache : false,
1669 type : "PUT",
1680 type : "PUT",
1670 data : JSON.stringify(data),
1681 data : JSON.stringify(model),
1671 headers : {'Content-Type': 'application/json'},
1682 headers : {'Content-Type': 'application/json'},
1672 success : $.proxy(this.save_notebook_success, this, start),
1683 success : $.proxy(this.save_notebook_success, this, start),
1673 error : $.proxy(this.save_notebook_error, this)
1684 error : $.proxy(this.save_notebook_error, this)
1674 };
1685 };
1686 if (extra_settings) {
1687 for (var key in extra_settings) {
1688 settings[key] = extra_settings[key];
1689 }
1690 }
1675 $([IPython.events]).trigger('notebook_saving.Notebook');
1691 $([IPython.events]).trigger('notebook_saving.Notebook');
1676 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1692 var url = utils.url_path_join(
1693 this.baseProjectUrl(),
1694 'api/notebooks',
1695 this.notebookPath(),
1696 this.notebook_name
1697 );
1677 $.ajax(url, settings);
1698 $.ajax(url, settings);
1678 };
1699 };
1679
1700
@@ -1727,16 +1748,137 b' var IPython = (function (IPython) {'
1727 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1748 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1728 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1749 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1729 };
1750 };
1751
1752 Notebook.prototype.new_notebook = function(){
1753 var path = this.notebookPath();
1754 var base_project_url = this.baseProjectUrl();
1755 var settings = {
1756 processData : false,
1757 cache : false,
1758 type : "POST",
1759 dataType : "json",
1760 async : false,
1761 success : function (data, status, xhr){
1762 var notebook_name = data.name;
1763 window.open(
1764 utils.url_path_join(
1765 base_project_url,
1766 'notebooks',
1767 path,
1768 notebook_name
1769 ),
1770 '_blank'
1771 );
1772 }
1773 };
1774 var url = utils.url_path_join(
1775 base_project_url,
1776 'api/notebooks',
1777 path
1778 );
1779 $.ajax(url,settings);
1780 };
1781
1782
1783 Notebook.prototype.copy_notebook = function(){
1784 var path = this.notebookPath();
1785 var base_project_url = this.baseProjectUrl();
1786 var settings = {
1787 processData : false,
1788 cache : false,
1789 type : "POST",
1790 dataType : "json",
1791 data : JSON.stringify({copy_from : this.notebook_name}),
1792 async : false,
1793 success : function (data, status, xhr) {
1794 window.open(utils.url_path_join(
1795 base_project_url,
1796 'notebooks',
1797 data.path,
1798 data.name
1799 ), '_blank');
1800 }
1801 };
1802 var url = utils.url_path_join(
1803 base_project_url,
1804 'api/notebooks',
1805 path
1806 );
1807 $.ajax(url,settings);
1808 };
1809
1810 Notebook.prototype.rename = function (nbname) {
1811 var that = this;
1812 var data = {name: nbname + '.ipynb'};
1813 var settings = {
1814 processData : false,
1815 cache : false,
1816 type : "PATCH",
1817 data : JSON.stringify(data),
1818 dataType: "json",
1819 headers : {'Content-Type': 'application/json'},
1820 success : $.proxy(that.rename_success, this),
1821 error : $.proxy(that.rename_error, this)
1822 };
1823 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1824 var url = utils.url_path_join(
1825 this.baseProjectUrl(),
1826 'api/notebooks',
1827 this.notebookPath(),
1828 this.notebook_name
1829 );
1830 $.ajax(url, settings);
1831 };
1832
1730
1833
1834 Notebook.prototype.rename_success = function (json, status, xhr) {
1835 this.notebook_name = json.name
1836 var name = this.notebook_name
1837 var path = json.path
1838 this.session.rename_notebook(name, path);
1839 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1840 }
1841
1842 Notebook.prototype.rename_error = function (json, status, xhr) {
1843 var that = this;
1844 var dialog = $('<div/>').append(
1845 $("<p/>").addClass("rename-message")
1846 .html('This notebook name already exists.')
1847 )
1848 IPython.dialog.modal({
1849 title: "Notebook Rename Error!",
1850 body: dialog,
1851 buttons : {
1852 "Cancel": {},
1853 "OK": {
1854 class: "btn-primary",
1855 click: function () {
1856 IPython.save_widget.rename_notebook();
1857 }}
1858 },
1859 open : function (event, ui) {
1860 var that = $(this);
1861 // Upon ENTER, click the OK button.
1862 that.find('input[type="text"]').keydown(function (event, ui) {
1863 if (event.which === utils.keycodes.ENTER) {
1864 that.find('.btn-primary').first().click();
1865 }
1866 });
1867 that.find('input[type="text"]').focus();
1868 }
1869 });
1870 }
1871
1731 /**
1872 /**
1732 * Request a notebook's data from the server.
1873 * Request a notebook's data from the server.
1733 *
1874 *
1734 * @method load_notebook
1875 * @method load_notebook
1735 * @param {String} notebook_id A notebook to load
1876 * @param {String} notebook_name and path A notebook to load
1736 */
1877 */
1737 Notebook.prototype.load_notebook = function (notebook_id) {
1878 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1738 var that = this;
1879 var that = this;
1739 this.notebook_id = notebook_id;
1880 this.notebook_name = notebook_name;
1881 this.notebook_path = notebook_path;
1740 // We do the call with settings so we can set cache to false.
1882 // We do the call with settings so we can set cache to false.
1741 var settings = {
1883 var settings = {
1742 processData : false,
1884 processData : false,
@@ -1747,7 +1889,12 b' var IPython = (function (IPython) {'
1747 error : $.proxy(this.load_notebook_error,this),
1889 error : $.proxy(this.load_notebook_error,this),
1748 };
1890 };
1749 $([IPython.events]).trigger('notebook_loading.Notebook');
1891 $([IPython.events]).trigger('notebook_loading.Notebook');
1750 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1892 var url = utils.url_path_join(
1893 this._baseProjectUrl,
1894 'api/notebooks',
1895 this.notebookPath(),
1896 this.notebook_name
1897 );
1751 $.ajax(url, settings);
1898 $.ajax(url, settings);
1752 };
1899 };
1753
1900
@@ -1805,12 +1952,13 b' var IPython = (function (IPython) {'
1805
1952
1806 }
1953 }
1807
1954
1808 // Create the kernel after the notebook is completely loaded to prevent
1955 // Create the session after the notebook is completely loaded to prevent
1809 // code execution upon loading, which is a security risk.
1956 // code execution upon loading, which is a security risk.
1810 this.start_kernel();
1957 if (this.session == null) {
1958 this.start_session();
1959 }
1811 // load our checkpoint list
1960 // load our checkpoint list
1812 IPython.notebook.list_checkpoints();
1961 IPython.notebook.list_checkpoints();
1813
1814 $([IPython.events]).trigger('notebook_loaded.Notebook');
1962 $([IPython.events]).trigger('notebook_loaded.Notebook');
1815 };
1963 };
1816
1964
@@ -1861,7 +2009,7 b' var IPython = (function (IPython) {'
1861 var found = false;
2009 var found = false;
1862 for (var i = 0; i < this.checkpoints.length; i++) {
2010 for (var i = 0; i < this.checkpoints.length; i++) {
1863 var existing = this.checkpoints[i];
2011 var existing = this.checkpoints[i];
1864 if (existing.checkpoint_id == checkpoint.checkpoint_id) {
2012 if (existing.id == checkpoint.id) {
1865 found = true;
2013 found = true;
1866 this.checkpoints[i] = checkpoint;
2014 this.checkpoints[i] = checkpoint;
1867 break;
2015 break;
@@ -1879,7 +2027,13 b' var IPython = (function (IPython) {'
1879 * @method list_checkpoints
2027 * @method list_checkpoints
1880 */
2028 */
1881 Notebook.prototype.list_checkpoints = function () {
2029 Notebook.prototype.list_checkpoints = function () {
1882 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
2030 var url = utils.url_path_join(
2031 this.baseProjectUrl(),
2032 'api/notebooks',
2033 this.notebookPath(),
2034 this.notebook_name,
2035 'checkpoints'
2036 );
1883 $.get(url).done(
2037 $.get(url).done(
1884 $.proxy(this.list_checkpoints_success, this)
2038 $.proxy(this.list_checkpoints_success, this)
1885 ).fail(
2039 ).fail(
@@ -1924,7 +2078,13 b' var IPython = (function (IPython) {'
1924 * @method create_checkpoint
2078 * @method create_checkpoint
1925 */
2079 */
1926 Notebook.prototype.create_checkpoint = function () {
2080 Notebook.prototype.create_checkpoint = function () {
1927 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
2081 var url = utils.url_path_join(
2082 this.baseProjectUrl(),
2083 'api/notebooks',
2084 this.notebookPath(),
2085 this.notebook_name,
2086 'checkpoints'
2087 );
1928 $.post(url).done(
2088 $.post(url).done(
1929 $.proxy(this.create_checkpoint_success, this)
2089 $.proxy(this.create_checkpoint_success, this)
1930 ).fail(
2090 ).fail(
@@ -1989,7 +2149,7 b' var IPython = (function (IPython) {'
1989 Revert : {
2149 Revert : {
1990 class : "btn-danger",
2150 class : "btn-danger",
1991 click : function () {
2151 click : function () {
1992 that.restore_checkpoint(checkpoint.checkpoint_id);
2152 that.restore_checkpoint(checkpoint.id);
1993 }
2153 }
1994 },
2154 },
1995 Cancel : {}
2155 Cancel : {}
@@ -2004,8 +2164,15 b' var IPython = (function (IPython) {'
2004 * @param {String} checkpoint ID
2164 * @param {String} checkpoint ID
2005 */
2165 */
2006 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2166 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2007 $([IPython.events]).trigger('checkpoint_restoring.Notebook', checkpoint);
2167 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2008 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
2168 var url = utils.url_path_join(
2169 this.baseProjectUrl(),
2170 'api/notebooks',
2171 this.notebookPath(),
2172 this.notebook_name,
2173 'checkpoints',
2174 checkpoint
2175 );
2009 $.post(url).done(
2176 $.post(url).done(
2010 $.proxy(this.restore_checkpoint_success, this)
2177 $.proxy(this.restore_checkpoint_success, this)
2011 ).fail(
2178 ).fail(
@@ -2023,7 +2190,7 b' var IPython = (function (IPython) {'
2023 */
2190 */
2024 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2191 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2025 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2192 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2026 this.load_notebook(this.notebook_id);
2193 this.load_notebook(this.notebook_name, this.notebook_path);
2027 };
2194 };
2028
2195
2029 /**
2196 /**
@@ -2045,8 +2212,15 b' var IPython = (function (IPython) {'
2045 * @param {String} checkpoint ID
2212 * @param {String} checkpoint ID
2046 */
2213 */
2047 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2214 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2048 $([IPython.events]).trigger('checkpoint_deleting.Notebook', checkpoint);
2215 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2049 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
2216 var url = utils.url_path_join(
2217 this.baseProjectUrl(),
2218 'api/notebooks',
2219 this.notebookPath(),
2220 this.notebook_name,
2221 'checkpoints',
2222 checkpoint
2223 );
2050 $.ajax(url, {
2224 $.ajax(url, {
2051 type: 'DELETE',
2225 type: 'DELETE',
2052 success: $.proxy(this.delete_checkpoint_success, this),
2226 success: $.proxy(this.delete_checkpoint_success, this),
@@ -2064,7 +2238,7 b' var IPython = (function (IPython) {'
2064 */
2238 */
2065 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2239 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2066 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2240 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2067 this.load_notebook(this.notebook_id);
2241 this.load_notebook(this.notebook_name, this.notebook_path);
2068 };
2242 };
2069
2243
2070 /**
2244 /**
@@ -2086,4 +2260,3 b' var IPython = (function (IPython) {'
2086 return IPython;
2260 return IPython;
2087
2261
2088 }(IPython));
2262 }(IPython));
2089
@@ -46,6 +46,11 b' var IPython = (function (IPython) {'
46 that.update_notebook_name();
46 that.update_notebook_name();
47 that.update_document_title();
47 that.update_document_title();
48 });
48 });
49 $([IPython.events]).on('notebook_renamed.Notebook', function () {
50 that.update_notebook_name();
51 that.update_document_title();
52 that.update_address_bar();
53 });
49 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
50 that.set_save_status('Autosave Failed!');
55 that.set_save_status('Autosave Failed!');
51 });
56 });
@@ -90,8 +95,7 b' var IPython = (function (IPython) {'
90 );
95 );
91 return false;
96 return false;
92 } else {
97 } else {
93 IPython.notebook.set_notebook_name(new_name);
98 IPython.notebook.rename(new_name);
94 IPython.notebook.save_notebook();
95 }
99 }
96 }}
100 }}
97 },
101 },
@@ -120,6 +124,17 b' var IPython = (function (IPython) {'
120 var nbname = IPython.notebook.get_notebook_name();
124 var nbname = IPython.notebook.get_notebook_name();
121 document.title = nbname;
125 document.title = nbname;
122 };
126 };
127
128 SaveWidget.prototype.update_address_bar = function(){
129 var nbname = IPython.notebook.notebook_name;
130 var path = IPython.notebook.notebookPath();
131 var state = {path : utils.url_path_join(path,nbname)};
132 window.history.replaceState(state, "", utils.url_path_join(
133 "/notebooks",
134 path,
135 nbname)
136 );
137 }
123
138
124
139
125 SaveWidget.prototype.set_save_status = function (msg) {
140 SaveWidget.prototype.set_save_status = function (msg) {
@@ -225,8 +225,8 b' var IPython = (function (IPython) {'
225 var callbacks = {
225 var callbacks = {
226 'object_info_reply': $.proxy(this._show, this)
226 'object_info_reply': $.proxy(this._show, this)
227 }
227 }
228 var oir_token = this.extract_oir_token(line)
228 var oir_token = this.extract_oir_token(line);
229 cell.kernel.object_info_request(oir_token, callbacks);
229 var msg_id = cell.kernel.object_info_request(oir_token, callbacks);
230 }
230 }
231
231
232 // make an imediate completion request
232 // make an imediate completion request
@@ -73,12 +73,12 b' var IPython = (function (IPython) {'
73 * @method start
73 * @method start
74 */
74 */
75 Kernel.prototype.start = function (params) {
75 Kernel.prototype.start = function (params) {
76 var that = this;
76 params = params || {};
77 if (!this.running) {
77 if (!this.running) {
78 var qs = $.param(params);
78 var qs = $.param(params);
79 var url = this.base_url + '?' + qs;
79 var url = this.base_url + '?' + qs;
80 $.post(url,
80 $.post(url,
81 $.proxy(that._kernel_started,that),
81 $.proxy(this._kernel_started, this),
82 'json'
82 'json'
83 );
83 );
84 };
84 };
@@ -94,12 +94,11 b' var IPython = (function (IPython) {'
94 */
94 */
95 Kernel.prototype.restart = function () {
95 Kernel.prototype.restart = function () {
96 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
96 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
97 var that = this;
98 if (this.running) {
97 if (this.running) {
99 this.stop_channels();
98 this.stop_channels();
100 var url = this.kernel_url + "/restart";
99 var url = utils.url_path_join(this.kernel_url, "restart");
101 $.post(url,
100 $.post(url,
102 $.proxy(that._kernel_started, that),
101 $.proxy(this._kernel_started, this),
103 'json'
102 'json'
104 );
103 );
105 };
104 };
@@ -107,9 +106,9 b' var IPython = (function (IPython) {'
107
106
108
107
109 Kernel.prototype._kernel_started = function (json) {
108 Kernel.prototype._kernel_started = function (json) {
110 console.log("Kernel started: ", json.kernel_id);
109 console.log("Kernel started: ", json.id);
111 this.running = true;
110 this.running = true;
112 this.kernel_id = json.kernel_id;
111 this.kernel_id = json.id;
113 var ws_url = json.ws_url;
112 var ws_url = json.ws_url;
114 if (ws_url.match(/wss?:\/\//) == null) {
113 if (ws_url.match(/wss?:\/\//) == null) {
115 // trailing 's' in https will become wss for secure web sockets
114 // trailing 's' in https will become wss for secure web sockets
@@ -117,14 +116,14 b' var IPython = (function (IPython) {'
117 ws_url = prot + location.host + ws_url;
116 ws_url = prot + location.host + ws_url;
118 };
117 };
119 this.ws_url = ws_url;
118 this.ws_url = ws_url;
120 this.kernel_url = this.base_url + "/" + this.kernel_id;
119 this.kernel_url = utils.url_path_join(this.base_url, this.kernel_id);
121 this.start_channels();
120 this.start_channels();
122 };
121 };
123
122
124
123
125 Kernel.prototype._websocket_closed = function(ws_url, early) {
124 Kernel.prototype._websocket_closed = function(ws_url, early) {
126 this.stop_channels();
125 this.stop_channels();
127 $([IPython.events]).trigger('websocket_closed.Kernel',
126 $([IPython.events]).trigger('websocket_closed.Kernel',
128 {ws_url: ws_url, kernel: this, early: early}
127 {ws_url: ws_url, kernel: this, early: early}
129 );
128 );
130 };
129 };
@@ -1,5 +1,5 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
@@ -10,6 +10,9 b''
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
14
15 var utils = IPython.utils;
13
16
14 var ClusterList = function (selector) {
17 var ClusterList = function (selector) {
15 this.selector = selector;
18 this.selector = selector;
@@ -48,14 +51,14 b' var IPython = (function (IPython) {'
48 dataType : "json",
51 dataType : "json",
49 success : $.proxy(this.load_list_success, this)
52 success : $.proxy(this.load_list_success, this)
50 };
53 };
51 var url = this.baseProjectUrl() + 'clusters';
54 var url = utils.url_path_join(this.baseProjectUrl(), 'clusters');
52 $.ajax(url, settings);
55 $.ajax(url, settings);
53 };
56 };
54
57
55
58
56 ClusterList.prototype.clear_list = function () {
59 ClusterList.prototype.clear_list = function () {
57 this.element.children('.list_item').remove();
60 this.element.children('.list_item').remove();
58 }
61 };
59
62
60 ClusterList.prototype.load_list_success = function (data, status, xhr) {
63 ClusterList.prototype.load_list_success = function (data, status, xhr) {
61 this.clear_list();
64 this.clear_list();
@@ -66,7 +69,7 b' var IPython = (function (IPython) {'
66 item.update_state(data[i]);
69 item.update_state(data[i]);
67 element.data('item', item);
70 element.data('item', item);
68 this.element.append(element);
71 this.element.append(element);
69 };
72 }
70 };
73 };
71
74
72
75
@@ -81,10 +84,9 b' var IPython = (function (IPython) {'
81 };
84 };
82
85
83
86
84
85 ClusterItem.prototype.style = function () {
87 ClusterItem.prototype.style = function () {
86 this.element.addClass('list_item').addClass("row-fluid");
88 this.element.addClass('list_item').addClass("row-fluid");
87 }
89 };
88
90
89 ClusterItem.prototype.update_state = function (data) {
91 ClusterItem.prototype.update_state = function (data) {
90 this.data = data;
92 this.data = data;
@@ -92,9 +94,8 b' var IPython = (function (IPython) {'
92 this.state_running();
94 this.state_running();
93 } else if (data.status === 'stopped') {
95 } else if (data.status === 'stopped') {
94 this.state_stopped();
96 this.state_stopped();
95 };
97 }
96
98 };
97 }
98
99
99
100
100 ClusterItem.prototype.state_stopped = function () {
101 ClusterItem.prototype.state_stopped = function () {
@@ -132,13 +133,18 b' var IPython = (function (IPython) {'
132 that.update_state(data);
133 that.update_state(data);
133 },
134 },
134 error : function (data, status, xhr) {
135 error : function (data, status, xhr) {
135 status_col.html("error starting cluster")
136 status_col.html("error starting cluster");
136 }
137 }
137 };
138 };
138 status_col.html('starting');
139 status_col.html('starting');
139 var url = that.baseProjectUrl() + 'clusters/' + that.data.profile + '/start';
140 var url = utils.url_path_join(
141 that.baseProjectUrl(),
142 'clusters',
143 that.data.profile,
144 'start'
145 );
140 $.ajax(url, settings);
146 $.ajax(url, settings);
141 };
147 }
142 });
148 });
143 };
149 };
144
150
@@ -169,11 +175,16 b' var IPython = (function (IPython) {'
169 },
175 },
170 error : function (data, status, xhr) {
176 error : function (data, status, xhr) {
171 console.log('error',data);
177 console.log('error',data);
172 status_col.html("error stopping cluster")
178 status_col.html("error stopping cluster");
173 }
179 }
174 };
180 };
175 status_col.html('stopping')
181 status_col.html('stopping');
176 var url = that.baseProjectUrl() + 'clusters/' + that.data.profile + '/stop';
182 var url = utils.url_path_join(
183 that.baseProjectUrl(),
184 'clusters',
185 that.data.profile,
186 'stop'
187 );
177 $.ajax(url, settings);
188 $.ajax(url, settings);
178 });
189 });
179 };
190 };
@@ -13,10 +13,11 b''
13 $(document).ready(function () {
13 $(document).ready(function () {
14
14
15 IPython.page = new IPython.Page();
15 IPython.page = new IPython.Page();
16 $('#new_notebook').click(function (e) {
16
17 window.open($('body').data('baseProjectUrl')+'new');
17 $('#new_notebook').button().click(function (e) {
18 IPython.notebook_list.new_notebook($('body').data('baseProjectUrl'))
18 });
19 });
19
20
20 IPython.notebook_list = new IPython.NotebookList('#notebook_list');
21 IPython.notebook_list = new IPython.NotebookList('#notebook_list');
21 IPython.cluster_list = new IPython.ClusterList('#cluster_list');
22 IPython.cluster_list = new IPython.ClusterList('#cluster_list');
22 IPython.login_widget = new IPython.LoginWidget('#login_widget');
23 IPython.login_widget = new IPython.LoginWidget('#login_widget');
@@ -30,14 +31,14 b' $(document).ready(function () {'
30 //refresh immediately , then start interval
31 //refresh immediately , then start interval
31 if($('.upload_button').length == 0)
32 if($('.upload_button').length == 0)
32 {
33 {
33 IPython.notebook_list.load_list();
34 IPython.notebook_list.load_sessions();
34 IPython.cluster_list.load_list();
35 IPython.cluster_list.load_list();
35 }
36 }
36 if (!interval_id){
37 if (!interval_id){
37 interval_id = setInterval(function(){
38 interval_id = setInterval(function(){
38 if($('.upload_button').length == 0)
39 if($('.upload_button').length == 0)
39 {
40 {
40 IPython.notebook_list.load_list();
41 IPython.notebook_list.load_sessions();
41 IPython.cluster_list.load_list();
42 IPython.cluster_list.load_list();
42 }
43 }
43 }, time_refresh*1000);
44 }, time_refresh*1000);
@@ -1,5 +1,5 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
@@ -10,6 +10,9 b''
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
14
15 var utils = IPython.utils;
13
16
14 var NotebookList = function (selector) {
17 var NotebookList = function (selector) {
15 this.selector = selector;
18 this.selector = selector;
@@ -18,12 +21,18 b' var IPython = (function (IPython) {'
18 this.style();
21 this.style();
19 this.bind_events();
22 this.bind_events();
20 }
23 }
24 this.notebooks_list = [];
25 this.sessions = {};
21 };
26 };
22
27
23 NotebookList.prototype.baseProjectUrl = function () {
28 NotebookList.prototype.baseProjectUrl = function () {
24 return $('body').data('baseProjectUrl')
29 return $('body').data('baseProjectUrl');
25 };
30 };
26
31
32 NotebookList.prototype.notebookPath = function() {
33 return $('body').data('notebookPath');
34 };
35
27 NotebookList.prototype.style = function () {
36 NotebookList.prototype.style = function () {
28 $('#notebook_toolbar').addClass('list_toolbar');
37 $('#notebook_toolbar').addClass('list_toolbar');
29 $('#drag_info').addClass('toolbar_info');
38 $('#drag_info').addClass('toolbar_info');
@@ -54,19 +63,18 b' var IPython = (function (IPython) {'
54 files = event.originalEvent.dataTransfer.files;
63 files = event.originalEvent.dataTransfer.files;
55 } else
64 } else
56 {
65 {
57 files = event.originalEvent.target.files
66 files = event.originalEvent.target.files;
58 }
67 }
59 for (var i = 0, f; f = files[i]; i++) {
68 for (var i = 0; i < files.length; i++) {
69 var f = files[i];
60 var reader = new FileReader();
70 var reader = new FileReader();
61 reader.readAsText(f);
71 reader.readAsText(f);
62 var fname = f.name.split('.');
72 var name_and_ext = utils.splitext(f.name);
63 var nbname = fname.slice(0,-1).join('.');
73 var nbname = name_and_ext[0];
64 var nbformat = fname.slice(-1)[0];
74 var file_ext = name_and_ext[-1];
65 if (nbformat === 'ipynb') {nbformat = 'json';};
75 if (file_ext === '.ipynb') {
66 if (nbformat === 'py' || nbformat === 'json') {
67 var item = that.new_notebook_item(0);
76 var item = that.new_notebook_item(0);
68 that.add_name_input(nbname, item);
77 that.add_name_input(nbname, item);
69 item.data('nbformat', nbformat);
70 // Store the notebook item in the reader so we can use it later
78 // Store the notebook item in the reader so we can use it later
71 // to know which item it belongs to.
79 // to know which item it belongs to.
72 $(reader).data('item', item);
80 $(reader).data('item', item);
@@ -75,15 +83,56 b' var IPython = (function (IPython) {'
75 that.add_notebook_data(event.target.result, nbitem);
83 that.add_notebook_data(event.target.result, nbitem);
76 that.add_upload_button(nbitem);
84 that.add_upload_button(nbitem);
77 };
85 };
78 };
86 } else {
87 var dialog = 'Uploaded notebooks must be .ipynb files';
88 IPython.dialog.modal({
89 title : 'Invalid file type',
90 body : dialog,
91 buttons : {'OK' : {'class' : 'btn-primary'}}
92 });
93 }
79 }
94 }
80 return false;
95 return false;
81 };
96 };
82
97
83 NotebookList.prototype.clear_list = function () {
98 NotebookList.prototype.clear_list = function () {
84 this.element.children('.list_item').remove();
99 this.element.children('.list_item').remove();
85 };
100 };
86
101
102 NotebookList.prototype.load_sessions = function(){
103 var that = this;
104 var settings = {
105 processData : false,
106 cache : false,
107 type : "GET",
108 dataType : "json",
109 success : $.proxy(that.sessions_loaded, this)
110 };
111 var url = this.baseProjectUrl() + 'api/sessions';
112 $.ajax(url,settings);
113 };
114
115
116 NotebookList.prototype.sessions_loaded = function(data){
117 this.sessions = {};
118 var len = data.length;
119 if (len > 0) {
120 for (var i=0; i<len; i++) {
121 var nb_path;
122 if (!data[i].notebook.path) {
123 nb_path = data[i].notebook.name;
124 }
125 else {
126 nb_path = utils.url_path_join(
127 data[i].notebook.path,
128 data[i].notebook.name
129 );
130 }
131 this.sessions[nb_path] = data[i].id;
132 }
133 }
134 this.load_list();
135 };
87
136
88 NotebookList.prototype.load_list = function () {
137 NotebookList.prototype.load_list = function () {
89 var that = this;
138 var that = this;
@@ -98,7 +147,12 b' var IPython = (function (IPython) {'
98 },this)
147 },this)
99 };
148 };
100
149
101 var url = this.baseProjectUrl() + 'notebooks';
150 var url = utils.url_path_join(
151 this.baseProjectUrl(),
152 'api',
153 'notebooks',
154 this.notebookPath()
155 );
102 $.ajax(url, settings);
156 $.ajax(url, settings);
103 };
157 };
104
158
@@ -106,33 +160,30 b' var IPython = (function (IPython) {'
106 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
160 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
107 var message = 'Notebook list empty.';
161 var message = 'Notebook list empty.';
108 if (param !== undefined && param.msg) {
162 if (param !== undefined && param.msg) {
109 var message = param.msg;
163 message = param.msg;
110 }
164 }
111 var len = data.length;
165 var len = data.length;
112 this.clear_list();
166 this.clear_list();
113
167 if (len === 0) {
114 if(len == 0)
115 {
116 $(this.new_notebook_item(0))
168 $(this.new_notebook_item(0))
117 .append(
169 .append(
118 $('<div style="margin:auto;text-align:center;color:grey"/>')
170 $('<div style="margin:auto;text-align:center;color:grey"/>')
119 .text(message)
171 .text(message)
120 )
172 );
121 }
173 }
122
123 for (var i=0; i<len; i++) {
174 for (var i=0; i<len; i++) {
124 var notebook_id = data[i].notebook_id;
175 var name = data[i].name;
125 var nbname = data[i].name;
176 var path = this.notebookPath();
126 var kernel = data[i].kernel_id;
177 var nbname = utils.splitext(name)[0];
127 var item = this.new_notebook_item(i);
178 var item = this.new_notebook_item(i);
128 this.add_link(notebook_id, nbname, item);
179 this.add_link(path, nbname, item);
129 // hide delete buttons when readonly
180 name = utils.url_path_join(this.notebookPath(), name);
130 if(kernel == null){
181 if(this.sessions[name] === undefined){
131 this.add_delete_button(item);
182 this.add_delete_button(item);
132 } else {
183 } else {
133 this.add_shutdown_button(item,kernel);
184 this.add_shutdown_button(item,this.sessions[name]);
134 }
185 }
135 };
186 }
136 };
187 };
137
188
138
189
@@ -157,13 +208,19 b' var IPython = (function (IPython) {'
157 };
208 };
158
209
159
210
160 NotebookList.prototype.add_link = function (notebook_id, nbname, item) {
211 NotebookList.prototype.add_link = function (path, nbname, item) {
161 item.data('nbname', nbname);
212 item.data('nbname', nbname);
162 item.data('notebook_id', notebook_id);
213 item.data('path', path);
163 item.find(".item_name").text(nbname);
214 item.find(".item_name").text(nbname);
164 item.find("a.item_link")
215 item.find("a.item_link")
165 .attr('href', this.baseProjectUrl()+notebook_id)
216 .attr('href',
166 .attr('target','_blank');
217 utils.url_path_join(
218 this.baseProjectUrl(),
219 "notebooks",
220 this.notebookPath(),
221 nbname + ".ipynb"
222 )
223 ).attr('target','_blank');
167 };
224 };
168
225
169
226
@@ -180,11 +237,11 b' var IPython = (function (IPython) {'
180
237
181
238
182 NotebookList.prototype.add_notebook_data = function (data, item) {
239 NotebookList.prototype.add_notebook_data = function (data, item) {
183 item.data('nbdata',data);
240 item.data('nbdata', data);
184 };
241 };
185
242
186
243
187 NotebookList.prototype.add_shutdown_button = function (item, kernel) {
244 NotebookList.prototype.add_shutdown_button = function (item, session) {
188 var that = this;
245 var that = this;
189 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini").
246 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini").
190 click(function (e) {
247 click(function (e) {
@@ -193,11 +250,15 b' var IPython = (function (IPython) {'
193 cache : false,
250 cache : false,
194 type : "DELETE",
251 type : "DELETE",
195 dataType : "json",
252 dataType : "json",
196 success : function (data, status, xhr) {
253 success : function () {
197 that.load_list();
254 that.load_sessions();
198 }
255 }
199 };
256 };
200 var url = that.baseProjectUrl() + 'kernels/'+kernel;
257 var url = utils.url_path_join(
258 that.baseProjectUrl(),
259 'api/sessions',
260 session
261 );
201 $.ajax(url, settings);
262 $.ajax(url, settings);
202 return false;
263 return false;
203 });
264 });
@@ -216,7 +277,6 b' var IPython = (function (IPython) {'
216 // data because the outer scopes values change as we iterate through the loop.
277 // data because the outer scopes values change as we iterate through the loop.
217 var parent_item = that.parents('div.list_item');
278 var parent_item = that.parents('div.list_item');
218 var nbname = parent_item.data('nbname');
279 var nbname = parent_item.data('nbname');
219 var notebook_id = parent_item.data('notebook_id');
220 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
280 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
221 IPython.dialog.modal({
281 IPython.dialog.modal({
222 title : "Delete notebook",
282 title : "Delete notebook",
@@ -234,7 +294,12 b' var IPython = (function (IPython) {'
234 parent_item.remove();
294 parent_item.remove();
235 }
295 }
236 };
296 };
237 var url = notebooklist.baseProjectUrl() + 'notebooks/' + notebook_id;
297 var url = utils.url_path_join(
298 notebooklist.baseProjectUrl(),
299 'api/notebooks',
300 notebooklist.notebookPath(),
301 nbname + '.ipynb'
302 );
238 $.ajax(url, settings);
303 $.ajax(url, settings);
239 }
304 }
240 },
305 },
@@ -252,30 +317,34 b' var IPython = (function (IPython) {'
252 var upload_button = $('<button/>').text("Upload")
317 var upload_button = $('<button/>').text("Upload")
253 .addClass('btn btn-primary btn-mini upload_button')
318 .addClass('btn btn-primary btn-mini upload_button')
254 .click(function (e) {
319 .click(function (e) {
255 var nbname = item.find('.item_name > input').attr('value');
320 var nbname = item.find('.item_name > input').val();
256 var nbformat = item.data('nbformat');
257 var nbdata = item.data('nbdata');
321 var nbdata = item.data('nbdata');
258 var content_type = 'text/plain';
322 var content_type = 'application/json';
259 if (nbformat === 'json') {
323 var model = {
260 content_type = 'application/json';
324 content : JSON.parse(nbdata),
261 } else if (nbformat === 'py') {
262 content_type = 'application/x-python';
263 };
325 };
264 var settings = {
326 var settings = {
265 processData : false,
327 processData : false,
266 cache : false,
328 cache : false,
267 type : 'POST',
329 type : 'POST',
268 dataType : 'json',
330 dataType : 'json',
269 data : nbdata,
331 data : JSON.stringify(model),
270 headers : {'Content-Type': content_type},
332 headers : {'Content-Type': content_type},
271 success : function (data, status, xhr) {
333 success : function (data, status, xhr) {
272 that.add_link(data, nbname, item);
334 that.add_link(data, nbname, item);
273 that.add_delete_button(item);
335 that.add_delete_button(item);
336 },
337 error : function (data, status, xhr) {
338 console.log(data, status);
274 }
339 }
275 };
340 };
276
341
277 var qs = $.param({name:nbname, format:nbformat});
342 var url = utils.url_path_join(
278 var url = that.baseProjectUrl() + 'notebooks?' + qs;
343 that.baseProjectUrl(),
344 'api/notebooks',
345 that.notebookPath(),
346 nbname + '.ipynb'
347 );
279 $.ajax(url, settings);
348 $.ajax(url, settings);
280 return false;
349 return false;
281 });
350 });
@@ -292,9 +361,37 b' var IPython = (function (IPython) {'
292 };
361 };
293
362
294
363
364 NotebookList.prototype.new_notebook = function(){
365 var path = this.notebookPath();
366 var base_project_url = this.baseProjectUrl();
367 var settings = {
368 processData : false,
369 cache : false,
370 type : "POST",
371 dataType : "json",
372 async : false,
373 success : function (data, status, xhr) {
374 var notebook_name = data.name;
375 window.open(
376 utils.url_path_join(
377 base_project_url,
378 'notebooks',
379 path,
380 notebook_name),
381 '_blank'
382 );
383 }
384 };
385 var url = utils.url_path_join(
386 base_project_url,
387 'api/notebooks',
388 path
389 );
390 $.ajax(url, settings);
391 };
392
295 IPython.NotebookList = NotebookList;
393 IPython.NotebookList = NotebookList;
296
394
297 return IPython;
395 return IPython;
298
396
299 }(IPython));
397 }(IPython));
300
@@ -21,10 +21,11 b' window.mathjax_url = "{{mathjax_url}}";'
21
21
22 {% block params %}
22 {% block params %}
23
23
24 data-project={{project}}
24 data-project="{{project}}"
25 data-base-project-url={{base_project_url}}
25 data-base-project-url="{{base_project_url}}"
26 data-base-kernel-url={{base_kernel_url}}
26 data-base-kernel-url="{{base_kernel_url}}"
27 data-notebook-id={{notebook_id}}
27 data-notebook-name="{{notebook_name}}"
28 data-notebook-path="{{notebook_path}}"
28 class="notebook_app"
29 class="notebook_app"
29
30
30 {% endblock %}
31 {% endblock %}
@@ -72,8 +73,8 b' class="notebook_app"'
72 <li class="divider"></li>
73 <li class="divider"></li>
73 <li class="dropdown-submenu"><a href="#">Download as</a>
74 <li class="dropdown-submenu"><a href="#">Download as</a>
74 <ul class="dropdown-menu">
75 <ul class="dropdown-menu">
75 <li id="download_ipynb"><a href="#">IPython (.ipynb)</a></li>
76 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
76 <li id="download_py"><a href="#">Python (.py)</a></li>
77 <!-- <li id="download_py"><a href="#">Python (.py)</a></li> -->
77 </ul>
78 </ul>
78 </li>
79 </li>
79 <li class="divider"></li>
80 <li class="divider"></li>
@@ -240,6 +241,7 b' class="notebook_app"'
240 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
241 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
241 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
242 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
242 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
243 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
244 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
243 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
245 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
244 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
246 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
245 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
247 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
@@ -49,7 +49,7 b''
49 <div id="header" class="navbar navbar-static-top">
49 <div id="header" class="navbar navbar-static-top">
50 <div class="navbar-inner navbar-nobg">
50 <div class="navbar-inner navbar-nobg">
51 <div class="container">
51 <div class="container">
52 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_project_url}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
52 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_project_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
53
53
54 {% block login_widget %}
54 {% block login_widget %}
55
55
@@ -10,9 +10,10 b''
10
10
11 {% block params %}
11 {% block params %}
12
12
13 data-project={{project}}
13 data-project="{{project}}"
14 data-base-project-url={{base_project_url}}
14 data-base-project-url="{{base_project_url}}"
15 data-base-kernel-url={{base_kernel_url}}
15 data-notebook-path="{{notebook_path}}"
16 data-base-kernel-url="{{base_kernel_url}}"
16
17
17 {% endblock %}
18 {% endblock %}
18
19
@@ -46,7 +47,7 b' data-base-kernel-url={{base_kernel_url}}'
46 <div id="notebook_list_header" class="row-fluid list_header">
47 <div id="notebook_list_header" class="row-fluid list_header">
47 <div id="project_name">
48 <div id="project_name">
48 <ul class="breadcrumb">
49 <ul class="breadcrumb">
49 {% for component in project_component %}
50 {% for component in tree_url_path.strip('/').split('/') %}
50 <li>{{component}} <span>/</span></li>
51 <li>{{component}} <span>/</span></li>
51 {% endfor %}
52 {% endfor %}
52 </ul>
53 </ul>
@@ -82,6 +83,7 b' data-base-kernel-url={{base_kernel_url}}'
82
83
83 {% block script %}
84 {% block script %}
84 {{super()}}
85 {{super()}}
86 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
85 <script src="{{static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
87 <script src="{{static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
86 <script src="{{static_url("tree/js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
88 <script src="{{static_url("tree/js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
87 <script src="{{static_url("tree/js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
89 <script src="{{static_url("tree/js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
@@ -15,23 +15,53 b' Authors:'
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import os
18
19
19 from tornado import web
20 from tornado import web
20 from ..base.handlers import IPythonHandler
21 from ..base.handlers import IPythonHandler
22 from ..utils import url_path_join, path2url, url2path, url_escape
23 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
21
24
22 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
23 # Handlers
26 # Handlers
24 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
25
28
26
29
27 class ProjectDashboardHandler(IPythonHandler):
30 class TreeHandler(IPythonHandler):
31 """Render the tree view, listing notebooks, clusters, etc."""
28
32
29 @web.authenticated
33 @web.authenticated
30 def get(self):
34 def get(self, path='', name=None):
31 self.write(self.render_template('tree.html',
35 path = path.strip('/')
32 project=self.project,
36 nbm = self.notebook_manager
33 project_component=self.project.split('/'),
37 if name is not None:
38 # is a notebook, redirect to notebook handler
39 url = url_escape(url_path_join(
40 self.base_project_url, 'notebooks', path, name
41 ))
42 self.log.debug("Redirecting %s to %s", self.request.path, url)
43 self.redirect(url)
44 else:
45 if not nbm.path_exists(path=path):
46 # no such directory, 404
47 raise web.HTTPError(404)
48 self.write(self.render_template('tree.html',
49 project=self.project_dir,
50 tree_url_path=path,
51 notebook_path=path,
52 ))
53
54
55 class TreeRedirectHandler(IPythonHandler):
56 """Redirect a request to the corresponding tree URL"""
57
58 @web.authenticated
59 def get(self, path=''):
60 url = url_escape(url_path_join(
61 self.base_project_url, 'tree', path.strip('/')
34 ))
62 ))
63 self.log.debug("Redirecting %s to %s", self.request.path, url)
64 self.redirect(url)
35
65
36
66
37 #-----------------------------------------------------------------------------
67 #-----------------------------------------------------------------------------
@@ -39,4 +69,9 b' class ProjectDashboardHandler(IPythonHandler):'
39 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
40
70
41
71
42 default_handlers = [(r"/", ProjectDashboardHandler)] No newline at end of file
72 default_handlers = [
73 (r"/tree%s" % _notebook_path_regex, TreeHandler),
74 (r"/tree%s" % _path_regex, TreeHandler),
75 (r"/tree", TreeHandler),
76 (r"/", TreeRedirectHandler),
77 ]
@@ -12,6 +12,11 b' Authors:'
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 import os
16 from urllib import quote, unquote
17
18 from IPython.utils import py3compat
19
15 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
16 # Imports
21 # Imports
17 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
@@ -24,9 +29,43 b' def url_path_join(*pieces):'
24 """
29 """
25 initial = pieces[0].startswith('/')
30 initial = pieces[0].startswith('/')
26 final = pieces[-1].endswith('/')
31 final = pieces[-1].endswith('/')
27 striped = [s.strip('/') for s in pieces]
32 stripped = [s.strip('/') for s in pieces]
28 result = '/'.join(s for s in striped if s)
33 result = '/'.join(s for s in stripped if s)
29 if initial: result = '/' + result
34 if initial: result = '/' + result
30 if final: result = result + '/'
35 if final: result = result + '/'
31 if result == '//': result = '/'
36 if result == '//': result = '/'
32 return result
37 return result
38
39 def path2url(path):
40 """Convert a local file path to a URL"""
41 pieces = [ quote(p) for p in path.split(os.sep) ]
42 # preserve trailing /
43 if pieces[-1] == '':
44 pieces[-1] = '/'
45 url = url_path_join(*pieces)
46 return url
47
48 def url2path(url):
49 """Convert a URL to a local file path"""
50 pieces = [ unquote(p) for p in url.split('/') ]
51 path = os.path.join(*pieces)
52 return path
53
54 def url_escape(path):
55 """Escape special characters in a URL path
56
57 Turns '/foo bar/' into '/foo%20bar/'
58 """
59 parts = py3compat.unicode_to_str(path).split('/')
60 return u'/'.join([quote(p) for p in parts])
61
62 def url_unescape(path):
63 """Unescape special characters in a URL path
64
65 Turns '/foo%20bar/' into '/foo bar/'
66 """
67 return u'/'.join([
68 py3compat.str_to_unicode(unquote(p))
69 for p in py3compat.unicode_to_str(path).split('/')
70 ])
71
@@ -1,5 +1,11 b''
1 """Produce SVG versions of active plots for display by the rich Qt frontend.
1 """A matplotlib backend for publishing figures via display_data"""
2 """
2 #-----------------------------------------------------------------------------
3 # Copyright (C) 2011 The IPython Development Team
4 #
5 # Distributed under the terms of the BSD License. The full license is in
6 # the file COPYING, distributed as part of this software.
7 #-----------------------------------------------------------------------------
8
3 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
4 # Imports
10 # Imports
5 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
@@ -7,80 +13,14 b' from __future__ import print_function'
7
13
8 # Third-party imports
14 # Third-party imports
9 import matplotlib
15 import matplotlib
10 from matplotlib.backends.backend_agg import new_figure_manager, FigureCanvasAgg
16 from matplotlib.backends.backend_agg import FigureCanvasAgg
11 from matplotlib._pylab_helpers import Gcf
17 from matplotlib._pylab_helpers import Gcf
12
18
13 # Local imports.
19 # Local imports
14 from IPython.config.configurable import SingletonConfigurable
20 from IPython.core.getipython import get_ipython
15 from IPython.core.display import display
21 from IPython.core.display import display
16 from IPython.core.displaypub import publish_display_data
17 from IPython.core.pylabtools import print_figure, select_figure_format
18 from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, Bool
19 from IPython.utils.warn import warn
20
21 #-----------------------------------------------------------------------------
22 # Configurable for inline backend options
23 #-----------------------------------------------------------------------------
24 # inherit from InlineBackendConfig for deprecation purposes
25 class InlineBackendConfig(SingletonConfigurable):
26 pass
27
28 class InlineBackend(InlineBackendConfig):
29 """An object to store configuration of the inline backend."""
30
31 def _config_changed(self, name, old, new):
32 # warn on change of renamed config section
33 if new.InlineBackendConfig != old.InlineBackendConfig:
34 warn("InlineBackendConfig has been renamed to InlineBackend")
35 super(InlineBackend, self)._config_changed(name, old, new)
36
37 # The typical default figure size is too large for inline use,
38 # so we shrink the figure size to 6x4, and tweak fonts to
39 # make that fit.
40 rc = Dict({'figure.figsize': (6.0,4.0),
41 # play nicely with white background in the Qt and notebook frontend
42 'figure.facecolor': 'white',
43 'figure.edgecolor': 'white',
44 # 12pt labels get cutoff on 6x4 logplots, so use 10pt.
45 'font.size': 10,
46 # 72 dpi matches SVG/qtconsole
47 # this only affects PNG export, as SVG has no dpi setting
48 'savefig.dpi': 72,
49 # 10pt still needs a little more room on the xlabel:
50 'figure.subplot.bottom' : .125
51 }, config=True,
52 help="""Subset of matplotlib rcParams that should be different for the
53 inline backend."""
54 )
55
56 figure_format = CaselessStrEnum(['svg', 'png', 'retina'], default_value='png', config=True,
57 help="The image format for figures with the inline backend.")
58
59 def _figure_format_changed(self, name, old, new):
60 if self.shell is None:
61 return
62 else:
63 select_figure_format(self.shell, new)
64
65 close_figures = Bool(True, config=True,
66 help="""Close all figures at the end of each cell.
67
68 When True, ensures that each cell starts with no active figures, but it
69 also means that one must keep track of references in order to edit or
70 redraw figures in subsequent cells. This mode is ideal for the notebook,
71 where residual plots from other cells might be surprising.
72
73 When False, one must call figure() to create new figures. This means
74 that gcf() and getfigs() can reference figures created in other cells,
75 and the active figure can continue to be edited with pylab/pyplot
76 methods that reference the current active figure. This mode facilitates
77 iterative editing of figures, and behaves most consistently with
78 other matplotlib backends, but figure barriers between cells must
79 be explicit.
80 """)
81
82 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
83
22
23 from .config import InlineBackend
84
24
85 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
86 # Functions
26 # Functions
@@ -107,7 +47,6 b' def show(close=None):'
107 matplotlib.pyplot.close('all')
47 matplotlib.pyplot.close('all')
108
48
109
49
110
111 # This flag will be reset by draw_if_interactive when called
50 # This flag will be reset by draw_if_interactive when called
112 show._draw_called = False
51 show._draw_called = False
113 # list of figures to draw when flush_figures is called
52 # list of figures to draw when flush_figures is called
@@ -179,12 +118,11 b' def flush_figures():'
179 return show(True)
118 return show(True)
180 except Exception as e:
119 except Exception as e:
181 # safely show traceback if in IPython, else raise
120 # safely show traceback if in IPython, else raise
182 try:
121 ip = get_ipython()
183 get_ipython
122 if ip is None:
184 except NameError:
185 raise e
123 raise e
186 else:
124 else:
187 get_ipython().showtraceback()
125 ip.showtraceback()
188 return
126 return
189 try:
127 try:
190 # exclude any figures that were closed:
128 # exclude any figures that were closed:
@@ -194,13 +132,12 b' def flush_figures():'
194 display(fig)
132 display(fig)
195 except Exception as e:
133 except Exception as e:
196 # safely show traceback if in IPython, else raise
134 # safely show traceback if in IPython, else raise
197 try:
135 ip = get_ipython()
198 get_ipython
136 if ip is None:
199 except NameError:
200 raise e
137 raise e
201 else:
138 else:
202 get_ipython().showtraceback()
139 ip.showtraceback()
203 break
140 return
204 finally:
141 finally:
205 # clear flags for next round
142 # clear flags for next round
206 show._to_draw = []
143 show._to_draw = []
@@ -29,7 +29,7 b' from IPython.nbformat.v3 import ('
29 NotebookNode,
29 NotebookNode,
30 new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet,
30 new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet,
31 parse_filename, new_metadata, new_author, new_heading_cell, nbformat,
31 parse_filename, new_metadata, new_author, new_heading_cell, nbformat,
32 nbformat_minor,
32 nbformat_minor, to_notebook_json
33 )
33 )
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
@@ -141,11 +141,12 b" have['rpy2'] = test_for('rpy2')"
141 have['sqlite3'] = test_for('sqlite3')
141 have['sqlite3'] = test_for('sqlite3')
142 have['cython'] = test_for('Cython')
142 have['cython'] = test_for('Cython')
143 have['oct2py'] = test_for('oct2py')
143 have['oct2py'] = test_for('oct2py')
144 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
144 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
145 have['jinja2'] = test_for('jinja2')
145 have['jinja2'] = test_for('jinja2')
146 have['wx'] = test_for('wx')
146 have['wx'] = test_for('wx')
147 have['wx.aui'] = test_for('wx.aui')
147 have['wx.aui'] = test_for('wx.aui')
148 have['azure'] = test_for('azure')
148 have['azure'] = test_for('azure')
149 have['requests'] = test_for('requests')
149 have['sphinx'] = test_for('sphinx')
150 have['sphinx'] = test_for('sphinx')
150
151
151 min_zmq = (2,1,11)
152 min_zmq = (2,1,11)
@@ -270,7 +271,7 b" test_sections['qt'].requires('zmq', 'qt', 'pygments')"
270
271
271 # html:
272 # html:
272 sec = test_sections['html']
273 sec = test_sections['html']
273 sec.requires('zmq', 'tornado')
274 sec.requires('zmq', 'tornado', 'requests')
274 # The notebook 'static' directory contains JS, css and other
275 # The notebook 'static' directory contains JS, css and other
275 # files for web serving. Occasionally projects may put a .py
276 # files for web serving. Occasionally projects may put a .py
276 # file in there (MathJax ships a conf.py), so we might as
277 # file in there (MathJax ships a conf.py), so we might as
General Comments 0
You need to be logged in to leave comments. Login now