1
1
"""Import a list of assets from a yaml file and create them in the superset assets folder."""
2
2
import os
3
- from zipfile import ZipFile
4
-
3
+ import uuid
5
4
import yaml
5
+ from zipfile import ZipFile
6
6
from superset .app import create_app
7
7
8
8
app = create_app ()
9
9
app .app_context ().push ()
10
10
11
+ from superset import security_manager
11
12
from superset .commands .importers .v1 .assets import ImportAssetsCommand
12
13
from superset .commands .importers .v1 .utils import get_contents_from_bundle
13
14
from superset .extensions import db
14
15
from superset .models .dashboard import Dashboard
15
16
from superset .utils .database import get_or_create_db
16
17
17
- from superset import security_manager
18
+ from copy import deepcopy
18
19
19
20
BASE_DIR = "/app/assets/superset"
20
21
25
26
"table_name" : "datasets" ,
26
27
}
27
28
29
+ for folder in ASSET_FOLDER_MAPPING .values ():
30
+ os .makedirs (f"{ BASE_DIR } /{ folder } " , exist_ok = True )
31
+
28
32
FILE_NAME_ATTRIBUTE = "_file_name"
29
33
34
+ TRANSLATIONS_FILE_PATH = "/app/pythonpath/locale.yaml"
35
+ ASSETS_FILE_PATH = "/app/pythonpath/assets.yaml"
36
+ ASSETS_ZIP_PATH = "/app/assets/assets.zip"
37
+
38
+ ASSETS_TRANSLATIONS = yaml .load (open (TRANSLATIONS_FILE_PATH , "r" ), Loader = yaml .FullLoader )
39
+
30
40
31
41
def main ():
32
42
create_assets ()
@@ -35,7 +45,7 @@ def main():
35
45
def create_assets ():
36
46
"""Create assets from a yaml file."""
37
47
roles = {}
38
- with open ("/app/pythonpath/assets.yaml" , "r" ) as file :
48
+ with open (ASSETS_FILE_PATH , "r" ) as file :
39
49
extra_assets = yaml .safe_load (file )
40
50
41
51
if not extra_assets :
@@ -45,53 +55,149 @@ def create_assets():
45
55
# For each asset, create a file in the right folder
46
56
for asset in extra_assets :
47
57
if FILE_NAME_ATTRIBUTE not in asset :
48
- print (f"Asset { asset } has no _file_name" )
49
- continue
58
+ raise Exception (f"Asset { asset } has no { FILE_NAME_ATTRIBUTE } " )
50
59
file_name = asset .pop (FILE_NAME_ATTRIBUTE )
51
60
52
61
# Find the right folder to create the asset in
53
62
for asset_name , folder in ASSET_FOLDER_MAPPING .items ():
54
- if asset_name in asset :
55
- if folder == "databases" :
56
- # This will fix the URI connection string by setting the right password.
57
- create_superset_db (
58
- asset ["database_name" ], asset ["sqlalchemy_uri" ]
59
- )
60
- elif folder == "dashboards" :
61
- dashboard_roles = asset .pop ("_roles" , None )
62
- if dashboard_roles :
63
- roles [asset ["uuid" ]] = [security_manager .find_role (role ) for role in dashboard_roles ]
64
-
65
- path = f"{ BASE_DIR } /{ folder } /{ file_name } "
66
- os .makedirs (os .path .dirname (path ), exist_ok = True )
67
- file = open (path , "w" )
68
- yaml .dump (asset , file )
69
- file .close ()
70
- break
71
-
72
- # Create the zip file and import the assets
73
- zip_path = "/app/assets/assets.zip"
74
- with ZipFile (zip_path , "w" ) as zip :
75
- for folder in ASSET_FOLDER_MAPPING .values ():
76
- for file_name in os .listdir (f"{ BASE_DIR } /{ folder } " ):
77
- zip .write (f"{ BASE_DIR } /{ folder } /{ file_name } " , f"import/{ folder } /{ file_name } " )
78
- zip .write (f"{ BASE_DIR } /metadata.yaml" , "import/metadata.yaml" )
63
+ if not asset_name in asset :
64
+ continue
79
65
80
- contents = get_contents_from_bundle (zip )
81
- command = ImportAssetsCommand (
82
- contents ,
66
+ write_asset_to_file (asset , asset_name , folder , file_name , roles )
67
+ break
68
+
69
+ create_zip_and_import_assets ()
70
+ update_dashboard_roles (roles )
71
+
72
+
73
+ def get_uuid5 (base_uuid , name ):
74
+ """Generate an idempotent uuid."""
75
+ base_uuid = uuid .UUID (base_uuid )
76
+ base_namespace = uuid .uuid5 (base_uuid , "superset" )
77
+ return uuid .uuid5 (base_namespace , name )
78
+
79
+
80
+ def write_asset_to_file (asset , asset_name , folder , file_name , roles ):
81
+ """Write an asset to a file and generated translated assets"""
82
+ if folder == "databases" :
83
+ # This will fix the URI connection string by setting the right password.
84
+ create_superset_db (asset ["database_name" ], asset ["sqlalchemy_uri" ])
85
+
86
+ asset_translation = ASSETS_TRANSLATIONS .get (asset .get ("uuid" ), {})
87
+
88
+ for language , title in asset_translation .items ():
89
+ updated_asset = generate_translated_asset (
90
+ asset , asset_name , folder , language , title , roles
83
91
)
84
92
85
- command .run ()
93
+ path = f"{ BASE_DIR } /{ folder } /{ file_name } -{ language } .yaml"
94
+ with open (path , "w" ) as file :
95
+ yaml .dump (updated_asset , file )
86
96
87
- os .remove (zip_path )
88
-
89
- # Create the roles
90
- for dashboard_uuid , role_ids in roles .items ():
91
- dashboard = db .session .query (Dashboard ).filter_by (uuid = dashboard_uuid ).one ()
92
- dashboard .roles = role_ids
93
- dashboard .published = True
94
- db .session .commit ()
97
+ ## WARNING: Dashboard are assigned a Dummy role which prevents users to
98
+ # access the original dashboards.
99
+ dashboard_roles = asset .pop ("_roles" , None )
100
+ if dashboard_roles :
101
+ roles [asset ["uuid" ]] = [
102
+ security_manager .find_role ("Admin" )
103
+ ]
104
+
105
+ path = f"{ BASE_DIR } /{ folder } /{ file_name } .yaml"
106
+ with open (path , "w" ) as file :
107
+ yaml .dump (asset , file )
108
+
109
+
110
+ def generate_translated_asset (asset , asset_name , folder , language , title , roles ):
111
+ """Generate a translated asset with their elements updated"""
112
+ copy = deepcopy (asset )
113
+ copy ["uuid" ] = str (get_uuid5 (copy ["uuid" ], language ))
114
+ copy [asset_name ] = title
115
+
116
+ if folder == "dashboards" :
117
+ copy ["slug" ] = f"{ copy ['slug' ]} -{ language } "
118
+
119
+ dashboard_roles = copy .pop ("_roles" , [])
120
+ translated_dashboard_roles = []
121
+
122
+ for role in dashboard_roles :
123
+ translated_dashboard_roles .append (f"{ role } - { language } " )
124
+
125
+ roles [copy ["uuid" ]] = [
126
+ security_manager .find_role (role ) for role in translated_dashboard_roles
127
+ ]
128
+
129
+ generate_translated_dashboard_elements (copy , language )
130
+ generate_translated_dashboard_filters (copy , language )
131
+ return copy
132
+
133
+
134
+ def generate_translated_dashboard_elements (copy , language ):
135
+ """Generate translated elements for a dashboard"""
136
+ position = copy .get ("position" , {})
137
+
138
+ for element in position .values ():
139
+ if not isinstance (element , dict ):
140
+ continue
141
+
142
+ meta = element .get ("meta" , {})
143
+ original_uuid = meta .get ("uuid" , None )
144
+
145
+ element_type = element .get ("type" , "Unknown" )
146
+
147
+ translation , element_type , element_id = None , None , None
148
+
149
+ if original_uuid :
150
+ if not original_uuid in ASSETS_TRANSLATIONS :
151
+ print (f"Chart { meta ['uuid' ]} not found in translations" )
152
+ continue
153
+
154
+ element_type = "Chart"
155
+ element_id = str (get_uuid5 (original_uuid , language ))
156
+ translation = ASSETS_TRANSLATIONS .get (original_uuid , {}).get (
157
+ language , meta ["sliceName" ]
158
+ )
159
+
160
+ meta ["sliceName" ] = translation
161
+ meta ["uuid" ] = element_id
162
+
163
+ elif element .get ("type" ) == "TAB" :
164
+ chart_body_id = element .get ("id" )
165
+ if not meta or not meta .get ("text" ):
166
+ continue
167
+
168
+ if not chart_body_id in ASSETS_TRANSLATIONS :
169
+ print (f"Tab { chart_body_id } not found in translations" )
170
+ continue
171
+
172
+ element_type = "Tab"
173
+ element_id = chart_body_id
174
+ translation = ASSETS_TRANSLATIONS .get (chart_body_id , {}).get (
175
+ language , meta ["text" ]
176
+ )
177
+
178
+ meta ["text" ] = translation
179
+
180
+ if translation and element_type and element_id :
181
+ print (
182
+ f"Generating { element_type } { element_id } for language { language } { translation } "
183
+ )
184
+
185
+
186
+ def generate_translated_dashboard_filters (copy , language ):
187
+ """Generate translated filters for a dashboard"""
188
+ metadata = copy .get ("metadata" , {})
189
+
190
+ for filter in metadata .get ("native_filter_configuration" , []):
191
+ element_type = "Filter"
192
+ element_id = filter ["id" ]
193
+ translation = ASSETS_TRANSLATIONS .get (element_id , {}).get (
194
+ language , filter ["name" ]
195
+ )
196
+
197
+ filter ["name" ] = translation
198
+ print (
199
+ f"Generating { element_type } { element_id } for language { language } { translation } "
200
+ )
95
201
96
202
97
203
def create_superset_db (database_name , uri ) -> None :
@@ -101,5 +207,30 @@ def create_superset_db(database_name, uri) -> None:
101
207
db .session .commit ()
102
208
103
209
210
+ def create_zip_and_import_assets ():
211
+ """Create a zip file with all the assets and import them in superset"""
212
+ with ZipFile (ASSETS_ZIP_PATH , "w" ) as zip :
213
+ for folder in ASSET_FOLDER_MAPPING .values ():
214
+ for file_name in os .listdir (f"{ BASE_DIR } /{ folder } " ):
215
+ zip .write (
216
+ f"{ BASE_DIR } /{ folder } /{ file_name } " , f"import/{ folder } /{ file_name } "
217
+ )
218
+ zip .write (f"{ BASE_DIR } /metadata.yaml" , "import/metadata.yaml" )
219
+ contents = get_contents_from_bundle (zip )
220
+ command = ImportAssetsCommand (contents )
221
+ command .run ()
222
+
223
+ os .remove (ASSETS_ZIP_PATH )
224
+
225
+
226
+ def update_dashboard_roles (roles ):
227
+ """Update the roles of the dashboards"""
228
+ for dashboard_uuid , role_ids in roles .items ():
229
+ dashboard = db .session .query (Dashboard ).filter_by (uuid = dashboard_uuid ).one ()
230
+ dashboard .roles = role_ids
231
+ dashboard .published = True
232
+ db .session .commit ()
233
+
234
+
104
235
if __name__ == "__main__" :
105
236
main ()
0 commit comments