182 lines
7.9 KiB
Python
182 lines
7.9 KiB
Python
|
import random
|
||
|
from datetime import datetime
|
||
|
from dateutil.relativedelta import relativedelta
|
||
|
from odoo.tools import pycompat
|
||
|
|
||
|
|
||
|
def Random(seed):
|
||
|
""" Return a random number generator object with the given seed. """
|
||
|
r = random.Random()
|
||
|
r.seed(seed, version=2)
|
||
|
return r
|
||
|
|
||
|
|
||
|
def format_str(val, counter, values):
|
||
|
""" Format the given value (with method ``format``) when it is a string. """
|
||
|
if isinstance(val, str):
|
||
|
return val.format(counter=counter, values=values)
|
||
|
return val
|
||
|
|
||
|
|
||
|
def chain_factories(field_factories, model_name):
|
||
|
""" Instantiate a generator by calling all the field factories. """
|
||
|
generator = root_factory()
|
||
|
for (fname, field_factory) in field_factories:
|
||
|
generator = field_factory(generator, fname, model_name)
|
||
|
return generator
|
||
|
|
||
|
|
||
|
def root_factory():
|
||
|
""" Return a generator with empty values dictionaries (except for the flag ``__complete``). """
|
||
|
yield {'__complete': False}
|
||
|
while True:
|
||
|
yield {'__complete': True}
|
||
|
|
||
|
|
||
|
def randomize(vals, weights=None, seed=False, formatter=format_str, counter_offset=0):
|
||
|
""" Return a factory for an iterator of values dicts with pseudo-randomly
|
||
|
chosen values (among ``vals``) for a field.
|
||
|
|
||
|
:param list vals: list in which a value will be chosen, depending on `weights`
|
||
|
:param list weights: list of probabilistic weights
|
||
|
:param seed: optional initialization of the random number generator
|
||
|
:param function formatter: (val, counter, values) --> formatted_value
|
||
|
:param int counter_offset:
|
||
|
:returns: function of the form (iterator, field_name, model_name) -> values
|
||
|
:rtype: function (iterator, str, str) -> dict
|
||
|
"""
|
||
|
def generate(iterator, field_name, model_name):
|
||
|
r = Random('%s+field+%s' % (model_name, seed or field_name))
|
||
|
for counter, values in enumerate(iterator):
|
||
|
val = r.choices(vals, weights)[0]
|
||
|
values[field_name] = formatter(val, counter + counter_offset, values)
|
||
|
yield values
|
||
|
return generate
|
||
|
|
||
|
|
||
|
def cartesian(vals, weights=None, seed=False, formatter=format_str, then=None):
|
||
|
""" Return a factory for an iterator of values dicts that combines all ``vals`` for
|
||
|
the field with the other field values in input.
|
||
|
|
||
|
:param list vals: list in which a value will be chosen, depending on `weights`
|
||
|
:param list weights: list of probabilistic weights
|
||
|
:param seed: optional initialization of the random number generator
|
||
|
:param function formatter: (val, counter, values) --> formatted_value
|
||
|
:param function then: if defined, factory used when vals has been consumed.
|
||
|
:returns: function of the form (iterator, field_name, model_name) -> values
|
||
|
:rtype: function (iterator, str, str) -> dict
|
||
|
"""
|
||
|
def generate(iterator, field_name, model_name):
|
||
|
counter = 0
|
||
|
for values in iterator:
|
||
|
if values['__complete']:
|
||
|
break # will consume and lose an element, (complete so a filling element). If it is a problem, use peekable instead.
|
||
|
for val in vals:
|
||
|
yield {**values, field_name: formatter(val, counter, values)}
|
||
|
counter += 1
|
||
|
factory = then or randomize(vals, weights, seed, formatter, counter)
|
||
|
yield from factory(iterator, field_name, model_name)
|
||
|
return generate
|
||
|
|
||
|
|
||
|
def iterate(vals, weights=None, seed=False, formatter=format_str, then=None):
|
||
|
""" Return a factory for an iterator of values dicts that picks a value among ``vals``
|
||
|
for each input. Once all ``vals`` have been used once, resume as ``then`` or as a
|
||
|
``randomize`` generator.
|
||
|
|
||
|
:param list vals: list in which a value will be chosen, depending on `weights`
|
||
|
:param list weights: list of probabilistic weights
|
||
|
:param seed: optional initialization of the random number generator
|
||
|
:param function formatter: (val, counter, values) --> formatted_value
|
||
|
:param function then: if defined, factory used when vals has been consumed.
|
||
|
:returns: function of the form (iterator, field_name, model_name) -> values
|
||
|
:rtype: function (iterator, str, str) -> dict
|
||
|
"""
|
||
|
def generate(iterator, field_name, model_name):
|
||
|
counter = 0
|
||
|
for val in vals: # iteratable order is important, shortest first
|
||
|
values = next(iterator)
|
||
|
values[field_name] = formatter(val, counter, values)
|
||
|
values['__complete'] = False
|
||
|
yield values
|
||
|
counter += 1
|
||
|
factory = then or randomize(vals, weights, seed, formatter, counter)
|
||
|
yield from factory(iterator, field_name, model_name)
|
||
|
return generate
|
||
|
|
||
|
|
||
|
def constant(val, formatter=format_str):
|
||
|
""" Return a factory for an iterator of values dicts that sets the field
|
||
|
to the given value in each input dict.
|
||
|
|
||
|
:returns: function of the form (iterator, field_name, model_name) -> values
|
||
|
:rtype: function (iterator, str, str) -> dict
|
||
|
"""
|
||
|
def generate(iterator, field_name, _):
|
||
|
for counter, values in enumerate(iterator):
|
||
|
values[field_name] = formatter(val, counter, values)
|
||
|
yield values
|
||
|
return generate
|
||
|
|
||
|
|
||
|
def compute(function, seed=None):
|
||
|
""" Return a factory for an iterator of values dicts that computes the field value
|
||
|
as ``function(values, counter, random)``, where ``values`` is the other field values,
|
||
|
``counter`` is an integer, and ``random`` is a pseudo-random number generator.
|
||
|
|
||
|
:param callable function: (values, counter, random) --> field_values
|
||
|
:param seed: optional initialization of the random number generator
|
||
|
:returns: function of the form (iterator, field_name, model_name) -> values
|
||
|
:rtype: function (iterator, str, str) -> dict
|
||
|
"""
|
||
|
def generate(iterator, field_name, model_name):
|
||
|
r = Random('%s+field+%s' % (model_name, seed or field_name))
|
||
|
for counter, values in enumerate(iterator):
|
||
|
val = function(values=values, counter=counter, random=r)
|
||
|
values[field_name] = val
|
||
|
yield values
|
||
|
return generate
|
||
|
|
||
|
def randint(a, b, seed=None):
|
||
|
""" Return a factory for an iterator of values dicts that sets the field
|
||
|
to a random integer between a and b included in each input dict.
|
||
|
|
||
|
:param int a: minimal random value
|
||
|
:param int b: maximal random value
|
||
|
:param int seed:
|
||
|
:returns: function of the form (iterator, field_name, model_name) -> values
|
||
|
:rtype: function (iterator, str, str) -> dict
|
||
|
"""
|
||
|
def get_rand_int(random=None, **kwargs):
|
||
|
return random.randint(a, b)
|
||
|
return compute(get_rand_int, seed=seed)
|
||
|
|
||
|
def randfloat(a, b, seed=None):
|
||
|
""" Return a factory for an iterator of values dicts that sets the field
|
||
|
to a random float between a and b included in each input dict.
|
||
|
"""
|
||
|
def get_rand_float(random=None, **kwargs):
|
||
|
return random.uniform(a, b)
|
||
|
return compute(get_rand_float, seed=seed)
|
||
|
|
||
|
def randdatetime(*, base_date=None, relative_before=None, relative_after=None, seed=None):
|
||
|
""" Return a factory for an iterator of values dicts that sets the field
|
||
|
to a random datetime between relative_before and relative_after, relatively to
|
||
|
base_date
|
||
|
|
||
|
:param datetime base_date: override the default base date if needed.
|
||
|
:param relativedelta|timedelta relative_after: range up which we can go after the
|
||
|
base date. If not set, defaults to 0, i.e. only in the past of reference.
|
||
|
:param relativedelta|timedelta relative_before: range up which we can go before the
|
||
|
base date. If not set, defaults to 0, i.e. only in the future of reference.
|
||
|
:param seed:
|
||
|
:return: iterator for random dates inside the defined range
|
||
|
"""
|
||
|
base_date = base_date or datetime(2020, 1, 1)
|
||
|
seconds_before = relative_before and ((base_date + relative_before) - base_date).total_seconds() or 0
|
||
|
seconds_after = relative_after and ((base_date + relative_after) - base_date).total_seconds() or 0
|
||
|
|
||
|
def get_rand_datetime(random=None, **kwargs):
|
||
|
return base_date + relativedelta(seconds=random.randint(int(seconds_before), int(seconds_after)))
|
||
|
return compute(get_rand_datetime, seed=seed)
|