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