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