Skip to content

Commit

Permalink
Merge pull request #658 from plotly/2.0.0
Browse files Browse the repository at this point in the history
2.0.0
  • Loading branch information
theengineear authored Jan 19, 2017
2 parents 4caa1b4 + f1b4981 commit 0f5f7cc
Show file tree
Hide file tree
Showing 82 changed files with 8,784 additions and 6,835 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [2.0.0]

### Changed
- `plotly.exceptions.PlotlyRequestException` is *always* raised for network
failures. Previously either a `PlotlyError`, `PlotlyRequestException`, or a
`requests.exceptions.ReqestException` could be raised. In particular, scripts
which depend on `try-except` blocks containing network requests should be
revisited.
- `plotly.py:sign_in` now validates to the plotly server specified in your
config. If it cannot make a successful request, it raises a `PlotlyError`.
- `plotly.figure_factory` will raise an `ImportError` if `numpy` is not
installed.

### Deprecated
- `plotly.tools.FigureFactory`. Use `plotly.figure_factory.*`.
- (optional imports) `plotly.tools._*_imported` It was private anyhow, but now
it's gone. (e.g., `_numpy_imported`)
- (plotly v2 helper) `plotly.py._api_v2` It was private anyhow, but now it's
gone.

## [1.13.0] - 2016-01-17
### Added
- Python 3.5 has been added as a tested environment for this package.
Expand Down
6 changes: 5 additions & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,9 @@ test:
- sudo chmod -R 444 ${PLOTLY_CONFIG_DIR} && python -c "import plotly"

# test that giving back write permissions works again
# this also has to pass the test suite that follows
- sudo chmod -R 777 ${PLOTLY_CONFIG_DIR} && python -c "import plotly"

# test that figure_factory cannot be imported with only core requirements.
# since optional requirements is part of the test suite, we don't need to
# worry about testing that it *can* be imported in this case.
- $(! python -c "import plotly.figure_factory")
5 changes: 3 additions & 2 deletions optional-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ numpy
# matplotlib==1.3.1

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

## ipython ##
ipython
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')

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
4 changes: 4 additions & 0 deletions plotly/api/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from __future__ import absolute_import

from plotly.api.v2 import (files, folders, grids, images, plot_schema, plots,
users)
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)
Loading

0 comments on commit 0f5f7cc

Please sign in to comment.