summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier-Do <xdo@odoo.com>2020-03-27 10:22:37 +0000
committerXavier-Do <xdo@odoo.com>2020-03-30 21:30:13 +0000
commitf545fd274d7692a7d206aae50d5a68546bafe2bb (patch)
tree2d386cb563c454e37806570a399cb8555afc4f53
parent2dc4ef69c145b02250ef3ebda2a7c11f90fe9423 (diff)
[IMP] core, base: add tooling to populate database
Some use case like testing performance or upgrade scripts required a database with prefilled data, covering basic corner cases. A solution can be to create data a procedural way. This commit proposes an API to easily populate a model, usually by giving a list of possible values for each field or by giving a compute method that will be based on raw values of other fields. The basic way to define how to populate a new field is to override `_populate_factories`, a method that returns a sequence of pairs `(field_name, factory)`. The definition of a field is a "factory", a function that returns a neverending iterator combining its value(s) with the values of the iterator given in parameter. Some factory helpers are given in `tools.populate.py`: - `iterate(vals, weighs)` ensures that one record is created for each value by iterating on them, then resumes as `random.choice` on those vals following weights once the first iteration is finished. - `cartesian(vals, weights)` makes a cartesian product of its own values with the values of its input iterator, then resumes as a randomized generator. - `compute(function)` calls the given function with the current values dict and a random object, and assigns the current field to the returned value. - ... Each iterator yields dictionaries of field values, and the factory should add a value for the current field(s). The yielded dictionaries also contain a pseudo_field `"__complete"`, that indicates whether this step is some randomized data to reach the expected count of records. A falsy value indicates that the iterator is still covering mandatory cases. This indicates whether a cartesian product is finished, or an `iterate` has consumed all its values. The order of the factories is quite important, since some computed fields may need other fields to be defined, and `cartesian` factories should always be at the beginning to avoid having too many combination. That is why the factories are given as a list of pairs instead of a dictionary; this makes it easier to insert elements at any place. Example: field A: cartesian([T, F]) field B: cartesian([0, 1]) field C: iterate([a, b, c, d, e]) field D: compute(1-B) _c is shortcut for __complete _ is a random value, or result of a random value ``` iter | root | field A | field B | field C | field D | result 0 {_c:F} {... A:T} {... B:0} {...C:a} {...D:1} T,0,a,1 complete:False {... B:1} {...C:b} {...D:0} T,1,b,0 complete:False {... A:F} {... B:0} {...C:c} {...D:1} F,0,c,1 complete:False {... B:1} {...C:d} {...D:0} F,1,d,0 complete:False 1 {_c:T} {... A:_} {... B:_} {...C:e,_c:F} {...D:_} _,_,e,_ complete:False 2 {_c:T} {... A:_} {... B:_} {...C:_} {...D:_} _,_,_,_ complete:True ``` X-original-commit: 4c0182dafa584853ed83a166096f45c33c06a245
-rw-r--r--odoo/addons/test_populate/__init__.py2
-rw-r--r--odoo/addons/test_populate/__manifest__.py13
-rw-r--r--odoo/addons/test_populate/ir.model.access.csv5
-rw-r--r--odoo/addons/test_populate/models.py94
-rw-r--r--odoo/addons/test_populate/tests/__init__.py4
-rw-r--r--odoo/addons/test_populate/tests/test_populate.py157
-rw-r--r--odoo/cli/__init__.py1
-rw-r--r--odoo/cli/populate.py88
-rw-r--r--odoo/models.py61
-rw-r--r--odoo/tools/config.py5
-rw-r--r--odoo/tools/populate.py113
11 files changed, 542 insertions, 1 deletions
diff --git a/odoo/addons/test_populate/__init__.py b/odoo/addons/test_populate/__init__.py
new file mode 100644
index 00000000000..a0fdc10fe11
--- /dev/null
+++ b/odoo/addons/test_populate/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import models
diff --git a/odoo/addons/test_populate/__manifest__.py b/odoo/addons/test_populate/__manifest__.py
new file mode 100644
index 00000000000..a19d973a142
--- /dev/null
+++ b/odoo/addons/test_populate/__manifest__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'test-populate',
+ 'version': '0.1',
+ 'category': 'Tests',
+ 'description': """A module to test populate.""",
+ 'depends': ['base'],
+ 'data': [
+ 'ir.model.access.csv',
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/odoo/addons/test_populate/ir.model.access.csv b/odoo/addons/test_populate/ir.model.access.csv
new file mode 100644
index 00000000000..4be68aa1702
--- /dev/null
+++ b/odoo/addons/test_populate/ir.model.access.csv
@@ -0,0 +1,5 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+access_test_populate,access_test_populate,model_test_populate,,1,1,1,1
+access_test_populate_category,access_test_populate_category,model_test_populate_category,,1,1,1,1
+access_test_no_populat,access_test_no_populat,model_test_no_populate,,1,1,1,1
+access_test_populate_inherit,access_test_populate_inherit,model_test_populate_inherit,,1,1,1,1
diff --git a/odoo/addons/test_populate/models.py b/odoo/addons/test_populate/models.py
new file mode 100644
index 00000000000..d59524d185b
--- /dev/null
+++ b/odoo/addons/test_populate/models.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, fields, api
+
+from odoo.tools import populate, pycompat
+
+
+class TestPopulateModel(models.Model):
+ _name = 'test.populate'
+ _description = 'Test Populate'
+
+ name = fields.Char(default='Foo')
+ state = fields.Selection([('a', 'A'), ('b', 'B')], default='a')
+ active = fields.Boolean('Active', default=True)
+ category_id = fields.Many2one('test.populate.category', 'Category')
+ some_ref = fields.Integer('Reference')
+ dependant_field_1 = fields.Char('Dependant 1')
+ dependant_field_2 = fields.Char('Dependant 2')
+
+ _populate_dependencies = ['test.populate.category']
+
+ _populate_sizes = {
+ 'small': 20,
+ 'medium': 30,
+ 'large': 100,
+ }
+
+ def _populate_factories(self):
+
+ # cross dependant field in a sub generator, cartesian product of two fields
+ dependant_factories = [
+ ('dependant_field_1', populate.cartesian(['d1_1', 'd1_2'])),
+ ('dependant_field_2', populate.cartesian(['d2_1', 'd2_2', 'd2_3_{counter}'])),
+ ]
+ def generate_dependant(iterator, *args):
+ dependants_generator = populate.chain_factories(dependant_factories, self._name)
+ for values in dependants_generator:
+ dependant_values = next(iterator)
+ yield {**values, **dependant_values, '__complete': values['__complete'] and dependant_values['__complete']}
+
+ def get_name(values=None, counter=0, **kwargs):
+ active = 'active' if values['active'] else 'inactive'
+ cat = 'filling' if values['__complete'] else 'corner'
+ return '%s_%s_%s' % (active, cat, counter)
+
+ category_ids = self.env.registry.populated_models['test.populate.category']
+
+ return [
+ ('active', populate.cartesian([True, False], [3, 1])),
+ ('state', populate.cartesian([False] + self.env['test.populate']._fields['state'].get_values(self.env))),
+ ('some_ref', populate.iterate([False, 1, 2, 3, 4])),
+ ('_dependant', generate_dependant),
+ ('name', populate.compute(get_name)),
+ ('category_id', populate.randomize([False] + category_ids)),
+ ]
+
+class TestPopulateDependencyModel(models.Model):
+ _name = 'test.populate.category'
+ _description = 'Test Populate Category'
+
+ _populate_sizes = {
+ 'small': 3,
+ 'medium': 10,
+ 'large': 20,
+ }
+ name = fields.Char('Name', required=True, default='Cat1')
+ active = fields.Boolean('Active', default=True)
+
+ def _populate_factories(self):
+ return [
+ ('active', populate.cartesian([True, False], [9, 1])),
+ ('name', populate.cartesian(['Cat1', 'Cat2', 'Cat3'])),
+ ]
+
+class TestNoPopulateModelInherit(models.Model):
+ _name = 'test.populate.inherit'
+ _inherit = 'test.populate'
+
+ _description = 'Test populate inherit'
+
+ additionnal_field = fields.Char(required=True)
+
+ def _populate_factories(self):
+ return super()._populate_factories() + [
+ ('additionnal_field', populate.iterate(['V1', 'V2', 'V3'])),
+ ]
+
+
+class TestNoPopulateModel(models.Model):
+ _name = 'test.no.populate'
+ _description = 'A model with no populate method and a required field, should not crash'
+
+ name = fields.Char(required=True)
diff --git a/odoo/addons/test_populate/tests/__init__.py b/odoo/addons/test_populate/tests/__init__.py
new file mode 100644
index 00000000000..a77cffb103d
--- /dev/null
+++ b/odoo/addons/test_populate/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import test_populate
+
diff --git a/odoo/addons/test_populate/tests/test_populate.py b/odoo/addons/test_populate/tests/test_populate.py
new file mode 100644
index 00000000000..09243fefdea
--- /dev/null
+++ b/odoo/addons/test_populate/tests/test_populate.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import collections
+import logging
+from odoo.tests import common
+from odoo.cli.populate import Populate
+from odoo.tools import mute_logger, populate
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
+
+
+_logger = logging.getLogger(__name__)
+
+# todo patch cursor commit
+class TestPopulate(common.TransactionCase):
+ def setUp(self):
+ super(TestPopulate, self).setUp()
+ patcher = patch.object(self.cr, 'commit')
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ def test_dependency(self):
+ ordered_models = Populate._get_ordered_models(self.env, ['test.populate'])
+ ordered_models_names = [model._name for model in ordered_models]
+ self.assertEqual(ordered_models_names, ['test.populate.category', 'test.populate'])
+
+ @mute_logger('odoo.cli.populate')
+ def test_no_populate(self):
+ """ Check that model with no populate method are not populated"""
+ model = 'test.no.populate'
+ populated = Populate.populate(self.env, 'small', [model])
+ new = populated[model]
+ self.assertFalse(new)
+
+ @mute_logger('odoo.cli.populate')
+ def test_populate(self):
+ """ Check that model with populate methods are correctly populated"""
+ model = 'test.populate'
+ populated = Populate.populate(self.env, 'small', [model])
+ records = self.check_test_populate_values(populated, model)
+
+ # pseudo random after cartesian with ~ 1/4 False, 3/4 True
+ # seed is model dependant
+ self.assertEqual(records.mapped('active')[6:20], [
+ True, True, True, True, True, True, False, False, True, True, True, True, False, True
+ ])
+ # pseudo random after iterate
+ self.assertEqual(records.mapped('some_ref')[5:20], [
+ 1, 0, 2, 4, 4, 3, 4, 1, 2, 2, 2, 4, 4, 1, 2
+ ])
+
+ @mute_logger('odoo.cli.populate')
+ def test_populate_inherit(self):
+ """ Check that model with populate methods are correctly populated"""
+ model = 'test.populate.inherit'
+ populated = Populate.populate(self.env, 'small', [model])
+ records = self.check_test_populate_values(populated, model) # should be same values as base class
+ # and additionnal_field has own values set
+ # iterate then pseudo random
+ self.assertEqual(records.mapped('additionnal_field')[:20], [
+ 'V1', 'V2', 'V3', # iterate
+ 'V3', 'V1', 'V2', 'V1', 'V2', 'V1', 'V2', 'V2', 'V2', 'V1', 'V1', 'V3', 'V1', 'V2', 'V2', 'V3', 'V2' # pseudorandom
+ ])
+
+ def check_test_populate_values(self, populated, model):
+ new = populated[model]
+ self.assertTrue(new)
+ records = self.env[model].browse(new)
+ # main cartesian product
+ self.assertEqual(records.mapped('active')[:6], [
+ True, True, True,
+ False, False, False,
+ ])
+ self.assertEqual(records.mapped('state')[:6], [
+ False, 'a', 'b',
+ False, 'a', 'b',
+ ])
+
+ # custom name call
+ self.assertEqual(records.mapped('name')[:6], [
+ 'active_corner_0', 'active_corner_1', 'active_corner_2',
+ 'inactive_corner_3', 'inactive_corner_4', 'inactive_corner_5',
+ ])
+ self.assertIn('filling', records.mapped('name')[6]) # filling when cartesian and iterate are done
+ # iterate then pseudo random
+ self.assertEqual(records.mapped('some_ref')[:5], [
+ 0, 1, 2, 3, 4 # iterate
+ ])
+
+ # some custom multi field generator (as cartesian product in this example)
+ self.assertEqual(records.mapped('dependant_field_1')[:6], [
+ 'd1_1', 'd1_1', 'd1_1',
+ 'd1_2', 'd1_2', 'd1_2'
+ ])
+ self.assertEqual(records.mapped('dependant_field_2')[:6], [
+ 'd2_1', 'd2_2', 'd2_3_0',
+ 'd2_1', 'd2_2', 'd2_3_1'
+ ])
+ used_category_ids = set(records.mapped('category_id').ids[:20])
+ self.assertEqual(len(used_category_ids), 6) # event if id may change, with given seed, the 6 category are used
+ generated_category_ids = set(populated['test.populate.category'])
+ self.assertFalse(used_category_ids-generated_category_ids) # all category are the generated one
+ self.assertFalse(hasattr(self.env.registry, 'populated_models'), 'populated_models flag has been removed from registry')
+
+ return records
+
+
+@common.tagged('-at_install', 'post_install')
+class TestPopulateValidation(common.TransactionCase):
+ """ check that all fields in _populate_factories exists """
+ def setUp(self):
+ super(TestPopulateValidation, self).setUp()
+ self.env.registry.populated_models = collections.defaultdict(list)
+ self.addCleanup(delattr, self.env.registry, 'populated_models')
+
+ def test_populate_factories(self):
+ for model in self.env.values():
+ factories = model._populate_factories() or []
+ factories_fields = set([field_name for field_name, factory in factories if not field_name.startswith('_')])
+ missing = factories_fields - model._fields.keys()
+ self.assertFalse(missing, 'Fields %s not found in model %s' % (missing, model._name))
+
+
+@common.tagged('-standard', '-at_install', 'post_install', 'missing_populate')
+class TestPopulateMissing(common.TransactionCase):
+ """ check that all fields in _populate_factories exists """
+ def setUp(self):
+ super(TestPopulateMissing, self).setUp()
+ self.env.registry.populated_models = collections.defaultdict(list)
+ self.addCleanup(delattr, self.env.registry, 'populated_models')
+
+ def test_populate_missing_factories(self):
+ no_factory_models = []
+ for model in self.env.values():
+ factories = model._populate_factories()
+ if not factories:
+ if model._transient or model._abstract:
+ continue
+ ir_model = self.env['ir.model'].search([('model', '=', model._name)])
+ if all(module.startswith('test_') for module in ir_model.modules.split(',')):
+ continue
+ no_factory_models.append(model._name)
+ else:
+ factories_fields = next(populate.chain_factories(factories, model._name)).keys()
+ def is_electable(field):
+ return not field.compute \
+ and field.store \
+ and field.name not in ('create_uid', 'write_uid', 'write_date', 'create_date', 'id') \
+ and field.type not in ('many2many', 'one2many')
+ electable_fields = set([key for key, field in model._fields.items() if is_electable(field)])
+ no_factory_fields = set(electable_fields - factories_fields)
+ if no_factory_fields:
+ _logger.info('Model %s has some undefined field: %s', model._name, no_factory_fields)
+
+ _logger.info('No populate factories defiend for %s', no_factory_models) \ No newline at end of file
diff --git a/odoo/cli/__init__.py b/odoo/cli/__init__.py
index 4b860fe68d6..78a080eca1c 100644
--- a/odoo/cli/__init__.py
+++ b/odoo/cli/__init__.py
@@ -11,3 +11,4 @@ from . import scaffold
from . import server
from . import shell
from . import start
+from . import populate
diff --git a/odoo/cli/populate.py b/odoo/cli/populate.py
new file mode 100644
index 00000000000..8f0101c20be
--- /dev/null
+++ b/odoo/cli/populate.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import time
+import fnmatch
+import logging
+import optparse
+import odoo
+
+from . import Command
+_logger = logging.getLogger(__name__)
+
+
+class Populate(Command):
+
+ def run(self, cmdargs):
+ parser = odoo.tools.config.parser
+ group = optparse.OptionGroup(parser, "Populate Configuration")
+ group.add_option("--size", dest="population_size",
+ help="Populate database with auto-generated data. Value should be the population size: small, medium or large",
+ default='small')
+ group.add_option("--models",
+ dest='populate_models',
+ help="Comma separated list of model or pattern (fnmatch)")
+ parser.add_option_group(group)
+ opt = odoo.tools.config.parse_config(cmdargs)
+ populate_models = opt.populate_models and set(opt.populate_models.split(','))
+ population_size = opt.population_size
+ with odoo.api.Environment.manage():
+ dbname = odoo.tools.config['db_name']
+ registry = odoo.registry(dbname)
+ with registry.cursor() as cr:
+ env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
+ self.populate(env, population_size, populate_models)
+
+
+ @classmethod
+ def populate(cls, env, size, model_patterns=False):
+ registry = env.registry
+ populated_models = None
+ try:
+ registry.populated_models = {} # todo master, initialize with already populated models
+ ordered_models = cls._get_ordered_models(env, model_patterns)
+
+ _logger.log(25, 'Populating database')
+ for model in ordered_models:
+ _logger.info('Populating database for model %s', model._name)
+ t0 = time.time()
+ registry.populated_models[model._name] = model._populate(size).ids
+ # todo indicate somewhere that model is populated
+ env.cr.commit()
+ model_time = time.time() - t0
+ if model_time > 1:
+ _logger.info('Populated database for model %s in %ss', model._name, model_time)
+ except:
+ _logger.exception('Something went wrong populating database')
+ finally:
+ populated_models = registry.populated_models
+ del registry.populated_models
+
+ return populated_models
+
+ @classmethod
+ def _get_ordered_models(cls, env, model_patterns=False):
+ _logger.info('Computing model order')
+ processed = set()
+ ordered_models = []
+ visited = set()
+ def add_model(model):
+ if model not in processed:
+ if model in visited:
+ raise ValueError('Cyclic dependency detected for %s' % model)
+ visited.add(model)
+ for dep in model._populate_dependencies:
+ add_model(env[dep])
+ ordered_models.append(model)
+ processed.add(model)
+ for model in env.values():
+ ir_model = env['ir.model'].search([('model', '=', model._name)])
+ if model_patterns and not any(fnmatch.fnmatch(model._name, match) for match in model_patterns):
+ continue
+ if model._transient or model._abstract:
+ continue
+ if not model_patterns and all(module.startswith('test_') for module in ir_model.modules.split(',')):
+ continue
+ add_model(model)
+
+ return ordered_models
diff --git a/odoo/models.py b/odoo/models.py
index 89b7b7a099c..52dadd4b041 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -61,6 +61,7 @@ from .tools.func import frame_codeinfo
from .tools.misc import CountingStream, clean_context, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT, get_lang
from .tools.translate import _
from .tools import date_utils
+from .tools import populate
_logger = logging.getLogger(__name__)
_schema = logging.getLogger(__name__ + '.schema')
@@ -6160,6 +6161,66 @@ Record ids: %(records)s
"""
return 'placeholder.png'
+ def _populate_factories(self):
+ """ Return a list of pairs ``(field_name, factory)``, ``factory`` is a function that creates
+ a generator of values (dict of field values). Its API suggested by::
+
+ # generator yield values by iterating on base_generator and adding a value
+ # for the given field in the dicts yielded by base_generator
+ generator = factory(base_generator, field_name, model_name)
+ # suggested usage
+ for values in generator:
+ model.create(values)
+ """
+ return []
+
+ @property
+ def _populate_sizes(self):
+ """ Return a dict mapping symbolic sizes (``'small'``, ``'medium'``, ``'large'``) to integers,
+ giving the minimal number of records that method ``_populate`` should create.
+ """
+ return {
+ 'small': 10, # minimal representative set
+ 'medium': 100, # average database load
+ 'large': 1000, # maxi database load
+ }
+
+ @property
+ def _populate_dependencies(self):
+ return []
+
+ def _populate(self, size):
+ """ Create records to populate this model.
+
+ :param size: symbolic size for the number of records: ``'small'``, ``'medium'`` or ``'large'``
+ """
+ batch_size = 1000
+ min_size = self._populate_sizes[size]
+
+ record_count = 0
+ create_values = []
+ complete = False
+ field_generators = self._populate_factories()
+ if not field_generators:
+ return self.browse() # maybe create an automatic generator?
+
+ records_batches = []
+ generator = populate.chain_factories(field_generators, self._name)
+ while record_count <= min_size or not complete:
+ values = next(generator)
+ complete = values.pop('__complete')
+ create_values.append(values)
+ record_count += 1
+ if len(create_values) >= batch_size:
+ _logger.info('Batch: %s/%s', record_count, min_size)
+ records_batches.append(self.create(create_values))
+ create_values = []
+
+ if create_values:
+ records_batches.append(self.create(create_values))
+ return self.concat(*records_batches)
+
+
collections.Set.register(BaseModel)
# not exactly true as BaseModel doesn't have __reversed__, index or count
collections.Sequence.register(BaseModel)
diff --git a/odoo/tools/config.py b/odoo/tools/config.py
index efcd71ccf57..65e22cc37af 100644
--- a/odoo/tools/config.py
+++ b/odoo/tools/config.py
@@ -352,9 +352,10 @@ class configmanager(object):
odoo.tools.config.parse_config(sys.argv[1:])
"""
- self._parse_config(args)
+ opt = self._parse_config(args)
odoo.netsvc.init_logger()
odoo.modules.module.initialize_sys_path()
+ return opt
def _parse_config(self, args=None):
if args is None:
@@ -529,6 +530,8 @@ class configmanager(object):
m.strip() for m in self.options['server_wide_modules'].split(',') if m.strip()
]
+ return opt
+
def _is_addons_path(self, path):
from odoo.modules.module import MANIFEST_NAMES
for f in os.listdir(path):
diff --git a/odoo/tools/populate.py b/odoo/tools/populate.py
new file mode 100644
index 00000000000..ddac293eabf
--- /dev/null
+++ b/odoo/tools/populate.py
@@ -0,0 +1,113 @@
+import random
+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):
+ """ Instanciate 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.
+ """
+ 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.
+ """
+ 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 a ``randomize``
+ generator.
+ """
+ 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.
+ """
+ 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.
+ """
+ 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 the random integer between a and b included in each input dict.
+ """
+ def get_rand_int(random=None, **kwargs):
+ random.randint(a, b)
+ return compute(get_rand_int, seed=seed)