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

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3 

4__author__ = 'Scott Burns <scott.s.burns@gmail.com>' 

5__license__ = 'MIT' 

6__copyright__ = '2014, Vanderbilt University' 

7 

8""" 

9 

10Low-level HTTP functionality 

11 

12""" 

13 

14__author__ = 'Scott Burns' 

15__copyright__ = ' Copyright 2014, Vanderbilt University' 

16 

17 

18from requests import post, RequestException 

19import json 

20 

21 

22RedcapError = RequestException 

23 

24 

25class RCAPIError(Exception): 

26 """ Errors corresponding to a misuse of the REDCap API """ 

27 pass 

28 

29 

30class RCRequest(object): 

31 """ 

32 Private class wrapping the REDCap API. Decodes response from redcap 

33 and returns it. 

34 

35 References 

36 ---------- 

37 https://redcap.vanderbilt.edu/api/help/ 

38 

39 Users shouldn't really need to use this, the Project class is the 

40 biggest consumer. 

41 """ 

42 

43 def __init__(self, url, payload, qtype): 

44 """ 

45 Constructor 

46 

47 Parameters 

48 ---------- 

49 url : str 

50 REDCap API URL 

51 payload : dict 

52 key,values corresponding to the REDCap API 

53 qtype : str 

54 Used to validate payload contents against API 

55 """ 

56 self.url = url 

57 self.payload = payload 

58 self.type = qtype 

59 if qtype: 

60 self.validate() 

61 fmt_key = 'returnFormat' if 'returnFormat' in payload else 'format' 

62 self.fmt = payload[fmt_key] 

63 

64 def validate(self): 

65 """Checks that at least required params exist""" 

66 required = ['token', 'content'] 

67 valid_data = { 

68 'exp_record': (['type', 'format'], 'record', 

69 'Exporting record but content is not record'), 

70 'del_record': (['format'], 'record', 

71 'Deleting record but content is not record'), 

72 'imp_record': (['type', 'overwriteBehavior', 'data', 'format'], 

73 'record', 'Importing record but content is not record'), 

74 'metadata': (['format'], 'metadata', 

75 'Requesting metadata but content != metadata'), 

76 'exp_file': (['action', 'record', 'field'], 'file', 

77 'Exporting file but content is not file'), 

78 'imp_file': (['action', 'record', 'field'], 'file', 

79 'Importing file but content is not file'), 

80 'del_file': (['action', 'record', 'field'], 'file', 

81 'Deleteing file but content is not file'), 

82 'exp_event': (['format'], 'event', 

83 'Exporting events but content is not event'), 

84 'exp_arm': (['format'], 'arm', 

85 'Exporting arms but content is not arm'), 

86 'exp_fem': (['format'], 'formEventMapping', 

87 'Exporting form-event mappings but content != formEventMapping'), 

88 'exp_next_id': ([], 'generateNextRecordName', 

89 'Generating next record name but content is not generateNextRecordName'), 

90 'exp_proj': (['format'], 'project', 

91 'Exporting project info but content is not project'), 

92 'exp_user': (['format'], 'user', 

93 'Exporting users but content is not user'), 

94 'exp_survey_participant_list': (['instrument'], 'participantList', 

95 'Exporting Survey Participant List but content != participantList'), 

96 'version': (['format'], 'version', 

97 'Requesting version but content != version') 

98 } 

99 extra, req_content, err_msg = valid_data[self.type] 

100 required.extend(extra) 

101 required = set(required) 

102 pl_keys = set(self.payload.keys()) 

103 # if req is not subset of payload keys, this call is wrong 

104 if not set(required) <= pl_keys: 

105 # what is not in pl_keys? 

106 not_pre = required - pl_keys 

107 raise RCAPIError("Required keys: %s" % ', '.join(not_pre)) 

108 # Check content, raise with err_msg if not good 

109 try: 

110 if self.payload['content'] != req_content: 

111 raise RCAPIError(err_msg) 

112 except KeyError: 

113 raise RCAPIError('content not in payload') 

114 

115 def execute(self, **kwargs): 

116 """Execute the API request and return data 

117 

118 Parameters 

119 ---------- 

120 kwargs : 

121 passed to requests.post() 

122 

123 Returns 

124 ------- 

125 response : list, str 

126 data object from JSON decoding process if format=='json', 

127 else return raw string (ie format=='csv'|'xml') 

128 """ 

129 r = post(self.url, data=self.payload, **kwargs) 

130 # Raise if we need to 

131 self.raise_for_status(r) 

132 content = self.get_content(r) 

133 return content, r.headers 

134 

135 def get_content(self, r): 

136 """Abstraction for grabbing content from a returned response""" 

137 if self.type == 'exp_file': 

138 # don't use the decoded r.text 

139 return r.content 

140 elif self.type == 'version': 

141 return r.content 

142 else: 

143 if self.fmt == 'json': 

144 content = {} 

145 # Decode 

146 try: 

147 # Watch out for bad/empty json 

148 content = json.loads(r.text, strict=False) 

149 except ValueError as e: 

150 if not self.expect_empty_json(): 

151 # reraise for requests that shouldn't send empty json 

152 raise ValueError(e) 

153 finally: 

154 return content 

155 else: 

156 return r.text 

157 

158 def expect_empty_json(self): 

159 """Some responses are known to send empty responses""" 

160 return self.type in ('imp_file', 'del_file') 

161 

162 def raise_for_status(self, r): 

163 """Given a response, raise for bad status for certain actions 

164 

165 Some redcap api methods don't return error messages 

166 that the user could test for or otherwise use. Therefore, we 

167 need to do the testing ourself 

168 

169 Raising for everything wouldn't let the user see the 

170 (hopefully helpful) error message""" 

171 if self.type in ('metadata', 'exp_file', 'imp_file', 'del_file'): 

172 r.raise_for_status() 

173 # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 

174 # specifically 10.5 

175 if 500 <= r.status_code < 600: 

176 raise RedcapError(r.content) 

177 

178 if 400 == r.status_code and self.type == 'exp_record': 

179 raise RedcapError(r.content)