171 lines
5.1 KiB
Python
171 lines
5.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright 2015 Mirantis, Inc.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
from bintrees import FastRBTree
|
|
from collections import defaultdict
|
|
import functools
|
|
import operator
|
|
import six
|
|
|
|
|
|
def _make_operator(direction, op):
|
|
"""Makes search operator from low-level operation and search direction."""
|
|
return functools.partial(direction, condition=op)
|
|
|
|
|
|
def _start_upperbound(versions, version, condition):
|
|
"""Gets all versions from [start, version] that meet condition.
|
|
|
|
:param versions: the tree of versions.
|
|
:param version: the required version
|
|
:param condition: condition for search
|
|
:return: the list of found versions
|
|
"""
|
|
|
|
result = list(versions.value_slice(None, version))
|
|
try:
|
|
bound = versions.ceiling_item(version)
|
|
if condition(bound[0], version):
|
|
result.append(bound[1])
|
|
except KeyError:
|
|
pass
|
|
return result
|
|
|
|
|
|
def _lowerbound_end(versions, version, condition):
|
|
"""Gets all versions from [version, end] that meet condition.
|
|
|
|
:param versions: the tree of versions.
|
|
:param version: the required version
|
|
:param condition: condition for search
|
|
:return: the list of found versions
|
|
"""
|
|
result = []
|
|
items = iter(versions.item_slice(version, None))
|
|
bound = next(items, None)
|
|
if bound is None:
|
|
return result
|
|
if condition(bound[0], version):
|
|
result.append(bound[1])
|
|
result.extend(x[1] for x in items)
|
|
return result
|
|
|
|
|
|
def _equal(versions, version):
|
|
"""Gets the package with specified version.
|
|
|
|
:param versions: the tree of versions.
|
|
:param version: the required version
|
|
"""
|
|
value = versions.get(version, None)
|
|
return [] if value is None else [value]
|
|
|
|
|
|
def _any(versions, _):
|
|
"""Gets the package with max version.
|
|
|
|
:param versions: the tree of versions.
|
|
"""
|
|
return list(versions.values())
|
|
|
|
|
|
class Index(object):
|
|
"""The search index for packages.
|
|
|
|
Builds three search-indexes:
|
|
- index of packages with versions.
|
|
- index of virtual packages (provides).
|
|
- index of obsoleted packages (obsoletes).
|
|
|
|
Uses to find package by name and range of versions.
|
|
"""
|
|
|
|
operators = {
|
|
None: _any,
|
|
"<": _make_operator(_start_upperbound, operator.lt),
|
|
"<=": _make_operator(_start_upperbound, operator.le),
|
|
">": _make_operator(_lowerbound_end, operator.gt),
|
|
">=": _make_operator(_lowerbound_end, operator.ge),
|
|
"=": _equal,
|
|
}
|
|
|
|
def __init__(self):
|
|
self.packages = defaultdict(FastRBTree)
|
|
|
|
def __iter__(self):
|
|
"""Iterates over all packages including versions."""
|
|
return self.get_all()
|
|
|
|
def __len__(self, _reduce=six.functools.reduce):
|
|
"""Returns the total number of packages with versions."""
|
|
return _reduce(
|
|
lambda x, y: x + len(y),
|
|
six.itervalues(self.packages),
|
|
0
|
|
)
|
|
|
|
def __contains__(self, name):
|
|
"""Checks that index contains any package with such name."""
|
|
return name in self.packages
|
|
|
|
def get_all(self):
|
|
"""Gets sequence from all of packages including versions."""
|
|
|
|
for versions in six.itervalues(self.packages):
|
|
for version in versions.values():
|
|
yield version
|
|
|
|
def find_all(self, name, version_range):
|
|
"""Finds the packages by name and range of versions.
|
|
|
|
:param name: the package`s name.
|
|
:param version_range: the range of versions.
|
|
:return: the list of suitable packages
|
|
"""
|
|
if name in self.packages:
|
|
return self._find_versions(self.packages[name], version_range)
|
|
return []
|
|
|
|
def add(self, package):
|
|
"""Adds new package to indexes.
|
|
|
|
:param package: the package object.
|
|
"""
|
|
self.packages[package.name][package.version] = package
|
|
|
|
@staticmethod
|
|
def _find_versions(versions, version_range):
|
|
"""Searches accurate version.
|
|
|
|
Search for the highest version out of intersection
|
|
of existing and required range of versions.
|
|
|
|
:param versions: the existing versions
|
|
:param version_range: the required range of versions
|
|
:return: package if found, otherwise None
|
|
"""
|
|
|
|
try:
|
|
op = Index.operators[version_range.op]
|
|
except KeyError:
|
|
raise ValueError(
|
|
"Unsupported operation: {0}"
|
|
.format(version_range.op)
|
|
)
|
|
return op(versions, version_range.edge)
|