Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/redcap/request.py : 27%

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 -*-
4__author__ = 'Scott Burns <scott.s.burns@gmail.com>'
5__license__ = 'MIT'
6__copyright__ = '2014, Vanderbilt University'
8"""
10Low-level HTTP functionality
12"""
14__author__ = 'Scott Burns'
15__copyright__ = ' Copyright 2014, Vanderbilt University'
18from requests import post, RequestException
19import json
22RedcapError = RequestException
25class RCAPIError(Exception):
26 """ Errors corresponding to a misuse of the REDCap API """
27 pass
30class RCRequest(object):
31 """
32 Private class wrapping the REDCap API. Decodes response from redcap
33 and returns it.
35 References
36 ----------
37 https://redcap.vanderbilt.edu/api/help/
39 Users shouldn't really need to use this, the Project class is the
40 biggest consumer.
41 """
43 def __init__(self, url, payload, qtype):
44 """
45 Constructor
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]
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')
115 def execute(self, **kwargs):
116 """Execute the API request and return data
118 Parameters
119 ----------
120 kwargs :
121 passed to requests.post()
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
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
158 def expect_empty_json(self):
159 """Some responses are known to send empty responses"""
160 return self.type in ('imp_file', 'del_file')
162 def raise_for_status(self, r):
163 """Given a response, raise for bad status for certain actions
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
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)
178 if 400 == r.status_code and self.type == 'exp_record':
179 raise RedcapError(r.content)