-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
Copy pathutils.py
179 lines (140 loc) · 6 KB
/
utils.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
import requests
import json as _json
from requests.exceptions import RequestException
from retrying import retry
import _plotly_utils.exceptions
from chart_studio import config, exceptions
from chart_studio.api.utils import basic_auth
from _plotly_utils.utils import PlotlyJSONEncoder
def make_params(**kwargs):
"""
Helper to create a params dict, skipping undefined entries.
:returns: (dict) A params dict to pass to `request`.
"""
return {k: v for k, v in kwargs.items() if v is not None}
def build_url(resource, id="", route=""):
"""
Create a url for a request on a V2 resource.
:param (str) resource: E.g., 'files', 'plots', 'grids', etc.
:param (str) id: The unique identifier for the resource.
:param (str) route: Detail/list route. E.g., 'restore', 'lookup', etc.
:return: (str) The url.
"""
base = config.get_config()["plotly_api_domain"]
formatter = {"base": base, "resource": resource, "id": id, "route": route}
# Add path to base url depending on the input params. Note that `route`
# can refer to a 'list' or a 'detail' route. Since it cannot refer to
# both at the same time, it's overloaded in this function.
if id:
if route:
url = "{base}/v2/{resource}/{id}/{route}".format(**formatter)
else:
url = "{base}/v2/{resource}/{id}".format(**formatter)
else:
if route:
url = "{base}/v2/{resource}/{route}".format(**formatter)
else:
url = "{base}/v2/{resource}".format(**formatter)
return url
def validate_response(response):
"""
Raise a helpful PlotlyRequestError for failed requests.
:param (requests.Response) response: A Response object from an api request.
:raises: (PlotlyRequestError) If the request failed for any reason.
:returns: (None)
"""
if response.ok:
return
content = response.content
status_code = response.status_code
try:
parsed_content = response.json()
except ValueError:
message = content if content else "No Content"
raise exceptions.PlotlyRequestError(message, status_code, content)
message = ""
if isinstance(parsed_content, dict):
errors = parsed_content.get("errors", [])
messages = [error.get("message") for error in errors]
message = "\n".join([msg for msg in messages if msg])
if not message:
message = content if content else "No Content"
raise exceptions.PlotlyRequestError(message, status_code, content)
def get_headers():
"""
Using session credentials/config, get headers for a V2 API request.
Users may have their own proxy layer and so we free up the `authorization`
header for this purpose (instead adding the user authorization in a new
`plotly-authorization` header). See pull #239.
:returns: (dict) Headers to add to a requests.request call.
"""
from plotly import version
creds = config.get_credentials()
headers = {
"plotly-client-platform": "python {}".format(version.stable_semver()),
"content-type": "application/json",
}
plotly_auth = basic_auth(creds["username"], creds["api_key"])
proxy_auth = basic_auth(creds["proxy_username"], creds["proxy_password"])
if config.get_config()["plotly_proxy_authorization"]:
headers["authorization"] = proxy_auth
if creds["username"] and creds["api_key"]:
headers["plotly-authorization"] = plotly_auth
else:
if creds["username"] and creds["api_key"]:
headers["authorization"] = plotly_auth
return headers
def should_retry(exception):
if isinstance(exception, exceptions.PlotlyRequestError):
if isinstance(exception.status_code, int) and (
500 <= exception.status_code < 600 or exception.status_code == 429
):
# Retry on 5XX and 429 (image export throttling) errors.
return True
elif "Uh oh, an error occurred" in exception.message:
return True
return False
@retry(
wait_exponential_multiplier=1000,
wait_exponential_max=16000,
stop_max_delay=180000,
retry_on_exception=should_retry,
)
def request(method, url, **kwargs):
"""
Central place to make any api v2 api request.
:param (str) method: The request method ('get', 'put', 'delete', ...).
:param (str) url: The full api url to make the request to.
:param kwargs: These are passed along (but possibly mutated) to requests.
:return: (requests.Response) The response directly from requests.
"""
kwargs["headers"] = dict(kwargs.get("headers", {}), **get_headers())
# Change boolean params to lowercase strings. E.g., `True` --> `'true'`.
# Just change the value so that requests handles query string creation.
if isinstance(kwargs.get("params"), dict):
kwargs["params"] = kwargs["params"].copy()
for key in kwargs["params"]:
if isinstance(kwargs["params"][key], bool):
kwargs["params"][key] = _json.dumps(kwargs["params"][key])
# We have a special json encoding class for non-native objects.
if kwargs.get("json") is not None:
if kwargs.get("data"):
raise _plotly_utils.exceptions.PlotlyError(
"Cannot supply data and json kwargs."
)
kwargs["data"] = _json.dumps(
kwargs.pop("json"), sort_keys=True, cls=PlotlyJSONEncoder
)
# The config file determines whether reuqests should *verify*.
kwargs["verify"] = config.get_config()["plotly_ssl_verification"]
try:
response = requests.request(method, url, **kwargs)
except RequestException as e:
# The message can be an exception. E.g., MaxRetryError.
message = str(getattr(e, "message", "No message"))
response = getattr(e, "response", None)
status_code = response.status_code if response else None
content = response.content if response else "No content"
raise exceptions.PlotlyRequestError(message, status_code, content)
validate_response(response)
return response