Skip to content

Commit 87aa278

Browse files
authored
Merge pull request #5813 from kevin-bates/http-new-terminal
Add support for creating terminals via GET
2 parents c297a01 + 639da8d commit 87aa278

File tree

4 files changed

+111
-3
lines changed

4 files changed

+111
-3
lines changed

notebook/terminal/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ipython_genutils.py3compat import which
1111
from notebook.utils import url_path_join as ujoin
1212
from .terminalmanager import TerminalManager
13-
from .handlers import TerminalHandler, TermSocket
13+
from .handlers import TerminalHandler, TermSocket, NewTerminalHandler, NamedTerminalHandler
1414
from . import api_handlers
1515

1616

@@ -42,6 +42,8 @@ def initialize(nb_app):
4242
terminal_manager.log = nb_app.log
4343
base_url = nb_app.web_app.settings['base_url']
4444
handlers = [
45+
(ujoin(base_url, r"/terminals/new"), NamedTerminalHandler),
46+
(ujoin(base_url, r"/terminals/new/(\w+)"), NewTerminalHandler),
4547
(ujoin(base_url, r"/terminals/(\w+)"), TerminalHandler),
4648
(ujoin(base_url, r"/terminals/websocket/(\w+)"), TermSocket,
4749
{'term_manager': terminal_manager}),

notebook/terminal/handlers.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Copyright (c) Jupyter Development Team.
44
# Distributed under the terms of the Modified BSD License.
55

6+
import json
67
from tornado import web
78
import terminado
89
from notebook._tz import utcnow
@@ -15,7 +16,34 @@ class TerminalHandler(IPythonHandler):
1516
@web.authenticated
1617
def get(self, term_name):
1718
self.write(self.render_template('terminal.html',
18-
ws_path="terminals/websocket/%s" % term_name))
19+
ws_path="terminals/websocket/%s" % term_name))
20+
21+
22+
class NamedTerminalHandler(IPythonHandler):
23+
"""Creates and renders a named terminal interface."""
24+
@web.authenticated
25+
def get(self):
26+
model = self.terminal_manager.create()
27+
term_name = model['name']
28+
new_path = self.request.path.replace("terminals/new", "terminals/" + term_name)
29+
self.redirect(new_path)
30+
31+
32+
class NewTerminalHandler(IPythonHandler):
33+
"""Creates and renders a terminal interface using the named argument."""
34+
@web.authenticated
35+
def get(self, term_name):
36+
if term_name == 'new':
37+
raise web.HTTPError(400, "Terminal name 'new' is reserved.")
38+
new_path = self.request.path.replace("new/{}".format(term_name), term_name)
39+
if term_name in self.terminal_manager.terminals:
40+
self.set_header('Location', new_path)
41+
self.set_status(302)
42+
self.finish(json.dumps(self.terminal_manager.get_terminal_model(term_name)))
43+
return
44+
45+
self.terminal_manager.create_with_name(term_name)
46+
self.redirect(new_path)
1947

2048

2149
class TermSocket(WebSocketMixin, IPythonHandler, terminado.TermSocket):

notebook/terminal/terminalmanager.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ def __init__(self, *args, **kwargs):
4444
def create(self):
4545
"""Create a new terminal."""
4646
name, term = self.new_named_terminal()
47+
return self._finish_create(name, term)
48+
49+
def create_with_name(self, name):
50+
"""Create a new terminal."""
51+
if name in self.terminals:
52+
raise web.HTTPError(409, "A terminal with name '{}' already exists.".format(name))
53+
term = self.get_terminal(name)
54+
return self._finish_create(name, term)
55+
56+
def _finish_create(self, name, term):
4757
# Monkey-patch last-activity, similar to kernels. Should we need
4858
# more functionality per terminal, we can look into possible sub-
4959
# classing or containment then.

notebook/terminal/tests/test_terminals_api.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def tearDown(self):
5454
self.term_api.shutdown(k['name'])
5555

5656
def test_no_terminals(self):
57-
# Make sure there are no terminals running at the start
57+
# Make sure there are no terminals are running at the start
5858
terminals = self.term_api.list().json()
5959
self.assertEqual(terminals, [])
6060

@@ -65,6 +65,74 @@ def test_create_terminal(self):
6565
self.assertEqual(r.status_code, 200)
6666
self.assertIsInstance(term1, dict)
6767

68+
def test_create_terminal_via_get(self):
69+
# Test creation of terminal via GET against terminals/new/<name>
70+
r = self.term_api._req('GET', 'terminals/new')
71+
self.assertEqual(r.status_code, 200)
72+
73+
r = self.term_api.get('1')
74+
term1 = r.json()
75+
self.assertEqual(r.status_code, 200)
76+
self.assertIsInstance(term1, dict)
77+
self.assertEqual(term1['name'], '1')
78+
79+
# hit the same endpoint a second time and ensure a second named terminal is created
80+
r = self.term_api._req('GET', 'terminals/new')
81+
self.assertEqual(r.status_code, 200)
82+
83+
r = self.term_api.get('2')
84+
term2 = r.json()
85+
self.assertEqual(r.status_code, 200)
86+
self.assertIsInstance(term2, dict)
87+
self.assertEqual(term2['name'], '2')
88+
89+
r = self.term_api.shutdown('2')
90+
self.assertEqual(r.status_code, 204)
91+
92+
# Make sure there is 1 terminal running
93+
terminals = self.term_api.list().json()
94+
self.assertEqual(len(terminals), 1)
95+
96+
r = self.term_api.shutdown('1')
97+
self.assertEqual(r.status_code, 204)
98+
99+
# Make sure there are no terminals are running
100+
terminals = self.term_api.list().json()
101+
self.assertEqual(len(terminals), 0)
102+
103+
def test_create_terminal_with_name(self):
104+
# Test creation of terminal via GET against terminals/new/<name>
105+
r = self.term_api._req('GET', 'terminals/new/foo')
106+
self.assertEqual(r.status_code, 200)
107+
108+
r = self.term_api.get('foo')
109+
foo_term = r.json()
110+
self.assertEqual(r.status_code, 200)
111+
self.assertIsInstance(foo_term, dict)
112+
self.assertEqual(foo_term['name'], 'foo')
113+
114+
# hit the same endpoint a second time and ensure 302 with Location is returned
115+
r = self.term_api._req('GET', 'terminals/new/foo')
116+
# Access the "interesting" response from the history
117+
self.assertEqual(len(r.history), 1)
118+
r = r.history[0]
119+
foo_term = r.json()
120+
self.assertEqual(r.status_code, 302)
121+
self.assertEqual(r.headers['Location'], self.url_prefix + "terminals/foo")
122+
self.assertIsInstance(foo_term, dict)
123+
self.assertEqual(foo_term['name'], 'foo')
124+
125+
r = self.term_api.shutdown('foo')
126+
self.assertEqual(r.status_code, 204)
127+
128+
# Make sure there are no terminals are running
129+
terminals = self.term_api.list().json()
130+
self.assertEqual(len(terminals), 0)
131+
132+
# hit terminals/new/new and ensure that 400 is raised
133+
with assert_http_error(400):
134+
self.term_api._req('GET', 'terminals/new/new')
135+
68136
def test_terminal_root_handler(self):
69137
# POST request
70138
r = self.term_api.start()

0 commit comments

Comments
 (0)