##// END OF EJS Templates
Add test for copying notebook through REST API
Thomas Kluyver -
Show More
@@ -1,193 +1,200 b''
1 """Test the notebooks webservice API."""
1 """Test the notebooks webservice API."""
2
2
3 import io
3 import io
4 import os
4 import os
5 import shutil
5 import shutil
6 from zmq.utils import jsonapi
6 from zmq.utils import jsonapi
7
7
8 pjoin = os.path.join
8 pjoin = os.path.join
9
9
10 import requests
10 import requests
11
11
12 from IPython.html.utils import url_path_join
12 from IPython.html.utils import url_path_join
13 from IPython.html.tests.launchnotebook import NotebookTestBase
13 from IPython.html.tests.launchnotebook import NotebookTestBase
14 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
14 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
15 new_heading_cell, to_notebook_json)
15 new_heading_cell, to_notebook_json)
16 from IPython.utils.data import uniq_stable
16 from IPython.utils.data import uniq_stable
17
17
18 class NBAPI(object):
18 class NBAPI(object):
19 """Wrapper for notebook API calls."""
19 """Wrapper for notebook API calls."""
20 def __init__(self, base_url):
20 def __init__(self, base_url):
21 self.base_url = base_url
21 self.base_url = base_url
22
22
23 @property
23 @property
24 def nb_url(self):
24 def nb_url(self):
25 return url_path_join(self.base_url, 'api/notebooks')
25 return url_path_join(self.base_url, 'api/notebooks')
26
26
27 def _req(self, verb, path, body=None):
27 def _req(self, verb, path, body=None):
28 response = requests.request(verb,
28 response = requests.request(verb,
29 url_path_join(self.base_url, 'api/notebooks', path), data=body)
29 url_path_join(self.base_url, 'api/notebooks', path), data=body)
30 response.raise_for_status()
30 response.raise_for_status()
31 return response
31 return response
32
32
33 def list(self, path='/'):
33 def list(self, path='/'):
34 return self._req('GET', path)
34 return self._req('GET', path)
35
35
36 def read(self, name, path='/'):
36 def read(self, name, path='/'):
37 return self._req('GET', url_path_join(path, name))
37 return self._req('GET', url_path_join(path, name))
38
38
39 def create_untitled(self, path='/'):
39 def create_untitled(self, path='/'):
40 return self._req('POST', path)
40 return self._req('POST', path)
41
41
42 def upload(self, name, body, path='/'):
42 def upload(self, name, body, path='/'):
43 return self._req('POST', url_path_join(path, name), body)
43 return self._req('POST', url_path_join(path, name), body)
44
44
45 def copy(self, name, path='/'):
46 return self._req('POST', url_path_join(path, name, 'copy'))
47
45 def save(self, name, body, path='/'):
48 def save(self, name, body, path='/'):
46 return self._req('PUT', url_path_join(path, name), body)
49 return self._req('PUT', url_path_join(path, name), body)
47
50
48 def delete(self, name, path='/'):
51 def delete(self, name, path='/'):
49 return self._req('DELETE', url_path_join(path, name))
52 return self._req('DELETE', url_path_join(path, name))
50
53
51 def rename(self, name, path, new_name):
54 def rename(self, name, path, new_name):
52 body = jsonapi.dumps({'name': new_name})
55 body = jsonapi.dumps({'name': new_name})
53 return self._req('PATCH', url_path_join(path, name), body)
56 return self._req('PATCH', url_path_join(path, name), body)
54
57
55 class APITest(NotebookTestBase):
58 class APITest(NotebookTestBase):
56 """Test the kernels web service API"""
59 """Test the kernels web service API"""
57 dirs_nbs = [('', 'inroot'),
60 dirs_nbs = [('', 'inroot'),
58 ('Directory with spaces in', 'inspace'),
61 ('Directory with spaces in', 'inspace'),
59 (u'unicodΓ©', 'innonascii'),
62 (u'unicodΓ©', 'innonascii'),
60 ('foo', 'a'),
63 ('foo', 'a'),
61 ('foo', 'b'),
64 ('foo', 'b'),
62 ('foo', 'name with spaces'),
65 ('foo', 'name with spaces'),
63 ('foo', u'unicodΓ©'),
66 ('foo', u'unicodΓ©'),
64 ('foo/bar', 'baz'),
67 ('foo/bar', 'baz'),
65 ]
68 ]
66
69
67 dirs = uniq_stable([d for (d,n) in dirs_nbs])
70 dirs = uniq_stable([d for (d,n) in dirs_nbs])
68 del dirs[0] # remove ''
71 del dirs[0] # remove ''
69
72
70 def setUp(self):
73 def setUp(self):
71 nbdir = self.notebook_dir.name
74 nbdir = self.notebook_dir.name
72
75
73 for d in self.dirs:
76 for d in self.dirs:
74 os.mkdir(pjoin(nbdir, d))
77 os.mkdir(pjoin(nbdir, d))
75
78
76 for d, name in self.dirs_nbs:
79 for d, name in self.dirs_nbs:
77 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
80 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
78 nb = new_notebook(name=name)
81 nb = new_notebook(name=name)
79 write(nb, f, format='ipynb')
82 write(nb, f, format='ipynb')
80
83
81 self.nb_api = NBAPI(self.base_url())
84 self.nb_api = NBAPI(self.base_url())
82
85
83 def tearDown(self):
86 def tearDown(self):
84 nbdir = self.notebook_dir.name
87 nbdir = self.notebook_dir.name
85
88
86 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©']:
89 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©']:
87 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
90 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
88
91
89 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
92 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
90 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
93 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
91
94
92 def test_list_notebooks(self):
95 def test_list_notebooks(self):
93 nbs = self.nb_api.list().json()
96 nbs = self.nb_api.list().json()
94 self.assertEqual(len(nbs), 1)
97 self.assertEqual(len(nbs), 1)
95 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
98 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
96
99
97 nbs = self.nb_api.list('/Directory with spaces in/').json()
100 nbs = self.nb_api.list('/Directory with spaces in/').json()
98 self.assertEqual(len(nbs), 1)
101 self.assertEqual(len(nbs), 1)
99 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
102 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
100
103
101 nbs = self.nb_api.list(u'/unicodΓ©/').json()
104 nbs = self.nb_api.list(u'/unicodΓ©/').json()
102 self.assertEqual(len(nbs), 1)
105 self.assertEqual(len(nbs), 1)
103 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
106 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
104
107
105 nbs = self.nb_api.list('/foo/bar/').json()
108 nbs = self.nb_api.list('/foo/bar/').json()
106 self.assertEqual(len(nbs), 1)
109 self.assertEqual(len(nbs), 1)
107 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
110 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
108
111
109 nbs = self.nb_api.list('foo').json()
112 nbs = self.nb_api.list('foo').json()
110 self.assertEqual(len(nbs), 4)
113 self.assertEqual(len(nbs), 4)
111 nbnames = set(n['name'] for n in nbs)
114 nbnames = set(n['name'] for n in nbs)
112 self.assertEqual(nbnames, {'a.ipynb', 'b.ipynb',
115 self.assertEqual(nbnames, {'a.ipynb', 'b.ipynb',
113 'name with spaces.ipynb', u'unicodΓ©.ipynb'})
116 'name with spaces.ipynb', u'unicodΓ©.ipynb'})
114
117
115 def test_get_contents(self):
118 def test_get_contents(self):
116 for d, name in self.dirs_nbs:
119 for d, name in self.dirs_nbs:
117 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
120 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
118 self.assertEqual(nb['name'], '%s.ipynb' % name)
121 self.assertEqual(nb['name'], '%s.ipynb' % name)
119 self.assertIn('content', nb)
122 self.assertIn('content', nb)
120 self.assertIn('metadata', nb['content'])
123 self.assertIn('metadata', nb['content'])
121 self.assertIsInstance(nb['content']['metadata'], dict)
124 self.assertIsInstance(nb['content']['metadata'], dict)
122
125
123 # Name that doesn't exist - should be a 404
126 # Name that doesn't exist - should be a 404
124 try:
127 try:
125 self.nb_api.read('q.ipynb', 'foo')
128 self.nb_api.read('q.ipynb', 'foo')
126 except requests.HTTPError as e:
129 except requests.HTTPError as e:
127 self.assertEqual(e.response.status_code, 404)
130 self.assertEqual(e.response.status_code, 404)
128 else:
131 else:
129 assert False, "Reading a non-existent notebook should fail"
132 assert False, "Reading a non-existent notebook should fail"
130
133
131 def _check_nb_created(self, resp, name, path):
134 def _check_nb_created(self, resp, name, path):
132 self.assertEqual(resp.status_code, 201)
135 self.assertEqual(resp.status_code, 201)
133 self.assertEqual(resp.headers['Location'].split('/')[-1], name)
136 self.assertEqual(resp.headers['Location'].split('/')[-1], name)
134 self.assertEqual(resp.json()['name'], name)
137 self.assertEqual(resp.json()['name'], name)
135 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
138 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
136
139
137 def test_create_untitled(self):
140 def test_create_untitled(self):
138 resp = self.nb_api.create_untitled(path='foo')
141 resp = self.nb_api.create_untitled(path='foo')
139 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo')
142 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo')
140
143
141 # Second time
144 # Second time
142 resp = self.nb_api.create_untitled(path='foo')
145 resp = self.nb_api.create_untitled(path='foo')
143 self._check_nb_created(resp, 'Untitled1.ipynb', 'foo')
146 self._check_nb_created(resp, 'Untitled1.ipynb', 'foo')
144
147
145 # And two directories down
148 # And two directories down
146 resp = self.nb_api.create_untitled(path='foo/bar')
149 resp = self.nb_api.create_untitled(path='foo/bar')
147 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
150 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
148
151
149 def test_upload(self):
152 def test_upload(self):
150 nb = new_notebook(name='Upload test')
153 nb = new_notebook(name='Upload test')
151 nbmodel = {'content': nb}
154 nbmodel = {'content': nb}
152 resp = self.nb_api.upload('Upload test.ipynb', path='foo',
155 resp = self.nb_api.upload('Upload test.ipynb', path='foo',
153 body=jsonapi.dumps(nbmodel))
156 body=jsonapi.dumps(nbmodel))
154 self._check_nb_created(resp, 'Upload test.ipynb', 'foo')
157 self._check_nb_created(resp, 'Upload test.ipynb', 'foo')
155
158
159 def test_copy(self):
160 resp = self.nb_api.copy('a.ipynb', path='foo')
161 self._check_nb_created(resp, 'a-Copy0.ipynb', 'foo')
162
156 def test_delete(self):
163 def test_delete(self):
157 for d, name in self.dirs_nbs:
164 for d, name in self.dirs_nbs:
158 resp = self.nb_api.delete('%s.ipynb' % name, d)
165 resp = self.nb_api.delete('%s.ipynb' % name, d)
159 self.assertEqual(resp.status_code, 204)
166 self.assertEqual(resp.status_code, 204)
160
167
161 for d in self.dirs + ['/']:
168 for d in self.dirs + ['/']:
162 nbs = self.nb_api.list(d).json()
169 nbs = self.nb_api.list(d).json()
163 self.assertEqual(len(nbs), 0)
170 self.assertEqual(len(nbs), 0)
164
171
165 def test_rename(self):
172 def test_rename(self):
166 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
173 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
167 if False:
174 if False:
168 # XXX: Spec says this should be set, but it isn't
175 # XXX: Spec says this should be set, but it isn't
169 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
176 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
170 self.assertEqual(resp.json()['name'], 'z.ipynb')
177 self.assertEqual(resp.json()['name'], 'z.ipynb')
171 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
178 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
172
179
173 nbs = self.nb_api.list('foo').json()
180 nbs = self.nb_api.list('foo').json()
174 nbnames = set(n['name'] for n in nbs)
181 nbnames = set(n['name'] for n in nbs)
175 self.assertIn('z.ipynb', nbnames)
182 self.assertIn('z.ipynb', nbnames)
176 self.assertNotIn('a.ipynb', nbnames)
183 self.assertNotIn('a.ipynb', nbnames)
177
184
178 def test_save(self):
185 def test_save(self):
179 resp = self.nb_api.read('a.ipynb', 'foo')
186 resp = self.nb_api.read('a.ipynb', 'foo')
180 nbcontent = jsonapi.loads(resp.text)['content']
187 nbcontent = jsonapi.loads(resp.text)['content']
181 nb = to_notebook_json(nbcontent)
188 nb = to_notebook_json(nbcontent)
182 ws = new_worksheet()
189 ws = new_worksheet()
183 nb.worksheets = [ws]
190 nb.worksheets = [ws]
184 ws.cells.append(new_heading_cell('Created by test'))
191 ws.cells.append(new_heading_cell('Created by test'))
185
192
186 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
193 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
187 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
194 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
188
195
189 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
196 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
190 with open(nbfile, 'r') as f:
197 with open(nbfile, 'r') as f:
191 newnb = read(f, format='ipynb')
198 newnb = read(f, format='ipynb')
192 self.assertEqual(newnb.worksheets[0].cells[0].source,
199 self.assertEqual(newnb.worksheets[0].cells[0].source,
193 'Created by test')
200 'Created by test')
General Comments 0
You need to be logged in to leave comments. Login now