dbt-selly/dbt-env/lib/python3.8/site-packages/dbt/task/debug.py

410 lines
14 KiB
Python

# coding=utf-8
import os
import platform
import sys
from typing import Optional, Dict, Any, List
from dbt.logger import GLOBAL_LOGGER as logger
import dbt.clients.system
import dbt.exceptions
from dbt.adapters.factory import get_adapter, register_adapter
from dbt.config import Project, Profile, PROFILES_DIR
from dbt.config.renderer import DbtProjectYamlRenderer, ProfileRenderer
from dbt.config.utils import parse_cli_vars
from dbt.context.base import generate_base_context
from dbt.context.target import generate_target_context
from dbt.clients.yaml_helper import load_yaml_text
from dbt.links import ProfileConfigDocs
from dbt.ui import green, red
from dbt.utils import pluralize
from dbt.version import get_installed_version
from dbt.task.base import BaseTask, get_nearest_project_dir
PROFILE_DIR_MESSAGE = """To view your profiles.yml file, run:
{open_cmd} {profiles_dir}"""
ONLY_PROFILE_MESSAGE = '''
A `dbt_project.yml` file was not found in this directory.
Using the only profile `{}`.
'''.lstrip()
MULTIPLE_PROFILE_MESSAGE = '''
A `dbt_project.yml` file was not found in this directory.
dbt found the following profiles:
{}
To debug one of these profiles, run:
dbt debug --profile [profile-name]
'''.lstrip()
COULD_NOT_CONNECT_MESSAGE = '''
dbt was unable to connect to the specified database.
The database returned the following error:
>{err}
Check your database credentials and try again. For more information, visit:
{url}
'''.lstrip()
MISSING_PROFILE_MESSAGE = '''
dbt looked for a profiles.yml file in {path}, but did
not find one. For more information on configuring your profile, consult the
documentation:
{url}
'''.lstrip()
FILE_NOT_FOUND = 'file not found'
class QueryCommentedProfile(Profile):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.query_comment = None
class DebugTask(BaseTask):
def __init__(self, args, config):
super().__init__(args, config)
self.profiles_dir = getattr(self.args, 'profiles_dir', PROFILES_DIR)
self.profile_path = os.path.join(self.profiles_dir, 'profiles.yml')
try:
self.project_dir = get_nearest_project_dir(self.args)
except dbt.exceptions.Exception:
# we probably couldn't find a project directory. Set project dir
# to whatever was given, or default to the current directory.
if args.project_dir:
self.project_dir = args.project_dir
else:
self.project_dir = os.getcwd()
self.project_path = os.path.join(self.project_dir, 'dbt_project.yml')
self.cli_vars = parse_cli_vars(getattr(self.args, 'vars', '{}'))
# set by _load_*
self.profile: Optional[Profile] = None
self.profile_fail_details = ''
self.raw_profile_data: Optional[Dict[str, Any]] = None
self.profile_name: Optional[str] = None
self.project: Optional[Project] = None
self.project_fail_details = ''
self.any_failure = False
self.messages: List[str] = []
@property
def project_profile(self):
if self.project is None:
return None
return self.project.profile_name
def path_info(self):
open_cmd = dbt.clients.system.open_dir_cmd()
message = PROFILE_DIR_MESSAGE.format(
open_cmd=open_cmd,
profiles_dir=self.profiles_dir
)
logger.info(message)
def run(self):
if self.args.config_dir:
self.path_info()
return not self.any_failure
version = get_installed_version().to_version_string(skip_matcher=True)
print('dbt version: {}'.format(version))
print('python version: {}'.format(sys.version.split()[0]))
print('python path: {}'.format(sys.executable))
print('os info: {}'.format(platform.platform()))
print('Using profiles.yml file at {}'.format(self.profile_path))
print('Using dbt_project.yml file at {}'.format(self.project_path))
print('')
self.test_configuration()
self.test_dependencies()
self.test_connection()
if self.any_failure:
print(red(f"{(pluralize(len(self.messages), 'check'))} failed:"))
else:
print(green('All checks passed!'))
for message in self.messages:
print(message)
print('')
return not self.any_failure
def interpret_results(self, results):
return results
def _load_project(self):
if not os.path.exists(self.project_path):
self.project_fail_details = FILE_NOT_FOUND
return red('ERROR not found')
if self.profile is None:
ctx = generate_base_context(self.cli_vars)
else:
ctx = generate_target_context(self.profile, self.cli_vars)
renderer = DbtProjectYamlRenderer(ctx)
try:
self.project = Project.from_project_root(
self.project_dir,
renderer,
verify_version=getattr(self.args, 'version_check', False),
)
except dbt.exceptions.DbtConfigError as exc:
self.project_fail_details = str(exc)
return red('ERROR invalid')
return green('OK found and valid')
def _profile_found(self):
if not self.raw_profile_data:
return red('ERROR not found')
assert self.raw_profile_data is not None
if self.profile_name in self.raw_profile_data:
return green('OK found')
else:
return red('ERROR not found')
def _target_found(self):
requirements = (self.raw_profile_data and self.profile_name and
self.target_name)
if not requirements:
return red('ERROR not found')
# mypy appeasement, we checked just above
assert self.raw_profile_data is not None
assert self.profile_name is not None
assert self.target_name is not None
if self.profile_name not in self.raw_profile_data:
return red('ERROR not found')
profiles = self.raw_profile_data[self.profile_name]['outputs']
if self.target_name not in profiles:
return red('ERROR not found')
return green('OK found')
def _choose_profile_names(self) -> Optional[List[str]]:
project_profile: Optional[str] = None
if os.path.exists(self.project_path):
try:
partial = Project.partial_load(
os.path.dirname(self.project_path),
verify_version=getattr(self.args, 'version_check', False),
)
renderer = DbtProjectYamlRenderer(
generate_base_context(self.cli_vars)
)
project_profile = partial.render_profile_name(renderer)
except dbt.exceptions.DbtProjectError:
pass
args_profile: Optional[str] = getattr(self.args, 'profile', None)
try:
return [Profile.pick_profile_name(args_profile, project_profile)]
except dbt.exceptions.DbtConfigError:
pass
# try to guess
profiles = []
if self.raw_profile_data:
profiles = [k for k in self.raw_profile_data if k != 'config']
if project_profile is None:
self.messages.append('Could not load dbt_project.yml')
elif len(profiles) == 0:
self.messages.append('The profiles.yml has no profiles')
elif len(profiles) == 1:
self.messages.append(ONLY_PROFILE_MESSAGE.format(profiles[0]))
else:
self.messages.append(MULTIPLE_PROFILE_MESSAGE.format(
'\n'.join(' - {}'.format(o) for o in profiles)
))
return profiles
def _choose_target_name(self, profile_name: str):
has_raw_profile = (
self.raw_profile_data is not None and
profile_name in self.raw_profile_data
)
if not has_raw_profile:
return None
# mypy appeasement, we checked just above
assert self.raw_profile_data is not None
raw_profile = self.raw_profile_data[profile_name]
renderer = ProfileRenderer(generate_base_context(self.cli_vars))
target_name, _ = Profile.render_profile(
raw_profile=raw_profile,
profile_name=profile_name,
target_override=getattr(self.args, 'target', None),
renderer=renderer
)
return target_name
def _load_profile(self):
if not os.path.exists(self.profile_path):
self.profile_fail_details = FILE_NOT_FOUND
self.messages.append(MISSING_PROFILE_MESSAGE.format(
path=self.profile_path, url=ProfileConfigDocs
))
self.any_failure = True
return red('ERROR not found')
try:
raw_profile_data = load_yaml_text(
dbt.clients.system.load_file_contents(self.profile_path)
)
except Exception:
pass # we'll report this when we try to load the profile for real
else:
if isinstance(raw_profile_data, dict):
self.raw_profile_data = raw_profile_data
profile_errors = []
profile_names = self._choose_profile_names()
renderer = ProfileRenderer(generate_base_context(self.cli_vars))
for profile_name in profile_names:
try:
profile: Profile = QueryCommentedProfile.render_from_args(
self.args, renderer, profile_name
)
except dbt.exceptions.DbtConfigError as exc:
profile_errors.append(str(exc))
else:
if len(profile_names) == 1:
# if a profile was specified, set it on the task
self.target_name = self._choose_target_name(profile_name)
self.profile = profile
if profile_errors:
self.profile_fail_details = '\n\n'.join(profile_errors)
return red('ERROR invalid')
return green('OK found and valid')
def test_git(self):
try:
dbt.clients.system.run_cmd(os.getcwd(), ['git', '--help'])
except dbt.exceptions.ExecutableError as exc:
self.messages.append('Error from git --help: {!s}'.format(exc))
self.any_failure = True
return red('ERROR')
return green('OK found')
def test_dependencies(self):
print('Required dependencies:')
print(' - git [{}]'.format(self.test_git()))
print('')
def test_configuration(self):
profile_status = self._load_profile()
project_status = self._load_project()
print('Configuration:')
print(' profiles.yml file [{}]'.format(profile_status))
print(' dbt_project.yml file [{}]'.format(project_status))
# skip profile stuff if we can't find a profile name
if self.profile_name is not None:
print(' profile: {} [{}]'.format(self.profile_name,
self._profile_found()))
print(' target: {} [{}]'.format(self.target_name,
self._target_found()))
print('')
self._log_project_fail()
self._log_profile_fail()
def _log_project_fail(self):
if not self.project_fail_details:
return
self.any_failure = True
if self.project_fail_details == FILE_NOT_FOUND:
return
msg = (
f'Project loading failed for the following reason:'
f'\n{self.project_fail_details}'
f'\n'
)
self.messages.append(msg)
def _log_profile_fail(self):
if not self.profile_fail_details:
return
self.any_failure = True
if self.profile_fail_details == FILE_NOT_FOUND:
return
msg = (
f'Profile loading failed for the following reason:'
f'\n{self.profile_fail_details}'
f'\n'
)
self.messages.append(msg)
@staticmethod
def attempt_connection(profile):
"""Return a string containing the error message, or None if there was
no error.
"""
register_adapter(profile)
adapter = get_adapter(profile)
try:
with adapter.connection_named('debug'):
adapter.debug_query()
except Exception as exc:
return COULD_NOT_CONNECT_MESSAGE.format(
err=str(exc),
url=ProfileConfigDocs,
)
return None
def _connection_result(self):
result = self.attempt_connection(self.profile)
if result is not None:
self.messages.append(result)
self.any_failure = True
return red('ERROR')
return green('OK connection ok')
def test_connection(self):
if not self.profile:
return
print('Connection:')
for k, v in self.profile.credentials.connection_info():
print(' {}: {}'.format(k, v))
print(' Connection test: [{}]'.format(self._connection_result()))
print('')
@classmethod
def validate_connection(cls, target_dict):
"""Validate a connection dictionary. On error, raises a DbtConfigError.
"""
target_name = 'test'
# make a fake profile that we can parse
profile_data = {
'outputs': {
target_name: target_dict,
},
}
# this will raise a DbtConfigError on failure
profile = Profile.from_raw_profile_info(
raw_profile=profile_data,
profile_name='',
target_override=target_name,
renderer=ProfileRenderer(generate_base_context({})),
)
result = cls.attempt_connection(profile)
if result is not None:
raise dbt.exceptions.DbtProfileError(
result,
result_type='connection_failure'
)