diff --git a/_plotly_future_/__init__.py b/_plotly_future_/__init__.py
index f3cc263c507..8b4b558d993 100644
--- a/_plotly_future_/__init__.py
+++ b/_plotly_future_/__init__.py
@@ -1,3 +1,6 @@
+import warnings
+import functools
+
_future_flags = set()
@@ -6,3 +9,49 @@ def _assert_plotly_not_imported():
if 'plotly' in sys.modules:
raise ImportError("""\
The _plotly_future_ module must be imported before the plotly module""")
+
+
+warnings.filterwarnings(
+ 'default',
+ '.*?is deprecated, please use chart_studio*',
+ DeprecationWarning
+)
+
+
+def _chart_studio_warning(submodule):
+ if 'extract_chart_studio' in _future_flags:
+ warnings.warn(
+ 'The plotly.{submodule} module is deprecated, '
+ 'please use chart_studio.{submodule} instead'
+ .format(submodule=submodule),
+ DeprecationWarning,
+ stacklevel=2)
+
+
+def _chart_studio_deprecation(fn):
+
+ fn_name = fn.__name__
+ fn_module = fn.__module__
+ plotly_name = '.'.join(
+ ['plotly'] + fn_module.split('.')[1:] + [fn_name])
+ chart_studio_name = '.'.join(
+ ['chart_studio'] + fn_module.split('.')[1:] + [fn_name])
+
+ msg = """\
+{plotly_name} is deprecated, please use {chart_studio_name}\
+""".format(plotly_name=plotly_name, chart_studio_name=chart_studio_name)
+
+ @functools.wraps(fn)
+ def wrapper(*args, **kwargs):
+ if 'extract_chart_studio' in _future_flags:
+ warnings.warn(
+ msg,
+ DeprecationWarning,
+ stacklevel=2)
+
+ return fn(*args, **kwargs)
+
+ return wrapper
+
+
+__all__ = ['_future_flags', '_chart_studio_warning']
diff --git a/_plotly_future_/extract_chart_studio.py b/_plotly_future_/extract_chart_studio.py
new file mode 100644
index 00000000000..a3de7eca345
--- /dev/null
+++ b/_plotly_future_/extract_chart_studio.py
@@ -0,0 +1,5 @@
+from __future__ import absolute_import
+from _plotly_future_ import _future_flags, _assert_plotly_not_imported
+
+_assert_plotly_not_imported()
+_future_flags.add('extract_chart_studio')
diff --git a/_plotly_future_/remove_deprecations.py b/_plotly_future_/remove_deprecations.py
new file mode 100644
index 00000000000..3e47048038d
--- /dev/null
+++ b/_plotly_future_/remove_deprecations.py
@@ -0,0 +1,5 @@
+from __future__ import absolute_import
+from _plotly_future_ import _future_flags, _assert_plotly_not_imported
+
+_assert_plotly_not_imported()
+_future_flags.add('remove_deprecations')
diff --git a/_plotly_future_/v4.py b/_plotly_future_/v4.py
index fdbad2b012c..7e5c023a720 100644
--- a/_plotly_future_/v4.py
+++ b/_plotly_future_/v4.py
@@ -1,2 +1,4 @@
from __future__ import absolute_import
-from _plotly_future_ import renderer_defaults, template_defaults
+from _plotly_future_ import (
+ renderer_defaults, template_defaults, extract_chart_studio,
+ remove_deprecations)
diff --git a/_plotly_utils/basevalidators.py b/_plotly_utils/basevalidators.py
index db6d4e4d4a4..c1f0d193341 100644
--- a/_plotly_utils/basevalidators.py
+++ b/_plotly_utils/basevalidators.py
@@ -1,33 +1,18 @@
+from __future__ import absolute_import
+
import base64
import numbers
import textwrap
import uuid
from importlib import import_module
import copy
-
import io
from copy import deepcopy
-
import re
-
-# Optional imports
-# ----------------
import sys
from six import string_types
-np = None
-pd = None
-
-try:
- np = import_module('numpy')
-
- try:
- pd = import_module('pandas')
- except ImportError:
- pass
-
-except ImportError:
- pass
+from _plotly_utils.optional_imports import get_module
# back-port of fullmatch from Py3.4+
@@ -50,6 +35,8 @@ def to_scalar_or_list(v):
# Python native scalar type ('float' in the example above).
# We explicitly check if is has the 'item' method, which conventionally
# converts these types to native scalars.
+ np = get_module('numpy')
+ pd = get_module('pandas')
if np and np.isscalar(v) and hasattr(v, 'item'):
return v.item()
if isinstance(v, (list, tuple)):
@@ -86,7 +73,8 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
np.ndarray
Numpy array with the 'WRITEABLE' flag set to False
"""
-
+ np = get_module('numpy')
+ pd = get_module('pandas')
assert np is not None
# ### Process kind ###
@@ -175,7 +163,9 @@ def is_numpy_convertable(v):
def is_homogeneous_array(v):
"""
Return whether a value is considered to be a homogeneous array
- """
+ """
+ np = get_module('numpy')
+ pd = get_module('pandas')
if ((np and isinstance(v, np.ndarray) or
(pd and isinstance(v, (pd.Series, pd.Index))))):
return True
@@ -616,7 +606,7 @@ def description(self):
as a plotly.grid_objs.Column object""".format(plotly_name=self.plotly_name))
def validate_coerce(self, v):
- from plotly.grid_objs import Column
+ from chart_studio.grid_objs import Column
if v is None:
# Pass None through
pass
@@ -704,7 +694,7 @@ def validate_coerce(self, v):
# Pass None through
pass
elif self.array_ok and is_homogeneous_array(v):
-
+ np = get_module('numpy')
try:
v_array = copy_to_readonly_numpy_array(v, force_numeric=True)
except (ValueError, TypeError, OverflowError):
@@ -825,7 +815,7 @@ def validate_coerce(self, v):
# Pass None through
pass
elif self.array_ok and is_homogeneous_array(v):
-
+ np = get_module('numpy')
v_array = copy_to_readonly_numpy_array(v,
kind=('i', 'u'),
force_numeric=True)
@@ -964,6 +954,8 @@ def validate_coerce(self, v):
self.raise_invalid_elements(invalid_els)
if is_homogeneous_array(v):
+ np = get_module('numpy')
+
# If not strict, let numpy cast elements to strings
v = copy_to_readonly_numpy_array(v, kind='U')
diff --git a/_plotly_utils/exceptions.py b/_plotly_utils/exceptions.py
new file mode 100644
index 00000000000..11a19a5c7c6
--- /dev/null
+++ b/_plotly_utils/exceptions.py
@@ -0,0 +1,82 @@
+class PlotlyError(Exception):
+ pass
+
+
+class PlotlyEmptyDataError(PlotlyError):
+ pass
+
+
+class PlotlyGraphObjectError(PlotlyError):
+ def __init__(self, message='', path=(), notes=()):
+ """
+ General graph object error for validation failures.
+
+ :param (str|unicode) message: The error message.
+ :param (iterable) path: A path pointing to the error.
+ :param notes: Add additional notes, but keep default exception message.
+
+ """
+ self.message = message
+ self.plain_message = message # for backwards compat
+ self.path = list(path)
+ self.notes = notes
+ super(PlotlyGraphObjectError, self).__init__(message)
+
+ def __str__(self):
+ """This is called by Python to present the error message."""
+ format_dict = {
+ 'message': self.message,
+ 'path': '[' + ']['.join(repr(k) for k in self.path) + ']',
+ 'notes': '\n'.join(self.notes)
+ }
+ return ('{message}\n\nPath To Error: {path}\n\n{notes}'
+ .format(**format_dict))
+
+
+class PlotlyDictKeyError(PlotlyGraphObjectError):
+ def __init__(self, obj, path, notes=()):
+ """See PlotlyGraphObjectError.__init__ for param docs."""
+ format_dict = {'attribute': path[-1], 'object_name': obj._name}
+ message = ("'{attribute}' is not allowed in '{object_name}'"
+ .format(**format_dict))
+ notes = [obj.help(return_help=True)] + list(notes)
+ super(PlotlyDictKeyError, self).__init__(
+ message=message, path=path, notes=notes
+ )
+
+
+class PlotlyDictValueError(PlotlyGraphObjectError):
+ def __init__(self, obj, path, notes=()):
+ """See PlotlyGraphObjectError.__init__ for param docs."""
+ format_dict = {'attribute': path[-1], 'object_name': obj._name}
+ message = ("'{attribute}' has invalid value inside '{object_name}'"
+ .format(**format_dict))
+ notes = [obj.help(path[-1], return_help=True)] + list(notes)
+ super(PlotlyDictValueError, self).__init__(
+ message=message, notes=notes, path=path
+ )
+
+
+class PlotlyListEntryError(PlotlyGraphObjectError):
+ def __init__(self, obj, path, notes=()):
+ """See PlotlyGraphObjectError.__init__ for param docs."""
+ format_dict = {'index': path[-1], 'object_name': obj._name}
+ message = ("Invalid entry found in '{object_name}' at index, '{index}'"
+ .format(**format_dict))
+ notes = [obj.help(return_help=True)] + list(notes)
+ super(PlotlyListEntryError, self).__init__(
+ message=message, path=path, notes=notes
+ )
+
+
+class PlotlyDataTypeError(PlotlyGraphObjectError):
+ def __init__(self, obj, path, notes=()):
+ """See PlotlyGraphObjectError.__init__ for param docs."""
+ format_dict = {'index': path[-1], 'object_name': obj._name}
+ message = ("Invalid entry found in '{object_name}' at index, '{index}'"
+ .format(**format_dict))
+ note = "It's invalid because it doesn't contain a valid 'type' value."
+ notes = [note] + list(notes)
+ super(PlotlyDataTypeError, self).__init__(
+ message=message, path=path, notes=notes
+ )
\ No newline at end of file
diff --git a/_plotly_utils/files.py b/_plotly_utils/files.py
new file mode 100644
index 00000000000..f77f277adaf
--- /dev/null
+++ b/_plotly_utils/files.py
@@ -0,0 +1,36 @@
+import os
+
+PLOTLY_DIR = os.environ.get("PLOTLY_DIR",
+ os.path.join(os.path.expanduser("~"), ".plotly"))
+TEST_FILE = os.path.join(PLOTLY_DIR, ".permission_test")
+
+
+def _permissions():
+ try:
+ if not os.path.exists(PLOTLY_DIR):
+ try:
+ os.mkdir(PLOTLY_DIR)
+ except Exception:
+ # in case of race
+ if not os.path.isdir(PLOTLY_DIR):
+ raise
+ with open(TEST_FILE, 'w') as f:
+ f.write('testing\n')
+ try:
+ os.remove(TEST_FILE)
+ except Exception:
+ pass
+ return True
+ except Exception: # Do not trap KeyboardInterrupt.
+ return False
+
+
+_file_permissions = None
+
+
+def ensure_writable_plotly_dir():
+ # Cache permissions status
+ global _file_permissions
+ if _file_permissions is None:
+ _file_permissions = _permissions()
+ return _file_permissions
diff --git a/_plotly_utils/optional_imports.py b/_plotly_utils/optional_imports.py
new file mode 100644
index 00000000000..7f49d1fe26f
--- /dev/null
+++ b/_plotly_utils/optional_imports.py
@@ -0,0 +1,31 @@
+"""
+Stand-alone module to provide information about whether optional deps exist.
+
+"""
+from __future__ import absolute_import
+
+from importlib import import_module
+import logging
+
+logger = logging.getLogger(__name__)
+_not_importable = set()
+
+
+def get_module(name):
+ """
+ Return module or None. Absolute import is required.
+
+ :param (str) name: Dot-separated module path. E.g., 'scipy.stats'.
+ :raise: (ImportError) Only when exc_msg is defined.
+ :return: (module|None) If import succeeds, the module will be returned.
+
+ """
+ if name not in _not_importable:
+ try:
+ return import_module(name)
+ except ImportError:
+ _not_importable.add(name)
+ except Exception as e:
+ _not_importable.add(name)
+ msg = "Error importing optional module {}".format(name)
+ logger.exception(msg)
diff --git a/_plotly_utils/utils.py b/_plotly_utils/utils.py
new file mode 100644
index 00000000000..ab5d03653af
--- /dev/null
+++ b/_plotly_utils/utils.py
@@ -0,0 +1,244 @@
+import datetime
+import decimal
+import json as _json
+import sys
+
+import pytz
+
+from _plotly_utils.optional_imports import get_module
+
+
+PY36_OR_LATER = (
+ sys.version_info.major == 3 and sys.version_info.minor >= 6
+)
+
+
+class PlotlyJSONEncoder(_json.JSONEncoder):
+ """
+ Meant to be passed as the `cls` kwarg to json.dumps(obj, cls=..)
+
+ See PlotlyJSONEncoder.default for more implementation information.
+
+ Additionally, this encoder overrides nan functionality so that 'Inf',
+ 'NaN' and '-Inf' encode to 'null'. Which is stricter JSON than the Python
+ version.
+
+ """
+
+ def coerce_to_strict(self, const):
+ """
+ This is used to ultimately *encode* into strict JSON, see `encode`
+
+ """
+ # before python 2.7, 'true', 'false', 'null', were include here.
+ if const in ('Infinity', '-Infinity', 'NaN'):
+ return None
+ else:
+ return const
+
+ def encode(self, o):
+ """
+ Load and then dump the result using parse_constant kwarg
+
+ Note that setting invalid separators will cause a failure at this step.
+
+ """
+
+ # this will raise errors in a normal-expected way
+ encoded_o = super(PlotlyJSONEncoder, self).encode(o)
+
+ # now:
+ # 1. `loads` to switch Infinity, -Infinity, NaN to None
+ # 2. `dumps` again so you get 'null' instead of extended JSON
+ try:
+ new_o = _json.loads(encoded_o,
+ parse_constant=self.coerce_to_strict)
+ except ValueError:
+
+ # invalid separators will fail here. raise a helpful exception
+ raise ValueError(
+ "Encoding into strict JSON failed. Did you set the separators "
+ "valid JSON separators?"
+ )
+ else:
+ return _json.dumps(new_o, sort_keys=self.sort_keys,
+ indent=self.indent,
+ separators=(self.item_separator,
+ self.key_separator))
+
+ def default(self, obj):
+ """
+ Accept an object (of unknown type) and try to encode with priority:
+ 1. builtin: user-defined objects
+ 2. sage: sage math cloud
+ 3. pandas: dataframes/series
+ 4. numpy: ndarrays
+ 5. datetime: time/datetime objects
+
+ Each method throws a NotEncoded exception if it fails.
+
+ The default method will only get hit if the object is not a type that
+ is naturally encoded by json:
+
+ Normal objects:
+ dict object
+ list, tuple array
+ str, unicode string
+ int, long, float number
+ True true
+ False false
+ None null
+
+ Extended objects:
+ float('nan') 'NaN'
+ float('infinity') 'Infinity'
+ float('-infinity') '-Infinity'
+
+ Therefore, we only anticipate either unknown iterables or values here.
+
+ """
+ # TODO: The ordering if these methods is *very* important. Is this OK?
+ encoding_methods = (
+ self.encode_as_plotly,
+ self.encode_as_sage,
+ self.encode_as_numpy,
+ self.encode_as_pandas,
+ self.encode_as_datetime,
+ self.encode_as_date,
+ self.encode_as_list, # because some values have `tolist` do last.
+ self.encode_as_decimal
+ )
+ for encoding_method in encoding_methods:
+ try:
+ return encoding_method(obj)
+ except NotEncodable:
+ pass
+ return _json.JSONEncoder.default(self, obj)
+
+ @staticmethod
+ def encode_as_plotly(obj):
+ """Attempt to use a builtin `to_plotly_json` method."""
+ try:
+ return obj.to_plotly_json()
+ except AttributeError:
+ raise NotEncodable
+
+ @staticmethod
+ def encode_as_list(obj):
+ """Attempt to use `tolist` method to convert to normal Python list."""
+ if hasattr(obj, 'tolist'):
+ return obj.tolist()
+ else:
+ raise NotEncodable
+
+ @staticmethod
+ def encode_as_sage(obj):
+ """Attempt to convert sage.all.RR to floats and sage.all.ZZ to ints"""
+ sage_all = get_module('sage.all')
+ if not sage_all:
+ raise NotEncodable
+
+ if obj in sage_all.RR:
+ return float(obj)
+ elif obj in sage_all.ZZ:
+ return int(obj)
+ else:
+ raise NotEncodable
+
+ @staticmethod
+ def encode_as_pandas(obj):
+ """Attempt to convert pandas.NaT"""
+ pandas = get_module('pandas')
+ if not pandas:
+ raise NotEncodable
+
+ if obj is pandas.NaT:
+ return None
+ else:
+ raise NotEncodable
+
+ @staticmethod
+ def encode_as_numpy(obj):
+ """Attempt to convert numpy.ma.core.masked"""
+ numpy = get_module('numpy')
+ if not numpy:
+ raise NotEncodable
+
+ if obj is numpy.ma.core.masked:
+ return float('nan')
+ else:
+ raise NotEncodable
+
+ @staticmethod
+ def encode_as_datetime(obj):
+ """Attempt to convert to utc-iso time string using datetime methods."""
+ # Since PY36, isoformat() converts UTC
+ # datetime.datetime objs to UTC T04:00:00
+ if not (PY36_OR_LATER and (isinstance(obj, datetime.datetime) and
+ obj.tzinfo is None)):
+ try:
+ obj = obj.astimezone(pytz.utc)
+ except ValueError:
+ # we'll get a value error if trying to convert with naive datetime
+ pass
+ except TypeError:
+ # pandas throws a typeerror here instead of a value error, it's OK
+ pass
+ except AttributeError:
+ # we'll get an attribute error if astimezone DNE
+ raise NotEncodable
+
+ # now we need to get a nicely formatted time string
+ try:
+ time_string = obj.isoformat()
+ except AttributeError:
+ raise NotEncodable
+ else:
+ return iso_to_plotly_time_string(time_string)
+
+ @staticmethod
+ def encode_as_date(obj):
+ """Attempt to convert to utc-iso time string using date methods."""
+ try:
+ time_string = obj.isoformat()
+ except AttributeError:
+ raise NotEncodable
+ else:
+ return iso_to_plotly_time_string(time_string)
+
+ @staticmethod
+ def encode_as_decimal(obj):
+ """Attempt to encode decimal by converting it to float"""
+ if isinstance(obj, decimal.Decimal):
+ return float(obj)
+ else:
+ raise NotEncodable
+
+
+class NotEncodable(Exception):
+ pass
+
+
+def iso_to_plotly_time_string(iso_string):
+ """Remove timezone info and replace 'T' delimeter with ' ' (ws)."""
+ # make sure we don't send timezone info to plotly
+ if (iso_string.split('-')[:3] is '00:00') or\
+ (iso_string.split('+')[0] is '00:00'):
+ raise Exception("Plotly won't accept timestrings with timezone info.\n"
+ "All timestrings are assumed to be in UTC.")
+
+ iso_string = iso_string.replace('-00:00', '').replace('+00:00', '')
+
+ if iso_string.endswith('T00:00:00'):
+ return iso_string.replace('T00:00:00', '')
+ else:
+ return iso_string.replace('T', ' ')
+
+
+def template_doc(**names):
+ def _decorator(func):
+ if sys.version[:3] != '3.2':
+ if func.__doc__ is not None:
+ func.__doc__ = func.__doc__.format(**names)
+ return func
+ return _decorator
diff --git a/chart_studio/__init__.py b/chart_studio/__init__.py
new file mode 100644
index 00000000000..c1adbe629b9
--- /dev/null
+++ b/chart_studio/__init__.py
@@ -0,0 +1,2 @@
+from __future__ import absolute_import
+from chart_studio import (plotly, dashboard_objs, grid_objs, session)
diff --git a/chart_studio/api/__init__.py b/chart_studio/api/__init__.py
new file mode 100644
index 00000000000..eb018c3ff09
--- /dev/null
+++ b/chart_studio/api/__init__.py
@@ -0,0 +1 @@
+from . import utils
diff --git a/chart_studio/api/utils.py b/chart_studio/api/utils.py
new file mode 100644
index 00000000000..d9d1d21f504
--- /dev/null
+++ b/chart_studio/api/utils.py
@@ -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()
+ )
diff --git a/chart_studio/api/v1/__init__.py b/chart_studio/api/v1/__init__.py
new file mode 100644
index 00000000000..05fbba4143a
--- /dev/null
+++ b/chart_studio/api/v1/__init__.py
@@ -0,0 +1,3 @@
+from __future__ import absolute_import
+
+from chart_studio.api.v1.clientresp import clientresp
diff --git a/plotly/api/v1/clientresp.py b/chart_studio/api/v1/clientresp.py
similarity index 83%
rename from plotly/api/v1/clientresp.py
rename to chart_studio/api/v1/clientresp.py
index d27fc5e8121..dd933e7fd9b 100644
--- a/plotly/api/v1/clientresp.py
+++ b/chart_studio/api/v1/clientresp.py
@@ -5,8 +5,10 @@
from requests.compat import json as _json
-from plotly import config, utils, version
-from plotly.api.v1.utils import request
+
+from _plotly_utils.utils import PlotlyJSONEncoder
+from chart_studio import config, utils
+from chart_studio.api.v1.utils import request
def clientresp(data, **kwargs):
@@ -19,10 +21,12 @@ def clientresp(data, **kwargs):
:param (list) data: The data array from a figure.
"""
+ from plotly import version
+
creds = config.get_credentials()
cfg = config.get_config()
- dumps_kwargs = {'sort_keys': True, 'cls': utils.PlotlyJSONEncoder}
+ dumps_kwargs = {'sort_keys': True, 'cls': PlotlyJSONEncoder}
payload = {
'platform': 'python', 'version': version.stable_semver(),
diff --git a/chart_studio/api/v1/utils.py b/chart_studio/api/v1/utils.py
new file mode 100644
index 00000000000..d0c40263a17
--- /dev/null
+++ b/chart_studio/api/v1/utils.py
@@ -0,0 +1,93 @@
+from __future__ import absolute_import
+
+import requests
+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 chart_studio.api.v2.utils import should_retry
+
+
+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
+
+
+@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 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 chart_studio.api.v2.utils.request for examples on how to do this.
+ raise _plotly_utils.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
diff --git a/chart_studio/api/v2/__init__.py b/chart_studio/api/v2/__init__.py
new file mode 100644
index 00000000000..c248f72543d
--- /dev/null
+++ b/chart_studio/api/v2/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import absolute_import
+
+from chart_studio.api.v2 import (dash_apps, dashboards, files, folders, grids,
+ images, plot_schema, plots,
+ spectacle_presentations, users)
diff --git a/plotly/api/v2/dash_apps.py b/chart_studio/api/v2/dash_apps.py
similarity index 90%
rename from plotly/api/v2/dash_apps.py
rename to chart_studio/api/v2/dash_apps.py
index e38848b53ae..c46ec3ff69e 100644
--- a/plotly/api/v2/dash_apps.py
+++ b/chart_studio/api/v2/dash_apps.py
@@ -3,7 +3,7 @@
"""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, request
+from chart_studio.api.v2.utils import build_url, request
RESOURCE = 'dash-apps'
diff --git a/plotly/api/v2/dashboards.py b/chart_studio/api/v2/dashboards.py
similarity index 93%
rename from plotly/api/v2/dashboards.py
rename to chart_studio/api/v2/dashboards.py
index c9aecf3e4a5..60c4e0dd898 100644
--- a/plotly/api/v2/dashboards.py
+++ b/chart_studio/api/v2/dashboards.py
@@ -6,7 +6,7 @@
"""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, request
+from chart_studio.api.v2.utils import build_url, request
RESOURCE = 'dashboards'
diff --git a/chart_studio/api/v2/files.py b/chart_studio/api/v2/files.py
new file mode 100644
index 00000000000..1e250158f66
--- /dev/null
+++ b/chart_studio/api/v2/files.py
@@ -0,0 +1,85 @@
+"""Interface to Plotly's /v2/files endpoints."""
+from __future__ import absolute_import
+
+from chart_studio.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)
diff --git a/plotly/api/v2/folders.py b/chart_studio/api/v2/folders.py
similarity index 97%
rename from plotly/api/v2/folders.py
rename to chart_studio/api/v2/folders.py
index 2dcf84670e7..81d72466ca1 100644
--- a/plotly/api/v2/folders.py
+++ b/chart_studio/api/v2/folders.py
@@ -1,7 +1,7 @@
"""Interface to Plotly's /v2/folders endpoints."""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, make_params, request
+from chart_studio.api.v2.utils import build_url, make_params, request
RESOURCE = 'folders'
diff --git a/plotly/api/v2/grids.py b/chart_studio/api/v2/grids.py
similarity index 98%
rename from plotly/api/v2/grids.py
rename to chart_studio/api/v2/grids.py
index 144ec3bd23f..726419a9b3d 100644
--- a/plotly/api/v2/grids.py
+++ b/chart_studio/api/v2/grids.py
@@ -1,7 +1,7 @@
"""Interface to Plotly's /v2/grids endpoints."""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, make_params, request
+from chart_studio.api.v2.utils import build_url, make_params, request
RESOURCE = 'grids'
diff --git a/plotly/api/v2/images.py b/chart_studio/api/v2/images.py
similarity index 88%
rename from plotly/api/v2/images.py
rename to chart_studio/api/v2/images.py
index 4c9d1816081..c6f7ea1a781 100644
--- a/plotly/api/v2/images.py
+++ b/chart_studio/api/v2/images.py
@@ -1,7 +1,7 @@
"""Interface to Plotly's /v2/images endpoints."""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, request
+from chart_studio.api.v2.utils import build_url, request
RESOURCE = 'images'
diff --git a/plotly/api/v2/plot_schema.py b/chart_studio/api/v2/plot_schema.py
similarity index 88%
rename from plotly/api/v2/plot_schema.py
rename to chart_studio/api/v2/plot_schema.py
index 4edbc0a707b..9b9a7ea7edf 100644
--- a/plotly/api/v2/plot_schema.py
+++ b/chart_studio/api/v2/plot_schema.py
@@ -1,7 +1,7 @@
"""Interface to Plotly's /v2/plot-schema endpoints."""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, make_params, request
+from chart_studio.api.v2.utils import build_url, make_params, request
RESOURCE = 'plot-schema'
diff --git a/plotly/api/v2/plots.py b/chart_studio/api/v2/plots.py
similarity index 98%
rename from plotly/api/v2/plots.py
rename to chart_studio/api/v2/plots.py
index da9f2d9e395..d33c01b7068 100644
--- a/plotly/api/v2/plots.py
+++ b/chart_studio/api/v2/plots.py
@@ -1,7 +1,7 @@
"""Interface to Plotly's /v2/plots endpoints."""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, make_params, request
+from chart_studio.api.v2.utils import build_url, make_params, request
RESOURCE = 'plots'
diff --git a/plotly/api/v2/spectacle_presentations.py b/chart_studio/api/v2/spectacle_presentations.py
similarity index 92%
rename from plotly/api/v2/spectacle_presentations.py
rename to chart_studio/api/v2/spectacle_presentations.py
index e34cf972bd1..343809d4586 100644
--- a/plotly/api/v2/spectacle_presentations.py
+++ b/chart_studio/api/v2/spectacle_presentations.py
@@ -3,7 +3,7 @@
"""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, request
+from chart_studio.api.v2.utils import build_url, request
RESOURCE = 'spectacle-presentations'
diff --git a/plotly/api/v2/users.py b/chart_studio/api/v2/users.py
similarity index 86%
rename from plotly/api/v2/users.py
rename to chart_studio/api/v2/users.py
index cdfaf51c488..b9300c2107e 100644
--- a/plotly/api/v2/users.py
+++ b/chart_studio/api/v2/users.py
@@ -1,7 +1,7 @@
"""Interface to Plotly's /v2/files endpoints."""
from __future__ import absolute_import
-from plotly.api.v2.utils import build_url, request
+from chart_studio.api.v2.utils import build_url, request
RESOURCE = 'users'
diff --git a/chart_studio/api/v2/utils.py b/chart_studio/api/v2/utils.py
new file mode 100644
index 00000000000..ec9a4201b39
--- /dev/null
+++ b/chart_studio/api/v2/utils.py
@@ -0,0 +1,173 @@
+from __future__ import absolute_import
+
+import requests
+from requests.compat 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
diff --git a/chart_studio/config.py b/chart_studio/config.py
new file mode 100644
index 00000000000..5cb2b30ad48
--- /dev/null
+++ b/chart_studio/config.py
@@ -0,0 +1,35 @@
+"""
+Merges and prioritizes file/session config and credentials.
+
+This is promoted to its own module to simplify imports.
+
+"""
+from __future__ import absolute_import
+
+from chart_studio import session, tools
+
+
+def get_credentials():
+ """Returns the credentials that will be sent to plotly."""
+ credentials = tools.get_credentials_file()
+ session_credentials = session.get_session_credentials()
+ for credentials_key in credentials:
+
+ # checking for not false, but truthy value here is the desired behavior
+ session_value = session_credentials.get(credentials_key)
+ if session_value is False or session_value:
+ credentials[credentials_key] = session_value
+ return credentials
+
+
+def get_config():
+ """Returns either module config or file config."""
+ config = tools.get_config_file()
+ session_config = session.get_session_config()
+ for config_key in config:
+
+ # checking for not false, but truthy value here is the desired behavior
+ session_value = session_config.get(config_key)
+ if session_value is False or session_value:
+ config[config_key] = session_value
+ return config
diff --git a/plotly/dashboard_objs/__init__.py b/chart_studio/dashboard_objs/__init__.py
similarity index 100%
rename from plotly/dashboard_objs/__init__.py
rename to chart_studio/dashboard_objs/__init__.py
diff --git a/chart_studio/dashboard_objs/dashboard_objs.py b/chart_studio/dashboard_objs/dashboard_objs.py
new file mode 100644
index 00000000000..13ef7032e00
--- /dev/null
+++ b/chart_studio/dashboard_objs/dashboard_objs.py
@@ -0,0 +1,629 @@
+"""
+dashboard_objs
+==========
+
+A module for creating and manipulating dashboard content. You can create
+a Dashboard object, insert boxes, swap boxes, remove a box and get an HTML
+preview of the Dashboard.
+```
+"""
+
+import pprint
+
+import _plotly_utils.exceptions
+from _plotly_utils import optional_imports
+from chart_studio import exceptions
+
+IPython = optional_imports.get_module('IPython')
+
+# default parameters for HTML preview
+MASTER_WIDTH = 500
+MASTER_HEIGHT = 500
+FONT_SIZE = 9
+
+
+ID_NOT_VALID_MESSAGE = (
+ "Your box_id must be a number in your dashboard. To view a "
+ "representation of your dashboard run get_preview()."
+)
+
+
+def _empty_box():
+ empty_box = {
+ 'type': 'box',
+ 'boxType': 'empty'
+ }
+ return empty_box
+
+
+def _box(fileId='', shareKey=None, title=''):
+ box = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': fileId,
+ 'shareKey': shareKey,
+ 'title': title
+ }
+ return box
+
+def _container(box_1=None, box_2=None,
+ size=50, sizeUnit='%',
+ direction='vertical'):
+ if box_1 is None:
+ box_1 = _empty_box()
+ if box_2 is None:
+ box_2 = _empty_box()
+
+ container = {
+ 'type': 'split',
+ 'size': size,
+ 'sizeUnit': sizeUnit,
+ 'direction': direction,
+ 'first': box_1,
+ 'second': box_2
+ }
+
+ return container
+
+dashboard_html = ("""
+
+
+
+
+
+
+
+
+
+
+""".format(width=MASTER_WIDTH, height=MASTER_HEIGHT))
+
+
+def _draw_line_through_box(dashboard_html, top_left_x, top_left_y, box_w,
+ box_h, is_horizontal, direction, fill_percent=50):
+ if is_horizontal:
+ new_top_left_x = top_left_x + box_w * (fill_percent / 100.)
+ new_top_left_y = top_left_y
+ new_box_w = 1
+ new_box_h = box_h
+ else:
+ new_top_left_x = top_left_x
+ new_top_left_y = top_left_y + box_h * (fill_percent / 100.)
+ new_box_w = box_w
+ new_box_h = 1
+
+ html_box = """
+ context.beginPath();
+ context.rect({top_left_x}, {top_left_y}, {box_w}, {box_h});
+ context.lineWidth = 1;
+ context.strokeStyle = 'black';
+ context.stroke();
+ """.format(top_left_x=new_top_left_x, top_left_y=new_top_left_y,
+ box_w=new_box_w, box_h=new_box_h)
+
+ index_for_new_box = dashboard_html.find('') - 1
+ dashboard_html = (dashboard_html[:index_for_new_box] + html_box +
+ dashboard_html[index_for_new_box:])
+ return dashboard_html
+
+
+def _add_html_text(dashboard_html, text, top_left_x, top_left_y, box_w,
+ box_h):
+ html_text = """
+ context.font = '{}pt Times New Roman';
+ context.textAlign = 'center';
+ context.fillText({}, {} + 0.5*{}, {} + 0.5*{});
+ """.format(FONT_SIZE, text, top_left_x, box_w, top_left_y, box_h)
+
+ index_to_add_text = dashboard_html.find('') - 1
+ dashboard_html = (dashboard_html[:index_to_add_text] + html_text +
+ dashboard_html[index_to_add_text:])
+ return dashboard_html
+
+
+class Dashboard(dict):
+ """
+ Dashboard class for creating interactive dashboard objects.
+
+ Dashboards are dicts that contain boxes which hold plot information.
+ These boxes can be arranged in various ways. The most basic form of
+ a box is:
+
+ ```
+ {
+ 'type': 'box',
+ 'boxType': 'plot'
+ }
+ ```
+
+ where 'fileId' can be set to the 'username:#' of your plot. The other
+ parameters a box takes are `shareKey` (default is None) and `title`
+ (default is '').
+
+ `.get_preview()` should be called quite regularly to get an HTML
+ representation of the dashboard in which the boxes in the HTML
+ are labelled with on-the-fly-generated numbers or box ids which
+ change after each modification to the dashboard.
+
+ `.get_box()` returns the box located in the dashboard by calling
+ its box id as displayed via `.get_preview()`.
+
+ Example 1: Create a simple Dashboard object
+ ```
+ import plotly.dashboard_objs as dashboard
+
+ box_a = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:some#',
+ 'title': 'box a'
+ }
+
+ box_b = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:some#',
+ 'title': 'box b'
+ }
+
+ box_c = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:some#',
+ 'title': 'box c'
+ }
+
+ my_dboard = dashboard.Dashboard()
+ my_dboard.insert(box_a)
+ # my_dboard.get_preview()
+ my_dboard.insert(box_b, 'above', 1)
+ # my_dboard.get_preview()
+ my_dboard.insert(box_c, 'left', 2)
+ # my_dboard.get_preview()
+ my_dboard.swap(1, 2)
+ # my_dboard.get_preview()
+ my_dboard.remove(1)
+ # my_dboard.get_preview()
+ ```
+
+ Example 2: 4 vertical boxes of equal height
+ ```
+ import plotly.dashboard_objs as dashboard
+
+ box_a = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:some#',
+ 'title': 'box a'
+ }
+
+ my_dboard = dashboard.Dashboard()
+ my_dboard.insert(box_a)
+ my_dboard.insert(box_a, 'below', 1)
+ my_dboard.insert(box_a, 'below', 1)
+ my_dboard.insert(box_a, 'below', 3)
+ # my_dboard.get_preview()
+ ```
+ """
+ def __init__(self, content=None):
+ if content is None:
+ content = {}
+
+ if not content:
+ self['layout'] = None
+ self['version'] = 2
+ self['settings'] = {}
+ else:
+ self['layout'] = content['layout']
+ self['version'] = content['version']
+ self['settings'] = content['settings']
+
+ def _compute_box_ids(self):
+ from plotly.utils import node_generator
+
+ box_ids_to_path = {}
+ all_nodes = list(node_generator(self['layout']))
+ all_nodes.sort(key=lambda x: x[1])
+ for node in all_nodes:
+ if (node[1] != () and node[0]['type'] == 'box'
+ and node[0]['boxType'] != 'empty'):
+ try:
+ max_id = max(box_ids_to_path.keys())
+ except ValueError:
+ max_id = 0
+ box_ids_to_path[max_id + 1] = node[1]
+
+ return box_ids_to_path
+
+ def _insert(self, box_or_container, path):
+ if any(first_second not in ['first', 'second']
+ for first_second in path):
+ raise _plotly_utils.exceptions.PlotlyError(
+ "Invalid path. Your 'path' list must only contain "
+ "the strings 'first' and 'second'."
+ )
+
+ if 'first' in self['layout']:
+ loc_in_dashboard = self['layout']
+ for index, first_second in enumerate(path):
+ if index != len(path) - 1:
+ loc_in_dashboard = loc_in_dashboard[first_second]
+ else:
+ loc_in_dashboard[first_second] = box_or_container
+
+ else:
+ self['layout'] = box_or_container
+
+ def _make_all_nodes_and_paths(self):
+ from plotly.utils import node_generator
+
+ all_nodes = list(node_generator(self['layout']))
+ all_nodes.sort(key=lambda x: x[1])
+
+ # remove path 'second' as it's always an empty box
+ all_paths = []
+ for node in all_nodes:
+ all_paths.append(node[1])
+ path_second = ('second',)
+ if path_second in all_paths:
+ all_paths.remove(path_second)
+ return all_nodes, all_paths
+
+ def _path_to_box(self, path):
+ loc_in_dashboard = self['layout']
+ for first_second in path:
+ loc_in_dashboard = loc_in_dashboard[first_second]
+ return loc_in_dashboard
+
+ def _set_dashboard_size(self):
+ # set dashboard size to keep consistent with GUI
+ num_of_boxes = len(self._compute_box_ids())
+ if num_of_boxes == 0:
+ pass
+ elif num_of_boxes == 1:
+ self['layout']['size'] = 800
+ self['layout']['sizeUnit'] = 'px'
+ elif num_of_boxes == 2:
+ self['layout']['size'] = 1500
+ self['layout']['sizeUnit'] = 'px'
+ else:
+ self['layout']['size'] = 1500 + 350 * (num_of_boxes - 2)
+ self['layout']['sizeUnit'] = 'px'
+
+ def get_box(self, box_id):
+ """Returns box from box_id number."""
+ box_ids_to_path = self._compute_box_ids()
+ loc_in_dashboard = self['layout']
+
+ if box_id not in box_ids_to_path.keys():
+ raise _plotly_utils.exceptions.PlotlyError(ID_NOT_VALID_MESSAGE)
+ for first_second in box_ids_to_path[box_id]:
+ loc_in_dashboard = loc_in_dashboard[first_second]
+ return loc_in_dashboard
+
+ def get_preview(self):
+ """
+ Returns JSON or HTML respresentation of the dashboard.
+
+ If IPython is not imported, returns a pretty print of the dashboard
+ dict. Otherwise, returns an IPython.core.display.HTML display of the
+ dashboard.
+
+ The algorithm used to build the HTML preview involves going through
+ the paths of the node generator of the dashboard. The paths of the
+ dashboard are sequenced through from shorter to longer and whether
+ it's a box or container that lies at the end of the path determines
+ the action.
+
+ If it's a container, draw a line in the figure to divide the current
+ box into two and store the specs of the resulting two boxes. If the
+ path points to a terminal box (often containing a plot), then draw
+ the box id in the center of the box.
+
+ It's important to note that these box ids are generated on-the-fly and
+ they do not necessarily stay assigned to the boxes they were once
+ assigned to.
+ """
+ if IPython is None:
+ pprint.pprint(self)
+ return
+
+ elif self['layout'] is None:
+ return IPython.display.HTML(dashboard_html)
+
+ top_left_x = 0
+ top_left_y = 0
+ box_w = MASTER_WIDTH
+ box_h = MASTER_HEIGHT
+ html_figure = dashboard_html
+ box_ids_to_path = self._compute_box_ids()
+ # used to store info about box dimensions
+ path_to_box_specs = {}
+ first_box_specs = {
+ 'top_left_x': top_left_x,
+ 'top_left_y': top_left_y,
+ 'box_w': box_w,
+ 'box_h': box_h
+ }
+ # uses tuples to store paths as for hashable keys
+ path_to_box_specs[('first',)] = first_box_specs
+
+ # generate all paths
+ all_nodes, all_paths = self._make_all_nodes_and_paths()
+
+ max_path_len = max(len(path) for path in all_paths)
+ for path_len in range(1, max_path_len + 1):
+ for path in [path for path in all_paths if len(path) == path_len]:
+ current_box_specs = path_to_box_specs[path]
+
+ if self._path_to_box(path)['type'] == 'split':
+ fill_percent = self._path_to_box(path)['size']
+ direction = self._path_to_box(path)['direction']
+ is_horizontal = (direction == 'horizontal')
+
+ top_left_x = current_box_specs['top_left_x']
+ top_left_y = current_box_specs['top_left_y']
+ box_w = current_box_specs['box_w']
+ box_h = current_box_specs['box_h']
+
+ html_figure = _draw_line_through_box(
+ html_figure, top_left_x, top_left_y, box_w, box_h,
+ is_horizontal=is_horizontal, direction=direction,
+ fill_percent=fill_percent
+ )
+
+ # determine the specs for resulting two box split
+ if is_horizontal:
+ new_top_left_x = top_left_x
+ new_top_left_y = top_left_y
+ new_box_w = box_w * (fill_percent / 100.)
+ new_box_h = box_h
+
+ new_top_left_x_2 = top_left_x + new_box_w
+ new_top_left_y_2 = top_left_y
+ new_box_w_2 = box_w * ((100 - fill_percent) / 100.)
+ new_box_h_2 = box_h
+ else:
+ new_top_left_x = top_left_x
+ new_top_left_y = top_left_y
+ new_box_w = box_w
+ new_box_h = box_h * (fill_percent / 100.)
+
+ new_top_left_x_2 = top_left_x
+ new_top_left_y_2 = (top_left_y +
+ box_h * (fill_percent / 100.))
+ new_box_w_2 = box_w
+ new_box_h_2 = box_h * ((100 - fill_percent) / 100.)
+
+ first_box_specs = {
+ 'top_left_x': top_left_x,
+ 'top_left_y': top_left_y,
+ 'box_w': new_box_w,
+ 'box_h': new_box_h
+ }
+ second_box_specs = {
+ 'top_left_x': new_top_left_x_2,
+ 'top_left_y': new_top_left_y_2,
+ 'box_w': new_box_w_2,
+ 'box_h': new_box_h_2
+ }
+
+ path_to_box_specs[path + ('first',)] = first_box_specs
+ path_to_box_specs[path + ('second',)] = second_box_specs
+
+ elif self._path_to_box(path)['type'] == 'box':
+ for box_id in box_ids_to_path:
+ if box_ids_to_path[box_id] == path:
+ number = box_id
+ html_figure = _add_html_text(
+ html_figure, number,
+ path_to_box_specs[path]['top_left_x'],
+ path_to_box_specs[path]['top_left_y'],
+ path_to_box_specs[path]['box_w'],
+ path_to_box_specs[path]['box_h'],
+ )
+
+ # display HTML representation
+ return IPython.display.HTML(html_figure)
+
+ def insert(self, box, side='above', box_id=None, fill_percent=50):
+ """
+ Insert a box into your dashboard layout.
+
+ :param (dict) box: the box you are inserting into the dashboard.
+ :param (str) side: specifies where your new box is going to be placed
+ relative to the given 'box_id'. Valid values are 'above', 'below',
+ 'left', and 'right'.
+ :param (int) box_id: the box id which is used as a reference for the
+ insertion of the new box. Box ids are memoryless numbers that are
+ generated on-the-fly and assigned to boxes in the layout each time
+ .get_preview() is run.
+ :param (float) fill_percent: specifies the percentage of the container
+ box from the given 'side' that the new box occupies. For example
+ if you apply the method\n
+ .insert(box=new_box, box_id=2, side='left', fill_percent=20)\n
+ to a dashboard object, a new box is inserted 20% from the left
+ side of the box with id #2. Run .get_preview() to see the box ids
+ assigned to each box in the dashboard layout.
+ Default = 50
+ Example:
+ ```
+ import plotly.dashboard_objs as dashboard
+
+ box_a = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:some#',
+ 'title': 'box a'
+ }
+
+ my_dboard = dashboard.Dashboard()
+ my_dboard.insert(box_a)
+ my_dboard.insert(box_a, 'left', 1)
+ my_dboard.insert(box_a, 'below', 2)
+ my_dboard.insert(box_a, 'right', 3)
+ my_dboard.insert(box_a, 'above', 4, fill_percent=20)
+
+ my_dboard.get_preview()
+ ```
+ """
+ box_ids_to_path = self._compute_box_ids()
+
+ # doesn't need box_id or side specified for first box
+ if self['layout'] is None:
+ self['layout'] = _container(
+ box, _empty_box(), size=MASTER_HEIGHT, sizeUnit='px'
+ )
+ else:
+ if box_id is None:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "Make sure the box_id is specfied if there is at least "
+ "one box in your dashboard."
+ )
+ if box_id not in box_ids_to_path:
+ raise _plotly_utils.exceptions.PlotlyError(ID_NOT_VALID_MESSAGE)
+
+ if fill_percent < 0 or fill_percent > 100:
+ raise _plotly_utils.exceptions.PlotlyError(
+ 'fill_percent must be a number between 0 and 100 '
+ 'inclusive'
+ )
+ if side == 'above':
+ old_box = self.get_box(box_id)
+ self._insert(
+ _container(box, old_box, direction='vertical',
+ size=fill_percent),
+ box_ids_to_path[box_id]
+ )
+ elif side == 'below':
+ old_box = self.get_box(box_id)
+ self._insert(
+ _container(old_box, box, direction='vertical',
+ size=100 - fill_percent),
+ box_ids_to_path[box_id]
+ )
+ elif side == 'left':
+ old_box = self.get_box(box_id)
+ self._insert(
+ _container(box, old_box, direction='horizontal',
+ size=fill_percent),
+ box_ids_to_path[box_id]
+ )
+ elif side == 'right':
+ old_box = self.get_box(box_id)
+ self._insert(
+ _container(old_box, box, direction='horizontal',
+ size =100 - fill_percent),
+ box_ids_to_path[box_id]
+ )
+ else:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "If there is at least one box in your dashboard, you "
+ "must specify a valid side value. You must choose from "
+ "'above', 'below', 'left', and 'right'."
+ )
+
+ self._set_dashboard_size()
+
+ def remove(self, box_id):
+ """
+ Remove a box from the dashboard by its box_id.
+
+ Example:
+ ```
+ import plotly.dashboard_objs as dashboard
+
+ box_a = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:some#',
+ 'title': 'box a'
+ }
+
+ my_dboard = dashboard.Dashboard()
+ my_dboard.insert(box_a)
+ my_dboard.remove(1)
+ my_dboard.get_preview()
+ ```
+ """
+ box_ids_to_path = self._compute_box_ids()
+ if box_id not in box_ids_to_path:
+ raise _plotly_utils.exceptions.PlotlyError(ID_NOT_VALID_MESSAGE)
+
+ path = box_ids_to_path[box_id]
+ if path != ('first',):
+ container_for_box_id = self._path_to_box(path[:-1])
+ if path[-1] == 'first':
+ adjacent_path = 'second'
+ elif path[-1] == 'second':
+ adjacent_path = 'first'
+ adjacent_box = container_for_box_id[adjacent_path]
+
+ self._insert(adjacent_box, path[:-1])
+ else:
+ self['layout'] = None
+
+ self._set_dashboard_size()
+
+ def swap(self, box_id_1, box_id_2):
+ """
+ Swap two boxes with their specified ids.
+
+ Example:
+ ```
+ import plotly.dashboard_objs as dashboard
+
+ box_a = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:first#',
+ 'title': 'box a'
+ }
+
+ box_b = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'username:second#',
+ 'title': 'box b'
+ }
+
+ my_dboard = dashboard.Dashboard()
+ my_dboard.insert(box_a)
+ my_dboard.insert(box_b, 'above', 1)
+
+ # check box at box id 1
+ box_at_1 = my_dboard.get_box(1)
+ print(box_at_1)
+
+ my_dboard.swap(1, 2)
+
+ box_after_swap = my_dboard.get_box(1)
+ print(box_after_swap)
+ ```
+ """
+ box_ids_to_path = self._compute_box_ids()
+ box_a = self.get_box(box_id_1)
+ box_b = self.get_box(box_id_2)
+
+ box_a_path = box_ids_to_path[box_id_1]
+ box_b_path = box_ids_to_path[box_id_2]
+
+ for pairs in [(box_a_path, box_b), (box_b_path, box_a)]:
+ loc_in_dashboard = self['layout']
+ for first_second in pairs[0][:-1]:
+ loc_in_dashboard = loc_in_dashboard[first_second]
+ loc_in_dashboard[pairs[0][-1]] = pairs[1]
diff --git a/chart_studio/exceptions.py b/chart_studio/exceptions.py
new file mode 100644
index 00000000000..fa5054b86b3
--- /dev/null
+++ b/chart_studio/exceptions.py
@@ -0,0 +1,91 @@
+"""
+exceptions
+==========
+
+A module that contains plotly's exception hierarchy.
+
+"""
+from __future__ import absolute_import
+
+from chart_studio.api.utils import to_native_utf8_string
+
+
+# Base Plotly Error
+from _plotly_utils.exceptions import PlotlyError
+
+
+class InputError(PlotlyError):
+ pass
+
+
+class PlotlyRequestError(PlotlyError):
+ """General API error. Raised for *all* failed requests."""
+
+ def __init__(self, message, status_code, content):
+ self.message = to_native_utf8_string(message)
+ self.status_code = status_code
+ self.content = content
+
+ def __str__(self):
+ return self.message
+
+
+# Grid Errors
+COLUMN_NOT_YET_UPLOADED_MESSAGE = (
+ "Hm... it looks like your column '{column_name}' hasn't "
+ "been uploaded to Plotly yet. You need to upload your "
+ "column to Plotly before you can assign it to '{reference}'.\n"
+ "To upload, try `plotly.plotly.grid_objs.upload` or "
+ "`plotly.plotly.grid_objs.append_column`.\n"
+ "Questions? chris@plot.ly"
+)
+
+NON_UNIQUE_COLUMN_MESSAGE = (
+ "Yikes, plotly grids currently "
+ "can't have duplicate column names. Rename "
+ "the column \"{0}\" and try again."
+)
+
+# Local Config Errors
+class PlotlyLocalError(PlotlyError):
+ pass
+
+
+class PlotlyLocalCredentialsError(PlotlyLocalError):
+ def __init__(self):
+ message = (
+ "\n"
+ "Couldn't find a 'username', 'api-key' pair for you on your local "
+ "machine. To sign in temporarily (until you stop running Python), "
+ "run:\n"
+ ">>> import plotly.plotly as py\n"
+ ">>> py.sign_in('username', 'api_key')\n\n"
+ "Even better, save your credentials permanently using the 'tools' "
+ "module:\n"
+ ">>> import plotly.tools as tls\n"
+ ">>> tls.set_credentials_file(username='username', "
+ "api_key='api-key')\n\n"
+ "For more help, see https://plot.ly/python.\n"
+ )
+ super(PlotlyLocalCredentialsError, self).__init__(message)
+
+
+# Server Errors
+class PlotlyServerError(PlotlyError):
+ pass
+
+
+class PlotlyConnectionError(PlotlyServerError):
+ pass
+
+
+class PlotlyCredentialError(PlotlyServerError):
+ pass
+
+
+class PlotlyAccountError(PlotlyServerError):
+ pass
+
+
+class PlotlyRateLimitError(PlotlyServerError):
+ pass
diff --git a/chart_studio/files.py b/chart_studio/files.py
new file mode 100644
index 00000000000..19fe98ea5cf
--- /dev/null
+++ b/chart_studio/files.py
@@ -0,0 +1,24 @@
+from __future__ import absolute_import
+
+import os
+
+# file structure
+from _plotly_utils.files import PLOTLY_DIR
+
+CREDENTIALS_FILE = os.path.join(PLOTLY_DIR, ".credentials")
+CONFIG_FILE = os.path.join(PLOTLY_DIR, ".config")
+
+# this sets both the DEFAULTS and the TYPES for these files
+FILE_CONTENT = {CREDENTIALS_FILE: {'username': '',
+ 'api_key': '',
+ 'proxy_username': '',
+ 'proxy_password': '',
+ 'stream_ids': []},
+ CONFIG_FILE: {'plotly_domain': 'https://plot.ly',
+ 'plotly_streaming_domain': 'stream.plot.ly',
+ 'plotly_api_domain': 'https://api.plot.ly',
+ 'plotly_ssl_verification': True,
+ 'plotly_proxy_authorization': False,
+ 'world_readable': True,
+ 'sharing': 'public',
+ 'auto_open': True}}
diff --git a/chart_studio/grid_objs/__init__.py b/chart_studio/grid_objs/__init__.py
new file mode 100644
index 00000000000..ae484f25e17
--- /dev/null
+++ b/chart_studio/grid_objs/__init__.py
@@ -0,0 +1,8 @@
+""""
+grid_objs
+=========
+
+"""
+from __future__ import absolute_import
+
+from chart_studio.grid_objs.grid_objs import Grid, Column
diff --git a/chart_studio/grid_objs/grid_objs.py b/chart_studio/grid_objs/grid_objs.py
new file mode 100644
index 00000000000..414d9800d2e
--- /dev/null
+++ b/chart_studio/grid_objs/grid_objs.py
@@ -0,0 +1,299 @@
+"""
+grid_objs
+=========
+
+"""
+from __future__ import absolute_import
+
+import _plotly_utils.exceptions
+
+try:
+ from collections.abc import MutableSequence
+except ImportError:
+ from collections import MutableSequence
+
+from requests.compat import json as _json
+
+from _plotly_utils.optional_imports import get_module
+from chart_studio import utils, exceptions
+
+__all__ = None
+
+
+class Column(object):
+ """
+ Columns make up Plotly Grids and can be the source of
+ data for Plotly Graphs.
+ They have a name and an array of data.
+ They can be uploaded to Plotly with the `plotly.plotly.grid_ops`
+ class.
+
+ Usage example 1: Upload a set of columns as a grid to Plotly
+ ```
+ from plotly.grid_objs import Grid, Column
+ import plotly.plotly as py
+ column_1 = Column([1, 2, 3], 'time')
+ column_2 = Column([4, 2, 5], 'voltage')
+ grid = Grid([column_1, column_2])
+ py.grid_ops.upload(grid, 'time vs voltage')
+ ```
+
+ Usage example 2: Make a graph based with data that is sourced
+ from a newly uploaded Plotly columns
+ ```
+ import plotly.plotly as py
+ from plotly.grid_objs import Grid, Column
+ from plotly.graph_objs import Scatter
+ # Upload a grid
+ column_1 = Column([1, 2, 3], 'time')
+ column_2 = Column([4, 2, 5], 'voltage')
+ grid = Grid([column_1, column_2])
+ py.grid_ops.upload(grid, 'time vs voltage')
+
+ # Build a Plotly graph object sourced from the
+ # grid's columns
+ trace = Scatter(xsrc=grid[0], ysrc=grid[1])
+ py.plot([trace], filename='graph from grid')
+ ```
+ """
+ def __init__(self, data, name):
+ """
+ Initialize a Plotly column with `data` and `name`.
+ `data` is an array of strings, numbers, or dates.
+ `name` is the name of the column as it will apppear
+ in the Plotly grid. Names must be unique to a grid.
+ """
+
+ # TODO: data type checking
+ self.data = data
+ # TODO: name type checking
+ self.name = name
+
+ self.id = ''
+
+ def __str__(self):
+ max_chars = 10
+ jdata = _json.dumps(self.data, cls=utils.PlotlyJSONEncoder)
+ if len(jdata) > max_chars:
+ data_string = jdata[:max_chars] + "...]"
+ else:
+ data_string = jdata
+ string = ''
+ return string.format(name=self.name, data=data_string, id=self.id)
+
+ def __repr__(self):
+ return 'Column("{0}", {1})'.format(self.data, self.name)
+
+ def to_plotly_json(self):
+ return {'name': self.name, 'data': self.data}
+
+
+class Grid(MutableSequence):
+ """
+ Grid is Plotly's Python representation of Plotly Grids.
+ Plotly Grids are tabular data made up of columns. They can be
+ uploaded, appended to, and can source the data for Plotly
+ graphs.
+
+ A plotly.grid_objs.Grid object is essentially a list.
+
+ Usage example 1: Upload a set of columns as a grid to Plotly
+ ```
+ from plotly.grid_objs import Grid, Column
+ import plotly.plotly as py
+ column_1 = Column([1, 2, 3], 'time')
+ column_2 = Column([4, 2, 5], 'voltage')
+ grid = Grid([column_1, column_2])
+ py.grid_ops.upload(grid, 'time vs voltage')
+ ```
+
+ Usage example 2: Make a graph based with data that is sourced
+ from a newly uploaded Plotly columns
+ ```
+ import plotly.plotly as py
+ from plotly.grid_objs import Grid, Column
+ from plotly.graph_objs import Scatter
+ # Upload a grid
+ column_1 = Column([1, 2, 3], 'time')
+ column_2 = Column([4, 2, 5], 'voltage')
+ grid = Grid([column_1, column_2])
+ py.grid_ops.upload(grid, 'time vs voltage')
+
+ # Build a Plotly graph object sourced from the
+ # grid's columns
+ trace = Scatter(xsrc=grid[0], ysrc=grid[1])
+ py.plot([trace], filename='graph from grid')
+ ```
+ """
+ def __init__(self, columns_or_json, fid=None):
+ """
+ Initialize a grid with an iterable of `plotly.grid_objs.Column`
+ objects or a json/dict describing a grid. See second usage example
+ below for the necessary structure of the dict.
+
+ :param (str|bool) fid: should not be accessible to users. Default
+ is 'None' but if a grid is retrieved via `py.get_grid()` then the
+ retrieved grid response will contain the fid which will be
+ necessary to set `self.id` and `self._columns.id` below.
+
+ Example from iterable of columns:
+ ```
+ column_1 = Column([1, 2, 3], 'time')
+ column_2 = Column([4, 2, 5], 'voltage')
+ grid = Grid([column_1, column_2])
+ ```
+ Example from json grid
+ ```
+ grid_json = {
+ 'cols': {
+ 'time': {'data': [1, 2, 3], 'order': 0, 'uid': '4cd7fc'},
+ 'voltage': {'data': [4, 2, 5], 'order': 1, 'uid': u'2744be'}
+ }
+ }
+ grid = Grid(grid_json)
+ ```
+ """
+ # TODO: verify that columns are actually columns
+ pd = get_module('pandas')
+ if pd and isinstance(columns_or_json, pd.DataFrame):
+ duplicate_name = utils.get_first_duplicate(columns_or_json.columns)
+ if duplicate_name:
+ err = exceptions.NON_UNIQUE_COLUMN_MESSAGE.format(duplicate_name)
+ raise exceptions.InputError(err)
+
+ # create columns from dataframe
+ all_columns = []
+ for name in columns_or_json.columns:
+ all_columns.append(Column(columns_or_json[name].tolist(), name))
+ self._columns = all_columns
+ self.id = ''
+
+ elif isinstance(columns_or_json, dict):
+ # check that fid is entered
+ if fid is None:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "If you are manually converting a raw json/dict grid "
+ "into a Grid instance, you must ensure that 'fid' is "
+ "set to your file ID. This looks like 'username:187'."
+ )
+
+ self.id = fid
+
+ # check if 'cols' is a root key
+ if 'cols' not in columns_or_json:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "'cols' must be a root key in your json grid."
+ )
+
+ # check if 'data', 'order' and 'uid' are not in columns
+ grid_col_keys = ['data', 'order', 'uid']
+
+ for column_name in columns_or_json['cols']:
+ for key in grid_col_keys:
+ if key not in columns_or_json['cols'][column_name]:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "Each column name of your dictionary must have "
+ "'data', 'order' and 'uid' as keys."
+ )
+ # collect and sort all orders in case orders do not start
+ # at zero or there are jump discontinuities between them
+ all_orders = []
+ for column_name in columns_or_json['cols'].keys():
+ all_orders.append(columns_or_json['cols'][column_name]['order'])
+ all_orders.sort()
+
+ # put columns in order in a list
+ ordered_columns = []
+ for order in all_orders:
+ for column_name in columns_or_json['cols'].keys():
+ if columns_or_json['cols'][column_name]['order'] == order:
+ break
+
+ ordered_columns.append(Column(
+ columns_or_json['cols'][column_name]['data'],
+ column_name)
+ )
+ self._columns = ordered_columns
+
+ # fill in column_ids
+ for column in self:
+ column.id = self.id + ':' + columns_or_json['cols'][column.name]['uid']
+
+ else:
+ column_names = [column.name for column in columns_or_json]
+ duplicate_name = utils.get_first_duplicate(column_names)
+ if duplicate_name:
+ err = exceptions.NON_UNIQUE_COLUMN_MESSAGE.format(duplicate_name)
+ raise exceptions.InputError(err)
+
+ self._columns = list(columns_or_json)
+ self.id = ''
+
+ def __repr__(self):
+ return self._columns.__repr__()
+
+ def __getitem__(self, index):
+ return self._columns[index]
+
+ def __setitem__(self, index, column):
+ self._validate_insertion(column)
+ return self._columns.__setitem__(index, column)
+
+ def __delitem__(self, index):
+ del self._columns[index]
+
+ def __len__(self):
+ return len(self._columns)
+
+ def insert(self, index, column):
+ self._validate_insertion(column)
+ self._columns.insert(index, column)
+
+ def _validate_insertion(self, column):
+ """
+ Raise an error if we're gonna add a duplicate column name
+ """
+ existing_column_names = [col.name for col in self._columns]
+ if column.name in existing_column_names:
+ err = exceptions.NON_UNIQUE_COLUMN_MESSAGE.format(column.name)
+ raise exceptions.InputError(err)
+
+ def _to_plotly_grid_json(self):
+ grid_json = {'cols': {}}
+ for column_index, column in enumerate(self):
+ grid_json['cols'][column.name] = {
+ 'data': column.data,
+ 'order': column_index
+ }
+ return grid_json
+
+ def get_column(self, column_name):
+ """ Return the first column with name `column_name`.
+ If no column with `column_name` exists in this grid, return None.
+ """
+ for column in self._columns:
+ if column.name == column_name:
+ return column
+
+ def get_column_reference(self, column_name):
+ """
+ Returns the column reference of given column in the grid by its name.
+
+ Raises an error if the column name is not in the grid. Otherwise,
+ returns the fid:uid pair, which may be the empty string.
+ """
+ column_id = None
+ for column in self._columns:
+ if column.name == column_name:
+ column_id = column.id
+ break
+
+ if column_id is None:
+ col_names = []
+ for column in self._columns:
+ col_names.append(column.name)
+ raise _plotly_utils.exceptions.PlotlyError(
+ "Whoops, that column name doesn't match any of the column "
+ "names in your grid. You must pick from {cols}".format(cols=col_names)
+ )
+ return column_id
diff --git a/chart_studio/plotly/__init__.py b/chart_studio/plotly/__init__.py
new file mode 100644
index 00000000000..2415356cfc8
--- /dev/null
+++ b/chart_studio/plotly/__init__.py
@@ -0,0 +1,31 @@
+"""
+plotly
+======
+
+This module defines functionality that requires interaction between your
+local machine and Plotly. Almost all functionality used here will require a
+verifiable account (username/api-key pair) and a network connection.
+
+"""
+from . plotly import (
+ sign_in,
+ update_plot_options,
+ get_credentials,
+ iplot,
+ plot,
+ iplot_mpl,
+ plot_mpl,
+ get_figure,
+ Stream,
+ image,
+ grid_ops,
+ meta_ops,
+ file_ops,
+ get_config,
+ get_grid,
+ dashboard_ops,
+ presentation_ops,
+ create_animations,
+ icreate_animations,
+ parse_grid_id_args
+)
diff --git a/plotly/plotly/chunked_requests/__init__.py b/chart_studio/plotly/chunked_requests/__init__.py
similarity index 100%
rename from plotly/plotly/chunked_requests/__init__.py
rename to chart_studio/plotly/chunked_requests/__init__.py
diff --git a/plotly/plotly/chunked_requests/chunked_request.py b/chart_studio/plotly/chunked_requests/chunked_request.py
similarity index 99%
rename from plotly/plotly/chunked_requests/chunked_request.py
rename to chart_studio/plotly/chunked_requests/chunked_request.py
index 4f8d325edb3..5f39704c720 100644
--- a/plotly/plotly/chunked_requests/chunked_request.py
+++ b/chart_studio/plotly/chunked_requests/chunked_request.py
@@ -6,7 +6,7 @@
from six.moves import http_client
from six.moves.urllib.parse import urlparse, unquote
-from plotly.api import utils
+from chart_studio.api import utils
class Stream:
diff --git a/plotly/plotly/plotly.py b/chart_studio/plotly/plotly.py
similarity index 96%
rename from plotly/plotly/plotly.py
rename to chart_studio/plotly/plotly.py
index 1227a98651e..763c431985f 100644
--- a/plotly/plotly/plotly.py
+++ b/chart_studio/plotly/plotly.py
@@ -21,7 +21,6 @@
import json
import os
import time
-import uuid
import warnings
import webbrowser
@@ -29,20 +28,19 @@
import six.moves
from requests.compat import json as _json
+import _plotly_utils.utils
+import _plotly_utils.exceptions
from _plotly_utils.basevalidators import CompoundValidator, is_array
-from plotly import exceptions, files, session, tools, utils
-from plotly.api import v1, v2
-from plotly.basedatatypes import BaseTraceType, BaseFigure, BaseLayoutType
-from plotly.plotly import chunked_requests
+from _plotly_utils.utils import PlotlyJSONEncoder
-from plotly.graph_objs import Figure
-
-from plotly.grid_objs import Grid
-from plotly.dashboard_objs import dashboard_objs as dashboard
+from chart_studio import files, session, tools, utils, exceptions
+from chart_studio.api import v1, v2
+from chart_studio.plotly import chunked_requests
+from chart_studio.grid_objs import Grid
+from chart_studio.dashboard_objs import dashboard_objs as dashboard
# This is imported like this for backwards compat. Careful if changing.
-from plotly.config import get_config, get_credentials
-
+from chart_studio.config import get_config, get_credentials
__all__ = None
@@ -73,7 +71,7 @@ def sign_in(username, api_key, **kwargs):
# with the given, username, api_key, and plotly_api_domain.
v2.users.current()
except exceptions.PlotlyRequestError:
- raise exceptions.PlotlyError('Sign in failed.')
+ raise _plotly_utils.exceptions.PlotlyError('Sign in failed.')
update_plot_options = session.update_session_plot_options
@@ -155,6 +153,7 @@ def iplot(figure_or_data, **plot_options):
world_readable (default=True) -- Deprecated: use "sharing".
Make this figure private/public
"""
+ from plotly.basedatatypes import BaseFigure, BaseLayoutType
if 'auto_open' not in plot_options:
plot_options['auto_open'] = False
url = plot(figure_or_data, **plot_options)
@@ -222,7 +221,8 @@ def plot(figure_or_data, validate=True, **plot_options):
Make this figure private/public
"""
- figure = tools.return_figure_from_figure_or_data(figure_or_data, validate)
+ import plotly.tools
+ figure = plotly.tools.return_figure_from_figure_or_data(figure_or_data, validate)
for entry in figure['data']:
if ('type' in entry) and (entry['type'] == 'scattergl'):
continue
@@ -251,7 +251,7 @@ def plot(figure_or_data, validate=True, **plot_options):
plot_options = _plot_option_logic(plot_options)
- fig = tools._replace_newline(figure) # does not mutate figure
+ fig = plotly.tools._replace_newline(figure) # does not mutate figure
data = fig.get('data', [])
plot_options['layout'] = fig.get('layout', {})
response = v1.clientresp(data, **plot_options)
@@ -291,11 +291,12 @@ def iplot_mpl(fig, resize=True, strip_style=False, update=None,
plot_options -- run help(plotly.plotly.iplot)
"""
- fig = tools.mpl_to_plotly(fig, resize=resize, strip_style=strip_style)
+ import plotly.tools
+ fig = plotly.tools.mpl_to_plotly(fig, resize=resize, strip_style=strip_style)
if update and isinstance(update, dict):
fig.update(update)
elif update is not None:
- raise exceptions.PlotlyGraphObjectError(
+ raise _plotly_utils.exceptions.PlotlyGraphObjectError(
"'update' must be dictionary-like and a valid plotly Figure "
"object. Run 'help(plotly.graph_objs.Figure)' for more info."
)
@@ -323,11 +324,12 @@ def plot_mpl(fig, resize=True, strip_style=False, update=None, **plot_options):
plot_options -- run help(plotly.plotly.plot)
"""
- fig = tools.mpl_to_plotly(fig, resize=resize, strip_style=strip_style)
+ import plotly.tools
+ fig = plotly.tools.mpl_to_plotly(fig, resize=resize, strip_style=strip_style)
if update and isinstance(update, dict):
fig.update(update)
elif update is not None:
- raise exceptions.PlotlyGraphObjectError(
+ raise _plotly_utils.exceptions.PlotlyGraphObjectError(
"'update' must be dictionary-like and a valid plotly Figure "
"object. Run 'help(plotly.graph_objs.Figure)' for more info."
)
@@ -442,11 +444,12 @@ def get_figure(file_owner_or_url, file_id=None, raw=False):
Run `help(plotly.graph_objs.Figure)` for a list of valid properties.
"""
+ import plotly.tools
plotly_rest_url = get_config()['plotly_domain']
if file_id is None: # assume we're using a url
url = file_owner_or_url
if url[:len(plotly_rest_url)] != plotly_rest_url:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"Because you didn't supply a 'file_id' in the call, "
"we're assuming you're trying to snag a figure from a url. "
"You supplied the url, '{0}', we expected it to start with "
@@ -461,14 +464,14 @@ def get_figure(file_owner_or_url, file_id=None, raw=False):
try:
int(file_id)
except ValueError:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"The 'file_id' argument was not able to be converted into an "
"integer number. Make sure that the positional 'file_id' argument "
"is a number that can be converted into an integer or a string "
"that can be converted into an integer."
)
if int(file_id) < 0:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"The 'file_id' argument must be a non-negative number."
)
@@ -515,10 +518,10 @@ def get_figure(file_owner_or_url, file_id=None, raw=False):
if raw:
return figure
- return tools.get_graph_obj(figure, obj_type='Figure')
+ return plotly.tools.get_graph_obj(figure, obj_type='Figure')
-@utils.template_doc(**tools.get_config_file())
+@_plotly_utils.utils.template_doc(**tools.get_config_file())
class Stream:
"""
Interface to Plotly's real-time graphing API.
@@ -555,7 +558,7 @@ class Stream:
HTTP_PORT = 80
HTTPS_PORT = 443
- @utils.template_doc(**tools.get_config_file())
+ @_plotly_utils.utils.template_doc(**tools.get_config_file())
def __init__(self, stream_id):
"""
Initialize a Stream object with your unique stream_id.
@@ -607,7 +610,7 @@ def heartbeat(self, reconnect_on=(200, '', 408, 502)):
try:
self._stream.write('\n', reconnect_on=reconnect_on)
except AttributeError:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"Stream has not been opened yet, "
"cannot write to a closed connection. "
"Call `open()` on the stream to open the stream."
@@ -677,6 +680,7 @@ def write(self, trace, layout=None,
"""
# Convert trace objects to dictionaries
+ from plotly.basedatatypes import BaseTraceType
if isinstance(trace, BaseTraceType):
stream_object = trace.to_plotly_json()
else:
@@ -689,13 +693,13 @@ def write(self, trace, layout=None,
stream_object.update(dict(layout=layout))
# TODO: allow string version of this?
- jdata = _json.dumps(stream_object, cls=utils.PlotlyJSONEncoder)
+ jdata = _json.dumps(stream_object, cls=PlotlyJSONEncoder)
jdata += "\n"
try:
self._stream.write(jdata, reconnect_on=reconnect_on)
except AttributeError:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"Stream has not been opened yet, "
"cannot write to a closed connection. "
"Call `open()` on the stream to open the stream.")
@@ -712,7 +716,7 @@ def close(self):
try:
self._stream.close()
except AttributeError:
- raise exceptions.PlotlyError("Stream has not been opened yet.")
+ raise _plotly_utils.exceptions.PlotlyError("Stream has not been opened yet.")
class image:
@@ -745,10 +749,11 @@ def get(figure_or_data, format='png', width=None, height=None, scale=None):
"""
# TODO: format is a built-in name... we shouldn't really use it
- figure = tools.return_figure_from_figure_or_data(figure_or_data, True)
+ import plotly.tools
+ figure = plotly.tools.return_figure_from_figure_or_data(figure_or_data, True)
if format not in ['png', 'svg', 'jpeg', 'pdf', 'emf']:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"Invalid format. This version of your Plotly-Python "
"package currently only supports png, svg, jpeg, and pdf. "
"Learn more about image exporting, and the currently "
@@ -759,7 +764,7 @@ def get(figure_or_data, format='png', width=None, height=None, scale=None):
try:
scale = float(scale)
except:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"Invalid scale parameter. Scale must be a number."
)
@@ -808,7 +813,7 @@ def ishow(cls, figure_or_data, format='png', width=None, height=None,
py.image.ishow(fig, 'png', scale=3)
"""
if format == 'pdf':
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"Aw, snap! "
"It's not currently possible to embed a pdf into "
"an IPython notebook. You can save the pdf "
@@ -945,7 +950,7 @@ def _fill_in_response_column_ids(cls, request_columns,
def ensure_uploaded(fid):
if fid:
return
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
'This operation requires that the grid has already been uploaded '
'to Plotly. Try `uploading` first.'
)
@@ -1119,7 +1124,7 @@ def append_columns(cls, columns, grid=None, grid_url=None):
# This is sorta gross, we need to double-encode this.
body = {
- 'cols': _json.dumps(columns, cls=utils.PlotlyJSONEncoder)
+ 'cols': _json.dumps(columns, cls=PlotlyJSONEncoder)
}
fid = grid_id
response = v2.grids.col_create(fid, body)
@@ -1369,7 +1374,7 @@ def add_share_key_to_url(plot_url, attempt=0):
if not share_key_enabled:
attempt += 1
if attempt == 50:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
"The sharekey could not be enabled at this time so the graph "
"is saved as private. Try again to save as 'secret' later."
)
@@ -1380,7 +1385,8 @@ def add_share_key_to_url(plot_url, attempt=0):
def _send_to_plotly(figure, **plot_options):
- fig = tools._replace_newline(figure) # does not mutate figure
+ import plotly.tools
+ fig = plotly.tools._replace_newline(figure) # does not mutate figure
data = fig.get('data', [])
response = v1.clientresp(data, **plot_options)
@@ -1447,7 +1453,7 @@ def _create_or_update(data, filetype):
fid = matching_file['fid']
res = api_module.update(fid, data)
else:
- raise exceptions.PlotlyError("""
+ raise _plotly_utils.exceptions.PlotlyError("""
'{filename}' is already a {other_filetype} in your account.
While you can overwrite {filetype}s with the same name, you can't overwrite
files with a different type. Try deleting '{filename}' in your account or
@@ -1642,7 +1648,7 @@ def upload(cls, presentation, filename, sharing='public', auto_open=True):
elif sharing in ['private', 'secret']:
world_readable = False
else:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
SHARING_ERROR_MSG
)
data = {
@@ -1686,7 +1692,7 @@ def _extract_grid_graph_obj(obj_dict, reference_obj, grid, path):
Function modifies obj_dict and grid in-place
"""
- from plotly.grid_objs import Column
+ from chart_studio.grid_objs import Column
for prop in list(obj_dict.keys()):
propsrc = '{}src'.format(prop)
@@ -1745,6 +1751,8 @@ def _extract_grid_from_fig_like(fig, grid=None, path=''):
Columns are named with the path the corresponding data array
(e.g. 'data.0.marker.size')
"""
+ from plotly.basedatatypes import BaseFigure
+ from plotly.graph_objs import Figure
if grid is None:
# If not grid, this is top-level call so deep copy figure
@@ -1800,6 +1808,7 @@ def _set_grid_column_references(figure, grid):
None
Function modifies figure in-place
"""
+ from plotly.basedatatypes import BaseFigure
for col in grid:
prop_path = BaseFigure._str_to_dict_path(col.name)
prop_parent = figure
@@ -2009,7 +2018,7 @@ def create_animations(figure, filename=None, sharing='public', auto_open=True):
payload['world_readable'] = False
payload['share_key_enabled'] = True
else:
- raise exceptions.PlotlyError(
+ raise _plotly_utils.exceptions.PlotlyError(
SHARING_ERROR_MSG
)
@@ -2052,6 +2061,7 @@ def icreate_animations(figure, filename=None, sharing='public', auto_open=False)
This function is based off `plotly.plotly.iplot`. See `plotly.plotly.
create_animations` Doc String for param descriptions.
"""
+ from plotly.basedatatypes import BaseFigure, BaseLayoutType
url = create_animations(figure, filename, sharing, auto_open)
if isinstance(figure, dict):
diff --git a/plotly/presentation_objs/__init__.py b/chart_studio/presentation_objs/__init__.py
similarity index 100%
rename from plotly/presentation_objs/__init__.py
rename to chart_studio/presentation_objs/__init__.py
diff --git a/chart_studio/presentation_objs/presentation_objs.py b/chart_studio/presentation_objs/presentation_objs.py
new file mode 100644
index 00000000000..699f6996121
--- /dev/null
+++ b/chart_studio/presentation_objs/presentation_objs.py
@@ -0,0 +1,1177 @@
+"""
+dashboard_objs
+==========
+
+A module for creating and manipulating spectacle-presentation dashboards.
+"""
+
+import copy
+import random
+import re
+import string
+import warnings
+
+import _plotly_utils.exceptions
+from chart_studio import exceptions
+from chart_studio.config import get_config
+
+HEIGHT = 700.0
+WIDTH = 1000.0
+
+CODEPANE_THEMES = ['tomorrow', 'tomorrowNight']
+
+VALID_LANGUAGES = ['cpp', 'cs', 'css', 'fsharp', 'go', 'haskell', 'java',
+ 'javascript', 'jsx', 'julia', 'xml', 'matlab', 'php',
+ 'python', 'r', 'ruby', 'scala', 'sql', 'yaml']
+
+VALID_TRANSITIONS = ['slide', 'zoom', 'fade', 'spin']
+
+PRES_THEMES = ['moods', 'martik']
+
+VALID_GROUPTYPES = [
+ 'leftgroup_v', 'rightgroup_v', 'middle', 'checkerboard_topleft',
+ 'checkerboard_topright'
+]
+
+fontWeight_dict = {
+ 'Thin': {'fontWeight': 100},
+ 'Thin Italic': {'fontWeight': 100, 'fontStyle': 'italic'},
+ 'Light': {'fontWeight': 300},
+ 'Light Italic': {'fontWeight': 300, 'fontStyle': 'italic'},
+ 'Regular': {'fontWeight': 400},
+ 'Regular Italic': {'fontWeight': 400, 'fontStyle': 'italic'},
+ 'Medium': {'fontWeight': 500},
+ 'Medium Italic': {'fontWeight': 500, 'fontStyle': 'italic'},
+ 'Bold': {'fontWeight': 700},
+ 'Bold Italic': {'fontWeight': 700, 'fontStyle': 'italic'},
+ 'Black': {'fontWeight': 900},
+ 'Black Italic': {'fontWeight': 900, 'fontStyle': 'italic'},
+}
+
+
+def list_of_options(iterable, conj='and', period=True):
+ """
+ Returns an English listing of objects seperated by commas ','
+
+ For example, ['foo', 'bar', 'baz'] becomes 'foo, bar and baz'
+ if the conjunction 'and' is selected.
+ """
+ if len(iterable) < 2:
+ raise _plotly_utils.exceptions.PlotlyError(
+ 'Your list or tuple must contain at least 2 items.'
+ )
+ template = (len(iterable) - 2)*'{}, ' + '{} ' + conj + ' {}' + period*'.'
+ return template.format(*iterable)
+
+
+# Error Messages
+STYLE_ERROR = "Your presentation style must be {}".format(
+ list_of_options(PRES_THEMES, conj='or', period=True)
+)
+
+CODE_ENV_ERROR = (
+ "If you are putting a block of code into your markdown "
+ "presentation, make sure your denote the start and end "
+ "of the code environment with the '```' characters. For "
+ "example, your markdown string would include something "
+ "like:\n\n```python\nx = 2\ny = 1\nprint x\n```\n\n"
+ "Notice how the language that you want the code to be "
+ "displayed in is immediately to the right of first "
+ "entering '```', i.e. '```python'."
+)
+
+LANG_ERROR = (
+ "The language of your code block should be "
+ "clearly indicated after the first ``` that "
+ "begins the code block. The valid languages to "
+ "choose from are" + list_of_options(
+ VALID_LANGUAGES
+ )
+)
+
+
+def _generate_id(size):
+ letters_and_numbers = string.ascii_letters
+ for num in range(10):
+ letters_and_numbers += str(num)
+ letters_and_numbers += str(num)
+ id_str = ''
+ for _ in range(size):
+ id_str += random.choice(list(letters_and_numbers))
+
+ return id_str
+
+
+paragraph_styles = {
+ 'Body': {
+ 'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 11,
+ 'fontStyle': 'normal',
+ 'fontWeight': 400,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none',
+ 'wordBreak': 'break-word'
+ },
+ 'Body Small': {
+ 'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 10,
+ 'fontStyle': 'normal',
+ 'fontWeight': 400,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'
+ },
+ 'Caption': {
+ 'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 11,
+ 'fontStyle': 'italic',
+ 'fontWeight': 400,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'
+ },
+ 'Heading 1': {
+ 'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 26,
+ 'fontStyle': 'normal',
+ 'fontWeight': 400,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none',
+ },
+ 'Heading 2': {
+ 'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 20,
+ 'fontStyle': 'normal',
+ 'fontWeight': 400,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'
+ },
+ 'Heading 3': {
+ 'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 11,
+ 'fontStyle': 'normal',
+ 'fontWeight': 700,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'
+ }
+}
+
+
+def _empty_slide(transition, id):
+ empty_slide = {'children': [],
+ 'id': id,
+ 'props': {'style': {}, 'transition': transition}}
+ return empty_slide
+
+
+def _box(boxtype, text_or_url, left, top, height, width, id, props_attr,
+ style_attr, paragraphStyle):
+ children_list = []
+ fontFamily = "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace"
+ if boxtype == 'Text':
+ children_list = text_or_url.split('\n')
+
+ props = {
+ 'isQuote': False,
+ 'listType': None,
+ 'paragraphStyle': paragraphStyle,
+ 'size': 4,
+ 'style': copy.deepcopy(paragraph_styles[paragraphStyle])
+ }
+
+ props['style'].update(
+ {'height': height,
+ 'left': left,
+ 'top': top,
+ 'width': width,
+ 'position': 'absolute'}
+ )
+
+ elif boxtype == 'Image':
+ # height, width are set to default 512
+ # as set by the Presentation Editor
+ props = {
+ 'height': 512,
+ 'imageName': None,
+ 'src': text_or_url,
+ 'style': {'height': height,
+ 'left': left,
+ 'opacity': 1,
+ 'position': 'absolute',
+ 'top': top,
+ 'width': width},
+ 'width': 512
+ }
+ elif boxtype == 'Plotly':
+ if '?share_key' in text_or_url:
+ src = text_or_url
+ else:
+ src = text_or_url + '.embed?link=false'
+ props = {
+ 'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': src,
+ 'style': {'height': height,
+ 'left': left,
+ 'position': 'absolute',
+ 'top': top,
+ 'width': width}
+ }
+ elif boxtype == 'CodePane':
+ props = {
+ 'language': 'python',
+ 'source': text_or_url,
+ 'style': {'fontFamily': fontFamily,
+ 'fontSize': 13,
+ 'height': height,
+ 'left': left,
+ 'margin': 0,
+ 'position': 'absolute',
+ 'textAlign': 'left',
+ 'top': top,
+ 'width': width},
+ 'theme': 'tomorrowNight'
+ }
+
+ # update props and style attributes
+ for item in props_attr.items():
+ props[item[0]] = item[1]
+ for item in style_attr.items():
+ props['style'][item[0]] = item[1]
+
+ child = {
+ 'children': children_list,
+ 'id': id,
+ 'props': props,
+ 'type': boxtype
+ }
+
+ if boxtype == 'Text':
+ child['defaultHeight'] = 36
+ child['defaultWidth'] = 52
+ child['resizeVertical'] = False
+ if boxtype == 'CodePane':
+ child['defaultText'] = 'Code'
+
+ return child
+
+
+def _percentage_to_pixel(value, side):
+ if side == 'left':
+ return WIDTH * (0.01 * value)
+ elif side == 'top':
+ return HEIGHT * (0.01 * value)
+ elif side == 'height':
+ return HEIGHT * (0.01 * value)
+ elif side == 'width':
+ return WIDTH * (0.01 * value)
+
+
+def _return_box_position(left, top, height, width):
+ values_dict = {
+ 'left': left,
+ 'top': top,
+ 'height': height,
+ 'width': width,
+ }
+ for key in iter(values_dict):
+ if isinstance(values_dict[key], str):
+ var = float(values_dict[key][: -2])
+ else:
+ var = _percentage_to_pixel(values_dict[key], key)
+ values_dict[key] = var
+
+ return (values_dict['left'], values_dict['top'],
+ values_dict['height'], values_dict['width'])
+
+
+def _remove_extra_whitespace_from_line(line):
+ line = line.lstrip()
+ line = line.rstrip()
+ return line
+
+
+def _list_of_slides(markdown_string):
+ if not markdown_string.endswith('\n---\n'):
+ markdown_string += '\n---\n'
+
+ text_blocks = re.split('\n-{2,}\n', markdown_string)
+
+ list_of_slides = []
+ for text in text_blocks:
+ if not all(char in ['\n', '-', ' '] for char in text):
+ list_of_slides.append(text)
+
+ if '\n-\n' in markdown_string:
+ msg = ("You have at least one '-' by itself on its own line in your "
+ "markdown string. If you are trying to denote a new slide, "
+ "make sure that the line has 3 '-'s like this: \n\n---\n\n"
+ "A new slide will NOT be created here.")
+ warnings.warn(msg)
+
+ return list_of_slides
+
+
+def _top_spec_for_text_at_bottom(text_block, width_per, per_from_bottom=0,
+ min_top=30):
+ # This function ensures that if there is a large block of
+ # text in your slide it will not overflow off the bottom
+ # of the slide.
+ # The input for this function are a block of text and the
+ # params that define where it will be placed in the slide.
+ # The function makes some calculations and will output a
+ # 'top' value (i.e. the left, top, height, width css params)
+ # so that the text block will come down to some specified
+ # distance from the bottom of the page.
+
+ # TODO: customize this function for different fonts/sizes
+ max_lines = 37
+ one_char_percent_width = 0.764
+ chars_in_full_line = width_per / one_char_percent_width
+
+ num_of_lines = 0
+ char_group = 0
+ for char in text_block:
+ if char == '\n':
+ num_of_lines += 1
+ char_group = 0
+ else:
+ if char_group >= chars_in_full_line:
+ char_group = 0
+ num_of_lines += 1
+ else:
+ char_group += 1
+
+ num_of_lines += 1
+ top_frac = (max_lines - num_of_lines) / float(max_lines)
+ top = top_frac * 100 - per_from_bottom
+
+ # to be safe
+ return max(top, min_top)
+
+
+def _box_specs_gen(num_of_boxes, grouptype='leftgroup_v', width_range=50,
+ height_range=50, margin=2, betw_boxes=4, middle_center=50):
+ # the (left, top, width, height) specs
+ # are added to specs_for_boxes
+ specs_for_boxes = []
+ if num_of_boxes == 1 and grouptype in ['leftgroup_v', 'rightgroup_v']:
+ if grouptype == 'rightgroup_v':
+ left_shift = (100 - width_range)
+ else:
+ left_shift = 0
+
+ box_spec = (
+ left_shift + (margin / WIDTH) * 100,
+ (margin / HEIGHT) * 100,
+ 100 - (2 * margin / HEIGHT * 100),
+ width_range - (2 * margin / WIDTH) * 100
+ )
+ specs_for_boxes.append(box_spec)
+
+ elif num_of_boxes > 1 and grouptype in ['leftgroup_v', 'rightgroup_v']:
+ if grouptype == 'rightgroup_v':
+ left_shift = (100 - width_range)
+ else:
+ left_shift = 0
+
+ if num_of_boxes % 2 == 0:
+ box_width_px = 0.5 * (
+ (float(width_range)/100) * WIDTH - 2 * margin - betw_boxes
+ )
+ box_width = (box_width_px / WIDTH) * 100
+
+ height = (200.0 / (num_of_boxes * HEIGHT)) * (
+ HEIGHT - (num_of_boxes / 2 - 1) * betw_boxes - 2 * margin
+ )
+
+ left1 = left_shift + (margin / WIDTH) * 100
+ left2 = left_shift + (
+ ((margin + betw_boxes) / WIDTH) * 100 + box_width
+ )
+ for left in [left1, left2]:
+ for j in range(int(num_of_boxes / 2)):
+ top = (margin * 100 / HEIGHT) + j * (
+ height + (betw_boxes * 100 / HEIGHT)
+ )
+ specs = (
+ left,
+ top,
+ height,
+ box_width
+ )
+ specs_for_boxes.append(specs)
+
+ if num_of_boxes % 2 == 1:
+ width = width_range - (200 * margin) / WIDTH
+ height = (100.0 / (num_of_boxes * HEIGHT)) * (
+ HEIGHT - (num_of_boxes - 1) * betw_boxes - 2 * margin
+ )
+ left = left_shift + (margin / WIDTH) * 100
+ for j in range(num_of_boxes):
+ top = (margin / HEIGHT) * 100 + j * (
+ height + (betw_boxes / HEIGHT) * 100
+ )
+ specs = (
+ left,
+ top,
+ height,
+ width
+ )
+ specs_for_boxes.append(specs)
+
+ elif grouptype == 'middle':
+ top = float(middle_center - (height_range / 2))
+ height = height_range
+ width = (1 / float(num_of_boxes)) * (
+ width_range - (num_of_boxes - 1) * (100*betw_boxes/WIDTH)
+ )
+ for j in range(num_of_boxes):
+ left = ((100 - float(width_range)) / 2) + j * (
+ width + (betw_boxes / WIDTH) * 100
+ )
+ specs = (left, top, height, width)
+ specs_for_boxes.append(specs)
+
+ elif 'checkerboard' in grouptype and num_of_boxes == 2:
+ if grouptype == 'checkerboard_topleft':
+ for j in range(2):
+ left = j * 50
+ top = j * 50
+ height = 50
+ width = 50
+ specs = (
+ left,
+ top,
+ height,
+ width
+ )
+ specs_for_boxes.append(specs)
+ else:
+ for j in range(2):
+ left = 50 * (1 - j)
+ top = j * 50
+ height = 50
+ width = 50
+ specs = (
+ left,
+ top,
+ height,
+ width
+ )
+ specs_for_boxes.append(specs)
+ return specs_for_boxes
+
+
+def _return_layout_specs(num_of_boxes, url_lines, title_lines, text_block,
+ code_blocks, slide_num, style):
+ # returns specs of the form (left, top, height, width)
+ code_theme = 'tomorrowNight'
+ if style == 'martik':
+ specs_for_boxes = []
+ margin = 18 # in pxs
+
+ # set Headings styles
+ paragraph_styles['Heading 1'].update(
+ {'color': '#0D0A1E',
+ 'fontFamily': 'Raleway',
+ 'fontSize': 55,
+ 'fontWeight': fontWeight_dict['Bold']['fontWeight']}
+ )
+
+ paragraph_styles['Heading 2'] = copy.deepcopy(
+ paragraph_styles['Heading 1']
+ )
+ paragraph_styles['Heading 2'].update({'fontSize': 36})
+ paragraph_styles['Heading 3'] = copy.deepcopy(
+ paragraph_styles['Heading 1']
+ )
+ paragraph_styles['Heading 3'].update({'fontSize': 30})
+
+ # set Body style
+ paragraph_styles['Body'].update(
+ {'color': '#96969C',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 16,
+ 'fontWeight': fontWeight_dict['Regular']['fontWeight']}
+ )
+
+ bkgd_color = '#F4FAFB'
+ title_font_color = '#0D0A1E'
+ text_font_color = '#96969C'
+ if num_of_boxes == 0 and slide_num == 0:
+ text_textAlign = 'center'
+ else:
+ text_textAlign = 'left'
+ if num_of_boxes == 0:
+ specs_for_title = (0, 50, 20, 100)
+ specs_for_text = (15, 60, 50, 70)
+
+ bkgd_color = '#0D0A1E'
+ title_font_color = '#F4FAFB'
+ text_font_color = '#F4FAFB'
+ elif num_of_boxes == 1:
+ if code_blocks != [] or (url_lines != [] and
+ get_config()['plotly_domain'] in
+ url_lines[0]):
+ if code_blocks != []:
+ w_range = 40
+ else:
+ w_range = 60
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, 80,
+ per_from_bottom=(margin / HEIGHT) * 100
+ )
+ specs_for_title = (0, 3, 20, 100)
+ specs_for_text = (10, text_top, 30, 80)
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='middle', width_range=w_range,
+ height_range=60, margin=margin, betw_boxes=4
+ )
+ bkgd_color = '#0D0A1E'
+ title_font_color = '#F4FAFB'
+ text_font_color = '#F4FAFB'
+ code_theme = 'tomorrow'
+ elif title_lines == [] and text_block == '':
+ specs_for_title = (0, 50, 20, 100)
+ specs_for_text = (15, 60, 50, 70)
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='middle', width_range=50,
+ height_range=80, margin=0, betw_boxes=0
+ )
+ else:
+ title_text_width = 40 - (margin / WIDTH) * 100
+
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, title_text_width,
+ per_from_bottom=(margin / HEIGHT) * 100
+ )
+ specs_for_title = (60, 3, 20, 40)
+ specs_for_text = (60, text_top, 1, title_text_width)
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='leftgroup_v', width_range=60,
+ margin=margin, betw_boxes=4
+ )
+ bkgd_color = '#0D0A1E'
+ title_font_color = '#F4FAFB'
+ text_font_color = '#F4FAFB'
+ elif num_of_boxes == 2 and url_lines != []:
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, 46, per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=50
+ )
+ specs_for_title = (0, 3, 20, 50)
+ specs_for_text = (52, text_top, 40, 46)
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='checkerboard_topright'
+ )
+ elif num_of_boxes >= 2 and url_lines == []:
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, 92, per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=15
+ )
+ if num_of_boxes == 2:
+ betw_boxes = 90
+ else:
+ betw_boxes = 10
+ specs_for_title = (0, 3, 20, 100)
+ specs_for_text = (4, text_top, 1, 92)
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='middle', width_range=92,
+ height_range=60, margin=margin, betw_boxes=betw_boxes
+ )
+ code_theme = 'tomorrow'
+ else:
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, 40 - (margin / WIDTH) * 100,
+ per_from_bottom=(margin / HEIGHT) * 100
+ )
+ specs_for_title = (0, 3, 20, 40 - (margin / WIDTH) * 100)
+ specs_for_text = (
+ (margin / WIDTH) * 100, text_top, 50,
+ 40 - (margin / WIDTH) * 100
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='rightgroup_v', width_range=60,
+ margin=margin, betw_boxes=4
+ )
+
+ elif style == 'moods':
+ specs_for_boxes = []
+ margin = 18
+ code_theme = 'tomorrowNight'
+
+ # set Headings styles
+ paragraph_styles['Heading 1'].update(
+ {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 55,
+ 'fontWeight': fontWeight_dict['Black']['fontWeight']}
+ )
+
+ paragraph_styles['Heading 2'] = copy.deepcopy(
+ paragraph_styles['Heading 1']
+ )
+ paragraph_styles['Heading 2'].update({'fontSize': 36})
+ paragraph_styles['Heading 3'] = copy.deepcopy(
+ paragraph_styles['Heading 1']
+ )
+ paragraph_styles['Heading 3'].update({'fontSize': 30})
+
+ # set Body style
+ paragraph_styles['Body'].update(
+ {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 16,
+ 'fontWeight': fontWeight_dict['Thin']['fontWeight']}
+ )
+
+ bkgd_color = '#FFFFFF'
+ title_font_color = None
+ text_font_color = None
+ if num_of_boxes == 0 and slide_num == 0:
+ text_textAlign = 'center'
+ else:
+ text_textAlign = 'left'
+ if num_of_boxes == 0:
+ if slide_num == 0 or text_block == '':
+ bkgd_color = '#F7F7F7'
+ specs_for_title = (0, 50, 20, 100)
+ specs_for_text = (15, 60, 50, 70)
+ else:
+ bkgd_color = '#F7F7F7'
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=90,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=20
+ )
+ specs_for_title = (0, 2, 20, 100)
+ specs_for_text = (5, text_top, 50, 90)
+
+ elif num_of_boxes == 1:
+ if code_blocks != []:
+ # code
+ if text_block == '':
+ margin = 5
+ specs_for_title = (0, 3, 20, 100)
+ specs_for_text = (0, 0, 0, 0)
+ top = 12
+ specs_for_boxes = [
+ (margin, top, 100 - top - margin, 100 - 2 * margin)
+ ]
+
+ elif slide_num % 2 == 0:
+ # middle center
+ width_per = 90
+ height_range = 60
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=100 - height_range / 2.
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='middle',
+ width_range=50, height_range=60, margin=margin,
+ )
+ specs_for_title = (0, 3, 20, 100)
+ specs_for_text = (
+ 5, text_top, 2, width_per
+ )
+ else:
+ # right
+ width_per = 50
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=30
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='rightgroup_v',
+ width_range=50, margin=40,
+ )
+ specs_for_title = (0, 3, 20, 50)
+ specs_for_text = (
+ 2, text_top, 2, width_per - 2
+ )
+ elif (url_lines != [] and
+ get_config()['plotly_domain'] in url_lines[0]):
+ # url
+ if slide_num % 2 == 0:
+ # top half
+ width_per = 95
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=60
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='middle',
+ width_range=100, height_range=60,
+ middle_center=30
+ )
+ specs_for_title = (0, 60, 20, 100)
+ specs_for_text = (
+ 2.5, text_top, 2, width_per
+ )
+ else:
+ # middle across
+ width_per = 95
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=60
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='middle',
+ width_range=100, height_range=60
+ )
+ specs_for_title = (0, 3, 20, 100)
+ specs_for_text = (
+ 2.5, text_top, 2, width_per
+ )
+ else:
+ # image
+ if slide_num % 2 == 0:
+ # right
+ width_per = 50
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=30
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='rightgroup_v',
+ width_range=50, margin=0,
+ )
+ specs_for_title = (0, 3, 20, 50)
+ specs_for_text = (
+ 2, text_top, 2, width_per - 2
+ )
+ else:
+ # left
+ width_per = 50
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=30
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='leftgroup_v',
+ width_range=50, margin=0,
+ )
+ specs_for_title = (50, 3, 20, 50)
+ specs_for_text = (
+ 52, text_top, 2, width_per - 2
+ )
+ elif num_of_boxes == 2:
+ # right stack
+ width_per = 50
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=30
+ )
+ specs_for_boxes = [(50, 0, 50, 50), (50, 50, 50, 50)]
+ specs_for_title = (0, 3, 20, 50)
+ specs_for_text = (
+ 2, text_top, 2, width_per - 2
+ )
+ elif num_of_boxes == 3:
+ # middle top
+ width_per = 95
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=40
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='middle',
+ width_range=100, height_range=40, middle_center=30
+ )
+ specs_for_title = (0, 0, 20, 100)
+ specs_for_text = (
+ 2.5, text_top, 2, width_per
+ )
+ else:
+ # right stack
+ width_per = 40
+ text_top = _top_spec_for_text_at_bottom(
+ text_block, width_per=width_per,
+ per_from_bottom=(margin / HEIGHT) * 100,
+ min_top=30
+ )
+ specs_for_boxes = _box_specs_gen(
+ num_of_boxes, grouptype='rightgroup_v',
+ width_range=60, margin=0,
+ )
+ specs_for_title = (0, 3, 20, 40)
+ specs_for_text = (
+ 2, text_top, 2, width_per - 2
+ )
+
+ # set text style attributes
+ title_style_attr = {}
+ text_style_attr = {'textAlign': text_textAlign}
+
+ if text_font_color:
+ text_style_attr['color'] = text_font_color
+ if title_font_color:
+ title_style_attr['color'] = title_font_color
+
+ return (specs_for_boxes, specs_for_title, specs_for_text, bkgd_color,
+ title_style_attr, text_style_attr, code_theme)
+
+
+def _url_parens_contained(url_name, line):
+ return line.startswith(url_name + '(') and line.endswith(')')
+
+
+class Presentation(dict):
+ """
+ The Presentation class for creating spectacle-presentations.
+
+ The Presentations API is a means for creating JSON blobs which are then
+ converted Spectacle Presentations. To use the API you only need to define
+ a block string and define your slides using markdown. Then you can upload
+ your presentation to the Plotly Server.
+
+ Rules for your presentation string:
+ - use '---' to denote a slide break.
+ - headers work as per usual, where if '#' is used before a line of text
+ then it is interpretted as a header. Only the first header in a slide is
+ displayed on the slide. There are only 3 heading sizes: #, ## and ###.
+ 4 or more hashes will be interpretted as ###.
+ - you can set the type of slide transition you want by writing a line that
+ starts with 'transition: ' before your first header line in the slide,
+ and write the types of transition you want after. Your transition to
+ choose from are 'slide', 'zoom', 'fade' and 'spin'.
+ - to insert a Plotly chart into your slide, write a line that has the form
+ Plotly(url) with your url pointing to your chart. Note that it is
+ STRONGLY advised that your chart has fig['layout']['autosize'] = True.
+ - to insert an image from the web, write a line with the form Image(url)
+ - to insert a block of text, begin with a line that denotes the code
+ envoronment '```lang' where lang is a valid programming language. To find
+ the valid languages run:\n
+ 'plotly.presentation_objs.presentation_objs.VALID_LANGUAGES'\n
+ To end the code block environment,
+ write a single '```' line. All Plotly(url) and Image(url) lines will NOT
+ be interpretted as a Plotly or Image url if they are in the code block.
+
+ :param (str) markdown_string: the block string that denotes the slides,
+ slide properties, and images to be placed in the presentation. If
+ 'markdown_string' is set to 'None', the JSON for a presentation with
+ one empty slide will be created.
+ :param (str) style: the theme that the presentation will take on. The
+ themes that are available now are 'martik' and 'moods'.
+ Default = 'moods'.
+ :param (bool) imgStretch: if set to False, all images in the presentation
+ will not have heights and widths that will not exceed the parent
+ container they belong to. In other words, images will keep their
+ original aspect ratios.
+ Default = True.
+
+ For examples see the documentation:\n
+ https://plot.ly/python/presentations-api/
+ """
+ def __init__(self, markdown_string=None, style='moods', imgStretch=True):
+ self['presentation'] = {
+ 'slides': [],
+ 'slidePreviews': [None for _ in range(496)],
+ 'version': '0.1.3',
+ 'paragraphStyles': paragraph_styles
+ }
+
+ if markdown_string:
+ if style not in PRES_THEMES:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "Your presentation style must be {}".format(
+ list_of_options(PRES_THEMES, conj='or', period=True)
+ )
+ )
+ self._markdown_to_presentation(markdown_string, style, imgStretch)
+ else:
+ self._add_empty_slide()
+
+ def _markdown_to_presentation(self, markdown_string, style, imgStretch):
+ list_of_slides = _list_of_slides(markdown_string)
+
+ for slide_num, slide in enumerate(list_of_slides):
+ lines_in_slide = slide.split('\n')
+ title_lines = []
+
+ # validate blocks of code
+ if slide.count('```') % 2 != 0:
+ raise _plotly_utils.exceptions.PlotlyError(CODE_ENV_ERROR)
+
+ # find code blocks
+ code_indices = []
+ code_blocks = []
+ wdw_size = len('```')
+ for j in range(len(slide)):
+ if slide[j:j+wdw_size] == '```':
+ code_indices.append(j)
+
+ for k in range(int(len(code_indices) / 2)):
+ code_blocks.append(
+ slide[code_indices[2 * k]:code_indices[(2 * k) + 1]]
+ )
+
+ lang_and_code_tuples = []
+ for code_block in code_blocks:
+ # validate code blocks
+ code_by_lines = code_block.split('\n')
+ language = _remove_extra_whitespace_from_line(
+ code_by_lines[0][3:]
+ ).lower()
+ if language == '' or language not in VALID_LANGUAGES:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "The language of your code block should be "
+ "clearly indicated after the first ``` that "
+ "begins the code block. The valid languages to "
+ "choose from are" + list_of_options(
+ VALID_LANGUAGES
+ )
+ )
+ lang_and_code_tuples.append(
+ (language, '\n'.join(code_by_lines[1:]))
+ )
+
+ # collect text, code and urls
+ title_lines = []
+ url_lines = []
+ text_lines = []
+ inCode = False
+
+ for line in lines_in_slide:
+ # inCode handling
+ if line[:3] == '```' and len(line) > 3:
+ inCode = True
+ if line == '```':
+ inCode = False
+
+ if not inCode and line != '```':
+ if len(line) > 0 and line[0] == '#':
+ title_lines.append(line)
+ elif (_url_parens_contained('Plotly', line) or
+ _url_parens_contained('Image', line)):
+ if (line.startswith('Plotly(') and
+ get_config()['plotly_domain'] not in line):
+ raise _plotly_utils.exceptions.PlotlyError(
+ "You are attempting to insert a Plotly Chart "
+ "in your slide but your url does not have "
+ "your plotly domain '{}' in it.".format(
+ get_config()['plotly_domain']
+ )
+ )
+ url_lines.append(line)
+ else:
+ # find and set transition properties
+ trans = 'transition:'
+ if line.startswith(trans) and title_lines == []:
+ slide_trans = line[len(trans):]
+ slide_trans = _remove_extra_whitespace_from_line(
+ slide_trans
+ )
+ slide_transition_list = []
+ for key in VALID_TRANSITIONS:
+ if key in slide_trans:
+ slide_transition_list.append(key)
+
+ if slide_transition_list == []:
+ slide_transition_list.append('slide')
+ self._set_transition(
+ slide_transition_list, slide_num
+ )
+
+ else:
+ text_lines.append(line)
+
+ # make text block
+ for i in range(2):
+ try:
+ while text_lines[-i] == '':
+ text_lines.pop(-i)
+ except IndexError:
+ pass
+
+ text_block = '\n'.join(text_lines)
+ num_of_boxes = len(url_lines) + len(lang_and_code_tuples)
+
+ (specs_for_boxes, specs_for_title, specs_for_text, bkgd_color,
+ title_style_attr, text_style_attr,
+ code_theme) = _return_layout_specs(
+ num_of_boxes, url_lines, title_lines, text_block, code_blocks,
+ slide_num, style
+ )
+
+ # background color
+ self._color_background(bkgd_color, slide_num)
+
+ # insert title, text, code, and images
+ if len(title_lines) > 0:
+ # clean titles
+ title = title_lines[0]
+ num_hashes = 0
+ while title[0] == '#':
+ title = title[1:]
+ num_hashes += 1
+ title = _remove_extra_whitespace_from_line(title)
+
+ self._insert(
+ box='Text', text_or_url=title,
+ left=specs_for_title[0], top=specs_for_title[1],
+ height=specs_for_title[2], width=specs_for_title[3],
+ slide=slide_num, style_attr=title_style_attr,
+ paragraphStyle='Heading 1'.format(
+ min(num_hashes, 3)
+ )
+ )
+
+ # text
+ if len(text_lines) > 0:
+ self._insert(
+ box='Text', text_or_url=text_block,
+ left=specs_for_text[0], top=specs_for_text[1],
+ height=specs_for_text[2], width=specs_for_text[3],
+ slide=slide_num, style_attr=text_style_attr,
+ paragraphStyle='Body'
+ )
+
+ url_and_code_blocks = list(url_lines + lang_and_code_tuples)
+ for k, specs in enumerate(specs_for_boxes):
+ url_or_code = url_and_code_blocks[k]
+ if isinstance(url_or_code, tuple):
+ # code
+ language = url_or_code[0]
+ code = url_or_code[1]
+ box_name = 'CodePane'
+
+ # code style
+ props_attr = {}
+ props_attr['language'] = language
+ props_attr['theme'] = code_theme
+
+ self._insert(box=box_name, text_or_url=code,
+ left=specs[0], top=specs[1],
+ height=specs[2], width=specs[3],
+ slide=slide_num, props_attr=props_attr)
+ else:
+ # url
+ if get_config()['plotly_domain'] in url_or_code:
+ box_name = 'Plotly'
+ else:
+ box_name = 'Image'
+ url = url_or_code[len(box_name) + 1: -1]
+
+ self._insert(box=box_name, text_or_url=url,
+ left=specs[0], top=specs[1],
+ height=specs[2], width=specs[3],
+ slide=slide_num)
+
+ if not imgStretch:
+ for s, slide in enumerate(self['presentation']['slides']):
+ for c, child in enumerate(slide['children']):
+ if child['type'] in ['Image', 'Plotly']:
+ deep_child = child['props']['style']
+ width = deep_child['width']
+ height = deep_child['height']
+
+ if width >= height:
+ deep_child['max-width'] = deep_child.pop('width')
+ else:
+ deep_child['max-height'] = deep_child.pop('height')
+
+ def _add_empty_slide(self):
+ self['presentation']['slides'].append(
+ _empty_slide(['slide'], _generate_id(9))
+ )
+
+ def _add_missing_slides(self, slide):
+ # add slides if desired slide number isn't in the presentation
+ try:
+ self['presentation']['slides'][slide]['children']
+ except IndexError:
+ num_of_slides = len(self['presentation']['slides'])
+ for _ in range(slide - num_of_slides + 1):
+ self._add_empty_slide()
+
+ def _insert(self, box, text_or_url, left, top, height, width, slide=0,
+ props_attr={}, style_attr={}, paragraphStyle=None):
+ self._add_missing_slides(slide)
+
+ left, top, height, width = _return_box_position(left, top, height,
+ width)
+ new_id = _generate_id(9)
+ child = _box(box, text_or_url, left, top, height, width, new_id,
+ props_attr, style_attr, paragraphStyle)
+
+ self['presentation']['slides'][slide]['children'].append(child)
+
+ def _color_background(self, color, slide):
+ self._add_missing_slides(slide)
+
+ loc = self['presentation']['slides'][slide]
+ loc['props']['style']['backgroundColor'] = color
+
+ def _background_image(self, url, slide, bkrd_image_dict):
+ self._add_missing_slides(slide)
+
+ loc = self['presentation']['slides'][slide]['props']
+
+ # default settings
+ size = 'stretch'
+ repeat = 'no-repeat'
+
+ if 'background-size:' in bkrd_image_dict:
+ size = bkrd_image_dict['background-size:']
+ if 'background-repeat:' in bkrd_image_dict:
+ repeat = bkrd_image_dict['background-repeat:']
+
+ if size == 'stretch':
+ backgroundSize = '100% 100%'
+ elif size == 'original':
+ backgroundSize = 'auto'
+ elif size == 'contain':
+ backgroundSize = 'contain'
+ elif size == 'cover':
+ backgroundSize = 'cover'
+
+ style = {
+ 'backgroundImage': 'url({})'.format(url),
+ 'backgroundPosition': 'center center',
+ 'backgroundRepeat': repeat,
+ 'backgroundSize': backgroundSize
+ }
+
+ for item in style.items():
+ loc['style'].setdefault(item[0], item[1])
+
+ loc['backgroundImageSrc'] = url
+ loc['backgroundImageName'] = None
+
+ def _set_transition(self, transition, slide):
+ self._add_missing_slides(slide)
+ loc = self['presentation']['slides'][slide]['props']
+ loc['transition'] = transition
diff --git a/chart_studio/session.py b/chart_studio/session.py
new file mode 100644
index 00000000000..2397bbcac8a
--- /dev/null
+++ b/chart_studio/session.py
@@ -0,0 +1,159 @@
+"""
+The session module handles the user's current credentials, config and plot opts
+
+This allows users to dynamically change which plotly domain they're using,
+which user they're signed in as, and plotting defaults.
+
+"""
+from __future__ import absolute_import
+
+import copy
+
+import six
+
+import _plotly_utils.exceptions
+
+
+_session = {
+ 'credentials': {},
+ 'config': {},
+ 'plot_options': {}
+}
+
+CREDENTIALS_KEYS = {
+ 'username': six.string_types,
+ 'api_key': six.string_types,
+ 'proxy_username': six.string_types,
+ 'proxy_password': six.string_types,
+ 'stream_ids': list
+}
+
+CONFIG_KEYS = {
+ 'plotly_domain': six.string_types,
+ 'plotly_streaming_domain': six.string_types,
+ 'plotly_api_domain': six.string_types,
+ 'plotly_ssl_verification': bool,
+ 'plotly_proxy_authorization': bool,
+ 'world_readable': bool,
+ 'auto_open': bool,
+ 'sharing': six.string_types
+}
+
+PLOT_OPTIONS = {
+ 'filename': six.string_types,
+ 'fileopt': six.string_types,
+ 'validate': bool,
+ 'world_readable': bool,
+ 'auto_open': bool,
+ 'sharing': six.string_types
+}
+
+SHARING_OPTIONS = ['public', 'private', 'secret']
+
+
+def sign_in(username, api_key, **kwargs):
+ """
+ Set set session credentials and config (not saved to file).
+
+ If unspecified, credentials and config are searched for in `.plotly` dir.
+
+ :param (str) username: The username you'd use to sign in to Plotly
+ :param (str) api_key: The api key associated with above username
+ :param (list|optional) stream_ids: Stream tokens for above credentials
+ :param (str|optional) proxy_username: The un associated with with your Proxy
+ :param (str|optional) proxy_password: The pw associated with your Proxy un
+
+ :param (str|optional) plotly_domain:
+ :param (str|optional) plotly_streaming_domain:
+ :param (str|optional) plotly_api_domain:
+ :param (bool|optional) plotly_ssl_verification:
+ :param (bool|optional) plotly_proxy_authorization:
+ :param (bool|optional) world_readable:
+
+ """
+ # TODO: verify these _credentials with plotly
+
+ # kwargs will contain all our info
+ kwargs.update(username=username, api_key=api_key)
+
+ # raise error if key isn't valid anywhere
+ for key in kwargs:
+ if key not in CREDENTIALS_KEYS and key not in CONFIG_KEYS:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "{} is not a valid config or credentials key".format(key)
+ )
+
+ # add credentials, raise error if type is wrong.
+ for key in CREDENTIALS_KEYS:
+ if key in kwargs:
+ if not isinstance(kwargs[key], CREDENTIALS_KEYS[key]):
+ raise _plotly_utils.exceptions.PlotlyError(
+ "{} must be of type '{}'"
+ .format(key, CREDENTIALS_KEYS[key])
+ )
+ _session['credentials'][key] = kwargs[key]
+
+ # add config, raise error if type is wrong.
+ for key in CONFIG_KEYS:
+ if key in kwargs:
+ if not isinstance(kwargs[key], CONFIG_KEYS[key]):
+ raise _plotly_utils.exceptions.PlotlyError("{} must be of type '{}'"
+ .format(key, CONFIG_KEYS[key]))
+ _session['config'][key] = kwargs.get(key)
+
+ # add plot options, raise error if type is wrong.
+ for key in PLOT_OPTIONS:
+ if key in kwargs:
+ if not isinstance(kwargs[key], CONFIG_KEYS[key]):
+ raise _plotly_utils.exceptions.PlotlyError("{} must be of type '{}'"
+ .format(key, CONFIG_KEYS[key]))
+ _session['plot_options'][key] = kwargs.get(key)
+
+
+def update_session_plot_options(**kwargs):
+ """
+ Update the _session plot_options
+
+ :param (str|optional) filename: What the file will be named in Plotly
+ :param (str|optional) fileopt: 'overwrite', 'append', 'new', or 'extend'
+ :param (bool|optional) world_readable: Make public or private.
+ :param (dict|optional) sharing: 'public', 'private', 'secret'
+ :param (bool|optional) auto_open: For `plot`, open in new browser tab?
+ :param (bool|optional) validate: Error locally if data doesn't pass?
+
+ """
+ # raise exception if key is invalid or value is the wrong type
+ for key in kwargs:
+ if key not in PLOT_OPTIONS:
+ raise _plotly_utils.exceptions.PlotlyError(
+ "{} is not a valid config or plot option key".format(key)
+ )
+ if not isinstance(kwargs[key], PLOT_OPTIONS[key]):
+ raise _plotly_utils.exceptions.PlotlyError("{} must be of type '{}'"
+ .format(key, PLOT_OPTIONS[key]))
+
+ # raise exception if sharing is invalid
+ if (key == 'sharing' and not (kwargs[key] in SHARING_OPTIONS)):
+ raise _plotly_utils.exceptions.PlotlyError("'{0}' must be of either '{1}', '{2}'"
+ " or '{3}'"
+ .format(key, *SHARING_OPTIONS))
+
+ # update local _session dict with new plot options
+ _session['plot_options'].update(kwargs)
+
+
+def get_session_plot_options():
+ """ Returns a copy of the user supplied plot options.
+ Use `update_plot_options()` to change.
+ """
+ return copy.deepcopy(_session['plot_options'])
+
+
+def get_session_config():
+ """Returns either module config or file config."""
+ return copy.deepcopy(_session['config'])
+
+
+def get_session_credentials():
+ """Returns the credentials that will be sent to plotly."""
+ return copy.deepcopy(_session['credentials'])
diff --git a/chart_studio/tests/__init__.py b/chart_studio/tests/__init__.py
new file mode 100644
index 00000000000..950eaad7d6f
--- /dev/null
+++ b/chart_studio/tests/__init__.py
@@ -0,0 +1,6 @@
+try:
+ # Set matplotlib backend once here
+ import matplotlib
+ matplotlib.use('Agg')
+except:
+ pass
diff --git a/chart_studio/tests/test_core/__init__.py b/chart_studio/tests/test_core/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_core/test_tools/__init__.py b/chart_studio/tests/test_core/test_tools/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_core/test_tools/test_configuration.py b/chart_studio/tests/test_core/test_tools/test_configuration.py
new file mode 100644
index 00000000000..9215b008e47
--- /dev/null
+++ b/chart_studio/tests/test_core/test_tools/test_configuration.py
@@ -0,0 +1,16 @@
+from __future__ import absolute_import
+
+from unittest import TestCase
+
+from chart_studio.files import CONFIG_FILE, FILE_CONTENT
+from chart_studio.tools import get_config_defaults
+
+
+class TestGetConfigDefaults(TestCase):
+
+ def test_config_dict_is_equivalent_copy(self):
+
+ original = FILE_CONTENT[CONFIG_FILE]
+ copy = get_config_defaults()
+ self.assertIsNot(copy, original)
+ self.assertEqual(copy, original)
diff --git a/chart_studio/tests/test_core/test_tools/test_file_tools.py b/chart_studio/tests/test_core/test_tools/test_file_tools.py
new file mode 100644
index 00000000000..3f5549625ca
--- /dev/null
+++ b/chart_studio/tests/test_core/test_tools/test_file_tools.py
@@ -0,0 +1,105 @@
+from chart_studio import tools
+from chart_studio.tests.utils import PlotlyTestCase
+
+import warnings
+
+
+class FileToolsTest(PlotlyTestCase):
+
+ def test_set_config_file_all_entries(self):
+
+ # Check set_config and get_config return the same values
+
+ domain, streaming_domain, api, sharing = ('this', 'thing',
+ 'that', 'private')
+ ssl_verify, proxy_auth, world_readable, auto_open = (True, True,
+ False, False)
+ tools.set_config_file(plotly_domain=domain,
+ plotly_streaming_domain=streaming_domain,
+ plotly_api_domain=api,
+ plotly_ssl_verification=ssl_verify,
+ plotly_proxy_authorization=proxy_auth,
+ world_readable=world_readable,
+ auto_open=auto_open)
+ config = tools.get_config_file()
+ self.assertEqual(config['plotly_domain'], domain)
+ self.assertEqual(config['plotly_streaming_domain'], streaming_domain)
+ self.assertEqual(config['plotly_api_domain'], api)
+ self.assertEqual(config['plotly_ssl_verification'], ssl_verify)
+ self.assertEqual(config['plotly_proxy_authorization'], proxy_auth)
+ self.assertEqual(config['world_readable'], world_readable)
+ self.assertEqual(config['sharing'], sharing)
+ self.assertEqual(config['auto_open'], auto_open)
+ tools.reset_config_file()
+
+ def test_set_config_file_two_entries(self):
+
+ # Check set_config and get_config given only two entries return the
+ # same values
+
+ domain, streaming_domain = 'this', 'thing'
+ tools.set_config_file(plotly_domain=domain,
+ plotly_streaming_domain=streaming_domain)
+ config = tools.get_config_file()
+ self.assertEqual(config['plotly_domain'], domain)
+ self.assertEqual(config['plotly_streaming_domain'], streaming_domain)
+ tools.reset_config_file()
+
+ def test_set_config_file_world_readable(self):
+
+ # Return TypeError when world_readable type is not a bool
+
+ kwargs = {'world_readable': 'True'}
+ self.assertRaises(TypeError, tools.set_config_file, **kwargs)
+
+ def test_set_config_expected_warning_msg(self):
+
+ # Check that UserWarning is being called with http plotly_domain
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ kwargs = {'plotly_domain': 'http://www.foo-bar.com'}
+ tools.set_config_file(**kwargs)
+ assert len(w) == 1
+ assert issubclass(w[-1].category, UserWarning)
+ assert "plotly_domain" in str(w[-1].message)
+
+
+ def test_set_config_no_warning_msg_if_plotly_domain_is_https(self):
+
+ # Check that no UserWarning is being called with https plotly_domain
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ kwargs = {'plotly_domain': 'https://www.foo-bar.com'}
+ tools.set_config_file(**kwargs)
+ assert len(w) == 0
+
+
+ def test_reset_config_file(self):
+
+ # Check reset_config and get_config return the same values
+
+ tools.reset_config_file()
+ config = tools.get_config_file()
+ self.assertEqual(config['plotly_domain'], 'https://plot.ly')
+ self.assertEqual(config['plotly_streaming_domain'], 'stream.plot.ly')
+
+ def test_get_credentials_file(self):
+
+ # Check get_credentials returns all the keys
+
+ original_creds = tools.get_credentials_file()
+ expected = ['username', 'stream_ids', 'api_key', 'proxy_username',
+ 'proxy_password']
+ self.assertTrue(all(x in original_creds for x in expected))
+
+ def test_reset_credentials_file(self):
+
+ # Check get_cred return all the keys
+
+ tools.reset_credentials_file()
+ reset_creds = tools.get_credentials_file()
+ expected = ['username', 'stream_ids', 'api_key', 'proxy_username',
+ 'proxy_password']
+ self.assertTrue(all(x in reset_creds for x in expected))
diff --git a/chart_studio/tests/test_core/test_tools/test_get_embed.py b/chart_studio/tests/test_core/test_tools/test_get_embed.py
new file mode 100644
index 00000000000..7b49365e54f
--- /dev/null
+++ b/chart_studio/tests/test_core/test_tools/test_get_embed.py
@@ -0,0 +1,45 @@
+from __future__ import absolute_import
+
+from unittest import TestCase
+
+from nose.tools import raises
+
+import chart_studio.tools as tls
+from _plotly_utils.exceptions import PlotlyError
+
+
+def test_get_valid_embed():
+ url = 'https://plot.ly/~PlotBot/82/'
+ tls.get_embed(url)
+
+
+@raises(PlotlyError)
+def test_get_invalid_embed():
+ url = 'https://plot.ly/~PlotBot/a/'
+ tls.get_embed(url)
+
+
+class TestGetEmbed(TestCase):
+
+ def test_get_embed_url_with_share_key(self):
+
+ # Check the embed url for url with share_key included
+
+ get_embed_return = tls.get_embed('https://plot.ly/~neda/6572' +
+ '?share_key=AH4MyPlyDyDWYA2cM2kj2m')
+ expected_get_embed = ("").format(plotly_rest_url="https://" +
+ "plot.ly",
+ file_owner="neda",
+ file_id="6572",
+ share_key="AH4MyPlyDyDWYA2" +
+ "cM2kj2m",
+ iframe_height=525,
+ iframe_width="100%")
+ self.assertEqual(get_embed_return, expected_get_embed)
diff --git a/chart_studio/tests/test_optional/__init__.py b/chart_studio/tests/test_optional/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_optional/test_grid/__init__.py b/chart_studio/tests/test_optional/test_grid/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_optional/test_grid/test_grid.py b/chart_studio/tests/test_optional/test_grid/test_grid.py
new file mode 100644
index 00000000000..aaf48967843
--- /dev/null
+++ b/chart_studio/tests/test_optional/test_grid/test_grid.py
@@ -0,0 +1,32 @@
+"""
+test_grid:
+==========
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+from unittest import TestCase
+
+from chart_studio.exceptions import InputError
+from chart_studio.grid_objs import Grid
+
+import pandas as pd
+
+
+class TestDataframeToGrid(TestCase):
+
+ # Test duplicate columns
+ def test_duplicate_columns(self):
+ df = pd.DataFrame([[1, 'a'], [2, 'b']],
+ columns=['col_1', 'col_1'])
+
+ expected_message = (
+ "Yikes, plotly grids currently "
+ "can't have duplicate column names. Rename "
+ "the column \"{}\" and try again.".format('col_1')
+ )
+
+ with self.assertRaisesRegexp(InputError, expected_message):
+ Grid(df)
diff --git a/chart_studio/tests/test_optional/test_ipython/__init__.py b/chart_studio/tests/test_optional/test_ipython/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_optional/test_ipython/test_widgets.py b/chart_studio/tests/test_optional/test_ipython/test_widgets.py
new file mode 100644
index 00000000000..8cd365d1e49
--- /dev/null
+++ b/chart_studio/tests/test_optional/test_ipython/test_widgets.py
@@ -0,0 +1,14 @@
+"""
+Module for testing IPython widgets
+
+"""
+from __future__ import absolute_import
+
+from unittest import TestCase
+
+from chart_studio.widgets import GraphWidget
+
+class TestWidgets(TestCase):
+
+ def test_instantiate_graph_widget(self):
+ widget = GraphWidget
diff --git a/chart_studio/tests/test_optional/test_matplotlylib/__init__.py b/chart_studio/tests/test_optional/test_matplotlylib/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_optional/test_matplotlylib/test_plot_mpl.py b/chart_studio/tests/test_optional/test_matplotlylib/test_plot_mpl.py
new file mode 100644
index 00000000000..83732461ba9
--- /dev/null
+++ b/chart_studio/tests/test_optional/test_matplotlylib/test_plot_mpl.py
@@ -0,0 +1,55 @@
+"""
+test_plot_mpl:
+==============
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+from nose.plugins.attrib import attr
+from nose.tools import raises
+
+import _plotly_utils.exceptions
+from plotly import optional_imports
+from chart_studio.plotly import plotly as py
+from unittest import TestCase
+
+matplotlylib = optional_imports.get_module('plotly.matplotlylib')
+
+if matplotlylib:
+ import matplotlib.pyplot as plt
+
+
+@attr('matplotlib')
+class PlotMPLTest(TestCase):
+ def setUp(self):
+ py.sign_in('PlotlyImageTest', '786r5mecv0',
+ plotly_domain='https://plot.ly')
+
+ @raises(_plotly_utils.exceptions.PlotlyGraphObjectError)
+ def test_update_type_error(self):
+ fig, ax = plt.subplots()
+ ax.plot([1, 2, 3])
+ update = []
+ py.plot_mpl(fig, update=update, filename="nosetests", auto_open=False)
+
+ @raises(KeyError)
+ def test_update_validation_error(self):
+ fig, ax = plt.subplots()
+ ax.plot([1, 2, 3])
+ update = {'invalid': 'anything'}
+ py.plot_mpl(fig, update=update, filename="nosetests", auto_open=False)
+
+ @attr('slow')
+ def test_update(self):
+ fig, ax = plt.subplots()
+ ax.plot([1, 2, 3])
+ title = 'new title'
+ update = {'layout': {'title': title}}
+ url = py.plot_mpl(fig, update=update, filename="nosetests",
+ auto_open=False)
+ un = url.replace("https://plot.ly/~", "").split('/')[0]
+ fid = url.replace("https://plot.ly/~", "").split('/')[1]
+ pfig = py.get_figure(un, fid)
+ assert pfig['layout']['title']['text'] == title
diff --git a/chart_studio/tests/test_optional/test_utils/__init__.py b/chart_studio/tests/test_optional/test_utils/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_optional/test_utils/test_utils.py b/chart_studio/tests/test_optional/test_utils/test_utils.py
new file mode 100644
index 00000000000..49e16cadc16
--- /dev/null
+++ b/chart_studio/tests/test_optional/test_utils/test_utils.py
@@ -0,0 +1,25 @@
+import json as _json
+
+import _plotly_utils.utils
+from chart_studio.grid_objs import Column
+from plotly import utils
+from plotly.tests.test_optional.test_utils.test_utils import numeric_list, \
+ mixed_list, np_list
+
+
+def test_column_json_encoding():
+ columns = [
+ Column(numeric_list, 'col 1'),
+ Column(mixed_list, 'col 2'),
+ Column(np_list, 'col 3')
+ ]
+ json_columns = _json.dumps(
+ columns, cls=_plotly_utils.utils.PlotlyJSONEncoder, sort_keys=True
+ )
+ assert('[{"data": [1, 2, 3], "name": "col 1"}, '
+ '{"data": [1, "A", "2014-01-05", '
+ '"2014-01-05 01:01:01", '
+ '"2014-01-05 01:01:01.000001"], '
+ '"name": "col 2"}, '
+ '{"data": [1, 2, 3, null, null, null, '
+ '"2014-01-05"], "name": "col 3"}]' == json_columns)
\ No newline at end of file
diff --git a/chart_studio/tests/test_plot_ly/__init__.py b/chart_studio/tests/test_plot_ly/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_api/__init__.py b/chart_studio/tests/test_plot_ly/test_api/__init__.py
new file mode 100644
index 00000000000..5a2ce755612
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/__init__.py
@@ -0,0 +1,66 @@
+from __future__ import absolute_import
+
+from requests import Response
+
+from chart_studio.session import sign_in
+from chart_studio.tests.utils import PlotlyTestCase
+
+import sys
+
+# import from mock
+if sys.version_info.major == 3 and sys.version_info.minor >= 3:
+ from unittest.mock import patch
+else:
+ from mock import patch
+
+
+class PlotlyApiTestCase(PlotlyTestCase):
+
+ def mock(self, path_string):
+ patcher = patch(path_string)
+ new_mock = patcher.start()
+ self.addCleanup(patcher.stop)
+ return new_mock
+
+ def setUp(self):
+
+ super(PlotlyApiTestCase, self).setUp()
+
+ self.username = 'foo'
+ self.api_key = 'bar'
+
+ self.proxy_username = 'cnet'
+ self.proxy_password = 'hoopla'
+ self.stream_ids = ['heyThere']
+
+ self.plotly_api_domain = 'https://api.do.not.exist'
+ self.plotly_domain = 'https://who.am.i'
+ self.plotly_proxy_authorization = False
+ self.plotly_streaming_domain = 'stream.does.not.exist'
+ self.plotly_ssl_verification = True
+
+ sign_in(
+ username=self.username,
+ api_key=self.api_key,
+ proxy_username=self.proxy_username,
+ proxy_password=self.proxy_password,
+ stream_ids=self.stream_ids,
+ plotly_domain=self.plotly_domain,
+ plotly_api_domain=self.plotly_api_domain,
+ plotly_streaming_domain=self.plotly_streaming_domain,
+ plotly_proxy_authorization=self.plotly_proxy_authorization,
+ plotly_ssl_verification=self.plotly_ssl_verification
+ )
+
+ def to_bytes(self, string):
+ try:
+ return string.encode('utf-8')
+ except AttributeError:
+ return string
+
+ def get_response(self, content=b'', status_code=200):
+ response = Response()
+ response.status_code = status_code
+ response._content = content
+ response.encoding = 'utf-8'
+ return response
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v1/__init__.py b/chart_studio/tests/test_plot_ly/test_api/test_v1/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v1/test_clientresp.py b/chart_studio/tests/test_plot_ly/test_api/test_v1/test_clientresp.py
new file mode 100644
index 00000000000..2ce3fe66df2
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v1/test_clientresp.py
@@ -0,0 +1,62 @@
+from __future__ import absolute_import
+
+from plotly import version
+from chart_studio.api.v1 import clientresp
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class Duck(object):
+ def to_plotly_json(self):
+ return 'what else floats?'
+
+
+class ClientrespTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(ClientrespTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v1.utils.requests.request')
+ self.request_mock.return_value = self.get_response(b'{}', 200)
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v1.utils.validate_response')
+
+ def test_data_only(self):
+ data = [{'y': [3, 5], 'name': Duck()}]
+ clientresp(data)
+ assert self.request_mock.call_count == 1
+
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(url, '{}/clientresp'.format(self.plotly_domain))
+ expected_data = ({
+ 'origin': 'plot',
+ 'args': '[{"name": "what else floats?", "y": [3, 5]}]',
+ 'platform': 'python', 'version': version.stable_semver(), 'key': 'bar',
+ 'kwargs': '{}', 'un': 'foo'
+ })
+ self.assertEqual(kwargs['data'], expected_data)
+ self.assertTrue(kwargs['verify'])
+ self.assertEqual(kwargs['headers'], {})
+
+ def test_data_and_kwargs(self):
+ data = [{'y': [3, 5], 'name': Duck()}]
+ clientresp_kwargs = {'layout': {'title': 'mah plot'}, 'filename': 'ok'}
+ clientresp(data, **clientresp_kwargs)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(url, '{}/clientresp'.format(self.plotly_domain))
+ expected_data = ({
+ 'origin': 'plot',
+ 'args': '[{"name": "what else floats?", "y": [3, 5]}]',
+ 'platform': 'python', 'version': version.stable_semver(), 'key': 'bar',
+ 'kwargs': '{"filename": "ok", "layout": {"title": "mah plot"}}',
+ 'un': 'foo'
+ })
+ self.assertEqual(kwargs['data'], expected_data)
+ self.assertTrue(kwargs['verify'])
+ self.assertEqual(kwargs['headers'], {})
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v1/test_utils.py b/chart_studio/tests/test_plot_ly/test_api/test_v1/test_utils.py
new file mode 100644
index 00000000000..8d599f973d6
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v1/test_utils.py
@@ -0,0 +1,181 @@
+from __future__ import absolute_import
+
+from requests import Response
+from requests.compat import json as _json
+from requests.exceptions import ConnectionError
+
+from chart_studio.api.utils import to_native_utf8_string
+from chart_studio.api.v1 import utils
+from chart_studio.exceptions import PlotlyRequestError
+from _plotly_utils.exceptions import PlotlyError
+from chart_studio.session import sign_in
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+from chart_studio.tests.utils import PlotlyTestCase
+
+import sys
+
+# import from mock, MagicMock
+if sys.version_info.major == 3 and sys.version_info.minor >= 3:
+ from unittest.mock import MagicMock, patch
+else:
+ from mock import patch, MagicMock
+
+
+class ValidateResponseTest(PlotlyApiTestCase):
+
+ def test_validate_ok(self):
+ try:
+ utils.validate_response(self.get_response(content=b'{}'))
+ except PlotlyRequestError:
+ self.fail('Expected this to pass!')
+
+ def test_validate_not_ok(self):
+ bad_status_codes = (400, 404, 500)
+ for bad_status_code in bad_status_codes:
+ response = self.get_response(content=b'{}',
+ status_code=bad_status_code)
+ self.assertRaises(PlotlyRequestError, utils.validate_response,
+ response)
+
+ def test_validate_no_content(self):
+
+ # We shouldn't flake if the response has no content.
+
+ response = self.get_response(content=b'', status_code=200)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, 'No Content')
+ self.assertEqual(e.status_code, 200)
+ self.assertEqual(e.content, b'')
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_non_json_content(self):
+ response = self.get_response(content=b'foobar', status_code=200)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, 'foobar')
+ self.assertEqual(e.status_code, 200)
+ self.assertEqual(e.content, b'foobar')
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_array(self):
+ content = self.to_bytes(_json.dumps([1, 2, 3]))
+ response = self.get_response(content=content, status_code=200)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, to_native_utf8_string(content))
+ self.assertEqual(e.status_code, 200)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_dict_no_error(self):
+ content = self.to_bytes(_json.dumps({'foo': 'bar'}))
+ response = self.get_response(content=content, status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, to_native_utf8_string(content))
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_dict_error_empty(self):
+ content = self.to_bytes(_json.dumps({'error': ''}))
+ response = self.get_response(content=content, status_code=200)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError:
+ self.fail('Expected this not to raise!')
+
+ def test_validate_json_content_dict_one_error_ok(self):
+ content = self.to_bytes(_json.dumps({'error': 'not ok!'}))
+ response = self.get_response(content=content, status_code=200)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, 'not ok!')
+ self.assertEqual(e.status_code, 200)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+
+class GetHeadersTest(PlotlyTestCase):
+
+ def setUp(self):
+ super(GetHeadersTest, self).setUp()
+ self.domain = 'https://foo.bar'
+ self.username = 'hodor'
+ self.api_key = 'secret'
+ sign_in(self.username, self.api_key, proxy_username='kleen-kanteen',
+ proxy_password='hydrated', plotly_proxy_authorization=False)
+
+ def test_normal_auth(self):
+ headers = utils.get_headers()
+ expected_headers = {}
+ self.assertEqual(headers, expected_headers)
+
+ def test_proxy_auth(self):
+ sign_in(self.username, self.api_key, plotly_proxy_authorization=True)
+ headers = utils.get_headers()
+ expected_headers = {
+ 'authorization': 'Basic a2xlZW4ta2FudGVlbjpoeWRyYXRlZA=='
+ }
+ self.assertEqual(headers, expected_headers)
+
+
+class RequestTest(PlotlyTestCase):
+
+ def setUp(self):
+ super(RequestTest, self).setUp()
+ self.domain = 'https://foo.bar'
+ self.username = 'hodor'
+ self.api_key = 'secret'
+ sign_in(self.username, self.api_key, proxy_username='kleen-kanteen',
+ proxy_password='hydrated', plotly_proxy_authorization=False)
+
+ # Mock the actual api call, we don't want to do network tests here.
+ patcher = patch('chart_studio.api.v1.utils.requests.request')
+ self.request_mock = patcher.start()
+ self.addCleanup(patcher.stop)
+ self.request_mock.return_value = MagicMock(Response)
+
+ # Mock the validation function since we test that elsewhere.
+ patcher = patch('chart_studio.api.v1.utils.validate_response')
+ self.validate_response_mock = patcher.start()
+ self.addCleanup(patcher.stop)
+
+ self.method = 'get'
+ self.url = 'https://foo.bar.does.not.exist.anywhere'
+
+ def test_request_with_json(self):
+
+ # You can pass along non-native objects in the `json` kwarg for a
+ # requests.request, however, V1 packs up json objects a little
+ # differently, so we don't allow such requests.
+
+ self.assertRaises(PlotlyError, utils.request, self.method,
+ self.url, json={})
+
+ def test_request_with_ConnectionError(self):
+
+ # requests can flake out and not return a response object, we want to
+ # make sure we remain consistent with our errors.
+
+ self.request_mock.side_effect = ConnectionError()
+ self.assertRaises(PlotlyRequestError, utils.request, self.method,
+ self.url)
+
+ def test_request_validate_response(self):
+
+ # Finally, we check details elsewhere, but make sure we do validate.
+
+ utils.request(self.method, self.url)
+ assert self.validate_response_mock.call_count == 1
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/__init__.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_files.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_files.py
new file mode 100644
index 00000000000..e911af4cbc9
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_files.py
@@ -0,0 +1,104 @@
+from __future__ import absolute_import
+
+from chart_studio.api.v2 import files
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class FilesTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(FilesTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v2.utils.validate_response')
+
+ def test_retrieve(self):
+ files.retrieve('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/files/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {})
+
+ def test_retrieve_share_key(self):
+ files.retrieve('hodor:88', share_key='foobar')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/files/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {'share_key': 'foobar'})
+
+ def test_update(self):
+ new_filename = '..zzZ ..zzZ'
+ files.update('hodor:88', body={'filename': new_filename})
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'put')
+ self.assertEqual(
+ url, '{}/v2/files/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['data'],
+ '{{"filename": "{}"}}'.format(new_filename))
+
+ def test_trash(self):
+ files.trash('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/files/hodor:88/trash'.format(self.plotly_api_domain)
+ )
+
+ def test_restore(self):
+ files.restore('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/files/hodor:88/restore'.format(self.plotly_api_domain)
+ )
+
+ def test_permanent_delete(self):
+ files.permanent_delete('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'delete')
+ self.assertEqual(
+ url,
+ '{}/v2/files/hodor:88/permanent_delete'
+ .format(self.plotly_api_domain)
+ )
+
+ def test_lookup(self):
+
+ # requests does urlencode, so don't worry about the `' '` character!
+
+ path = '/mah plot'
+ parent = 43
+ user = 'someone'
+ exists = True
+ files.lookup(path=path, parent=parent, user=user, exists=exists)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ expected_params = {'path': path, 'parent': parent, 'exists': 'true',
+ 'user': user}
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/files/lookup'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], expected_params)
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_folders.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_folders.py
new file mode 100644
index 00000000000..0d0780f2b22
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_folders.py
@@ -0,0 +1,114 @@
+from __future__ import absolute_import
+
+from chart_studio.api.v2 import folders
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class FoldersTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(FoldersTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v2.utils.validate_response')
+
+ def test_create(self):
+ path = '/foo/man/bar/'
+ folders.create({'path': path})
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(url, '{}/v2/folders'.format(self.plotly_api_domain))
+ self.assertEqual(kwargs['data'], '{{"path": "{}"}}'.format(path))
+
+ def test_retrieve(self):
+ folders.retrieve('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/folders/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {})
+
+ def test_retrieve_share_key(self):
+ folders.retrieve('hodor:88', share_key='foobar')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/folders/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {'share_key': 'foobar'})
+
+ def test_update(self):
+ new_filename = '..zzZ ..zzZ'
+ folders.update('hodor:88', body={'filename': new_filename})
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'put')
+ self.assertEqual(
+ url, '{}/v2/folders/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['data'],
+ '{{"filename": "{}"}}'.format(new_filename))
+
+ def test_trash(self):
+ folders.trash('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/folders/hodor:88/trash'.format(self.plotly_api_domain)
+ )
+
+ def test_restore(self):
+ folders.restore('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/folders/hodor:88/restore'.format(self.plotly_api_domain)
+ )
+
+ def test_permanent_delete(self):
+ folders.permanent_delete('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'delete')
+ self.assertEqual(
+ url,
+ '{}/v2/folders/hodor:88/permanent_delete'
+ .format(self.plotly_api_domain)
+ )
+
+ def test_lookup(self):
+
+ # requests does urlencode, so don't worry about the `' '` character!
+
+ path = '/mah folder'
+ parent = 43
+ user = 'someone'
+ exists = True
+ folders.lookup(path=path, parent=parent, user=user, exists=exists)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ expected_params = {'path': path, 'parent': parent, 'exists': 'true',
+ 'user': user}
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/folders/lookup'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], expected_params)
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_grids.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_grids.py
new file mode 100644
index 00000000000..32e3ea3cfe1
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_grids.py
@@ -0,0 +1,185 @@
+from __future__ import absolute_import
+
+from requests.compat import json as _json
+
+from chart_studio.api.v2 import grids
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class GridsTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(GridsTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v2.utils.validate_response')
+
+ def test_create(self):
+ filename = 'a grid'
+ grids.create({'filename': filename})
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(url, '{}/v2/grids'.format(self.plotly_api_domain))
+ self.assertEqual(
+ kwargs['data'], '{{"filename": "{}"}}'.format(filename)
+ )
+
+ def test_retrieve(self):
+ grids.retrieve('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {})
+
+ def test_retrieve_share_key(self):
+ grids.retrieve('hodor:88', share_key='foobar')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {'share_key': 'foobar'})
+
+ def test_update(self):
+ new_filename = '..zzZ ..zzZ'
+ grids.update('hodor:88', body={'filename': new_filename})
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'put')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['data'],
+ '{{"filename": "{}"}}'.format(new_filename))
+
+ def test_trash(self):
+ grids.trash('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88/trash'.format(self.plotly_api_domain)
+ )
+
+ def test_restore(self):
+ grids.restore('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88/restore'.format(self.plotly_api_domain)
+ )
+
+ def test_permanent_delete(self):
+ grids.permanent_delete('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'delete')
+ self.assertEqual(
+ url,
+ '{}/v2/grids/hodor:88/permanent_delete'
+ .format(self.plotly_api_domain)
+ )
+
+ def test_lookup(self):
+
+ # requests does urlencode, so don't worry about the `' '` character!
+
+ path = '/mah grid'
+ parent = 43
+ user = 'someone'
+ exists = True
+ grids.lookup(path=path, parent=parent, user=user, exists=exists)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ expected_params = {'path': path, 'parent': parent, 'exists': 'true',
+ 'user': user}
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/grids/lookup'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], expected_params)
+
+ def test_col_create(self):
+ cols = [
+ {'name': 'foo', 'data': [1, 2, 3]},
+ {'name': 'bar', 'data': ['b', 'a', 'r']},
+ ]
+ body = {'cols': _json.dumps(cols, sort_keys=True)}
+ grids.col_create('hodor:88', body)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88/col'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['data'], _json.dumps(body, sort_keys=True))
+
+ def test_col_retrieve(self):
+ grids.col_retrieve('hodor:88', 'aaaaaa,bbbbbb')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88/col'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {'uid': 'aaaaaa,bbbbbb'})
+
+ def test_col_update(self):
+ cols = [
+ {'name': 'foo', 'data': [1, 2, 3]},
+ {'name': 'bar', 'data': ['b', 'a', 'r']},
+ ]
+ body = {'cols': _json.dumps(cols, sort_keys=True)}
+ grids.col_update('hodor:88', 'aaaaaa,bbbbbb', body)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'put')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88/col'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {'uid': 'aaaaaa,bbbbbb'})
+ self.assertEqual(kwargs['data'], _json.dumps(body, sort_keys=True))
+
+ def test_col_delete(self):
+ grids.col_delete('hodor:88', 'aaaaaa,bbbbbb')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'delete')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88/col'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {'uid': 'aaaaaa,bbbbbb'})
+
+ def test_row(self):
+ body = {'rows': [[1, 'A'], [2, 'B']]}
+ grids.row('hodor:88', body)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/grids/hodor:88/row'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['data'], _json.dumps(body, sort_keys=True))
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_images.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_images.py
new file mode 100644
index 00000000000..5830a36ed2b
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_images.py
@@ -0,0 +1,41 @@
+from __future__ import absolute_import
+
+from requests.compat import json as _json
+
+from chart_studio.api.v2 import images
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class ImagesTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(ImagesTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v2.utils.validate_response')
+
+ def test_create(self):
+
+ body = {
+ "figure": {
+ "data": [{"y": [10, 10, 2, 20]}],
+ "layout": {"width": 700}
+ },
+ "width": 1000,
+ "height": 500,
+ "format": "png",
+ "scale": 4,
+ "encoded": False
+ }
+
+ images.create(body)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(url, '{}/v2/images'.format(self.plotly_api_domain))
+ self.assertEqual(kwargs['data'], _json.dumps(body, sort_keys=True))
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_plot_schema.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_plot_schema.py
new file mode 100644
index 00000000000..e79798c504c
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_plot_schema.py
@@ -0,0 +1,30 @@
+from __future__ import absolute_import
+
+from chart_studio.api.v2 import plot_schema
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class PlotSchemaTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(PlotSchemaTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v2.utils.validate_response')
+
+ def test_retrieve(self):
+
+ plot_schema.retrieve('some-hash', timeout=400)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/plot-schema'.format(self.plotly_api_domain)
+ )
+ self.assertTrue(kwargs['timeout'])
+ self.assertEqual(kwargs['params'], {'sha1': 'some-hash'})
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_plots.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_plots.py
new file mode 100644
index 00000000000..ed4157226f1
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_plots.py
@@ -0,0 +1,116 @@
+from __future__ import absolute_import
+
+from chart_studio.api.v2 import plots
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class PlotsTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(PlotsTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v2.utils.validate_response')
+
+ def test_create(self):
+ filename = 'a plot'
+ plots.create({'filename': filename})
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(url, '{}/v2/plots'.format(self.plotly_api_domain))
+ self.assertEqual(
+ kwargs['data'], '{{"filename": "{}"}}'.format(filename)
+ )
+
+ def test_retrieve(self):
+ plots.retrieve('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/plots/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {})
+
+ def test_retrieve_share_key(self):
+ plots.retrieve('hodor:88', share_key='foobar')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/plots/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], {'share_key': 'foobar'})
+
+ def test_update(self):
+ new_filename = '..zzZ ..zzZ'
+ plots.update('hodor:88', body={'filename': new_filename})
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'put')
+ self.assertEqual(
+ url, '{}/v2/plots/hodor:88'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['data'],
+ '{{"filename": "{}"}}'.format(new_filename))
+
+ def test_trash(self):
+ plots.trash('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/plots/hodor:88/trash'.format(self.plotly_api_domain)
+ )
+
+ def test_restore(self):
+ plots.restore('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'post')
+ self.assertEqual(
+ url, '{}/v2/plots/hodor:88/restore'.format(self.plotly_api_domain)
+ )
+
+ def test_permanent_delete(self):
+ plots.permanent_delete('hodor:88')
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'delete')
+ self.assertEqual(
+ url,
+ '{}/v2/plots/hodor:88/permanent_delete'
+ .format(self.plotly_api_domain)
+ )
+
+ def test_lookup(self):
+
+ # requests does urlencode, so don't worry about the `' '` character!
+
+ path = '/mah plot'
+ parent = 43
+ user = 'someone'
+ exists = True
+ plots.lookup(path=path, parent=parent, user=user, exists=exists)
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ expected_params = {'path': path, 'parent': parent, 'exists': 'true',
+ 'user': user}
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/plots/lookup'.format(self.plotly_api_domain)
+ )
+ self.assertEqual(kwargs['params'], expected_params)
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_users.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_users.py
new file mode 100644
index 00000000000..5e787424437
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_users.py
@@ -0,0 +1,28 @@
+from __future__ import absolute_import
+
+from chart_studio.api.v2 import users
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class UsersTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(UsersTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.mock('chart_studio.api.v2.utils.validate_response')
+
+ def test_current(self):
+ users.current()
+ assert self.request_mock.call_count == 1
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ self.assertEqual(method, 'get')
+ self.assertEqual(
+ url, '{}/v2/users/current'.format(self.plotly_api_domain)
+ )
+ self.assertNotIn('params', kwargs)
diff --git a/chart_studio/tests/test_plot_ly/test_api/test_v2/test_utils.py b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_utils.py
new file mode 100644
index 00000000000..7ae1143cd41
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_api/test_v2/test_utils.py
@@ -0,0 +1,252 @@
+from __future__ import absolute_import
+
+from requests.compat import json as _json
+from requests.exceptions import ConnectionError
+
+from plotly import version
+from chart_studio.api.utils import to_native_utf8_string
+from chart_studio.api.v2 import utils
+from chart_studio.exceptions import PlotlyRequestError
+from chart_studio.session import sign_in
+from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase
+
+
+class MakeParamsTest(PlotlyApiTestCase):
+
+ def test_make_params(self):
+ params = utils.make_params(foo='FOO', bar=None)
+ self.assertEqual(params, {'foo': 'FOO'})
+
+ def test_make_params_empty(self):
+ params = utils.make_params(foo=None, bar=None)
+ self.assertEqual(params, {})
+
+
+class BuildUrlTest(PlotlyApiTestCase):
+
+ def test_build_url(self):
+ url = utils.build_url('cats')
+ self.assertEqual(url, '{}/v2/cats'.format(self.plotly_api_domain))
+
+ def test_build_url_id(self):
+ url = utils.build_url('cats', id='MsKitty')
+ self.assertEqual(
+ url, '{}/v2/cats/MsKitty'.format(self.plotly_api_domain)
+ )
+
+ def test_build_url_route(self):
+ url = utils.build_url('cats', route='about')
+ self.assertEqual(
+ url, '{}/v2/cats/about'.format(self.plotly_api_domain)
+ )
+
+ def test_build_url_id_route(self):
+ url = utils.build_url('cats', id='MsKitty', route='de-claw')
+ self.assertEqual(
+ url, '{}/v2/cats/MsKitty/de-claw'.format(self.plotly_api_domain)
+ )
+
+
+class ValidateResponseTest(PlotlyApiTestCase):
+
+ def test_validate_ok(self):
+ try:
+ utils.validate_response(self.get_response())
+ except PlotlyRequestError:
+ self.fail('Expected this to pass!')
+
+ def test_validate_not_ok(self):
+ bad_status_codes = (400, 404, 500)
+ for bad_status_code in bad_status_codes:
+ response = self.get_response(status_code=bad_status_code)
+ self.assertRaises(PlotlyRequestError, utils.validate_response,
+ response)
+
+ def test_validate_no_content(self):
+
+ # We shouldn't flake if the response has no content.
+
+ response = self.get_response(content=b'', status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, u'No Content')
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content.decode('utf-8'), u'')
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_non_json_content(self):
+ response = self.get_response(content=b'foobar', status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, 'foobar')
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, b'foobar')
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_array(self):
+ content = self.to_bytes(_json.dumps([1, 2, 3]))
+ response = self.get_response(content=content, status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, to_native_utf8_string(content))
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_dict_no_errors(self):
+ content = self.to_bytes(_json.dumps({'foo': 'bar'}))
+ response = self.get_response(content=content, status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, to_native_utf8_string(content))
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_dict_one_error_bad(self):
+ content = self.to_bytes(_json.dumps({'errors': [{}]}))
+ response = self.get_response(content=content, status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, to_native_utf8_string(content))
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+ content = self.to_bytes(_json.dumps({'errors': [{'message': ''}]}))
+ response = self.get_response(content=content, status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, to_native_utf8_string(content))
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_dict_one_error_ok(self):
+ content = self.to_bytes(_json.dumps(
+ {'errors': [{'message': 'not ok!'}]}))
+ response = self.get_response(content=content, status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, 'not ok!')
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+ def test_validate_json_content_dict_multiple_errors(self):
+ content = self.to_bytes(_json.dumps({'errors': [
+ {'message': 'not ok!'}, {'message': 'bad job...'}
+ ]}))
+ response = self.get_response(content=content, status_code=400)
+ try:
+ utils.validate_response(response)
+ except PlotlyRequestError as e:
+ self.assertEqual(e.message, 'not ok!\nbad job...')
+ self.assertEqual(e.status_code, 400)
+ self.assertEqual(e.content, content)
+ else:
+ self.fail('Expected this to raise!')
+
+
+class GetHeadersTest(PlotlyApiTestCase):
+
+ def test_normal_auth(self):
+ headers = utils.get_headers()
+ expected_headers = {
+ 'plotly-client-platform': 'python {}'.format(version.stable_semver()),
+ 'authorization': 'Basic Zm9vOmJhcg==',
+ 'content-type': 'application/json'
+ }
+ self.assertEqual(headers, expected_headers)
+
+ def test_proxy_auth(self):
+ sign_in(self.username, self.api_key, plotly_proxy_authorization=True)
+ headers = utils.get_headers()
+ expected_headers = {
+ 'plotly-client-platform': 'python {}'.format(version.stable_semver()),
+ 'authorization': 'Basic Y25ldDpob29wbGE=',
+ 'plotly-authorization': 'Basic Zm9vOmJhcg==',
+ 'content-type': 'application/json'
+ }
+ self.assertEqual(headers, expected_headers)
+
+
+class RequestTest(PlotlyApiTestCase):
+
+ def setUp(self):
+ super(RequestTest, self).setUp()
+
+ # Mock the actual api call, we don't want to do network tests here.
+ self.request_mock = self.mock('chart_studio.api.v2.utils.requests.request')
+ self.request_mock.return_value = self.get_response()
+
+ # Mock the validation function since we can test that elsewhere.
+ self.validate_response_mock = self.mock(
+ 'chart_studio.api.v2.utils.validate_response')
+
+ self.method = 'get'
+ self.url = 'https://foo.bar.does.not.exist.anywhere'
+
+ def test_request_with_params(self):
+
+ # urlencode transforms `True` --> `'True'`, which isn't super helpful,
+ # Our backend accepts the JS `true`, so we want `True` --> `'true'`.
+
+ params = {'foo': True, 'bar': 'True', 'baz': False, 'zap': 0}
+ utils.request(self.method, self.url, params=params)
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ expected_params = {'foo': 'true', 'bar': 'True', 'baz': 'false',
+ 'zap': 0}
+ self.assertEqual(method, self.method)
+ self.assertEqual(url, self.url)
+ self.assertEqual(kwargs['params'], expected_params)
+
+ def test_request_with_non_native_objects(self):
+
+ # We always send along json, but it may contain non-native objects like
+ # a pandas array or a Column reference. Make sure that's handled in one
+ # central place.
+
+ class Duck(object):
+ def to_plotly_json(self):
+ return 'what else floats?'
+
+ utils.request(self.method, self.url, json={'foo': [Duck(), Duck()]})
+ args, kwargs = self.request_mock.call_args
+ method, url = args
+ expected_data = '{"foo": ["what else floats?", "what else floats?"]}'
+ self.assertEqual(method, self.method)
+ self.assertEqual(url, self.url)
+ self.assertEqual(kwargs['data'], expected_data)
+ self.assertNotIn('json', kwargs)
+
+ def test_request_with_ConnectionError(self):
+
+ # requests can flake out and not return a response object, we want to
+ # make sure we remain consistent with our errors.
+
+ self.request_mock.side_effect = ConnectionError()
+ self.assertRaises(PlotlyRequestError, utils.request, self.method,
+ self.url)
+
+ def test_request_validate_response(self):
+
+ # Finally, we check details elsewhere, but make sure we do validate.
+
+ utils.request(self.method, self.url)
+ assert self.request_mock.call_count == 1
diff --git a/chart_studio/tests/test_plot_ly/test_dashboard/__init__.py b/chart_studio/tests/test_plot_ly/test_dashboard/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_dashboard/test_dashboard.py b/chart_studio/tests/test_plot_ly/test_dashboard/test_dashboard.py
new file mode 100644
index 00000000000..184f788b1fe
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_dashboard/test_dashboard.py
@@ -0,0 +1,141 @@
+"""
+test_dashboard:
+==========
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+from unittest import TestCase
+from _plotly_utils.exceptions import PlotlyError
+import chart_studio.dashboard_objs.dashboard_objs as dashboard
+
+
+class TestDashboard(TestCase):
+
+ def test_invalid_path(self):
+
+ my_box = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'AdamKulidjian:327',
+ 'shareKey': None,
+ 'title': 'box 1'
+ }
+ dash = dashboard.Dashboard()
+
+ message = (
+ "Invalid path. Your 'path' list must only contain "
+ "the strings 'first' and 'second'."
+ )
+
+ self.assertRaisesRegexp(PlotlyError, message,
+ dash._insert, my_box, 'third')
+
+ def test_box_id_none(self):
+
+ my_box = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'AdamKulidjian:327',
+ 'shareKey': None,
+ 'title': 'box 1'
+ }
+
+ dash = dashboard.Dashboard()
+ dash.insert(my_box, 'above', None)
+
+ message = (
+ "Make sure the box_id is specfied if there is at least "
+ "one box in your dashboard."
+ )
+
+ self.assertRaisesRegexp(PlotlyError, message, dash.insert,
+ my_box, 'above', None)
+
+ def test_id_not_valid(self):
+ my_box = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'AdamKulidjian:327',
+ 'shareKey': None,
+ 'title': 'box 1'
+ }
+
+ message = (
+ "Your box_id must be a number in your dashboard. To view a "
+ "representation of your dashboard run get_preview()."
+ )
+
+ dash = dashboard.Dashboard()
+ dash.insert(my_box, 'above', 1)
+
+ # insert box
+ self.assertRaisesRegexp(PlotlyError, message, dash.insert, my_box,
+ 'above', 0)
+ # get box by id
+ self.assertRaisesRegexp(PlotlyError, message, dash.get_box, 0)
+
+ # remove box
+ self.assertRaisesRegexp(PlotlyError, message, dash.remove, 0)
+
+ def test_invalid_side(self):
+ my_box = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'AdamKulidjian:327',
+ 'shareKey': None,
+ 'title': 'box 1'
+ }
+
+ message = (
+ "If there is at least one box in your dashboard, you "
+ "must specify a valid side value. You must choose from "
+ "'above', 'below', 'left', and 'right'."
+ )
+
+ dash = dashboard.Dashboard()
+ dash.insert(my_box, 'above', 0)
+
+ self.assertRaisesRegexp(PlotlyError, message, dash.insert,
+ my_box, 'somewhere', 1)
+
+ def test_dashboard_dict(self):
+ my_box = {
+ 'type': 'box',
+ 'boxType': 'plot',
+ 'fileId': 'AdamKulidjian:327',
+ 'shareKey': None,
+ 'title': 'box 1'
+ }
+
+ dash = dashboard.Dashboard()
+ dash.insert(my_box)
+ dash.insert(my_box, 'above', 1)
+
+ expected_dashboard = {
+ 'layout': {'direction': 'vertical',
+ 'first': {'direction': 'vertical',
+ 'first': {'boxType': 'plot',
+ 'fileId': 'AdamKulidjian:327',
+ 'shareKey': None,
+ 'title': 'box 1',
+ 'type': 'box'},
+ 'second': {'boxType': 'plot',
+ 'fileId': 'AdamKulidjian:327',
+ 'shareKey': None,
+ 'title': 'box 1',
+ 'type': 'box'},
+ 'size': 50,
+ 'sizeUnit': '%',
+ 'type': 'split'},
+ 'second': {'boxType': 'empty', 'type': 'box'},
+ 'size': 1500,
+ 'sizeUnit': 'px',
+ 'type': 'split'},
+ 'settings': {},
+ 'version': 2
+ }
+
+ self.assertEqual(dash['layout'], expected_dashboard['layout'])
diff --git a/chart_studio/tests/test_plot_ly/test_file/__init__.py b/chart_studio/tests/test_plot_ly/test_file/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_file/test_file.py b/chart_studio/tests/test_plot_ly/test_file/test_file.py
new file mode 100644
index 00000000000..c46383d0119
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_file/test_file.py
@@ -0,0 +1,55 @@
+"""
+test_meta:
+==========
+
+A module intended for use with Nose.
+
+"""
+import random
+import string
+
+from nose.plugins.attrib import attr
+
+from chart_studio import plotly as py
+from chart_studio.exceptions import PlotlyRequestError
+from chart_studio.tests.utils import PlotlyTestCase
+
+
+@attr('slow')
+class FolderAPITestCase(PlotlyTestCase):
+
+ def setUp(self):
+ super(FolderAPITestCase, self).setUp()
+ py.sign_in('PythonTest', 'xnyU0DEwvAQQCwHVseIL')
+
+ def _random_filename(self):
+ choice_chars = string.ascii_letters + string.digits
+ random_chars = [random.choice(choice_chars) for _ in range(10)]
+ unique_filename = 'Valid Folder ' + ''.join(random_chars)
+ return unique_filename
+
+ def test_create_folder(self):
+ try:
+ py.file_ops.mkdirs(self._random_filename())
+ except PlotlyRequestError as e:
+ self.fail('Expected this *not* to fail! Status: {}'
+ .format(e.status_code))
+
+ def test_create_nested_folders(self):
+ first_folder = self._random_filename()
+ nested_folder = '{0}/{1}'.format(first_folder, self._random_filename())
+ try:
+ py.file_ops.mkdirs(nested_folder)
+ except PlotlyRequestError as e:
+ self.fail('Expected this *not* to fail! Status: {}'
+ .format(e.status_code))
+
+ def test_duplicate_folders(self):
+ first_folder = self._random_filename()
+ py.file_ops.mkdirs(first_folder)
+ try:
+ py.file_ops.mkdirs(first_folder)
+ except PlotlyRequestError as e:
+ pass
+ else:
+ self.fail('Expected this to fail!')
diff --git a/chart_studio/tests/test_plot_ly/test_get_figure/__init__.py b/chart_studio/tests/test_plot_ly/test_get_figure/__init__.py
new file mode 100644
index 00000000000..1118eb01e82
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_get_figure/__init__.py
@@ -0,0 +1,5 @@
+import warnings
+
+
+def setup_package():
+ warnings.filterwarnings('ignore')
diff --git a/chart_studio/tests/test_plot_ly/test_get_figure/test_get_figure.py b/chart_studio/tests/test_plot_ly/test_get_figure/test_get_figure.py
new file mode 100644
index 00000000000..b23288b8b4d
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_get_figure/test_get_figure.py
@@ -0,0 +1,110 @@
+"""
+test_get_figure:
+=================
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+from unittest import skipIf
+
+import six
+from nose.plugins.attrib import attr
+
+import _plotly_utils.exceptions
+from chart_studio import exceptions
+from chart_studio.plotly import plotly as py
+from chart_studio.tests.utils import PlotlyTestCase
+
+
+def is_trivial(obj):
+ if isinstance(obj, (dict, list)):
+ if len(obj):
+ if isinstance(obj, dict):
+ tests = (is_trivial(obj[key]) for key in obj)
+ return all(tests)
+ elif isinstance(obj, list):
+ tests = (is_trivial(entry) for entry in obj)
+ return all(tests)
+ else:
+ return False
+ else:
+ return True
+ elif obj is None:
+ return True
+ else:
+ return False
+
+
+class GetFigureTest(PlotlyTestCase):
+
+ @attr('slow')
+ def test_get_figure(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ file_id = 13183
+ py.sign_in(un, ak)
+ py.get_figure('PlotlyImageTest', str(file_id))
+
+ @attr('slow')
+ def test_get_figure_with_url(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ url = "https://plot.ly/~PlotlyImageTest/13183/"
+ py.sign_in(un, ak)
+ py.get_figure(url)
+
+ def test_get_figure_invalid_1(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ url = "https://plot.ly/~PlotlyImageTest/a/"
+ py.sign_in(un, ak)
+ with self.assertRaises(exceptions.PlotlyError):
+ py.get_figure(url)
+
+ @attr('slow')
+ def test_get_figure_invalid_2(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ url = "https://plot.ly/~PlotlyImageTest/-1/"
+ py.sign_in(un, ak)
+ with self.assertRaises(exceptions.PlotlyError):
+ py.get_figure(url)
+
+ # demonstrates error if fig has invalid parts
+ def test_get_figure_invalid_3(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ url = "https://plot.ly/~PlotlyImageTest/2/"
+ py.sign_in(un, ak)
+ with self.assertRaises(ValueError):
+ py.get_figure(url)
+
+ @attr('slow')
+ def test_get_figure_does_not_exist(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ url = "https://plot.ly/~PlotlyImageTest/1000000000/"
+ py.sign_in(un, ak)
+ with self.assertRaises(_plotly_utils.exceptions.PlotlyError):
+ py.get_figure(url)
+
+ @attr('slow')
+ def test_get_figure_raw(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ file_id = 2
+ py.sign_in(un, ak)
+ py.get_figure('PlotlyImageTest', str(file_id), raw=True)
+
+
+class TestBytesVStrings(PlotlyTestCase):
+
+ @skipIf(not six.PY3, 'Decoding and missing escapes only seen in PY3')
+ def test_proper_escaping(self):
+ un = 'PlotlyImageTest'
+ ak = '786r5mecv0'
+ url = "https://plot.ly/~PlotlyImageTest/13185/"
+ py.sign_in(un, ak)
+ py.get_figure(url)
diff --git a/chart_studio/tests/test_plot_ly/test_get_requests/__init__.py b/chart_studio/tests/test_plot_ly/test_get_requests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_get_requests/test_get_requests.py b/chart_studio/tests/test_plot_ly/test_get_requests/test_get_requests.py
new file mode 100644
index 00000000000..7ca4d607b59
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_get_requests/test_get_requests.py
@@ -0,0 +1,142 @@
+"""
+test_get_requests:
+==================
+
+A module intended for use with Nose.
+
+"""
+import copy
+
+import requests
+import six
+from nose.plugins.attrib import attr
+from requests.compat import json as _json
+
+from chart_studio.tests.utils import PlotlyTestCase
+
+default_headers = {'plotly-username': '',
+ 'plotly-apikey': '',
+ 'plotly-version': '2.0',
+ 'plotly-platform': 'pythonz'}
+
+server = "https://plot.ly"
+
+
+class GetRequestsTest(PlotlyTestCase):
+
+ @attr('slow')
+ def test_user_does_not_exist(self):
+ username = 'user_does_not_exist'
+ api_key = 'invalid-apikey'
+ file_owner = 'get_test_user'
+ file_id = 0
+ hd = copy.copy(default_headers)
+ hd['plotly-username'] = username
+ hd['plotly-apikey'] = api_key
+ resource = "/apigetfile/{0}/{1}/".format(file_owner, file_id)
+ response = requests.get(server + resource, headers=hd)
+ if six.PY3:
+ content = _json.loads(response.content.decode('unicode_escape'))
+ else:
+ content = _json.loads(response.content)
+ error_message = ("Aw, snap! We don't have an account for {0}. Want to "
+ "try again? Sign in is not case sensitive."
+ .format(username))
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(content['error'], error_message)
+
+ @attr('slow')
+ def test_file_does_not_exist(self):
+ username = 'PlotlyImageTest'
+ api_key = '786r5mecv0'
+ file_owner = 'get_test_user'
+ file_id = 1000
+ hd = copy.copy(default_headers)
+ hd['plotly-username'] = username
+ hd['plotly-apikey'] = api_key
+ resource = "/apigetfile/{0}/{1}/".format(file_owner, file_id)
+ response = requests.get(server + resource, headers=hd)
+ if six.PY3:
+ content = _json.loads(response.content.decode('unicode_escape'))
+ else:
+ content = _json.loads(response.content)
+ error_message = ("Aw, snap! It looks like this file does "
+ "not exist. Want to try again?")
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(content['error'], error_message)
+
+ @attr('slow')
+ def test_wrong_api_key(self): # TODO: does this test the right thing?
+ username = 'PlotlyImageTest'
+ api_key = 'invalid-apikey'
+ file_owner = 'get_test_user'
+ file_id = 0
+ hd = copy.copy(default_headers)
+ hd['plotly-username'] = username
+ hd['plotly-apikey'] = api_key
+ resource = "/apigetfile/{0}/{1}/".format(file_owner, file_id)
+ response = requests.get(server + resource, headers=hd)
+ self.assertEqual(response.status_code, 401)
+ # TODO: check error message?
+
+ # Locked File
+ # TODO
+
+ @attr('slow')
+ def test_private_permission_defined(self):
+ username = 'PlotlyImageTest'
+ api_key = '786r5mecv0'
+ file_owner = 'get_test_user'
+ file_id = 1 # 1 is a private file
+ hd = copy.copy(default_headers)
+ hd['plotly-username'] = username
+ hd['plotly-apikey'] = api_key
+ resource = "/apigetfile/{0}/{1}/".format(file_owner, file_id)
+ response = requests.get(server + resource, headers=hd)
+ if six.PY3:
+ content = _json.loads(response.content.decode('unicode_escape'))
+ else:
+ content = _json.loads(response.content)
+ self.assertEqual(response.status_code, 403)
+
+ # Private File that is shared
+ # TODO
+
+ @attr('slow')
+ def test_missing_headers(self):
+ file_owner = 'get_test_user'
+ file_id = 0
+ resource = "/apigetfile/{0}/{1}/".format(file_owner, file_id)
+ headers = list(default_headers.keys())
+ for header in headers:
+ hd = copy.copy(default_headers)
+ del hd[header]
+ response = requests.get(server + resource, headers=hd)
+ if six.PY3:
+ content = _json.loads(response.content.decode('unicode_escape'))
+ else:
+ content = _json.loads(response.content)
+ self.assertEqual(response.status_code, 422)
+
+ @attr('slow')
+ def test_valid_request(self):
+ username = 'PlotlyImageTest'
+ api_key = '786r5mecv0'
+ file_owner = 'get_test_user'
+ file_id = 0
+ hd = copy.copy(default_headers)
+ hd['plotly-username'] = username
+ hd['plotly-apikey'] = api_key
+ resource = "/apigetfile/{0}/{1}/".format(file_owner, file_id)
+ response = requests.get(server + resource, headers=hd)
+ if six.PY3:
+ content = _json.loads(response.content.decode('unicode_escape'))
+ else:
+ content = _json.loads(response.content)
+ self.assertEqual(response.status_code, 200)
+ # content = _json.loads(res.content)
+ # response_payload = content['payload']
+ # figure = response_payload['figure']
+ # if figure['data'][0]['x'] != [u'1', u'2', u'3']:
+ # print('ERROR')
+ # return res
diff --git a/chart_studio/tests/test_plot_ly/test_grid/__init__.py b/chart_studio/tests/test_plot_ly/test_grid/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_grid/test_grid.py b/chart_studio/tests/test_plot_ly/test_grid/test_grid.py
new file mode 100644
index 00000000000..601c02ed2e2
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_grid/test_grid.py
@@ -0,0 +1,184 @@
+"""
+test_grid:
+==========
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+import random
+import string
+from unittest import skip
+
+from nose.plugins.attrib import attr
+
+from chart_studio import plotly as py
+from chart_studio.exceptions import InputError, PlotlyRequestError
+from _plotly_utils.exceptions import PlotlyError
+from plotly.graph_objs import Scatter
+from chart_studio.grid_objs import Column, Grid
+from chart_studio.plotly import parse_grid_id_args
+from chart_studio.tests.utils import PlotlyTestCase
+
+
+def random_filename():
+ choice_chars = string.ascii_letters + string.digits
+ random_chars = [random.choice(choice_chars) for _ in range(10)]
+ unique_filename = 'Valid Grid ' + ''.join(random_chars)
+ return unique_filename
+
+
+class GridTest(PlotlyTestCase):
+
+ # Test grid args
+ _grid_id = 'chris:3043'
+ _grid = Grid([])
+ _grid.id = _grid_id
+ _grid_url = 'https://plot.ly/~chris/3043/my-grid'
+
+ def setUp(self):
+ super(GridTest, self).setUp()
+ py.sign_in('PythonTest', 'xnyU0DEwvAQQCwHVseIL')
+
+ def get_grid(self):
+ c1 = Column([1, 2, 3, 4], 'first column')
+ c2 = Column(['a', 'b', 'c', 'd'], 'second column')
+ g = Grid([c1, c2])
+ return g
+
+ def upload_and_return_grid(self):
+ g = self.get_grid()
+ unique_filename = random_filename()
+ py.grid_ops.upload(g, unique_filename, auto_open=False)
+ return g
+
+ # Nominal usage
+ @attr('slow')
+ def test_grid_upload(self):
+ self.upload_and_return_grid()
+
+ @attr('slow')
+ def test_grid_upload_in_new_folder(self):
+ g = self.get_grid()
+ path = (
+ 'new folder: {0}/grid in folder {1}'
+ .format(random_filename(), random_filename())
+ )
+ py.grid_ops.upload(g, path, auto_open=False)
+
+ @attr('slow')
+ def test_grid_upload_in_existing_folder(self):
+ g = self.get_grid()
+ folder = random_filename()
+ filename = random_filename()
+ py.file_ops.mkdirs(folder)
+ path = (
+ 'existing folder: {0}/grid in folder {1}'
+ .format(folder, filename)
+ )
+ py.grid_ops.upload(g, path, auto_open=False)
+
+ @attr('slow')
+ def test_column_append(self):
+ g = self.upload_and_return_grid()
+ new_col = Column([1, 5, 3], 'new col')
+ py.grid_ops.append_columns([new_col], grid=g)
+
+ @attr('slow')
+ def test_row_append(self):
+ g = self.upload_and_return_grid()
+ new_rows = [[1, 2], [10, 20]]
+ py.grid_ops.append_rows(new_rows, grid=g)
+
+ @attr('slow')
+ def test_plot_from_grid(self):
+ g = self.upload_and_return_grid()
+ url = py.plot([Scatter(xsrc=g[0].id, ysrc=g[1].id)],
+ auto_open=False, filename='plot from grid')
+ return url, g
+
+ @attr('slow')
+ def test_get_figure_from_references(self):
+ url, g = self.test_plot_from_grid()
+ fig = py.get_figure(url)
+ data = fig['data']
+ trace = data[0]
+ assert(tuple(g[0].data) == tuple(trace['x']))
+ assert(tuple(g[1].data) == tuple(trace['y']))
+
+ def test_grid_id_args(self):
+ self.assertEqual(parse_grid_id_args(self._grid, None),
+ parse_grid_id_args(None, self._grid_url))
+
+ def test_no_grid_id_args(self):
+ with self.assertRaises(InputError):
+ parse_grid_id_args(None, None)
+
+ def test_overspecified_grid_args(self):
+ with self.assertRaises(InputError):
+ parse_grid_id_args(self._grid, self._grid_url)
+
+ # not broken anymore since plotly 3.0.0
+ # def test_scatter_from_non_uploaded_grid(self):
+ # c1 = Column([1, 2, 3, 4], 'first column')
+ # c2 = Column(['a', 'b', 'c', 'd'], 'second column')
+ # g = Grid([c1, c2])
+ # with self.assertRaises(ValueError):
+ # Scatter(xsrc=g[0], ysrc=g[1])
+
+ def test_column_append_of_non_uploaded_grid(self):
+ c1 = Column([1, 2, 3, 4], 'first column')
+ c2 = Column(['a', 'b', 'c', 'd'], 'second column')
+ g = Grid([c1])
+ with self.assertRaises(PlotlyError):
+ py.grid_ops.append_columns([c2], grid=g)
+
+ def test_row_append_of_non_uploaded_grid(self):
+ c1 = Column([1, 2, 3, 4], 'first column')
+ rows = [[1], [2]]
+ g = Grid([c1])
+ with self.assertRaises(PlotlyError):
+ py.grid_ops.append_rows(rows, grid=g)
+
+ # Input Errors
+ @attr('slow')
+ def test_unequal_length_rows(self):
+ g = self.upload_and_return_grid()
+ rows = [[1, 2], ['to', 'many', 'cells']]
+ with self.assertRaises(InputError):
+ py.grid_ops.append_rows(rows, grid=g)
+
+ # Test duplicate columns
+ def test_duplicate_columns(self):
+ c1 = Column([1, 2, 3, 4], 'first column')
+ c2 = Column(['a', 'b', 'c', 'd'], 'first column')
+ with self.assertRaises(InputError):
+ Grid([c1, c2])
+
+ # Test delete
+ @attr('slow')
+ def test_delete_grid(self):
+ g = self.get_grid()
+ fn = random_filename()
+ py.grid_ops.upload(g, fn, auto_open=False)
+ py.grid_ops.delete(g)
+ py.grid_ops.upload(g, fn, auto_open=False)
+
+ # Plotly failures
+ @skip('adding this for now so test_file_tools pass, more info' +
+ 'https://github.com/plotly/python-api/issues/262')
+ def test_duplicate_filenames(self):
+ c1 = Column([1, 2, 3, 4], 'first column')
+ g = Grid([c1])
+
+ random_chars = [random.choice(string.ascii_uppercase)
+ for _ in range(5)]
+ unique_filename = 'Valid Grid ' + ''.join(random_chars)
+ py.grid_ops.upload(g, unique_filename, auto_open=False)
+ try:
+ py.grid_ops.upload(g, unique_filename, auto_open=False)
+ except PlotlyRequestError as e:
+ pass
+ else:
+ self.fail('Expected this to fail!')
diff --git a/chart_studio/tests/test_plot_ly/test_image/__init__.py b/chart_studio/tests/test_plot_ly/test_image/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_image/test_image.py b/chart_studio/tests/test_plot_ly/test_image/test_image.py
new file mode 100644
index 00000000000..4c74bc753a3
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_image/test_image.py
@@ -0,0 +1,77 @@
+from __future__ import absolute_import
+
+import imghdr
+import tempfile
+import os
+import itertools
+import warnings
+
+from nose.plugins.attrib import attr
+
+import _plotly_utils.exceptions
+from chart_studio.plotly import plotly as py
+from chart_studio.tests.utils import PlotlyTestCase
+
+
+@attr('slow')
+class TestImage(PlotlyTestCase):
+
+ def setUp(self):
+ super(TestImage, self).setUp()
+ py.sign_in('PlotlyImageTest', '786r5mecv0')
+ self.data = [{'x': [1, 2, 3], 'y': [3, 1, 6]}]
+
+
+def _generate_image_get_returns_valid_image_test(image_format,
+ width, height, scale):
+ def test(self):
+ # TODO: better understand why this intermittently fails. See #649
+ num_attempts = 5
+ for i in range(num_attempts):
+ if i > 0:
+ warnings.warn('image test intermittently failed, retrying...')
+ try:
+ image = py.image.get(self.data, image_format, width, height,
+ scale)
+ if image_format in ['png', 'jpeg']:
+ assert imghdr.what('', image) == image_format
+ return
+ except (KeyError, _plotly_utils.exceptions.PlotlyError):
+ if i == num_attempts - 1:
+ raise
+
+ return test
+
+
+def _generate_image_save_as_saves_valid_image(image_format,
+ width, height, scale):
+ def _test(self):
+ f, filename = tempfile.mkstemp('.{}'.format(image_format))
+ py.image.save_as(self.data, filename, format=image_format,
+ width=width, height=height, scale=scale)
+ if image_format in ['png', 'jpeg']:
+ assert imghdr.what(filename) == image_format
+ else:
+ assert os.path.getsize(filename) > 0
+
+ os.remove(filename)
+
+ return _test
+
+kwargs = {
+ 'format': ['png', 'jpeg', 'pdf', 'svg', 'emf'],
+ 'width': [None, 300],
+ 'height': [None, 300],
+ 'scale': [None, 5]
+}
+
+for args in itertools.product(kwargs['format'], kwargs['width'],
+ kwargs['height'], kwargs['scale']):
+ for test_generator in [_generate_image_get_returns_valid_image_test,
+ _generate_image_save_as_saves_valid_image]:
+
+ _test = test_generator(*args)
+ arg_string = ', '.join([str(a) for a in args])
+ test_name = test_generator.__name__.replace('_generate', 'test')
+ test_name += '({})'.format(arg_string)
+ setattr(TestImage, test_name, _test)
diff --git a/chart_studio/tests/test_plot_ly/test_meta/__init__.py b/chart_studio/tests/test_plot_ly/test_meta/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_meta/test_meta.py b/chart_studio/tests/test_plot_ly/test_meta/test_meta.py
new file mode 100644
index 00000000000..3c8010dded2
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_meta/test_meta.py
@@ -0,0 +1,63 @@
+"""
+test_meta:
+==========
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+import random
+import string
+
+from nose.plugins.attrib import attr
+from unittest import skip
+
+from chart_studio import plotly as py
+from chart_studio.exceptions import PlotlyRequestError
+from chart_studio.grid_objs import Column, Grid
+from chart_studio.tests.utils import PlotlyTestCase
+
+
+class MetaTest(PlotlyTestCase):
+
+ _grid = grid = Grid([Column([1, 2, 3, 4], 'first column')])
+ _meta = {"settings": {"scope1": {"model": "Unicorn Finder", "voltage": 4}}}
+
+ def setUp(self):
+ super(MetaTest, self).setUp()
+ py.sign_in('PythonTest', 'xnyU0DEwvAQQCwHVseIL')
+
+ def random_filename(self):
+ random_chars = [random.choice(string.ascii_uppercase) for _ in range(5)]
+ unique_filename = 'Valid Grid with Meta '+''.join(random_chars)
+ return unique_filename
+
+ @attr('slow')
+ def test_upload_meta(self):
+ unique_filename = self.random_filename()
+ grid_url = py.grid_ops.upload(self._grid, unique_filename,
+ auto_open=False)
+
+ # Add some Metadata to that grid
+ py.meta_ops.upload(self._meta, grid_url=grid_url)
+
+ @attr('slow')
+ def test_upload_meta_with_grid(self):
+ c1 = Column([1, 2, 3, 4], 'first column')
+ Grid([c1])
+
+ unique_filename = self.random_filename()
+
+ py.grid_ops.upload(
+ self._grid,
+ unique_filename,
+ meta=self._meta,
+ auto_open=False)
+
+ @skip('adding this for now so test_file_tools pass, more info' +
+ 'https://github.com/plotly/python-api/issues/263')
+ def test_metadata_to_nonexistent_grid(self):
+ non_exist_meta_url = 'https://local.plot.ly/~GridTest/999999999'
+ with self.assertRaises(PlotlyRequestError):
+ py.meta_ops.upload(self._meta, grid_url=non_exist_meta_url)
diff --git a/chart_studio/tests/test_plot_ly/test_plotly/__init__.py b/chart_studio/tests/test_plot_ly/test_plotly/__init__.py
new file mode 100644
index 00000000000..1118eb01e82
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_plotly/__init__.py
@@ -0,0 +1,5 @@
+import warnings
+
+
+def setup_package():
+ warnings.filterwarnings('ignore')
diff --git a/chart_studio/tests/test_plot_ly/test_plotly/test_credentials.py b/chart_studio/tests/test_plot_ly/test_plotly/test_credentials.py
new file mode 100644
index 00000000000..628b047d373
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_plotly/test_credentials.py
@@ -0,0 +1,93 @@
+from __future__ import absolute_import
+
+import _plotly_utils.exceptions
+from chart_studio import plotly as py, exceptions
+import chart_studio.session as session
+import chart_studio.tools as tls
+from chart_studio.tests.utils import PlotlyTestCase
+
+import sys
+
+# import from mock
+if sys.version_info.major == 3 and sys.version_info.minor >= 3:
+ from unittest.mock import patch
+else:
+ from mock import patch
+
+
+class TestSignIn(PlotlyTestCase):
+
+ def setUp(self):
+ super(TestSignIn, self).setUp()
+ patcher = patch('chart_studio.api.v2.users.current')
+ self.users_current_mock = patcher.start()
+ self.addCleanup(patcher.stop)
+
+ def test_get_credentials(self):
+ session_credentials = session.get_session_credentials()
+ if 'username' in session_credentials:
+ del session._session['credentials']['username']
+ if 'api_key' in session_credentials:
+ del session._session['credentials']['api_key']
+ creds = py.get_credentials()
+ file_creds = tls.get_credentials_file()
+ self.assertEqual(creds, file_creds)
+
+ def test_sign_in(self):
+ un = 'anyone'
+ ak = 'something'
+ # TODO, add this!
+ # si = ['this', 'and-this']
+ py.sign_in(un, ak)
+ creds = py.get_credentials()
+ self.assertEqual(creds['username'], un)
+ self.assertEqual(creds['api_key'], ak)
+ # TODO, and check it!
+ # assert creds['stream_ids'] == si
+
+ def test_get_config(self):
+ plotly_domain = 'test domain'
+ plotly_streaming_domain = 'test streaming domain'
+ config1 = py.get_config()
+ session._session['config']['plotly_domain'] = plotly_domain
+ config2 = py.get_config()
+ session._session['config']['plotly_streaming_domain'] = (
+ plotly_streaming_domain
+ )
+ config3 = py.get_config()
+ self.assertEqual(config2['plotly_domain'], plotly_domain)
+ self.assertNotEqual(
+ config2['plotly_streaming_domain'], plotly_streaming_domain
+ )
+ self.assertEqual(
+ config3['plotly_streaming_domain'], plotly_streaming_domain
+ )
+
+ def test_sign_in_with_config(self):
+ username = 'place holder'
+ api_key = 'place holder'
+ plotly_domain = 'test domain'
+ plotly_streaming_domain = 'test streaming domain'
+ plotly_ssl_verification = False
+ py.sign_in(
+ username,
+ api_key,
+ plotly_domain=plotly_domain,
+ plotly_streaming_domain=plotly_streaming_domain,
+ plotly_ssl_verification=plotly_ssl_verification
+ )
+ config = py.get_config()
+ self.assertEqual(config['plotly_domain'], plotly_domain)
+ self.assertEqual(
+ config['plotly_streaming_domain'], plotly_streaming_domain
+ )
+ self.assertEqual(
+ config['plotly_ssl_verification'], plotly_ssl_verification
+ )
+
+ def test_sign_in_cannot_validate(self):
+ self.users_current_mock.side_effect = exceptions.PlotlyRequestError(
+ 'msg', 400, 'foobar'
+ )
+ with self.assertRaisesRegexp(_plotly_utils.exceptions.PlotlyError, 'Sign in failed'):
+ py.sign_in('foo', 'bar')
diff --git a/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py b/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py
new file mode 100644
index 00000000000..fe6915cbc5d
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py
@@ -0,0 +1,441 @@
+"""
+test_plot:
+==========
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+import requests
+import six
+import sys
+from requests.compat import json as _json
+import warnings
+
+from nose.plugins.attrib import attr
+
+import chart_studio.tools as tls
+import plotly.tools
+from chart_studio import session
+from chart_studio.tests.utils import PlotlyTestCase
+from chart_studio.plotly import plotly as py
+from _plotly_utils.exceptions import PlotlyError, PlotlyEmptyDataError
+from chart_studio.files import CONFIG_FILE
+
+
+# import from mock
+if sys.version_info.major == 3 and sys.version_info.minor >= 3:
+ from unittest.mock import patch
+else:
+ from mock import patch
+
+
+class TestPlot(PlotlyTestCase):
+
+ def setUp(self):
+ super(TestPlot, self).setUp()
+ py.sign_in('PlotlyImageTest', '786r5mecv0')
+ self.simple_figure = {'data': [{'x': [1, 2, 3], 'y': [2, 1, 2]}]}
+
+ @attr('slow')
+ def test_plot_valid(self):
+ fig = {
+ 'data': [
+ {
+ 'x': (1, 2, 3),
+ 'y': (2, 1, 2)
+ }
+ ],
+ 'layout': {'title': {'text': 'simple'}}
+ }
+ url = py.plot(fig, auto_open=False, filename='plot_valid')
+ saved_fig = py.get_figure(url)
+ self.assertEqual(saved_fig['data'][0]['x'], fig['data'][0]['x'])
+ self.assertEqual(saved_fig['data'][0]['y'], fig['data'][0]['y'])
+ self.assertEqual(saved_fig['layout']['title']['text'],
+ fig['layout']['title']['text'])
+
+ def test_plot_invalid(self):
+ fig = {
+ 'data': [
+ {
+ 'x': [1, 2, 3],
+ 'y': [2, 1, 2],
+ 'z': [3, 4, 1]
+ }
+ ]
+ }
+ with self.assertRaises(ValueError):
+ py.plot(fig, auto_open=False, filename='plot_invalid')
+
+ def test_plot_invalid_args_1(self):
+ with self.assertRaises(TypeError):
+ py.plot(x=[1, 2, 3], y=[2, 1, 2], auto_open=False,
+ filename='plot_invalid')
+
+ def test_plot_invalid_args_2(self):
+ with self.assertRaises(ValueError):
+ py.plot([1, 2, 3], [2, 1, 2], auto_open=False,
+ filename='plot_invalid')
+
+ def test_plot_empty_data(self):
+ self.assertRaises(PlotlyEmptyDataError, py.plot, [],
+ filename='plot_invalid')
+
+ def test_plot_sharing_invalid_argument(self):
+
+ # Raise an error if sharing argument is incorrect
+ # correct arguments {'public, 'private', 'secret'}
+
+ kwargs = {'filename': 'invalid-sharing-argument',
+ 'sharing': 'privste'}
+
+ with self.assertRaisesRegexp(
+ PlotlyError,
+ "The 'sharing' argument only accepts"):
+ py.plot(self.simple_figure, **kwargs)
+
+ def test_plot_world_readable_sharing_conflict_1(self):
+
+ # Raise an error if world_readable=False but sharing='public'
+
+ kwargs = {'filename': 'invalid-privacy-setting',
+ 'world_readable': False,
+ 'sharing': 'public'}
+
+ with self.assertRaisesRegexp(
+ PlotlyError,
+ 'setting your plot privacy to both public and private.'):
+ py.plot(self.simple_figure, **kwargs)
+
+ def test_plot_world_readable_sharing_conflict_2(self):
+
+ # Raise an error if world_readable=True but sharing='secret'
+
+ kwargs = {'filename': 'invalid-privacy-setting',
+ 'world_readable': True,
+ 'sharing': 'secret'}
+
+ with self.assertRaisesRegexp(
+ PlotlyError,
+ 'setting your plot privacy to both public and private.'):
+ py.plot(self.simple_figure, **kwargs)
+
+ def test_plot_option_logic_only_world_readable_given(self):
+
+ # If sharing is not given and world_readable=False,
+ # sharing should be set to private
+
+ kwargs = {'filename': 'test',
+ 'auto_open': True,
+ 'fileopt': 'overwrite',
+ 'validate': True,
+ 'world_readable': False}
+
+ plot_option_logic = py._plot_option_logic(kwargs)
+
+ expected_plot_option_logic = {'filename': 'test',
+ 'auto_open': True,
+ 'fileopt': 'overwrite',
+ 'validate': True,
+ 'world_readable': False,
+ 'sharing': 'private'}
+ self.assertEqual(plot_option_logic, expected_plot_option_logic)
+
+ def test_plot_option_logic_only_sharing_given(self):
+
+ # If world_readable is not given and sharing ='private',
+ # world_readable should be set to False
+
+ kwargs = {'filename': 'test',
+ 'auto_open': True,
+ 'fileopt': 'overwrite',
+ 'validate': True,
+ 'sharing': 'private'}
+
+ plot_option_logic = py._plot_option_logic(kwargs)
+
+ expected_plot_option_logic = {'filename': 'test',
+ 'auto_open': True,
+ 'fileopt': 'overwrite',
+ 'validate': True,
+ 'world_readable': False,
+ 'sharing': 'private'}
+ self.assertEqual(plot_option_logic, expected_plot_option_logic)
+
+ def test_plot_option_fileopt_deprecations(self):
+
+ # Make sure DeprecationWarnings aren't filtered out by nose
+ warnings.filterwarnings('default', category=DeprecationWarning)
+
+ # If filename is not given and fileopt is not 'new',
+ # raise a deprecation warning
+ kwargs = {'auto_open': True,
+ 'fileopt': 'overwrite',
+ 'validate': True,
+ 'sharing': 'private'}
+
+ with warnings.catch_warnings(record=True) as w:
+ plot_option_logic = py._plot_option_logic(kwargs)
+ assert w[0].category == DeprecationWarning
+
+ expected_plot_option_logic = {'filename': 'plot from API',
+ 'auto_open': True,
+ 'fileopt': 'overwrite',
+ 'validate': True,
+ 'world_readable': False,
+ 'sharing': 'private'}
+ self.assertEqual(plot_option_logic, expected_plot_option_logic)
+
+ # If filename is given and fileopt is not 'overwrite',
+ # raise a depreacation warning
+ kwargs = {'filename': 'test',
+ 'auto_open': True,
+ 'fileopt': 'append',
+ 'validate': True,
+ 'sharing': 'private'}
+
+ with warnings.catch_warnings(record=True) as w:
+ plot_option_logic = py._plot_option_logic(kwargs)
+ assert w[0].category == DeprecationWarning
+
+ expected_plot_option_logic = {'filename': 'test',
+ 'auto_open': True,
+ 'fileopt': 'append',
+ 'validate': True,
+ 'world_readable': False,
+ 'sharing': 'private'}
+ self.assertEqual(plot_option_logic, expected_plot_option_logic)
+
+ @attr('slow')
+ def test_plot_url_given_sharing_key(self):
+
+ # Give share_key is requested, the retun url should contain
+ # the share_key
+
+ validate = True
+ fig = plotly.tools.return_figure_from_figure_or_data(self.simple_figure,
+ validate)
+ kwargs = {'filename': 'is_share_key_included',
+ 'fileopt': 'overwrite',
+ 'world_readable': False,
+ 'sharing': 'secret'}
+ response = py._send_to_plotly(fig, **kwargs)
+ plot_url = response['url']
+
+ self.assertTrue('share_key=' in plot_url)
+
+ @attr('slow')
+ def test_plot_url_response_given_sharing_key(self):
+
+ # Given share_key is requested, get request of the url should
+ # be 200
+
+ kwargs = {'filename': 'is_share_key_included',
+ 'fileopt': 'overwrite',
+ 'auto_open': False,
+ 'world_readable': False,
+ 'sharing': 'secret'}
+
+ plot_url = py.plot(self.simple_figure, **kwargs)
+ # shareplot basically always gives a 200 if even if permission denied
+ # embedplot returns an actual 404
+ embed_url = plot_url.split('?')[0] + '.embed?' + plot_url.split('?')[1]
+ response = requests.get(embed_url)
+
+ self.assertEqual(response.status_code, 200)
+
+ @attr('slow')
+ def test_private_plot_response_with_and_without_share_key(self):
+
+ # The json file of the private plot should be 404 and once
+ # share_key is added it should be 200
+
+ kwargs = {'filename': 'is_share_key_included',
+ 'fileopt': 'overwrite',
+ 'world_readable': False,
+ 'sharing': 'private'}
+
+ private_plot_url = py._send_to_plotly(self.simple_figure,
+ **kwargs)['url']
+ private_plot_response = requests.get(private_plot_url + ".json")
+
+ # The json file of the private plot should be 404
+ self.assertEqual(private_plot_response.status_code, 404)
+
+ secret_plot_url = py.add_share_key_to_url(private_plot_url)
+ urlsplit = six.moves.urllib.parse.urlparse(secret_plot_url)
+ secret_plot_json_file = six.moves.urllib.parse.urljoin(
+ urlsplit.geturl(), "?.json" + urlsplit.query)
+ secret_plot_response = requests.get(secret_plot_json_file)
+
+ # The json file of the secret plot should be 200
+ self.assertTrue(secret_plot_response.status_code, 200)
+
+
+class TestPlotOptionLogic(PlotlyTestCase):
+ conflicting_option_set = (
+ {'world_readable': True, 'sharing': 'secret'},
+ {'world_readable': True, 'sharing': 'private'},
+ {'world_readable': False, 'sharing': 'public'}
+ )
+
+ def setUp(self):
+ super(TestPlotOptionLogic, self).setUp()
+
+ # Make sure we don't hit sign-in validation failures.
+ patcher = patch('chart_studio.api.v2.users.current')
+ self.users_current_mock = patcher.start()
+ self.addCleanup(patcher.stop)
+
+ # Some tests specifically check how *file-level* plot options alter
+ # plot option logic. In order not to re-write that, we simply clear the
+ # *session* information since it would take precedent. The _session is
+ # set when you `sign_in`.
+ session._session['plot_options'].clear()
+
+ def test_default_options(self):
+ options = py._plot_option_logic({})
+ config_options = tls.get_config_file()
+ for key in options:
+ if key != 'fileopt' and key in config_options:
+ self.assertEqual(options[key], config_options[key])
+
+ def test_conflicting_plot_options_in_plot_option_logic(self):
+ for plot_options in self.conflicting_option_set:
+ self.assertRaises(PlotlyError, py._plot_option_logic,
+ plot_options)
+
+ def test_set_config_updates_plot_options(self):
+ original_config = tls.get_config_file()
+ new_options = {
+ 'world_readable': not original_config['world_readable'],
+ 'auto_open': not original_config['auto_open'],
+ 'sharing': ('public' if original_config['world_readable'] is False
+ else 'secret')
+ }
+ tls.set_config_file(**new_options)
+ options = py._plot_option_logic({})
+ for key in new_options:
+ self.assertEqual(new_options[key], options[key])
+
+
+def generate_conflicting_plot_options_in_signin():
+ """sign_in overrides the default plot options.
+ conflicting options aren't raised until plot or iplot is called,
+ through _plot_option_logic
+ """
+ def gen_test(plot_options):
+ def test(self):
+ py.sign_in('username', 'key', **plot_options)
+ self.assertRaises(PlotlyError, py._plot_option_logic, {})
+ return test
+
+ for i, plot_options in enumerate(TestPlotOptionLogic.conflicting_option_set):
+ setattr(TestPlotOptionLogic,
+ 'test_conflicting_plot_options_in_signin_{}'.format(i),
+ gen_test(plot_options))
+generate_conflicting_plot_options_in_signin()
+
+
+def generate_conflicting_plot_options_in_tools_dot_set_config():
+ """tls.set_config overrides the default plot options.
+ conflicting options are actually raised when the options are saved,
+ because we push out default arguments for folks, and we don't want to
+ require users to specify both world_readable and secret *and* we don't
+ want to raise an error if they specified only one of these options
+ and didn't know that a default option was being saved for them.
+ """
+ def gen_test(plot_options):
+ def test(self):
+ self.assertRaises(PlotlyError, tls.set_config_file,
+ **plot_options)
+ return test
+
+ for i, plot_options in enumerate(TestPlotOptionLogic.conflicting_option_set):
+ setattr(TestPlotOptionLogic,
+ 'test_conflicting_plot_options_in_'
+ 'tools_dot_set_config{}'.format(i),
+ gen_test(plot_options))
+generate_conflicting_plot_options_in_tools_dot_set_config()
+
+
+def generate_conflicting_plot_options_with_json_writes_of_config():
+ """ if the user wrote their own options in the config file,
+ then we'll raise the error when the call plot or iplot through
+ _plot_option_logic
+ """
+ def gen_test(plot_options):
+ def test(self):
+ config = _json.load(open(CONFIG_FILE))
+ with open(CONFIG_FILE, 'w') as f:
+ config.update(plot_options)
+ f.write(_json.dumps(config))
+ self.assertRaises(PlotlyError, py._plot_option_logic, {})
+ return test
+
+ for i, plot_options in enumerate(TestPlotOptionLogic.conflicting_option_set):
+ setattr(TestPlotOptionLogic,
+ 'test_conflicting_plot_options_with_'
+ 'json_writes_of_config{}'.format(i),
+ gen_test(plot_options))
+generate_conflicting_plot_options_with_json_writes_of_config()
+
+
+def generate_private_sharing_and_public_world_readable_precedence():
+ """ Test that call signature arguments applied through _plot_option_logic
+ overwrite options supplied through py.sign_in which overwrite options
+ set through tls.set_config
+ """
+ plot_option_sets = (
+ {
+ 'parent': {'world_readable': True, 'auto_open': False},
+ 'child': {'sharing': 'secret', 'auto_open': True},
+ 'expected_output': {'world_readable': False,
+ 'sharing': 'secret',
+ 'auto_open': True}
+ },
+ {
+ 'parent': {'world_readable': True, 'auto_open': True},
+ 'child': {'sharing': 'private', 'auto_open': False},
+ 'expected_output': {'world_readable': False,
+ 'sharing': 'private',
+ 'auto_open': False}
+ },
+ {
+ 'parent': {'world_readable': False, 'auto_open': False},
+ 'child': {'sharing': 'public', 'auto_open': True},
+ 'expected_output': {'world_readable': True,
+ 'sharing': 'public',
+ 'auto_open': True}
+ }
+ )
+
+ def gen_test_signin(plot_options):
+ def test(self):
+ py.sign_in('username', 'key', **plot_options['parent'])
+ options = py._plot_option_logic(plot_options['child'])
+ for option, value in plot_options['expected_output'].items():
+ self.assertEqual(options[option], value)
+ return test
+
+ def gen_test_config(plot_options):
+ def test(self):
+ tls.set_config(**plot_options['parent'])
+ options = py._plot_option_logic(plot_options['child'])
+ for option, value in plot_options['expected_output'].items():
+ self.assertEqual(options[option], value)
+
+ for i, plot_options in enumerate(plot_option_sets):
+ setattr(TestPlotOptionLogic,
+ 'test_private_sharing_and_public_'
+ 'world_readable_precedence_signin{}'.format(i),
+ gen_test_signin(plot_options))
+
+ setattr(TestPlotOptionLogic,
+ 'test_private_sharing_and_public_'
+ 'world_readable_precedence_config{}'.format(i),
+ gen_test_config(plot_options))
+
+generate_private_sharing_and_public_world_readable_precedence()
diff --git a/chart_studio/tests/test_plot_ly/test_session/__init__.py b/chart_studio/tests/test_plot_ly/test_session/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_session/test_session.py b/chart_studio/tests/test_plot_ly/test_session/test_session.py
new file mode 100644
index 00000000000..ae1c6a67c57
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_session/test_session.py
@@ -0,0 +1,34 @@
+from __future__ import absolute_import
+
+from chart_studio.tests.utils import PlotlyTestCase
+
+from chart_studio import session
+from chart_studio.session import update_session_plot_options, SHARING_OPTIONS
+from _plotly_utils.exceptions import PlotlyError
+
+
+class TestSession(PlotlyTestCase):
+
+ def setUp(self):
+ super(TestSession, self).setUp()
+ session._session['plot_options'].clear()
+
+ def test_update_session_plot_options_invalid_sharing_argument(self):
+
+ # Return PlotlyError when sharing arguement is not
+ # 'public', 'private' or 'secret'
+
+ kwargs = {'sharing': 'priva'}
+ self.assertRaises(PlotlyError, update_session_plot_options, **kwargs)
+
+ def test_update_session_plot_options_valid_sharing_argument(self):
+
+ # _session['plot_options'] should contain sharing key after
+ # update_session_plot_options is called by correct arguments
+ # 'public, 'private' or 'secret'
+ from chart_studio.session import _session
+ for key in SHARING_OPTIONS:
+ kwargs = {'sharing': key}
+ update_session_plot_options(**kwargs)
+
+ self.assertEqual(_session['plot_options'], kwargs)
diff --git a/chart_studio/tests/test_plot_ly/test_spectacle_presentation/__init__.py b/chart_studio/tests/test_plot_ly/test_spectacle_presentation/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_spectacle_presentation/test_spectacle_presentation.py b/chart_studio/tests/test_plot_ly/test_spectacle_presentation/test_spectacle_presentation.py
new file mode 100644
index 00000000000..b1f9431783c
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_spectacle_presentation/test_spectacle_presentation.py
@@ -0,0 +1,363 @@
+"""
+test_spectacle_presentation:
+==========
+
+A module intended for use with Nose.
+
+"""
+from __future__ import absolute_import
+
+from unittest import TestCase
+from _plotly_utils.exceptions import PlotlyError
+import chart_studio
+import chart_studio.presentation_objs as pres
+
+
+class TestPresentation(TestCase):
+
+ def test_invalid_style(self):
+ markdown_string = """
+ # one slide
+ """
+
+ self.assertRaisesRegexp(
+ PlotlyError, chart_studio.presentation_objs.presentation_objs.STYLE_ERROR,
+ pres.Presentation, markdown_string, style='foo'
+ )
+
+ def test_open_code_block(self):
+ markdown_string = """
+ # one slide
+
+ ```python
+ x = 2 + 2
+ print x
+ """
+
+ self.assertRaisesRegexp(
+ PlotlyError, chart_studio.presentation_objs.presentation_objs.CODE_ENV_ERROR,
+ pres.Presentation, markdown_string, style='moods'
+ )
+
+ def test_invalid_code_language(self):
+ markdown_string = """
+ ```foo
+ x = 2 + 2
+ print x
+ ```
+ """
+
+ self.assertRaisesRegexp(
+ PlotlyError, chart_studio.presentation_objs.presentation_objs.LANG_ERROR, pres.Presentation,
+ markdown_string, style='moods'
+ )
+
+ def test_expected_pres(self):
+ markdown_string = "# title\n---\ntransition: zoom, fade, fade\n# Colors\nColors are everywhere around us.\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\nImage(https://raw.githubusercontent.com/jackparmer/gradient-backgrounds/master/moods1.png)\n```python\nx=1\n```\n---\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\nPlotly(https://plot.ly/~AdamKulidjian/3564/)\n---\n"
+
+ my_pres = pres.Presentation(
+ markdown_string, style='moods', imgStretch=True
+ )
+
+ exp_pres = {'presentation': {'paragraphStyles': {'Body': {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 16,
+ 'fontStyle': 'normal',
+ 'fontWeight': 100,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none',
+ 'wordBreak': 'break-word'},
+ 'Body Small': {'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 10,
+ 'fontStyle': 'normal',
+ 'fontWeight': 400,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'},
+ 'Caption': {'color': '#3d3d3d',
+ 'fontFamily': 'Open Sans',
+ 'fontSize': 11,
+ 'fontStyle': 'italic',
+ 'fontWeight': 400,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'},
+ 'Heading 1': {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 55,
+ 'fontStyle': 'normal',
+ 'fontWeight': 900,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'},
+ 'Heading 2': {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 36,
+ 'fontStyle': 'normal',
+ 'fontWeight': 900,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'},
+ 'Heading 3': {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 30,
+ 'fontStyle': 'normal',
+ 'fontWeight': 900,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'textAlign': 'center',
+ 'textDecoration': 'none'}},
+ 'slidePreviews': [None for _ in range(496)],
+ 'slides': [{'children': [{'children': ['title'],
+ 'defaultHeight': 36,
+ 'defaultWidth': 52,
+ 'id': 'CfaAzcSZE',
+ 'props': {'isQuote': False,
+ 'listType': None,
+ 'paragraphStyle': 'Heading 1',
+ 'size': 4,
+ 'style': {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 55,
+ 'fontStyle': 'normal',
+ 'fontWeight': 900,
+ 'height': 140.0,
+ 'left': 0.0,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'position': 'absolute',
+ 'textAlign': 'center',
+ 'textDecoration': 'none',
+ 'top': 350.0,
+ 'width': 1000.0}},
+ 'resizeVertical': False,
+ 'type': 'Text'}],
+ 'id': 'ibvfOQeNy',
+ 'props': {'style': {'backgroundColor': '#F7F7F7'},
+ 'transition': ['slide']}},
+ {'children': [{'children': ['Colors'],
+ 'defaultHeight': 36,
+ 'defaultWidth': 52,
+ 'id': 'YcGQJ21AY',
+ 'props': {'isQuote': False,
+ 'listType': None,
+ 'paragraphStyle': 'Heading 1',
+ 'size': 4,
+ 'style': {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 55,
+ 'fontStyle': 'normal',
+ 'fontWeight': 900,
+ 'height': 140.0,
+ 'left': 0.0,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'position': 'absolute',
+ 'textAlign': 'center',
+ 'textDecoration': 'none',
+ 'top': 0.0,
+ 'width': 1000.0}},
+ 'resizeVertical': False,
+ 'type': 'Text'},
+ {'children': ['Colors are everywhere around us.'],
+ 'defaultHeight': 36,
+ 'defaultWidth': 52,
+ 'id': 'G0tcGP89U',
+ 'props': {'isQuote': False,
+ 'listType': None,
+ 'paragraphStyle': 'Body',
+ 'size': 4,
+ 'style': {'color': '#000016',
+ 'fontFamily': 'Roboto',
+ 'fontSize': 16,
+ 'fontStyle': 'normal',
+ 'fontWeight': 100,
+ 'height': 14.0,
+ 'left': 25.0,
+ 'lineHeight': 'normal',
+ 'minWidth': 20,
+ 'opacity': 1,
+ 'position': 'absolute',
+ 'textAlign': 'left',
+ 'textDecoration': 'none',
+ 'top': 663.0810810810812,
+ 'width': 950.0000000000001,
+ 'wordBreak': 'break-word'}},
+ 'resizeVertical': False,
+ 'type': 'Text'},
+ {'children': [],
+ 'id': 'c4scRvuIe',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 280.0,
+ 'left': 0.0,
+ 'position': 'absolute',
+ 'top': 70.0,
+ 'width': 330.66666666666663}},
+ 'type': 'Plotly'},
+ {'children': [],
+ 'id': 'yScDKejKG',
+ 'props': {'height': 512,
+ 'imageName': None,
+ 'src': 'https://raw.githubusercontent.com/jackparmer/gradient-backgrounds/master/moods1.png',
+ 'style': {'height': 280.0,
+ 'left': 334.66666666666663,
+ 'opacity': 1,
+ 'position': 'absolute',
+ 'top': 70.0,
+ 'width': 330.66666666666663},
+ 'width': 512},
+ 'type': 'Image'},
+ {'children': [],
+ 'defaultText': 'Code',
+ 'id': 'fuUrIyVrv',
+ 'props': {'language': 'python',
+ 'source': 'x=1\n',
+ 'style': {'fontFamily': "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace",
+ 'fontSize': 13,
+ 'height': 280.0,
+ 'left': 669.3333333333333,
+ 'margin': 0,
+ 'position': 'absolute',
+ 'textAlign': 'left',
+ 'top': 70.0,
+ 'width': 330.66666666666663},
+ 'theme': 'tomorrowNight'},
+ 'type': 'CodePane'}],
+ 'id': '7eG6TvKqU',
+ 'props': {'style': {'backgroundColor': '#FFFFFF'},
+ 'transition': ['zoom', 'fade']}},
+ {'children': [{'children': [],
+ 'id': '83EtFjFKM',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 96.57142857142857,
+ 'left': 400.0,
+ 'position': 'absolute',
+ 'top': 0.0,
+ 'width': 600.0}},
+ 'type': 'Plotly'},
+ {'children': [],
+ 'id': 'V9vJYk8bF',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 96.57142857142857,
+ 'left': 400.0,
+ 'position': 'absolute',
+ 'top': 100.57142857142856,
+ 'width': 600.0}},
+ 'type': 'Plotly'},
+ {'children': [],
+ 'id': 'DzCfXMyhv',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 96.57142857142857,
+ 'left': 400.0,
+ 'position': 'absolute',
+ 'top': 201.1428571428571,
+ 'width': 600.0}},
+ 'type': 'Plotly'},
+ {'children': [],
+ 'id': 'YFf7M2BON',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 96.57142857142857,
+ 'left': 400.0,
+ 'position': 'absolute',
+ 'top': 301.71428571428567,
+ 'width': 600.0}},
+ 'type': 'Plotly'},
+ {'children': [],
+ 'id': 'CARvApdzw',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 96.57142857142857,
+ 'left': 400.0,
+ 'position': 'absolute',
+ 'top': 402.2857142857142,
+ 'width': 600.0}},
+ 'type': 'Plotly'},
+ {'children': [],
+ 'id': '194ZxaSko',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 96.57142857142857,
+ 'left': 400.0,
+ 'position': 'absolute',
+ 'top': 502.85714285714283,
+ 'width': 600.0}},
+ 'type': 'Plotly'},
+ {'children': [],
+ 'id': 'SOwRH1rLV',
+ 'props': {'frameBorder': 0,
+ 'scrolling': 'no',
+ 'src': 'https://plot.ly/~AdamKulidjian/3564/.embed?link=false',
+ 'style': {'height': 96.57142857142857,
+ 'left': 400.0,
+ 'position': 'absolute',
+ 'top': 603.4285714285713,
+ 'width': 600.0}},
+ 'type': 'Plotly'}],
+ 'id': 'S6VmZlI5Q',
+ 'props': {'style': {'backgroundColor': '#FFFFFF'},
+ 'transition': ['slide']}}],
+ 'version': '0.1.3'}}
+
+ for k in ['version', 'paragraphStyles', 'slidePreviews']:
+ self.assertEqual(
+ my_pres['presentation'][k],
+ exp_pres['presentation'][k]
+ )
+
+ self.assertEqual(
+ len(my_pres['presentation']['slides']),
+ len(exp_pres['presentation']['slides'])
+ )
+
+ for slide_idx in range(len(my_pres['presentation']['slides'])):
+ childs = my_pres['presentation']['slides'][slide_idx]['children']
+ # transitions and background color
+ self.assertEqual(
+ my_pres['presentation']['slides'][slide_idx]['props'],
+ exp_pres['presentation']['slides'][slide_idx]['props']
+ )
+ for child_idx in range(len(childs)):
+ # check urls
+ if (my_pres['presentation']['slides'][slide_idx]['children']
+ [child_idx]['type'] in ['Image', 'Plotly']):
+ self.assertEqual(
+ (my_pres['presentation']['slides'][slide_idx]
+ ['children'][child_idx]['props']),
+ (exp_pres['presentation']['slides'][slide_idx]
+ ['children'][child_idx]['props'])
+ )
+
+ # styles in children
+ self.assertEqual(
+ (my_pres['presentation']['slides'][slide_idx]
+ ['children'][child_idx]['props']),
+ (exp_pres['presentation']['slides'][slide_idx]
+ ['children'][child_idx]['props'])
+ )
diff --git a/chart_studio/tests/test_plot_ly/test_stream/__init__.py b/chart_studio/tests/test_plot_ly/test_stream/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/chart_studio/tests/test_plot_ly/test_stream/test_stream.py b/chart_studio/tests/test_plot_ly/test_stream/test_stream.py
new file mode 100644
index 00000000000..7d7aac7670a
--- /dev/null
+++ b/chart_studio/tests/test_plot_ly/test_stream/test_stream.py
@@ -0,0 +1,166 @@
+"""
+Streaming tests.
+
+"""
+from __future__ import absolute_import
+
+import time
+
+from nose.plugins.attrib import attr
+
+from chart_studio import plotly as py
+from plotly.graph_objs import (Layout, Scatter, Stream)
+from chart_studio.tests.utils import PlotlyTestCase
+
+un = 'PythonAPI'
+ak = 'ubpiol2cve'
+tk = 'vaia8trjjb'
+config = {'plotly_domain': 'https://plot.ly',
+ 'plotly_streaming_domain': 'stream.plot.ly',
+ 'plotly_api_domain': 'https://api.plot.ly',
+ 'plotly_ssl_verification': False}
+
+
+class TestStreaming(PlotlyTestCase):
+
+ def setUp(self):
+ super(TestStreaming, self).setUp()
+ py.sign_in(un, ak, **config)
+
+ #@attr('slow')
+ def test_initialize_stream_plot(self):
+ py.sign_in(un, ak)
+ stream = Stream(token=tk, maxpoints=50)
+ url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)],
+ auto_open=False,
+ world_readable=True,
+ filename='stream-test')
+ assert url == 'https://plot.ly/~PythonAPI/461'
+ time.sleep(.5)
+
+ @attr('slow')
+ def test_stream_single_points(self):
+ py.sign_in(un, ak)
+ stream = Stream(token=tk, maxpoints=50)
+ res = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)],
+ auto_open=False,
+ world_readable=True,
+ filename='stream-test')
+ time.sleep(.5)
+ my_stream = py.Stream(tk)
+ my_stream.open()
+ my_stream.write(Scatter(x=[1], y=[10]))
+ time.sleep(.5)
+ my_stream.close()
+
+ @attr('slow')
+ def test_stream_multiple_points(self):
+ py.sign_in(un, ak)
+ stream = Stream(token=tk, maxpoints=50)
+ url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)],
+ auto_open=False,
+ world_readable=True,
+ filename='stream-test')
+ time.sleep(.5)
+ my_stream = py.Stream(tk)
+ my_stream.open()
+ my_stream.write(Scatter(x=[1, 2, 3, 4], y=[2, 1, 2, 5]))
+ time.sleep(.5)
+ my_stream.close()
+
+ @attr('slow')
+ def test_stream_layout(self):
+ py.sign_in(un, ak)
+ stream = Stream(token=tk, maxpoints=50)
+ url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)],
+ auto_open=False,
+ world_readable=True,
+ filename='stream-test')
+ time.sleep(.5)
+ title_0 = "some title i picked first"
+ title_1 = "this other title i picked second"
+ my_stream = py.Stream(tk)
+ my_stream.open()
+ my_stream.write(Scatter(x=[1], y=[10]), layout=Layout(title=title_0))
+ time.sleep(.5)
+ my_stream.close()
+ my_stream.open()
+ my_stream.write(Scatter(x=[1], y=[10]), layout=Layout(title=title_1))
+ my_stream.close()
+
+ @attr('slow')
+ def test_stream_unstreamable(self):
+
+ # even though `name` isn't streamable, we don't validate it --> pass
+
+ py.sign_in(un, ak)
+ my_stream = py.Stream(tk)
+ my_stream.open()
+ my_stream.write(Scatter(x=[1], y=[10], name='nope'))
+ my_stream.close()
+
+ def test_stream_no_scheme(self):
+
+ # If no scheme is used in the plotly_streaming_domain, port 80
+ # should be used for streaming and ssl_enabled should be False
+
+ py.sign_in(un, ak, **{'plotly_streaming_domain': 'stream.plot.ly'})
+ my_stream = py.Stream(tk)
+ expected_streaming_specs = {
+ 'server': 'stream.plot.ly',
+ 'port': 80,
+ 'ssl_enabled': False,
+ 'ssl_verification_enabled': False,
+ 'headers': {
+ 'Host': 'stream.plot.ly',
+ 'plotly-streamtoken': tk
+ }
+ }
+ actual_streaming_specs = my_stream.get_streaming_specs()
+ self.assertEqual(expected_streaming_specs, actual_streaming_specs)
+
+ def test_stream_http(self):
+
+ # If the http scheme is used in the plotly_streaming_domain, port 80
+ # should be used for streaming and ssl_enabled should be False
+
+ py.sign_in(un, ak,
+ **{'plotly_streaming_domain': 'http://stream.plot.ly'})
+ my_stream = py.Stream(tk)
+ expected_streaming_specs = {
+ 'server': 'stream.plot.ly',
+ 'port': 80,
+ 'ssl_enabled': False,
+ 'ssl_verification_enabled': False,
+ 'headers': {
+ 'Host': 'stream.plot.ly',
+ 'plotly-streamtoken': tk
+ }
+ }
+ actual_streaming_specs = my_stream.get_streaming_specs()
+ self.assertEqual(expected_streaming_specs, actual_streaming_specs)
+
+ def test_stream_https(self):
+
+ # If the https scheme is used in the plotly_streaming_domain, port 443
+ # should be used for streaming, ssl_enabled should be True,
+ # and ssl_verification_enabled should equal plotly_ssl_verification
+
+ ssl_stream_config = {
+ 'plotly_streaming_domain': 'https://stream.plot.ly',
+ 'plotly_ssl_verification': True
+ }
+ py.sign_in(un, ak, **ssl_stream_config)
+ my_stream = py.Stream(tk)
+ expected_streaming_specs = {
+ 'server': 'stream.plot.ly',
+ 'port': 443,
+ 'ssl_enabled': True,
+ 'ssl_verification_enabled': True,
+ 'headers': {
+ 'Host': 'stream.plot.ly',
+ 'plotly-streamtoken': tk
+ }
+ }
+ actual_streaming_specs = my_stream.get_streaming_specs()
+ self.assertEqual(expected_streaming_specs, actual_streaming_specs)
diff --git a/chart_studio/tests/utils.py b/chart_studio/tests/utils.py
new file mode 100644
index 00000000000..b840cc3699f
--- /dev/null
+++ b/chart_studio/tests/utils.py
@@ -0,0 +1,52 @@
+import copy
+from unittest import TestCase
+
+from chart_studio import session, files, utils
+from plotly.files import ensure_writable_plotly_dir
+
+class PlotlyTestCase(TestCase):
+
+ # parent test case to assist with clean up of local credentials/config
+
+ def __init__(self, *args, **kwargs):
+ self._credentials = None
+ self._config = None
+ self._graph_reference = None
+ self._session = None
+ super(PlotlyTestCase, self).__init__(*args, **kwargs)
+
+ @classmethod
+ def setUpClass(cls):
+ session._session = {
+ 'credentials': {},
+ 'config': {},
+ 'plot_options': {}
+ }
+
+ def setUp(self):
+ self.stash_session()
+ self.stash_files()
+ defaults = dict(files.FILE_CONTENT[files.CREDENTIALS_FILE],
+ **files.FILE_CONTENT[files.CONFIG_FILE])
+ session.sign_in(**defaults)
+
+ def tearDown(self):
+ self.restore_files()
+ self.restore_session()
+
+ def stash_files(self):
+ self._credentials = utils.load_json_dict(files.CREDENTIALS_FILE)
+ self._config = utils.load_json_dict(files.CONFIG_FILE)
+
+ def restore_files(self):
+ if self._credentials and ensure_writable_plotly_dir():
+ utils.save_json_dict(files.CREDENTIALS_FILE, self._credentials)
+ if self._config and ensure_writable_plotly_dir():
+ utils.save_json_dict(files.CONFIG_FILE, self._config)
+
+ def stash_session(self):
+ self._session = copy.deepcopy(session._session)
+
+ def restore_session(self):
+ session._session.clear() # clear and update to preserve references.
+ session._session.update(self._session)
\ No newline at end of file
diff --git a/chart_studio/tools.py b/chart_studio/tools.py
new file mode 100644
index 00000000000..d04a23c3ea1
--- /dev/null
+++ b/chart_studio/tools.py
@@ -0,0 +1,399 @@
+# -*- coding: utf-8 -*-
+
+"""
+tools
+=====
+
+Functions that USERS will possibly want access to.
+
+"""
+from __future__ import absolute_import
+
+import warnings
+
+import six
+import copy
+
+from _plotly_utils import optional_imports
+import _plotly_utils.exceptions
+from _plotly_utils.files import ensure_writable_plotly_dir
+
+from chart_studio import session, utils
+from chart_studio.files import CONFIG_FILE, CREDENTIALS_FILE, FILE_CONTENT
+
+ipython_core_display = optional_imports.get_module('IPython.core.display')
+sage_salvus = optional_imports.get_module('sage_salvus')
+
+
+def get_config_defaults():
+ """
+ Convenience function to check current settings against defaults.
+
+ Example:
+
+ if plotly_domain != get_config_defaults()['plotly_domain']:
+ # do something
+
+ """
+ return dict(FILE_CONTENT[CONFIG_FILE]) # performs a shallow copy
+
+
+def ensure_local_plotly_files():
+ """Ensure that filesystem is setup/filled out in a valid way.
+ If the config or credential files aren't filled out, then write them
+ to the disk.
+ """
+ if ensure_writable_plotly_dir():
+ for fn in [CREDENTIALS_FILE, CONFIG_FILE]:
+ utils.ensure_file_exists(fn)
+ contents = utils.load_json_dict(fn)
+ contents_orig = contents.copy()
+ for key, val in list(FILE_CONTENT[fn].items()):
+ # TODO: removed type checking below, may want to revisit
+ if key not in contents:
+ contents[key] = val
+ contents_keys = list(contents.keys())
+ for key in contents_keys:
+ if key not in FILE_CONTENT[fn]:
+ del contents[key]
+ # save only if contents has changed.
+ # This is to avoid .credentials or .config file to be overwritten randomly,
+ # which we constantly keep experiencing
+ # (sync issues? the file might be locked for writing by other process in file._permissions)
+ if contents_orig.keys() != contents.keys():
+ utils.save_json_dict(fn, contents)
+
+ else:
+ warnings.warn("Looks like you don't have 'read-write' permission to "
+ "your 'home' ('~') directory or to our '~/.plotly' "
+ "directory. That means plotly's python api can't setup "
+ "local configuration files. No problem though! You'll "
+ "just have to sign-in using 'plotly.plotly.sign_in()'. "
+ "For help with that: 'help(plotly.plotly.sign_in)'."
+ "\nQuestions? Visit https://support.plot.ly")
+
+
+### credentials tools ###
+
+def set_credentials_file(username=None,
+ api_key=None,
+ stream_ids=None,
+ proxy_username=None,
+ proxy_password=None):
+ """Set the keyword-value pairs in `~/.plotly_credentials`.
+
+ :param (str) username: The username you'd use to sign in to Plotly
+ :param (str) api_key: The api key associated with above username
+ :param (list) stream_ids: Stream tokens for above credentials
+ :param (str) proxy_username: The un associated with with your Proxy
+ :param (str) proxy_password: The pw associated with your Proxy un
+
+ """
+ if not ensure_writable_plotly_dir():
+ raise _plotly_utils.exceptions.PlotlyError("You don't have proper file permissions "
+ "to run this function.")
+ ensure_local_plotly_files() # make sure what's there is OK
+ credentials = get_credentials_file()
+ if isinstance(username, six.string_types):
+ credentials['username'] = username
+ if isinstance(api_key, six.string_types):
+ credentials['api_key'] = api_key
+ if isinstance(proxy_username, six.string_types):
+ credentials['proxy_username'] = proxy_username
+ if isinstance(proxy_password, six.string_types):
+ credentials['proxy_password'] = proxy_password
+ if isinstance(stream_ids, (list, tuple)):
+ credentials['stream_ids'] = stream_ids
+ utils.save_json_dict(CREDENTIALS_FILE, credentials)
+ ensure_local_plotly_files() # make sure what we just put there is OK
+
+
+def get_credentials_file(*args):
+ """Return specified args from `~/.plotly_credentials`. as dict.
+
+ Returns all if no arguments are specified.
+
+ Example:
+ get_credentials_file('username')
+
+ """
+ # Read credentials from file if possible
+ credentials = utils.load_json_dict(CREDENTIALS_FILE, *args)
+ if not credentials:
+ # Credentials could not be read, use defaults
+ credentials = copy.copy(FILE_CONTENT[CREDENTIALS_FILE])
+
+ return credentials
+
+
+def reset_credentials_file():
+ ensure_local_plotly_files() # make sure what's there is OK
+ utils.save_json_dict(CREDENTIALS_FILE, {})
+ ensure_local_plotly_files() # put the defaults back
+
+
+### config tools ###
+
+def set_config_file(plotly_domain=None,
+ plotly_streaming_domain=None,
+ plotly_api_domain=None,
+ plotly_ssl_verification=None,
+ plotly_proxy_authorization=None,
+ world_readable=None,
+ sharing=None,
+ auto_open=None):
+ """Set the keyword-value pairs in `~/.plotly/.config`.
+
+ :param (str) plotly_domain: ex - https://plot.ly
+ :param (str) plotly_streaming_domain: ex - stream.plot.ly
+ :param (str) plotly_api_domain: ex - https://api.plot.ly
+ :param (bool) plotly_ssl_verification: True = verify, False = don't verify
+ :param (bool) plotly_proxy_authorization: True = use plotly proxy auth creds
+ :param (bool) world_readable: True = public, False = private
+
+ """
+ if not ensure_writable_plotly_dir():
+ raise _plotly_utils.exceptions.PlotlyError("You don't have proper file permissions "
+ "to run this function.")
+ ensure_local_plotly_files() # make sure what's there is OK
+ utils.validate_world_readable_and_sharing_settings({
+ 'sharing': sharing, 'world_readable': world_readable})
+
+ settings = get_config_file()
+ if isinstance(plotly_domain, six.string_types):
+ settings['plotly_domain'] = plotly_domain
+ elif plotly_domain is not None:
+ raise TypeError('plotly_domain should be a string')
+ if isinstance(plotly_streaming_domain, six.string_types):
+ settings['plotly_streaming_domain'] = plotly_streaming_domain
+ elif plotly_streaming_domain is not None:
+ raise TypeError('plotly_streaming_domain should be a string')
+ if isinstance(plotly_api_domain, six.string_types):
+ settings['plotly_api_domain'] = plotly_api_domain
+ elif plotly_api_domain is not None:
+ raise TypeError('plotly_api_domain should be a string')
+ if isinstance(plotly_ssl_verification, (six.string_types, bool)):
+ settings['plotly_ssl_verification'] = plotly_ssl_verification
+ elif plotly_ssl_verification is not None:
+ raise TypeError('plotly_ssl_verification should be a boolean')
+ if isinstance(plotly_proxy_authorization, (six.string_types, bool)):
+ settings['plotly_proxy_authorization'] = plotly_proxy_authorization
+ elif plotly_proxy_authorization is not None:
+ raise TypeError('plotly_proxy_authorization should be a boolean')
+ if isinstance(auto_open, bool):
+ settings['auto_open'] = auto_open
+ elif auto_open is not None:
+ raise TypeError('auto_open should be a boolean')
+
+ # validate plotly_domain and plotly_api_domain
+ utils.validate_plotly_domains(
+ {'plotly_domain': plotly_domain, 'plotly_api_domain': plotly_api_domain}
+ )
+
+ if isinstance(world_readable, bool):
+ settings['world_readable'] = world_readable
+ settings.pop('sharing')
+ elif world_readable is not None:
+ raise TypeError('Input should be a boolean')
+ if isinstance(sharing, six.string_types):
+ settings['sharing'] = sharing
+ elif sharing is not None:
+ raise TypeError('sharing should be a string')
+ utils.set_sharing_and_world_readable(settings)
+
+ utils.save_json_dict(CONFIG_FILE, settings)
+ ensure_local_plotly_files() # make sure what we just put there is OK
+
+
+def get_config_file(*args):
+ """Return specified args from `~/.plotly/.config`. as tuple.
+
+ Returns all if no arguments are specified.
+
+ Example:
+ get_config_file('plotly_domain')
+
+ """
+ # Read config from file if possible
+ config = utils.load_json_dict(CONFIG_FILE, *args)
+ if not config:
+ # Config could not be read, use defaults
+ config = copy.copy(FILE_CONTENT[CONFIG_FILE])
+
+ return config
+
+
+def reset_config_file():
+ ensure_local_plotly_files() # make sure what's there is OK
+ f = open(CONFIG_FILE, 'w')
+ f.close()
+ ensure_local_plotly_files() # put the defaults back
+
+
+### embed tools ###
+
+def get_embed(file_owner_or_url, file_id=None, width="100%", height=525):
+ """Returns HTML code to embed figure on a webpage as an