Skip to content

Try getting the error message in response_handler #640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
2 changes: 1 addition & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependencies:
- bash circle/setup.sh

# install testing tools for circle's version of things
- pip install nose coverage
- pip install nose coverage mock
- pip install -I .

# we need to cd out of the project root to ensure the install worked
Expand Down
1 change: 1 addition & 0 deletions optional-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ numpy
# matplotlib==1.3.1

## testing dependencies ##
mock==2.0.0
nose==1.3.3

## ipython dependencies ##
Expand Down
Empty file added plotly/api/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions plotly/api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from base64 import b64encode

from requests.compat import builtin_str, is_py2


def _to_native_string(string, encoding):
if isinstance(string, builtin_str):
return string
if is_py2:
return string.encode(encoding)
return string.decode(encoding)


def to_native_utf8_string(string):
return _to_native_string(string, 'utf-8')


def to_native_ascii_string(string):
return _to_native_string(string, 'ascii')


def basic_auth(username, password):
"""
Creates the basic auth value to be used in an authorization header.

This is mostly copied from the requests library.

:param (str) username: A Plotly username.
:param (str) password: The password for the given Plotly username.
:returns: (str) An 'authorization' header for use in a request header.

"""
if isinstance(username, str):
username = username.encode('latin1')

if isinstance(password, str):
password = password.encode('latin1')
Copy link
Member

@chriddyp chriddyp Jan 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why latin1 and not utf-8?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd have to ask Kenneth Reitz. It's just something I pulled from requests.HTTPBasicAuth. My guess is that it doesn't matter, but I'd rather just leave it as was written there.


return 'Basic ' + to_native_ascii_string(
b64encode(b':'.join((username, password))).strip()
)
3 changes: 3 additions & 0 deletions plotly/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from __future__ import absolute_import

from plotly.api.v1.clientresp import clientresp
44 changes: 44 additions & 0 deletions plotly/api/v1/clientresp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Interface to deprecated /clientresp API. Subject to deletion."""
from __future__ import absolute_import

import warnings

from requests.compat import json as _json

from plotly import config, utils, version
from plotly.api.v1.utils import request


def clientresp(data, **kwargs):
"""
Deprecated endpoint, still used because it can parse data out of a plot.

When we get around to forcing users to create grids and then create plots,
we can finally get rid of this.

:param (list) data: The data array from a figure.

"""
creds = config.get_credentials()
cfg = config.get_config()

dumps_kwargs = {'sort_keys': True, 'cls': utils.PlotlyJSONEncoder}

payload = {
'platform': 'python', 'version': version.__version__,
'args': _json.dumps(data, **dumps_kwargs),
'un': creds['username'], 'key': creds['api_key'], 'origin': 'plot',
'kwargs': _json.dumps(kwargs, **dumps_kwargs)
}

url = '{plotly_domain}/clientresp'.format(**cfg)
response = request('post', url, data=payload)

# Old functionality, just keeping it around.
parsed_content = response.json()
if parsed_content.get('warning'):
warnings.warn(parsed_content['warning'])
if parsed_content.get('message'):
print(parsed_content['message'])

return response
87 changes: 87 additions & 0 deletions plotly/api/v1/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from __future__ import absolute_import

import requests
from requests.exceptions import RequestException

from plotly import config, exceptions
from plotly.api.utils import basic_auth


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)

"""
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):
error = parsed_content.get('error')
if error:
message = error
else:
if response.ok:
return
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 v1 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.

"""
headers = {}
creds = config.get_credentials()
proxy_auth = basic_auth(creds['proxy_username'], creds['proxy_password'])

if config.get_config()['plotly_proxy_authorization']:
headers['authorization'] = proxy_auth

return headers


def request(method, url, **kwargs):
"""
Central place to make any v1 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 to requests.
:return: (requests.Response) The response directly from requests.

"""
if kwargs.get('json', None) is not None:
# See plotly.api.v2.utils.request for examples on how to do this.
raise exceptions.PlotlyError('V1 API does not handle arbitrary json.')
kwargs['headers'] = dict(kwargs.get('headers', {}), **get_headers())
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
3 changes: 3 additions & 0 deletions plotly/api/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from __future__ import absolute_import

from plotly.api.v2 import files, folders, grids, images, plot_schema, plots
85 changes: 85 additions & 0 deletions plotly/api/v2/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Interface to Plotly's /v2/files endpoints."""
from __future__ import absolute_import

from plotly.api.v2.utils import build_url, make_params, request

RESOURCE = 'files'


def retrieve(fid, share_key=None):
"""
Retrieve a general file from Plotly.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:param (str) share_key: The secret key granting 'read' access if private.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid)
params = make_params(share_key=share_key)
return request('get', url, params=params)


def update(fid, body):
"""
Update a general file from Plotly.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:param (dict) body: A mapping of body param names to values.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid)
return request('put', url, json=body)


def trash(fid):
"""
Soft-delete a general file from Plotly. (Can be undone with 'restore').

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid, route='trash')
return request('post', url)


def restore(fid):
"""
Restore a trashed, general file from Plotly. See 'trash'.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid, route='restore')
return request('post', url)


def permanent_delete(fid):
"""
Permanently delete a trashed, general file from Plotly. See 'trash'.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid, route='permanent_delete')
return request('delete', url)


def lookup(path, parent=None, user=None, exists=None):
"""
Retrieve a general file from Plotly without needing a fid.

:param (str) path: The '/'-delimited path specifying the file location.
:param (int) parent: Parent id, an integer, which the path is relative to.
:param (str) user: The username to target files for. Defaults to requestor.
:param (bool) exists: If True, don't return the full file, just a flag.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, route='lookup')
params = make_params(path=path, parent=parent, user=user, exists=exists)
return request('get', url, params=params)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, this all looks really good

103 changes: 103 additions & 0 deletions plotly/api/v2/folders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Interface to Plotly's /v2/folders endpoints."""
from __future__ import absolute_import

from plotly.api.v2.utils import build_url, make_params, request

RESOURCE = 'folders'


def create(body):
"""
Create a new folder.

:param (dict) body: A mapping of body param names to values.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE)
return request('post', url, json=body)


def retrieve(fid, share_key=None):
"""
Retrieve a folder from Plotly.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:param (str) share_key: The secret key granting 'read' access if private.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid)
params = make_params(share_key=share_key)
return request('get', url, params=params)


def update(fid, body):
"""
Update a folder from Plotly.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:param (dict) body: A mapping of body param names to values.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid)
return request('put', url, json=body)


def trash(fid):
"""
Soft-delete a folder from Plotly. (Can be undone with 'restore').

This action is recursively done on files inside the folder.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid, route='trash')
return request('post', url)


def restore(fid):
"""
Restore a trashed folder from Plotly. See 'trash'.

This action is recursively done on files inside the folder.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid, route='restore')
return request('post', url)


def permanent_delete(fid):
"""
Permanently delete a trashed folder file from Plotly. See 'trash'.

This action is recursively done on files inside the folder.

:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, id=fid, route='permanent_delete')
return request('delete', url)


def lookup(path, parent=None, user=None, exists=None):
"""
Retrieve a folder file from Plotly without needing a fid.

:param (str) path: The '/'-delimited path specifying the file location.
:param (int) parent: Parent id, an integer, which the path is relative to.
:param (str) user: The username to target files for. Defaults to requestor.
:param (bool) exists: If True, don't return the full file, just a flag.
:returns: (requests.Response) Returns response directly from requests.

"""
url = build_url(RESOURCE, route='lookup')
params = make_params(path=path, parent=parent, user=user, exists=exists)
return request('get', url, params=params)
Loading