Repository: dbt-labs/dbt-core

Issue:
tox updates



Discussion:

Resolving patch:
diff --git a/core/setup.py b/core/setup.py
--- a/core/setup.py
+++ b/core/setup.py
@@ -64,7 +64,7 @@ def read(fname):
         'sqlparse>=0.2.3,<0.4',
         'networkx>=2.3,<3',
         'minimal-snowplow-tracker==0.0.2',
-        'colorama>=0.3.9,<0.4.4',
+        'colorama>=0.3.9,<0.4.5',
         'agate>=1.6,<1.6.2',
         'isodate>=0.6,<0.7',
         'json-rpc>=1.12,<2',
diff --git a/scripts/build-dbt.py b/scripts/build-dbt.py
deleted file mode 100644
--- a/scripts/build-dbt.py
+++ /dev/null
@@ -1,934 +0,0 @@
-import json
-import os
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
-import textwrap
-import time
-import venv  # type: ignore
-import zipfile
-
-from typing import Dict
-
-from argparse import ArgumentParser
-from dataclasses import dataclass
-from pathlib import Path
-from urllib.request import urlopen
-
-from typing import Optional, Iterator, Tuple, List, Iterable
-
-
-HOMEBREW_PYTHON = (3, 8)
-
-
-# This should match the pattern in .bumpversion.cfg
-VERSION_PATTERN = re.compile(
-    r'(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)'
-    r'((?P<prerelease>[a-z]+)(?P<num>\d+))?'
-)
-
-
-class Version:
-    def __init__(self, raw: str) -> None:
-        self.raw = raw
-        match = VERSION_PATTERN.match(self.raw)
-        assert match is not None, f'Invalid version: {self.raw}'
-        groups = match.groupdict()
-
-        self.major: int = int(groups['major'])
-        self.minor: int = int(groups['minor'])
-        self.patch: int = int(groups['patch'])
-        self.prerelease: Optional[str] = None
-        self.num: Optional[int] = None
-
-        if groups['num'] is not None:
-            self.prerelease = groups['prerelease']
-            self.num = int(groups['num'])
-
-    def __str__(self):
-        return self.raw
-
-    def homebrew_class_name(self) -> str:
-        name = f'DbtAT{self.major}{self.minor}{self.patch}'
-        if self.prerelease is not None and self.num is not None:
-            name = f'{name}{self.prerelease.title()}{self.num}'
-        return name
-
-    def homebrew_filename(self):
-        version_str = f'{self.major}.{self.minor}.{self.patch}'
-        if self.prerelease is not None and self.num is not None:
-            version_str = f'{version_str}-{self.prerelease}{self.num}'
-        return f'dbt@{version_str}.rb'
-
-
-@dataclass
-class Arguments:
-    version: Version
-    part: str
-    path: Path
-    homebrew_path: Path
-    homebrew_set_default: bool
-    set_version: bool
-    build_pypi: bool
-    upload_pypi: bool
-    test_upload: bool
-    build_homebrew: bool
-    build_docker: bool
-    upload_docker: bool
-    write_requirements: bool
-    write_dockerfile: bool
-
-    @classmethod
-    def parse(cls) -> 'Arguments':
-        parser = ArgumentParser(
-            prog="Bump dbt's version, build packages"
-        )
-        parser.add_argument(
-            'version',
-            type=Version,
-            help="The version to set",
-        )
-        parser.add_argument(
-            'part',
-            type=str,
-            help="The part of the version to update",
-        )
-        parser.add_argument(
-            '--path',
-            type=Path,
-            help='The path to the dbt repository',
-            default=Path.cwd(),
-        )
-        parser.add_argument(
-            '--homebrew-path',
-            type=Path,
-            help='The path to the dbt homebrew install',
-            default=(Path.cwd() / '../homebrew-dbt'),
-        )
-        parser.add_argument(
-            '--homebrew-set-default',
-            action='store_true',
-            help='If set, make this homebrew version the default',
-        )
-        parser.add_argument(
-            '--no-set-version',
-            dest='set_version',
-            action='store_false',
-            help='Skip bumping the version',
-        )
-        parser.add_argument(
-            '--no-build-pypi',
-            dest='build_pypi',
-            action='store_false',
-            help='skip building pypi',
-        )
-        parser.add_argument(
-            '--no-build-docker',
-            dest='build_docker',
-            action='store_false',
-            help='skip building docker images',
-        )
-        parser.add_argument(
-            '--no-upload-docker',
-            dest='upload_docker',
-            action='store_false',
-            help='skip uploading docker images',
-        )
-
-        uploading = parser.add_mutually_exclusive_group()
-
-        uploading.add_argument(
-            '--upload-pypi',
-            dest='force_upload_pypi',
-            action='store_true',
-            help='upload to pypi even if building is disabled'
-        )
-
-        uploading.add_argument(
-            '--no-upload-pypi',
-            dest='no_upload_pypi',
-            action='store_true',
-            help='skip uploading to pypi',
-        )
-
-        parser.add_argument(
-            '--no-upload',
-            dest='test_upload',
-            action='store_false',
-            help='Skip uploading to pypitest',
-        )
-
-        parser.add_argument(
-            '--no-build-homebrew',
-            dest='build_homebrew',
-            action='store_false',
-            help='Skip building homebrew packages',
-        )
-        parser.add_argument(
-            '--no-write-requirements',
-            dest='write_requirements',
-            action='store_false',
-            help='Skip writing the requirements file. It must exist.'
-        )
-        parser.add_argument(
-            '--no-write-dockerfile',
-            dest='write_dockerfile',
-            action='store_false',
-            help='Skip writing the dockerfile. It must exist.'
-        )
-        parsed = parser.parse_args()
-
-        upload_pypi = parsed.build_pypi
-        if parsed.force_upload_pypi:
-            upload_pypi = True
-        elif parsed.no_upload_pypi:
-            upload_pypi = False
-
-        return cls(
-            version=parsed.version,
-            part=parsed.part,
-            path=parsed.path,
-            homebrew_path=parsed.homebrew_path,
-            homebrew_set_default=parsed.homebrew_set_default,
-            set_version=parsed.set_version,
-            build_pypi=parsed.build_pypi,
-            upload_pypi=upload_pypi,
-            test_upload=parsed.test_upload,
-            build_homebrew=parsed.build_homebrew,
-            build_docker=parsed.build_docker,
-            upload_docker=parsed.upload_docker,
-            write_requirements=parsed.write_requirements,
-            write_dockerfile=parsed.write_dockerfile,
-        )
-
-
-def collect_output(cmd, cwd=None, stderr=subprocess.PIPE) -> str:
-    try:
-        result = subprocess.run(
-            cmd, cwd=cwd, check=True, stdout=subprocess.PIPE, stderr=stderr
-        )
-    except subprocess.CalledProcessError as exc:
-        print(f'Command {exc.cmd} failed')
-        if exc.output:
-            print(exc.output.decode('utf-8'))
-        if exc.stderr:
-            print(exc.stderr.decode('utf-8'), file=sys.stderr)
-        raise
-    return result.stdout.decode('utf-8')
-
-
-def run_command(cmd, cwd=None) -> None:
-    result = collect_output(cmd, stderr=subprocess.STDOUT, cwd=cwd)
-    print(result)
-
-
-def set_version(path: Path, version: Version, part: str):
-    # bumpversion --commit --no-tag --new-version "${version}" "${port}"
-    cmd = [
-        'bumpversion', '--commit', '--no-tag', '--new-version',
-        str(version), part
-    ]
-    print(f'bumping version to {version}')
-    run_command(cmd, cwd=path)
-    print(f'bumped version to {version}')
-
-
-class PypiBuilder:
-    _SUBPACKAGES = (
-        'core',
-        'plugins/postgres',
-        'plugins/redshift',
-        'plugins/bigquery',
-        'plugins/snowflake',
-    )
-
-    def __init__(self, dbt_path: Path):
-        self.dbt_path = dbt_path
-
-    @staticmethod
-    def _dist_for(path: Path, make=False) -> Path:
-        dist_path = path / 'dist'
-        if dist_path.exists():
-            shutil.rmtree(dist_path)
-        if make:
-            os.makedirs(dist_path)
-        build_path = path / 'build'
-        if build_path.exists():
-            shutil.rmtree(build_path)
-        return dist_path
-
-    @staticmethod
-    def _build_pypi_package(path: Path):
-        print(f'building package in {path}')
-        cmd = ['python', 'setup.py', 'sdist', 'bdist_wheel']
-        run_command(cmd, cwd=path)
-        print(f'finished building package in {path}')
-
-    @staticmethod
-    def _all_packages_in(path: Path) -> Iterator[Path]:
-        path = path / 'dist'
-        for pattern in ('*.tar.gz', '*.whl'):
-            yield from path.glob(pattern)
-
-    def _build_subpackage(self, name: str) -> Iterator[Path]:
-        subpath = self.dbt_path / name
-        self._dist_for(subpath)
-        self._build_pypi_package(subpath)
-        return self._all_packages_in(subpath)
-
-    def build(self):
-        print('building pypi packages')
-        dist_path = self._dist_for(self.dbt_path)
-        sub_pkgs: List[Path] = []
-        for path in self._SUBPACKAGES:
-            sub_pkgs.extend(self._build_subpackage(path))
-
-        # now build the main package
-        self._build_pypi_package(self.dbt_path)
-        # now copy everything from the subpackages in
-        for package in sub_pkgs:
-            shutil.copy(str(package), dist_path)
-
-        print('built pypi packages')
-
-    def upload(self, *, test=True):
-        cmd = ['twine', 'check']
-        cmd.extend(str(p) for p in self._all_packages_in(self.dbt_path))
-        run_command(cmd)
-        cmd = ['twine', 'upload']
-        if test:
-            cmd.extend(['--repository', 'pypitest'])
-        cmd.extend(str(p) for p in self._all_packages_in(self.dbt_path))
-        print('uploading packages: {}'.format(' '.join(cmd)))
-        run_command(cmd)
-        print('uploaded packages')
-
-
-class PipInstaller(venv.EnvBuilder):
-    def __init__(self, packages: List[str]) -> None:
-        super().__init__(with_pip=True)
-        self.packages = packages
-
-    def post_setup(self, context):
-        # we can't run from the dbt directory or this gets all weird, so
-        # install from an empty temp directory and then remove it.
-        tmp = tempfile.mkdtemp()
-        cmd = [context.env_exe, '-m', 'pip', 'install', '--upgrade']
-        cmd.extend(self.packages)
-        print(f'installing {self.packages}')
-        try:
-            run_command(cmd, cwd=tmp)
-        finally:
-            os.rmdir(tmp)
-        print(f'finished installing {self.packages}')
-
-    def create(self, venv_path):
-        os.makedirs(venv_path.parent, exist_ok=True)
-        if venv_path.exists():
-            shutil.rmtree(venv_path)
-        return super().create(venv_path)
-
-
-def _require_wheels(dbt_path: Path) -> List[Path]:
-    dist_path = dbt_path / 'dist'
-    wheels = list(dist_path.glob('*.whl'))
-    if not wheels:
-        raise ValueError(
-            f'No wheels found in {dist_path} - run scripts/build-wheels.sh'
-        )
-    return wheels
-
-
-class DistFolderEnv(PipInstaller):
-    def __init__(self, dbt_path: Path) -> None:
-        self.wheels = _require_wheels(dbt_path)
-        super().__init__(packages=self.wheels)
-
-
-class HomebrewVirtualenv(PipInstaller):
-    def __init__(self, dbt_version: Version) -> None:
-        super().__init__([f'dbt=={dbt_version}'])
-
-
-@dataclass
-class HomebrewDependency:
-    name: str
-    url: str
-    sha256: str
-    version: str
-
-    def render(self, indent: int = 2) -> str:
-        result = textwrap.dedent(f'''\
-            resource "{self.name}" do  # {self.name}=={self.version}
-              url "{self.url}"
-              sha256 "{self.sha256}"
-            end
-        ''')
-        return textwrap.indent(result, ' '*indent)
-
-    def __str__(self) -> str:
-        return self.render(indent=0)
-
-
-@dataclass
-class HomebrewTemplate:
-    url_data: str
-    hash_data: str
-    dependencies: List[HomebrewDependency]
-
-
-def _make_venv_at(root: Path, name: str, builder: venv.EnvBuilder):
-    venv_path = root / name
-    os.makedirs(root, exist_ok=True)
-    if venv_path.exists():
-        shutil.rmtree(venv_path)
-
-    builder.create(venv_path)
-    return venv_path
-
-
-class HomebrewBuilder:
-    def __init__(
-        self,
-        dbt_path: Path,
-        version: Version,
-        homebrew_path: Path,
-        set_default: bool,
-    ) -> None:
-        self.dbt_path = dbt_path
-        self.version = version
-        self.homebrew_path = homebrew_path
-        self.set_default = set_default
-        self._template: Optional[HomebrewTemplate] = None
-
-    def make_venv(self) -> HomebrewVirtualenv:
-        env = HomebrewVirtualenv(self.version)
-        max_attempts = 10
-        for attempt in range(1, max_attempts+1):
-            # after uploading to pypi, it can take a few minutes for installing
-            # to work. Retry a few times...
-            try:
-                env.create(self.homebrew_venv_path)
-                return
-            except subprocess.CalledProcessError:
-                if attempt == max_attempts:
-                    raise
-                else:
-                    print(
-                        f'installation failed - waiting 60s for pypi to see '
-                        f'the new version (attempt {attempt}/{max_attempts})'
-                    )
-                    time.sleep(60)
-
-        return env
-
-    @property
-    def versioned_formula_path(self) -> Path:
-        return (
-            self.homebrew_path / 'Formula' / self.version.homebrew_filename()
-        )
-
-    @property
-    def default_formula_path(self) -> Path:
-        return (
-            self.homebrew_path / 'Formula/dbt.rb'
-        )
-
-    @property
-    def homebrew_venv_path(self) -> Path:
-        return self.dbt_path / 'build' / 'homebrew-venv'
-
-    @staticmethod
-    def _dbt_homebrew_formula_fmt() -> str:
-        return textwrap.dedent('''\
-            class {formula_name} < Formula
-              include Language::Python::Virtualenv
-
-              desc "Data build tool"
-              homepage "https://github.com/fishtown-analytics/dbt"
-              url "{url_data}"
-              sha256 "{hash_data}"
-              revision 1
-
-              bottle do
-                root_url "http://bottles.getdbt.com"
-                # bottle hashes + versions go here
-              end
-
-              depends_on "openssl@1.1"
-              depends_on "postgresql"
-              depends_on "python"
-
-            {dependencies}
-            {trailer}
-            end
-            ''')
-
-    @staticmethod
-    def _dbt_homebrew_trailer() -> str:
-        dedented = textwrap.dedent('''\
-          def install
-            venv = virtualenv_create(libexec, "python3")
-
-            res = resources.map(&:name).to_set
-
-            res.each do |r|
-              venv.pip_install resource(r)
-            end
-
-            venv.pip_install_and_link buildpath
-
-            bin.install_symlink "#{libexec}/bin/dbt" => "dbt"
-          end
-
-          test do
-            (testpath/"dbt_project.yml").write(
-              "{name: 'test', version: '0.0.1', profile: 'default'}",
-            )
-            (testpath/".dbt/profiles.yml").write(
-              "{default: {outputs: {default: {type: 'postgres', threads: 1,
-              host: 'localhost', port: 5432, user: 'root', pass: 'password',
-              dbname: 'test', schema: 'test'}}, target: 'default'}}",
-            )
-            (testpath/"models/test.sql").write("select * from test")
-            system "#{bin}/dbt", "test"
-          end''')
-        return textwrap.indent(dedented, '  ')
-
-    def get_formula_data(
-        self, versioned: bool = True
-    ) -> str:
-        fmt = self._dbt_homebrew_formula_fmt()
-        trailer = self._dbt_homebrew_trailer()
-        if versioned:
-            formula_name = self.version.homebrew_class_name()
-        else:
-            formula_name = 'Dbt'
-
-        dependencies_str = '\n'.join(
-            d.render() for d in self.template.dependencies
-        )
-
-        return fmt.format(
-            formula_name=formula_name,
-            version=self.version,
-            url_data=self.template.url_data,
-            hash_data=self.template.hash_data,
-            dependencies=dependencies_str,
-            trailer=trailer,
-        )
-
-    @property
-    def template(self) -> HomebrewTemplate:
-        if self._template is None:
-            self.make_venv()
-            print('done setting up virtualenv')
-
-            dependencies = []
-            dbt_package = None
-            for pkg in self._get_packages():
-                if pkg.name == 'dbt':
-                    if pkg.version != str(self.version):
-                        raise ValueError(
-                            f'Found an invalid dbt=={pkg.version}, '
-                            f'expected dbt=={self.version}'
-                        )
-                    dbt_package = pkg
-                else:
-                    # we can assume that anything starting with dbt- in a fresh
-                    # venv is a dbt package, I hope
-                    if pkg.name.startswith('dbt-'):
-                        if pkg.version != str(self.version):
-                            raise ValueError(
-                                f'Found an invalid {pkg.name}=={pkg.version}, '
-                                f'expected {pkg.name}=={self.version}'
-                            )
-                    dependencies.append(pkg)
-            if dbt_package is None:
-                raise RuntimeError(
-                    'never found dbt in "pip freeze -l" output'
-                )
-            template = HomebrewTemplate(
-                url_data=dbt_package.url,
-                hash_data=dbt_package.sha256,
-                dependencies=dependencies,
-            )
-            self._template = template
-        else:
-            template = self._template
-        return template
-
-    def _get_pypi_info(self, pkg: str, version: str) -> Tuple[str, str]:
-        fp = urlopen(f'https://pypi.org/pypi/{pkg}/{version}/json')
-        try:
-            data = json.load(fp)
-        finally:
-            fp.close()
-        assert 'urls' in data
-        for pkginfo in data['urls']:
-            assert 'packagetype' in pkginfo
-            if pkginfo['packagetype'] == 'sdist':
-                assert 'url' in pkginfo
-                assert 'digests' in pkginfo
-                assert 'sha256' in pkginfo['digests']
-                url = pkginfo['url']
-                sha256 = pkginfo['digests']['sha256']
-                return url, sha256
-        raise ValueError(f'Never got a valid sdist for {pkg}=={version}')
-
-    def _get_packages(self) -> Iterator[HomebrewDependency]:
-        pip = self.homebrew_venv_path / 'bin/pip'
-        cmd = [pip, 'freeze', '-l']
-        raw = collect_output(cmd).split('\n')
-        for line in raw:
-            if not line:
-                continue
-            parts = line.split('==')
-            if len(parts) != 2:
-                raise ValueError(
-                    f'Could not parse pip freeze output line: {line}'
-                )
-            name, version = parts
-            url, sha256 = self._get_pypi_info(name, version)
-            dep = HomebrewDependency(
-                name=name, url=url, sha256=sha256, version=version
-            )
-            yield dep
-
-    def _remove_dbt_resource(self, lines: List[str]) -> Iterator[str]:
-        # TODO: fork poet or extract the good bits to avoid this
-        line_iter = iter(lines)
-        # don't do a double-newline or "brew audit" gets mad
-        for line in line_iter:
-            # skip the contents of the "dbt" resource block.
-            if line.strip() == 'resource "dbt" do':
-                for skip in line_iter:
-                    if skip.strip() == 'end':
-                        # skip the newline after 'end'
-                        next(line_iter)
-                        break
-            else:
-                yield line
-
-    def create_versioned_formula_file(self):
-        formula_contents = self.get_formula_data(versioned=True)
-        if self.versioned_formula_path.exists():
-            print('Homebrew formula path already exists, overwriting')
-        self.versioned_formula_path.write_text(formula_contents)
-
-    def commit_versioned_formula(self):
-        # add a commit for the new formula
-        run_command(
-            ['git', 'add', self.versioned_formula_path],
-            cwd=self.homebrew_path
-        )
-        run_command(
-            ['git', 'commit', '-m', f'add dbt@{self.version}'],
-            cwd=self.homebrew_path
-        )
-
-    def commit_default_formula(self):
-        run_command(
-            ['git', 'add', self.default_formula_path],
-            cwd=self.homebrew_path
-        )
-        run_command(
-            ['git', 'commit', '-m', f'upgrade dbt to {self.version}'],
-            cwd=self.homebrew_path
-        )
-
-    @staticmethod
-    def run_tests(formula_path: Path, audit: bool = True):
-        path = os.path.normpath(formula_path)
-        run_command(['brew', 'uninstall', '--force', path])
-        versions = [
-            l.strip() for l in
-            collect_output(['brew', 'list']).split('\n')
-            if l.strip().startswith('dbt@') or l.strip() == 'dbt'
-        ]
-        if versions:
-            run_command(['brew', 'unlink'] + versions)
-        run_command(['brew', 'install', path])
-        run_command(['brew', 'test', path])
-        if audit:
-            run_command(['brew', 'audit', '--strict', path])
-
-    def create_default_package(self):
-        os.remove(self.default_formula_path)
-        formula_contents = self.get_formula_data(versioned=False)
-        self.default_formula_path.write_text(formula_contents)
-
-    def build(self):
-        self.create_versioned_formula_file()
-        # self.run_tests(formula_path=self.versioned_formula_path)
-        self.commit_versioned_formula()
-
-        if self.set_default:
-            self.create_default_package()
-            # self.run_tests(formula_path=self.default_formula_path, audit=False)
-            self.commit_default_formula()
-
-
-class WheelInfo:
-    def __init__(self, path):
-        self.path = path
-
-    @staticmethod
-    def _extract_distinfo_path(wfile: zipfile.ZipFile) -> zipfile.Path:
-        zpath = zipfile.Path(root=wfile)
-        for path in zpath.iterdir():
-            if path.name.endswith('.dist-info'):
-                return path
-        raise ValueError('Wheel with no dist-info?')
-
-    def get_metadata(self) -> Dict[str, str]:
-        with zipfile.ZipFile(self.path) as wf:
-            distinfo = self._extract_distinfo_path(wf)
-            metadata = distinfo / 'METADATA'
-            metadata_dict: Dict[str, str] = {}
-            for line in metadata.read_text().split('\n'):
-                parts = line.split(': ', 1)
-                if len(parts) == 2:
-                    metadata_dict[parts[0]] = parts[1]
-            return metadata_dict
-
-    def package_name(self) -> str:
-        metadata = self.get_metadata()
-        if 'Name' not in metadata:
-            raise ValueError('Wheel with no name?')
-        return metadata['Name']
-
-
-class DockerBuilder:
-    """The docker builder requires the existence of a dbt package"""
-    def __init__(self, dbt_path: Path, version: Version) -> None:
-        self.dbt_path = dbt_path
-        self.version = version
-
-    @property
-    def docker_path(self) -> Path:
-        return self.dbt_path / 'docker'
-
-    @property
-    def dockerfile_name(self) -> str:
-        return f'Dockerfile.{self.version}'
-
-    @property
-    def dockerfile_path(self) -> Path:
-        return self.docker_path / self.dockerfile_name
-
-    @property
-    def requirements_path(self) -> Path:
-        return self.docker_path / 'requirements'
-
-    @property
-    def requirements_file_name(self) -> str:
-        return f'requirements.{self.version}.txt'
-
-    @property
-    def dockerfile_venv_path(self) -> Path:
-        return self.dbt_path / 'build' / 'docker-venv'
-
-    @property
-    def requirements_txt_path(self) -> Path:
-        return self.requirements_path / self.requirements_file_name
-
-    def make_venv(self) -> DistFolderEnv:
-        env = DistFolderEnv(self.dbt_path)
-
-        env.create(self.dockerfile_venv_path)
-        return env
-
-    def get_frozen(self) -> str:
-        env = self.make_venv()
-        pip_path = self.dockerfile_venv_path / 'bin/pip'
-        cmd = [pip_path, 'freeze']
-        wheel_names = {
-            WheelInfo(wheel_path).package_name() for wheel_path in env.wheels
-        }
-        # remove the dependencies in dbt itself
-        return '\n'.join([
-            dep for dep in collect_output(cmd).split('\n')
-            if dep.split('==')[0] not in wheel_names
-        ])
-
-    def write_lockfile(self):
-        freeze = self.get_frozen()
-        path = self.requirements_txt_path
-        if path.exists():
-            raise ValueError(f'Found existing requirements file at {path}!')
-        os.makedirs(path.parent, exist_ok=True)
-        path.write_text(freeze)
-
-    def get_dockerfile_contents(self):
-        dist_path = (self.dbt_path / 'dist').relative_to(Path.cwd())
-        wheel_paths = ' '.join(
-            os.path.join('.', 'dist', p.name)
-            for p in _require_wheels(self.dbt_path)
-        )
-
-        requirements_path = self.requirements_txt_path.relative_to(Path.cwd())
-
-        return textwrap.dedent(
-            f'''\
-            FROM python:3.8.1-slim-buster
-
-            RUN apt-get update && \
-                apt-get dist-upgrade -y && \
-                apt-get install -y  --no-install-recommends \
-                    git software-properties-common make build-essential \
-                    ca-certificates libpq-dev && \
-                apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-
-            COPY {requirements_path} ./{self.requirements_file_name}
-            COPY {dist_path} ./dist
-            RUN pip install --upgrade pip setuptools
-            RUN pip install --requirement ./{self.requirements_file_name}
-            RUN pip install {wheel_paths}
-
-            RUN useradd -mU dbt_user
-
-            ENV PYTHONIOENCODING=utf-8
-            ENV LANG C.UTF-8
-
-            WORKDIR /usr/app
-            VOLUME /usr/app
-
-            USER dbt_user
-            ENTRYPOINT dbt
-            '''
-        )
-
-    def write_dockerfile(self):
-        dockerfile = self.get_dockerfile_contents()
-        path = self.dockerfile_path
-        if path.exists():
-            raise ValueError(f'Found existing docker file at {path}!')
-        os.makedirs(path.parent, exist_ok=True)
-        path.write_text(dockerfile)
-
-    @property
-    def image_tag(self):
-        return f'dbt:{self.version}'
-
-    @property
-    def remote_tag(self):
-        return f'fishtownanalytics/{self.image_tag}'
-
-    def create_docker_image(self):
-        run_command(
-            [
-                'docker', 'build',
-                '-f', self.dockerfile_path,
-                '--tag', self.image_tag,
-                # '--no-cache',
-                self.dbt_path,
-            ],
-            cwd=self.dbt_path
-        )
-
-    def set_remote_tag(self):
-        # tag it
-        run_command(
-            ['docker', 'tag', self.image_tag, self.remote_tag],
-            cwd=self.dbt_path,
-        )
-
-    def commit_docker_folder(self):
-        # commit the contents of docker/
-        run_command(
-            ['git', 'add', 'docker'],
-            cwd=self.dbt_path
-        )
-        commit_msg = f'Add {self.image_tag} dockerfiles and requirements'
-        run_command(['git', 'commit', '-m', commit_msg], cwd=self.dbt_path)
-
-    def build(
-        self,
-        write_requirements: bool = True,
-        write_dockerfile: bool = True
-    ):
-        if write_requirements:
-            self.write_lockfile()
-        if write_dockerfile:
-            self.write_dockerfile()
-        self.commit_docker_folder()
-        self.create_docker_image()
-        self.set_remote_tag()
-
-    def push(self):
-        run_command(
-            ['docker', 'push', self.remote_tag]
-        )
-
-
-def sanity_check():
-    if sys.version_info[:len(HOMEBREW_PYTHON)] != HOMEBREW_PYTHON:
-        python_version_str = '.'.join(str(i) for i in HOMEBREW_PYTHON)
-        print(f'This script must be run with python {python_version_str}')
-        sys.exit(1)
-
-    # avoid "what's a bdist_wheel" errors
-    try:
-        import wheel  # type: ignore # noqa
-    except ImportError:
-        print(
-            'The wheel package is required to build. Please run:\n'
-            'pip install -r dev_requirements.txt'
-        )
-        sys.exit(1)
-
-
-def upgrade_to(args: Arguments):
-    if args.set_version:
-        set_version(args.path, args.version, args.part)
-
-    builder = PypiBuilder(args.path)
-    if args.build_pypi:
-        builder.build()
-
-    if args.upload_pypi:
-        if args.test_upload:
-            builder.upload()
-            input(
-                f'Ensure https://test.pypi.org/project/dbt/{args.version}/ '
-                'exists and looks reasonable'
-            )
-        builder.upload(test=False)
-
-    if args.build_homebrew:
-        if args.upload_pypi:
-            print('waiting a minute for pypi before trying to pip install')
-            # if we uploaded to pypi, wait a minute before we bother trying to
-            # pip install
-            time.sleep(60)
-        HomebrewBuilder(
-            dbt_path=args.path,
-            version=args.version,
-            homebrew_path=args.homebrew_path,
-            set_default=args.homebrew_set_default,
-        ).build()
-
-    if args.build_docker:
-        builder = DockerBuilder(
-            dbt_path=args.path,
-            version=args.version,
-        )
-        builder.build(
-            write_requirements=args.write_requirements,
-            write_dockerfile=args.write_dockerfile,
-        )
-        if args.upload_docker:
-            builder.push()
-
-
-def main():
-    sanity_check()
-    args = Arguments.parse()
-    upgrade_to(args)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/scripts/dtr.py b/scripts/dtr.py
deleted file mode 100755
--- a/scripts/dtr.py
+++ /dev/null
@@ -1,369 +0,0 @@
-#!/usr/bin/env python3
-import argparse
-import os
-import shlex
-import shutil
-import subprocess
-import sys
-
-# Python version defaults to 3.6
-# To run postgres integration tests: `dtr.py -i -t pg` (this is the default)
-# To run postgres integration tests, clearing `dbt.log` beforehand: `dtr.py -il -t pg`
-# dtr.py -i -t pg -a test/integration/029_docs_generate_tests
-# To run postgres + redshift integration tests: `dtr.py -i  -t pg -t rs`
-# To drop to pdb on failure, add `--pdb` or `-p`
-# To run mypy tests: `dtr.py -m`.
-# To run flake8 test: `dtr.py -f`.
-# To run unit tests: `dtr.py -u`
-# To run rpc tests: `dtr.py -r`
-
-_SHORTHAND = {
-    'p': 'postgres',
-    'pg': 'postgres',
-    'postgres': 'postgres',
-    'pr': 'presto',
-    'presto': 'presto',
-    'r': 'redshift',
-    'rs': 'redshift',
-    'redshift': 'redshift',
-    'b': 'bigquery',
-    'bq': 'bigquery',
-    'bigquery': 'bigquery',
-    's': 'snowflake',
-    'sf': 'snowflake',
-    'snowflake': 'snowflake',
-}
-
-
-def type_convert(types: str):
-    result = set()
-    for t in types.split(','):
-        try:
-            result.add(_SHORTHAND[t])
-        except KeyError:
-            raise ValueError(
-                'value "{}" not allowed, must be one of [{}]'
-                .format(t, ','.join('"{}"'.format(k) for k in _SHORTHAND)))
-    return result
-
-
-def parse_args(argv):
-    if not argv:
-        argv.extend(['-it', 'pg'])
-    parser = argparse.ArgumentParser()
-    parser.add_argument(
-        '-f', '--flake8',
-        help='run flake8',
-        dest='commands',
-        action='append_const', const=Flake8Builder
-    )
-    parser.add_argument(
-        '-m', '--mypy',
-        help='Run mypy',
-        dest='commands',
-        action='append_const', const=MypyBuilder
-    )
-    parser.add_argument(
-        '-u', '--unit',
-        help='run unit tests',
-        dest='commands',
-        action='append_const', const=UnitBuilder
-    )
-    parser.add_argument(
-        '-i', '--integration',
-        help='run integration tests',
-        dest='commands',
-        action='append_const', const=IntegrationBuilder
-    )
-    parser.add_argument(
-        '-r', '--rpc',
-        help='run rpc tests',
-        dest='commands',
-        action='append_const', const=RPCBuilder
-    )
-
-    parser.add_argument('-v', '--python-version',
-                        default='38', choices=['36', '37', '38'],
-                        help='what python version to run')
-    parser.add_argument(
-        '-t', '--types',
-        default=None,
-        help='The types of tests to run, if this is an integration run'
-    )
-    parser.add_argument(
-        '-c', '--continue',
-        action='store_false', dest='stop',
-        help='If set, continue on failures'
-    )
-    parser.add_argument(
-        '-l', '--remove-logs',
-        action='store_true',
-        help='remove dbt log files before running'
-    )
-
-    parser.add_argument(
-        '-1', '--single-threaded',
-        action='store_true',
-        help='Specify if the DBT_TEST_SINGLE_THREADED environment variable should be set'
-    )
-    parser.add_argument(
-        '--coverage',
-        action='store_true',
-        help='Make a coverage report and print it to the terminal'
-    )
-    parser.add_argument(
-        '-p', '--pdb',
-        action='store_true',
-        help='Drop into ipdb on failures, implies "--no-multi"'
-    )
-    parser.add_argument(
-        '-k',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Pass-through to pytest, test selector expression'
-    )
-    parser.add_argument(
-        '--no-multi',
-        action='store_false',
-        dest='multi',
-        help='Turn off multiprocessing'
-    )
-
-    parser.add_argument(
-        '--docker-args',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Specify docker-compose args')
-    parser.add_argument(
-        '--tox-args',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Specify tox args')
-    parser.add_argument(
-        '--pylint-args',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Specify pylint args')
-    parser.add_argument(
-        '-a', '--test-args',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Specify integration test parameters, tacked on to the end'
-    )
-    parser.add_argument(
-        '--unit-args',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Specify unit test parameters, tacked on to the end'
-    )
-    parser.add_argument(
-        '--flake8-args',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Specify flake8 parameters, tacked on to the end'
-    )
-    parser.add_argument(
-        '--mypy-args',
-        action='append',
-        nargs='?',
-        default=[],
-        help='Specify mypy parameters, tacked on to the end'
-    )
-    parser.add_argument(
-        'extra',
-        nargs='*',
-        default=[],
-        help='Any extra args that will apply to all pytest runs'
-    )
-    parser.add_argument(
-        '--debug',
-        action='store_true',
-    )
-
-    parsed = parser.parse_args(argv)
-    if parsed.types:
-        parsed.types = type_convert(parsed.types)
-    else:
-        parsed.types = {'postgres', 'redshift', 'bigquery', 'snowflake'}
-    return parsed
-
-
-class ArgBuilder(object):
-
-    def __init__(self, parsed):
-        self.parsed = parsed
-        self.args = []
-        self.add_test_environment_args()
-
-    def add_extras(self):
-        raise NotImplementedError
-
-    def add_container_args(self):
-        pass
-
-    def run(self):
-        print('args={}'.format(self.args))
-        result = subprocess.run(self.args)
-        result.check_returncode()
-
-    def add_test_environment_args(self):
-        pass
-
-
-class DockerBuilder(ArgBuilder):
-    def add_docker_args(self):
-        self.args = ['docker-compose', 'run', '--rm']
-        if self.parsed.debug:
-            self.parsed.single_threaded = True
-            self.args.extend(('-e', 'DBT_MACRO_DEBUGGING=write'))
-        if self.parsed.single_threaded:
-            self.args.extend(('-e', 'DBT_TEST_SINGLE_THREADED=y'))
-        if self.parsed.docker_args:
-            self.args.extend(self.parsed.docker_args)
-        self.args.append('test')
-
-    def add_test_environment_args(self):
-        super().add_test_environment_args()
-        self.add_docker_args()
-
-
-class ToxBuilder(DockerBuilder):
-    def envname(self):
-        raise NotImplementedError('need an env name')
-
-    def add_tox_args(self):
-        self.args.extend(['tox', '-e', self.envname()])
-        if self.parsed.tox_args:
-            self.args.extend(self.parsed.tox_args)
-        self.args.append('--')
-
-    def add_test_environment_args(self):
-        super().add_test_environment_args()
-        self.add_tox_args()
-
-
-class PytestBuilder(ToxBuilder):
-    DEFAUlTS = None
-
-    def envname(self):
-        return 'explicit-py{}'.format(self.parsed.python_version)
-
-    def add_pytest_args(self):
-        assert self.DEFAUlTS is not None
-        self.args.append('-s')
-        if self.parsed.pdb:
-            self.args.extend(['--pdb', '--pdbcls=IPython.terminal.debugger:Pdb'])
-            self.parsed.multi = False
-        if self.parsed.stop:
-            self.args.append('-x')
-        if self.parsed.coverage:
-            self.args.extend(('--cov', 'dbt', '--cov-branch', '--cov-report', 'term'))
-        for arg in self.parsed.k:
-            self.args.extend(('-k', arg))
-        if self.parsed.multi:
-            self.args.extend(('-n', 'auto'))
-
-        if not self.add_extra_pytest_args():
-            self.args.extend(self.DEFAUlTS)
-
-    def add_extra_pytest_args(self):
-        raise NotImplementedError
-
-    def add_test_environment_args(self):
-        super().add_test_environment_args()
-        self.add_pytest_args()
-
-
-class IntegrationBuilder(PytestBuilder):
-    DEFAUlTS = ['test/integration']
-
-    def add_extra_pytest_args(self):
-        if self.parsed.types:
-            self.args.append('-m')
-            typestrs = ('profile_{}'.format(t) for t in self.parsed.types)
-            selector = ' or '.join(typestrs)
-            self.args.append(shlex.quote(selector))
-        start = len(self.args)
-        self.args.extend(self.parsed.test_args)
-        self.args.extend(self.parsed.extra)
-        return len(self.args) - start > 0
-
-
-class RPCBuilder(PytestBuilder):
-    DEFAUlTS = ['test/rpc']
-
-    def add_extra_pytest_args(self):
-        start = len(self.args)
-        self.args.extend(self.parsed.test_args)
-        self.args.extend(self.parsed.extra)
-        return len(self.args) - start > 0
-
-
-class UnitBuilder(PytestBuilder):
-    DEFAUlTS = ['test/unit']
-
-    def add_extra_pytest_args(self):
-        start = len(self.args)
-        self.args.extend(self.parsed.unit_args)
-        self.args.extend(self.parsed.extra)
-        return len(self.args) - start > 0
-
-
-class Flake8Builder(ArgBuilder):
-    def add_test_environment_args(self):
-        self.args.extend(['flake8', '--select', 'E,W,F', '--ignore', 'W504'])
-        start = len(self.args)
-        self.args.extend(self.parsed.flake8_args)
-        if len(self.args) == start:
-            if os.path.exists('dbt/main.py'):
-                self.args.append('dbt')
-            elif os.path.exists('core/dbt/main.py'):
-                self.args.append('core/dbt')
-                for adapter in ('postgres', 'redshift', 'bigquery', 'snowflake'):
-                    self.args.append('plugins/{}/dbt'.format(adapter))
-
-
-class MypyBuilder(ToxBuilder):
-    def envname(self):
-        return 'mypy-dev'
-
-    def run(self):
-        # The cache is a big source of false errors
-        if os.path.exists('./.mypy_cache'):
-            shutil.rmtree('./.mypy_cache')
-        return super().run()
-
-    def add_test_environment_args(self):
-        super().add_test_environment_args()
-        self.args.extend(self.parsed.mypy_args)
-
-
-def main(argv=None):
-    if argv is None:
-        argv = sys.argv[1:]
-    parsed = parse_args(argv)
-    print('args={}'.format(parsed))
-    if parsed.remove_logs:
-        path = 'logs/dbt.log'
-        if os.path.exists(path):
-            os.remove(path)
-
-    try:
-        for cls in parsed.commands:
-            builder = cls(parsed)
-            builder.run()
-    except subprocess.CalledProcessError:
-        print('failed!')
-        sys.exit(1)
-    print('success!')
-
-
-if __name__ == '__main__':
-    main()


Test patch:
diff --git a/Dockerfile.test b/Dockerfile.test
--- a/Dockerfile.test
+++ b/Dockerfile.test
@@ -46,9 +46,7 @@ RUN curl -LO https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_V
     && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
     && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
 
-RUN pip3 install -U "tox==3.14.4" wheel "six>=1.14.0,<1.15.0" "virtualenv==20.0.3" setuptools
-# tox fails if the 'python' interpreter (python2) doesn't have `tox` installed
-RUN pip install -U "tox==3.14.4" "six>=1.14.0,<1.15.0" "virtualenv==20.0.3" setuptools
+RUN pip3 install -U tox wheel six setuptools
 
 # These args are passed in via docker-compose, which reads then from the .env file.
 # On Linux, run `make .env` to create the .env file for the current user.
diff --git a/test/integration/001_simple_copy_test/test_simple_copy.py b/test/integration/001_simple_copy_test/test_simple_copy.py
--- a/test/integration/001_simple_copy_test/test_simple_copy.py
+++ b/test/integration/001_simple_copy_test/test_simple_copy.py
@@ -52,9 +52,9 @@ def test__postgres__simple_copy(self):
 
         self.use_default_project({"data-paths": [self.dir("seed-update")]})
         results = self.run_dbt(["seed"])
-        self.assertEqual(len(results),  1)
+        self.assertEqual(len(results), 1)
         results = self.run_dbt()
-        self.assertEqual(len(results),  7)
+        self.assertEqual(len(results), 7)
 
         self.assertManyTablesEqual(["seed", "view_model", "incremental", "materialized", "get_and_ref"])
 
diff --git a/test/integration/015_cli_invocation_tests/test_cli_invocation.py b/test/integration/015_cli_invocation_tests/test_cli_invocation.py
--- a/test/integration/015_cli_invocation_tests/test_cli_invocation.py
+++ b/test/integration/015_cli_invocation_tests/test_cli_invocation.py
@@ -149,7 +149,7 @@ def test_postgres_dbt_commands_with_cwd_as_project_dir(self):
 
     @use_profile('postgres')
     def test_postgres_dbt_commands_with_randomdir_as_project_dir(self):
-        workdir = os.getcwd()
+        workdir = self.test_root_dir
         with tempfile.TemporaryDirectory() as tmpdir:
             os.chdir(tmpdir)
             self._run_simple_dbt_commands(workdir)
@@ -157,7 +157,7 @@ def test_postgres_dbt_commands_with_randomdir_as_project_dir(self):
 
     @use_profile('postgres')
     def test_postgres_dbt_commands_with_relative_dir_as_project_dir(self):
-        workdir = os.getcwd()
+        workdir = self.test_root_dir
         with tempfile.TemporaryDirectory() as tmpdir:
             os.chdir(tmpdir)
             self._run_simple_dbt_commands(os.path.relpath(workdir, tmpdir))
diff --git a/test/integration/100_rpc_test/test_rpc.py b/test/integration/100_rpc_test/test_rpc.py
--- a/test/integration/100_rpc_test/test_rpc.py
+++ b/test/integration/100_rpc_test/test_rpc.py
@@ -392,8 +392,8 @@ def make_many_requests(self, num_requests):
         return stored
 
 
-@mark.flaky(rerun_filter=addr_in_use)
 class TestRPCServerCompileRun(HasRPCServer):
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_compile_sql_postgres(self):
         trivial = self.async_query(
@@ -478,6 +478,7 @@ def test_compile_sql_postgres(self):
             compiled_sql=_select_from_ephemeral
         )
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_run_sql_postgres(self):
         # seed + run dbt to make models before using them!
@@ -601,7 +602,7 @@ def test_run_sql_postgres(self):
             table={'column_names': ['id'], 'rows': [[1.0]]}
         )
 
-    @mark.flaky(rerun_filter=None)
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_ps_kill_postgres(self):
         task_tags = {
@@ -691,7 +692,7 @@ def test_ps_kill_postgres(self):
         self.assertGreater(rowdict[1]['elapsed'], 0)
         self.assertIsNone(rowdict[1]['tags'])
 
-    @mark.flaky(rerun_filter=lambda *a, **kw: True)
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_ps_kill_longwait_postgres(self):
         request_token, request_id = self.get_sleep_query()
@@ -704,6 +705,7 @@ def test_ps_kill_longwait_postgres(self):
         result_data = self.kill_and_assert(request_token, request_id)
         self.assertTrue(len(result_data['logs']) > 0)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_invalid_requests_postgres(self):
         # invalid method -> error on the initial query
@@ -759,6 +761,7 @@ def test_invalid_requests_postgres(self):
         self.assertIn('logs', error_data)
         self.assertTrue(len(error_data['logs']) > 0)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_timeout_postgres(self):
         data = self.async_query(
@@ -781,7 +784,6 @@ def test_timeout_postgres(self):
         return
 
 
-@mark.flaky(rerun_filter=addr_in_use)
 class TestRPCServerProjects(HasRPCServer):
     def assertHasResults(self, result, expected, *, missing=None, num_expected=None):
         dct = self.assertIsResult(result)
@@ -819,6 +821,7 @@ def assertHasTestResults(self, results, expected, pass_results=None):
 
         self.assertEqual(passes, pass_results)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_seed_project_postgres(self):
         # testing "dbt seed" is tricky so we'll just jam some sql in there
@@ -830,6 +833,7 @@ def test_seed_project_postgres(self):
         result = self.async_query('seed', show=False).json()
         self.correct_seed_result(result)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_seed_project_cli_postgres(self):
         self.run_sql_file("seed.sql")
@@ -839,6 +843,7 @@ def test_seed_project_cli_postgres(self):
         result = self.async_query('cli_args', cli='seed').json()
         self.correct_seed_result(result)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_compile_project_postgres(self):
 
@@ -859,6 +864,7 @@ def test_compile_project_postgres(self):
             num_expected=6,
         )
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_compile_project_cli_postgres(self):
         self.run_dbt_with_vars(['compile'])
@@ -879,6 +885,7 @@ def test_compile_project_cli_postgres(self):
             num_expected=6,
         )
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_run_project_postgres(self):
         result = self.async_query('run').json()
@@ -887,6 +894,7 @@ def test_run_project_postgres(self):
             result, {'descendant_model', 'multi_source_model', 'nonsource_descendant'})
         self.assertTablesEqual('multi_source_model', 'expected_multi_source')
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_run_project_cli_postgres(self):
         result = self.async_query('cli_args', cli='run').json()
@@ -894,6 +902,7 @@ def test_run_project_cli_postgres(self):
             result, {'descendant_model', 'multi_source_model', 'nonsource_descendant'})
         self.assertTablesEqual('multi_source_model', 'expected_multi_source')
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_test_project_postgres(self):
         self.run_dbt_with_vars(['run'])
@@ -902,6 +911,7 @@ def test_test_project_postgres(self):
         self.assertIn('results', result)
         self.assertHasTestResults(result['results'], 4)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_test_project_cli_postgres(self):
         self.run_dbt_with_vars(['run'])
@@ -910,6 +920,7 @@ def test_test_project_cli_postgres(self):
         self.assertIn('results', result)
         self.assertHasTestResults(result['results'], 4)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     def assertManifestExists(self, nodes_length, sources_length):
         self.assertTrue(os.path.exists('target/manifest.json'))
         with open('target/manifest.json') as fp:
@@ -919,6 +930,7 @@ def assertManifestExists(self, nodes_length, sources_length):
         self.assertIn('sources', manifest)
         self.assertEqual(len(manifest['sources']), sources_length)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     def assertHasDocsGenerated(self, result, expected):
         dct = self.assertIsResult(result)
         self.assertIn('state', dct)
@@ -930,11 +942,13 @@ def assertHasDocsGenerated(self, result, expected):
         sources = dct['sources']
         self.assertEqual(set(sources), expected['sources'])
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     def assertCatalogExists(self):
         self.assertTrue(os.path.exists('target/catalog.json'))
         with open('target/catalog.json') as fp:
             catalog = json.load(fp)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     def _correct_docs_generate_result(self, result):
         expected = {
             'nodes': {
@@ -956,6 +970,7 @@ def _correct_docs_generate_result(self, result):
         self.assertCatalogExists()
         self.assertManifestExists(12, 5)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_docs_generate_postgres(self):
         self.run_dbt_with_vars(['run'])
@@ -965,6 +980,7 @@ def test_docs_generate_postgres(self):
         result = self.async_query('docs.generate').json()
         self._correct_docs_generate_result(result)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_docs_generate_postgres_cli(self):
         self.run_dbt_with_vars(['run'])
@@ -974,6 +990,7 @@ def test_docs_generate_postgres_cli(self):
         result = self.async_query('cli_args', cli='docs generate').json()
         self._correct_docs_generate_result(result)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_deps_postgres(self):
         self.async_query('deps').json()
@@ -984,10 +1001,8 @@ def test_deps_postgres_cli(self):
         self.async_query('cli_args', cli='deps').json()
 
 
-@mark.flaky(rerun_filter=addr_in_use)
 class TestRPCTaskManagement(HasRPCServer):
-
-    @mark.flaky(rerun_filter=lambda *a, **kw: True)
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_sighup_postgres(self):
         status = self.assertIsResult(self.query('status').json())
@@ -1035,6 +1050,7 @@ def test_sighup_postgres(self):
         self.assertRunning([alive])
         self.kill_and_assert(*alive)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_gc_by_time_postgres(self):
         # make a few normal requests
@@ -1055,6 +1071,7 @@ def test_gc_by_time_postgres(self):
         result = self.assertIsResult(resp)
         self.assertEqual(len(result['rows']), 0)
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_gc_by_id_postgres(self):
         # make 10 requests, then gc half of them
@@ -1095,7 +1112,6 @@ def _compare_result(self, result):
         return result['result']['state'] in ('error', 'ready')
 
 
-@mark.flaky(rerun_filter=addr_in_use)
 class TestRPCServerDeps(HasRPCServer):
     ServerProcess = CompletingServerProcess
     should_seed = False
@@ -1112,7 +1128,7 @@ def tearDown(self):
         super().tearDown()
 
     @property
-    def packages_config(selF):
+    def packages_config(self):
         return {
             # this is config-version 2, but with no upper bound
             'packages': [
@@ -1143,6 +1159,7 @@ def _check_deps_ok(self, status):
         self.assertEqual(len(os.listdir('./dbt_modules')), 1)
         self.assertIsResult(self.async_query('compile').json())
 
+    @mark.flaky(rerun_filter=addr_in_use, max_runs=3)
     @use_profile('postgres')
     def test_deps_compilation_postgres(self):
         status = self._check_start_predeps()
diff --git a/test/integration/base.py b/test/integration/base.py
--- a/test/integration/base.py
+++ b/test/integration/base.py
@@ -141,9 +141,7 @@ class DBTIntegrationTest(unittest.TestCase):
 
     @property
     def database_host(self):
-        if os.name == 'nt':
-            return 'localhost'
-        return 'database'
+        return os.environ.get('DOCKER_TEST_DATABASE_HOST', 'localhost')
 
     def postgres_profile(self):
         return {
diff --git a/test/rpc/__init__.py b/test/rpc/__init__.py
--- a/test/rpc/__init__.py
+++ b/test/rpc/__init__.py
@@ -0,0 +1,4 @@
+import os
+import pytest
+
+pytestmark = pytest.mark.skipif(os.name == 'nt', reason='"dbt rpc" not supported on windows')
diff --git a/test/rpc/conftest.py b/test/rpc/conftest.py
--- a/test/rpc/conftest.py
+++ b/test/rpc/conftest.py
@@ -1,4 +1,5 @@
 import os
+import sys
 import pytest
 import random
 import time
@@ -12,6 +13,11 @@ def pytest_addoption(parser):
         '--profile', default='postgres', help='Use the postgres profile',
     )
 
+def pytest_runtest_setup(item):
+    # this is a hack in place to work around marking tests at the module level
+    # https://github.com/pytest-dev/pytest/issues/5830
+    if os.name == 'nt':
+        pytest.skip('"dbt rpc" not supported on windows')
 
 def _get_item_profiles(item) -> Set[str]:
     supported = set()
@@ -57,8 +63,9 @@ def profiles_root(tmpdir):
 def project_root(tmpdir):
     return tmpdir.mkdir('project')
 
-
 def postgres_profile_data(unique_schema):
+    database_host = os.environ.get('DOCKER_TEST_DATABASE_HOST', 'localhost')
+
     return {
         'config': {
             'send_anonymous_usage_stats': False
@@ -68,7 +75,7 @@ def postgres_profile_data(unique_schema):
                 'default': {
                     'type': 'postgres',
                     'threads': 4,
-                    'host': 'database',
+                    'host': database_host,
                     'port': 5432,
                     'user': 'root',
                     'pass': 'password',
@@ -78,7 +85,7 @@ def postgres_profile_data(unique_schema):
                 'other_schema': {
                     'type': 'postgres',
                     'threads': 4,
-                    'host': 'database',
+                    'host': database_host,
                     'port': 5432,
                     'user': 'root',
                     'pass': 'password',
diff --git a/test/setup.sh b/test/setup.sh
deleted file mode 100755
--- a/test/setup.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-. /usr/local/bin/virtualenvwrapper.sh
-mkdir -p ~/.virtualenv
-mkvirtualenv dbt
-
-cd /usr/src/app
-
-pip install -r requirements.txt
-pip install -r dev_requirements.txt
diff --git a/test/setup_db.sh b/test/setup_db.sh
--- a/test/setup_db.sh
+++ b/test/setup_db.sh
@@ -6,7 +6,6 @@ env | grep '^PG'
 # If you want to run this script for your own postgresql (run with
 # docker-compose) it will look like this:
 # PGHOST=127.0.0.1 PGUSER=root PGPASSWORD=password PGDATABASE=postgres \
-#     bash test/setup.sh
 PGUSER="${PGUSER:-postgres}"
 export PGUSER
 PGPORT="${PGPORT:-5432}"