Skip to content

Commit 3a247c0

Browse files
committed
Create organized api modules 🎉!
This was driving me nuts. We basically manually handle creating and validating *each* api response inside each calling function. Even worse, we *sometimes* raise a `PlotlyRequestError` and *sometimes* just bubble up the `requests.exceptions.HTTPError` ;__;. This does the following: * Define an `api.v1` module that only includes `clientresp` (the only old api method we still *need* to cling to) * Define an `api.v2` module that includes all the new functionality of our v2 api. * Both `v1` and `v2` raise the same `PlotlyRequestError`, so that users only need to catch *one* exception class in scripts.
1 parent 2615218 commit 3a247c0

28 files changed

+1998
-1
lines changed

circle.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ dependencies:
1616
- bash circle/setup.sh
1717

1818
# install testing tools for circle's version of things
19-
- pip install nose coverage
19+
- pip install nose coverage mock
2020
- pip install -I .
2121

2222
# we need to cd out of the project root to ensure the install worked

optional-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ numpy
1212
# matplotlib==1.3.1
1313

1414
## testing dependencies ##
15+
mock==2.0.0
1516
nose==1.3.3
1617

1718
## ipython dependencies ##

plotly/api/__init__.py

Whitespace-only changes.

plotly/api/utils.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from base64 import b64encode
2+
3+
from requests.compat import builtin_str, is_py2
4+
5+
6+
def _to_native_string(string, encoding):
7+
if isinstance(string, builtin_str):
8+
return string
9+
if is_py2:
10+
return string.encode(encoding)
11+
return string.decode(encoding)
12+
13+
14+
def to_native_utf8_string(string):
15+
return _to_native_string(string, 'utf-8')
16+
17+
18+
def to_native_ascii_string(string):
19+
return _to_native_string(string, 'ascii')
20+
21+
22+
def basic_auth(username, password):
23+
"""
24+
Creates the basic auth value to be used in an authorization header.
25+
26+
This is mostly copied from the requests library.
27+
28+
:param (str) username: A Plotly username.
29+
:param (str) password: The password for the given Plotly username.
30+
:returns: (str) An 'authorization' header for use in a request header.
31+
32+
"""
33+
if isinstance(username, str):
34+
username = username.encode('latin1')
35+
36+
if isinstance(password, str):
37+
password = password.encode('latin1')
38+
39+
return 'Basic ' + to_native_ascii_string(
40+
b64encode(b':'.join((username, password))).strip()
41+
)

plotly/api/v1/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from __future__ import absolute_import
2+
3+
from plotly.api.v1.clientresp import clientresp

plotly/api/v1/clientresp.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Interface to deprecated /clientresp API. Subject to deletion."""
2+
from __future__ import absolute_import
3+
4+
import warnings
5+
6+
from requests.compat import json as _json
7+
8+
from plotly import config, utils, version
9+
from plotly.api.v1.utils import request
10+
11+
12+
def clientresp(data, **kwargs):
13+
"""
14+
Deprecated endpoint, still used because it can parse data out of a plot.
15+
16+
When we get around to forcing users to create grids and then create plots,
17+
we can finally get rid of this.
18+
19+
:param (list) data: The data array from a figure.
20+
21+
"""
22+
creds = config.get_credentials()
23+
cfg = config.get_config()
24+
25+
dumps_kwargs = {'sort_keys': True, 'cls': utils.PlotlyJSONEncoder}
26+
27+
payload = {
28+
'platform': 'python', 'version': version.__version__,
29+
'args': _json.dumps(data, **dumps_kwargs),
30+
'un': creds['username'], 'key': creds['api_key'], 'origin': 'plot',
31+
'kwargs': _json.dumps(kwargs, **dumps_kwargs)
32+
}
33+
34+
url = '{plotly_domain}/clientresp'.format(**cfg)
35+
response = request('post', url, data=payload)
36+
37+
# Old functionality, just keeping it around.
38+
parsed_content = response.json()
39+
if parsed_content.get('warning'):
40+
warnings.warn(parsed_content['warning'])
41+
if parsed_content.get('message'):
42+
print(parsed_content['message'])
43+
44+
return response

plotly/api/v1/utils.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import absolute_import
2+
3+
import requests
4+
from requests.exceptions import RequestException
5+
6+
from plotly import config, exceptions
7+
from plotly.api.utils import basic_auth
8+
9+
10+
def validate_response(response):
11+
"""
12+
Raise a helpful PlotlyRequestError for failed requests.
13+
14+
:param (requests.Response) response: A Response object from an api request.
15+
:raises: (PlotlyRequestError) If the request failed for any reason.
16+
:returns: (None)
17+
18+
"""
19+
content = response.content
20+
status_code = response.status_code
21+
try:
22+
parsed_content = response.json()
23+
except ValueError:
24+
message = content if content else 'No Content'
25+
raise exceptions.PlotlyRequestError(message, status_code, content)
26+
27+
message = ''
28+
if isinstance(parsed_content, dict):
29+
error = parsed_content.get('error')
30+
if error:
31+
message = error
32+
else:
33+
if response.ok:
34+
return
35+
if not message:
36+
message = content if content else 'No Content'
37+
38+
raise exceptions.PlotlyRequestError(message, status_code, content)
39+
40+
41+
def get_headers():
42+
"""
43+
Using session credentials/config, get headers for a v1 API request.
44+
45+
Users may have their own proxy layer and so we free up the `authorization`
46+
header for this purpose (instead adding the user authorization in a new
47+
`plotly-authorization` header). See pull #239.
48+
49+
:returns: (dict) Headers to add to a requests.request call.
50+
51+
"""
52+
headers = {}
53+
creds = config.get_credentials()
54+
proxy_auth = basic_auth(creds['proxy_username'], creds['proxy_password'])
55+
56+
if config.get_config()['plotly_proxy_authorization']:
57+
headers['authorization'] = proxy_auth
58+
59+
return headers
60+
61+
62+
def request(method, url, **kwargs):
63+
"""
64+
Central place to make any v1 api request.
65+
66+
:param (str) method: The request method ('get', 'put', 'delete', ...).
67+
:param (str) url: The full api url to make the request to.
68+
:param kwargs: These are passed along to requests.
69+
:return: (requests.Response) The response directly from requests.
70+
71+
"""
72+
if kwargs.get('json', None) is not None:
73+
# See plotly.api.v2.utils.request for examples on how to do this.
74+
raise exceptions.PlotlyError('V1 API does not handle arbitrary json.')
75+
kwargs['headers'] = dict(kwargs.get('headers', {}), **get_headers())
76+
kwargs['verify'] = config.get_config()['plotly_ssl_verification']
77+
try:
78+
response = requests.request(method, url, **kwargs)
79+
except RequestException as e:
80+
# The message can be an exception. E.g., MaxRetryError.
81+
message = str(getattr(e, 'message', 'No message'))
82+
response = getattr(e, 'response', None)
83+
status_code = response.status_code if response else None
84+
content = response.content if response else 'No content'
85+
raise exceptions.PlotlyRequestError(message, status_code, content)
86+
validate_response(response)
87+
return response

plotly/api/v2/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from __future__ import absolute_import
2+
3+
from plotly.api.v2 import files, folders, grids, images, plot_schema, plots

plotly/api/v2/files.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Interface to Plotly's /v2/files endpoints."""
2+
from __future__ import absolute_import
3+
4+
from plotly.api.v2.utils import build_url, make_params, request
5+
6+
RESOURCE = 'files'
7+
8+
9+
def retrieve(fid, share_key=None):
10+
"""
11+
Retrieve a general file from Plotly.
12+
13+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
14+
:param (str) share_key: The secret key granting 'read' access if private.
15+
:returns: (requests.Response) Returns response directly from requests.
16+
17+
"""
18+
url = build_url(RESOURCE, id=fid)
19+
params = make_params(share_key=share_key)
20+
return request('get', url, params=params)
21+
22+
23+
def update(fid, body):
24+
"""
25+
Update a general file from Plotly.
26+
27+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
28+
:param (dict) body: A mapping of body param names to values.
29+
:returns: (requests.Response) Returns response directly from requests.
30+
31+
"""
32+
url = build_url(RESOURCE, id=fid)
33+
return request('put', url, json=body)
34+
35+
36+
def trash(fid):
37+
"""
38+
Soft-delete a general file from Plotly. (Can be undone with 'restore').
39+
40+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
41+
:returns: (requests.Response) Returns response directly from requests.
42+
43+
"""
44+
url = build_url(RESOURCE, id=fid, route='trash')
45+
return request('post', url)
46+
47+
48+
def restore(fid):
49+
"""
50+
Restore a trashed, general file from Plotly. See 'trash'.
51+
52+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
53+
:returns: (requests.Response) Returns response directly from requests.
54+
55+
"""
56+
url = build_url(RESOURCE, id=fid, route='restore')
57+
return request('post', url)
58+
59+
60+
def permanent_delete(fid):
61+
"""
62+
Permanently delete a trashed, general file from Plotly. See 'trash'.
63+
64+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
65+
:returns: (requests.Response) Returns response directly from requests.
66+
67+
"""
68+
url = build_url(RESOURCE, id=fid, route='permanent_delete')
69+
return request('delete', url)
70+
71+
72+
def lookup(path, parent=None, user=None, exists=None):
73+
"""
74+
Retrieve a general file from Plotly without needing a fid.
75+
76+
:param (str) path: The '/'-delimited path specifying the file location.
77+
:param (int) parent: Parent id, an integer, which the path is relative to.
78+
:param (str) user: The username to target files for. Defaults to requestor.
79+
:param (bool) exists: If True, don't return the full file, just a flag.
80+
:returns: (requests.Response) Returns response directly from requests.
81+
82+
"""
83+
url = build_url(RESOURCE, route='lookup')
84+
params = make_params(path=path, parent=parent, user=user, exists=exists)
85+
return request('get', url, params=params)

plotly/api/v2/folders.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Interface to Plotly's /v2/folders endpoints."""
2+
from __future__ import absolute_import
3+
4+
from plotly.api.v2.utils import build_url, make_params, request
5+
6+
RESOURCE = 'folders'
7+
8+
9+
def create(body):
10+
"""
11+
Create a new folder.
12+
13+
:param (dict) body: A mapping of body param names to values.
14+
:returns: (requests.Response) Returns response directly from requests.
15+
16+
"""
17+
url = build_url(RESOURCE)
18+
return request('post', url, json=body)
19+
20+
21+
def retrieve(fid, share_key=None):
22+
"""
23+
Retrieve a folder from Plotly.
24+
25+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
26+
:param (str) share_key: The secret key granting 'read' access if private.
27+
:returns: (requests.Response) Returns response directly from requests.
28+
29+
"""
30+
url = build_url(RESOURCE, id=fid)
31+
params = make_params(share_key=share_key)
32+
return request('get', url, params=params)
33+
34+
35+
def update(fid, body):
36+
"""
37+
Update a folder from Plotly.
38+
39+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
40+
:param (dict) body: A mapping of body param names to values.
41+
:returns: (requests.Response) Returns response directly from requests.
42+
43+
"""
44+
url = build_url(RESOURCE, id=fid)
45+
return request('put', url, json=body)
46+
47+
48+
def trash(fid):
49+
"""
50+
Soft-delete a folder from Plotly. (Can be undone with 'restore').
51+
52+
This action is recursively done on files inside the folder.
53+
54+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
55+
:returns: (requests.Response) Returns response directly from requests.
56+
57+
"""
58+
url = build_url(RESOURCE, id=fid, route='trash')
59+
return request('post', url)
60+
61+
62+
def restore(fid):
63+
"""
64+
Restore a trashed folder from Plotly. See 'trash'.
65+
66+
This action is recursively done on files inside the folder.
67+
68+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
69+
:returns: (requests.Response) Returns response directly from requests.
70+
71+
"""
72+
url = build_url(RESOURCE, id=fid, route='restore')
73+
return request('post', url)
74+
75+
76+
def permanent_delete(fid):
77+
"""
78+
Permanently delete a trashed folder file from Plotly. See 'trash'.
79+
80+
This action is recursively done on files inside the folder.
81+
82+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
83+
:returns: (requests.Response) Returns response directly from requests.
84+
85+
"""
86+
url = build_url(RESOURCE, id=fid, route='permanent_delete')
87+
return request('delete', url)
88+
89+
90+
def lookup(path, parent=None, user=None, exists=None):
91+
"""
92+
Retrieve a folder file from Plotly without needing a fid.
93+
94+
:param (str) path: The '/'-delimited path specifying the file location.
95+
:param (int) parent: Parent id, an integer, which the path is relative to.
96+
:param (str) user: The username to target files for. Defaults to requestor.
97+
:param (bool) exists: If True, don't return the full file, just a flag.
98+
:returns: (requests.Response) Returns response directly from requests.
99+
100+
"""
101+
url = build_url(RESOURCE, route='lookup')
102+
params = make_params(path=path, parent=parent, user=user, exists=exists)
103+
return request('get', url, params=params)

0 commit comments

Comments
 (0)