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