hashids.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. """Implements the hashids algorithm in python. For more information, visit
  2. http://www.hashids.org/. Compatible with Python 2.6, 2.7 and 3"""
  3. import warnings
  4. from functools import wraps
  5. from math import ceil
  6. __version__ = '1.2.0'
  7. RATIO_SEPARATORS = 3.5
  8. RATIO_GUARDS = 12
  9. try:
  10. StrType = basestring
  11. except NameError:
  12. StrType = str
  13. def _is_str(candidate):
  14. """Returns whether a value is a string."""
  15. return isinstance(candidate, StrType)
  16. def _is_uint(number):
  17. """Returns whether a value is an unsigned integer."""
  18. try:
  19. return number == int(number) and number >= 0
  20. except ValueError:
  21. return False
  22. def _split(string, splitters):
  23. """Splits a string into parts at multiple characters"""
  24. part = ''
  25. for character in string:
  26. if character in splitters:
  27. yield part
  28. part = ''
  29. else:
  30. part += character
  31. yield part
  32. def _hash(number, alphabet):
  33. """Hashes `number` using the given `alphabet` sequence."""
  34. hashed = ''
  35. len_alphabet = len(alphabet)
  36. while True:
  37. hashed = alphabet[number % len_alphabet] + hashed
  38. number //= len_alphabet
  39. if not number:
  40. return hashed
  41. def _unhash(hashed, alphabet):
  42. """Restores a number tuple from hashed using the given `alphabet` index."""
  43. number = 0
  44. len_alphabet = len(alphabet)
  45. for character in hashed:
  46. position = alphabet.index(character)
  47. number *= len_alphabet
  48. number += position
  49. return number
  50. def _reorder(string, salt):
  51. """Reorders `string` according to `salt`."""
  52. len_salt = len(salt)
  53. if len_salt != 0:
  54. string = list(string)
  55. index, integer_sum = 0, 0
  56. for i in range(len(string) - 1, 0, -1):
  57. integer = ord(salt[index])
  58. integer_sum += integer
  59. j = (integer + index + integer_sum) % i
  60. string[i], string[j] = string[j], string[i]
  61. index = (index + 1) % len_salt
  62. string = ''.join(string)
  63. return string
  64. def _index_from_ratio(dividend, divisor):
  65. """Returns the ceiled ratio of two numbers as int."""
  66. return int(ceil(float(dividend) / divisor))
  67. def _ensure_length(encoded, min_length, alphabet, guards, values_hash):
  68. """Ensures the minimal hash length"""
  69. len_guards = len(guards)
  70. guard_index = (values_hash + ord(encoded[0])) % len_guards
  71. encoded = guards[guard_index] + encoded
  72. if len(encoded) < min_length:
  73. guard_index = (values_hash + ord(encoded[2])) % len_guards
  74. encoded += guards[guard_index]
  75. split_at = len(alphabet) // 2
  76. while len(encoded) < min_length:
  77. alphabet = _reorder(alphabet, alphabet)
  78. encoded = alphabet[split_at:] + encoded + alphabet[:split_at]
  79. excess = len(encoded) - min_length
  80. if excess > 0:
  81. from_index = excess // 2
  82. encoded = encoded[from_index:from_index+min_length]
  83. return encoded
  84. def _encode(values, salt, min_length, alphabet, separators, guards):
  85. """Helper function that does the hash building without argument checks."""
  86. len_alphabet = len(alphabet)
  87. len_separators = len(separators)
  88. values_hash = sum(x % (i + 100) for i, x in enumerate(values))
  89. encoded = lottery = alphabet[values_hash % len(alphabet)]
  90. for i, value in enumerate(values):
  91. alphabet_salt = (lottery + salt + alphabet)[:len_alphabet]
  92. alphabet = _reorder(alphabet, alphabet_salt)
  93. last = _hash(value, alphabet)
  94. encoded += last
  95. value %= ord(last[0]) + i
  96. encoded += separators[value % len_separators]
  97. encoded = encoded[:-1] # cut off last separator
  98. return (encoded if len(encoded) >= min_length else
  99. _ensure_length(encoded, min_length, alphabet, guards, values_hash))
  100. def _decode(hashid, salt, alphabet, separators, guards):
  101. """Helper method that restores the values encoded in a hashid without
  102. argument checks."""
  103. parts = tuple(_split(hashid, guards))
  104. hashid = parts[1] if 2 <= len(parts) <= 3 else parts[0]
  105. if not hashid:
  106. return
  107. lottery_char = hashid[0]
  108. hashid = hashid[1:]
  109. hash_parts = _split(hashid, separators)
  110. for part in hash_parts:
  111. alphabet_salt = (lottery_char + salt + alphabet)[:len(alphabet)]
  112. alphabet = _reorder(alphabet, alphabet_salt)
  113. yield _unhash(part, alphabet)
  114. def _deprecated(func):
  115. """A decorator that warns about deprecation when the passed-in function is
  116. invoked."""
  117. @wraps(func)
  118. def with_warning(*args, **kwargs):
  119. warnings.warn(
  120. ('The %s method is deprecated and will be removed in v2.*.*' %
  121. func.__name__),
  122. DeprecationWarning
  123. )
  124. return func(*args, **kwargs)
  125. return with_warning
  126. class Hashids(object):
  127. """Hashes and restores values using the "hashids" algorithm."""
  128. ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
  129. def __init__(self, salt='', min_length=0, alphabet=ALPHABET):
  130. """
  131. Initializes a Hashids object with salt, minimum length, and alphabet.
  132. :param salt: A string influencing the generated hash ids.
  133. :param min_length: The minimum length for generated hashes
  134. :param alphabet: The characters to use for the generated hash ids.
  135. """
  136. self._min_length = max(int(min_length), 0)
  137. self._salt = salt
  138. separators = ''.join(x for x in 'cfhistuCFHISTU' if x in alphabet)
  139. alphabet = ''.join(x for i, x in enumerate(alphabet)
  140. if alphabet.index(x) == i and x not in separators)
  141. len_alphabet, len_separators = len(alphabet), len(separators)
  142. if len_alphabet + len_separators < 16:
  143. raise ValueError('Alphabet must contain at least 16 '
  144. 'unique characters.')
  145. separators = _reorder(separators, salt)
  146. min_separators = _index_from_ratio(len_alphabet, RATIO_SEPARATORS)
  147. number_of_missing_separators = min_separators - len_separators
  148. if number_of_missing_separators > 0:
  149. separators += alphabet[:number_of_missing_separators]
  150. alphabet = alphabet[number_of_missing_separators:]
  151. len_alphabet = len(alphabet)
  152. alphabet = _reorder(alphabet, salt)
  153. num_guards = _index_from_ratio(len_alphabet, RATIO_GUARDS)
  154. if len_alphabet < 3:
  155. guards = separators[:num_guards]
  156. separators = separators[num_guards:]
  157. else:
  158. guards = alphabet[:num_guards]
  159. alphabet = alphabet[num_guards:]
  160. self._alphabet = alphabet
  161. self._guards = guards
  162. self._separators = separators
  163. # Support old API
  164. self.decrypt = _deprecated(self.decode)
  165. self.encrypt = _deprecated(self.encode)
  166. def encode(self, *values):
  167. """Builds a hash from the passed `values`.
  168. :param values The values to transform into a hashid
  169. >>> hashids = Hashids('arbitrary salt', 16, 'abcdefghijkl0123456')
  170. >>> hashids.encode(1, 23, 456)
  171. '1d6216i30h53elk3'
  172. """
  173. if not (values and all(_is_uint(x) for x in values)):
  174. return ''
  175. return _encode(values, self._salt, self._min_length, self._alphabet,
  176. self._separators, self._guards)
  177. def decode(self, hashid):
  178. """Restore a tuple of numbers from the passed `hashid`.
  179. :param hashid The hashid to decode
  180. >>> hashids = Hashids('arbitrary salt', 16, 'abcdefghijkl0123456')
  181. >>> hashids.decode('1d6216i30h53elk3')
  182. (1, 23, 456)
  183. """
  184. if not hashid or not _is_str(hashid):
  185. return ()
  186. try:
  187. numbers = tuple(_decode(hashid, self._salt, self._alphabet,
  188. self._separators, self._guards))
  189. return numbers if hashid == self.encode(*numbers) else ()
  190. except ValueError:
  191. return ()
  192. def encode_hex(self, hex_str):
  193. """Converts a hexadecimal string (e.g. a MongoDB id) to a hashid.
  194. :param hex_str The hexadecimal string to encodes
  195. >>> Hashids.encode_hex('507f1f77bcf86cd799439011')
  196. 'y42LW46J9luq3Xq9XMly'
  197. """
  198. numbers = (int('1' + hex_str[i:i+12], 16)
  199. for i in range(0, len(hex_str), 12))
  200. try:
  201. return self.encode(*numbers)
  202. except ValueError:
  203. return ''
  204. def decode_hex(self, hashid):
  205. """Restores a hexadecimal string (e.g. a MongoDB id) from a hashid.
  206. :param hashid The hashid to decode
  207. >>> Hashids.decode_hex('y42LW46J9luq3Xq9XMly')
  208. '507f1f77bcf86cd799439011'
  209. """
  210. return ''.join(('%x' % x)[1:] for x in self.decode(hashid))