|
|
# coding: utf-8
|
|
|
"""Test the notebooks webservice API."""
|
|
|
|
|
|
import io
|
|
|
import json
|
|
|
import os
|
|
|
import shutil
|
|
|
from unicodedata import normalize
|
|
|
|
|
|
pjoin = os.path.join
|
|
|
|
|
|
import requests
|
|
|
|
|
|
from IPython.html.utils import url_path_join, url_escape
|
|
|
from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
|
|
|
from IPython.nbformat import current
|
|
|
from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
|
|
|
new_heading_cell, to_notebook_json)
|
|
|
from IPython.nbformat import v2
|
|
|
from IPython.utils import py3compat
|
|
|
from IPython.utils.data import uniq_stable
|
|
|
|
|
|
|
|
|
class NBAPI(object):
|
|
|
"""Wrapper for notebook API calls."""
|
|
|
def __init__(self, base_url):
|
|
|
self.base_url = base_url
|
|
|
|
|
|
def _req(self, verb, path, body=None):
|
|
|
response = requests.request(verb,
|
|
|
url_path_join(self.base_url, 'api/notebooks', path),
|
|
|
data=body,
|
|
|
)
|
|
|
response.raise_for_status()
|
|
|
return response
|
|
|
|
|
|
def list(self, path='/'):
|
|
|
return self._req('GET', path)
|
|
|
|
|
|
def read(self, name, path='/'):
|
|
|
return self._req('GET', url_path_join(path, name))
|
|
|
|
|
|
def create_untitled(self, path='/'):
|
|
|
return self._req('POST', path)
|
|
|
|
|
|
def upload_untitled(self, body, path='/'):
|
|
|
return self._req('POST', path, body)
|
|
|
|
|
|
def copy_untitled(self, copy_from, path='/'):
|
|
|
body = json.dumps({'copy_from':copy_from})
|
|
|
return self._req('POST', path, body)
|
|
|
|
|
|
def create(self, name, path='/'):
|
|
|
return self._req('PUT', url_path_join(path, name))
|
|
|
|
|
|
def upload(self, name, body, path='/'):
|
|
|
return self._req('PUT', url_path_join(path, name), body)
|
|
|
|
|
|
def copy(self, copy_from, copy_to, path='/'):
|
|
|
body = json.dumps({'copy_from':copy_from})
|
|
|
return self._req('PUT', url_path_join(path, copy_to), body)
|
|
|
|
|
|
def save(self, name, body, path='/'):
|
|
|
return self._req('PUT', url_path_join(path, name), body)
|
|
|
|
|
|
def delete(self, name, path='/'):
|
|
|
return self._req('DELETE', url_path_join(path, name))
|
|
|
|
|
|
def rename(self, name, path, new_name):
|
|
|
body = json.dumps({'name': new_name})
|
|
|
return self._req('PATCH', url_path_join(path, name), body)
|
|
|
|
|
|
def get_checkpoints(self, name, path):
|
|
|
return self._req('GET', url_path_join(path, name, 'checkpoints'))
|
|
|
|
|
|
def new_checkpoint(self, name, path):
|
|
|
return self._req('POST', url_path_join(path, name, 'checkpoints'))
|
|
|
|
|
|
def restore_checkpoint(self, name, path, checkpoint_id):
|
|
|
return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
|
|
|
|
|
|
def delete_checkpoint(self, name, path, checkpoint_id):
|
|
|
return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
|
|
|
|
|
|
class APITest(NotebookTestBase):
|
|
|
"""Test the kernels web service API"""
|
|
|
dirs_nbs = [('', 'inroot'),
|
|
|
('Directory with spaces in', 'inspace'),
|
|
|
(u'unicodé', 'innonascii'),
|
|
|
('foo', 'a'),
|
|
|
('foo', 'b'),
|
|
|
('foo', 'name with spaces'),
|
|
|
('foo', u'unicodé'),
|
|
|
('foo/bar', 'baz'),
|
|
|
(u'å b', u'ç d')
|
|
|
]
|
|
|
|
|
|
dirs = uniq_stable([d for (d,n) in dirs_nbs])
|
|
|
del dirs[0] # remove ''
|
|
|
|
|
|
def setUp(self):
|
|
|
nbdir = self.notebook_dir.name
|
|
|
|
|
|
for d in self.dirs:
|
|
|
d.replace('/', os.sep)
|
|
|
if not os.path.isdir(pjoin(nbdir, d)):
|
|
|
os.mkdir(pjoin(nbdir, d))
|
|
|
|
|
|
for d, name in self.dirs_nbs:
|
|
|
d = d.replace('/', os.sep)
|
|
|
with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
|
|
|
nb = new_notebook(name=name)
|
|
|
write(nb, f, format='ipynb')
|
|
|
|
|
|
self.nb_api = NBAPI(self.base_url())
|
|
|
|
|
|
def tearDown(self):
|
|
|
nbdir = self.notebook_dir.name
|
|
|
|
|
|
for dname in ['foo', 'Directory with spaces in', u'unicodé', u'å b']:
|
|
|
shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
|
|
|
|
|
|
if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
|
|
|
os.unlink(pjoin(nbdir, 'inroot.ipynb'))
|
|
|
|
|
|
def test_list_notebooks(self):
|
|
|
nbs = self.nb_api.list().json()
|
|
|
self.assertEqual(len(nbs), 1)
|
|
|
self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
|
|
|
|
|
|
nbs = self.nb_api.list('/Directory with spaces in/').json()
|
|
|
self.assertEqual(len(nbs), 1)
|
|
|
self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
|
|
|
|
|
|
nbs = self.nb_api.list(u'/unicodé/').json()
|
|
|
self.assertEqual(len(nbs), 1)
|
|
|
self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
|
|
|
self.assertEqual(nbs[0]['path'], u'unicodé')
|
|
|
|
|
|
nbs = self.nb_api.list('/foo/bar/').json()
|
|
|
self.assertEqual(len(nbs), 1)
|
|
|
self.assertEqual(nbs[0]['name'], 'baz.ipynb')
|
|
|
self.assertEqual(nbs[0]['path'], 'foo/bar')
|
|
|
|
|
|
nbs = self.nb_api.list('foo').json()
|
|
|
self.assertEqual(len(nbs), 4)
|
|
|
nbnames = { normalize('NFC', n['name']) for n in nbs }
|
|
|
expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
|
|
|
expected = { normalize('NFC', name) for name in expected }
|
|
|
self.assertEqual(nbnames, expected)
|
|
|
|
|
|
def test_list_nonexistant_dir(self):
|
|
|
with assert_http_error(404):
|
|
|
self.nb_api.list('nonexistant')
|
|
|
|
|
|
def test_get_contents(self):
|
|
|
for d, name in self.dirs_nbs:
|
|
|
nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
|
|
|
self.assertEqual(nb['name'], u'%s.ipynb' % name)
|
|
|
self.assertIn('content', nb)
|
|
|
self.assertIn('metadata', nb['content'])
|
|
|
self.assertIsInstance(nb['content']['metadata'], dict)
|
|
|
|
|
|
# Name that doesn't exist - should be a 404
|
|
|
with assert_http_error(404):
|
|
|
self.nb_api.read('q.ipynb', 'foo')
|
|
|
|
|
|
def _check_nb_created(self, resp, name, path):
|
|
|
self.assertEqual(resp.status_code, 201)
|
|
|
location_header = py3compat.str_to_unicode(resp.headers['Location'])
|
|
|
self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
|
|
|
self.assertEqual(resp.json()['name'], name)
|
|
|
assert os.path.isfile(pjoin(
|
|
|
self.notebook_dir.name,
|
|
|
path.replace('/', os.sep),
|
|
|
name,
|
|
|
))
|
|
|
|
|
|
def test_create_untitled(self):
|
|
|
resp = self.nb_api.create_untitled(path=u'å b')
|
|
|
self._check_nb_created(resp, 'Untitled0.ipynb', u'å b')
|
|
|
|
|
|
# Second time
|
|
|
resp = self.nb_api.create_untitled(path=u'å b')
|
|
|
self._check_nb_created(resp, 'Untitled1.ipynb', u'å b')
|
|
|
|
|
|
# And two directories down
|
|
|
resp = self.nb_api.create_untitled(path='foo/bar')
|
|
|
self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
|
|
|
|
|
|
def test_upload_untitled(self):
|
|
|
nb = new_notebook(name='Upload test')
|
|
|
nbmodel = {'content': nb}
|
|
|
resp = self.nb_api.upload_untitled(path=u'å b',
|
|
|
body=json.dumps(nbmodel))
|
|
|
self._check_nb_created(resp, 'Untitled0.ipynb', u'å b')
|
|
|
|
|
|
def test_upload(self):
|
|
|
nb = new_notebook(name=u'ignored')
|
|
|
nbmodel = {'content': nb}
|
|
|
resp = self.nb_api.upload(u'Upload tést.ipynb', path=u'å b',
|
|
|
body=json.dumps(nbmodel))
|
|
|
self._check_nb_created(resp, u'Upload tést.ipynb', u'å b')
|
|
|
|
|
|
def test_upload_v2(self):
|
|
|
nb = v2.new_notebook()
|
|
|
ws = v2.new_worksheet()
|
|
|
nb.worksheets.append(ws)
|
|
|
ws.cells.append(v2.new_code_cell(input='print("hi")'))
|
|
|
nbmodel = {'content': nb}
|
|
|
resp = self.nb_api.upload(u'Upload tést.ipynb', path=u'å b',
|
|
|
body=json.dumps(nbmodel))
|
|
|
self._check_nb_created(resp, u'Upload tést.ipynb', u'å b')
|
|
|
resp = self.nb_api.read(u'Upload tést.ipynb', u'å b')
|
|
|
data = resp.json()
|
|
|
self.assertEqual(data['content']['nbformat'], current.nbformat)
|
|
|
self.assertEqual(data['content']['orig_nbformat'], 2)
|
|
|
|
|
|
def test_copy_untitled(self):
|
|
|
resp = self.nb_api.copy_untitled(u'ç d.ipynb', path=u'å b')
|
|
|
self._check_nb_created(resp, u'ç d-Copy0.ipynb', u'å b')
|
|
|
|
|
|
def test_copy(self):
|
|
|
resp = self.nb_api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b')
|
|
|
self._check_nb_created(resp, u'cøpy.ipynb', u'å b')
|
|
|
|
|
|
def test_delete(self):
|
|
|
for d, name in self.dirs_nbs:
|
|
|
resp = self.nb_api.delete('%s.ipynb' % name, d)
|
|
|
self.assertEqual(resp.status_code, 204)
|
|
|
|
|
|
for d in self.dirs + ['/']:
|
|
|
nbs = self.nb_api.list(d).json()
|
|
|
self.assertEqual(len(nbs), 0)
|
|
|
|
|
|
def test_rename(self):
|
|
|
resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
|
|
|
self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
|
|
|
self.assertEqual(resp.json()['name'], 'z.ipynb')
|
|
|
assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
|
|
|
|
|
|
nbs = self.nb_api.list('foo').json()
|
|
|
nbnames = set(n['name'] for n in nbs)
|
|
|
self.assertIn('z.ipynb', nbnames)
|
|
|
self.assertNotIn('a.ipynb', nbnames)
|
|
|
|
|
|
def test_save(self):
|
|
|
resp = self.nb_api.read('a.ipynb', 'foo')
|
|
|
nbcontent = json.loads(resp.text)['content']
|
|
|
nb = to_notebook_json(nbcontent)
|
|
|
ws = new_worksheet()
|
|
|
nb.worksheets = [ws]
|
|
|
ws.cells.append(new_heading_cell(u'Created by test ³'))
|
|
|
|
|
|
nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
|
|
|
resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
|
|
|
|
|
|
nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
|
|
|
with io.open(nbfile, 'r', encoding='utf-8') as f:
|
|
|
newnb = read(f, format='ipynb')
|
|
|
self.assertEqual(newnb.worksheets[0].cells[0].source,
|
|
|
u'Created by test ³')
|
|
|
nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
|
|
|
newnb = to_notebook_json(nbcontent)
|
|
|
self.assertEqual(newnb.worksheets[0].cells[0].source,
|
|
|
u'Created by test ³')
|
|
|
|
|
|
# Save and rename
|
|
|
nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
|
|
|
resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
|
|
|
saved = resp.json()
|
|
|
self.assertEqual(saved['name'], 'a2.ipynb')
|
|
|
self.assertEqual(saved['path'], 'foo/bar')
|
|
|
assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
|
|
|
assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
|
|
|
with assert_http_error(404):
|
|
|
self.nb_api.read('a.ipynb', 'foo')
|
|
|
|
|
|
def test_checkpoints(self):
|
|
|
resp = self.nb_api.read('a.ipynb', 'foo')
|
|
|
r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
|
|
|
self.assertEqual(r.status_code, 201)
|
|
|
cp1 = r.json()
|
|
|
self.assertEqual(set(cp1), {'id', 'last_modified'})
|
|
|
self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
|
|
|
|
|
|
# Modify it
|
|
|
nbcontent = json.loads(resp.text)['content']
|
|
|
nb = to_notebook_json(nbcontent)
|
|
|
ws = new_worksheet()
|
|
|
nb.worksheets = [ws]
|
|
|
hcell = new_heading_cell('Created by test')
|
|
|
ws.cells.append(hcell)
|
|
|
# Save
|
|
|
nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
|
|
|
resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
|
|
|
|
|
|
# List checkpoints
|
|
|
cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
|
|
|
self.assertEqual(cps, [cp1])
|
|
|
|
|
|
nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
|
|
|
nb = to_notebook_json(nbcontent)
|
|
|
self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
|
|
|
|
|
|
# Restore cp1
|
|
|
r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
|
|
|
self.assertEqual(r.status_code, 204)
|
|
|
nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
|
|
|
nb = to_notebook_json(nbcontent)
|
|
|
self.assertEqual(nb.worksheets, [])
|
|
|
|
|
|
# Delete cp1
|
|
|
r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
|
|
|
self.assertEqual(r.status_code, 204)
|
|
|
cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
|
|
|
self.assertEqual(cps, [])
|
|
|
|
|
|
|