Files
along_django/venv_prod/lib/python3.12/site-packages/hashids.py

274 lines
8.6 KiB
Python

"""Implements the hashids algorithm in python. For more information, visit http://hashids.org/"""
import warnings
from functools import wraps
from math import ceil
__version__ = '1.3.1'
RATIO_SEPARATORS = 3.5
RATIO_GUARDS = 12
try:
StrType = basestring
except NameError:
StrType = str
def _is_str(candidate):
"""Returns whether a value is a string."""
return isinstance(candidate, StrType)
def _is_uint(number):
"""Returns whether a value is an unsigned integer."""
try:
return number == int(number) and number >= 0
except ValueError:
return False
def _split(string, splitters):
"""Splits a string into parts at multiple characters"""
part = ''
for character in string:
if character in splitters:
yield part
part = ''
else:
part += character
yield part
def _hash(number, alphabet):
"""Hashes `number` using the given `alphabet` sequence."""
hashed = ''
len_alphabet = len(alphabet)
while True:
hashed = alphabet[number % len_alphabet] + hashed
number //= len_alphabet
if not number:
return hashed
def _unhash(hashed, alphabet):
"""Restores a number tuple from hashed using the given `alphabet` index."""
number = 0
len_alphabet = len(alphabet)
for character in hashed:
position = alphabet.index(character)
number *= len_alphabet
number += position
return number
def _reorder(string, salt):
"""Reorders `string` according to `salt`."""
len_salt = len(salt)
if len_salt != 0:
string = list(string)
index, integer_sum = 0, 0
for i in range(len(string) - 1, 0, -1):
integer = ord(salt[index])
integer_sum += integer
j = (integer + index + integer_sum) % i
string[i], string[j] = string[j], string[i]
index = (index + 1) % len_salt
string = ''.join(string)
return string
def _index_from_ratio(dividend, divisor):
"""Returns the ceiled ratio of two numbers as int."""
return int(ceil(float(dividend) / divisor))
def _ensure_length(encoded, min_length, alphabet, guards, values_hash):
"""Ensures the minimal hash length"""
len_guards = len(guards)
guard_index = (values_hash + ord(encoded[0])) % len_guards
encoded = guards[guard_index] + encoded
if len(encoded) < min_length:
guard_index = (values_hash + ord(encoded[2])) % len_guards
encoded += guards[guard_index]
split_at = len(alphabet) // 2
while len(encoded) < min_length:
alphabet = _reorder(alphabet, alphabet)
encoded = alphabet[split_at:] + encoded + alphabet[:split_at]
excess = len(encoded) - min_length
if excess > 0:
from_index = excess // 2
encoded = encoded[from_index:from_index+min_length]
return encoded
def _encode(values, salt, min_length, alphabet, separators, guards):
"""Helper function that does the hash building without argument checks."""
len_alphabet = len(alphabet)
len_separators = len(separators)
values_hash = sum(x % (i + 100) for i, x in enumerate(values))
encoded = lottery = alphabet[values_hash % len(alphabet)]
for i, value in enumerate(values):
alphabet_salt = (lottery + salt + alphabet)[:len_alphabet]
alphabet = _reorder(alphabet, alphabet_salt)
last = _hash(value, alphabet)
encoded += last
value %= ord(last[0]) + i
encoded += separators[value % len_separators]
encoded = encoded[:-1] # cut off last separator
return (encoded if len(encoded) >= min_length else
_ensure_length(encoded, min_length, alphabet, guards, values_hash))
def _decode(hashid, salt, alphabet, separators, guards):
"""Helper method that restores the values encoded in a hashid without
argument checks."""
parts = tuple(_split(hashid, guards))
hashid = parts[1] if 2 <= len(parts) <= 3 else parts[0]
if not hashid:
return
lottery_char = hashid[0]
hashid = hashid[1:]
hash_parts = _split(hashid, separators)
for part in hash_parts:
alphabet_salt = (lottery_char + salt + alphabet)[:len(alphabet)]
alphabet = _reorder(alphabet, alphabet_salt)
yield _unhash(part, alphabet)
def _deprecated(func, name):
"""A decorator that warns about deprecation when the passed-in function is
invoked."""
@wraps(func)
def with_warning(*args, **kwargs):
warnings.warn(
('The %s method is deprecated and will be removed in v2.*.*' %
name),
DeprecationWarning
)
return func(*args, **kwargs)
return with_warning
class Hashids(object):
"""Hashes and restores values using the "hashids" algorithm."""
ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
def __init__(self, salt='', min_length=0, alphabet=ALPHABET):
"""
Initializes a Hashids object with salt, minimum length, and alphabet.
:param salt: A string influencing the generated hash ids.
:param min_length: The minimum length for generated hashes
:param alphabet: The characters to use for the generated hash ids.
"""
self._min_length = max(int(min_length), 0)
self._salt = salt
separators = ''.join(x for x in 'cfhistuCFHISTU' if x in alphabet)
alphabet = ''.join(x for i, x in enumerate(alphabet)
if alphabet.index(x) == i and x not in separators)
len_alphabet, len_separators = len(alphabet), len(separators)
if len_alphabet + len_separators < 16:
raise ValueError('Alphabet must contain at least 16 '
'unique characters.')
separators = _reorder(separators, salt)
min_separators = _index_from_ratio(len_alphabet, RATIO_SEPARATORS)
number_of_missing_separators = min_separators - len_separators
if number_of_missing_separators > 0:
separators += alphabet[:number_of_missing_separators]
alphabet = alphabet[number_of_missing_separators:]
len_alphabet = len(alphabet)
alphabet = _reorder(alphabet, salt)
num_guards = _index_from_ratio(len_alphabet, RATIO_GUARDS)
if len_alphabet < 3:
guards = separators[:num_guards]
separators = separators[num_guards:]
else:
guards = alphabet[:num_guards]
alphabet = alphabet[num_guards:]
self._alphabet = alphabet
self._guards = guards
self._separators = separators
# Support old API
self.decrypt = _deprecated(self.decode, "decrypt")
self.encrypt = _deprecated(self.encode, "encrypt")
def encode(self, *values):
"""Builds a hash from the passed `values`.
:param values The values to transform into a hashid
>>> hashids = Hashids('arbitrary salt', 16, 'abcdefghijkl0123456')
>>> hashids.encode(1, 23, 456)
'1d6216i30h53elk3'
"""
if not (values and all(_is_uint(x) for x in values)):
return ''
return _encode(values, self._salt, self._min_length, self._alphabet,
self._separators, self._guards)
def decode(self, hashid):
"""Restore a tuple of numbers from the passed `hashid`.
:param hashid The hashid to decode
>>> hashids = Hashids('arbitrary salt', 16, 'abcdefghijkl0123456')
>>> hashids.decode('1d6216i30h53elk3')
(1, 23, 456)
"""
if not hashid or not _is_str(hashid):
return ()
try:
numbers = tuple(_decode(hashid, self._salt, self._alphabet,
self._separators, self._guards))
return numbers if hashid == self.encode(*numbers) else ()
except ValueError:
return ()
def encode_hex(self, hex_str):
"""Converts a hexadecimal string (e.g. a MongoDB id) to a hashid.
:param hex_str The hexadecimal string to encodes
>>> Hashids.encode_hex('507f1f77bcf86cd799439011')
'y42LW46J9luq3Xq9XMly'
"""
numbers = (int('1' + hex_str[i:i+12], 16)
for i in range(0, len(hex_str), 12))
try:
return self.encode(*numbers)
except ValueError:
return ''
def decode_hex(self, hashid):
"""Restores a hexadecimal string (e.g. a MongoDB id) from a hashid.
:param hashid The hashid to decode
>>> Hashids.decode_hex('y42LW46J9luq3Xq9XMly')
'507f1f77bcf86cd799439011'
"""
return ''.join(('%x' % x)[1:] for x in self.decode(hashid))