##// END OF EJS Templates
Added notebooks API tests.
Zachary Sailer -
Show More
@@ -0,0 +1,32 b''
1 """Test the sessions service API."""
2
3
4 import os
5 import sys
6 import json
7 import urllib
8
9 import requests
10
11 from IPython.html.tests.launchnotebook import NotebookTestBase
12
13 '''
14 class SessionAPITest(NotebookTestBase):
15 """Test the sessions web service API"""
16
17 def base_url(self):
18 return super(SessionAPITest,self).base_url() + 'api/sessions'
19
20 def test_no_sessions(self):
21 """Make sure there are no sessions running at the start"""
22 url = self.base_url()
23 r = requests.get(url)
24 self.assertEqual(r.json(), [])
25
26 def test_start_session(self):
27 url = self.base_url()
28 param = urllib.urlencode({'notebook_path': 'test.ipynb'})
29 r = requests.post(url, params=param)
30 print r
31 #self.assertNotEqual(r.json(), [])
32 ''' No newline at end of file
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,124 b''
1 """Test the all of the services API."""
2
3
4 import os
5 import sys
6 import json
7 import urllib
8 from zmq.utils import jsonapi
9
10 import requests
11
12 from IPython.html.tests.launchnotebook import NotebookTestBase
13
14 class APITest(NotebookTestBase):
15 """Test the kernels web service API"""
16
17 def base_url(self):
18 return super(APITest,self).base_url()
19
20 def notebooks_url(self):
21 return self.base_url() + 'api/notebooks'
22
23 def kernels_url(self):
24 return self.base_url() + 'api/kernels'
25
26 def sessions_url(self):
27 return self.base_url() + 'api/sessions'
28
29 def contents_url(self):
30 return self.contents_url() + 'api/contents'
31
32 def mknb(self, name='', path='/'):
33 url = self.notebooks_url() + path
34 return url, requests.post(url)
35
36 def delnb(self, name, path='/'):
37 url = self.notebooks_url() + path + name
38 r = requests.delete(url)
39 return r.status_code
40
41 def test_no_notebooks(self):
42 url = self.notebooks_url()
43 r = requests.get(url)
44 self.assertEqual(r.json(), [])
45
46 def test_root_notebook_handler(self):
47 # POST a notebook and test the dict thats returned.
48 url, nb = self.mknb()
49 data = nb.json()
50 assert isinstance(data, dict)
51 assert data.has_key("name")
52 assert data.has_key("path")
53 self.assertEqual(data['name'], u'Untitled0.ipynb')
54 self.assertEqual(data['path'], u'/')
55
56 # GET list of notebooks in directory.
57 r = requests.get(url)
58 assert isinstance(r.json(), list)
59 assert isinstance(r.json()[0], dict)
60
61 # GET with a notebook name.
62 url = self.notebooks_url() + '/Untitled0.ipynb'
63 r = requests.get(url)
64 assert isinstance(data, dict)
65 self.assertEqual(r.json(), data)
66
67 # PATCH (rename) request.
68 new_name = {'name':'test.ipynb'}
69 r = requests.patch(url, data=jsonapi.dumps(new_name))
70 data = r.json()
71 assert isinstance(data, dict)
72
73 # make sure the patch worked.
74 new_url = self.notebooks_url() + '/test.ipynb'
75 r = requests.get(new_url)
76 assert isinstance(r.json(), dict)
77 self.assertEqual(r.json(), data)
78
79 # GET bad (old) notebook name.
80 r = requests.get(url)
81 self.assertEqual(r.status_code, 404)
82
83 # POST notebooks to folders one and two levels down.
84 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
85 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
86 url, nb = self.mknb(path='/foo/')
87 url2, nb2 = self.mknb(path='/foo/bar/')
88 data = nb.json()
89 data2 = nb2.json()
90 assert isinstance(data, dict)
91 assert isinstance(data2, dict)
92 assert data.has_key("name")
93 assert data.has_key("path")
94 self.assertEqual(data['name'], u'Untitled0.ipynb')
95 self.assertEqual(data['path'], u'/foo/')
96 assert data2.has_key("name")
97 assert data2.has_key("path")
98 self.assertEqual(data2['name'], u'Untitled0.ipynb')
99 self.assertEqual(data2['path'], u'/foo/bar/')
100
101 # GET request on notebooks one and two levels down.
102 r = requests.get(url+'Untitled0.ipynb')
103 r2 = requests.get(url2+'Untitled0.ipynb')
104 assert isinstance(r.json(), dict)
105 self.assertEqual(r.json(), data)
106 assert isinstance(r2.json(), dict)
107 self.assertEqual(r2.json(), data2)
108
109 # PATCH notebooks that are one and two levels down.
110 new_name = {'name': 'testfoo.ipynb'}
111 r = requests.patch(url+'Untitled0.ipynb', data=jsonapi.dumps(new_name))
112 r = requests.get(url+'testfoo.ipynb')
113 data = r.json()
114 assert isinstance(data, dict)
115 assert data.has_key('name')
116 self.assertEqual(data['name'], 'testfoo.ipynb')
117 r = requests.get(url+'Untitled0.ipynb')
118 self.assertEqual(r.status_code, 404)
119
120 # DELETE notebooks
121 r = self.delnb('testfoo.ipynb', '/foo/')
122 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
123 self.assertEqual(r, 204)
124 self.assertEqual(r2, 204) No newline at end of file
@@ -1,115 +1,117 b''
1 """Tests for the content manager."""
1 """Tests for the content manager."""
2
2
3 import os
3 import os
4 from unittest import TestCase
4 from unittest import TestCase
5 from tempfile import NamedTemporaryFile
5 from tempfile import NamedTemporaryFile
6
6
7 from IPython.utils.tempdir import TemporaryDirectory
7 from IPython.utils.tempdir import TemporaryDirectory
8 from IPython.utils.traitlets import TraitError
8 from IPython.utils.traitlets import TraitError
9
9
10 from ..contentmanager import ContentManager
10 from ..contentmanager import ContentManager
11
11
12
12
13 class TestContentManager(TestCase):
13 class TestContentManager(TestCase):
14
14
15 def test_new_folder(self):
15 def test_new_folder(self):
16 with TemporaryDirectory() as td:
16 with TemporaryDirectory() as td:
17 # Test that a new directory/folder is created
17 # Test that a new directory/folder is created
18 cm = ContentManager(content_dir=td)
18 cm = ContentManager(content_dir=td)
19 name = cm.new_folder(None, '/')
19 name = cm.new_folder(None, '/')
20 path = cm.get_os_path(name, '/')
20 path = cm.get_os_path(name, '/')
21 self.assertTrue(os.path.isdir(path))
21 self.assertTrue(os.path.isdir(path))
22
22
23 # Test that a new directory is created with
23 # Test that a new directory is created with
24 # the name given.
24 # the name given.
25 name = cm.new_folder('foo')
25 name = cm.new_folder('foo')
26 path = cm.get_os_path(name)
26 path = cm.get_os_path(name)
27 self.assertTrue(os.path.isdir(path))
27 self.assertTrue(os.path.isdir(path))
28
28
29 # Test that a new directory/folder is created
29 # Test that a new directory/folder is created
30 # in the '/foo' subdirectory
30 # in the '/foo' subdirectory
31 name1 = cm.new_folder(None, '/foo/')
31 name1 = cm.new_folder(None, '/foo/')
32 path1 = cm.get_os_path(name1, '/foo/')
32 path1 = cm.get_os_path(name1, '/foo/')
33 self.assertTrue(os.path.isdir(path1))
33 self.assertTrue(os.path.isdir(path1))
34
34
35 # make another file and make sure it incremented
35 # make another file and make sure it incremented
36 # the name and does not write over another file.
36 # the name and does not write over another file.
37 name2 = cm.new_folder(None, '/foo/')
37 name2 = cm.new_folder(None, '/foo/')
38 path2 = cm.get_os_path(name, '/foo/')
38 path2 = cm.get_os_path(name, '/foo/')
39 self.assertEqual(name2, 'new_folder1')
39 self.assertEqual(name2, 'new_folder1')
40
40
41 # Test that an HTTP Error is raised when the user
41 # Test that an HTTP Error is raised when the user
42 # tries to create a new folder with a name that
42 # tries to create a new folder with a name that
43 # already exists
43 # already exists
44 bad_name = 'new_folder1'
44 bad_name = 'new_folder1'
45 self.assertRaises(HTTPError, cm.new_folder, name=bad_name, path='/foo/')
45 self.assertRaises(HTTPError, cm.new_folder, name=bad_name, path='/foo/')
46
46
47 def test_delete_folder(self):
47 def test_delete_folder(self):
48 with TemporaryDirectory() as td:
48 with TemporaryDirectory() as td:
49 # Create a folder
49 # Create a folder
50 cm = ContentManager(content_dir=td)
50 cm = ContentManager(content_dir=td)
51 name = cm.new_folder('test_folder', '/')
51 name = cm.new_folder('test_folder', '/')
52 path = cm.get_os_path(name, '/')
52 path = cm.get_os_path(name, '/')
53
53
54 # Raise an exception when trying to delete a
54 # Raise an exception when trying to delete a
55 # folder that does not exist.
55 # folder that does not exist.
56 self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/')
56 self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/')
57
57
58 # Create a subfolder in the folder created above.
58 # Create a subfolder in the folder created above.
59 # *Recall 'name' = 'test_folder' (the new path for
59 # *Recall 'name' = 'test_folder' (the new path for
60 # subfolder)
60 # subfolder)
61 name01 = cm.new_folder(None, name)
61 name01 = cm.new_folder(None, name)
62 path01 = cm.get_os_path(name01, name)
62 path01 = cm.get_os_path(name01, name)
63 # Try to delete a subfolder that does not exist.
63 # Try to delete a subfolder that does not exist.
64 self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/')
64 self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/')
65 # Delete the created subfolder
65 # Delete the created subfolder
66 cm.delete_content(name01, name)
66 cm.delete_content(name01, name)
67 self.assertFalse(os.path.isdir(path01))
67 self.assertFalse(os.path.isdir(path01))
68
68
69 # Delete the created folder
69 # Delete the created folder
70 cm.delete_content(name, '/')
70 cm.delete_content(name, '/')
71 self.assertFalse(os.path.isdir(path))
71 self.assertFalse(os.path.isdir(path))
72
72
73 self.assertRaises(HTTPError, cm.delete_content, name=None, content_path='/')
73 self.assertRaises(HTTPError, cm.delete_content, name=None, content_path='/')
74 self.assertRaises(HTTPError, cm.delete_content, name='/', content_path='/')
74 self.assertRaises(HTTPError, cm.delete_content, name='/', content_path='/')
75
75
76
76
77 def test_get_content_names(self):
77 def test_get_content_names(self):
78 with TemporaryDirectory() as td:
78 with TemporaryDirectory() as td:
79 # Create a few folders and subfolders
79 # Create a few folders and subfolders
80 cm = ContentManager(content_dir=td)
80 cm = ContentManager(content_dir=td)
81 name1 = cm.new_folder('fold1', '/')
81 name1 = cm.new_folder('fold1', '/')
82 name2 = cm.new_folder('fold2', '/')
82 name2 = cm.new_folder('fold2', '/')
83 name3 = cm.new_folder('fold3', '/')
83 name3 = cm.new_folder('fold3', '/')
84 name01 = cm.new_folder('fold01', 'fold1')
84 name01 = cm.new_folder('fold01', 'fold1')
85 name02 = cm.new_folder('fold02', 'fold1')
85 name02 = cm.new_folder('fold02', 'fold1')
86 name03 = cm.new_folder('fold03', 'fold1')
86 name03 = cm.new_folder('fold03', 'fold1')
87
87
88 # List the names in the root folder
88 # List the names in the root folder
89 names = cm.get_content_names('/')
89 names = cm.get_content_names('/')
90 expected = ['fold1', 'fold2', 'fold3']
90 expected = ['fold1', 'fold2', 'fold3']
91 self.assertEqual(set(names), set(expected))
91 self.assertEqual(set(names), set(expected))
92
92
93 # List the names in the subfolder 'fold1'.
93 # List the names in the subfolder 'fold1'.
94 names = cm.get_content_names('fold1')
94 names = cm.get_content_names('fold1')
95 expected = ['fold01', 'fold02', 'fold03']
95 expected = ['fold01', 'fold02', 'fold03']
96 self.assertEqual(set(names), set(expected))
96 self.assertEqual(set(names), set(expected))
97
97
98 def test_content_model(self):
98 def test_content_model(self):
99 with TemporaryDirectory() as td:
99 with TemporaryDirectory() as td:
100 # Create a few folders and subfolders
100 # Create a few folders and subfolders
101 cm = ContentManager(content_dir=td)
101 cm = ContentManager(content_dir=td)
102 name1 = cm.new_folder('fold1', '/')
102 name1 = cm.new_folder('fold1', '/')
103 name2 = cm.new_folder('fold2', '/')
103 name2 = cm.new_folder('fold2', '/')
104 name01 = cm.new_folder('fold01', 'fold1')
104 name01 = cm.new_folder('fold01', 'fold1')
105 name02 = cm.new_folder('fold02', 'fold1')
105 name02 = cm.new_folder('fold02', 'fold1')
106
106
107 # Check to see if the correct model and list of
107 # Check to see if the correct model and list of
108 # model dicts are returned for root directory
108 # model dicts are returned for root directory
109 # and subdirectory.
109 # and subdirectory.
110 contents = cm.list_contents('/')
110 contents = cm.list_contents('/')
111 contents1 = cm.list_contents('fold1')
111 contents1 = cm.list_contents('fold1')
112 self.assertEqual(type(contents), type(list()))
112 assert isinstance(contents, list)
113 self.assertEqual(type(contents[0]), type(dict()))
113 assert isinstance(contents[0], dict)
114 assert contents[0].has_key('path')
115 assert contents[0].has_key('path')
114 self.assertEqual(contents[0]['path'], '/')
116 self.assertEqual(contents[0]['path'], '/')
115 self.assertEqual(contents1[0]['path'], 'fold1')
117 self.assertEqual(contents1[0]['path'], 'fold1')
@@ -1,373 +1,347 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import datetime
19 import datetime
20 import io
20 import io
21 import os
21 import os
22 import glob
22 import glob
23 import shutil
23 import shutil
24
24
25 from unicodedata import normalize
25 from unicodedata import normalize
26
26
27 from tornado import web
27 from tornado import web
28
28
29 from .nbmanager import NotebookManager
29 from .nbmanager import NotebookManager
30 from IPython.nbformat import current
30 from IPython.nbformat import current
31 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
32 from IPython.utils import tz
32 from IPython.utils import tz
33
33
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Classes
35 # Classes
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class FileNotebookManager(NotebookManager):
38 class FileNotebookManager(NotebookManager):
39
39
40 save_script = Bool(False, config=True,
40 save_script = Bool(False, config=True,
41 help="""Automatically create a Python script when saving the notebook.
41 help="""Automatically create a Python script when saving the notebook.
42
42
43 For easier use of import, %run and %load across notebooks, a
43 For easier use of import, %run and %load across notebooks, a
44 <notebook-name>.py script will be created next to any
44 <notebook-name>.py script will be created next to any
45 <notebook-name>.ipynb on each save. This can also be set with the
45 <notebook-name>.ipynb on each save. This can also be set with the
46 short `--script` flag.
46 short `--script` flag.
47 """
47 """
48 )
48 )
49
49
50 checkpoint_dir = Unicode(config=True,
50 checkpoint_dir = Unicode(config=True,
51 help="""The location in which to keep notebook checkpoints
51 help="""The location in which to keep notebook checkpoints
52
52
53 By default, it is notebook-dir/.ipynb_checkpoints
53 By default, it is notebook-dir/.ipynb_checkpoints
54 """
54 """
55 )
55 )
56 def _checkpoint_dir_default(self):
56 def _checkpoint_dir_default(self):
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58
58
59 def _checkpoint_dir_changed(self, name, old, new):
59 def _checkpoint_dir_changed(self, name, old, new):
60 """do a bit of validation of the checkpoint dir"""
60 """do a bit of validation of the checkpoint dir"""
61 if not os.path.isabs(new):
61 if not os.path.isabs(new):
62 # If we receive a non-absolute path, make it absolute.
62 # If we receive a non-absolute path, make it absolute.
63 abs_new = os.path.abspath(new)
63 abs_new = os.path.abspath(new)
64 self.checkpoint_dir = abs_new
64 self.checkpoint_dir = abs_new
65 return
65 return
66 if os.path.exists(new) and not os.path.isdir(new):
66 if os.path.exists(new) and not os.path.isdir(new):
67 raise TraitError("checkpoint dir %r is not a directory" % new)
67 raise TraitError("checkpoint dir %r is not a directory" % new)
68 if not os.path.exists(new):
68 if not os.path.exists(new):
69 self.log.info("Creating checkpoint dir %s", new)
69 self.log.info("Creating checkpoint dir %s", new)
70 try:
70 try:
71 os.mkdir(new)
71 os.mkdir(new)
72 except:
72 except:
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
74
74
75 filename_ext = Unicode(u'.ipynb')
75 filename_ext = Unicode(u'.ipynb')
76
76
77
77
78 def get_notebook_names(self, path):
78 def get_notebook_names(self, path):
79 """List all notebook names in the notebook dir."""
79 """List all notebook names in the notebook dir."""
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
81 names = [os.path.basename(name)
81 names = [os.path.basename(name)
82 for name in names]
82 for name in names]
83 return names
83 return names
84
84
85 def list_notebooks(self, path):
85 def list_notebooks(self, path):
86 """List all notebooks in the notebook dir."""
86 """List all notebooks in the notebook dir."""
87 notebook_names = self.get_notebook_names(path)
87 notebook_names = self.get_notebook_names(path)
88 notebooks = []
88 notebooks = []
89 for name in notebook_names:
89 for name in notebook_names:
90 model = self.notebook_model(name, path, content=False)
90 model = self.notebook_model(name, path, content=False)
91 notebooks.append(model)
91 notebooks.append(model)
92 return notebooks
92 return notebooks
93
93
94 def change_notebook(self, data, notebook_name, notebook_path='/'):
94 def change_notebook(self, data, notebook_name, notebook_path='/'):
95 """Changes notebook"""
95 """Changes notebook"""
96 changes = data.keys()
96 changes = data.keys()
97 response = 200
98 for change in changes:
97 for change in changes:
99 full_path = self.get_os_path(notebook_name, notebook_path)
98 full_path = self.get_os_path(notebook_name, notebook_path)
100 if change == "name":
99 if change == "name":
101 new_path = self.get_os_path(data['name'], notebook_path)
100 new_path = self.get_os_path(data['name'], notebook_path)
102 if not os.path.isfile(new_path):
101 try:
103 os.rename(full_path,
102 os.rename(full_path,
104 self.get_os_path(data['name'], notebook_path))
103 self.get_os_path(data['name'], notebook_path))
105 notebook_name = data['name']
104 notebook_name = data['name']
106 else:
105 except OSError as e:
107 response = 409
106 raise web.HTTPError(409, u'Notebook name already exists.')
108 if change == "path":
107 if change == "path":
109 new_path = self.get_os_path(data['name'], data['path'])
108 new_path = self.get_os_path(data['name'], data['path'])
110 stutil.move(full_path, new_path)
109 stutil.move(full_path, new_path)
111 notebook_path = data['path']
110 notebook_path = data['path']
112 if change == "content":
111 if change == "content":
113 self.save_notebook(data, notebook_name, notebook_path)
112 self.save_notebook(data, notebook_name, notebook_path)
114 model = self.notebook_model(notebook_name, notebook_path)
113 model = self.notebook_model(notebook_name, notebook_path)
115 return model, response
114 return model
116
115
117 def notebook_exists(self, name, path):
116 def notebook_exists(self, name, path):
118 """Returns a True if the notebook exists. Else, returns False.
117 """Returns a True if the notebook exists. Else, returns False.
119
118
120 Parameters
119 Parameters
121 ----------
120 ----------
122 name : string
121 name : string
123 The name of the notebook you are checking.
122 The name of the notebook you are checking.
124 path : string
123 path : string
125 The relative path to the notebook (with '/' as separator)
124 The relative path to the notebook (with '/' as separator)
126
125
127 Returns
126 Returns
128 -------
127 -------
129 bool
128 bool
130 """
129 """
131 path = self.get_os_path(name, path)
130 path = self.get_os_path(name, path)
132 return os.path.isfile(path)
131 return os.path.isfile(path)
133
132
134 def get_os_path(self, fname, path='/'):
135 """Given a notebook name and a server URL path, return its file system
136 path.
137
138 Parameters
139 ----------
140 fname : string
141 The name of a notebook file with the .ipynb extension
142 path : string
143 The relative URL path (with '/' as separator) to the named
144 notebook.
145
146 Returns
147 -------
148 path : string
149 A file system path that combines notebook_dir (location where
150 server started), the relative path, and the filename with the
151 current operating system's url.
152 """
153 parts = path.split('/')
154 parts = [p for p in parts if p != ''] # remove duplicate splits
155 parts += [fname]
156 path = os.path.join(self.notebook_dir, *parts)
157 return path
158
159 def read_notebook_object_from_path(self, path):
133 def read_notebook_object_from_path(self, path):
160 """read a notebook object from a path"""
134 """read a notebook object from a path"""
161 info = os.stat(path)
135 info = os.stat(path)
162 last_modified = tz.utcfromtimestamp(info.st_mtime)
136 last_modified = tz.utcfromtimestamp(info.st_mtime)
163 with open(path,'r') as f:
137 with open(path,'r') as f:
164 s = f.read()
138 s = f.read()
165 try:
139 try:
166 # v1 and v2 and json in the .ipynb files.
140 # v1 and v2 and json in the .ipynb files.
167 nb = current.reads(s, u'json')
141 nb = current.reads(s, u'json')
168 except ValueError as e:
142 except ValueError as e:
169 msg = u"Unreadable Notebook: %s" % e
143 msg = u"Unreadable Notebook: %s" % e
170 raise web.HTTPError(400, msg, reason=msg)
144 raise web.HTTPError(400, msg, reason=msg)
171 return last_modified, nb
145 return last_modified, nb
172
146
173 def read_notebook_object(self, notebook_name, notebook_path='/'):
147 def read_notebook_object(self, notebook_name, notebook_path='/'):
174 """Get the Notebook representation of a notebook by notebook_name."""
148 """Get the Notebook representation of a notebook by notebook_name."""
175 path = self.get_os_path(notebook_name, notebook_path)
149 path = self.get_os_path(notebook_name, notebook_path)
176 if not os.path.isfile(path):
150 if not os.path.isfile(path):
177 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
151 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
178 last_modified, nb = self.read_notebook_object_from_path(path)
152 last_modified, nb = self.read_notebook_object_from_path(path)
179 # Always use the filename as the notebook name.
153 # Always use the filename as the notebook name.
180 # Eventually we will get rid of the notebook name in the metadata
154 # Eventually we will get rid of the notebook name in the metadata
181 # but for now, that name is just an empty string. Until the notebooks
155 # but for now, that name is just an empty string. Until the notebooks
182 # web service knows about names in URLs we still pass the name
156 # web service knows about names in URLs we still pass the name
183 # back to the web app using the metadata though.
157 # back to the web app using the metadata though.
184 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
158 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
185 return last_modified, nb
159 return last_modified, nb
186
160
187 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
161 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
188 """Save an existing notebook object by notebook_name."""
162 """Save an existing notebook object by notebook_name."""
189 if new_name == None:
163 if new_name == None:
190 try:
164 try:
191 new_name = normalize('NFC', nb.metadata.name)
165 new_name = normalize('NFC', nb.metadata.name)
192 except AttributeError:
166 except AttributeError:
193 raise web.HTTPError(400, u'Missing notebook name')
167 raise web.HTTPError(400, u'Missing notebook name')
194
168
195 new_path = notebook_path
169 new_path = notebook_path
196 old_name = notebook_name
170 old_name = notebook_name
197 old_checkpoints = self.list_checkpoints(old_name)
171 old_checkpoints = self.list_checkpoints(old_name)
198
172
199 path = self.get_os_path(new_name, new_path)
173 path = self.get_os_path(new_name, new_path)
200
174
201 # Right before we save the notebook, we write an empty string as the
175 # Right before we save the notebook, we write an empty string as the
202 # notebook name in the metadata. This is to prepare for removing
176 # notebook name in the metadata. This is to prepare for removing
203 # this attribute entirely post 1.0. The web app still uses the metadata
177 # this attribute entirely post 1.0. The web app still uses the metadata
204 # name for now.
178 # name for now.
205 nb.metadata.name = u''
179 nb.metadata.name = u''
206
180
207 try:
181 try:
208 self.log.debug("Autosaving notebook %s", path)
182 self.log.debug("Autosaving notebook %s", path)
209 with open(path,'w') as f:
183 with open(path,'w') as f:
210 current.write(nb, f, u'json')
184 current.write(nb, f, u'json')
211 except Exception as e:
185 except Exception as e:
212 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
186 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
213
187
214 # save .py script as well
188 # save .py script as well
215 if self.save_script:
189 if self.save_script:
216 pypath = os.path.splitext(path)[0] + '.py'
190 pypath = os.path.splitext(path)[0] + '.py'
217 self.log.debug("Writing script %s", pypath)
191 self.log.debug("Writing script %s", pypath)
218 try:
192 try:
219 with io.open(pypath,'w', encoding='utf-8') as f:
193 with io.open(pypath,'w', encoding='utf-8') as f:
220 current.write(nb, f, u'py')
194 current.write(nb, f, u'py')
221 except Exception as e:
195 except Exception as e:
222 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
196 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
223
197
224 if old_name != None:
198 if old_name != None:
225 # remove old files if the name changed
199 # remove old files if the name changed
226 if old_name != new_name:
200 if old_name != new_name:
227 # remove renamed original, if it exists
201 # remove renamed original, if it exists
228 old_path = self.get_os_path(old_name, notebook_path)
202 old_path = self.get_os_path(old_name, notebook_path)
229 if os.path.isfile(old_path):
203 if os.path.isfile(old_path):
230 self.log.debug("unlinking notebook %s", old_path)
204 self.log.debug("unlinking notebook %s", old_path)
231 os.unlink(old_path)
205 os.unlink(old_path)
232
206
233 # cleanup old script, if it exists
207 # cleanup old script, if it exists
234 if self.save_script:
208 if self.save_script:
235 old_pypath = os.path.splitext(old_path)[0] + '.py'
209 old_pypath = os.path.splitext(old_path)[0] + '.py'
236 if os.path.isfile(old_pypath):
210 if os.path.isfile(old_pypath):
237 self.log.debug("unlinking script %s", old_pypath)
211 self.log.debug("unlinking script %s", old_pypath)
238 os.unlink(old_pypath)
212 os.unlink(old_pypath)
239
213
240 # rename checkpoints to follow file
214 # rename checkpoints to follow file
241 for cp in old_checkpoints:
215 for cp in old_checkpoints:
242 checkpoint_id = cp['checkpoint_id']
216 checkpoint_id = cp['checkpoint_id']
243 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
217 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
244 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
218 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
245 if os.path.isfile(old_cp_path):
219 if os.path.isfile(old_cp_path):
246 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
220 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
247 os.rename(old_cp_path, new_cp_path)
221 os.rename(old_cp_path, new_cp_path)
248
222
249 return new_name
223 return new_name
250
224
251 def delete_notebook(self, notebook_name, notebook_path):
225 def delete_notebook(self, notebook_name, notebook_path):
252 """Delete notebook by notebook_name."""
226 """Delete notebook by notebook_name."""
253 nb_path = self.get_os_path(notebook_name, notebook_path)
227 nb_path = self.get_os_path(notebook_name, notebook_path)
254 if not os.path.isfile(nb_path):
228 if not os.path.isfile(nb_path):
255 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
256
230
257 # clear checkpoints
231 # clear checkpoints
258 for checkpoint in self.list_checkpoints(notebook_name):
232 for checkpoint in self.list_checkpoints(notebook_name):
259 checkpoint_id = checkpoint['checkpoint_id']
233 checkpoint_id = checkpoint['checkpoint_id']
260 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
234 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
261 self.log.debug(path)
235 self.log.debug(path)
262 if os.path.isfile(path):
236 if os.path.isfile(path):
263 self.log.debug("unlinking checkpoint %s", path)
237 self.log.debug("unlinking checkpoint %s", path)
264 os.unlink(path)
238 os.unlink(path)
265
239
266 self.log.debug("unlinking notebook %s", nb_path)
240 self.log.debug("unlinking notebook %s", nb_path)
267 os.unlink(nb_path)
241 os.unlink(nb_path)
268
242
269 def increment_filename(self, basename, notebook_path='/'):
243 def increment_filename(self, basename, notebook_path='/'):
270 """Return a non-used filename of the form basename<int>.
244 """Return a non-used filename of the form basename<int>.
271
245
272 This searches through the filenames (basename0, basename1, ...)
246 This searches through the filenames (basename0, basename1, ...)
273 until is find one that is not already being used. It is used to
247 until is find one that is not already being used. It is used to
274 create Untitled and Copy names that are unique.
248 create Untitled and Copy names that are unique.
275 """
249 """
276 i = 0
250 i = 0
277 while True:
251 while True:
278 name = u'%s%i.ipynb' % (basename,i)
252 name = u'%s%i.ipynb' % (basename,i)
279 path = self.get_os_path(name, notebook_path)
253 path = self.get_os_path(name, notebook_path)
280 if not os.path.isfile(path):
254 if not os.path.isfile(path):
281 break
255 break
282 else:
256 else:
283 i = i+1
257 i = i+1
284 return name
258 return name
285
259
286 # Checkpoint-related utilities
260 # Checkpoint-related utilities
287
261
288 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
262 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
289 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
263 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
290 filename = u"{name}-{checkpoint_id}{ext}".format(
264 filename = u"{name}-{checkpoint_id}{ext}".format(
291 name=name,
265 name=name,
292 checkpoint_id=checkpoint_id,
266 checkpoint_id=checkpoint_id,
293 ext=self.filename_ext,
267 ext=self.filename_ext,
294 )
268 )
295 if notebook_path ==None:
269 if notebook_path ==None:
296 path = os.path.join(self.checkpoint_dir, filename)
270 path = os.path.join(self.checkpoint_dir, filename)
297 else:
271 else:
298 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
272 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
299 return path
273 return path
300
274
301 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
275 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
302 """find the path to a checkpoint"""
276 """find the path to a checkpoint"""
303 name = notebook_name
277 name = notebook_name
304 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
278 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
305
279
306 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
280 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
307 """construct the info dict for a given checkpoint"""
281 """construct the info dict for a given checkpoint"""
308 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
282 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
309 stats = os.stat(path)
283 stats = os.stat(path)
310 last_modified = tz.utcfromtimestamp(stats.st_mtime)
284 last_modified = tz.utcfromtimestamp(stats.st_mtime)
311 info = dict(
285 info = dict(
312 checkpoint_id = checkpoint_id,
286 checkpoint_id = checkpoint_id,
313 last_modified = last_modified,
287 last_modified = last_modified,
314 )
288 )
315
289
316 return info
290 return info
317
291
318 # public checkpoint API
292 # public checkpoint API
319
293
320 def create_checkpoint(self, notebook_name, notebook_path='/'):
294 def create_checkpoint(self, notebook_name, notebook_path='/'):
321 """Create a checkpoint from the current state of a notebook"""
295 """Create a checkpoint from the current state of a notebook"""
322 nb_path = self.get_os_path(notebook_name, notebook_path)
296 nb_path = self.get_os_path(notebook_name, notebook_path)
323 # only the one checkpoint ID:
297 # only the one checkpoint ID:
324 checkpoint_id = u"checkpoint"
298 checkpoint_id = u"checkpoint"
325 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
299 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
326 self.log.debug("creating checkpoint for notebook %s", notebook_name)
300 self.log.debug("creating checkpoint for notebook %s", notebook_name)
327 if not os.path.exists(self.checkpoint_dir):
301 if not os.path.exists(self.checkpoint_dir):
328 os.mkdir(self.checkpoint_dir)
302 os.mkdir(self.checkpoint_dir)
329 shutil.copy2(nb_path, cp_path)
303 shutil.copy2(nb_path, cp_path)
330
304
331 # return the checkpoint info
305 # return the checkpoint info
332 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
306 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
333
307
334 def list_checkpoints(self, notebook_name, notebook_path='/'):
308 def list_checkpoints(self, notebook_name, notebook_path='/'):
335 """list the checkpoints for a given notebook
309 """list the checkpoints for a given notebook
336
310
337 This notebook manager currently only supports one checkpoint per notebook.
311 This notebook manager currently only supports one checkpoint per notebook.
338 """
312 """
339 checkpoint_id = "checkpoint"
313 checkpoint_id = "checkpoint"
340 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
314 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
341 if not os.path.exists(path):
315 if not os.path.exists(path):
342 return []
316 return []
343 else:
317 else:
344 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
318 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
345
319
346
320
347 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
321 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
348 """restore a notebook to a checkpointed state"""
322 """restore a notebook to a checkpointed state"""
349 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
323 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
350 nb_path = self.get_os_path(notebook_name, notebook_path)
324 nb_path = self.get_os_path(notebook_name, notebook_path)
351 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
325 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
352 if not os.path.isfile(cp_path):
326 if not os.path.isfile(cp_path):
353 self.log.debug("checkpoint file does not exist: %s", cp_path)
327 self.log.debug("checkpoint file does not exist: %s", cp_path)
354 raise web.HTTPError(404,
328 raise web.HTTPError(404,
355 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
329 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
356 )
330 )
357 # ensure notebook is readable (never restore from an unreadable notebook)
331 # ensure notebook is readable (never restore from an unreadable notebook)
358 last_modified, nb = self.read_notebook_object_from_path(cp_path)
332 last_modified, nb = self.read_notebook_object_from_path(cp_path)
359 shutil.copy2(cp_path, nb_path)
333 shutil.copy2(cp_path, nb_path)
360 self.log.debug("copying %s -> %s", cp_path, nb_path)
334 self.log.debug("copying %s -> %s", cp_path, nb_path)
361
335
362 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
336 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
363 """delete a notebook's checkpoint"""
337 """delete a notebook's checkpoint"""
364 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
338 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
365 if not os.path.isfile(path):
339 if not os.path.isfile(path):
366 raise web.HTTPError(404,
340 raise web.HTTPError(404,
367 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
368 )
342 )
369 self.log.debug("unlinking %s", path)
343 self.log.debug("unlinking %s", path)
370 os.unlink(path)
344 os.unlink(path)
371
345
372 def info_string(self):
346 def info_string(self):
373 return "Serving notebooks from local directory: %s" % self.notebook_dir
347 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,226 +1,216 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the notebooks web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 from tornado import web
19 from tornado import web
20
20
21 from zmq.utils import jsonapi
21 from zmq.utils import jsonapi
22
22
23 from IPython.utils.jsonutil import date_default
23 from IPython.utils.jsonutil import date_default
24
24
25 from ...base.handlers import IPythonHandler
25 from ...base.handlers import IPythonHandler
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Notebook web service handlers
28 # Notebook web service handlers
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31
31
32 class NotebookRootHandler(IPythonHandler):
32 class NotebookRootHandler(IPythonHandler):
33
33
34 @web.authenticated
34 @web.authenticated
35 def get(self):
35 def get(self):
36 """get returns a list of notebooks from the location
36 """get returns a list of notebooks from the location
37 where the server was started."""
37 where the server was started."""
38 nbm = self.notebook_manager
38 nbm = self.notebook_manager
39 notebooks = nbm.list_notebooks("")
39 notebooks = nbm.list_notebooks("/")
40 self.finish(jsonapi.dumps(notebooks))
40 self.finish(jsonapi.dumps(notebooks))
41
41
42 @web.authenticated
42 @web.authenticated
43 def post(self):
43 def post(self):
44 """post creates a notebooks in the directory where the
44 """post creates a notebooks in the directory where the
45 server was started"""
45 server was started"""
46 nbm = self.notebook_manager
46 nbm = self.notebook_manager
47 body = self.request.body.strip()
47 body = self.request.body.strip()
48 format = self.get_argument('format', default='json')
48 format = self.get_argument('format', default='json')
49 name = self.get_argument('name', default=None)
49 name = self.get_argument('name', default=None)
50 if body:
50 if body:
51 fname = nbm.save_new_notebook(body, notebook_path='/', name=name, format=format)
51 fname = nbm.save_new_notebook(body, notebook_path='/', name=name, format=format)
52 else:
52 else:
53 fname = nbm.new_notebook(notebook_path='/')
53 fname = nbm.new_notebook(notebook_path='/')
54 self.set_header('Location', nbm.notebook_dir + fname)
54 self.set_header('Location', nbm.notebook_dir + fname)
55 model = nbm.notebook_model(fname)
55 model = nbm.notebook_model(fname)
56 self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, notebook_name))
56 self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, fname))
57 self.finish(jsonapi.dumps(model))
57 self.finish(jsonapi.dumps(model))
58
58
59
60 class NotebookRootRedirect(IPythonHandler):
61
62 @web.authenticated
63 def get(self):
64 """get redirects to not include trailing backslash"""
65 self.redirect("/api/notebooks")
66
67
68 class NotebookHandler(IPythonHandler):
59 class NotebookHandler(IPythonHandler):
69
60
70 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST','DELETE')
61 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST','DELETE')
71
62
72 @web.authenticated
63 @web.authenticated
73 def get(self, notebook_path):
64 def get(self, notebook_path):
74 """get checks if a notebook is not named, an returns a list of notebooks
65 """get checks if a notebook is not named, an returns a list of notebooks
75 in the notebook path given. If a name is given, return
66 in the notebook path given. If a name is given, return
76 the notebook representation"""
67 the notebook representation"""
77 nbm = self.notebook_manager
68 nbm = self.notebook_manager
78 name, path = nbm.named_notebook_path(notebook_path)
69 name, path = nbm.named_notebook_path(notebook_path)
79
70
80 # Check to see if a notebook name was given
71 # Check to see if a notebook name was given
81 if name is None:
72 if name is None:
82 # List notebooks in 'notebook_path'
73 # List notebooks in 'notebook_path'
83 notebooks = nbm.list_notebooks(path)
74 notebooks = nbm.list_notebooks(path)
84 self.finish(jsonapi.dumps(notebooks))
75 self.finish(jsonapi.dumps(notebooks))
85 else:
76 else:
86 # get and return notebook representation
77 # get and return notebook representation
87 format = self.get_argument('format', default='json')
78 format = self.get_argument('format', default='json')
88 download = self.get_argument('download', default='False')
79 download = self.get_argument('download', default='False')
89 model = nbm.notebook_model(name,path)
80 model = nbm.notebook_model(name, path)
90 last_mod, representation, name = nbm.get_notebook(name, path, format)
81 last_mod, representation, name = nbm.get_notebook(name, path, format)
91 self.set_header('Last-Modified', last_mod)
82 self.set_header('Last-Modified', last_mod)
92
83
93 if download == 'True':
84 if download == 'True':
94 if format == u'json':
85 if format == u'json':
95 self.set_header('Content-Type', 'application/json')
86 self.set_header('Content-Type', 'application/json')
96 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
87 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
97 self.finish(representation)
88 self.finish(representation)
98 elif format == u'py':
89 elif format == u'py':
99 self.set_header('Content-Type', 'application/x-python')
90 self.set_header('Content-Type', 'application/x-python')
100 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
91 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
101 self.finish(representation)
92 self.finish(representation)
102 else:
93 else:
103 self.finish(jsonapi.dumps(model))
94 self.finish(jsonapi.dumps(model))
104
95
105 @web.authenticated
96 @web.authenticated
106 def patch(self, notebook_path):
97 def patch(self, notebook_path):
107 """patch is currently used strictly for notebook renaming.
98 """patch is currently used strictly for notebook renaming.
108 Changes the notebook name to the name given in data."""
99 Changes the notebook name to the name given in data."""
109 nbm = self.notebook_manager
100 nbm = self.notebook_manager
110 name, path = nbm.named_notebook_path(notebook_path)
101 name, path = nbm.named_notebook_path(notebook_path)
111 data = jsonapi.loads(self.request.body)
102 data = jsonapi.loads(self.request.body)
112 model, response = nbm.change_notebook(data, name, path)
103 model = nbm.change_notebook(data, name, path)
113 self.set_status(response)
114 self.finish(jsonapi.dumps(model))
104 self.finish(jsonapi.dumps(model))
115
105
116 @web.authenticated
106 @web.authenticated
117 def post(self,notebook_path):
107 def post(self,notebook_path):
118 """Create a new notebook in the location given by 'notebook_path'."""
108 """Create a new notebook in the location given by 'notebook_path'."""
119 nbm = self.notebook_manager
109 nbm = self.notebook_manager
120 fname, path = nbm.named_notebook_path(notebook_path)
110 fname, path = nbm.named_notebook_path(notebook_path)
121 body = self.request.body.strip()
111 body = self.request.body.strip()
122 format = self.get_argument('format', default='json')
112 format = self.get_argument('format', default='json')
123 name = self.get_argument('name', default=None)
113 name = self.get_argument('name', default=None)
124 if body:
114 if body:
125 fname = nbm.save_new_notebook(body, notebook_path=path, name=name, format=format)
115 fname = nbm.save_new_notebook(body, notebook_path=path, name=name, format=format)
126 else:
116 else:
127 fname = nbm.new_notebook(notebook_path=path)
117 fname = nbm.new_notebook(notebook_path=path)
128 self.set_header('Location', nbm.notebook_dir + path + fname)
118 self.set_header('Location', nbm.notebook_dir + path + fname)
129 model = nbm.notebook_model(fname, path)
119 model = nbm.notebook_model(fname, path)
130 self.finish(jsonapi.dumps(model))
120 self.finish(jsonapi.dumps(model))
131
121
132 @web.authenticated
122 @web.authenticated
133 def put(self, notebook_path):
123 def put(self, notebook_path):
134 """saves the notebook in the location given by 'notebook_path'."""
124 """saves the notebook in the location given by 'notebook_path'."""
135 nbm = self.notebook_manager
125 nbm = self.notebook_manager
136 name, path = nbm.named_notebook_path(notebook_path)
126 name, path = nbm.named_notebook_path(notebook_path)
137 format = self.get_argument('format', default='json')
127 format = self.get_argument('format', default='json')
138 nbm.save_notebook(self.request.body, notebook_path=path, name=name, format=format)
128 nbm.save_notebook(self.request.body, notebook_path=path, name=name, format=format)
139 model = nbm.notebook_model(name, path)
129 model = nbm.notebook_model(name, path)
140 self.set_status(204)
130 self.set_status(204)
141 self.finish(jsonapi.dumps(model))
131 self.finish(jsonapi.dumps(model))
142
132
143 @web.authenticated
133 @web.authenticated
144 def delete(self, notebook_path):
134 def delete(self, notebook_path):
145 """delete rmoves the notebook in the given notebook path"""
135 """delete rmoves the notebook in the given notebook path"""
146 nbm = self.notebook_manager
136 nbm = self.notebook_manager
147 name, path = nbm.named_notebook_path(notebook_path)
137 name, path = nbm.named_notebook_path(notebook_path)
148 nbm.delete_notebook(name, path)
138 nbm.delete_notebook(name, path)
149 self.set_status(204)
139 self.set_status(204)
150 self.finish()
140 self.finish()
151
141
152
142
153 class NotebookCheckpointsHandler(IPythonHandler):
143 class NotebookCheckpointsHandler(IPythonHandler):
154
144
155 SUPPORTED_METHODS = ('GET', 'POST')
145 SUPPORTED_METHODS = ('GET', 'POST')
156
146
157 @web.authenticated
147 @web.authenticated
158 def get(self, notebook_path):
148 def get(self, notebook_path):
159 """get lists checkpoints for a notebook"""
149 """get lists checkpoints for a notebook"""
160 nbm = self.notebook_manager
150 nbm = self.notebook_manager
161 name, path = nbm.named_notebook_path(notebook_path)
151 name, path = nbm.named_notebook_path(notebook_path)
162 checkpoints = nbm.list_checkpoints(name, path)
152 checkpoints = nbm.list_checkpoints(name, path)
163 data = jsonapi.dumps(checkpoints, default=date_default)
153 data = jsonapi.dumps(checkpoints, default=date_default)
164 self.finish(data)
154 self.finish(data)
165
155
166 @web.authenticated
156 @web.authenticated
167 def post(self, notebook_path):
157 def post(self, notebook_path):
168 """post creates a new checkpoint"""
158 """post creates a new checkpoint"""
169 nbm = self.notebook_manager
159 nbm = self.notebook_manager
170 name, path = nbm.named_notebook_path(notebook_path)
160 name, path = nbm.named_notebook_path(notebook_path)
171 checkpoint = nbm.create_checkpoint(name, path)
161 checkpoint = nbm.create_checkpoint(name, path)
172 data = jsonapi.dumps(checkpoint, default=date_default)
162 data = jsonapi.dumps(checkpoint, default=date_default)
173 if path == None:
163 if path == None:
174 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
164 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
175 self.base_project_url, name, checkpoint['checkpoint_id']
165 self.base_project_url, name, checkpoint['checkpoint_id']
176 ))
166 ))
177 else:
167 else:
178 self.set_header('Location', '{0}notebooks/{1}/{2}/checkpoints/{3}'.format(
168 self.set_header('Location', '{0}notebooks/{1}/{2}/checkpoints/{3}'.format(
179 self.base_project_url, path, name, checkpoint['checkpoint_id']
169 self.base_project_url, path, name, checkpoint['checkpoint_id']
180 ))
170 ))
181 self.finish(data)
171 self.finish(data)
182
172
183
173
184 class ModifyNotebookCheckpointsHandler(IPythonHandler):
174 class ModifyNotebookCheckpointsHandler(IPythonHandler):
185
175
186 SUPPORTED_METHODS = ('POST', 'DELETE')
176 SUPPORTED_METHODS = ('POST', 'DELETE')
187
177
188 @web.authenticated
178 @web.authenticated
189 def post(self, notebook_path, checkpoint_id):
179 def post(self, notebook_path, checkpoint_id):
190 """post restores a notebook from a checkpoint"""
180 """post restores a notebook from a checkpoint"""
191 nbm = self.notebook_manager
181 nbm = self.notebook_manager
192 name, path = nbm.named_notebook_path(notebook_path)
182 name, path = nbm.named_notebook_path(notebook_path)
193 nbm.restore_checkpoint(name, checkpoint_id, path)
183 nbm.restore_checkpoint(name, checkpoint_id, path)
194 self.set_status(204)
184 self.set_status(204)
195 self.finish()
185 self.finish()
196
186
197 @web.authenticated
187 @web.authenticated
198 def delete(self, notebook_path, checkpoint_id):
188 def delete(self, notebook_path, checkpoint_id):
199 """delete clears a checkpoint for a given notebook"""
189 """delete clears a checkpoint for a given notebook"""
200 nbm = self.notebook_manager
190 nbm = self.notebook_manager
201 name, path = nbm.named_notebook_path(notebook_path)
191 name, path = nbm.named_notebook_path(notebook_path)
202 nbm.delete_checkpoint(name, checkpoint_id, path)
192 nbm.delete_checkpoint(name, checkpoint_id, path)
203 self.set_status(204)
193 self.set_status(204)
204 self.finish()
194 self.finish()
205
195
206 #-----------------------------------------------------------------------------
196 #-----------------------------------------------------------------------------
207 # URL to handler mappings
197 # URL to handler mappings
208 #-----------------------------------------------------------------------------
198 #-----------------------------------------------------------------------------
209
199
210
200
211 _notebook_path_regex = r"(?P<notebook_path>.+)"
201 _notebook_path_regex = r"(?P<notebook_path>.+)"
212 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
202 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
213
203
214 default_handlers = [
204 default_handlers = [
215 (r"api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
205 (r"api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
216 (r"api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
206 (r"api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
217 ModifyNotebookCheckpointsHandler),
207 ModifyNotebookCheckpointsHandler),
218 (r"api/notebooks/%s/" % _notebook_path_regex, NotebookHandler),
208 (r"api/notebooks/%s/" % _notebook_path_regex, NotebookHandler),
219 (r"api/notebooks/%s" % _notebook_path_regex, NotebookHandler),
209 (r"api/notebooks/%s" % _notebook_path_regex, NotebookHandler),
220 (r"api/notebooks/", NotebookRootRedirect),
210 (r"api/notebooks/", NotebookRootHandler),
221 (r"api/notebooks", NotebookRootHandler),
211 (r"api/notebooks", NotebookRootHandler),
222 ]
212 ]
223
213
224
214
225
215
226
216
@@ -1,263 +1,289 b''
1 """A base class notebook manager.
1 """A base class notebook manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 import uuid
20 import uuid
21
21
22 from tornado import web
22 from tornado import web
23 from urllib import quote, unquote
23 from urllib import quote, unquote
24
24
25 from IPython.config.configurable import LoggingConfigurable
25 from IPython.config.configurable import LoggingConfigurable
26 from IPython.nbformat import current
26 from IPython.nbformat import current
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Classes
30 # Classes
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 class NotebookManager(LoggingConfigurable):
33 class NotebookManager(LoggingConfigurable):
34
34
35 # Todo:
35 # Todo:
36 # The notebook_dir attribute is used to mean a couple of different things:
36 # The notebook_dir attribute is used to mean a couple of different things:
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
38 # 2. The cwd of the kernel for a project.
38 # 2. The cwd of the kernel for a project.
39 # Right now we use this attribute in a number of different places and
39 # Right now we use this attribute in a number of different places and
40 # we are going to have to disentangle all of this.
40 # we are going to have to disentangle all of this.
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
42 The directory to use for notebooks.
42 The directory to use for notebooks.
43 """)
43 """)
44
44
45 def named_notebook_path(self, notebook_path):
45 def named_notebook_path(self, notebook_path):
46 """Given a notebook_path name, returns a (name, path) tuple, where
46 """Given a notebook_path name, returns a (name, path) tuple, where
47 name is a .ipynb file, and path is the directory for the file, which
47 name is a .ipynb file, and path is the directory for the file, which
48 *always* starts *and* ends with a '/' character.
48 *always* starts *and* ends with a '/' character.
49
49
50 Parameters
50 Parameters
51 ----------
51 ----------
52 notebook_path : string
52 notebook_path : string
53 A path that may be a .ipynb name or a directory
53 A path that may be a .ipynb name or a directory
54
54
55 Returns
55 Returns
56 -------
56 -------
57 name : string or None
57 name : string or None
58 the filename of the notebook, or None if not a .ipynb extension
58 the filename of the notebook, or None if not a .ipynb extension
59 path : string
59 path : string
60 the path to the directory which contains the notebook
60 the path to the directory which contains the notebook
61 """
61 """
62 names = notebook_path.split('/')
62 names = notebook_path.split('/')
63 names = [n for n in names if n != ''] # remove duplicate splits
63 names = [n for n in names if n != ''] # remove duplicate splits
64
64
65 names = [''] + names
65 names = [''] + names
66
66
67 if names and names[-1].endswith(".ipynb"):
67 if names and names[-1].endswith(".ipynb"):
68 name = names[-1]
68 name = names[-1]
69 path = "/".join(names[:-1]) + '/'
69 path = "/".join(names[:-1]) + '/'
70 else:
70 else:
71 name = None
71 name = None
72 path = "/".join(names) + '/'
72 path = "/".join(names) + '/'
73 return name, path
73 return name, path
74
74
75 def get_os_path(self, fname=None, path='/'):
76 """Given a notebook name and a server URL path, return its file system
77 path.
78
79 Parameters
80 ----------
81 fname : string
82 The name of a notebook file with the .ipynb extension
83 path : string
84 The relative URL path (with '/' as separator) to the named
85 notebook.
86
87 Returns
88 -------
89 path : string
90 A file system path that combines notebook_dir (location where
91 server started), the relative path, and the filename with the
92 current operating system's url.
93 """
94 parts = path.split('/')
95 parts = [p for p in parts if p != ''] # remove duplicate splits
96 if fname is not None:
97 parts += [fname]
98 path = os.path.join(self.notebook_dir, *parts)
99 return path
100
75 def url_encode(self, path):
101 def url_encode(self, path):
76 """Returns the path with all special characters URL encoded"""
102 """Returns the path with all special characters URL encoded"""
77 parts = os.path.split(path)
103 parts = os.path.split(path)
78 return os.path.join(*[quote(p) for p in parts])
104 return os.path.join(*[quote(p) for p in parts])
79
105
80 def url_decode(self, path):
106 def url_decode(self, path):
81 """Returns the URL with special characters decoded"""
107 """Returns the URL with special characters decoded"""
82 parts = os.path.split(path)
108 parts = os.path.split(path)
83 return os.path.join(*[unquote(p) for p in parts])
109 return os.path.join(*[unquote(p) for p in parts])
84
110
85 def _notebook_dir_changed(self, new):
111 def _notebook_dir_changed(self, new):
86 """do a bit of validation of the notebook dir"""
112 """do a bit of validation of the notebook dir"""
87 if not os.path.isabs(new):
113 if not os.path.isabs(new):
88 # If we receive a non-absolute path, make it absolute.
114 # If we receive a non-absolute path, make it absolute.
89 abs_new = os.path.abspath(new)
115 abs_new = os.path.abspath(new)
90 #self.notebook_dir = os.path.dirname(abs_new)
116 #self.notebook_dir = os.path.dirname(abs_new)
91 return
117 return
92 if os.path.exists(new) and not os.path.isdir(new):
118 if os.path.exists(new) and not os.path.isdir(new):
93 raise TraitError("notebook dir %r is not a directory" % new)
119 raise TraitError("notebook dir %r is not a directory" % new)
94 if not os.path.exists(new):
120 if not os.path.exists(new):
95 self.log.info("Creating notebook dir %s", new)
121 self.log.info("Creating notebook dir %s", new)
96 try:
122 try:
97 os.mkdir(new)
123 os.mkdir(new)
98 except:
124 except:
99 raise TraitError("Couldn't create notebook dir %r" % new)
125 raise TraitError("Couldn't create notebook dir %r" % new)
100
126
101 allowed_formats = List([u'json',u'py'])
127 allowed_formats = List([u'json',u'py'])
102
128
103 def add_new_folder(self, path=None):
129 def add_new_folder(self, path=None):
104 new_path = os.path.join(self.notebook_dir, path)
130 new_path = os.path.join(self.notebook_dir, path)
105 if not os.path.exists(new_path):
131 if not os.path.exists(new_path):
106 os.makedirs(new_path)
132 os.makedirs(new_path)
107 else:
133 else:
108 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
134 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
109
135
110 def load_notebook_names(self, path):
136 def load_notebook_names(self, path):
111 """Load the notebook names into memory.
137 """Load the notebook names into memory.
112
138
113 This should be called once immediately after the notebook manager
139 This should be called once immediately after the notebook manager
114 is created to load the existing notebooks into the mapping in
140 is created to load the existing notebooks into the mapping in
115 memory.
141 memory.
116 """
142 """
117 self.list_notebooks(path)
143 self.list_notebooks(path)
118
144
119 def list_notebooks(self):
145 def list_notebooks(self):
120 """List all notebooks.
146 """List all notebooks.
121
147
122 This returns a list of dicts, each of the form::
148 This returns a list of dicts, each of the form::
123
149
124 dict(notebook_id=notebook,name=name)
150 dict(notebook_id=notebook,name=name)
125
151
126 This list of dicts should be sorted by name::
152 This list of dicts should be sorted by name::
127
153
128 data = sorted(data, key=lambda item: item['name'])
154 data = sorted(data, key=lambda item: item['name'])
129 """
155 """
130 raise NotImplementedError('must be implemented in a subclass')
156 raise NotImplementedError('must be implemented in a subclass')
131
157
132 def notebook_model(self, notebook_name, notebook_path='/', content=True):
158 def notebook_model(self, notebook_name, notebook_path='/', content=True):
133 """ Creates the standard notebook model """
159 """ Creates the standard notebook model """
134 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
160 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
135 model = {"name": notebook_name,
161 model = {"name": notebook_name,
136 "path": notebook_path,
162 "path": notebook_path,
137 "last_modified (UTC)": last_modified.ctime()}
163 "last_modified (UTC)": last_modified.ctime()}
138 if content == True:
164 if content is True:
139 model['content'] = contents
165 model['content'] = contents
140 return model
166 return model
141
167
142 def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
168 def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
143 """Get the representation of a notebook in format by notebook_name."""
169 """Get the representation of a notebook in format by notebook_name."""
144 format = unicode(format)
170 format = unicode(format)
145 if format not in self.allowed_formats:
171 if format not in self.allowed_formats:
146 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
172 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
147 kwargs = {}
173 kwargs = {}
148 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
174 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
149 if format == 'json':
175 if format == 'json':
150 # don't split lines for sending over the wire, because it
176 # don't split lines for sending over the wire, because it
151 # should match the Python in-memory format.
177 # should match the Python in-memory format.
152 kwargs['split_lines'] = False
178 kwargs['split_lines'] = False
153 representation = current.writes(nb, format, **kwargs)
179 representation = current.writes(nb, format, **kwargs)
154 name = nb.metadata.get('name', 'notebook')
180 name = nb.metadata.get('name', 'notebook')
155 return last_mod, representation, name
181 return last_mod, representation, name
156
182
157 def read_notebook_object(self, notebook_name, notebook_path='/'):
183 def read_notebook_object(self, notebook_name, notebook_path='/'):
158 """Get the object representation of a notebook by notebook_id."""
184 """Get the object representation of a notebook by notebook_id."""
159 raise NotImplementedError('must be implemented in a subclass')
185 raise NotImplementedError('must be implemented in a subclass')
160
186
161 def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
187 def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
162 """Save a new notebook and return its name.
188 """Save a new notebook and return its name.
163
189
164 If a name is passed in, it overrides any values in the notebook data
190 If a name is passed in, it overrides any values in the notebook data
165 and the value in the data is updated to use that value.
191 and the value in the data is updated to use that value.
166 """
192 """
167 if format not in self.allowed_formats:
193 if format not in self.allowed_formats:
168 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
194 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
169
195
170 try:
196 try:
171 nb = current.reads(data.decode('utf-8'), format)
197 nb = current.reads(data.decode('utf-8'), format)
172 except:
198 except:
173 raise web.HTTPError(400, u'Invalid JSON data')
199 raise web.HTTPError(400, u'Invalid JSON data')
174
200
175 if name is None:
201 if name is None:
176 try:
202 try:
177 name = nb.metadata.name
203 name = nb.metadata.name
178 except AttributeError:
204 except AttributeError:
179 raise web.HTTPError(400, u'Missing notebook name')
205 raise web.HTTPError(400, u'Missing notebook name')
180 nb.metadata.name = name
206 nb.metadata.name = name
181
207
182 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
208 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
183 return notebook_name
209 return notebook_name
184
210
185 def save_notebook(self, data, notebook_path='/', name=None, new_name=None, format=u'json'):
211 def save_notebook(self, data, notebook_path='/', name=None, new_name=None, format=u'json'):
186 """Save an existing notebook by notebook_name."""
212 """Save an existing notebook by notebook_name."""
187 if format not in self.allowed_formats:
213 if format not in self.allowed_formats:
188 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
214 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
189
215
190 try:
216 try:
191 nb = current.reads(data.decode('utf-8'), format)
217 nb = current.reads(data.decode('utf-8'), format)
192 except:
218 except:
193 raise web.HTTPError(400, u'Invalid JSON data')
219 raise web.HTTPError(400, u'Invalid JSON data')
194
220
195 if name is not None:
221 if name is not None:
196 nb.metadata.name = name
222 nb.metadata.name = name
197 self.write_notebook_object(nb, name, notebook_path, new_name)
223 self.write_notebook_object(nb, name, notebook_path, new_name)
198
224
199 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name=None):
225 def write_notebook_object(self, nb, notebook_name='/', notebook_path='/', new_name=None):
200 """Write a notebook object and return its notebook_name.
226 """Write a notebook object and return its notebook_name.
201
227
202 If notebook_name is None, this method should create a new notebook_name.
228 If notebook_name is None, this method should create a new notebook_name.
203 If notebook_name is not None, this method should check to make sure it
229 If notebook_name is not None, this method should check to make sure it
204 exists and is valid.
230 exists and is valid.
205 """
231 """
206 raise NotImplementedError('must be implemented in a subclass')
232 raise NotImplementedError('must be implemented in a subclass')
207
233
208 def delete_notebook(self, notebook_name, notebook_path):
234 def delete_notebook(self, notebook_name, notebook_path):
209 """Delete notebook by notebook_id."""
235 """Delete notebook by notebook_id."""
210 raise NotImplementedError('must be implemented in a subclass')
236 raise NotImplementedError('must be implemented in a subclass')
211
237
212 def increment_filename(self, name):
238 def increment_filename(self, name):
213 """Increment a filename to make it unique.
239 """Increment a filename to make it unique.
214
240
215 This exists for notebook stores that must have unique names. When a notebook
241 This exists for notebook stores that must have unique names. When a notebook
216 is created or copied this method constructs a unique filename, typically
242 is created or copied this method constructs a unique filename, typically
217 by appending an integer to the name.
243 by appending an integer to the name.
218 """
244 """
219 return name
245 return name
220
246
221 def new_notebook(self, notebook_path='/'):
247 def new_notebook(self, notebook_path='/'):
222 """Create a new notebook and return its notebook_name."""
248 """Create a new notebook and return its notebook_name."""
223 name = self.increment_filename('Untitled', notebook_path)
249 name = self.increment_filename('Untitled', notebook_path)
224 metadata = current.new_metadata(name=name)
250 metadata = current.new_metadata(name=name)
225 nb = current.new_notebook(metadata=metadata)
251 nb = current.new_notebook(metadata=metadata)
226 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
252 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
227 return notebook_name
253 return notebook_name
228
254
229 def copy_notebook(self, name, path='/'):
255 def copy_notebook(self, name, path='/'):
230 """Copy an existing notebook and return its new notebook_name."""
256 """Copy an existing notebook and return its new notebook_name."""
231 last_mod, nb = self.read_notebook_object(name, path)
257 last_mod, nb = self.read_notebook_object(name, path)
232 name = nb.metadata.name + '-Copy'
258 name = nb.metadata.name + '-Copy'
233 name = self.increment_filename(name, path)
259 name = self.increment_filename(name, path)
234 nb.metadata.name = name
260 nb.metadata.name = name
235 notebook_name = self.write_notebook_object(nb, notebook_path = path)
261 notebook_name = self.write_notebook_object(nb, notebook_path = path)
236 return notebook_name
262 return notebook_name
237
263
238 # Checkpoint-related
264 # Checkpoint-related
239
265
240 def create_checkpoint(self, notebook_name, notebook_path='/'):
266 def create_checkpoint(self, notebook_name, notebook_path='/'):
241 """Create a checkpoint of the current state of a notebook
267 """Create a checkpoint of the current state of a notebook
242
268
243 Returns a checkpoint_id for the new checkpoint.
269 Returns a checkpoint_id for the new checkpoint.
244 """
270 """
245 raise NotImplementedError("must be implemented in a subclass")
271 raise NotImplementedError("must be implemented in a subclass")
246
272
247 def list_checkpoints(self, notebook_name, notebook_path='/'):
273 def list_checkpoints(self, notebook_name, notebook_path='/'):
248 """Return a list of checkpoints for a given notebook"""
274 """Return a list of checkpoints for a given notebook"""
249 return []
275 return []
250
276
251 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
277 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
252 """Restore a notebook from one of its checkpoints"""
278 """Restore a notebook from one of its checkpoints"""
253 raise NotImplementedError("must be implemented in a subclass")
279 raise NotImplementedError("must be implemented in a subclass")
254
280
255 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
281 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
256 """delete a checkpoint for a notebook"""
282 """delete a checkpoint for a notebook"""
257 raise NotImplementedError("must be implemented in a subclass")
283 raise NotImplementedError("must be implemented in a subclass")
258
284
259 def log_info(self):
285 def log_info(self):
260 self.log.info(self.info_string())
286 self.log.info(self.info_string())
261
287
262 def info_string(self):
288 def info_string(self):
263 return "Serving notebooks"
289 return "Serving notebooks"
@@ -1,40 +1,41 b''
1 """Base class for notebook tests."""
1 """Base class for notebook tests."""
2
2
3 import sys
3 import sys
4 import time
4 import time
5 from subprocess import Popen, PIPE
5 from subprocess import Popen, PIPE
6 from unittest import TestCase
6 from unittest import TestCase
7
7
8 from IPython.utils.tempdir import TemporaryDirectory
8 from IPython.utils.tempdir import TemporaryDirectory
9
9
10
10
11 class NotebookTestBase(TestCase):
11 class NotebookTestBase(TestCase):
12 """A base class for tests that need a running notebook.
12 """A base class for tests that need a running notebook.
13
13
14 This creates an empty profile in a temp ipython_dir
14 This creates an empty profile in a temp ipython_dir
15 and then starts the notebook server with a separate temp notebook_dir.
15 and then starts the notebook server with a separate temp notebook_dir.
16 """
16 """
17
17
18 port = 12342
18 port = 12342
19
19
20 def setUp(self):
20 def setUp(self):
21 self.ipython_dir = TemporaryDirectory()
21 self.ipython_dir = TemporaryDirectory()
22 self.notebook_dir = TemporaryDirectory()
22 self.notebook_dir = TemporaryDirectory()
23 notebook_args = [
23 notebook_args = [
24 sys.executable, '-c',
24 sys.executable, '-c',
25 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
25 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
26 '--port=%d' % self.port,
26 '--port=%d' % self.port,
27 '--no-browser',
27 '--no-browser',
28 '--ipython-dir=%s' % self.ipython_dir.name,
28 '--ipython-dir=%s' % self.ipython_dir.name,
29 '--notebook-dir=%s' % self.notebook_dir.name
29 '--notebook-dir=%s' % self.notebook_dir.name
30 ]
30 ]
31 #self.notebook = Popen(notebook_args)
31 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
32 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
32 time.sleep(3.0)
33 time.sleep(3.0)
33
34
34 def tearDown(self):
35 def tearDown(self):
35 self.notebook.terminate()
36 self.notebook.terminate()
36 self.ipython_dir.cleanup()
37 self.ipython_dir.cleanup()
37 self.notebook_dir.cleanup()
38 self.notebook_dir.cleanup()
38
39
39 def base_url(self):
40 def base_url(self):
40 return 'http://localhost:%i/' % self.port
41 return 'http://localhost:%i/' % self.port
General Comments 0
You need to be logged in to leave comments. Login now