Hide keyboard shortcuts

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 

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

14store = {} # 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 

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