SemVer utility to store object versions in DB
Adds an utility which may convert Version objects (provided by semantic_version library) into a combination of long 64-bit integer value and two preformatted strings. If stored in the database these values unambiguously define object precedence according to semver notation. This may be used for correct ordering of database queries. Adds a class which is compatible with SQLAlchemy composite field, so one may use it directly in SQLAlchemy object models to define version fields and query the database. Needed for Artifacts model Depends-on: I74c00625634f246a96a1a9db4e6ff4335e649404 Implements-blueprint: semver-support Change-Id: I33212ea92cca50a143b9141f8147f7db27bb9e7c
This commit is contained in:
parent
7217bd35c8
commit
c88781ecc3
@ -434,3 +434,7 @@ class MetadefTagNotFound(NotFound):
|
||||
message = _("The metadata definition tag with"
|
||||
" name=%(name)s was not found in"
|
||||
" namespace=%(namespace_name)s.")
|
||||
|
||||
|
||||
class InvalidVersion(Invalid):
|
||||
message = _("Version is invalid: %(reason)s")
|
||||
|
144
glance/common/semver_db.py
Normal file
144
glance/common/semver_db.py
Normal file
@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2015 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import semantic_version
|
||||
|
||||
from glance.common import exception
|
||||
from glance import i18n
|
||||
|
||||
MAX_COMPONENT_LENGTH = pow(2, 16) - 1
|
||||
MAX_NUMERIC_PRERELEASE_LENGTH = 6
|
||||
|
||||
_ = i18n._
|
||||
|
||||
|
||||
class DBVersion(object):
|
||||
def __init__(self, components_long, prerelease, build):
|
||||
"""
|
||||
Creates a DBVersion object out of 3 component fields. This initializer
|
||||
is supposed to be called from SQLAlchemy if 3 database columns are
|
||||
mapped to this composite field.
|
||||
|
||||
:param components_long: a 64-bit long value, containing numeric
|
||||
components of the version
|
||||
:param prerelease: a prerelease label of the version, optionally
|
||||
preformatted with leading zeroes in numeric-only parts of the label
|
||||
:param build: a build label of the version
|
||||
"""
|
||||
version_string = '%s.%s.%s' % _long_to_components(components_long)
|
||||
if prerelease:
|
||||
version_string += '-' + _strip_leading_zeroes_from_prerelease(
|
||||
prerelease)
|
||||
|
||||
if build:
|
||||
version_string += '+' + build
|
||||
self.version = semantic_version.Version(version_string)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.version)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, DBVersion) and
|
||||
other.version == self.version)
|
||||
|
||||
def __ne__(self, other):
|
||||
return (not isinstance(other, DBVersion)
|
||||
or self.version != other.version)
|
||||
|
||||
def __composite_values__(self):
|
||||
long_version = _version_to_long(self.version)
|
||||
prerelease = _add_leading_zeroes_to_prerelease(self.version.prerelease)
|
||||
build = '.'.join(self.version.build) if self.version.build else None
|
||||
return long_version, prerelease, build
|
||||
|
||||
|
||||
def parse(version_string):
|
||||
version = semantic_version.Version.coerce(version_string)
|
||||
return DBVersion(_version_to_long(version),
|
||||
'.'.join(version.prerelease),
|
||||
'.'.join(version.build))
|
||||
|
||||
|
||||
def _check_limit(value):
|
||||
if value > MAX_COMPONENT_LENGTH:
|
||||
reason = _("Version component is too "
|
||||
"large (%d max)") % MAX_COMPONENT_LENGTH
|
||||
raise exception.InvalidVersion(reason=reason)
|
||||
|
||||
|
||||
def _version_to_long(version):
|
||||
"""
|
||||
Converts the numeric part of the semver version into the 64-bit long value
|
||||
using the following logic:
|
||||
|
||||
* major version is stored in first 16 bits of the value
|
||||
* minor version is stored in next 16 bits
|
||||
* patch version is stored in following 16 bits
|
||||
* next 2 bits are used to store the flag: if the version has pre-release
|
||||
label then these bits are 00, otherwise they are 11. Intermediate values
|
||||
of the flag (01 and 10) are reserved for future usage.
|
||||
* last 14 bits of the value are reserved fo future usage
|
||||
|
||||
The numeric components of version are checked so their value do not exceed
|
||||
16 bits.
|
||||
|
||||
:param version: a semantic_version.Version object
|
||||
"""
|
||||
_check_limit(version.major)
|
||||
_check_limit(version.minor)
|
||||
_check_limit(version.patch)
|
||||
major = version.major << 48
|
||||
minor = version.minor << 32
|
||||
patch = version.patch << 16
|
||||
flag = 0 if version.prerelease else 2
|
||||
flag <<= 14
|
||||
return major | minor | patch | flag
|
||||
|
||||
|
||||
def _long_to_components(value):
|
||||
major = value >> 48
|
||||
minor = (value - (major << 48)) >> 32
|
||||
patch = (value - (major << 48) - (minor << 32)) >> 16
|
||||
return str(major), str(minor), str(patch)
|
||||
|
||||
|
||||
def _add_leading_zeroes_to_prerelease(label_tuple):
|
||||
if label_tuple is None:
|
||||
return None
|
||||
res = []
|
||||
for component in label_tuple:
|
||||
if component.isdigit():
|
||||
if len(component) > MAX_NUMERIC_PRERELEASE_LENGTH:
|
||||
reason = _("Prerelease numeric component is too large "
|
||||
"(%d characters "
|
||||
"max)") % MAX_NUMERIC_PRERELEASE_LENGTH
|
||||
raise exception.InvalidVersion(reason=reason)
|
||||
res.append(component.rjust(MAX_NUMERIC_PRERELEASE_LENGTH, '0'))
|
||||
else:
|
||||
res.append(component)
|
||||
return '.'.join(res)
|
||||
|
||||
|
||||
def _strip_leading_zeroes_from_prerelease(string_value):
|
||||
res = []
|
||||
for component in string_value.split('.'):
|
||||
if component.isdigit():
|
||||
val = component.lstrip('0')
|
||||
if len(val) == 0: # Corner case: when the component is just '0'
|
||||
val = '0' # it will be stripped completely, so restore it
|
||||
res.append(val)
|
||||
else:
|
||||
res.append(component)
|
||||
return '.'.join(res)
|
77
glance/tests/unit/common/test_semver.py
Normal file
77
glance/tests/unit/common/test_semver.py
Normal file
@ -0,0 +1,77 @@
|
||||
# Copyright (c) 2015 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import semver_db
|
||||
from glance.tests import utils as test_utils
|
||||
|
||||
|
||||
class SemVerTestCase(test_utils.BaseTestCase):
|
||||
def test_long_conversion(self):
|
||||
initial = '1.2.3-beta+07.17.2014'
|
||||
v = semver_db.parse(initial)
|
||||
l, prerelease, build = v.__composite_values__()
|
||||
v2 = semver_db.DBVersion(l, prerelease, build)
|
||||
self.assertEqual(initial, str(v2))
|
||||
|
||||
def test_major_comparison_as_long(self):
|
||||
v1 = semver_db.parse("1.1.100")
|
||||
v2 = semver_db.parse("2.0.0")
|
||||
self.assertTrue(v2.__composite_values__()[0] >
|
||||
v1.__composite_values__()[0])
|
||||
|
||||
def test_minor_comparison_as_long(self):
|
||||
v1 = semver_db.parse("1.1.100")
|
||||
v2 = semver_db.parse("2.0.0")
|
||||
self.assertTrue(v2.__composite_values__()[0] >
|
||||
v1.__composite_values__()[0])
|
||||
|
||||
def test_patch_comparison_as_long(self):
|
||||
v1 = semver_db.parse("1.1.1")
|
||||
v2 = semver_db.parse("1.1.100")
|
||||
self.assertTrue(v2.__composite_values__()[0] >
|
||||
v1.__composite_values__()[0])
|
||||
|
||||
def test_label_comparison_as_long(self):
|
||||
v1 = semver_db.parse("1.1.1-alpha")
|
||||
v2 = semver_db.parse("1.1.1")
|
||||
self.assertTrue(v2.__composite_values__()[0] >
|
||||
v1.__composite_values__()[0])
|
||||
|
||||
def test_label_comparison_as_string(self):
|
||||
versions = [
|
||||
semver_db.parse("1.1.1-0.10.a.23.y.255").__composite_values__()[1],
|
||||
semver_db.parse("1.1.1-0.10.z.23.x.255").__composite_values__()[1],
|
||||
semver_db.parse("1.1.1-0.10.z.23.y.255").__composite_values__()[1],
|
||||
semver_db.parse("1.1.1-0.10.z.23.y.256").__composite_values__()[1],
|
||||
semver_db.parse("1.1.1-0.10.z.24.y.255").__composite_values__()[1],
|
||||
semver_db.parse("1.1.1-0.11.z.24.y.255").__composite_values__()[1],
|
||||
semver_db.parse("1.1.1-1.11.z.24.y.255").__composite_values__()[1],
|
||||
semver_db.parse("1.1.1-alp.1.2.3.4.5.6").__composite_values__()[1]]
|
||||
for i in xrange(len(versions) - 1):
|
||||
self.assertLess(versions[i], versions[i + 1])
|
||||
|
||||
def test_too_large_version(self):
|
||||
version1 = '1.1.65536'
|
||||
version2 = '1.65536.1'
|
||||
version3 = '65536.1.1'
|
||||
self.assertRaises(exception.InvalidVersion, semver_db.parse, version1)
|
||||
self.assertRaises(exception.InvalidVersion, semver_db.parse, version2)
|
||||
self.assertRaises(exception.InvalidVersion, semver_db.parse, version3)
|
||||
|
||||
def test_too_long_numeric_segments(self):
|
||||
version = semver_db.parse('1.0.0-alpha.1234567')
|
||||
self.assertRaises(exception.InvalidVersion,
|
||||
version.__composite_values__)
|
@ -45,7 +45,7 @@ Paste
|
||||
jsonschema>=2.0.0,<3.0.0
|
||||
python-keystoneclient>=1.1.0
|
||||
pyOpenSSL>=0.11
|
||||
|
||||
semantic_version>=2.3.1
|
||||
# Required by openstack.common libraries
|
||||
six>=1.9.0
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user