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