Skip to content

2.0.0 #658

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

Merged
merged 26 commits into from
Jan 19, 2017
Merged

2.0.0 #658

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e68cbf5
Try getting the error message in response_handler
alexandresobolevski Dec 21, 2016
19162af
Did not mean to commit this
alexandresobolevski Dec 21, 2016
35f3bcd
Always raise PlotlyRequestError
alexandresobolevski Dec 22, 2016
cf4d831
Change expected exception
alexandresobolevski Dec 22, 2016
cf159d6
Factor out `get_config` and `get_credentials`.
theengineear Dec 22, 2016
15d3c8b
Generalize our `PlotlyRequestError`.
theengineear Dec 27, 2016
c0c6e6c
Create organized api modules :tada:!
theengineear Dec 27, 2016
403034e
Use the new api definitions in all api calls.
theengineear Dec 27, 2016
a320248
Use `api.v2.plot_schema` to get GRAPH_REFERENCE.
theengineear Dec 27, 2016
4304f0e
:wrench: Allow `proxy_*` in `sign_in`.
theengineear Dec 27, 2016
6634971
Use requests.compat.json instead of plain json.
theengineear Dec 29, 2016
9832c11
:dolls: Remove outdated comment.
theengineear Jan 4, 2017
70e7211
Move FF from `tools.py` to `/figure_factory` pkg.
theengineear Jan 6, 2017
fb02d40
Stop doing weird protected imports for FF stuff.
theengineear Jan 6, 2017
c859ac0
Fix FF docs.
theengineear Jan 6, 2017
867fc11
Some pep:8ball: changes.
theengineear Jan 6, 2017
a617845
Add an `optional_imports` module.
theengineear Oct 2, 2015
51db9bc
Use `optional_imports` instead of `_*_imported`.
theengineear Jan 9, 2017
b7af7e3
Update CHANGELOG.md.
theengineear Jan 11, 2017
65ee9d2
Ensure `plot` sends layout along.
theengineear Jan 17, 2017
8fd6c8f
Namespace `session` import in `plotly.py`.
theengineear Jan 18, 2017
8ea90fe
Add `users.current` endpoint to v2 api.
theengineear Jan 18, 2017
0b6e010
Raise `PlotlyError` if sign_in fails.
theengineear Jan 18, 2017
1a6605f
Fail if user tries to import FF without `numpy`.
theengineear Jan 18, 2017
0259649
Move all FF tests into `test_optional`.
theengineear Jan 18, 2017
f1b4981
Version bump —> 2.0.0 (see CHANGELOG.md).
theengineear Jan 18, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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