Skip to content

Commit 0d23993

Browse files
authored
Merge pull request #16 from brooklyn-data/environment_per_adapter_version
Switch to package-version level environment management
2 parents 8f28704 + 089e86d commit 0d23993

File tree

14 files changed

+306
-376
lines changed

14 files changed

+306
-376
lines changed

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,27 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased](https://github.com/brooklyn-data/dbtenv/compare/v1.3.2...HEAD)
8+
## [Unreleased](https://github.com/brooklyn-data/dbtenv/compare/v2.0.0a1...HEAD)
99

1010
### Added
1111

1212
### Changed
1313

1414
### Fixed
1515

16-
## [1.3.2](https://github.com/brooklyn-data/dbtenv/compare/v1.3.1...1.3.2)
16+
## [2.0.0a1](https://github.com/brooklyn-data/dbtenv/compare/v1.3.2...v2.0.0a1)
17+
18+
### Added
19+
- dbtenv now operates at the adapter-version level, introduced by dbt in version 1.0.0. The interface is identical to prior versions, dbtenv will automatically detect the needed adapter version from `profiles.yml`, or the `--adapter` argument set in a dbt command passed to `dbtenv --execute`.
20+
21+
### Changed
22+
- Dropped support for Homebrew.
23+
- Previously created environments through dbtenv cannot be used, and will be recreated by dbtenv at the adapter-version level.
24+
- dbtenv's default behaviour is not to install missing dbt adapter versions automatically. It can be disabled by setting the `DBTENV_AUTO_INSTALL` environment variable to `false`.
25+
26+
### Fixed
27+
28+
## [1.3.2](https://github.com/brooklyn-data/dbtenv/compare/v1.3.1...v1.3.2)
1729

1830
### Added
1931

README.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# dbtenv
22

3-
dbtenv lets you easily install and run multiple versions of [dbt](https://docs.getdbt.com/docs/introduction) using [pip](https://pip.pypa.io/) with [Python virtual environments](https://docs.python.org/3/library/venv.html), or optionally using [Homebrew](https://brew.sh/) on Mac or Linux.
3+
dbtenv is a version manager for dbt, automatically installing and switching to the needed adapter and version of [dbt](https://docs.getdbt.com/docs/introduction).
44

55

66
## Installation
@@ -13,23 +13,25 @@ dbtenv lets you easily install and run multiple versions of [dbt](https://docs.g
1313

1414
Run `dbtenv --help` to see some overall documentation for dbtenv, including its available sub-commands, and run `dbtenv <sub-command> --help` to see documentation for that sub-command.
1515

16-
### Using pip and/or Homebrew
17-
By default dbtenv uses [pip](https://pip.pypa.io/) to install dbt versions from the [Python Package Index](https://pypi.org/project/dbt/#history) into Python virtual environments within `~/.dbt/versions`.
18-
19-
However, on Mac or Linux systems dbtenv will automatically detect and use any version-specific dbt installations from [Homebrew](https://brew.sh/) (e.g. `[email protected]` but not plain `dbt`), and you can have dbtenv use Homebrew to install new dbt versions by setting a `DBTENV_DEFAULT_INSTALLER=homebrew` environment variable, or specifying `--installer homebrew` when running `dbtenv install`.
16+
dbtenv uses [pip](https://pip.pypa.io/) to install dbt versions from the [Python Package Index](https://pypi.org/project/dbt/#history) into Python virtual environments within `~/.dbt/versions`.
2017

2118
### Installing dbt versions
22-
You can run `dbtenv versions` to list the versions of dbt available to install, and run `dbtenv install <version>` to install a version.
19+
You can run `dbtenv versions` to list the versions of dbt available to install, and run `dbtenv install <version>` to install a specific version.
2320

24-
If you don't want to have to run `dbtenv install <version>` manually, you can set a `DBTENV_AUTO_INSTALL=true` environment variable so that as you run commands like `dbtenv version` or `dbtenv execute` any dbt version specified that isn't already installed will be installed automatically.
21+
dbtenv will automatically install the required version of dbt for the current project by default. To disable this behaviour, set the environment variable `DBTENV_AUTO_INSTALL` to `false`.
2522

26-
Some tips when dbtenv is using pip:
27-
- You can customize where the dbt version-specific Python virtual environments are created by setting `DBTENV_VENVS_DIRECTORY` and `DBTENV_VENVS_PREFIX` environment variables.
23+
Some tips:
24+
- You can customize where the dbt package-version-specific Python virtual environments are created by setting the `DBTENV_VENVS_DIRECTORY` environment variable.
2825
- You can have dbtenv only install Python packages that were actually available on the date the dbt version was released by setting a `DBTENV_SIMULATE_RELEASE_DATE=true` environment variable, or specifying `--simulate-release-date` when running `dbtenv install`.
2926
This can help if newer versions of dbt's dependencies are causing installation problems.
3027
- By default dbtenv uses whichever Python version it was installed with to install dbt, but that can be changed by setting a `DBTENV_PYTHON` environment variable to the path of a different Python executable, or specifying `--python <path>` when running `dbtenv install`.
3128

3229
### Switching between dbt versions
30+
#### Adapter type
31+
If a dbtenv command is invoked from within a dbt project, dbtenv will try to determine the in-use adapter from the default set for the project's profile in `profiles.yml`. If the `--adapter` argument is set in the dbt command passed to `dbtenv execute`, dbtenv will use that adapter's type instead.
32+
33+
#### dbt version
34+
3335
dbtenv determines which dbt version to use by trying to read it from the following sources, in this order, using the first one it finds:
3436

3537
1. The `dbtenv execute` command's optional `--dbt <version>` argument.
@@ -48,7 +50,7 @@ You can:
4850
- Run `dbtenv version --global <version>` to set the dbt version globally in the `~/.dbt/version` file.
4951
- Run `dbtenv version --local <version>` to set the dbt version for the current directory in a `.dbt_version` file.
5052

51-
### Running dbt versions
53+
### Running dbt through dbtenv
5254
Run `dbtenv execute -- <dbt arguments>` to execute the dbt version determined dynamically based on the current environment, or run `dbtenv execute --dbt <version> -- <dbt arguments>` to execute the specified dbt version.
5355

5456
For example:

dbtenv/__init__.py

Lines changed: 37 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616

1717
DEFAULT_VENVS_DIRECTORY = os.path.normpath('~/.dbt/versions')
18-
DEFAULT_VENVS_PREFIX = ''
1918

2019
GLOBAL_VERSION_FILE = os.path.normpath('~/.dbt/version')
2120
LOCAL_VERSION_FILE = '.dbt_version'
@@ -28,7 +27,6 @@
2827
QUIET_VAR = 'DBTENV_QUIET'
2928
SIMULATE_RELEASE_DATE_VAR = 'DBTENV_SIMULATE_RELEASE_DATE'
3029
VENVS_DIRECTORY_VAR = 'DBTENV_VENVS_DIRECTORY'
31-
VENVS_PREFIX_VAR = 'DBTENV_VENVS_PREFIX'
3230

3331

3432
def string_is_true(value: str) -> bool:
@@ -66,71 +64,71 @@ def get(self, name: str, default: Optional[Any] = None) -> Optional[Any]:
6664

6765
class Installer(Enum):
6866
PIP = 'pip'
69-
HOMEBREW = 'homebrew'
7067

7168
def __str__(self) -> str:
7269
return self.value
7370

7471

7572
class Version(distutils.version.LooseVersion):
76-
def __init__(self, version: str, source: Optional[str] = None, source_description: Optional[str] = None) -> None:
77-
self.pypi_version = self.homebrew_version = self.raw_version = version.strip()
78-
self.source = source
79-
if source and not source_description:
80-
self.source_description = f"set by {source}"
73+
def __init__(self, pip_specifier: str = None, adapter_type: str = None, version: str = None, source: Optional[str] = None, source_description: Optional[str] = None) -> None:
74+
if pip_specifier:
75+
self.pip_specifier = pip_specifier
76+
self.name, self.version = re.match(r"(.*)==(.*)", pip_specifier).groups()
77+
self.pypi_version = self.version
8178
else:
82-
self.source_description = source_description
79+
self.name = f"dbt-{adapter_type}"
80+
self.pypi_version = version
81+
self.pip_specifier = f"{self.name}=={self.pypi_version}"
82+
if not self.name.startswith('dbt'):
83+
raise(Exception)
84+
self.source_description = source_description
85+
self.source = source
8386

84-
version_match = re.match(r'(?P<version>\d+\.\d+\.\d+)(-?(?P<prerelease>[a-z].*))?', self.raw_version)
87+
version_match = re.match(r'(?P<version>\d+\.\d+\.\d+)(-?(?P<prerelease>[a-z].*))?', self.pypi_version)
8588
self.is_semantic = version_match is not None
8689
self.is_stable = version_match is not None and not version_match['prerelease']
8790
self.major_minor_patch = version_match['version'] if version_match is not None else None
8891
self.prerelease = version_match['prerelease'] if version_match is not None else None
8992

90-
# dbt pre-release versions are formatted slightly differently in PyPI and Homebrew.
93+
# dbt pre-release versions are formatted slightly differently.
9194
if version_match and version_match['prerelease']:
9295
self.pypi_version = f"{version_match['version']}{version_match['prerelease']}"
93-
self.homebrew_version = f"{version_match['version']}-{version_match['prerelease']}"
9496

95-
# Standardize on the PyPI version for comparison and hashing.
9697
super().__init__(self.pypi_version)
9798

9899
def __hash__(self) -> int:
99100
return self.pypi_version.__hash__()
100101

101102
def __str__(self) -> str:
102-
return self.raw_version
103+
return self.pip_specifier
103104

104105
def __repr__(self) -> str:
105-
return f"Version('{self.raw_version}')"
106+
return f"Version('{self.pip_specifier}')"
106107

107-
def _cmp(self, other: Any) -> int:
108-
# Comparing standard integer-based versions to non-standard text versions will raise a TypeError.
109-
# In such cases we'll fall back to comparing the entire version strings rather than the individual parts.
110-
try:
111-
return super()._cmp(other)
112-
except:
113-
if isinstance(other, str):
114-
return self._str_cmp(other)
115-
if isinstance(other, Version):
116-
return self._str_cmp(other.pypi_version)
117-
raise
118-
119-
def _str_cmp(self, other: str) -> int:
120-
if self.pypi_version == other:
108+
def _cmp(self, other: 'Position') -> int:
109+
if self.name < other.name:
110+
return -1
111+
if self.name > other.name:
112+
return 1
113+
if self.pypi_version == other.pypi_version:
121114
return 0
122-
if self.pypi_version < other:
115+
if self.pypi_version < other.pypi_version:
123116
return -1
124-
if self.pypi_version > other:
117+
if self.pypi_version > other.pypi_version:
125118
return 1
126119

120+
@property
121+
def source(self):
122+
return self._source
123+
124+
@source.setter
125+
def source(self, source):
126+
if source and not self.source_description:
127+
self.source_description = f"set by {source}"
128+
self._source = source
129+
127130
def get_installer_version(self, installer: Installer) -> str:
128-
if installer == Installer.PIP:
129-
return self.pypi_version
130-
elif installer == Installer.HOMEBREW:
131-
return self.homebrew_version
132-
else:
133-
return self.raw_version
131+
return self.pypi_version
134132

135133

136134
class Environment:
@@ -147,20 +145,9 @@ def __init__(self) -> None:
147145
self.project_directory = os.path.dirname(self.project_file) if self.project_file else None
148146

149147
self.venvs_directory = os.path.expanduser(self.env_vars.get(VENVS_DIRECTORY_VAR) or DEFAULT_VENVS_DIRECTORY)
150-
self.venvs_prefix = self.env_vars.get(VENVS_PREFIX_VAR) or DEFAULT_VENVS_PREFIX
151148

152149
self.global_version_file = os.path.expanduser(GLOBAL_VERSION_FILE)
153150

154-
self.homebrew_installed = False
155-
self.homebrew_prefix_directory = self.env_vars.get('HOMEBREW_PREFIX')
156-
if not self.homebrew_prefix_directory and self.os != 'Windows':
157-
brew_executable = shutil.which('brew')
158-
if brew_executable:
159-
self.homebrew_prefix_directory = os.path.dirname(os.path.dirname(brew_executable))
160-
if self.homebrew_prefix_directory and os.path.isdir(self.homebrew_prefix_directory):
161-
self.homebrew_installed = True
162-
logger.debug(f"Homebrew is installed with prefix `{self.homebrew_prefix_directory}`.")
163-
164151
_debug: Optional[bool] = None
165152

166153
@property
@@ -207,11 +194,7 @@ def update_logging_level(self) -> None:
207194

208195
@property
209196
def default_installer(self) -> Installer:
210-
if self._default_installer is None:
211-
if DEFAULT_INSTALLER_VAR in self.env_vars:
212-
self._default_installer = Installer(self.env_vars[DEFAULT_INSTALLER_VAR].lower())
213-
else:
214-
self._default_installer = Installer.PIP
197+
self._default_installer = Installer.PIP
215198

216199
return self._default_installer
217200

@@ -233,10 +216,6 @@ def primary_installer(self) -> Installer:
233216
def use_pip(self) -> bool:
234217
return not self.installer or self.installer == Installer.PIP
235218

236-
@property
237-
def use_homebrew(self) -> bool:
238-
return (not self.installer or self.installer == Installer.HOMEBREW) and self.homebrew_installed
239-
240219
_python: Optional[str] = None
241220

242221
@property
@@ -271,7 +250,7 @@ def auto_install(self) -> bool:
271250
if AUTO_INSTALL_VAR in self.env_vars:
272251
self._auto_install = string_is_true(self.env_vars[AUTO_INSTALL_VAR])
273252
else:
274-
self._auto_install = False
253+
self._auto_install = True
275254

276255
return self._auto_install
277256

dbtenv/execute.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Standard library
22
import argparse
3+
import re
34
from typing import List
45

56
# Local
@@ -34,10 +35,10 @@ def add_args_parser(self, subparsers: argparse._SubParsersAction, parent_parsers
3435
parser.add_argument(
3536
'--dbt',
3637
dest='dbt_version',
37-
type=Version,
38+
type=str,
3839
metavar='<dbt_version>',
3940
help="""
40-
Exact version of dbt to execute.
41+
dbt version to use (e.g. 1.0.1).
4142
If not specified, the dbt version will be automatically detected from the environment.
4243
"""
4344
)
@@ -53,11 +54,22 @@ def add_args_parser(self, subparsers: argparse._SubParsersAction, parent_parsers
5354
)
5455

5556
def execute(self, args: Args) -> None:
57+
arg_target_name = None
58+
for i, arg in enumerate(args.dbt_args):
59+
if arg == "--target":
60+
arg_target_name = args.dbt_args[i+1]
61+
break
62+
adapter_type = dbtenv.version.try_get_project_adapter_type(self.env.project_file, target_name=arg_target_name)
63+
if not adapter_type:
64+
logger.info("Could not determine adapter, either not running inside dbt project or no default target is set for the current project in profiles.yml.")
65+
return
66+
5667
if args.dbt_version:
57-
version = args.dbt_version
68+
version = Version(adapter_type=adapter_type, version=args.dbt_version)
5869
else:
59-
version = dbtenv.version.get_version(self.env)
60-
logger.info(f"Using dbt {version} ({version.source_description}).")
70+
arg_target_name = None
71+
version = dbtenv.version.get_version(self.env, adapter_type=adapter_type)
72+
logger.info(f"Using {version} ({version.source_description}).")
6173

6274
dbt = dbtenv.which.try_get_dbt(self.env, version)
6375
if not dbt:

0 commit comments

Comments
 (0)