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