##// END OF EJS Templates
Specify encoding for loading and saving notebooks.
Thomas Kluyver -
Show More
@@ -1,414 +1,414 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 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import datetime
21 21 import io
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from unicodedata import normalize
27 27
28 28 from tornado import web
29 29
30 30 from .nbmanager import NotebookManager
31 31 from IPython.nbformat import current
32 32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
33 33 from IPython.utils import tz
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Classes
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class FileNotebookManager(NotebookManager):
40 40
41 41 save_script = Bool(False, config=True,
42 42 help="""Automatically create a Python script when saving the notebook.
43 43
44 44 For easier use of import, %run and %load across notebooks, a
45 45 <notebook-name>.py script will be created next to any
46 46 <notebook-name>.ipynb on each save. This can also be set with the
47 47 short `--script` flag.
48 48 """
49 49 )
50 50
51 51 checkpoint_dir = Unicode(config=True,
52 52 help="""The location in which to keep notebook checkpoints
53 53
54 54 By default, it is notebook-dir/.ipynb_checkpoints
55 55 """
56 56 )
57 57 def _checkpoint_dir_default(self):
58 58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
59 59
60 60 def _checkpoint_dir_changed(self, name, old, new):
61 61 """do a bit of validation of the checkpoint dir"""
62 62 if not os.path.isabs(new):
63 63 # If we receive a non-absolute path, make it absolute.
64 64 abs_new = os.path.abspath(new)
65 65 self.checkpoint_dir = abs_new
66 66 return
67 67 if os.path.exists(new) and not os.path.isdir(new):
68 68 raise TraitError("checkpoint dir %r is not a directory" % new)
69 69 if not os.path.exists(new):
70 70 self.log.info("Creating checkpoint dir %s", new)
71 71 try:
72 72 os.mkdir(new)
73 73 except:
74 74 raise TraitError("Couldn't create checkpoint dir %r" % new)
75 75
76 76 def get_notebook_names(self, path=''):
77 77 """List all notebook names in the notebook dir and path."""
78 78 path = path.strip('/')
79 79 if not os.path.isdir(self.get_os_path(path=path)):
80 80 raise web.HTTPError(404, 'Directory not found: ' + path)
81 81 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
82 82 names = [os.path.basename(name)
83 83 for name in names]
84 84 return names
85 85
86 86 def increment_filename(self, basename, path=''):
87 87 """Return a non-used filename of the form basename<int>."""
88 88 path = path.strip('/')
89 89 i = 0
90 90 while True:
91 91 name = u'%s%i.ipynb' % (basename,i)
92 92 os_path = self.get_os_path(name, path)
93 93 if not os.path.isfile(os_path):
94 94 break
95 95 else:
96 96 i = i+1
97 97 return name
98 98
99 99 def path_exists(self, path):
100 100 """Does the API-style path (directory) actually exist?
101 101
102 102 Parameters
103 103 ----------
104 104 path : string
105 105 The path to check. This is an API path (`/` separated,
106 106 relative to base notebook-dir).
107 107
108 108 Returns
109 109 -------
110 110 exists : bool
111 111 Whether the path is indeed a directory.
112 112 """
113 113 path = path.strip('/')
114 114 os_path = self.get_os_path(path=path)
115 115 return os.path.isdir(os_path)
116 116
117 117 def get_os_path(self, name=None, path=''):
118 118 """Given a notebook name and a URL path, return its file system
119 119 path.
120 120
121 121 Parameters
122 122 ----------
123 123 name : string
124 124 The name of a notebook file with the .ipynb extension
125 125 path : string
126 126 The relative URL path (with '/' as separator) to the named
127 127 notebook.
128 128
129 129 Returns
130 130 -------
131 131 path : string
132 132 A file system path that combines notebook_dir (location where
133 133 server started), the relative path, and the filename with the
134 134 current operating system's url.
135 135 """
136 136 parts = path.strip('/').split('/')
137 137 parts = [p for p in parts if p != ''] # remove duplicate splits
138 138 if name is not None:
139 139 parts.append(name)
140 140 path = os.path.join(self.notebook_dir, *parts)
141 141 return path
142 142
143 143 def notebook_exists(self, name, path=''):
144 144 """Returns a True if the notebook exists. Else, returns False.
145 145
146 146 Parameters
147 147 ----------
148 148 name : string
149 149 The name of the notebook you are checking.
150 150 path : string
151 151 The relative path to the notebook (with '/' as separator)
152 152
153 153 Returns
154 154 -------
155 155 bool
156 156 """
157 157 path = path.strip('/')
158 158 nbpath = self.get_os_path(name, path=path)
159 159 return os.path.isfile(nbpath)
160 160
161 161 def list_notebooks(self, path):
162 162 """Returns a list of dictionaries that are the standard model
163 163 for all notebooks in the relative 'path'.
164 164
165 165 Parameters
166 166 ----------
167 167 path : str
168 168 the URL path that describes the relative path for the
169 169 listed notebooks
170 170
171 171 Returns
172 172 -------
173 173 notebooks : list of dicts
174 174 a list of the notebook models without 'content'
175 175 """
176 176 path = path.strip('/')
177 177 notebook_names = self.get_notebook_names(path)
178 178 notebooks = []
179 179 for name in notebook_names:
180 180 model = self.get_notebook_model(name, path, content=False)
181 181 notebooks.append(model)
182 182 notebooks = sorted(notebooks, key=lambda item: item['name'])
183 183 return notebooks
184 184
185 185 def get_notebook_model(self, name, path='', content=True):
186 186 """ Takes a path and name for a notebook and returns it's model
187 187
188 188 Parameters
189 189 ----------
190 190 name : str
191 191 the name of the notebook
192 192 path : str
193 193 the URL path that describes the relative path for
194 194 the notebook
195 195
196 196 Returns
197 197 -------
198 198 model : dict
199 199 the notebook model. If contents=True, returns the 'contents'
200 200 dict in the model as well.
201 201 """
202 202 path = path.strip('/')
203 203 if not self.notebook_exists(name=name, path=path):
204 204 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
205 205 os_path = self.get_os_path(name, path)
206 206 info = os.stat(os_path)
207 207 last_modified = tz.utcfromtimestamp(info.st_mtime)
208 208 created = tz.utcfromtimestamp(info.st_ctime)
209 209 # Create the notebook model.
210 210 model ={}
211 211 model['name'] = name
212 212 model['path'] = path
213 213 model['last_modified'] = last_modified
214 214 model['created'] = last_modified
215 215 if content is True:
216 with open(os_path, 'r') as f:
216 with io.open(os_path, 'r', encoding='utf-8') as f:
217 217 try:
218 218 nb = current.read(f, u'json')
219 219 except Exception as e:
220 220 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
221 221 model['content'] = nb
222 222 return model
223 223
224 224 def save_notebook_model(self, model, name='', path=''):
225 225 """Save the notebook model and return the model with no content."""
226 226 path = path.strip('/')
227 227
228 228 if 'content' not in model:
229 229 raise web.HTTPError(400, u'No notebook JSON data provided')
230 230
231 231 new_path = model.get('path', path).strip('/')
232 232 new_name = model.get('name', name)
233 233
234 234 if path != new_path or name != new_name:
235 235 self.rename_notebook(name, path, new_name, new_path)
236 236
237 237 # Save the notebook file
238 238 os_path = self.get_os_path(new_name, new_path)
239 239 nb = current.to_notebook_json(model['content'])
240 240 if 'name' in nb['metadata']:
241 241 nb['metadata']['name'] = u''
242 242 try:
243 243 self.log.debug("Autosaving notebook %s", os_path)
244 with open(os_path, 'w') as f:
244 with io.open(os_path, 'w', encoding='utf-8') as f:
245 245 current.write(nb, f, u'json')
246 246 except Exception as e:
247 247 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
248 248
249 249 # Save .py script as well
250 250 if self.save_script:
251 251 py_path = os.path.splitext(os_path)[0] + '.py'
252 252 self.log.debug("Writing script %s", py_path)
253 253 try:
254 254 with io.open(py_path, 'w', encoding='utf-8') as f:
255 255 current.write(model, f, u'py')
256 256 except Exception as e:
257 257 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
258 258
259 259 model = self.get_notebook_model(new_name, new_path, content=False)
260 260 return model
261 261
262 262 def update_notebook_model(self, model, name, path=''):
263 263 """Update the notebook's path and/or name"""
264 264 path = path.strip('/')
265 265 new_name = model.get('name', name)
266 266 new_path = model.get('path', path).strip('/')
267 267 if path != new_path or name != new_name:
268 268 self.rename_notebook(name, path, new_name, new_path)
269 269 model = self.get_notebook_model(new_name, new_path, content=False)
270 270 return model
271 271
272 272 def delete_notebook_model(self, name, path=''):
273 273 """Delete notebook by name and path."""
274 274 path = path.strip('/')
275 275 os_path = self.get_os_path(name, path)
276 276 if not os.path.isfile(os_path):
277 277 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
278 278
279 279 # clear checkpoints
280 280 for checkpoint in self.list_checkpoints(name, path):
281 281 checkpoint_id = checkpoint['checkpoint_id']
282 282 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
283 283 if os.path.isfile(cp_path):
284 284 self.log.debug("Unlinking checkpoint %s", cp_path)
285 285 os.unlink(cp_path)
286 286
287 287 self.log.debug("Unlinking notebook %s", os_path)
288 288 os.unlink(os_path)
289 289
290 290 def rename_notebook(self, old_name, old_path, new_name, new_path):
291 291 """Rename a notebook."""
292 292 old_path = old_path.strip('/')
293 293 new_path = new_path.strip('/')
294 294 if new_name == old_name and new_path == old_path:
295 295 return
296 296
297 297 new_os_path = self.get_os_path(new_name, new_path)
298 298 old_os_path = self.get_os_path(old_name, old_path)
299 299
300 300 # Should we proceed with the move?
301 301 if os.path.isfile(new_os_path):
302 302 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
303 303 if self.save_script:
304 304 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
305 305 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
306 306 if os.path.isfile(new_py_path):
307 307 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
308 308
309 309 # Move the notebook file
310 310 try:
311 311 os.rename(old_os_path, new_os_path)
312 312 except Exception as e:
313 313 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
314 314
315 315 # Move the checkpoints
316 316 old_checkpoints = self.list_checkpoints(old_name, old_path)
317 317 for cp in old_checkpoints:
318 318 checkpoint_id = cp['checkpoint_id']
319 319 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
320 320 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
321 321 if os.path.isfile(old_cp_path):
322 322 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
323 323 os.rename(old_cp_path, new_cp_path)
324 324
325 325 # Move the .py script
326 326 if self.save_script:
327 327 os.rename(old_py_path, new_py_path)
328 328
329 329 # Checkpoint-related utilities
330 330
331 331 def get_checkpoint_path(self, checkpoint_id, name, path=''):
332 332 """find the path to a checkpoint"""
333 333 path = path.strip('/')
334 334 filename = u"{name}-{checkpoint_id}{ext}".format(
335 335 name=name,
336 336 checkpoint_id=checkpoint_id,
337 337 ext=self.filename_ext,
338 338 )
339 339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
340 340 return cp_path
341 341
342 342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
343 343 """construct the info dict for a given checkpoint"""
344 344 path = path.strip('/')
345 345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
346 346 stats = os.stat(cp_path)
347 347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
348 348 info = dict(
349 349 checkpoint_id = checkpoint_id,
350 350 last_modified = last_modified,
351 351 )
352 352 return info
353 353
354 354 # public checkpoint API
355 355
356 356 def create_checkpoint(self, name, path=''):
357 357 """Create a checkpoint from the current state of a notebook"""
358 358 path = path.strip('/')
359 359 nb_path = self.get_os_path(name, path)
360 360 # only the one checkpoint ID:
361 361 checkpoint_id = u"checkpoint"
362 362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
363 363 self.log.debug("creating checkpoint for notebook %s", name)
364 364 if not os.path.exists(self.checkpoint_dir):
365 365 os.mkdir(self.checkpoint_dir)
366 366 shutil.copy2(nb_path, cp_path)
367 367
368 368 # return the checkpoint info
369 369 return self.get_checkpoint_model(checkpoint_id, name, path)
370 370
371 371 def list_checkpoints(self, name, path=''):
372 372 """list the checkpoints for a given notebook
373 373
374 374 This notebook manager currently only supports one checkpoint per notebook.
375 375 """
376 376 path = path.strip('/')
377 377 checkpoint_id = "checkpoint"
378 378 path = self.get_checkpoint_path(checkpoint_id, name, path)
379 379 if not os.path.exists(path):
380 380 return []
381 381 else:
382 382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
383 383
384 384
385 385 def restore_checkpoint(self, checkpoint_id, name, path=''):
386 386 """restore a notebook to a checkpointed state"""
387 387 path = path.strip('/')
388 388 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
389 389 nb_path = self.get_os_path(name, path)
390 390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
391 391 if not os.path.isfile(cp_path):
392 392 self.log.debug("checkpoint file does not exist: %s", cp_path)
393 393 raise web.HTTPError(404,
394 394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
395 395 )
396 396 # ensure notebook is readable (never restore from an unreadable notebook)
397 with open(cp_path, 'r') as f:
397 with io.open(cp_path, 'r', encoding='utf-8') as f:
398 398 nb = current.read(f, u'json')
399 399 shutil.copy2(cp_path, nb_path)
400 400 self.log.debug("copying %s -> %s", cp_path, nb_path)
401 401
402 402 def delete_checkpoint(self, checkpoint_id, name, path=''):
403 403 """delete a notebook's checkpoint"""
404 404 path = path.strip('/')
405 405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
406 406 if not os.path.isfile(cp_path):
407 407 raise web.HTTPError(404,
408 408 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
409 409 )
410 410 self.log.debug("unlinking %s", cp_path)
411 411 os.unlink(cp_path)
412 412
413 413 def info_string(self):
414 414 return "Serving notebooks from local directory: %s" % self.notebook_dir
General Comments 0
You need to be logged in to leave comments. Login now