16
16
"""
17
17
from __future__ import absolute_import
18
18
19
+ import base64
19
20
import copy
20
21
import json
21
22
import os
22
23
import time
24
+ import uuid
23
25
import warnings
24
26
import webbrowser
25
27
26
28
import six
27
29
import six .moves
28
30
from requests .compat import json as _json
29
31
32
+ from _plotly_utils .basevalidators import CompoundValidator , is_array
30
33
from plotly import exceptions , files , session , tools , utils
31
34
from plotly .api import v1 , v2
32
35
from plotly .basedatatypes import BaseTraceType , BaseFigure , BaseLayoutType
33
36
from plotly .plotly import chunked_requests
34
37
35
- from plotly .graph_objs import Scatter
38
+ from plotly .graph_objs import Figure
36
39
37
- from plotly .grid_objs import Grid , Column
40
+ from plotly .grid_objs import Grid
38
41
from plotly .dashboard_objs import dashboard_objs as dashboard
39
42
40
43
# This is imported like this for backwards compat. Careful if changing.
52
55
'sharing' : files .FILE_CONTENT [files .CONFIG_FILE ]['sharing' ]
53
56
}
54
57
58
+ warnings .filterwarnings (
59
+ 'default' , r'The fileopt parameter is deprecated .*' , DeprecationWarning
60
+ )
61
+
55
62
SHARING_ERROR_MSG = (
56
63
"Whoops, sharing can only be set to either 'public', 'private', or "
57
64
"'secret'."
@@ -71,7 +78,7 @@ def sign_in(username, api_key, **kwargs):
71
78
update_plot_options = session .update_session_plot_options
72
79
73
80
74
- def _plot_option_logic (plot_options_from_call_signature ):
81
+ def _plot_option_logic (plot_options_from_args ):
75
82
"""
76
83
Given some plot_options as part of a plot call, decide on final options.
77
84
Precedence:
@@ -84,10 +91,21 @@ def _plot_option_logic(plot_options_from_call_signature):
84
91
default_plot_options = copy .deepcopy (DEFAULT_PLOT_OPTIONS )
85
92
file_options = tools .get_config_file ()
86
93
session_options = session .get_session_plot_options ()
87
- plot_options_from_call_signature = copy .deepcopy (plot_options_from_call_signature )
94
+ plot_options_from_args = copy .deepcopy (plot_options_from_args )
95
+
96
+ # fileopt deprecation warnings
97
+ fileopt_warning = ('The fileopt parameter is deprecated '
98
+ 'and will be removed in plotly.py version 4' )
99
+ if ('filename' in plot_options_from_args and
100
+ plot_options_from_args .get ('fileopt' , 'overwrite' ) != 'overwrite' ):
101
+ warnings .warn (fileopt_warning , DeprecationWarning )
102
+
103
+ if ('filename' not in plot_options_from_args and
104
+ plot_options_from_args .get ('fileopt' , 'new' ) != 'new' ):
105
+ warnings .warn (fileopt_warning , DeprecationWarning )
88
106
89
107
# Validate options and fill in defaults w world_readable and sharing
90
- for option_set in [plot_options_from_call_signature ,
108
+ for option_set in [plot_options_from_args ,
91
109
session_options , file_options ]:
92
110
utils .validate_world_readable_and_sharing_settings (option_set )
93
111
utils .set_sharing_and_world_readable (option_set )
@@ -101,7 +119,7 @@ def _plot_option_logic(plot_options_from_call_signature):
101
119
user_plot_options .update (default_plot_options )
102
120
user_plot_options .update (file_options )
103
121
user_plot_options .update (session_options )
104
- user_plot_options .update (plot_options_from_call_signature )
122
+ user_plot_options .update (plot_options_from_args )
105
123
user_plot_options = {k : v for k , v in user_plot_options .items ()
106
124
if k in default_plot_options }
107
125
@@ -881,6 +899,20 @@ def mkdirs(cls, folder_path):
881
899
response = v2 .folders .create ({'path' : folder_path })
882
900
return response .status_code
883
901
902
+ @classmethod
903
+ def ensure_dirs (cls , folder_path ):
904
+ """
905
+ Create folder(s) if they don't exist, but unlike mkdirs, doesn't
906
+ raise an error if folder path already exist
907
+ """
908
+ try :
909
+ cls .mkdirs (folder_path )
910
+ except exceptions .PlotlyRequestError as e :
911
+ if 'already exists' in e .message :
912
+ pass
913
+ else :
914
+ raise e
915
+
884
916
885
917
class grid_ops :
886
918
"""
@@ -919,7 +951,7 @@ def ensure_uploaded(fid):
919
951
)
920
952
921
953
@classmethod
922
- def upload (cls , grid , filename ,
954
+ def upload (cls , grid , filename = None ,
923
955
world_readable = True , auto_open = True , meta = None ):
924
956
"""
925
957
Upload a grid to your Plotly account with the specified filename.
@@ -933,7 +965,8 @@ def upload(cls, grid, filename,
933
965
separated by backslashes (`/`).
934
966
If a grid, plot, or folder already exists with the same
935
967
filename, a `plotly.exceptions.RequestError` will be
936
- thrown with status_code 409
968
+ thrown with status_code 409. If filename is None,
969
+ and randomly generated filename will be used.
937
970
938
971
Optional keyword arguments:
939
972
- world_readable (default=True): make this grid publically (True)
@@ -979,23 +1012,31 @@ def upload(cls, grid, filename,
979
1012
```
980
1013
981
1014
"""
982
- # Make a folder path
983
- if filename [- 1 ] == '/' :
984
- filename = filename [0 :- 1 ]
985
-
986
- paths = filename .split ('/' )
987
- parent_path = '/' .join (paths [0 :- 1 ])
988
-
989
- filename = paths [- 1 ]
990
-
991
- if parent_path != '' :
992
- file_ops .mkdirs (parent_path )
993
-
994
1015
# transmorgify grid object into plotly's format
995
1016
grid_json = grid ._to_plotly_grid_json ()
996
1017
if meta is not None :
997
1018
grid_json ['metadata' ] = meta
998
1019
1020
+ # Make a folder path
1021
+ if filename :
1022
+ if filename [- 1 ] == '/' :
1023
+ filename = filename [0 :- 1 ]
1024
+
1025
+ paths = filename .split ('/' )
1026
+ parent_path = '/' .join (paths [0 :- 1 ])
1027
+ filename = paths [- 1 ]
1028
+
1029
+ if parent_path != '' :
1030
+ file_ops .ensure_dirs (parent_path )
1031
+ else :
1032
+ # Create anonymous grid name
1033
+ hash_val = hash (json .dumps (grid_json , sort_keys = True ))
1034
+ id = base64 .urlsafe_b64encode (str (hash_val ).encode ('utf8' ))
1035
+ id_str = id .decode (encoding = 'utf8' ).replace ('=' , '' )
1036
+ filename = 'grid_' + id_str
1037
+ # filename = 'grid_' + str(hash_val)
1038
+ parent_path = ''
1039
+
999
1040
payload = {
1000
1041
'filename' : filename ,
1001
1042
'data' : grid_json ,
@@ -1005,12 +1046,11 @@ def upload(cls, grid, filename,
1005
1046
if parent_path != '' :
1006
1047
payload ['parent_path' ] = parent_path
1007
1048
1008
- response = v2 . grids . create (payload )
1049
+ file_info = _create_or_update (payload , 'grid' )
1009
1050
1010
- parsed_content = response .json ()
1011
- cols = parsed_content ['file' ]['cols' ]
1012
- fid = parsed_content ['file' ]['fid' ]
1013
- web_url = parsed_content ['file' ]['web_url' ]
1051
+ cols = file_info ['cols' ]
1052
+ fid = file_info ['fid' ]
1053
+ web_url = file_info ['web_url' ]
1014
1054
1015
1055
# mutate the grid columns with the id's returned from the server
1016
1056
cls ._fill_in_response_column_ids (grid , cols , fid )
@@ -1373,6 +1413,66 @@ def get_grid(grid_url, raw=False):
1373
1413
return Grid (parsed_content , fid )
1374
1414
1375
1415
1416
+ def _create_or_update (data , filetype ):
1417
+ """
1418
+ Create or update (if file exists) and grid, plot, spectacle, or dashboard
1419
+ object
1420
+
1421
+ Parameters
1422
+ ----------
1423
+ data: dict
1424
+ update/create API payload
1425
+ filetype: str
1426
+ One of 'plot', 'grid', 'spectacle_presentation', or 'dashboard'
1427
+
1428
+ Returns
1429
+ -------
1430
+ dict
1431
+ File info from API response
1432
+ """
1433
+ api_module = getattr (v2 , filetype + 's' )
1434
+
1435
+ # lookup if pre-existing filename already exists
1436
+ if 'parent_path' in data :
1437
+ filename = data ['parent_path' ] + '/' + data ['filename' ]
1438
+ else :
1439
+ filename = data .get ('filename' , None )
1440
+
1441
+ if filename :
1442
+ try :
1443
+ lookup_res = v2 .files .lookup (filename )
1444
+ matching_file = json .loads (lookup_res .content )
1445
+
1446
+ if matching_file ['filetype' ] == filetype :
1447
+ fid = matching_file ['fid' ]
1448
+ res = api_module .update (fid , data )
1449
+ else :
1450
+ raise exceptions .PlotlyError ("""
1451
+ '{filename}' is already a {other_filetype} in your account.
1452
+ While you can overwrite {filetype}s with the same name, you can't overwrite
1453
+ files with a different type. Try deleting '{filename}' in your account or
1454
+ changing the filename.""" .format (
1455
+ filename = filename ,
1456
+ filetype = filetype ,
1457
+ other_filetype = matching_file ['filetype' ]
1458
+ )
1459
+ )
1460
+
1461
+ except exceptions .PlotlyRequestError :
1462
+ res = api_module .create (data )
1463
+ else :
1464
+ res = api_module .create (data )
1465
+
1466
+ # Check response
1467
+ res .raise_for_status ()
1468
+
1469
+ # Get resulting file content
1470
+ file_info = res .json ()
1471
+ file_info = file_info .get ('file' , file_info )
1472
+
1473
+ return file_info
1474
+
1475
+
1376
1476
class dashboard_ops :
1377
1477
"""
1378
1478
Interface to Plotly's Dashboards API.
@@ -1453,37 +1553,15 @@ def upload(cls, dashboard, filename, sharing='public', auto_open=True):
1453
1553
'world_readable' : world_readable
1454
1554
}
1455
1555
1456
- # lookup if pre-existing filename already exists
1457
- try :
1458
- lookup_res = v2 .files .lookup (filename )
1459
- matching_file = json .loads (lookup_res .content )
1556
+ file_info = _create_or_update (data , 'dashboard' )
1460
1557
1461
- if matching_file ['filetype' ] == 'dashboard' :
1462
- old_fid = matching_file ['fid' ]
1463
- res = v2 .dashboards .update (old_fid , data )
1464
- else :
1465
- raise exceptions .PlotlyError (
1466
- "'{filename}' is already a {filetype} in your account. "
1467
- "While you can overwrite dashboards with the same name, "
1468
- "you can't change overwrite files with a different type. "
1469
- "Try deleting '{filename}' in your account or changing "
1470
- "the filename." .format (
1471
- filename = filename ,
1472
- filetype = matching_file ['filetype' ]
1473
- )
1474
- )
1475
-
1476
- except exceptions .PlotlyRequestError :
1477
- res = v2 .dashboards .create (data )
1478
- res .raise_for_status ()
1479
-
1480
- url = res .json ()['web_url' ]
1558
+ url = file_info ['web_url' ]
1481
1559
1482
1560
if sharing == 'secret' :
1483
1561
url = add_share_key_to_url (url )
1484
1562
1485
1563
if auto_open :
1486
- webbrowser .open_new (res . json () ['web_url' ])
1564
+ webbrowser .open_new (file_info ['web_url' ])
1487
1565
1488
1566
return url
1489
1567
@@ -1573,40 +1651,164 @@ def upload(cls, presentation, filename, sharing='public', auto_open=True):
1573
1651
'world_readable' : world_readable
1574
1652
}
1575
1653
1576
- # lookup if pre-existing filename already exists
1577
- try :
1578
- lookup_res = v2 .files .lookup (filename )
1579
- lookup_res .raise_for_status ()
1580
- matching_file = json .loads (lookup_res .content )
1581
-
1582
- if matching_file ['filetype' ] != 'spectacle_presentation' :
1583
- raise exceptions .PlotlyError (
1584
- "'{filename}' is already a {filetype} in your account. "
1585
- "You can't overwrite a file that is not a spectacle_"
1586
- "presentation. Please pick another filename." .format (
1587
- filename = filename ,
1588
- filetype = matching_file ['filetype' ]
1589
- )
1590
- )
1591
- else :
1592
- old_fid = matching_file ['fid' ]
1593
- res = v2 .spectacle_presentations .update (old_fid , data )
1654
+ file_info = _create_or_update (data , 'spectacle_presentation' )
1594
1655
1595
- except exceptions .PlotlyRequestError :
1596
- res = v2 .spectacle_presentations .create (data )
1597
- res .raise_for_status ()
1598
-
1599
- url = res .json ()['web_url' ]
1656
+ url = file_info ['web_url' ]
1600
1657
1601
1658
if sharing == 'secret' :
1602
1659
url = add_share_key_to_url (url )
1603
1660
1604
1661
if auto_open :
1605
- webbrowser .open_new (res . json () ['web_url' ])
1662
+ webbrowser .open_new (file_info ['web_url' ])
1606
1663
1607
1664
return url
1608
1665
1609
1666
1667
+ def _extract_grid_graph_obj (obj_dict , reference_obj , grid , path ):
1668
+ """
1669
+ Extract inline data arrays from a graph_obj instance and place them in
1670
+ a grid
1671
+
1672
+ Parameters
1673
+ ----------
1674
+ obj_dict: dict
1675
+ dict representing a graph object that may contain inline arrays
1676
+ reference_obj: BasePlotlyType
1677
+ An empty instance of a `graph_obj` with type corresponding to obj_dict
1678
+ grid: Grid
1679
+ Grid to extract data arrays too
1680
+ path: str
1681
+ Path string of the location of `obj_dict` in the figure
1682
+
1683
+ Returns
1684
+ -------
1685
+ None
1686
+ Function modifies obj_dict and grid in-place
1687
+ """
1688
+
1689
+ from plotly .grid_objs import Column
1690
+
1691
+ for prop in list (obj_dict .keys ()):
1692
+ propsrc = '{}src' .format (prop )
1693
+ if propsrc in reference_obj :
1694
+ val = obj_dict [prop ]
1695
+ if is_array (val ):
1696
+ column = Column (val , path + prop )
1697
+ grid .append (column )
1698
+ obj_dict [propsrc ] = 'TBD'
1699
+ del obj_dict [prop ]
1700
+
1701
+ elif prop in reference_obj :
1702
+ prop_validator = reference_obj ._validators [prop ]
1703
+ if isinstance (prop_validator , CompoundValidator ):
1704
+ # Recurse on compound child
1705
+ _extract_grid_graph_obj (
1706
+ obj_dict [prop ],
1707
+ reference_obj [prop ],
1708
+ grid ,
1709
+ '{path}{prop}.' .format (path = path , prop = prop ))
1710
+
1711
+ # Chart studio doesn't handle links to columns inside object
1712
+ # arrays, so we don't extract them for now. Logic below works
1713
+ # and should be reinstated if chart studio gets this capability
1714
+ #
1715
+ # elif isinstance(prop_validator, CompoundArrayValidator):
1716
+ # # Recurse on elements of object arary
1717
+ # reference_element = prop_validator.validate_coerce([{}])[0]
1718
+ # for i, element_dict in enumerate(obj_dict[prop]):
1719
+ # _extract_grid_graph_obj(
1720
+ # element_dict,
1721
+ # reference_element,
1722
+ # grid,
1723
+ # '{path}{prop}.{i}.'.format(path=path, prop=prop, i=i)
1724
+ # )
1725
+
1726
+
1727
+ def _extract_grid_from_fig_like (fig , grid = None , path = '' ):
1728
+ """
1729
+ Extract inline data arrays from a figure and place them in a grid
1730
+
1731
+ Parameters
1732
+ ----------
1733
+ fig: dict
1734
+ A dict representing a figure or a frame
1735
+ grid: Grid or None (default None)
1736
+ The grid to place the extracted columns in. If None, a new grid will
1737
+ be constructed
1738
+ path: str (default '')
1739
+ Parent path, set to `frames` for use with frame objects
1740
+ Returns
1741
+ -------
1742
+ (dict, Grid)
1743
+ * dict: Figure dict with data arrays removed
1744
+ * Grid: Grid object containing one column for each removed data array.
1745
+ Columns are named with the path the corresponding data array
1746
+ (e.g. 'data.0.marker.size')
1747
+ """
1748
+
1749
+ if grid is None :
1750
+ # If not grid, this is top-level call so deep copy figure
1751
+ copy_fig = True
1752
+ grid = Grid ([])
1753
+ else :
1754
+ # Grid passed in so this is recursive call, don't copy figure
1755
+ copy_fig = False
1756
+
1757
+ if isinstance (fig , BaseFigure ):
1758
+ fig_dict = fig .to_dict ()
1759
+ elif isinstance (fig , dict ):
1760
+ fig_dict = copy .deepcopy (fig ) if copy_fig else fig
1761
+ else :
1762
+ raise ValueError ('Invalid figure type {}' .format (type (fig )))
1763
+
1764
+ # Process traces
1765
+ reference_fig = Figure ()
1766
+ reference_traces = {}
1767
+ for i , trace_dict in enumerate (fig_dict .get ('data' , [])):
1768
+ trace_type = trace_dict .get ('type' , 'scatter' )
1769
+ if trace_type not in reference_traces :
1770
+ reference_traces [trace_type ] = reference_fig .add_trace (
1771
+ {'type' : trace_type })
1772
+
1773
+ reference_trace = reference_traces [trace_type ]
1774
+ _extract_grid_graph_obj (
1775
+ trace_dict , reference_trace , grid , path + 'data.{}.' .format (i ))
1776
+
1777
+ # Process frames
1778
+ if 'frames' in fig_dict :
1779
+ for i , frame_dict in enumerate (fig_dict ['frames' ]):
1780
+ _extract_grid_from_fig_like (
1781
+ frame_dict , grid , 'frames.{}.' .format (i ))
1782
+
1783
+ return fig_dict , grid
1784
+
1785
+
1786
+ def _set_grid_column_references (figure , grid ):
1787
+ """
1788
+ Populate *src columns in a figure from uploaded grid
1789
+
1790
+ Parameters
1791
+ ----------
1792
+ figure: dict
1793
+ Figure dict that previously had inline data arrays extracted
1794
+ grid: Grid
1795
+ Grid that was created by extracting inline data arrays from figure
1796
+ using the _extract_grid_from_fig_like function
1797
+
1798
+ Returns
1799
+ -------
1800
+ None
1801
+ Function modifies figure in-place
1802
+ """
1803
+ for col in grid :
1804
+ prop_path = BaseFigure ._str_to_dict_path (col .name )
1805
+ prop_parent = figure
1806
+ for prop in prop_path [:- 1 ]:
1807
+ prop_parent = prop_parent [prop ]
1808
+
1809
+ prop_parent [prop_path [- 1 ] + 'src' ] = col .id
1810
+
1811
+
1610
1812
def create_animations (figure , filename = None , sharing = 'public' , auto_open = True ):
1611
1813
"""
1612
1814
BETA function that creates plots with animations via `frames`.
@@ -1773,43 +1975,69 @@ def create_animations(figure, filename=None, sharing='public', auto_open=True):
1773
1975
py.create_animations(figure, 'growing_circles')
1774
1976
```
1775
1977
"""
1776
- body = {
1978
+ payload = {
1777
1979
'figure' : figure ,
1778
1980
'world_readable' : True
1779
1981
}
1780
1982
1781
1983
# set filename if specified
1782
1984
if filename :
1783
- # warn user that creating folders isn't support in this version
1784
- if '/' in filename :
1785
- warnings .warn (
1786
- "This BETA version of 'create_animations' does not support "
1787
- "automatic folder creation. This means a filename of the form "
1788
- "'name1/name2' will just create the plot with that name only."
1789
- )
1790
- body ['filename' ] = filename
1985
+ # Strip trailing slash
1986
+ if filename [- 1 ] == '/' :
1987
+ filename = filename [0 :- 1 ]
1988
+
1989
+ # split off any parent directory
1990
+ paths = filename .split ('/' )
1991
+ parent_path = '/' .join (paths [0 :- 1 ])
1992
+ filename = paths [- 1 ]
1993
+
1994
+ # Create parent directory
1995
+ if parent_path != '' :
1996
+ file_ops .ensure_dirs (parent_path )
1997
+ payload ['parent_path' ] = parent_path
1998
+
1999
+ payload ['filename' ] = filename
2000
+ else :
2001
+ parent_path = ''
1791
2002
1792
2003
# set sharing
1793
2004
if sharing == 'public' :
1794
- body ['world_readable' ] = True
2005
+ payload ['world_readable' ] = True
1795
2006
elif sharing == 'private' :
1796
- body ['world_readable' ] = False
2007
+ payload ['world_readable' ] = False
1797
2008
elif sharing == 'secret' :
1798
- body ['world_readable' ] = False
1799
- body ['share_key_enabled' ] = True
2009
+ payload ['world_readable' ] = False
2010
+ payload ['share_key_enabled' ] = True
1800
2011
else :
1801
2012
raise exceptions .PlotlyError (
1802
2013
SHARING_ERROR_MSG
1803
2014
)
1804
2015
1805
- response = v2 .plots .create (body )
1806
- parsed_content = response .json ()
2016
+ # Extract grid
2017
+ figure , grid = _extract_grid_from_fig_like (figure )
2018
+
2019
+ if len (grid ) > 0 :
2020
+ if not filename :
2021
+ grid_filename = None
2022
+ elif parent_path :
2023
+ grid_filename = parent_path + '/' + filename + '_grid'
2024
+ else :
2025
+ grid_filename = filename + '_grid'
2026
+
2027
+ grid_ops .upload (grid = grid ,
2028
+ filename = grid_filename ,
2029
+ world_readable = payload ['world_readable' ],
2030
+ auto_open = False )
2031
+ _set_grid_column_references (figure , grid )
2032
+ payload ['figure' ] = figure
2033
+
2034
+ file_info = _create_or_update (payload , 'plot' )
1807
2035
1808
2036
if sharing == 'secret' :
1809
- web_url = (parsed_content [ 'file' ] ['web_url' ][:- 1 ] +
1810
- '?share_key=' + parsed_content [ 'file' ] ['share_key' ])
2037
+ web_url = (file_info ['web_url' ][:- 1 ] +
2038
+ '?share_key=' + file_info ['share_key' ])
1811
2039
else :
1812
- web_url = parsed_content [ 'file' ] ['web_url' ]
2040
+ web_url = file_info ['web_url' ]
1813
2041
1814
2042
if auto_open :
1815
2043
_open_url (web_url )
0 commit comments