4 # Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 """Utility functions with algorithms.
29 from ganeti import compat
30 from ganeti.utils import text
34 _SORTER_RE = re.compile("^%s(.*)$" % (_SORTER_GROUPS * "(\D+|\d+)?"))
35 _SORTER_DIGIT = re.compile("^\d+$")
38 def UniqueSequence(seq):
39 """Returns a list with unique elements.
41 Element order is preserved.
44 @param seq: the sequence with the source elements
46 @return: list of unique elements from seq
50 return [i for i in seq if i not in seen and not seen.add(i)]
53 def JoinDisjointDicts(dict_a, dict_b):
54 """Joins dictionaries with no conflicting keys.
56 Enforces the constraint that the two key sets must be disjoint, and then
57 merges the two dictionaries in a new dictionary that is returned to the
61 @param dict_a: the first dictionary
63 @param dict_b: the second dictionary
65 @return: a new dictionary containing all the key/value pairs contained in the
69 assert not (set(dict_a) & set(dict_b)), ("Duplicate keys found while joining"
70 " %s and %s" % (dict_a, dict_b))
71 result = dict_a.copy()
76 def FindDuplicates(seq):
77 """Identifies duplicates in a list.
79 Does not preserve element order.
82 @param seq: Sequence with source elements
84 @return: List of duplicate elements from seq
99 def _NiceSortTryInt(val):
100 """Attempts to convert a string to an integer.
103 if val and _SORTER_DIGIT.match(val):
109 def NiceSortKey(value):
110 """Extract key for sorting.
113 return [_NiceSortTryInt(grp)
114 for grp in _SORTER_RE.match(value).groups()]
117 def NiceSort(values, key=None):
118 """Sort a list of strings based on digit and non-digit groupings.
120 Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
121 will sort the list in the logical order C{['a1', 'a2', 'a10',
124 The sort algorithm breaks each name in groups of either only-digits
125 or no-digits. Only the first eight such groups are considered, and
126 after that we just use what's left of the string.
129 @param values: the names to be sorted
130 @type key: callable or None
131 @param key: function of one argument to extract a comparison key from each
132 list element, must return string
134 @return: a copy of the name list sorted with our algorithm
138 keyfunc = NiceSortKey
140 keyfunc = lambda value: NiceSortKey(key(value))
142 return sorted(values, key=keyfunc)
145 def InvertDict(dict_in):
146 """Inverts the key/value mapping of a dict.
148 @param dict_in: The dict to invert
149 @return: the inverted dict
152 return dict(zip(dict_in.values(), dict_in.keys()))
155 def InsertAtPos(src, pos, other):
156 """Inserts C{other} at given C{pos} into C{src}.
158 @note: This function does not modify C{src} in place but returns a new copy
161 @param src: The source list in which we want insert elements
163 @param pos: The position where we want to start insert C{other}
165 @param other: The other list to insert into C{src}
166 @return: A copy of C{src} with C{other} inserted at C{pos}
171 new.extend(src[pos:])
176 def SequenceToDict(seq, key=compat.fst):
177 """Converts a sequence to a dictionary with duplicate detection.
180 @param seq: Input sequence
182 @param key: Function for retrieving dictionary key from sequence element
188 duplicates = FindDuplicates(keys)
190 raise ValueError("Duplicate keys found: %s" % text.CommaJoin(duplicates))
192 assert len(keys) == len(seq)
194 return dict(zip(keys, seq))
197 def _MakeFlatToDict(data):
198 """Helper function for C{FlatToDict}.
200 This function is recursively called
202 @param data: The input data as described in C{FlatToDict}, already splitted
203 @returns: The so far converted dict
206 if not compat.fst(compat.fst(data)):
207 assert len(data) == 1, \
208 "not bottom most element, found %d elements, expected 1" % len(data)
209 return compat.snd(compat.fst(data))
211 keyfn = lambda e: compat.fst(e).pop(0)
212 return dict([(k, _MakeFlatToDict(list(g)))
213 for (k, g) in itertools.groupby(sorted(data), keyfn)])
216 def FlatToDict(data, field_sep="/"):
217 """Converts a flat structure to a fully fledged dict.
219 It accept a list of tuples in the form::
222 ("foo/bar", {"key1": "data1", "key2": "data2"}),
223 ("foo/baz", {"key3" :"data3" }),
226 where the first element is the key separated by C{field_sep}.
228 This would then return::
232 "bar": {"key1": "data1", "key2": "data2"},
233 "baz": {"key3" :"data3" },
237 @type data: list of tuple
238 @param data: Input list to convert
240 @param field_sep: The separator for the first field of the tuple
241 @returns: A dict based on the input list
244 return _MakeFlatToDict([(keys.split(field_sep), value)
245 for (keys, value) in data])
248 class RunningTimeout(object):
249 """Class to calculate remaining timeout when doing several operations.
259 def __init__(self, timeout, allow_negative, _time_fn=time.time):
260 """Initializes this class.
263 @param timeout: Timeout duration
264 @type allow_negative: bool
265 @param allow_negative: Whether to return values below zero
266 @param _time_fn: Time function for unittests
269 object.__init__(self)
271 if timeout is not None and timeout < 0.0:
272 raise ValueError("Timeout must not be negative")
274 self._timeout = timeout
275 self._allow_negative = allow_negative
276 self._time_fn = _time_fn
278 self._start_time = None
281 """Returns the remaining timeout.
284 if self._timeout is None:
287 # Get start time on first calculation
288 if self._start_time is None:
289 self._start_time = self._time_fn()
291 # Calculate remaining time
292 remaining_timeout = self._start_time + self._timeout - self._time_fn()
294 if not self._allow_negative:
295 # Ensure timeout is always >= 0
296 return max(0.0, remaining_timeout)
298 return remaining_timeout