99import yaml
1010from gooddata_sdk import (
1111 BasicCredentials ,
12+ CatalogDataSourceMotherDuck ,
1213 CatalogDataSourcePostgres ,
1314 CatalogDataSourceSnowflake ,
1415 CatalogDataSourceVertica ,
16+ MotherDuckAttributes ,
1517 PostgresAttributes ,
1618 SnowflakeAttributes ,
19+ TokenCredentialsFromEnvVar ,
1720 VerticaAttributes ,
1821)
1922
@@ -112,13 +115,50 @@ def to_gooddata(self, data_source_id: str, schema_name: str) -> CatalogDataSourc
112115 )
113116
114117
115- DbtOutput = Union [DbtOutputPostgreSQL , DbtOutputSnowflake , DbtOutputVertica ]
118+ @attrs .define (auto_attribs = True , kw_only = True )
119+ class DbtOutputMotherDuck (Base ):
120+ name : str
121+ title : str
122+ path : str
123+ schema : str
124+ database : str = ""
125+
126+ def __attrs_post_init__ (self ) -> None :
127+ self .database = self .validate_connection_props ()
128+
129+ def validate_connection_props (self ) -> str :
130+ if not self .path .startswith ("md:" ):
131+ raise ValueError (f"Path { self .path } is not a valid MotherDuck path." )
132+ _ , db_name = self .path .split (":" , 1 )
133+ if db_name :
134+ return db_name
135+ elif self .database :
136+ return self .database
137+ else :
138+ raise ValueError (f"Database name is neither specified in { self .path = } nor in { self .database = } " )
139+
140+ def to_gooddata (self , data_source_id : str , schema_name : str ) -> CatalogDataSourceMotherDuck :
141+ return CatalogDataSourceMotherDuck (
142+ id = data_source_id ,
143+ name = self .title ,
144+ db_specific_attributes = MotherDuckAttributes (
145+ db_name = quote_plus (self .database ),
146+ ),
147+ # Schema name is collected from dbt manifest from relevant tables
148+ schema = schema_name ,
149+ credentials = TokenCredentialsFromEnvVar (
150+ env_var_name = "MOTHERDUCK_TOKEN" ,
151+ ),
152+ )
153+
154+
155+ DbtOutput = Union [DbtOutputPostgreSQL , DbtOutputSnowflake , DbtOutputVertica , DbtOutputMotherDuck ]
116156
117157
118158@attrs .define (auto_attribs = True , kw_only = True )
119159class DbtProfile (Base ):
120160 name : str
121- outputs : List [Union [ DbtOutputPostgreSQL , DbtOutputSnowflake , DbtOutputVertica ] ]
161+ outputs : List [DbtOutput ]
122162
123163
124164class DbtProfiles :
@@ -135,14 +175,17 @@ def __init__(self, args: argparse.Namespace) -> None:
135175
136176 @staticmethod
137177 def inject_env_vars (output_def : Dict ) -> None :
138- env_re = re .compile (r"env_var\('([^']+)'(,\s*'([^']+)')?\)" )
178+ env_re = re .compile (r"\{\{ env_var\('([^']+)'(,\s*'([^']+)')?\) \}\} " )
139179 for output_key , output_value in output_def .items ():
140180 if (env_match := env_re .search (str (output_value ))) is not None :
141181 default_value = None
142182 if len (env_match .groups ()) == 3 :
143183 default_value = env_match .group (3 )
144184 final_value = os .getenv (env_match .group (1 )) or default_value
145- output_def [output_key ] = final_value
185+ if final_value is None :
186+ output_def [output_key ] = ""
187+ else :
188+ output_def [output_key ] = env_re .sub (final_value , str (output_value ))
146189 # else do nothing, real value seems to be stored in dbt profile
147190
148191 @staticmethod
@@ -154,6 +197,8 @@ def to_data_class(output: str, output_def: Dict) -> DbtOutput:
154197 return DbtOutputSnowflake .from_dict ({"name" : output , ** output_def })
155198 elif db_type == "vertica" :
156199 return DbtOutputVertica .from_dict ({"name" : output , ** output_def })
200+ elif db_type == "duckdb" :
201+ return DbtOutputMotherDuck .from_dict ({"name" : output , ** output_def })
157202 else :
158203 raise Exception (f"Unsupported database type { output = } { db_type = } " )
159204
0 commit comments