Recipes to generate random instances of various types, beyond the scope of random.
"""Recipes to generate random instances of various types, beyond the scope of random."""
from random_recipes.files import *

random_recipes/__init__.py
ipear3/random-recipes
from random_recipes.objects import *


def random_dict(types: list[SupportedRandomObjects] = supported_random_objects):
    """Return a random :class:`dict` containing `types` as keys and random object instances as values."""
    d = {}
    for t in types:
        if t in object_names_to_random_function_map:
            d[t] = object_names_to_random_function_map[t]()
    return d


def random_dicts(
    n: int = 10, id_start: int = 0, id_label: str = "id", **random_dict_kwargs
):
    """Yield random :class:`dict` instances except for the `id_label` key, which will be autoincremented from `id_start`"""
    for i in range(id_start, id_start + n):
        r = {id_label: i}
        d = random_dict(**random_dict_kwargs)
        if id_label in d:
            logging.warning(
                f"Was given an id_label, {id_label} that will be overridden by `random_dict(**random_dict_kwargs)`"
            )
        r.update(**d)
        yield r

random_recipes/collections.py
ipear3/random-recipes
import csv
from pathlib import Path

from random_recipes.collections import *


def random_csv(
    file="data/random.csv",
    header: bool = True,
    n: int = 100,
    id_start: int = 1,
    id_label: str = "id",
):
    """Generate random :class:`dict` instances and write them to a csv."""
    path = Path(file)
    dicts = random_dicts(n=n, id_start=id_start, id_label=id_label)
    with open(path, "w", newline="") as text_io:
        for i, record in enumerate(dicts):
            if i == 0:
                writer = csv.DictWriter(
                    text_io, fieldnames=record.keys(), dialect="unix"
                )
                if header:
                    writer.writeheader()
            writer.writerow(record)
    return path

random_recipes/files.py
ipear3/random-recipes
"""Recipes to generate random instances of various types, beyond the scope of random."""
import collections.abc
import logging
import string
import typing
from datetime import date, datetime
from decimal import Decimal
import random
import fractions


SupportedRandomObjects = typing.Literal[
    "bool", "bytes", "decimal", "date", "datetime", "int", "float", "fraction", "str"
]
supported_random_objects = typing.get_args(SupportedRandomObjects)


def random_bool() -> bool:
    """Return a random :class:`bool`."""
    return random.choice((False, True))


def random_bytes(n: int = 1) -> bytes:
    """Generate n random :class:`bytes`. Alias of `random.randbytes`."""
    return random.randbytes(n=n)


def random_decimal(scale: int = 40, precision: int = None) -> Decimal:
    """Return a random :class:`Decimal` number `precision` digits in length.

    Arguments:
        precision: Total digits
        scale: Digits right of the decimal point.
    """
    if precision is None:
        precision = scale
    if precision < scale:
        logging.warning(
            f"Given precision {precision} is less than {scale}. Resulting Decimal will be {precision} digits."
        )
        scale = precision
    int_digits = random_str(string.digits, k=precision - scale)
    first_decimal_digits = random_str(string.digits, k=scale - 1)
    last_decimal_digit = random.randint(1, 9)
    return Decimal(f"{int_digits}.{first_decimal_digits}{last_decimal_digit}")


def random_date(_min: date = date.min, _max: date = date.max) -> date:
    """Return a random :class:`date` between `_min` and `_max`."""
    return _min + random.random() * (_max - _min)


def random_datetime(
    _min: datetime = datetime.min,
    _max: datetime = datetime.max,
) -> datetime:
    """Return a random :class:`datetime` between `_min` and `_max`."""
    return _min + random.random() * (_max - _min)


def random_int(_min: int = 0, _max: int = 9):
    """Generate a random :class:`int`. Alias of `random.randint`."""
    return random.randint(a=_min, b=_max)


def random_float(float_digits: int = 40, int_min: int = 0, int_max: int = 0) -> float:
    """Return a random :class:`float` number `k` digits in length.

    Each digit is a random digit, 0-9, except the last digit, which is random 1-9.
    """
    return random.randint(int_min, int_max) + float(
        random_str(string.digits, k=float_digits)
    )


def random_fraction(
    num_min: int = 0, num_max: int = 10, den_min: int = 10, den_max: int = 10
) -> fractions.Fraction:
    """Return a random :class:`fractions.Fraction`."""
    return fractions.Fraction(
        random.randint(num_min, num_max), random.randint(den_min, den_max)
    )


def random_str(
    characters: collections.abc.Sequence = string.printable,
    relative_weights: collections.abc.Sequence[float | fractions.Fraction] = None,
    cumulative_weights: collections.abc.Sequence[float | fractions.Fraction] = None,
    k: int = 1,
) -> str:
    """Return a random :class:`str` with `k` `characters` chosen.

    Nearly an alias of `random.choices()` - elements are chosen from characters with replacement.

    If the relative weights or cumulative weights are not specified, the selections are made with equal probability.
    """
    return "".join(
        random.choices(
            population=characters,
            weights=relative_weights,
            cum_weights=cumulative_weights,
            k=k,
        )
    )


object_names_to_random_function_map: dict[str, typing.Callable[[], object]] = {
    "bool": random_bool,
    "bytes": random_bytes,
    "decimal": random_decimal,
    "date": random_date,
    "datetime": random_datetime,
    "int": random_int,
    "float": random_float,
    "fraction": random_fraction,
    "str": random_str,
}

random_recipes/objects.py
ipear3/random-recipes
from random_recipes.collections import *


class TestRandomDict:
    def test_random_dict_is_dict(self):
        assert isinstance(random_dict(), dict)


class TestRandomDicts:
    def test_random_dicts_is_dicts(self):
        assert isinstance(random_dicts(), collections.abc.Iterable)

tests/test_collections.py
ipear3/random-recipes
from random_recipes.files import *


class TestRandomCSV:
    def test_random_csv_is_csv(self):
        file = Path("data/random.csv")
        file.parent.mkdir(exist_ok=True, parents=True)
        file = random_csv(file=file)
        with open(file, newline="") as file:
            reader = csv.reader(file, dialect="unix")
            for row in reader:
                print(row)
            ok = True
        assert ok

tests/test_files.py
ipear3/random-recipes
from random_recipes.objects import *


class TestRandomBool:
    def test_random_bool_is_bool(self):
        assert isinstance(random_bool(), bool)


class TestRandomBytes:
    def test_random_bytes_is_bytes(self):
        assert isinstance(random_bytes(), bytes)


class TestRandomDecimal:
    def test_random_decimal_is_decimal(self):
        assert isinstance(random_decimal(), Decimal)

    def test_random_decimal_is_less_than_1_if_precision_eq_scale(self):
        assert random_decimal(scale=2, precision=2) < 1

    def test_random_decimal_is_given_scale(self):
        scale = 3
        assert len(str(random_decimal(scale=scale)).split(".")[1]) == scale

    def test_random_decimal_is_given_precision(self):
        precision = 3
        decimal_str = str(random_decimal(precision=precision))
        assert (
            len(decimal_str) == precision + 1 + 1 if decimal_str.startswith("0") else 0
        )


class TestRandomDate:
    def test_random_date_is_date(self):
        assert isinstance(random_date(), date)


class TestRandomDatetime:
    def test_random_datetime_is_datetime(self):
        assert isinstance(random_datetime(), datetime)


class TestRandomFloat:
    def test_random_float_is_float(self):
        assert isinstance(random_float(), float)


class TestRandomFraction:
    def test_random_fraction_is_fraction(self):
        assert isinstance(random_fraction(), fractions.Fraction)


class TestRandomStr:
    def test_random_str_is_str(self):
        assert isinstance(random_str(), str)

tests/test_objects.py
ipear3/random-recipes