Show More
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 |
@@ -0,0 +1,102 | |||||
|
1 | """Tornado handlers for frontend config storage.""" | |||
|
2 | ||||
|
3 | # Copyright (c) IPython Development Team. | |||
|
4 | # Distributed under the terms of the Modified BSD License. | |||
|
5 | import json | |||
|
6 | import os | |||
|
7 | import io | |||
|
8 | import errno | |||
|
9 | from tornado import web | |||
|
10 | ||||
|
11 | from IPython.utils.py3compat import PY3 | |||
|
12 | from ...base.handlers import IPythonHandler, json_errors | |||
|
13 | ||||
|
14 | def recursive_update(target, new): | |||
|
15 | """Recursively update one dictionary using another. | |||
|
16 | ||||
|
17 | None values will delete their keys. | |||
|
18 | """ | |||
|
19 | for k, v in new.items(): | |||
|
20 | if isinstance(v, dict): | |||
|
21 | if k not in target: | |||
|
22 | target[k] = {} | |||
|
23 | recursive_update(target[k], v) | |||
|
24 | if not target[k]: | |||
|
25 | # Prune empty subdicts | |||
|
26 | del target[k] | |||
|
27 | ||||
|
28 | elif v is None: | |||
|
29 | target.pop(k, None) | |||
|
30 | ||||
|
31 | else: | |||
|
32 | target[k] = v | |||
|
33 | ||||
|
34 | class ConfigHandler(IPythonHandler): | |||
|
35 | SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH') | |||
|
36 | ||||
|
37 | @property | |||
|
38 | def config_dir(self): | |||
|
39 | return os.path.join(self.profile_dir, 'nbconfig') | |||
|
40 | ||||
|
41 | def ensure_config_dir_exists(self): | |||
|
42 | try: | |||
|
43 | os.mkdir(self.config_dir, 0o755) | |||
|
44 | except OSError as e: | |||
|
45 | if e.errno != errno.EEXIST: | |||
|
46 | raise | |||
|
47 | ||||
|
48 | def file_name(self, section_name): | |||
|
49 | return os.path.join(self.config_dir, section_name+'.json') | |||
|
50 | ||||
|
51 | @web.authenticated | |||
|
52 | @json_errors | |||
|
53 | def get(self, section_name): | |||
|
54 | self.set_header("Content-Type", 'application/json') | |||
|
55 | filename = self.file_name(section_name) | |||
|
56 | if os.path.isfile(filename): | |||
|
57 | with io.open(filename, encoding='utf-8') as f: | |||
|
58 | self.finish(f.read()) | |||
|
59 | else: | |||
|
60 | self.finish("{}") | |||
|
61 | ||||
|
62 | @web.authenticated | |||
|
63 | @json_errors | |||
|
64 | def put(self, section_name): | |||
|
65 | self.get_json_body() # Will raise 400 if content is not valid JSON | |||
|
66 | filename = self.file_name(section_name) | |||
|
67 | self.ensure_config_dir_exists() | |||
|
68 | with open(filename, 'wb') as f: | |||
|
69 | f.write(self.request.body) | |||
|
70 | self.set_status(204) | |||
|
71 | ||||
|
72 | @web.authenticated | |||
|
73 | @json_errors | |||
|
74 | def patch(self, section_name): | |||
|
75 | filename = self.file_name(section_name) | |||
|
76 | if os.path.isfile(filename): | |||
|
77 | with io.open(filename, encoding='utf-8') as f: | |||
|
78 | section = json.load(f) | |||
|
79 | else: | |||
|
80 | section = {} | |||
|
81 | ||||
|
82 | update = self.get_json_body() | |||
|
83 | recursive_update(section, update) | |||
|
84 | ||||
|
85 | self.ensure_config_dir_exists() | |||
|
86 | if PY3: | |||
|
87 | f = io.open(filename, 'w', encoding='utf-8') | |||
|
88 | else: | |||
|
89 | f = open(filename, 'wb') | |||
|
90 | with f: | |||
|
91 | json.dump(section, f) | |||
|
92 | ||||
|
93 | self.finish(json.dumps(section)) | |||
|
94 | ||||
|
95 | ||||
|
96 | # URL to handler mappings | |||
|
97 | ||||
|
98 | section_name_regex = r"(?P<section_name>\w+)" | |||
|
99 | ||||
|
100 | default_handlers = [ | |||
|
101 | (r"/api/config/%s" % section_name_regex, ConfigHandler), | |||
|
102 | ] |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 |
@@ -0,0 +1,68 | |||||
|
1 | # coding: utf-8 | |||
|
2 | """Test the config webservice API.""" | |||
|
3 | ||||
|
4 | import json | |||
|
5 | ||||
|
6 | import requests | |||
|
7 | ||||
|
8 | from IPython.html.utils import url_path_join | |||
|
9 | from IPython.html.tests.launchnotebook import NotebookTestBase | |||
|
10 | ||||
|
11 | ||||
|
12 | class ConfigAPI(object): | |||
|
13 | """Wrapper for notebook API calls.""" | |||
|
14 | def __init__(self, base_url): | |||
|
15 | self.base_url = base_url | |||
|
16 | ||||
|
17 | def _req(self, verb, section, body=None): | |||
|
18 | response = requests.request(verb, | |||
|
19 | url_path_join(self.base_url, 'api/config', section), | |||
|
20 | data=body, | |||
|
21 | ) | |||
|
22 | response.raise_for_status() | |||
|
23 | return response | |||
|
24 | ||||
|
25 | def get(self, section): | |||
|
26 | return self._req('GET', section) | |||
|
27 | ||||
|
28 | def set(self, section, values): | |||
|
29 | return self._req('PUT', section, json.dumps(values)) | |||
|
30 | ||||
|
31 | def modify(self, section, values): | |||
|
32 | return self._req('PATCH', section, json.dumps(values)) | |||
|
33 | ||||
|
34 | class APITest(NotebookTestBase): | |||
|
35 | """Test the config web service API""" | |||
|
36 | def setUp(self): | |||
|
37 | self.config_api = ConfigAPI(self.base_url()) | |||
|
38 | ||||
|
39 | def test_create_retrieve_config(self): | |||
|
40 | sample = {'foo': 'bar', 'baz': 73} | |||
|
41 | r = self.config_api.set('example', sample) | |||
|
42 | self.assertEqual(r.status_code, 204) | |||
|
43 | ||||
|
44 | r = self.config_api.get('example') | |||
|
45 | self.assertEqual(r.status_code, 200) | |||
|
46 | self.assertEqual(r.json(), sample) | |||
|
47 | ||||
|
48 | def test_modify(self): | |||
|
49 | sample = {'foo': 'bar', 'baz': 73, | |||
|
50 | 'sub': {'a': 6, 'b': 7}, 'sub2': {'c': 8}} | |||
|
51 | self.config_api.set('example', sample) | |||
|
52 | ||||
|
53 | r = self.config_api.modify('example', {'foo': None, # should delete foo | |||
|
54 | 'baz': 75, | |||
|
55 | 'wib': [1,2,3], | |||
|
56 | 'sub': {'a': 8, 'b': None, 'd': 9}, | |||
|
57 | 'sub2': {'c': None} # should delete sub2 | |||
|
58 | }) | |||
|
59 | self.assertEqual(r.status_code, 200) | |||
|
60 | self.assertEqual(r.json(), {'baz': 75, 'wib': [1,2,3], | |||
|
61 | 'sub': {'a': 8, 'd': 9}}) | |||
|
62 | ||||
|
63 | def test_get_unknown(self): | |||
|
64 | # We should get an empty config dictionary instead of a 404 | |||
|
65 | r = self.config_api.get('nonexistant') | |||
|
66 | self.assertEqual(r.status_code, 200) | |||
|
67 | self.assertEqual(r.json(), {}) | |||
|
68 |
@@ -121,6 +121,10 class IPythonHandler(AuthenticatedHandler): | |||||
121 | else: |
|
121 | else: | |
122 | return app_log |
|
122 | return app_log | |
123 |
|
123 | |||
|
124 | @property | |||
|
125 | def profile_dir(self): | |||
|
126 | return self.settings.get('profile_dir', '') | |||
|
127 | ||||
124 | #--------------------------------------------------------------- |
|
128 | #--------------------------------------------------------------- | |
125 | # URLs |
|
129 | # URLs | |
126 | #--------------------------------------------------------------- |
|
130 | #--------------------------------------------------------------- |
@@ -180,6 +180,7 class NotebookWebApplication(web.Application): | |||||
180 | config=ipython_app.config, |
|
180 | config=ipython_app.config, | |
181 | jinja2_env=env, |
|
181 | jinja2_env=env, | |
182 | terminals_available=False, # Set later if terminals are available |
|
182 | terminals_available=False, # Set later if terminals are available | |
|
183 | profile_dir = ipython_app.profile_dir.location, | |||
183 | ) |
|
184 | ) | |
184 |
|
185 | |||
185 | # allow custom overrides for the tornado web app. |
|
186 | # allow custom overrides for the tornado web app. | |
@@ -198,6 +199,7 class NotebookWebApplication(web.Application): | |||||
198 | handlers.extend(load_handlers('notebook.handlers')) |
|
199 | handlers.extend(load_handlers('notebook.handlers')) | |
199 | handlers.extend(load_handlers('nbconvert.handlers')) |
|
200 | handlers.extend(load_handlers('nbconvert.handlers')) | |
200 | handlers.extend(load_handlers('kernelspecs.handlers')) |
|
201 | handlers.extend(load_handlers('kernelspecs.handlers')) | |
|
202 | handlers.extend(load_handlers('services.config.handlers')) | |||
201 | handlers.extend(load_handlers('services.kernels.handlers')) |
|
203 | handlers.extend(load_handlers('services.kernels.handlers')) | |
202 | handlers.extend(load_handlers('services.contents.handlers')) |
|
204 | handlers.extend(load_handlers('services.contents.handlers')) | |
203 | handlers.extend(load_handlers('services.clusters.handlers')) |
|
205 | handlers.extend(load_handlers('services.clusters.handlers')) |
General Comments 0
You need to be logged in to leave comments.
Login now