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

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

## 106 statements 100 run 6 missing 0 excluded 3 partial

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

1 import os

2 import json

3 import time

4 import uuid

5 import tornado.web

6 import tornado.escape

7 from .basehandler import BaseHandler

8 from gramex.config import app_log

9

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].

14 store = {} # Contents of the JSON data stores

15 _loaded = {} # Time when persistent stores were last loaded

16 _jsonstores = store # Internal legacy alias for store

17

18

19 class JSONHandler(BaseHandler):

20 '''

21 Provides a REST API for managing and persisting JSON data.

22

23 Sample URL configuration::

24

25 pattern: /$YAMLURL/data/(.*)

26 handler: JSONHandler

27 kwargs:

28 path: $YAMLPATH/data.json

29

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')

40

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)

65

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

88

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 }

98

99 def initialize(self, **kwargs):

100 super(JSONHandler, self).initialize(**kwargs)

101 self.set_header('Content-Type', 'application/json')

102

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))

107

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))

120

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))

130

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))

139

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')

147

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()