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