forked from civisanalytics/civis-python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathresponse.py
184 lines (149 loc) · 5.6 KB
/
response.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import requests
from civis._utils import camel_to_snake
class CivisClientError(Exception):
def __init__(self, message, response):
self.status_code = response.status_code
self.error_message = message
def __str__(self):
return self.error_message
def _response_to_json(response):
"""Parse a raw response to a dict.
Parameters
----------
response: requests.Response
A raw response returned by an API call.
Returns
-------
dict
The data in the response body.
Raises
------
CivisClientError
If the data in the raw response cannot be parsed.
"""
try:
return response.json()
except ValueError:
raise CivisClientError("Unable to parse JSON from response",
response)
def convert_response_data_type(response, headers=None, return_type='snake'):
"""Convert a raw response into a given type.
Parameters
----------
response : list, dict, or `requests.Response`
Convert this object into a different response object.
headers : dict, optional
If given and the return type supports it, attach these headers to the
converted response. If `response` is a `requests.Response`, the headers
will be inferred from it.
return_type : string, {'snake', 'raw', 'pandas'}
Convert the response to this type. See documentation on
`civis.APIClient` for details of the return types.
Returns
-------
list, dict, `civis.response.Response`, `requests.Response`,
`pandas.DataFrame`, or `pandas.Series`
Depending on the value of `return_type`.
"""
assert return_type in ['snake', 'raw', 'pandas'], 'Invalid return type'
if return_type == 'raw':
return response
if isinstance(response, requests.Response):
headers = response.headers
data = _response_to_json(response)
else:
data = response
if return_type == 'pandas':
import pandas as pd
if isinstance(data, list):
return pd.DataFrame.from_records(data)
# there may be nested objects or arrays in this series
return pd.Series(data)
elif return_type == 'snake':
if isinstance(data, list):
return [Response(d, headers=headers) for d in data]
return Response(data, headers=headers)
class Response(dict):
"""Custom Civis response object.
Attributes
----------
json_data : dict
This is `json_data` as it is originally returned to the user without
the key names being changed. See Notes.
headers : dict
This is the header for the API call without changing the key names.
calls_remaining : int
Number of API calls remaining before rate limit is reached.
rate_limit : int
Total number of calls per API rate limit period.
Notes
-----
The main features of this class are that it maps camelCase to snake_case
at the top level of the json object and attaches keys as attributes.
Nested object keys are not changed.
"""
def __init__(self, json_data, snake_case=True, headers=None):
self.json_data = json_data
if headers is not None:
# this circumvents recursive calls
self.headers = headers
self.calls_remaining = headers.get('X-RateLimit-Remaining')
self.rate_limit = headers.get('X-RateLimit-Limit')
for key, v in json_data.items():
if snake_case:
key = camel_to_snake(key)
if isinstance(v, dict):
val = Response(v, False)
elif isinstance(v, list):
val = [Response(o) if isinstance(o, dict) else o for o in v]
else:
val = v
self.update({key: val})
self.__dict__.update({key: val})
class PaginatedResponse:
"""A response object that supports iteration.
Parameters
----------
path : str
Make GET requests to this path.
initial_params : dict
Query params that should be passed along with each request. Note that
if `initial_params` contains the keys `page_num` or `limit`, they will
be ignored. The given dict is not modified.
endpoint : `civis.base.Endpoint`
An endpoint used to make API requests.
Notes
-----
This response is returned automatically by endpoints which support
pagination when the `iterator` kwarg is specified.
Examples
--------
>>> client = civis.APIClient()
>>> queries = client.queries.list(iterator=True)
>>> for query in queries:
... print(query['id'])
"""
def __init__(self, path, initial_params, endpoint):
self._path = path
self._params = initial_params.copy()
self._endpoint = endpoint
# We are paginating through all items, so start at the beginning and
# let the API determine the limit.
self._params['page_num'] = 1
self._params.pop('limit', None)
def __iter__(self):
while True:
response = self._endpoint._make_request('GET',
self._path,
self._params)
page_data = _response_to_json(response)
if len(page_data) == 0:
return
for data in page_data:
converted_data = convert_response_data_type(
data,
headers=response.headers,
return_type=self._endpoint._return_type
)
yield converted_data
self._params['page_num'] += 1