@@ -60,6 +60,7 @@ class Profile(HasCredentials):
60
60
credentials : Credentials
61
61
profile_env_vars : Dict [str , Any ]
62
62
log_cache_events : bool
63
+ secondary_profiles : Dict [str , "Profile" ]
63
64
64
65
def __init__ (
65
66
self ,
@@ -79,6 +80,7 @@ def __init__(
79
80
self .log_cache_events = (
80
81
get_flags ().LOG_CACHE_EVENTS
81
82
) # never available on init, set for adapter instantiation via AdapterRequiredConfig
83
+ self .secondary_profiles = {}
82
84
83
85
def to_profile_info (self , serialize_credentials : bool = False ) -> Dict [str , Any ]:
84
86
"""Unlike to_project_config, this dict is not a mirror of any existing
@@ -257,6 +259,7 @@ def render_profile(
257
259
profile_name : str ,
258
260
target_override : Optional [str ],
259
261
renderer : ProfileRenderer ,
262
+ is_secondary : bool = False ,
260
263
) -> Tuple [str , Dict [str , Any ]]:
261
264
"""This is a containment zone for the hateful way we're rendering
262
265
profiles.
@@ -273,6 +276,12 @@ def render_profile(
273
276
elif "target" in raw_profile :
274
277
# render the target if it was parsed from yaml
275
278
target_name = renderer .render_value (raw_profile ["target" ])
279
+ elif is_secondary and len (raw_profile .get ("outputs" , [])) == 1 :
280
+ # if we only have one target, we can infer the target name
281
+ # currently, this is only used for secondary profiles
282
+ target_name = next (iter (raw_profile ["outputs" ]))
283
+ # the event name is slightly misleading, but the message indicates that we inferred the target name for a profile
284
+ fire_event (MissingProfileTarget (profile_name = profile_name , target_name = target_name ))
276
285
else :
277
286
target_name = "default"
278
287
fire_event (MissingProfileTarget (profile_name = profile_name , target_name = target_name ))
@@ -293,6 +302,7 @@ def from_raw_profile_info(
293
302
renderer : ProfileRenderer ,
294
303
target_override : Optional [str ] = None ,
295
304
threads_override : Optional [int ] = None ,
305
+ is_secondary : bool = False ,
296
306
) -> "Profile" :
297
307
"""Create a profile from its raw profile information.
298
308
@@ -312,9 +322,14 @@ def from_raw_profile_info(
312
322
"""
313
323
# TODO: should it be, and the values coerced to bool?
314
324
target_name , profile_data = cls .render_profile (
315
- raw_profile , profile_name , target_override , renderer
325
+ raw_profile , profile_name , target_override , renderer , is_secondary = is_secondary
316
326
)
317
327
328
+ if is_secondary and "secondary_profiles" in profile_data :
329
+ raise DbtProfileError (
330
+ f"Secondary profile '{ profile_name } ' cannot have nested secondary profiles"
331
+ )
332
+
318
333
# valid connections never include the number of threads, but it's
319
334
# stored on a per-connection level in the raw configs
320
335
threads = profile_data .pop ("threads" , DEFAULT_THREADS )
@@ -325,13 +340,31 @@ def from_raw_profile_info(
325
340
profile_data , profile_name , target_name
326
341
)
327
342
328
- return cls .from_credentials (
343
+ profile = cls .from_credentials (
329
344
credentials = credentials ,
330
345
profile_name = profile_name ,
331
346
target_name = target_name ,
332
347
threads = threads ,
333
348
)
334
349
350
+ for p in profile_data .pop ("secondary_profiles" , []):
351
+ for secondary_profile_name , secondary_raw_profile in p .items ():
352
+ if secondary_profile_name in profile .secondary_profiles :
353
+ raise DbtProfileError (
354
+ f"Secondary profile '{ secondary_profile_name } ' is already defined"
355
+ )
356
+
357
+ profile .secondary_profiles [secondary_profile_name ] = cls .from_raw_profile_info (
358
+ secondary_raw_profile ,
359
+ secondary_profile_name ,
360
+ renderer ,
361
+ target_override = target_override ,
362
+ threads_override = threads_override ,
363
+ is_secondary = True ,
364
+ )
365
+
366
+ return profile
367
+
335
368
@classmethod
336
369
def from_raw_profiles (
337
370
cls ,
0 commit comments