11
11
from dbt .contracts .project import Project as ProjectContract , Configuration , \
12
12
PackageConfig , ProfileConfig
13
13
from dbt .exceptions import DbtProjectError , DbtProfileError
14
- from dbt .context .common import env_var
14
+ from dbt .context .common import env_var , Var
15
15
from dbt import compat
16
16
from dbt .adapters .factory import get_relation_class_by_name
17
17
@@ -90,27 +90,77 @@ def colorize_output(config):
90
90
return config .get ('use_colors' , True )
91
91
92
92
93
- def _render (key , value , ctx ):
94
- """Render an entry in the credentials dictionary, in case it's jinja.
93
+ class ConfigRenderer (object ):
94
+ """A renderer provides configuration rendering for a given set of cli
95
+ variables and a render type.
96
+ """
97
+ def __init__ (self , cli_vars ):
98
+ self .context = {'env_var' : env_var }
99
+ self .context ['var' ] = Var (None , self .context , cli_vars )
95
100
96
- If the parsed entry is a string and has the name 'port', this will attempt
97
- to cast it to an int, and on failure will return the parsed string.
101
+ @staticmethod
102
+ def _is_hook_path (keypath ):
103
+ if not keypath :
104
+ return False
98
105
99
- :param key str: The key to convert on.
100
- :param value Any: The value to potentially render
101
- :param ctx dict: The context dictionary, mapping function names to
102
- functions that take a str and return a value
103
- :return Any: The rendered entry.
104
- """
105
- if not isinstance (value , compat .basestring ):
106
- return value
107
- result = dbt .clients .jinja .get_rendered (value , ctx )
108
- if key == 'port' :
109
- try :
110
- return int (result )
111
- except ValueError :
112
- pass # let the validator or connection handle this
113
- return result
106
+ first = keypath [0 ]
107
+ # run hooks
108
+ if first in {'on-run-start' , 'on-run-end' }:
109
+ return True
110
+ # model hooks
111
+ if first in {'seeds' , 'models' }:
112
+ if 'pre-hook' in keypath or 'post-hook' in keypath :
113
+ return True
114
+
115
+ return False
116
+
117
+ def _render_project_entry (self , value , keypath ):
118
+ """Render an entry, in case it's jinja. This is meant to be passed to
119
+ dbt.utils.deep_map.
120
+
121
+ If the parsed entry is a string and has the name 'port', this will
122
+ attempt to cast it to an int, and on failure will return the parsed
123
+ string.
124
+
125
+ :param value Any: The value to potentially render
126
+ :param key str: The key to convert on.
127
+ :return Any: The rendered entry.
128
+ """
129
+ # hooks should be treated as raw sql, they'll get rendered later
130
+ if self ._is_hook_path (keypath ):
131
+ return value
132
+
133
+ return self .render_value (value )
134
+
135
+ def render_value (self , value , keypath = None ):
136
+ # keypath is ignored.
137
+ # if it wasn't read as a string, ignore it
138
+ if not isinstance (value , compat .basestring ):
139
+ return value
140
+
141
+ return dbt .clients .jinja .get_rendered (value , self .context )
142
+
143
+ def _render_profile_data (self , value , keypath ):
144
+ result = self .render_value (value )
145
+ if len (keypath ) == 1 and keypath [- 1 ] == 'port' :
146
+ try :
147
+ result = int (result )
148
+ except ValueError :
149
+ # let the validator or connection handle this
150
+ pass
151
+ return result
152
+
153
+ def render (self , as_parsed ):
154
+ return dbt .utils .deep_map (self .render_value , as_parsed )
155
+
156
+ def render_project (self , as_parsed ):
157
+ """Render the parsed data, returning a new dict (or whatever was read).
158
+ """
159
+ return dbt .utils .deep_map (self ._render_project_entry , as_parsed )
160
+
161
+ def render_profile_data (self , as_parsed ):
162
+ """Render the chosen profile entry, as it was parsed."""
163
+ return dbt .utils .deep_map (self ._render_profile_data , as_parsed )
114
164
115
165
116
166
class Project (object ):
@@ -269,7 +319,7 @@ def validate(self):
269
319
raise DbtProjectError (str (exc ))
270
320
271
321
@classmethod
272
- def from_project_root (cls , project_root ):
322
+ def from_project_root (cls , project_root , cli_vars ):
273
323
"""Create a project from a root directory. Reads in dbt_project.yml and
274
324
packages.yml, if it exists.
275
325
@@ -288,14 +338,19 @@ def from_project_root(cls, project_root):
288
338
.format (project_yaml_filepath )
289
339
)
290
340
341
+ if isinstance (cli_vars , compat .basestring ):
342
+ cli_vars = dbt .utils .parse_cli_vars (cli_vars )
343
+ renderer = ConfigRenderer (cli_vars )
344
+
291
345
project_dict = _load_yaml (project_yaml_filepath )
292
- project_dict ['project-root' ] = project_root
346
+ rendered_project = renderer .render_project (project_dict )
347
+ rendered_project ['project-root' ] = project_root
293
348
packages_dict = package_data_from_root (project_root )
294
- return cls .from_project_config (project_dict , packages_dict )
349
+ return cls .from_project_config (rendered_project , packages_dict )
295
350
296
351
@classmethod
297
- def from_current_directory (cls ):
298
- return cls .from_project_root (os .getcwd ())
352
+ def from_current_directory (cls , cli_vars ):
353
+ return cls .from_project_root (os .getcwd (), cli_vars )
299
354
300
355
def hashed_name (self ):
301
356
return hashlib .md5 (self .project_name .encode ('utf-8' )).hexdigest ()
@@ -348,16 +403,6 @@ def validate(self):
348
403
except dbt .exceptions .ValidationException as exc :
349
404
raise DbtProfileError (str (exc ))
350
405
351
- @staticmethod
352
- def _rendered_profile (profile ):
353
- # if entries are strings, we want to render them so we can get any
354
- # environment variables that might store important credentials
355
- # elements.
356
- return {
357
- k : _render (k , v , {'env_var' : env_var })
358
- for k , v in profile .items ()
359
- }
360
-
361
406
@staticmethod
362
407
def _credentials_from_profile (profile , profile_name , target_name ):
363
408
# credentials carry their 'type' in their actual type, not their
@@ -387,25 +432,12 @@ def _pick_profile_name(args_profile_name, project_profile_name=None):
387
432
return profile_name
388
433
389
434
@staticmethod
390
- def _pick_target (raw_profile , profile_name , target_override = None ):
391
-
392
- if target_override is not None :
393
- target_name = target_override
394
- elif 'target' in raw_profile :
395
- target_name = raw_profile ['target' ]
396
- else :
397
- raise DbtProfileError (
398
- "target not specified in profile '{}'" .format (profile_name )
399
- )
400
- return target_name
401
-
402
- @staticmethod
403
- def _get_profile_data (raw_profile , profile_name , target_name ):
404
- if 'outputs' not in raw_profile :
435
+ def _get_profile_data (profile , profile_name , target_name ):
436
+ if 'outputs' not in profile :
405
437
raise DbtProfileError (
406
438
"outputs not specified in profile '{}'" .format (profile_name )
407
439
)
408
- outputs = raw_profile ['outputs' ]
440
+ outputs = profile ['outputs' ]
409
441
410
442
if target_name not in outputs :
411
443
outputs = '\n ' .join (' - {}' .format (output )
@@ -454,15 +486,50 @@ def from_credentials(cls, credentials, threads, profile_name, target_name,
454
486
return profile
455
487
456
488
@classmethod
457
- def from_raw_profile_info (cls , raw_profile , profile_name , user_cfg = None ,
458
- target_override = None , threads_override = None ):
489
+ def _render_profile (cls , raw_profile , profile_name , target_override ,
490
+ cli_vars ):
491
+ """This is a containment zone for the hateful way we're rendering
492
+ profiles.
493
+ """
494
+ renderer = ConfigRenderer (cli_vars = cli_vars )
495
+
496
+ # rendering profiles is a bit complex. Two constraints cause trouble:
497
+ # 1) users should be able to use environment/cli variables to specify
498
+ # the target in their profile.
499
+ # 2) Missing environment/cli variables in profiles/targets that don't
500
+ # end up getting selected should not cause errors.
501
+ # so first we'll just render the target name, then we use that rendered
502
+ # name to extract a profile that we can render.
503
+ if target_override is not None :
504
+ target_name = target_override
505
+ elif 'target' in raw_profile :
506
+ # render the target if it was parsed from yaml
507
+ target_name = renderer .render_value (raw_profile ['target' ])
508
+ else :
509
+ raise DbtProfileError (
510
+ "target not specified in profile '{}'" .format (profile_name )
511
+ )
512
+
513
+ raw_profile_data = cls ._get_profile_data (
514
+ raw_profile , profile_name , target_name
515
+ )
516
+
517
+ profile_data = renderer .render_profile_data (raw_profile_data )
518
+ return target_name , profile_data
519
+
520
+ @classmethod
521
+ def from_raw_profile_info (cls , raw_profile , profile_name , cli_vars ,
522
+ user_cfg = None , target_override = None ,
523
+ threads_override = None ):
459
524
"""Create a profile from its raw profile information.
460
525
461
526
(this is an intermediate step, mostly useful for unit testing)
462
527
463
- :param raw_profiles dict: The profile data for a single profile, from
464
- disk as yaml.
528
+ :param raw_profile dict: The profile data for a single profile, from
529
+ disk as yaml and its values rendered with jinja .
465
530
:param profile_name str: The profile name used.
531
+ :param cli_vars dict: The command-line variables passed as arguments,
532
+ as a dict.
466
533
:param user_cfg Optional[dict]: The global config for the user, if it
467
534
was present.
468
535
:param target_override Optional[str]: The target to use, if provided on
@@ -473,22 +540,20 @@ def from_raw_profile_info(cls, raw_profile, profile_name, user_cfg=None,
473
540
target could not be found
474
541
:returns Profile: The new Profile object.
475
542
"""
476
- target_name = cls ._pick_target (
477
- raw_profile , profile_name , target_override
543
+ # user_cfg is not rendered since it only contains booleans.
544
+ # TODO: should it be, and the values coerced to bool?
545
+ target_name , profile_data = cls ._render_profile (
546
+ raw_profile , profile_name , target_override , cli_vars
478
547
)
479
- profile_data = cls ._get_profile_data (
480
- raw_profile , profile_name , target_name
481
- )
482
- rendered_profile = cls ._rendered_profile (profile_data )
483
548
484
549
# valid connections never include the number of threads, but it's
485
550
# stored on a per-connection level in the raw configs
486
- threads = rendered_profile .pop ('threads' , DEFAULT_THREADS )
551
+ threads = profile_data .pop ('threads' , DEFAULT_THREADS )
487
552
if threads_override is not None :
488
553
threads = threads_override
489
554
490
555
credentials = cls ._credentials_from_profile (
491
- rendered_profile , profile_name , target_name
556
+ profile_data , profile_name , target_name
492
557
)
493
558
return cls .from_credentials (
494
559
credentials = credentials ,
@@ -499,11 +564,13 @@ def from_raw_profile_info(cls, raw_profile, profile_name, user_cfg=None,
499
564
)
500
565
501
566
@classmethod
502
- def from_raw_profiles (cls , raw_profiles , profile_name ,
567
+ def from_raw_profiles (cls , raw_profiles , profile_name , cli_vars ,
503
568
target_override = None , threads_override = None ):
504
569
"""
505
570
:param raw_profiles dict: The profile data, from disk as yaml.
506
571
:param profile_name str: The profile name to use.
572
+ :param cli_vars dict: The command-line variables passed as arguments,
573
+ as a dict.
507
574
:param target_override Optional[str]: The target to use, if provided on
508
575
the command line.
509
576
:param threads_override Optional[str]: The thread count to use, if
@@ -514,29 +581,35 @@ def from_raw_profiles(cls, raw_profiles, profile_name,
514
581
target could not be found
515
582
:returns Profile: The new Profile object.
516
583
"""
517
- # TODO(jeb): Validate the raw_profiles structure right here
518
584
if profile_name not in raw_profiles :
519
585
raise DbtProjectError (
520
586
"Could not find profile named '{}'" .format (profile_name )
521
587
)
588
+
589
+ # First, we've already got our final decision on profile name, and we
590
+ # don't render keys, so we can pluck that out
522
591
raw_profile = raw_profiles [profile_name ]
592
+
523
593
user_cfg = raw_profiles .get ('config' )
524
594
525
595
return cls .from_raw_profile_info (
526
596
raw_profile = raw_profile ,
527
597
profile_name = profile_name ,
598
+ cli_vars = cli_vars ,
528
599
user_cfg = user_cfg ,
529
600
target_override = target_override ,
530
601
threads_override = threads_override ,
531
602
)
532
603
533
604
@classmethod
534
- def from_args (cls , args , project_profile_name = None ):
605
+ def from_args (cls , args , project_profile_name = None , cli_vars = None ):
535
606
"""Given the raw profiles as read from disk and the name of the desired
536
607
profile if specified, return the profile component of the runtime
537
608
config.
538
609
539
610
:param args argparse.Namespace: The arguments as parsed from the cli.
611
+ :param cli_vars dict: The command-line variables passed as arguments,
612
+ as a dict.
540
613
:param project_profile_name Optional[str]: The profile name, if
541
614
specified in a project.
542
615
:raises DbtProjectError: If there is no profile name specified in the
@@ -546,6 +619,9 @@ def from_args(cls, args, project_profile_name=None):
546
619
target could not be found.
547
620
:returns Profile: The new Profile object.
548
621
"""
622
+ if cli_vars is None :
623
+ cli_vars = dbt .utils .parse_cli_vars (getattr (args , 'vars' , '{}' ))
624
+
549
625
threads_override = getattr (args , 'threads' , None )
550
626
# TODO(jeb): is it even possible for this to not be set?
551
627
profiles_dir = getattr (args , 'profiles_dir' , DEFAULT_PROFILES_DIR )
@@ -557,6 +633,7 @@ def from_args(cls, args, project_profile_name=None):
557
633
return cls .from_raw_profiles (
558
634
raw_profiles = raw_profiles ,
559
635
profile_name = profile_name ,
636
+ cli_vars = cli_vars ,
560
637
target_override = target_override ,
561
638
threads_override = threads_override
562
639
)
@@ -697,8 +774,8 @@ def new_project(self, project_root):
697
774
# copy profile
698
775
profile = Profile (** self .to_profile_info ())
699
776
profile .validate ()
700
- # load the new project and its packages
701
- project = Project .from_project_root (project_root )
777
+ # load the new project and its packages. Don't pass cli variables.
778
+ project = Project .from_project_root (project_root , {} )
702
779
703
780
cfg = self .from_parts (
704
781
project = project ,
@@ -745,13 +822,18 @@ def from_args(cls, args):
745
822
:raises DbtProfileError: If the profile is invalid or missing.
746
823
:raises ValidationException: If the cli variables are invalid.
747
824
"""
825
+ cli_vars = dbt .utils .parse_cli_vars (getattr (args , 'vars' , '{}' ))
826
+
748
827
# build the project and read in packages.yml
749
- project = Project .from_current_directory ()
828
+ project = Project .from_current_directory (cli_vars )
750
829
751
830
# build the profile
752
- profile = Profile .from_args (args , project .profile_name )
831
+ profile = Profile .from_args (
832
+ args = args ,
833
+ project_profile_name = project .profile_name ,
834
+ cli_vars = cli_vars
835
+ )
753
836
754
- cli_vars = dbt .utils .parse_cli_vars (getattr (args , 'vars' , '{}' ))
755
837
return cls .from_parts (
756
838
project = project ,
757
839
profile = profile ,
0 commit comments