예제 #1
0
    def _check_python_compatibility(self, python: str) -> None:
        python_version_result = subprocess.run([python, '--version'],
                                               stdout=subprocess.PIPE)
        if python_version_result.returncode != 0:
            raise DbtenvError(f"Failed to run `{python}`.")
        python_version_output = python_version_result.stdout.decode(
            'utf-8').strip()
        python_version_match = re.search(
            r'(?P<major_version>\d+)\.(?P<minor_version>\d+)\.\d+',
            python_version_output)
        if not python_version_match:
            raise DbtenvError(
                f"No Python version number found in \"{python_version_output}\"."
            )
        python_version = python_version_match[0]
        python_major_version = int(python_version_match['major_version'])
        python_minor_version = int(python_version_match['minor_version'])

        if self.version < Version('0.20') and (python_major_version,
                                               python_minor_version) >= (3, 9):
            raise DbtenvError(
                f"Python {python_version} is being used, but dbt versions before 0.20.0 aren't compatible with Python 3.9 or above."
            )
        elif self.version < Version('0.15') and (
                python_major_version, python_minor_version) >= (3, 8):
            raise DbtenvError(
                f"Python {python_version} is being used, but dbt versions before 0.15 aren't compatible with Python 3.8 or above."
            )

        logger.debug(f"Python {python_version} should be compatible with dbt.")
예제 #2
0
    def install(self, force: bool = False) -> None:
        if self.version >= Version('1.0.0'):
            raise DbtenvError(
                "dbtenv does not support installing dbt versions greater than or equal to 1.0.0 through Homebrew. Configure dbtenv to use pip instead."
            )

        if self.is_installed():
            if force:
                logger.info(
                    f"dbt {self.version.homebrew_version} is already installed with Homebrew but will be reinstalled."
                )
                self.uninstall(force=True)
            else:
                raise DbtenvError(
                    f"dbt {self.version.homebrew_version} is already installed with Homebrew."
                )
        else:
            ensure_homebrew_dbt_tap()

        logger.info(
            f"Installing dbt {self.version.homebrew_version} with Homebrew.")
        brew_args = ['install', get_dbt_version_formula(self.version)]
        logger.debug(f"Running `brew` with arguments {brew_args}.")
        subprocess.run(['brew', *brew_args])
        # We can't rely on the Homebrew process return code to check for success/failure because a non-zero return code
        # might just indicate some extraneous problem, like a failure symlinking dbt into the bin directory.
        if self.is_installed():
            logger.info(
                f"Successfully installed dbt {self.version.homebrew_version} with Homebrew."
            )
        else:
            raise DbtenvError(
                f"Failed to install dbt {self.version.homebrew_version} with Homebrew."
            )
예제 #3
0
def install_dbt(env: Environment, version: Version) -> None:
    if env.primary_installer == Installer.PIP:
        dbtenv.pip.PipDbt(env, version).install()
    elif env.primary_installer == Installer.HOMEBREW:
        dbtenv.homebrew.HomebrewDbt(env, version).install()
    else:
        raise DbtenvError(f"Unknown installer `{env.primary_installer}`.")
예제 #4
0
def get_installable_versions(env: Environment) -> List[Version]:
    if env.primary_installer == Installer.PIP:
        return dbtenv.pip.get_pypi_dbt_versions()
    elif env.primary_installer == Installer.HOMEBREW:
        return dbtenv.homebrew.get_homebrew_dbt_versions()
    else:
        raise DbtenvError(f"Unknown installer `{env.primary_installer}`.")
예제 #5
0
def get_dbt(env: Environment, version: Version) -> Dbt:
    error = DbtenvError(f"No dbt {version} executable found.")

    pip_dbt = None
    if env.use_pip:
        pip_dbt = dbtenv.pip.PipDbt(env, version)
        try:
            pip_dbt.get_executable(
            )  # Raises an appropriate error if it's not installed.
            if env.primary_installer == Installer.PIP:
                return pip_dbt
        except DbtenvError as pip_error:
            if env.installer == Installer.PIP:
                raise
            else:
                error = pip_error

    if env.use_homebrew:
        homebrew_dbt = dbtenv.homebrew.HomebrewDbt(env, version)
        try:
            homebrew_dbt.get_executable(
            )  # Raises an appropriate error if it's not installed.
            return homebrew_dbt
        except DbtenvError as homebrew_error:
            if env.installer == Installer.HOMEBREW:
                raise
            elif env.primary_installer == Installer.HOMEBREW:
                error = homebrew_error

    if pip_dbt and pip_dbt.is_installed():
        return pip_dbt

    raise error
예제 #6
0
    def get_executable(self) -> str:
        if self._executable is None:
            if not os.path.isdir(self.keg_directory):
                raise DbtenvError(
                    f"No dbt {self.version.homebrew_version} installation found in `{self.keg_directory}`."
                )

            dbt_path = os.path.join(self.keg_directory, 'bin/dbt')
            if os.path.isfile(dbt_path):
                logger.debug(f"Found dbt executable `{dbt_path}`.")
                self._executable = dbt_path
            else:
                raise DbtenvError(
                    f"No dbt executable found in `{self.keg_directory}`.")

        return self._executable
예제 #7
0
def ensure_dbt_is_installed(env: Environment, version: Version) -> None:
    if not dbtenv.which.try_get_dbt(env, version):
        if env.auto_install:
            install_dbt(env, version)
        else:
            raise DbtenvError(
                f"No dbt {version} installation found and auto-install is not enabled."
            )
예제 #8
0
    def get_executable(self) -> str:
        if self._executable is None:
            if not os.path.isdir(self.venv_directory):
                raise DbtenvError(
                    f"No dbt {self.version.pypi_version} installation found in `{self.venv_directory}`."
                )

            dbt_subpath = 'Scripts\\dbt.exe' if self.env.os == 'Windows' else 'bin/dbt'
            dbt_path = os.path.join(self.venv_directory, dbt_subpath)
            if os.path.isfile(dbt_path):
                logger.debug(f"Found dbt executable `{dbt_path}`.")
                self._executable = dbt_path
            else:
                raise DbtenvError(
                    f"No dbt executable found in `{self.venv_directory}`.")

        return self._executable
예제 #9
0
 def _find_pip(self) -> str:
     pip_subpath = 'Scripts\\pip.exe' if self.env.os == 'Windows' else 'bin/pip'
     pip_path = os.path.join(self.venv_directory, pip_subpath)
     if os.path.isfile(pip_path):
         logger.debug(f"Found pip executable `{pip_path}`.")
         return pip_path
     else:
         raise DbtenvError(
             f"No pip executable found in `{self.venv_directory}`.")
예제 #10
0
def get_homebrew_dbt_versions() -> List[Version]:
    ensure_homebrew_dbt_tap()
    brew_args = ['info', '--json', 'dbt']
    logger.debug(f"Running `brew` with arguments {brew_args}.")
    brew_result = subprocess.run(['brew', *brew_args], stdout=subprocess.PIPE)
    if brew_result.returncode != 0:
        raise DbtenvError("Failed to get dbt info from Homebrew.")
    formula_metadata = json.loads(brew_result.stdout)
    return [
        get_dbt_formula_version(formula)
        for formula in formula_metadata[0]['versioned_formulae']
    ]
예제 #11
0
def ensure_homebrew_dbt_tap() -> None:
    # While it would be simplest to just always run `brew tap dbt-labs/dbt`, we need to first check for
    # an existing dbt tap because the "dbt-labs" GitHub organization used to be named "fishtown-analytics"
    # so people could have a "fishtown-analytics/dbt" tap, and having multiple dbt taps causes errors.
    tap_list_result = subprocess.run(['brew', 'tap'],
                                     stdout=subprocess.PIPE,
                                     text=True)
    if not re.search(r'\b(fishtown-analytics|dbt-labs)/dbt\b',
                     tap_list_result.stdout):
        logger.info('Adding the dbt Homebrew tap.')
        tap_dbt_result = subprocess.run(['brew', 'tap', 'dbt-labs/dbt'])
        if tap_dbt_result.returncode != 0:
            raise DbtenvError("Failed to add the dbt Homebrew tap.")
예제 #12
0
    def uninstall(self, force: bool = False) -> None:
        if not self.is_installed():
            raise DbtenvError(
                f"No dbt {self.version.homebrew_version} installation found in `{self.keg_directory}`."
            )

        if force or dbtenv.string_is_true(
                input(
                    f"Uninstall dbt {self.version.homebrew_version} from Homebrew? "
                )):
            brew_args = ['uninstall']
            if force:
                brew_args.append('--force')
            brew_args.append(get_dbt_version_formula(self.version))
            logger.debug(f"Running `brew` with arguments {brew_args}.")
            brew_result = subprocess.run(['brew', *brew_args])
            if brew_result.returncode != 0:
                raise DbtenvError(
                    f"Failed to uninstall dbt {self.version.homebrew_version} from Homebrew."
                )
            self._executable = None
            logger.info(
                f"Successfully uninstalled dbt {self.version.homebrew_version} from Homebrew."
            )
예제 #13
0
    def uninstall(self, force: bool = False) -> None:
        if not self.is_installed():
            raise DbtenvError(
                f"No dbt {self.version.pypi_version} installation found in `{self.venv_directory}`."
            )

        if force or dbtenv.string_is_true(
                input(
                    f"Uninstall dbt {self.version.pypi_version} from `{self.venv_directory}`? "
                )):
            shutil.rmtree(self.venv_directory)
            self._executable = None
            logger.info(
                f"Successfully uninstalled dbt {self.version.pypi_version} from `{self.venv_directory}`."
            )
예제 #14
0
    def execute(self, args: Args) -> None:
        attempted_uninstalls = 0

        if self.env.use_pip:
            pip_dbt = dbtenv.pip.PipDbt(self.env, args.dbt_version)
            if pip_dbt.is_installed():
                pip_dbt.uninstall(force=args.force)
                attempted_uninstalls += 1

        if self.env.use_homebrew:
            homebrew_dbt = dbtenv.homebrew.HomebrewDbt(self.env, args.dbt_version)
            if homebrew_dbt.is_installed():
                homebrew_dbt.uninstall(force=args.force)
                attempted_uninstalls += 1

        if attempted_uninstalls == 0:
            raise DbtenvError(f"No dbt {args.dbt_version} installation found.")
예제 #15
0
    def execute(self, args: Args) -> None:
        if args.dbt_version:
            version = args.dbt_version
        else:
            version = dbtenv.version.get_version(self.env)
            logger.info(f"Using dbt {version} ({version.source_description}).")

        if self.env.primary_installer == Installer.PIP:
            pip_dbt = dbtenv.pip.PipDbt(self.env, version)
            pip_dbt.install(force=args.force,
                            package_location=args.package_location,
                            editable=args.editable)
        elif self.env.primary_installer == Installer.HOMEBREW:
            homebrew_dbt = dbtenv.homebrew.HomebrewDbt(self.env, version)
            homebrew_dbt.install(force=args.force)
        else:
            raise DbtenvError(
                f"Unknown installer `{self.env.primary_installer}`.")
예제 #16
0
    def install(self,
                force: bool = False,
                package_location: Optional[str] = None,
                editable: bool = False) -> None:
        if self.is_installed():
            if force:
                logger.info(
                    f"`{self.venv_directory}` already exists but will be overwritten."
                )
                self._executable = None
            else:
                raise DbtenvError(f"`{self.venv_directory}` already exists.")

        python = self.env.python
        self._check_python_compatibility(python)

        logger.info(
            f"Creating virtual environment in `{self.venv_directory}` using `{python}`."
        )
        venv_result = subprocess.run(
            [python, '-m'
             'venv', '--clear', self.venv_directory])
        if venv_result.returncode != 0:
            raise DbtenvError(
                f"Failed to create virtual environment in `{self.venv_directory}`."
            )

        pip = self._find_pip()
        # Upgrade pip to avoid problems with packages that might require newer pip features.
        subprocess.run([pip, 'install', '--upgrade', 'pip'])
        # Install wheel to avoid pip falling back to using legacy `setup.py` installs.
        subprocess.run(
            [pip, 'install', '--disable-pip-version-check', 'wheel'])
        pip_args = ['install', '--disable-pip-version-check']
        if package_location:
            package_source = f"`{package_location}`"
            if editable:
                pip_args.append('--editable')
            pip_args.append(package_location)
        else:
            package_source = "the Python Package Index"
            if self.env.simulate_release_date:
                package_metadata = get_pypi_package_metadata('dbt')
                release_date = date.fromisoformat(package_metadata['releases'][
                    self.version.pypi_version][0]['upload_time'][:10])
                logger.info(
                    f"Simulating release date {release_date} for dbt {self.version}."
                )

                class ReleaseDateFilterPyPIRequestHandler(
                        BaseDateFilterPyPIRequestHandler):
                    date = release_date

                pip_filter_server = http.server.HTTPServer(
                    ('', 0), ReleaseDateFilterPyPIRequestHandler)
                pip_filter_port = pip_filter_server.socket.getsockname()[1]
                threading.Thread(target=pip_filter_server.serve_forever,
                                 daemon=True).start()
                pip_args.extend([
                    '--index-url', f'http://localhost:{pip_filter_port}/simple'
                ])
            elif self.version < Version('0.19.1'):
                # Versions prior to 0.19.1 just specified agate>=1.6, but agate 1.6.2 introduced a dependency on PyICU
                # which causes installation problems, so exclude that like versions 0.19.1 and above do.
                pip_args.append('agate>=1.6,<1.6.2')

            # Use correct dbt package name depending on version
            if self.version < Version('1.0.0'):
                pip_args.append(f'dbt=={self.version.pypi_version}')
            else:
                # See comment on adapter - dbt-core version coordination
                # https://getdbt.slack.com/archives/C02HM9AAXL4/p1637345945047100?thread_ts=1637323222.046100&cid=C02HM9AAXL4
                pip_args.append(f'dbt-core=={self.version.pypi_version}')
                pip_args.append(
                    f'dbt-postgres~={self.version.major_minor_patch}')
                pip_args.append(
                    f'dbt-redshift~={self.version.major_minor_patch}')
                pip_args.append(
                    f'dbt-snowflake~={self.version.major_minor_patch}')
                pip_args.append(
                    f'dbt-bigquery~={self.version.major_minor_patch}')
        logger.info(
            f"Installing dbt {self.version.pypi_version} from {package_source} into `{self.venv_directory}`."
        )

        logger.debug(f"Running `{pip}` with arguments {pip_args}.")
        pip_result = subprocess.run([pip, *pip_args])
        if pip_result.returncode != 0:
            raise DbtenvError(
                f"Failed to install dbt {self.version.pypi_version} from {package_source} into `{self.venv_directory}`."
            )

        logger.info(
            f"Successfully installed dbt {self.version.pypi_version} from {package_source} into `{self.venv_directory}`."
        )
예제 #17
0
def main(args: List[str] = None) -> None:
    try:
        if args is None:
            args = sys.argv[1:]

        logger.debug(f"Arguments = {args}")

        env = Environment()

        versions_subcommand = dbtenv.versions.VersionsSubcommand(env)
        install_subcommand = dbtenv.install.InstallSubcommand(env)
        version_subcommand = dbtenv.version.VersionSubcommand(env)
        which_subcommand = dbtenv.which.WhichSubcommand(env)
        execute_subcommand = dbtenv.execute.ExecuteSubcommand(env)
        uninstall_subcommand = dbtenv.uninstall.UninstallSubcommand(env)

        args_parser = build_root_args_parser(env)
        subparsers = args_parser.add_subparsers(dest='subcommand',
                                                title="Sub-commands")
        common_subcommand_args_parser = build_common_args_parser(
            env, dest_prefix='subcommand_')
        common_install_args_parser = build_common_install_args_parser(env)
        versions_subcommand.add_args_parser(subparsers,
                                            [common_subcommand_args_parser])
        install_subcommand.add_args_parser(
            subparsers,
            [common_subcommand_args_parser, common_install_args_parser])
        version_subcommand.add_args_parser(
            subparsers,
            [common_subcommand_args_parser, common_install_args_parser])
        which_subcommand.add_args_parser(subparsers,
                                         [common_subcommand_args_parser])
        execute_subcommand.add_args_parser(
            subparsers,
            [common_subcommand_args_parser, common_install_args_parser])
        uninstall_subcommand.add_args_parser(subparsers,
                                             [common_subcommand_args_parser])

        parsed_args = Args()
        args_parser.parse_args(args, namespace=parsed_args)

        debug = parsed_args.debug or parsed_args.get('subcommand_debug')
        if debug:
            env.debug = debug

        logger.debug(f"Parsed arguments = {parsed_args}")

        quiet = parsed_args.quiet or parsed_args.get('subcommand_quiet')
        if quiet:
            env.quiet = quiet

        installer = parsed_args.get('installer') or parsed_args.get(
            'subcommand_installer')
        if installer:
            env.installer = installer

        python = parsed_args.get('python')
        if python:
            env.python = python

        simulate_release_date = parsed_args.get('simulate_release_date')
        if simulate_release_date:
            env.simulate_release_date = simulate_release_date

        subcommand = parsed_args.subcommand
        if not subcommand:
            args_parser.print_help()
            sys.exit(ExitCode.FAILURE)

        if subcommand == versions_subcommand.name:
            versions_subcommand.execute(parsed_args)
        elif subcommand == install_subcommand.name:
            install_subcommand.execute(parsed_args)
        elif subcommand == version_subcommand.name:
            version_subcommand.execute(parsed_args)
        elif subcommand == which_subcommand.name:
            which_subcommand.execute(parsed_args)
        elif subcommand == execute_subcommand.name or subcommand in execute_subcommand.aliases:
            execute_subcommand.execute(parsed_args)
        elif subcommand == uninstall_subcommand.name:
            uninstall_subcommand.execute(parsed_args)
        else:
            raise DbtenvError(f"Unknown sub-command `{subcommand}`.")
    except DbtenvError as error:
        logger.error(error)
        sys.exit(ExitCode.FAILURE)
    except DbtError as dbt_error:
        logger.debug(dbt_error)
        sys.exit(dbt_error.exit_code)
    except KeyboardInterrupt:
        logger.debug("Received keyboard interrupt.")
        sys.exit(ExitCode.INTERRUPTED)

    sys.exit(ExitCode.SUCCESS)