Skip to content

Commit 2c72a7a

Browse files
timifasubaamistercrunch
authored andcommitted
Use json for imports and exports, not pickle (#4243)
* make superset imports and exports use json, not pickle * fix tests
1 parent 4b11f45 commit 2c72a7a

4 files changed

Lines changed: 81 additions & 19 deletions

File tree

superset/models/core.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import functools
1010
import json
1111
import logging
12-
import pickle
1312
import textwrap
1413

1514
from flask import escape, g, Markup, request
@@ -395,7 +394,7 @@ def import_obj(cls, dashboard_to_import, import_time=None):
395394
be overridden or just copies over. Slices that belong to this
396395
dashboard will be wired to existing tables. This function can be used
397396
to import/export dashboards between multiple superset instances.
398-
Audit metadata isn't copies over.
397+
Audit metadata isn't copied over.
399398
"""
400399
def alter_positions(dashboard, old_to_new_slc_id_dict):
401400
""" Updates slice_ids in the position json.
@@ -533,10 +532,10 @@ def export_dashboards(cls, dashboard_ids):
533532
make_transient(eager_datasource)
534533
eager_datasources.append(eager_datasource)
535534

536-
return pickle.dumps({
535+
return json.dumps({
537536
'dashboards': copied_dashboards,
538537
'datasources': eager_datasources,
539-
})
538+
}, cls=utils.DashboardEncoder, indent=4)
540539

541540

542541
class Database(Model, AuditMixinNullable, ImportMixin):

superset/utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from sqlalchemy import event, exc, select
4343
from sqlalchemy.types import TEXT, TypeDecorator
4444

45+
4546
logging.getLogger('MARKDOWN').setLevel(logging.INFO)
4647

4748
PY3K = sys.version_info >= (3, 0)
@@ -240,6 +241,55 @@ def dttm_from_timtuple(d):
240241
d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec)
241242

242243

244+
def decode_dashboards(o):
245+
"""
246+
Function to be passed into json.loads obj_hook parameter
247+
Recreates the dashboard object from a json representation.
248+
"""
249+
import superset.models.core as models
250+
from superset.connectors.sqla.models import (
251+
SqlaTable, SqlMetric, TableColumn,
252+
)
253+
254+
if '__Dashboard__' in o:
255+
d = models.Dashboard()
256+
d.__dict__.update(o['__Dashboard__'])
257+
return d
258+
elif '__Slice__' in o:
259+
d = models.Slice()
260+
d.__dict__.update(o['__Slice__'])
261+
return d
262+
elif '__TableColumn__' in o:
263+
d = TableColumn()
264+
d.__dict__.update(o['__TableColumn__'])
265+
return d
266+
elif '__SqlaTable__' in o:
267+
d = SqlaTable()
268+
d.__dict__.update(o['__SqlaTable__'])
269+
return d
270+
elif '__SqlMetric__' in o:
271+
d = SqlMetric()
272+
d.__dict__.update(o['__SqlMetric__'])
273+
return d
274+
elif '__datetime__' in o:
275+
return datetime.strptime(o['__datetime__'], '%Y-%m-%dT%H:%M:%S')
276+
else:
277+
return o
278+
279+
280+
class DashboardEncoder(json.JSONEncoder):
281+
# pylint: disable=E0202
282+
def default(self, o):
283+
try:
284+
vals = {
285+
k: v for k, v in o.__dict__.items() if k != '_sa_instance_state'}
286+
return {'__{}__'.format(o.__class__.__name__): vals}
287+
except Exception:
288+
if type(o) == datetime:
289+
return {'__datetime__': o.replace(microsecond=0).isoformat()}
290+
return json.JSONEncoder.default(self, o)
291+
292+
243293
def parse_human_timedelta(s):
244294
"""
245295
Returns ``datetime.datetime`` from natural language time deltas

superset/views/core.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import json
99
import logging
1010
import os
11-
import pickle
1211
import re
1312
import time
1413
import traceback
@@ -601,7 +600,7 @@ def download_dashboards(self):
601600
ids = request.args.getlist('id')
602601
return Response(
603602
models.Dashboard.export_dashboards(ids),
604-
headers=generate_download_headers('pickle'),
603+
headers=generate_download_headers('json'),
605604
mimetype='application/text')
606605
return self.render_template(
607606
'superset/export_dashboards.html',
@@ -1114,15 +1113,14 @@ def explore_json(self, datasource_type, datasource_id):
11141113
@has_access
11151114
@expose('/import_dashboards', methods=['GET', 'POST'])
11161115
def import_dashboards(self):
1117-
"""Overrides the dashboards using pickled instances from the file."""
1116+
"""Overrides the dashboards using json instances from the file."""
11181117
f = request.files.get('file')
11191118
if request.method == 'POST' and f:
11201119
current_tt = int(time.time())
1121-
data = pickle.load(f)
1120+
data = json.loads(f.stream.read(), object_hook=utils.decode_dashboards)
11221121
# TODO: import DRUID datasources
11231122
for table in data['datasources']:
1124-
ds_class = ConnectorRegistry.sources.get(table.type)
1125-
ds_class.import_obj(table, import_time=current_tt)
1123+
type(table).import_obj(table, import_time=current_tt)
11261124
db.session.commit()
11271125
for dashboard in data['dashboards']:
11281126
models.Dashboard.import_obj(

tests/import_export_tests.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
from __future__ import unicode_literals
66

77
import json
8-
import pickle
98
import unittest
109

1110
from sqlalchemy.orm.session import make_transient
1211

13-
from superset import db
12+
from superset import db, utils
1413
from superset.connectors.druid.models import (
1514
DruidColumn, DruidDatasource, DruidMetric,
1615
)
@@ -205,13 +204,22 @@ def test_export_1_dashboard(self):
205204
.format(birth_dash.id)
206205
)
207206
resp = self.client.get(export_dash_url)
208-
exported_dashboards = pickle.loads(resp.data)['dashboards']
207+
exported_dashboards = json.loads(
208+
resp.data.decode('utf-8'),
209+
object_hook=utils.decode_dashboards,
210+
)['dashboards']
209211
self.assert_dash_equals(birth_dash, exported_dashboards[0])
210212
self.assertEquals(
211213
birth_dash.id,
212-
json.loads(exported_dashboards[0].json_metadata)['remote_id'])
213-
214-
exported_tables = pickle.loads(resp.data)['datasources']
214+
json.loads(
215+
exported_dashboards[0].json_metadata,
216+
object_hook=utils.decode_dashboards,
217+
)['remote_id'])
218+
219+
exported_tables = json.loads(
220+
resp.data.decode('utf-8'),
221+
object_hook=utils.decode_dashboards,
222+
)['datasources']
215223
self.assertEquals(1, len(exported_tables))
216224
self.assert_table_equals(
217225
self.get_table_by_name('birth_names'), exported_tables[0])
@@ -223,8 +231,12 @@ def test_export_2_dashboards(self):
223231
'/dashboardmodelview/export_dashboards_form?id={}&id={}&action=go'
224232
.format(birth_dash.id, world_health_dash.id))
225233
resp = self.client.get(export_dash_url)
226-
exported_dashboards = sorted(pickle.loads(resp.data)['dashboards'],
227-
key=lambda d: d.dashboard_title)
234+
exported_dashboards = sorted(
235+
json.loads(
236+
resp.data.decode('utf-8'),
237+
object_hook=utils.decode_dashboards,
238+
)['dashboards'],
239+
key=lambda d: d.dashboard_title)
228240
self.assertEquals(2, len(exported_dashboards))
229241
self.assert_dash_equals(birth_dash, exported_dashboards[0])
230242
self.assertEquals(
@@ -239,7 +251,10 @@ def test_export_2_dashboards(self):
239251
)
240252

241253
exported_tables = sorted(
242-
pickle.loads(resp.data)['datasources'], key=lambda t: t.table_name)
254+
json.loads(
255+
resp.data.decode('utf-8'),
256+
object_hook=utils.decode_dashboards)['datasources'],
257+
key=lambda t: t.table_name)
243258
self.assertEquals(2, len(exported_tables))
244259
self.assert_table_equals(
245260
self.get_table_by_name('birth_names'), exported_tables[0])

0 commit comments

Comments
 (0)