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