##// END OF EJS Templates
Better use Python slicing
Thomas Kluyver -
Show More
@@ -1,266 +1,265
1 """A base class notebook manager.
1 """A base class notebook manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 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 os
19 import os
20 import uuid
20 import uuid
21
21
22 from tornado import web
22 from tornado import web
23 from urllib import quote, unquote
23 from urllib import quote, unquote
24
24
25 from IPython.config.configurable import LoggingConfigurable
25 from IPython.config.configurable import LoggingConfigurable
26 from IPython.nbformat import current
26 from IPython.nbformat import current
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Classes
30 # Classes
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 class NotebookManager(LoggingConfigurable):
33 class NotebookManager(LoggingConfigurable):
34
34
35 # Todo:
35 # Todo:
36 # The notebook_dir attribute is used to mean a couple of different things:
36 # The notebook_dir attribute is used to mean a couple of different things:
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
38 # 2. The cwd of the kernel for a project.
38 # 2. The cwd of the kernel for a project.
39 # Right now we use this attribute in a number of different places and
39 # Right now we use this attribute in a number of different places and
40 # we are going to have to disentangle all of this.
40 # we are going to have to disentangle all of this.
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
42 The directory to use for notebooks.
42 The directory to use for notebooks.
43 """)
43 """)
44
44
45 def named_notebook_path(self, notebook_path):
45 def named_notebook_path(self, notebook_path):
46
46
47 l = len(notebook_path)
48 names = notebook_path.split('/')
47 names = notebook_path.split('/')
49 if len(names) > 1:
48 if len(names) > 1:
50 name = names[len(names)-1]
49 name = names[-1]
51 if name[(len(name)-6):(len(name))] == ".ipynb":
50 if name.endswith(".ipynb"):
52 name = name
51 name = name
53 path = notebook_path[0:l-len(name)-1]+'/'
52 path = notebook_path[0:-len(name)-1]+'/'
54 else:
53 else:
55 name = None
54 name = None
56 path = notebook_path+'/'
55 path = notebook_path+'/'
57 else:
56 else:
58 name = names[0]
57 name = names[0]
59 if name[(len(name)-6):(len(name))] == ".ipynb":
58 if name.endswith(".ipynb"):
60 name = name
59 name = name
61 path = None
60 path = None
62 else:
61 else:
63 name = None
62 name = None
64 path = notebook_path+'/'
63 path = notebook_path+'/'
65 return name, path
64 return name, path
66
65
67 def url_encode(self, path):
66 def url_encode(self, path):
68 parts = path.split('/')
67 parts = path.split('/')
69 path=""
68 path=""
70 for part in parts:
69 for part in parts:
71 part = quote(part)
70 part = quote(part)
72 path = os.path.join(path,part)
71 path = os.path.join(path,part)
73 return path
72 return path
74
73
75 def url_decode(self, path):
74 def url_decode(self, path):
76 parts = path.split('/')
75 parts = path.split('/')
77 path=""
76 path=""
78 for part in parts:
77 for part in parts:
79 part = unquote(part)
78 part = unquote(part)
80 path = os.path.join(path,part)
79 path = os.path.join(path,part)
81 return path
80 return path
82
81
83 def _notebook_dir_changed(self, new):
82 def _notebook_dir_changed(self, new):
84 """do a bit of validation of the notebook dir"""
83 """do a bit of validation of the notebook dir"""
85 if not os.path.isabs(new):
84 if not os.path.isabs(new):
86 # If we receive a non-absolute path, make it absolute.
85 # If we receive a non-absolute path, make it absolute.
87 abs_new = os.path.abspath(new)
86 abs_new = os.path.abspath(new)
88 #self.notebook_dir = os.path.dirname(abs_new)
87 #self.notebook_dir = os.path.dirname(abs_new)
89 return
88 return
90 if os.path.exists(new) and not os.path.isdir(new):
89 if os.path.exists(new) and not os.path.isdir(new):
91 raise TraitError("notebook dir %r is not a directory" % new)
90 raise TraitError("notebook dir %r is not a directory" % new)
92 if not os.path.exists(new):
91 if not os.path.exists(new):
93 self.log.info("Creating notebook dir %s", new)
92 self.log.info("Creating notebook dir %s", new)
94 try:
93 try:
95 os.mkdir(new)
94 os.mkdir(new)
96 except:
95 except:
97 raise TraitError("Couldn't create notebook dir %r" % new)
96 raise TraitError("Couldn't create notebook dir %r" % new)
98
97
99 allowed_formats = List([u'json',u'py'])
98 allowed_formats = List([u'json',u'py'])
100
99
101 def add_new_folder(self, path=None):
100 def add_new_folder(self, path=None):
102 new_path = os.path.join(self.notebook_dir, path)
101 new_path = os.path.join(self.notebook_dir, path)
103 if not os.path.exists(new_path):
102 if not os.path.exists(new_path):
104 os.makedirs(new_path)
103 os.makedirs(new_path)
105 else:
104 else:
106 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
105 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
107
106
108 def load_notebook_names(self, path):
107 def load_notebook_names(self, path):
109 """Load the notebook names into memory.
108 """Load the notebook names into memory.
110
109
111 This should be called once immediately after the notebook manager
110 This should be called once immediately after the notebook manager
112 is created to load the existing notebooks into the mapping in
111 is created to load the existing notebooks into the mapping in
113 memory.
112 memory.
114 """
113 """
115 self.list_notebooks(path)
114 self.list_notebooks(path)
116
115
117 def list_notebooks(self):
116 def list_notebooks(self):
118 """List all notebooks.
117 """List all notebooks.
119
118
120 This returns a list of dicts, each of the form::
119 This returns a list of dicts, each of the form::
121
120
122 dict(notebook_id=notebook,name=name)
121 dict(notebook_id=notebook,name=name)
123
122
124 This list of dicts should be sorted by name::
123 This list of dicts should be sorted by name::
125
124
126 data = sorted(data, key=lambda item: item['name'])
125 data = sorted(data, key=lambda item: item['name'])
127 """
126 """
128 raise NotImplementedError('must be implemented in a subclass')
127 raise NotImplementedError('must be implemented in a subclass')
129
128
130
129
131 def notebook_exists(self, notebook_path):
130 def notebook_exists(self, notebook_path):
132 """Does a notebook exist?"""
131 """Does a notebook exist?"""
133
132
134
133
135 def notebook_model(self, notebook_name, notebook_path=None, content=True):
134 def notebook_model(self, notebook_name, notebook_path=None, content=True):
136 """ Creates the standard notebook model """
135 """ Creates the standard notebook model """
137 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
136 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
138 model = {"name": notebook_name,
137 model = {"name": notebook_name,
139 "path": notebook_path,
138 "path": notebook_path,
140 "last_modified (UTC)": last_modified.ctime()}
139 "last_modified (UTC)": last_modified.ctime()}
141 if content == True:
140 if content == True:
142 model['content'] = contents
141 model['content'] = contents
143 return model
142 return model
144
143
145 def get_notebook(self, notebook_name, notebook_path=None, format=u'json'):
144 def get_notebook(self, notebook_name, notebook_path=None, format=u'json'):
146 """Get the representation of a notebook in format by notebook_name."""
145 """Get the representation of a notebook in format by notebook_name."""
147 format = unicode(format)
146 format = unicode(format)
148 if format not in self.allowed_formats:
147 if format not in self.allowed_formats:
149 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
148 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
150 kwargs = {}
149 kwargs = {}
151 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
150 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
152 if format == 'json':
151 if format == 'json':
153 # don't split lines for sending over the wire, because it
152 # don't split lines for sending over the wire, because it
154 # should match the Python in-memory format.
153 # should match the Python in-memory format.
155 kwargs['split_lines'] = False
154 kwargs['split_lines'] = False
156 representation = current.writes(nb, format, **kwargs)
155 representation = current.writes(nb, format, **kwargs)
157 name = nb.metadata.get('name', 'notebook')
156 name = nb.metadata.get('name', 'notebook')
158 return last_mod, representation, name
157 return last_mod, representation, name
159
158
160 def read_notebook_object(self, notebook_name, notebook_path=None):
159 def read_notebook_object(self, notebook_name, notebook_path=None):
161 """Get the object representation of a notebook by notebook_id."""
160 """Get the object representation of a notebook by notebook_id."""
162 raise NotImplementedError('must be implemented in a subclass')
161 raise NotImplementedError('must be implemented in a subclass')
163
162
164 def save_new_notebook(self, data, notebook_path = None, name=None, format=u'json'):
163 def save_new_notebook(self, data, notebook_path = None, name=None, format=u'json'):
165 """Save a new notebook and return its name.
164 """Save a new notebook and return its name.
166
165
167 If a name is passed in, it overrides any values in the notebook data
166 If a name is passed in, it overrides any values in the notebook data
168 and the value in the data is updated to use that value.
167 and the value in the data is updated to use that value.
169 """
168 """
170 if format not in self.allowed_formats:
169 if format not in self.allowed_formats:
171 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
170 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
172
171
173 try:
172 try:
174 nb = current.reads(data.decode('utf-8'), format)
173 nb = current.reads(data.decode('utf-8'), format)
175 except:
174 except:
176 raise web.HTTPError(400, u'Invalid JSON data')
175 raise web.HTTPError(400, u'Invalid JSON data')
177
176
178 if name is None:
177 if name is None:
179 try:
178 try:
180 name = nb.metadata.name
179 name = nb.metadata.name
181 except AttributeError:
180 except AttributeError:
182 raise web.HTTPError(400, u'Missing notebook name')
181 raise web.HTTPError(400, u'Missing notebook name')
183 nb.metadata.name = name
182 nb.metadata.name = name
184
183
185 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
184 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
186 return notebook_name
185 return notebook_name
187
186
188 def save_notebook(self, data, notebook_path=None, name=None, new_name=None, format=u'json'):
187 def save_notebook(self, data, notebook_path=None, name=None, new_name=None, format=u'json'):
189 """Save an existing notebook by notebook_name."""
188 """Save an existing notebook by notebook_name."""
190 if format not in self.allowed_formats:
189 if format not in self.allowed_formats:
191 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
190 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
192
191
193 try:
192 try:
194 nb = current.reads(data.decode('utf-8'), format)
193 nb = current.reads(data.decode('utf-8'), format)
195 except:
194 except:
196 raise web.HTTPError(400, u'Invalid JSON data')
195 raise web.HTTPError(400, u'Invalid JSON data')
197
196
198 if name is not None:
197 if name is not None:
199 nb.metadata.name = name
198 nb.metadata.name = name
200 self.write_notebook_object(nb, name, notebook_path, new_name)
199 self.write_notebook_object(nb, name, notebook_path, new_name)
201
200
202 def write_notebook_object(self, nb, notebook_name=None, notebook_path=None, new_name=None):
201 def write_notebook_object(self, nb, notebook_name=None, notebook_path=None, new_name=None):
203 """Write a notebook object and return its notebook_name.
202 """Write a notebook object and return its notebook_name.
204
203
205 If notebook_name is None, this method should create a new notebook_name.
204 If notebook_name is None, this method should create a new notebook_name.
206 If notebook_name is not None, this method should check to make sure it
205 If notebook_name is not None, this method should check to make sure it
207 exists and is valid.
206 exists and is valid.
208 """
207 """
209 raise NotImplementedError('must be implemented in a subclass')
208 raise NotImplementedError('must be implemented in a subclass')
210
209
211 def delete_notebook(self, notebook_name, notebook_path):
210 def delete_notebook(self, notebook_name, notebook_path):
212 """Delete notebook by notebook_id."""
211 """Delete notebook by notebook_id."""
213 raise NotImplementedError('must be implemented in a subclass')
212 raise NotImplementedError('must be implemented in a subclass')
214
213
215 def increment_filename(self, name):
214 def increment_filename(self, name):
216 """Increment a filename to make it unique.
215 """Increment a filename to make it unique.
217
216
218 This exists for notebook stores that must have unique names. When a notebook
217 This exists for notebook stores that must have unique names. When a notebook
219 is created or copied this method constructs a unique filename, typically
218 is created or copied this method constructs a unique filename, typically
220 by appending an integer to the name.
219 by appending an integer to the name.
221 """
220 """
222 return name
221 return name
223
222
224 def new_notebook(self, notebook_path=None):
223 def new_notebook(self, notebook_path=None):
225 """Create a new notebook and return its notebook_id."""
224 """Create a new notebook and return its notebook_id."""
226 name = self.increment_filename('Untitled', notebook_path)
225 name = self.increment_filename('Untitled', notebook_path)
227 metadata = current.new_metadata(name=name)
226 metadata = current.new_metadata(name=name)
228 nb = current.new_notebook(metadata=metadata)
227 nb = current.new_notebook(metadata=metadata)
229 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
228 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
230 return notebook_name
229 return notebook_name
231
230
232 def copy_notebook(self, name, path=None):
231 def copy_notebook(self, name, path=None):
233 """Copy an existing notebook and return its notebook_id."""
232 """Copy an existing notebook and return its notebook_id."""
234 last_mod, nb = self.read_notebook_object(name, path)
233 last_mod, nb = self.read_notebook_object(name, path)
235 name = nb.metadata.name + '-Copy'
234 name = nb.metadata.name + '-Copy'
236 name = self.increment_filename(name, path)
235 name = self.increment_filename(name, path)
237 nb.metadata.name = name
236 nb.metadata.name = name
238 notebook_name = self.write_notebook_object(nb, notebook_path = path)
237 notebook_name = self.write_notebook_object(nb, notebook_path = path)
239 return notebook_name
238 return notebook_name
240
239
241 # Checkpoint-related
240 # Checkpoint-related
242
241
243 def create_checkpoint(self, notebook_name, notebook_path=None):
242 def create_checkpoint(self, notebook_name, notebook_path=None):
244 """Create a checkpoint of the current state of a notebook
243 """Create a checkpoint of the current state of a notebook
245
244
246 Returns a checkpoint_id for the new checkpoint.
245 Returns a checkpoint_id for the new checkpoint.
247 """
246 """
248 raise NotImplementedError("must be implemented in a subclass")
247 raise NotImplementedError("must be implemented in a subclass")
249
248
250 def list_checkpoints(self, notebook_name, notebook_path=None):
249 def list_checkpoints(self, notebook_name, notebook_path=None):
251 """Return a list of checkpoints for a given notebook"""
250 """Return a list of checkpoints for a given notebook"""
252 return []
251 return []
253
252
254 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
253 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
255 """Restore a notebook from one of its checkpoints"""
254 """Restore a notebook from one of its checkpoints"""
256 raise NotImplementedError("must be implemented in a subclass")
255 raise NotImplementedError("must be implemented in a subclass")
257
256
258 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
257 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
259 """delete a checkpoint for a notebook"""
258 """delete a checkpoint for a notebook"""
260 raise NotImplementedError("must be implemented in a subclass")
259 raise NotImplementedError("must be implemented in a subclass")
261
260
262 def log_info(self):
261 def log_info(self):
263 self.log.info(self.info_string())
262 self.log.info(self.info_string())
264
263
265 def info_string(self):
264 def info_string(self):
266 return "Serving notebooks"
265 return "Serving notebooks"
General Comments 0
You need to be logged in to leave comments. Login now