##// END OF EJS Templates
include error in 'Unexpected error' message.
MinRK -
Show More
@@ -1,255 +1,255 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) 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 import datetime
19 import datetime
20 import os
20 import os
21 import uuid
21 import uuid
22 import glob
22 import glob
23
23
24 from tornado import web
24 from tornado import web
25
25
26 from IPython.config.configurable import LoggingConfigurable
26 from IPython.config.configurable import LoggingConfigurable
27 from IPython.nbformat import current
27 from IPython.nbformat import current
28 from IPython.utils.traitlets import Unicode, List, Dict, Bool
28 from IPython.utils.traitlets import Unicode, List, Dict, Bool
29
29
30
30
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32 # Code
32 # Code
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34
34
35
35
36 class NotebookManager(LoggingConfigurable):
36 class NotebookManager(LoggingConfigurable):
37
37
38 notebook_dir = Unicode(os.getcwd(), config=True, help="""
38 notebook_dir = Unicode(os.getcwd(), config=True, help="""
39 The directory to use for notebooks.
39 The directory to use for notebooks.
40 """)
40 """)
41
41
42 save_script = Bool(False, config=True,
42 save_script = Bool(False, config=True,
43 help="""Also save notebooks as a Python script.
43 help="""Also save notebooks as a Python script.
44
44
45 For easier use of import/%loadpy across notebooks, a <notebook-name>.py
45 For easier use of import/%loadpy across notebooks, a <notebook-name>.py
46 script will be created next to any <notebook-name>.ipynb on each save.
46 script will be created next to any <notebook-name>.ipynb on each save.
47 """
47 """
48 )
48 )
49
49
50 filename_ext = Unicode(u'.ipynb')
50 filename_ext = Unicode(u'.ipynb')
51 allowed_formats = List([u'json',u'py'])
51 allowed_formats = List([u'json',u'py'])
52
52
53 # Map notebook_ids to notebook names
53 # Map notebook_ids to notebook names
54 mapping = Dict()
54 mapping = Dict()
55 # Map notebook names to notebook_ids
55 # Map notebook names to notebook_ids
56 rev_mapping = Dict()
56 rev_mapping = Dict()
57
57
58 def list_notebooks(self):
58 def list_notebooks(self):
59 """List all notebooks in the notebook dir.
59 """List all notebooks in the notebook dir.
60
60
61 This returns a list of dicts of the form::
61 This returns a list of dicts of the form::
62
62
63 dict(notebook_id=notebook,name=name)
63 dict(notebook_id=notebook,name=name)
64 """
64 """
65 names = glob.glob(os.path.join(self.notebook_dir,
65 names = glob.glob(os.path.join(self.notebook_dir,
66 '*' + self.filename_ext))
66 '*' + self.filename_ext))
67 names = [os.path.splitext(os.path.basename(name))[0]
67 names = [os.path.splitext(os.path.basename(name))[0]
68 for name in names]
68 for name in names]
69
69
70 data = []
70 data = []
71 for name in names:
71 for name in names:
72 if name not in self.rev_mapping:
72 if name not in self.rev_mapping:
73 notebook_id = self.new_notebook_id(name)
73 notebook_id = self.new_notebook_id(name)
74 else:
74 else:
75 notebook_id = self.rev_mapping[name]
75 notebook_id = self.rev_mapping[name]
76 data.append(dict(notebook_id=notebook_id,name=name))
76 data.append(dict(notebook_id=notebook_id,name=name))
77 data = sorted(data, key=lambda item: item['name'])
77 data = sorted(data, key=lambda item: item['name'])
78 return data
78 return data
79
79
80 def new_notebook_id(self, name):
80 def new_notebook_id(self, name):
81 """Generate a new notebook_id for a name and store its mappings."""
81 """Generate a new notebook_id for a name and store its mappings."""
82 # TODO: the following will give stable urls for notebooks, but unless
82 # TODO: the following will give stable urls for notebooks, but unless
83 # the notebooks are immediately redirected to their new urls when their
83 # the notebooks are immediately redirected to their new urls when their
84 # filemname changes, nasty inconsistencies result. So for now it's
84 # filemname changes, nasty inconsistencies result. So for now it's
85 # disabled and instead we use a random uuid4() call. But we leave the
85 # disabled and instead we use a random uuid4() call. But we leave the
86 # logic here so that we can later reactivate it, whhen the necessary
86 # logic here so that we can later reactivate it, whhen the necessary
87 # url redirection code is written.
87 # url redirection code is written.
88 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
88 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
89 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
89 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
90
90
91 notebook_id = unicode(uuid.uuid4())
91 notebook_id = unicode(uuid.uuid4())
92
92
93 self.mapping[notebook_id] = name
93 self.mapping[notebook_id] = name
94 self.rev_mapping[name] = notebook_id
94 self.rev_mapping[name] = notebook_id
95 return notebook_id
95 return notebook_id
96
96
97 def delete_notebook_id(self, notebook_id):
97 def delete_notebook_id(self, notebook_id):
98 """Delete a notebook's id only. This doesn't delete the actual notebook."""
98 """Delete a notebook's id only. This doesn't delete the actual notebook."""
99 name = self.mapping[notebook_id]
99 name = self.mapping[notebook_id]
100 del self.mapping[notebook_id]
100 del self.mapping[notebook_id]
101 del self.rev_mapping[name]
101 del self.rev_mapping[name]
102
102
103 def notebook_exists(self, notebook_id):
103 def notebook_exists(self, notebook_id):
104 """Does a notebook exist?"""
104 """Does a notebook exist?"""
105 if notebook_id not in self.mapping:
105 if notebook_id not in self.mapping:
106 return False
106 return False
107 path = self.get_path_by_name(self.mapping[notebook_id])
107 path = self.get_path_by_name(self.mapping[notebook_id])
108 return os.path.isfile(path)
108 return os.path.isfile(path)
109
109
110 def find_path(self, notebook_id):
110 def find_path(self, notebook_id):
111 """Return a full path to a notebook given its notebook_id."""
111 """Return a full path to a notebook given its notebook_id."""
112 try:
112 try:
113 name = self.mapping[notebook_id]
113 name = self.mapping[notebook_id]
114 except KeyError:
114 except KeyError:
115 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
115 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
116 return self.get_path_by_name(name)
116 return self.get_path_by_name(name)
117
117
118 def get_path_by_name(self, name):
118 def get_path_by_name(self, name):
119 """Return a full path to a notebook given its name."""
119 """Return a full path to a notebook given its name."""
120 filename = name + self.filename_ext
120 filename = name + self.filename_ext
121 path = os.path.join(self.notebook_dir, filename)
121 path = os.path.join(self.notebook_dir, filename)
122 return path
122 return path
123
123
124 def get_notebook(self, notebook_id, format=u'json'):
124 def get_notebook(self, notebook_id, format=u'json'):
125 """Get the representation of a notebook in format by notebook_id."""
125 """Get the representation of a notebook in format by notebook_id."""
126 format = unicode(format)
126 format = unicode(format)
127 if format not in self.allowed_formats:
127 if format not in self.allowed_formats:
128 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
128 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
129 last_modified, nb = self.get_notebook_object(notebook_id)
129 last_modified, nb = self.get_notebook_object(notebook_id)
130 kwargs = {}
130 kwargs = {}
131 if format == 'json':
131 if format == 'json':
132 # don't split lines for sending over the wire, because it
132 # don't split lines for sending over the wire, because it
133 # should match the Python in-memory format.
133 # should match the Python in-memory format.
134 kwargs['split_lines'] = False
134 kwargs['split_lines'] = False
135 data = current.writes(nb, format, **kwargs)
135 data = current.writes(nb, format, **kwargs)
136 name = nb.get('name','notebook')
136 name = nb.get('name','notebook')
137 return last_modified, name, data
137 return last_modified, name, data
138
138
139 def get_notebook_object(self, notebook_id):
139 def get_notebook_object(self, notebook_id):
140 """Get the NotebookNode representation of a notebook by notebook_id."""
140 """Get the NotebookNode representation of a notebook by notebook_id."""
141 path = self.find_path(notebook_id)
141 path = self.find_path(notebook_id)
142 if not os.path.isfile(path):
142 if not os.path.isfile(path):
143 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
143 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
144 info = os.stat(path)
144 info = os.stat(path)
145 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
145 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
146 with open(path,'r') as f:
146 with open(path,'r') as f:
147 s = f.read()
147 s = f.read()
148 try:
148 try:
149 # v1 and v2 and json in the .ipynb files.
149 # v1 and v2 and json in the .ipynb files.
150 nb = current.reads(s, u'json')
150 nb = current.reads(s, u'json')
151 except:
151 except:
152 raise web.HTTPError(500, u'Unreadable JSON notebook.')
152 raise web.HTTPError(500, u'Unreadable JSON notebook.')
153 if 'name' not in nb:
153 if 'name' not in nb:
154 nb.name = os.path.split(path)[-1].split(u'.')[0]
154 nb.name = os.path.split(path)[-1].split(u'.')[0]
155 return last_modified, nb
155 return last_modified, nb
156
156
157 def save_new_notebook(self, data, name=None, format=u'json'):
157 def save_new_notebook(self, data, name=None, format=u'json'):
158 """Save a new notebook and return its notebook_id.
158 """Save a new notebook and return its notebook_id.
159
159
160 If a name is passed in, it overrides any values in the notebook data
160 If a name is passed in, it overrides any values in the notebook data
161 and the value in the data is updated to use that value.
161 and the value in the data is updated to use that value.
162 """
162 """
163 if format not in self.allowed_formats:
163 if format not in self.allowed_formats:
164 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
164 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
165
165
166 try:
166 try:
167 nb = current.reads(data.decode('utf-8'), format)
167 nb = current.reads(data.decode('utf-8'), format)
168 except:
168 except:
169 raise web.HTTPError(400, u'Invalid JSON data')
169 raise web.HTTPError(400, u'Invalid JSON data')
170
170
171 if name is None:
171 if name is None:
172 try:
172 try:
173 name = nb.metadata.name
173 name = nb.metadata.name
174 except AttributeError:
174 except AttributeError:
175 raise web.HTTPError(400, u'Missing notebook name')
175 raise web.HTTPError(400, u'Missing notebook name')
176 nb.metadata.name = name
176 nb.metadata.name = name
177
177
178 notebook_id = self.new_notebook_id(name)
178 notebook_id = self.new_notebook_id(name)
179 self.save_notebook_object(notebook_id, nb)
179 self.save_notebook_object(notebook_id, nb)
180 return notebook_id
180 return notebook_id
181
181
182 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
182 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
183 """Save an existing notebook by notebook_id."""
183 """Save an existing notebook by notebook_id."""
184 if format not in self.allowed_formats:
184 if format not in self.allowed_formats:
185 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
185 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
186
186
187 try:
187 try:
188 nb = current.reads(data.decode('utf-8'), format)
188 nb = current.reads(data.decode('utf-8'), format)
189 except:
189 except:
190 raise web.HTTPError(400, u'Invalid JSON data')
190 raise web.HTTPError(400, u'Invalid JSON data')
191
191
192 if name is not None:
192 if name is not None:
193 nb.metadata.name = name
193 nb.metadata.name = name
194 self.save_notebook_object(notebook_id, nb)
194 self.save_notebook_object(notebook_id, nb)
195
195
196 def save_notebook_object(self, notebook_id, nb):
196 def save_notebook_object(self, notebook_id, nb):
197 """Save an existing notebook object by notebook_id."""
197 """Save an existing notebook object by notebook_id."""
198 if notebook_id not in self.mapping:
198 if notebook_id not in self.mapping:
199 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
199 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
200 old_name = self.mapping[notebook_id]
200 old_name = self.mapping[notebook_id]
201 try:
201 try:
202 new_name = nb.metadata.name
202 new_name = nb.metadata.name
203 except AttributeError:
203 except AttributeError:
204 raise web.HTTPError(400, u'Missing notebook name')
204 raise web.HTTPError(400, u'Missing notebook name')
205 path = self.get_path_by_name(new_name)
205 path = self.get_path_by_name(new_name)
206 try:
206 try:
207 with open(path,'w') as f:
207 with open(path,'w') as f:
208 current.write(nb, f, u'json')
208 current.write(nb, f, u'json')
209 except:
209 except Exception as e:
210 raise web.HTTPError(400, u'Unexpected error while saving notebook')
210 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
211 # save .py script as well
211 # save .py script as well
212 if self.save_script:
212 if self.save_script:
213 pypath = os.path.splitext(path)[0] + '.py'
213 pypath = os.path.splitext(path)[0] + '.py'
214 try:
214 try:
215 with open(pypath,'w') as f:
215 with open(pypath,'w') as f:
216 current.write(nb, f, u'py')
216 current.write(nb, f, u'py')
217 except:
217 except Exception as e:
218 raise web.HTTPError(400, u'Unexpected error while saving notebook as script')
218 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
219
219
220 if old_name != new_name:
220 if old_name != new_name:
221 old_path = self.get_path_by_name(old_name)
221 old_path = self.get_path_by_name(old_name)
222 if os.path.isfile(old_path):
222 if os.path.isfile(old_path):
223 os.unlink(old_path)
223 os.unlink(old_path)
224 if self.save_script:
224 if self.save_script:
225 old_pypath = os.path.splitext(old_path)[0] + '.py'
225 old_pypath = os.path.splitext(old_path)[0] + '.py'
226 if os.path.isfile(old_pypath):
226 if os.path.isfile(old_pypath):
227 os.unlink(old_pypath)
227 os.unlink(old_pypath)
228 self.mapping[notebook_id] = new_name
228 self.mapping[notebook_id] = new_name
229 self.rev_mapping[new_name] = notebook_id
229 self.rev_mapping[new_name] = notebook_id
230
230
231 def delete_notebook(self, notebook_id):
231 def delete_notebook(self, notebook_id):
232 """Delete notebook by notebook_id."""
232 """Delete notebook by notebook_id."""
233 path = self.find_path(notebook_id)
233 path = self.find_path(notebook_id)
234 if not os.path.isfile(path):
234 if not os.path.isfile(path):
235 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
235 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
236 os.unlink(path)
236 os.unlink(path)
237 self.delete_notebook_id(notebook_id)
237 self.delete_notebook_id(notebook_id)
238
238
239 def new_notebook(self):
239 def new_notebook(self):
240 """Create a new notebook and returns its notebook_id."""
240 """Create a new notebook and returns its notebook_id."""
241 i = 0
241 i = 0
242 while True:
242 while True:
243 name = u'Untitled%i' % i
243 name = u'Untitled%i' % i
244 path = self.get_path_by_name(name)
244 path = self.get_path_by_name(name)
245 if not os.path.isfile(path):
245 if not os.path.isfile(path):
246 break
246 break
247 else:
247 else:
248 i = i+1
248 i = i+1
249 notebook_id = self.new_notebook_id(name)
249 notebook_id = self.new_notebook_id(name)
250 metadata = current.new_metadata(name=name)
250 metadata = current.new_metadata(name=name)
251 nb = current.new_notebook(metadata=metadata)
251 nb = current.new_notebook(metadata=metadata)
252 with open(path,'w') as f:
252 with open(path,'w') as f:
253 current.write(nb, f, u'json')
253 current.write(nb, f, u'json')
254 return notebook_id
254 return notebook_id
255
255
General Comments 0
You need to be logged in to leave comments. Login now