Coverage for gramex\handlers\jsonhandler.py : 94%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import os
2import json
3import time
4import uuid
5import tornado.web
6import tornado.escape
7from .basehandler import BaseHandler
8from gramex.config import app_log
10# JSONHandler data is stored in store. Each handler is specified with a path.
11# store[path] holds the full data for that handler. It is saved in path at the
12# end of each request (if the data has changed.) The time data was last synced is
13# stored in _loaded[path].
14store = {} # Contents of the JSON data stores
15_loaded = {} # Time when persistent stores were last loaded
16_jsonstores = store # Internal legacy alias for store
19class JSONHandler(BaseHandler):
20 '''
21 Provides a REST API for managing and persisting JSON data.
23 Sample URL configuration::
25 pattern: /$YAMLURL/data/(.*)
26 handler: JSONHandler
27 kwargs:
28 path: $YAMLPATH/data.json
30 :arg string path: optional file where the JSON data is persisted. If not
31 specified, the JSON data is not persisted.
32 :arg string data: optional initial dataset, used only if path is not
33 specified. Defaults to null
34 '''
35 def parse_body_as_json(self):
36 try:
37 return tornado.escape.json_decode(self.request.body)
38 except ValueError:
39 raise tornado.web.HTTPError(status_code=400, log_message='Bad JSON', reason='Bad JSON')
41 def jsonwalk(self, jsonpath, create=False):
42 '''Return a parent, key, value from the JSON store where parent[key] == value'''
43 # Load data from self.path JSON file if it's specified, exists, and newer than last load.
44 # Otherwise, load the default data provided.
45 if self.path:
46 path = self.path
47 _jsonstores.setdefault(path, None)
48 self.changed = False
49 if os.path.exists(path):
50 if _loaded.get(path, 0) <= os.stat(path).st_mtime:
51 # Don't use encoding when reading JSON. We're using ensure_ascii=True
52 # Besides, when handling Py2 & Py3, just ignoring encoding works best
53 with open(path, mode='r') as handle: # noqa
54 try:
55 _jsonstores[path] = json.load(handle)
56 _loaded[path] = time.time()
57 except ValueError:
58 app_log.warning('Invalid JSON in %s', path)
59 self.changed = True
60 else:
61 self.changed = True
62 else:
63 path = self.name
64 _jsonstores.setdefault(path, self.default_data)
66 # Walk down the path and find the parent, key and data represented by jsonpath
67 parent, key, data = _jsonstores, path, _jsonstores[path]
68 if not jsonpath:
69 return parent, key, data
70 keys = [path] + jsonpath.split('/')
71 for index, key in enumerate(keys[1:]):
72 if hasattr(data, '__contains__') and key in data:
73 parent, data = data, data[key]
74 continue
75 if isinstance(data, list) and key.isdigit():
76 key = int(key)
77 if key < len(data):
78 parent, data = data, data[key]
79 continue
80 if create:
81 if not hasattr(data, '__contains__'):
82 parent[keys[index]] = data = {}
83 data[key] = {}
84 parent, data = data, data[key]
85 continue
86 return parent, key, None
87 return parent, key, data
89 @classmethod
90 def setup(cls, path=None, data=None, **kwargs):
91 super(JSONHandler, cls).setup(**kwargs)
92 cls.path = path
93 cls.default_data = data
94 cls.json_kwargs = {
95 'ensure_ascii': True,
96 'separators': (',', ':'),
97 }
99 def initialize(self, **kwargs):
100 super(JSONHandler, self).initialize(**kwargs)
101 self.set_header('Content-Type', 'application/json')
103 def get(self, jsonpath):
104 '''Return the JSON data at jsonpath. Return null for invalid paths.'''
105 parent, key, data = self.jsonwalk(jsonpath, create=False)
106 self.write(json.dumps(data, **self.json_kwargs))
108 def post(self, jsonpath):
109 '''Add data as a new unique key under jsonpath. Return {name: new_key}'''
110 parent, key, data = self.jsonwalk(jsonpath, create=True)
111 if self.request.body: 111 ↛ 119line 111 didn't jump to line 119, because the condition on line 111 was never false
112 if data is None:
113 parent[key] = data = {}
114 new_key = str(uuid.uuid4())
115 data[new_key] = self.parse_body_as_json()
116 self.write(json.dumps({'name': new_key}, **self.json_kwargs))
117 self.changed = True
118 else:
119 self.write(json.dumps(None))
121 def put(self, jsonpath):
122 '''Set JSON data at jsonpath. Return the data provided'''
123 parent, key, data = self.jsonwalk(jsonpath, create=True)
124 if self.request.body:
125 data = parent[key] = self.parse_body_as_json()
126 self.write(json.dumps(data, **self.json_kwargs))
127 self.changed = True
128 else:
129 self.write(json.dumps(None))
131 def patch(self, jsonpath):
132 '''Update JSON data at jsonpath. Return the data provided'''
133 parent, key, data = self.jsonwalk(jsonpath)
134 if data is not None: 134 ↛ 138line 134 didn't jump to line 138, because the condition on line 134 was never false
135 data = self.parse_body_as_json()
136 parent[key].update(data)
137 self.changed = True
138 self.write(json.dumps(data, **self.json_kwargs))
140 def delete(self, jsonpath):
141 '''Delete data at jsonpath. Return null'''
142 parent, key, data = self.jsonwalk(jsonpath)
143 if data is not None: 143 ↛ 146line 143 didn't jump to line 146, because the condition on line 143 was never false
144 del parent[key]
145 self.changed = True
146 self.write('null')
148 def on_finish(self):
149 # Write data to disk if changed. on_finish is called after writing the
150 # data, so the client is not waiting for the response.
151 if self.path and getattr(self, 'changed', False):
152 folder = os.path.dirname(os.path.abspath(self.path))
153 if not os.path.exists(folder):
154 os.makedirs(folder)
155 # Don't use encoding when reading JSON. We use ensure_ascii=True.
156 # When handling Py2 & Py3, just ignoring encoding works best.
157 with open(self.path, mode='w') as handle: # noqa
158 json.dump(_jsonstores.get(self.path), handle, **self.json_kwargs)
159 _loaded[self.path] = time.time()
160 super(JSONHandler, self).on_finish()