452 lines
13 KiB
Python
452 lines
13 KiB
Python
|
from dataclasses import dataclass
|
||
|
import re
|
||
|
from typing import List
|
||
|
|
||
|
from packaging import version as packaging_version
|
||
|
|
||
|
from dbt.exceptions import VersionsNotCompatibleException
|
||
|
import dbt.utils
|
||
|
|
||
|
from dbt.dataclass_schema import dbtClassMixin, StrEnum
|
||
|
from typing import Optional
|
||
|
|
||
|
|
||
|
class Matchers(StrEnum):
|
||
|
GREATER_THAN = '>'
|
||
|
GREATER_THAN_OR_EQUAL = '>='
|
||
|
LESS_THAN = '<'
|
||
|
LESS_THAN_OR_EQUAL = '<='
|
||
|
EXACT = '='
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class VersionSpecification(dbtClassMixin):
|
||
|
major: Optional[str] = None
|
||
|
minor: Optional[str] = None
|
||
|
patch: Optional[str] = None
|
||
|
prerelease: Optional[str] = None
|
||
|
build: Optional[str] = None
|
||
|
matcher: Matchers = Matchers.EXACT
|
||
|
|
||
|
|
||
|
_MATCHERS = r"(?P<matcher>\>=|\>|\<|\<=|=)?"
|
||
|
_NUM_NO_LEADING_ZEROS = r"(0|[1-9][0-9]*)"
|
||
|
_ALPHA = r"[0-9A-Za-z-]*"
|
||
|
_ALPHA_NO_LEADING_ZEROS = r"(0|[1-9A-Za-z-][0-9A-Za-z-]*)"
|
||
|
|
||
|
_BASE_VERSION_REGEX = r"""
|
||
|
(?P<major>{num_no_leading_zeros})\.
|
||
|
(?P<minor>{num_no_leading_zeros})\.
|
||
|
(?P<patch>{num_no_leading_zeros})
|
||
|
""".format(num_no_leading_zeros=_NUM_NO_LEADING_ZEROS)
|
||
|
|
||
|
_VERSION_EXTRA_REGEX = r"""
|
||
|
(\-?
|
||
|
(?P<prerelease>
|
||
|
{alpha_no_leading_zeros}(\.{alpha_no_leading_zeros})*))?
|
||
|
(\+
|
||
|
(?P<build>
|
||
|
{alpha}(\.{alpha})*))?
|
||
|
""".format(
|
||
|
alpha_no_leading_zeros=_ALPHA_NO_LEADING_ZEROS,
|
||
|
alpha=_ALPHA)
|
||
|
|
||
|
|
||
|
_VERSION_REGEX_PAT_STR = r"""
|
||
|
^
|
||
|
{matchers}
|
||
|
{base_version_regex}
|
||
|
{version_extra_regex}
|
||
|
$
|
||
|
""".format(
|
||
|
matchers=_MATCHERS,
|
||
|
base_version_regex=_BASE_VERSION_REGEX,
|
||
|
version_extra_regex=_VERSION_EXTRA_REGEX)
|
||
|
|
||
|
_VERSION_REGEX = re.compile(_VERSION_REGEX_PAT_STR, re.VERBOSE)
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class VersionSpecifier(VersionSpecification):
|
||
|
def to_version_string(self, skip_matcher=False):
|
||
|
prerelease = ''
|
||
|
build = ''
|
||
|
matcher = ''
|
||
|
|
||
|
if self.prerelease:
|
||
|
prerelease = '-' + self.prerelease
|
||
|
|
||
|
if self.build:
|
||
|
build = '+' + self.build
|
||
|
|
||
|
if not skip_matcher:
|
||
|
matcher = self.matcher
|
||
|
return '{}{}.{}.{}{}{}'.format(
|
||
|
matcher,
|
||
|
self.major,
|
||
|
self.minor,
|
||
|
self.patch,
|
||
|
prerelease,
|
||
|
build)
|
||
|
|
||
|
@classmethod
|
||
|
def from_version_string(cls, version_string):
|
||
|
match = _VERSION_REGEX.match(version_string)
|
||
|
|
||
|
if not match:
|
||
|
raise dbt.exceptions.SemverException(
|
||
|
'Could not parse version "{}"'.format(version_string))
|
||
|
|
||
|
matched = {k: v for k, v in match.groupdict().items() if v is not None}
|
||
|
|
||
|
return cls.from_dict(matched)
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.to_version_string()
|
||
|
|
||
|
def to_range(self):
|
||
|
range_start: VersionSpecifier = UnboundedVersionSpecifier()
|
||
|
range_end: VersionSpecifier = UnboundedVersionSpecifier()
|
||
|
|
||
|
if self.matcher == Matchers.EXACT:
|
||
|
range_start = self
|
||
|
range_end = self
|
||
|
|
||
|
elif self.matcher in [Matchers.GREATER_THAN,
|
||
|
Matchers.GREATER_THAN_OR_EQUAL]:
|
||
|
range_start = self
|
||
|
|
||
|
elif self.matcher in [Matchers.LESS_THAN,
|
||
|
Matchers.LESS_THAN_OR_EQUAL]:
|
||
|
range_end = self
|
||
|
|
||
|
return VersionRange(
|
||
|
start=range_start,
|
||
|
end=range_end)
|
||
|
|
||
|
def compare(self, other):
|
||
|
if self.is_unbounded or other.is_unbounded:
|
||
|
return 0
|
||
|
|
||
|
for key in ['major', 'minor', 'patch', 'prerelease']:
|
||
|
(a, b) = (getattr(self, key), getattr(other, key))
|
||
|
if key == 'prerelease':
|
||
|
if a is None and b is None:
|
||
|
continue
|
||
|
if a is None:
|
||
|
if self.matcher == Matchers.LESS_THAN:
|
||
|
# If 'a' is not a pre-release but 'b' is, and b must be
|
||
|
# less than a, return -1 to prevent installations of
|
||
|
# pre-releases with greater base version than a
|
||
|
# maximum specified non-pre-release version.
|
||
|
return -1
|
||
|
# Otherwise, stable releases are considered greater than
|
||
|
# pre-release
|
||
|
return 1
|
||
|
if b is None:
|
||
|
return -1
|
||
|
if packaging_version.parse(a) > packaging_version.parse(b):
|
||
|
return 1
|
||
|
elif packaging_version.parse(a) < packaging_version.parse(b):
|
||
|
return -1
|
||
|
|
||
|
equal = ((self.matcher == Matchers.GREATER_THAN_OR_EQUAL and
|
||
|
other.matcher == Matchers.LESS_THAN_OR_EQUAL) or
|
||
|
(self.matcher == Matchers.LESS_THAN_OR_EQUAL and
|
||
|
other.matcher == Matchers.GREATER_THAN_OR_EQUAL))
|
||
|
if equal:
|
||
|
return 0
|
||
|
|
||
|
lt = ((self.matcher == Matchers.LESS_THAN and
|
||
|
other.matcher == Matchers.LESS_THAN_OR_EQUAL) or
|
||
|
(other.matcher == Matchers.GREATER_THAN and
|
||
|
self.matcher == Matchers.GREATER_THAN_OR_EQUAL) or
|
||
|
(self.is_upper_bound and other.is_lower_bound))
|
||
|
if lt:
|
||
|
return -1
|
||
|
|
||
|
gt = ((other.matcher == Matchers.LESS_THAN and
|
||
|
self.matcher == Matchers.LESS_THAN_OR_EQUAL) or
|
||
|
(self.matcher == Matchers.GREATER_THAN and
|
||
|
other.matcher == Matchers.GREATER_THAN_OR_EQUAL) or
|
||
|
(self.is_lower_bound and other.is_upper_bound))
|
||
|
if gt:
|
||
|
return 1
|
||
|
|
||
|
return 0
|
||
|
|
||
|
def __lt__(self, other):
|
||
|
return self.compare(other) == -1
|
||
|
|
||
|
def __gt__(self, other):
|
||
|
return self.compare(other) == 1
|
||
|
|
||
|
def __eq___(self, other):
|
||
|
return self.compare(other) == 0
|
||
|
|
||
|
def __cmp___(self, other):
|
||
|
return self.compare(other)
|
||
|
|
||
|
@property
|
||
|
def is_unbounded(self):
|
||
|
return False
|
||
|
|
||
|
@property
|
||
|
def is_lower_bound(self):
|
||
|
return self.matcher in [Matchers.GREATER_THAN,
|
||
|
Matchers.GREATER_THAN_OR_EQUAL]
|
||
|
|
||
|
@property
|
||
|
def is_upper_bound(self):
|
||
|
return self.matcher in [Matchers.LESS_THAN,
|
||
|
Matchers.LESS_THAN_OR_EQUAL]
|
||
|
|
||
|
@property
|
||
|
def is_exact(self):
|
||
|
return self.matcher == Matchers.EXACT
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class VersionRange:
|
||
|
start: VersionSpecifier
|
||
|
end: VersionSpecifier
|
||
|
|
||
|
def _try_combine_exact(self, a, b):
|
||
|
if a.compare(b) == 0:
|
||
|
return a
|
||
|
else:
|
||
|
raise VersionsNotCompatibleException()
|
||
|
|
||
|
def _try_combine_lower_bound_with_exact(self, lower, exact):
|
||
|
comparison = lower.compare(exact)
|
||
|
|
||
|
if (comparison < 0 or
|
||
|
(comparison == 0 and
|
||
|
lower.matcher == Matchers.GREATER_THAN_OR_EQUAL)):
|
||
|
return exact
|
||
|
|
||
|
raise VersionsNotCompatibleException()
|
||
|
|
||
|
def _try_combine_lower_bound(self, a, b):
|
||
|
if b.is_unbounded:
|
||
|
return a
|
||
|
elif a.is_unbounded:
|
||
|
return b
|
||
|
|
||
|
if not (a.is_exact or b.is_exact):
|
||
|
comparison = (a.compare(b) < 0)
|
||
|
|
||
|
if comparison:
|
||
|
return b
|
||
|
else:
|
||
|
return a
|
||
|
|
||
|
elif a.is_exact:
|
||
|
return self._try_combine_lower_bound_with_exact(b, a)
|
||
|
|
||
|
elif b.is_exact:
|
||
|
return self._try_combine_lower_bound_with_exact(a, b)
|
||
|
|
||
|
def _try_combine_upper_bound_with_exact(self, upper, exact):
|
||
|
comparison = upper.compare(exact)
|
||
|
|
||
|
if (comparison > 0 or
|
||
|
(comparison == 0 and
|
||
|
upper.matcher == Matchers.LESS_THAN_OR_EQUAL)):
|
||
|
return exact
|
||
|
|
||
|
raise VersionsNotCompatibleException()
|
||
|
|
||
|
def _try_combine_upper_bound(self, a, b):
|
||
|
if b.is_unbounded:
|
||
|
return a
|
||
|
elif a.is_unbounded:
|
||
|
return b
|
||
|
|
||
|
if not (a.is_exact or b.is_exact):
|
||
|
comparison = (a.compare(b) > 0)
|
||
|
|
||
|
if comparison:
|
||
|
return b
|
||
|
else:
|
||
|
return a
|
||
|
|
||
|
elif a.is_exact:
|
||
|
return self._try_combine_upper_bound_with_exact(b, a)
|
||
|
|
||
|
elif b.is_exact:
|
||
|
return self._try_combine_upper_bound_with_exact(a, b)
|
||
|
|
||
|
def reduce(self, other):
|
||
|
start = None
|
||
|
|
||
|
if(self.start.is_exact and other.start.is_exact):
|
||
|
start = end = self._try_combine_exact(self.start, other.start)
|
||
|
|
||
|
else:
|
||
|
start = self._try_combine_lower_bound(self.start, other.start)
|
||
|
end = self._try_combine_upper_bound(self.end, other.end)
|
||
|
|
||
|
if start.compare(end) > 0:
|
||
|
raise VersionsNotCompatibleException()
|
||
|
|
||
|
return VersionRange(start=start, end=end)
|
||
|
|
||
|
def __str__(self):
|
||
|
result = []
|
||
|
|
||
|
if self.start.is_unbounded and self.end.is_unbounded:
|
||
|
return 'ANY'
|
||
|
|
||
|
if not self.start.is_unbounded:
|
||
|
result.append(self.start.to_version_string())
|
||
|
|
||
|
if not self.end.is_unbounded:
|
||
|
result.append(self.end.to_version_string())
|
||
|
|
||
|
return ', '.join(result)
|
||
|
|
||
|
def to_version_string_pair(self):
|
||
|
to_return = []
|
||
|
|
||
|
if not self.start.is_unbounded:
|
||
|
to_return.append(self.start.to_version_string())
|
||
|
|
||
|
if not self.end.is_unbounded:
|
||
|
to_return.append(self.end.to_version_string())
|
||
|
|
||
|
return to_return
|
||
|
|
||
|
|
||
|
class UnboundedVersionSpecifier(VersionSpecifier):
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super().__init__(
|
||
|
matcher=Matchers.EXACT,
|
||
|
major=None,
|
||
|
minor=None,
|
||
|
patch=None,
|
||
|
prerelease=None,
|
||
|
build=None
|
||
|
)
|
||
|
|
||
|
def __str__(self):
|
||
|
return "*"
|
||
|
|
||
|
@property
|
||
|
def is_unbounded(self):
|
||
|
return True
|
||
|
|
||
|
@property
|
||
|
def is_lower_bound(self):
|
||
|
return False
|
||
|
|
||
|
@property
|
||
|
def is_upper_bound(self):
|
||
|
return False
|
||
|
|
||
|
@property
|
||
|
def is_exact(self):
|
||
|
return False
|
||
|
|
||
|
|
||
|
def reduce_versions(*args):
|
||
|
version_specifiers = []
|
||
|
|
||
|
for version in args:
|
||
|
if isinstance(version, UnboundedVersionSpecifier) or version is None:
|
||
|
continue
|
||
|
|
||
|
elif isinstance(version, VersionSpecifier):
|
||
|
version_specifiers.append(version)
|
||
|
|
||
|
elif isinstance(version, VersionRange):
|
||
|
if not isinstance(version.start, UnboundedVersionSpecifier):
|
||
|
version_specifiers.append(version.start)
|
||
|
|
||
|
if not isinstance(version.end, UnboundedVersionSpecifier):
|
||
|
version_specifiers.append(version.end)
|
||
|
|
||
|
else:
|
||
|
version_specifiers.append(
|
||
|
VersionSpecifier.from_version_string(version))
|
||
|
|
||
|
for version_specifier in version_specifiers:
|
||
|
if not isinstance(version_specifier, VersionSpecifier):
|
||
|
raise Exception(version_specifier)
|
||
|
|
||
|
if not version_specifiers:
|
||
|
return VersionRange(start=UnboundedVersionSpecifier(),
|
||
|
end=UnboundedVersionSpecifier())
|
||
|
|
||
|
try:
|
||
|
to_return = version_specifiers.pop().to_range()
|
||
|
|
||
|
for version_specifier in version_specifiers:
|
||
|
to_return = to_return.reduce(version_specifier.to_range())
|
||
|
except VersionsNotCompatibleException:
|
||
|
raise VersionsNotCompatibleException(
|
||
|
'Could not find a satisfactory version from options: {}'
|
||
|
.format([str(a) for a in args]))
|
||
|
|
||
|
return to_return
|
||
|
|
||
|
|
||
|
def versions_compatible(*args):
|
||
|
if len(args) == 1:
|
||
|
return True
|
||
|
|
||
|
try:
|
||
|
reduce_versions(*args)
|
||
|
return True
|
||
|
except VersionsNotCompatibleException:
|
||
|
return False
|
||
|
|
||
|
|
||
|
def find_possible_versions(requested_range, available_versions):
|
||
|
possible_versions = []
|
||
|
|
||
|
for version_string in available_versions:
|
||
|
version = VersionSpecifier.from_version_string(version_string)
|
||
|
|
||
|
if(versions_compatible(version,
|
||
|
requested_range.start,
|
||
|
requested_range.end)):
|
||
|
possible_versions.append(version)
|
||
|
|
||
|
sorted_versions = sorted(possible_versions, reverse=True)
|
||
|
return [v.to_version_string(skip_matcher=True) for v in sorted_versions]
|
||
|
|
||
|
|
||
|
def resolve_to_specific_version(requested_range, available_versions):
|
||
|
max_version = None
|
||
|
max_version_string = None
|
||
|
|
||
|
for version_string in available_versions:
|
||
|
version = VersionSpecifier.from_version_string(version_string)
|
||
|
|
||
|
if(versions_compatible(version,
|
||
|
requested_range.start, requested_range.end) and
|
||
|
(max_version is None or max_version.compare(version) < 0)):
|
||
|
max_version = version
|
||
|
max_version_string = version_string
|
||
|
|
||
|
return max_version_string
|
||
|
|
||
|
|
||
|
def filter_installable(
|
||
|
versions: List[str],
|
||
|
install_prerelease: bool
|
||
|
) -> List[str]:
|
||
|
installable = []
|
||
|
installable_dict = {}
|
||
|
for version_string in versions:
|
||
|
version = VersionSpecifier.from_version_string(version_string)
|
||
|
if install_prerelease or not version.prerelease:
|
||
|
installable.append(version)
|
||
|
installable_dict[str(version)] = version_string
|
||
|
sorted_installable = sorted(installable)
|
||
|
sorted_installable_original_versions = [
|
||
|
str(installable_dict.get(str(version))) for version in sorted_installable
|
||
|
]
|
||
|
return sorted_installable_original_versions
|