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 |
|
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. |
|
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. |
|
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. |
|
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 |
|
507 | if os.sep != "/": | |
407 |
url_path = url_path.replace("/", os. |
|
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 N |
|
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, |
|
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_ |
|
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" % _ |
|
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 |
|
|
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 |
|
|
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 |
|
|
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) |
|
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. |
|
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 |
|
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( |
|
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( |
|
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, |
|
48 | def start_kernel(self, kernel_id=None, **kwargs): | |
74 |
"""Start a kernel for a |
|
49 | """Start a kernel for a session an return its kernel_id. | |
75 |
|
50 | |||
76 | Parameters |
|
51 | Parameters | |
77 | ---------- |
|
52 | ---------- | |
78 |
|
|
53 | kernel_id : uuid | |
79 |
The uuid |
|
54 | The uuid to associate the new kernel with. If this | |
80 |
is not None, this kernel will be persistent whenever |
|
55 | is not None, this kernel will be persistent whenever it is | |
81 |
request |
|
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 |
|
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 |
|
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 |
|
223 | if 'content' not in model: | |
179 |
raise web.HTTPError(40 |
|
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 |
# |
|
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( |
|
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 |
|
257 | def update_notebook_model(self, model, name, path=''): | |
240 |
""" |
|
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(n |
|
275 | for checkpoint in self.list_checkpoints(name, path): | |
247 |
checkpoint_id = checkpoint[' |
|
276 | checkpoint_id = checkpoint['id'] | |
248 |
path = self.get_checkpoint_path( |
|
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 |
s |
|
280 | os.unlink(cp_path) | |
252 | os.unlink(path) |
|
|||
253 |
|
281 | |||
254 |
self.log.debug(" |
|
282 | self.log.debug("Unlinking notebook %s", os_path) | |
255 |
os.unlink( |
|
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 |
|
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_ |
|
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 |
|
|
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, n |
|
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( |
|
357 | cp_path = self.get_checkpoint_path(checkpoint_id, name, path) | |
312 |
self.log.debug("creating checkpoint for notebook %s", n |
|
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_ |
|
364 | return self.get_checkpoint_model(checkpoint_id, name, path) | |
319 |
|
365 | |||
320 |
def list_checkpoints(self, n |
|
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_ |
|
377 | return [self.get_checkpoint_model(checkpoint_id, name, path)] | |
331 |
|
378 | |||
332 |
|
379 | |||
333 |
def restore_checkpoint(self, |
|
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 |
|
|
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' % (n |
|
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, |
|
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' % ( |
|
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) 20 |
|
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 . |
|
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. |
|
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(n |
|
219 | checkpoints = nbm.list_checkpoints(name, path) | |
100 |
data = json |
|
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(n |
|
228 | checkpoint = nbm.create_checkpoint(name, path) | |
108 |
data = json |
|
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( |
|
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( |
|
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", Notebook |
|
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 |
|
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 |
""" |
|
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 |
|
|
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 |
|
|
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_n |
|
107 | def save_notebook_model(self, model, name, path=''): | |
133 |
"""Save |
|
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 |
|
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 |
|
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( |
|
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, n |
|
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, n |
|
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, |
|
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, |
|
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 |
|
|
21 | fm = FileNotebookManager(notebook_dir=td) | |
17 |
self.assertEqual( |
|
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 |
|
|
27 | fm = FileNotebookManager(notebook_dir=nbdir) | |
23 |
self.assertEqual( |
|
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( |
|
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. |
|
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 notebo |
|
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 |
|
|
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( |
|
80 | window.open(utils.url_path_join( | |
74 | }); |
|
81 | that.baseProjectUrl(), | |
75 | this.element.find('#rename_notebook').click(function () { |
|
82 | 'tree', | |
76 |
|
|
83 | that.notebookPath() | |
|
84 | )); | |||
77 | }); |
|
85 | }); | |
78 | this.element.find('#copy_notebook').click(function () { |
|
86 | this.element.find('#copy_notebook').click(function () { | |
79 |
|
|
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_ |
|
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_ |
|
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. |
|
133 | IPython.notebook.session.delete(); | |
103 |
setTimeout(function(){window.close();}, |
|
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. |
|
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 (! |
|
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) 20 |
|
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. |
|
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. |
|
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 |
// |
|
1387 | // Session related things | |
1376 |
|
1388 | |||
1377 | /** |
|
1389 | /** | |
1378 |
* Start a new |
|
1390 | * Start a new session and set it on each code cell. | |
1379 | * |
|
1391 | * | |
1380 |
* @method start_ |
|
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. |
|
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. |
|
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 = |
|
1571 | this.metadata = content.metadata; | |
1561 |
this.notebook_name = data. |
|
1572 | this.notebook_name = data.name; | |
1562 | // Only handle 1 worksheet for now. |
|
1573 | // Only handle 1 worksheet for now. | |
1563 |
var worksheet = |
|
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 ( |
|
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 |
|
1668 | var model = {}; | |
1658 |
|
|
1669 | model.name = this.notebook_name; | |
1659 |
|
|
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( |
|
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_ |
|
1876 | * @param {String} notebook_name and path A notebook to load | |
1736 | */ |
|
1877 | */ | |
1737 |
Notebook.prototype.load_notebook = function (notebook_ |
|
1878 | Notebook.prototype.load_notebook = function (notebook_name, notebook_path) { | |
1738 | var that = this; |
|
1879 | var that = this; | |
1739 |
this.notebook_ |
|
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 |
|
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. |
|
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. |
|
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(' |
|
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_ |
|
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(' |
|
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_ |
|
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. |
|
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(th |
|
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 |
|
99 | var url = utils.url_path_join(this.kernel_url, "restart"); | |
101 | $.post(url, |
|
100 | $.post(url, | |
102 |
$.proxy(th |
|
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. |
|
109 | console.log("Kernel started: ", json.id); | |
111 | this.running = true; |
|
110 | this.running = true; | |
112 |
this.kernel_id = json. |
|
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 |
|
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) 20 |
|
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() |
|
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_ |
|
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_ |
|
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) 20 |
|
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 |
|
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 |
|
72 | var name_and_ext = utils.splitext(f.name); | |
63 |
var nbname = |
|
73 | var nbname = name_and_ext[0]; | |
64 |
var |
|
74 | var file_ext = name_and_ext[-1]; | |
65 |
if ( |
|
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 |
|
|
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 n |
|
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( |
|
179 | this.add_link(path, nbname, item); | |
129 | // hide delete buttons when readonly |
|
180 | name = utils.url_path_join(this.notebookPath(), name); | |
130 |
if( |
|
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, |
|
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 ( |
|
211 | NotebookList.prototype.add_link = function (path, nbname, item) { | |
161 | item.data('nbname', nbname); |
|
212 | item.data('nbname', nbname); | |
162 |
item.data(' |
|
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, |
|
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 ( |
|
253 | success : function () { | |
197 |
that.load_ |
|
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 = |
|
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'). |
|
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 = ' |
|
322 | var content_type = 'application/json'; | |
259 |
|
|
323 | var model = { | |
260 |
content |
|
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 : |
|
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 |
|
|
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- |
|
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 |
|
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 |
|
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 |
|
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 |
|
|
121 | ip = get_ipython() | |
183 |
|
|
122 | if ip is None: | |
184 | except NameError: |
|
|||
185 | raise e |
|
123 | raise e | |
186 | else: |
|
124 | else: | |
187 |
|
|
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 |
|
|
135 | ip = get_ipython() | |
198 |
|
|
136 | if ip is None: | |
199 | except NameError: |
|
|||
200 | raise e |
|
137 | raise e | |
201 | else: |
|
138 | else: | |
202 |
|
|
139 | ip.showtraceback() | |
203 |
|
|
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', ( |
|
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