Source code for hbutils.random.string

"""
Random string generation utilities for hashes, digits, and timestamps.

This module offers convenience helpers for producing random strings in
various formats, including base-N digit strings, cryptographic hash strings,
base64-encoded strings, and timestamp-prefixed hashes. Each public function
accepts an optional :class:`random.Random` instance to enable deterministic
output for testing or reproducibility.

The module provides the following public utilities:

* :func:`random_digits` - Generate random digits in any base from 2 to 36.
* :func:`random_bin_digits` - Generate random binary digits.
* :func:`random_hex_digits` - Generate random hexadecimal digits.
* :func:`random_md5` - Generate random MD5 hash string.
* :func:`random_sha1` - Generate random SHA1 hash string.
* :func:`random_base64` - Generate random base64-encoded string.
* :func:`random_md5_with_timestamp` - Generate MD5 hash with timestamp prefix.
* :func:`random_sha1_with_timestamp` - Generate SHA1 hash with timestamp prefix.

.. note::
   The random hash helpers generate random bytes first and then apply hashing
   or encoding. They are intended for unique identifiers rather than
   cryptographically secure tokens.

Example::

    >>> from hbutils.random import random_digits, random_sha1_with_timestamp
    >>> random_digits(length=8, base=16)
    '3af27b9c'
    >>> random_sha1_with_timestamp()
    '20220116233121916685_fba840b80163b55cd2295d84286a438bf8acb7c0'

"""
import io
import random
from datetime import datetime
from functools import partial
from random import _inst as _DEFAULT_RANDOM
from typing import Callable, Optional

from .binary import random_bytes
from ..encoding import md5, sha1, base64_encode

__all__ = [
    'random_digits', 'random_bin_digits', 'random_hex_digits',
    'random_md5', 'random_sha1', 'random_base64',
    'random_md5_with_timestamp', 'random_sha1_with_timestamp',
]


def _check_base(base: int) -> None:
    """
    Validate that the base is a valid integer between 2 and 36 inclusive.

    :param base: The base to validate.
    :type base: int
    :raises ValueError: If base is less than 2 or greater than 36.
    :raises TypeError: If base is not an integer.
    """
    if base < 2:
        raise ValueError(f'Base should be an integer no less than 2, but {repr(base)} found.')
    elif base > 36:
        raise ValueError(f'Base should be an integer no greater then 36, but {repr(base)} found.')
    elif not isinstance(base, int):
        raise TypeError(f'Base should be an integer, but {repr(type(base))} found.')


_0_ASCII = ord('0')
_LOWER_A_ASCII = ord('a')
_UPPER_A_ASCII = ord('A')


def _random_dchar(base: int, upper: bool, rnd: random.Random) -> str:
    """
    Generate a single random digit character in the specified base.

    :param base: The base for the digit (2-36).
    :type base: int
    :param upper: Whether to use uppercase letters for bases > 10.
    :type upper: bool
    :param rnd: Random number generator instance.
    :type rnd: random.Random
    :return: A single character representing a digit in the specified base.
    :rtype: str
    """
    _val = rnd.randint(0, base - 1)
    if _val < 10:
        _base = _0_ASCII
    elif upper:
        _base = _UPPER_A_ASCII - 10
    else:
        _base = _LOWER_A_ASCII - 10

    return chr(_base + _val)


[docs] def random_digits(length: int = 32, base: int = 10, upper: bool = False, rnd: Optional[random.Random] = None) -> str: """ Create random digits in the specified base. :param length: Length of the digits, default is 32. :type length: int :param base: Base of the digits, should be in [2, 36], default is 10. :type base: int :param upper: Upper the hex chars, default is ``False``. :type upper: bool :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random digital string. :rtype: str :raises ValueError: If ``base`` is less than 2 or greater than 36. :raises TypeError: If ``base`` is not an integer. Examples:: >>> from hbutils.random import random_digits >>> random_digits() '53518555004529024184262875530824' >>> random_digits(base=8) '77337055655313664176450107031511' >>> random_digits(48, base=8) '130107050101775254773050732461131017135371516420' """ _check_base(base) rnd = rnd or _DEFAULT_RANDOM with io.StringIO() as sio: for i in range(length): sio.write(_random_dchar(base, upper, rnd)) return sio.getvalue()
[docs] def random_bin_digits(length: int = 32, rnd: Optional[random.Random] = None) -> str: """ Create random binary digits. :param length: Length of the digits, default is 32. :type length: int :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random binary digital string. :rtype: str Examples:: >>> from hbutils.random import random_bin_digits >>> random_bin_digits() '11001011010101101100011010010011' >>> random_bin_digits(48) '010110000110101101111111011100010011101011010100' """ return random_digits(length, 2, False, rnd)
[docs] def random_hex_digits(length: int = 32, upper: bool = False, rnd: Optional[random.Random] = None) -> str: """ Create random hexadecimal digits. :param length: Length of the digits, default is 32. :type length: int :param upper: Whether to use uppercase letters, default is ``False``. :type upper: bool :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random hexadecimal digital string. :rtype: str Examples:: >>> from hbutils.random import random_hex_digits >>> random_hex_digits() 'bf4eadfb8c1700d74024833c3ce211a7' >>> random_hex_digits(upper=True) '7B85DE69A319BA132ACA27C7777A1C3E' >>> random_hex_digits(48) '7175a23730391687b7b5230c72d702a1664833a1c66783cc' """ return random_digits(length, 16, upper, rnd)
_RANDOM_BYTES_LENGTH = 64 def _random_hash(hash_func: Callable[[bytes], str], length: int = _RANDOM_BYTES_LENGTH, rnd: Optional[random.Random] = None) -> str: """ Generate a random hash string using the specified hash function. :param hash_func: The hash function to apply to random bytes. :type hash_func: callable :param length: Length of random bytes to generate, default is 64. :type length: int :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Hash string generated from random bytes. :rtype: str """ return hash_func(random_bytes(length, allow_zero=False, rnd=rnd))
[docs] def random_md5(rnd: Optional[random.Random] = None) -> str: """ Create random md5 string. :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random md5 string. :rtype: str Examples:: >>> from hbutils.random import random_md5 >>> random_md5() 'bbffd8913a7c49161ebe31b9092a9016' """ return _random_hash(md5, _RANDOM_BYTES_LENGTH, rnd)
[docs] def random_sha1(rnd: Optional[random.Random] = None) -> str: """ Create random sha1 string. :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random sha1 string. :rtype: str Examples:: >>> from hbutils.random import random_sha1 >>> random_sha1() '13135aa6b05482dcdbc1f5a25d117298571e7fab' """ return _random_hash(sha1, _RANDOM_BYTES_LENGTH, rnd)
[docs] def random_base64(length: int = _RANDOM_BYTES_LENGTH, rnd: Optional[random.Random] = None) -> str: """ Create random base64 string, may be useful when matrix verification code. :param length: Length of the original binary data, default is 64. :type length: int :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random base64 string. :rtype: str Examples:: >>> from hbutils.random import random_base64 >>> random_base64() 'PJZzHkM2-DpeXn1W9b3rp0I66MnOeD-31d2XYTA3va7N8DSNmQgvIINnvDMKWaRW-WHo_ftgKHg40z7XbDupbg==' >>> random_base64(48) 'siRZNSeytSUXlIgKYuZOzbhehhI7oabcxFDB07PkjyZ5b0DI5hGC0pqjJFlD6NGQ' """ return _random_hash(partial(base64_encode, urlsafe=True), length, rnd)
def _timestamp() -> str: """ Get current timestamp in the format 'YYYYMMDDHHMMSSffffff'. :return: Formatted timestamp string. :rtype: str """ return datetime.now().strftime("%Y%m%d%H%M%S%f")
[docs] def random_md5_with_timestamp(rnd: Optional[random.Random] = None) -> str: """ Create random md5 string with timestamp prefix. The format is '{timestamp}_{md5}' where timestamp is in 'YYYYMMDDHHMMSSffffff' format. :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random md5 string with timestamp prefix. :rtype: str Examples:: >>> from hbutils.random import random_md5_with_timestamp >>> random_md5_with_timestamp() '20220116233104357175_daf67fde4b758ff4aae21cc77f5ed689' """ return f'{_timestamp()}_{random_md5(rnd)}'
[docs] def random_sha1_with_timestamp(rnd: Optional[random.Random] = None) -> str: """ Create random sha1 string with timestamp prefix. The format is '{timestamp}_{sha1}' where timestamp is in 'YYYYMMDDHHMMSSffffff' format. :param rnd: Random object you used, default is ``None`` which means just use the default one provided by system. :type rnd: Optional[random.Random] :return: Random sha1 string with timestamp prefix. :rtype: str Examples:: >>> from hbutils.random import random_sha1_with_timestamp >>> random_sha1_with_timestamp() '20220116233121916685_fba840b80163b55cd2295d84286a438bf8acb7c0' """ return f'{_timestamp()}_{random_sha1(rnd)}'