@@ -27,7 +27,7 @@ def __init__(self, host: str, user: str, password: str, https = True):
27
27
self .protocol = "https" if https else "http"
28
28
self .session_id = self .get_session_id (user , password )
29
29
logging .info ("Session established successfully" )
30
-
30
+
31
31
def get_session_id (self , user : str , password : str ) -> str :
32
32
"""Obtains new session ID from API.
33
33
@@ -43,7 +43,7 @@ def get_session_id(self, user: str, password: str) -> str:
43
43
'username' : user ,
44
44
'password' : password
45
45
})['id' ]
46
-
46
+
47
47
def sync_and_wait (self , database : str , schema : str , models : list , timeout = 30 ) -> bool :
48
48
"""Synchronize with the database and wait for schema compatibility.
49
49
@@ -67,7 +67,7 @@ def sync_and_wait(self, database: str, schema: str, models: list, timeout = 30)
67
67
if not database_id :
68
68
logging .critical ("Cannot find database by name %s" , database )
69
69
return
70
-
70
+
71
71
self .api ('post' , f'/api/database/{ database_id } /sync_schema' )
72
72
73
73
deadline = int (time .time ()) + timeout
@@ -108,35 +108,37 @@ def models_compatible(self, database_id: str, schema: str, models: list) -> bool
108
108
if column_name not in table_lookup :
109
109
logging .warn ("Column %s not found in model %s" , column_name , model_name )
110
110
are_models_compatible = False
111
-
111
+
112
112
return are_models_compatible
113
113
114
- def export_models (self , database : str , schema : str , models : list ):
114
+ def export_models (self , database : str , schema : str , models : list , ignore_undefined : bool ):
115
115
"""Exports dbt models to Metabase database schema.
116
116
117
117
Arguments:
118
118
database {str} -- Metabase database name.
119
119
schema {str} -- Metabase schema name.
120
120
models {list} -- List of dbt models read from project.
121
+ ignore_undefined {bool} -- Ignore undefined properties.
121
122
"""
122
123
123
124
database_id = self .find_database_id (database )
124
125
if not database_id :
125
126
logging .critical ("Cannot find database by name %s" , database )
126
127
return
127
-
128
+
128
129
table_lookup , field_lookup = self .build_metadata_lookups (database_id , schema )
129
130
130
131
for model in models :
131
- self .export_model (model , table_lookup , field_lookup )
132
-
133
- def export_model (self , model : dict , table_lookup : dict , field_lookup : dict ):
132
+ self .export_model (model , table_lookup , field_lookup , ignore_undefined )
133
+
134
+ def export_model (self , model : dict , table_lookup : dict , field_lookup : dict , ignore_undefined : bool ):
134
135
"""Exports one dbt model to Metabase database schema.
135
136
136
137
Arguments:
137
138
model {dict} -- One dbt model read from project.
138
139
table_lookup {dict} -- Dictionary of Metabase tables indexed by name.
139
140
field_lookup {dict} -- Dictionary of Metabase fields indexed by name, indexed by table name.
141
+ ignore_undefined {bool} -- Ignore undefined properties.
140
142
"""
141
143
142
144
model_name = model ['name' ].upper ()
@@ -157,15 +159,16 @@ def export_model(self, model: dict, table_lookup: dict, field_lookup: dict):
157
159
logging .info ("Table %s is up-to-date" , model_name )
158
160
159
161
for column in model .get ('columns' , []):
160
- self .export_column (model_name , column , field_lookup )
161
-
162
- def export_column (self , model_name : str , column : dict , field_lookup : dict ):
162
+ self .export_column (model_name , column , field_lookup , ignore_undefined )
163
+
164
+ def export_column (self , model_name : str , column : dict , field_lookup : dict , ignore_undefined : bool ):
163
165
"""Exports one dbt column to Metabase database schema.
164
166
165
167
Arguments:
166
168
model_name {str} -- One dbt model name read from project.
167
169
column {dict} -- One dbt column read from project.
168
170
field_lookup {dict} -- Dictionary of Metabase fields indexed by name, indexed by table name.
171
+ ignore_undefined {bool} -- Ignore undefined properties.
169
172
"""
170
173
171
174
column_name = column ['name' ].upper ()
@@ -174,7 +177,7 @@ def export_column(self, model_name: str, column: dict, field_lookup: dict):
174
177
if not field :
175
178
logging .error ('Field %s.%s does not exist in Metabase' , model_name , column_name )
176
179
return
177
-
180
+
178
181
field_id = field ['id' ]
179
182
fk_target_field_id = None
180
183
if column .get ('special_type' ) == 'type/FK' :
@@ -183,35 +186,39 @@ def export_column(self, model_name: str, column: dict, field_lookup: dict):
183
186
fk_target_field_id = field_lookup .get (target_table , {}) \
184
187
.get (target_field , {}) \
185
188
.get ('id' )
186
-
189
+
187
190
if fk_target_field_id :
188
191
self .api ('put' , f'/api/field/{ fk_target_field_id } ' , json = {
189
192
'special_type' : 'type/PK'
190
193
})
191
194
else :
192
195
logging .error ("Unable to find foreign key target %s.%s" , target_table , target_field )
193
-
194
- # Nones are not accepted, default to normal
195
- if not column .get ('visibility_type' ):
196
- column ['visibility_type' ] = 'normal'
197
196
198
197
api_field = self .api ('get' , f'/api/field/{ field_id } ' )
199
198
200
- if api_field ['description' ] != column .get ('description' ) or \
201
- api_field ['special_type' ] != column .get ('special_type' ) or \
202
- api_field ['visibility_type' ] != column .get ('visibility_type' ) or \
203
- api_field ['fk_target_field_id' ] != fk_target_field_id :
199
+ payload = {}
200
+ payload_fields = ['description' , 'special_type' , 'visibility_type' ]
201
+ for name in payload_fields :
202
+ mb_value = api_field [name ]
203
+ dbt_value = column .get (name )
204
+ # Add null properties to payload only if they should not be ignored
205
+ if mb_value != dbt_value and (dbt_value or not ignore_undefined ):
206
+ # Nones are not accepted, default to normal
207
+ if name == 'visibility_type' :
208
+ payload [name ] = 'normal'
209
+ else :
210
+ payload [name ] = dbt_value
211
+
212
+ if api_field ['fk_target_field_id' ] != fk_target_field_id and (fk_target_field_id or not ignore_undefined ):
213
+ payload ['fk_target_field_id' ] = fk_target_field_id
214
+
215
+ if payload :
204
216
# Update with new values
205
- self .api ('put' , f'/api/field/{ field_id } ' , json = {
206
- 'description' : column .get ('description' ),
207
- 'special_type' : column .get ('special_type' ),
208
- 'visibility_type' : column .get ('visibility_type' ),
209
- 'fk_target_field_id' : fk_target_field_id
210
- })
217
+ self .api ('put' , f'/api/field/{ field_id } ' , json = payload )
211
218
logging .info ("Updated field %s.%s successfully" , model_name , column_name )
212
219
else :
213
220
logging .info ("Field %s.%s is up-to-date" , model_name , column_name )
214
-
221
+
215
222
def find_database_id (self , name : str ) -> str :
216
223
"""Finds Metabase database ID by name.
217
224
@@ -226,7 +233,7 @@ def find_database_id(self, name: str) -> str:
226
233
if database ['name' ].upper () == name .upper ():
227
234
return database ['id' ]
228
235
return None
229
-
236
+
230
237
def build_metadata_lookups (self , database_id : str , schema : str ) -> (dict , dict ):
231
238
"""Builds table and field lookups.
232
239
@@ -262,7 +269,7 @@ def build_metadata_lookups(self, database_id: str, schema: str) -> (dict, dict):
262
269
table_field_lookup [field_name ] = field
263
270
264
271
field_lookup [table_name ] = table_field_lookup
265
-
272
+
266
273
return table_lookup , field_lookup
267
274
268
275
def api (self , method : str , path : str , authenticated = True , critical = True , ** kwargs ) -> Any :
@@ -285,7 +292,7 @@ def api(self, method: str, path: str, authenticated = True, critical = True, **k
285
292
kwargs ['headers' ] = headers
286
293
else :
287
294
headers = kwargs ['headers' ].copy ()
288
-
295
+
289
296
if authenticated :
290
297
headers ['X-Metabase-Session' ] = self .session_id
291
298
0 commit comments