devtools/fill.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # -*- coding: iso-8859-1 -*-
       
     2 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     3 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     4 #
       
     5 # This file is part of CubicWeb.
       
     6 #
       
     7 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     8 # terms of the GNU Lesser General Public License as published by the Free
       
     9 # Software Foundation, either version 2.1 of the License, or (at your option)
       
    10 # any later version.
       
    11 #
       
    12 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    14 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    15 # details.
       
    16 #
       
    17 # You should have received a copy of the GNU Lesser General Public License along
       
    18 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    19 """This modules defines func / methods for creating test repositories"""
       
    20 from __future__ import print_function
       
    21 
       
    22 __docformat__ = "restructuredtext en"
       
    23 
       
    24 import logging
       
    25 from random import randint, choice
       
    26 from copy import deepcopy
       
    27 from datetime import datetime, date, time, timedelta
       
    28 from decimal import Decimal
       
    29 import inspect
       
    30 
       
    31 from six import text_type, add_metaclass
       
    32 from six.moves import range
       
    33 
       
    34 from logilab.common import attrdict
       
    35 from logilab.mtconverter import xml_escape
       
    36 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
       
    37                               IntervalBoundConstraint, BoundaryConstraint,
       
    38                               Attribute, actual_value)
       
    39 from rql.utils import decompose_b26 as base_decompose_b26
       
    40 
       
    41 from cubicweb import Binary
       
    42 from cubicweb.schema import RQLConstraint
       
    43 
       
    44 def custom_range(start, stop, step):
       
    45     while start < stop:
       
    46         yield start
       
    47         start += step
       
    48 
       
    49 def decompose_b26(index, ascii=False):
       
    50     """return a letter (base-26) decomposition of index"""
       
    51     if ascii:
       
    52         return base_decompose_b26(index)
       
    53     return base_decompose_b26(index, u'éabcdefghijklmnopqrstuvwxyz')
       
    54 
       
    55 def get_max_length(eschema, attrname):
       
    56     """returns the maximum length allowed for 'attrname'"""
       
    57     for cst in eschema.rdef(attrname).constraints:
       
    58         if isinstance(cst, SizeConstraint) and cst.max:
       
    59             return cst.max
       
    60     return 300
       
    61     #raise AttributeError('No Size constraint on attribute "%s"' % attrname)
       
    62 
       
    63 _GENERATED_VALUES = {}
       
    64 
       
    65 class _ValueGenerator(object):
       
    66     """generates integers / dates / strings / etc. to fill a DB table"""
       
    67 
       
    68     def __init__(self, eschema, choice_func=None):
       
    69         """<choice_func> is a function that returns a list of possible
       
    70         choices for a given entity type and an attribute name. It should
       
    71         looks like :
       
    72             def values_for(etype, attrname):
       
    73                 # some stuff ...
       
    74                 return alist_of_acceptable_values # or None
       
    75         """
       
    76         self.choice_func = choice_func
       
    77         self.eschema = eschema
       
    78 
       
    79     def generate_attribute_value(self, entity, attrname, index=1, **kwargs):
       
    80         if attrname in entity:
       
    81             return entity[attrname]
       
    82         eschema = self.eschema
       
    83         if not eschema.has_unique_values(attrname):
       
    84             value = self.__generate_value(entity, attrname, index, **kwargs)
       
    85         else:
       
    86             value = self.__generate_value(entity, attrname, index, **kwargs)
       
    87             while value in _GENERATED_VALUES.get((eschema, attrname), ()):
       
    88                 index += 1
       
    89                 value = self.__generate_value(entity, attrname, index, **kwargs)
       
    90             _GENERATED_VALUES.setdefault((eschema, attrname), set()).add(value)
       
    91         entity[attrname] = value
       
    92         return value
       
    93 
       
    94     def __generate_value(self, entity, attrname, index, **kwargs):
       
    95         """generates a consistent value for 'attrname'"""
       
    96         eschema = self.eschema
       
    97         attrtype = str(eschema.destination(attrname)).lower()
       
    98         # Before calling generate_%s functions, try to find values domain
       
    99         if self.choice_func is not None:
       
   100             values_domain = self.choice_func(eschema, attrname)
       
   101             if values_domain is not None:
       
   102                 return choice(values_domain)
       
   103         gen_func = getattr(self, 'generate_%s_%s' % (eschema, attrname),
       
   104                            getattr(self, 'generate_Any_%s' % attrname, None))
       
   105         if gen_func is not None:
       
   106             return gen_func(entity, index, **kwargs)
       
   107         # If no specific values domain, then generate a dummy value
       
   108         gen_func = getattr(self, 'generate_%s' % (attrtype))
       
   109         return gen_func(entity, attrname, index, **kwargs)
       
   110 
       
   111     def generate_string(self, entity, attrname, index, format=None):
       
   112         """generates a consistent value for 'attrname' if it's a string"""
       
   113         # First try to get choices
       
   114         choosed = self.get_choice(entity, attrname)
       
   115         if choosed is not None:
       
   116             return choosed
       
   117         # All other case, generate a default string
       
   118         attrlength = get_max_length(self.eschema, attrname)
       
   119         num_len = numlen(index)
       
   120         if num_len >= attrlength:
       
   121             ascii = self.eschema.rdef(attrname).internationalizable
       
   122             return ('&'+decompose_b26(index, ascii))[:attrlength]
       
   123         # always use plain text when no format is specified
       
   124         attrprefix = attrname[:max(attrlength-num_len-1, 0)]
       
   125         if format == 'text/html':
       
   126             value = u'<span>é%s<b>%d</b></span>' % (attrprefix, index)
       
   127         elif format == 'text/rest':
       
   128             value = u"""
       
   129 title
       
   130 -----
       
   131 
       
   132 * %s
       
   133 * %d
       
   134 * é&
       
   135 """ % (attrprefix, index)
       
   136         else:
       
   137             value = u'é&%s%d' % (attrprefix, index)
       
   138         return value[:attrlength]
       
   139 
       
   140     def generate_password(self, entity, attrname, index):
       
   141         """generates a consistent value for 'attrname' if it's a password"""
       
   142         return u'toto'
       
   143 
       
   144     def generate_integer(self, entity, attrname, index):
       
   145         """generates a consistent value for 'attrname' if it's an integer"""
       
   146         return self._constrained_generate(entity, attrname, 0, 1, index)
       
   147     generate_int = generate_bigint = generate_integer
       
   148 
       
   149     def generate_float(self, entity, attrname, index):
       
   150         """generates a consistent value for 'attrname' if it's a float"""
       
   151         return self._constrained_generate(entity, attrname, 0.0, 1.0, index)
       
   152 
       
   153     def generate_decimal(self, entity, attrname, index):
       
   154         """generates a consistent value for 'attrname' if it's a float"""
       
   155         return Decimal(str(self.generate_float(entity, attrname, index)))
       
   156 
       
   157     def generate_datetime(self, entity, attrname, index):
       
   158         """generates a random date (format is 'yyyy-mm-dd HH:MM')"""
       
   159         base = datetime(randint(2000, 2004), randint(1, 12), randint(1, 28), 11, index%60)
       
   160         return self._constrained_generate(entity, attrname, base, timedelta(hours=1), index)
       
   161 
       
   162     generate_tzdatetime = generate_datetime # XXX implementation should add a timezone
       
   163 
       
   164     def generate_date(self, entity, attrname, index):
       
   165         """generates a random date (format is 'yyyy-mm-dd')"""
       
   166         base = date(randint(2000, 2010), 1, 1) + timedelta(randint(1, 365))
       
   167         return self._constrained_generate(entity, attrname, base, timedelta(days=1), index)
       
   168 
       
   169     def generate_interval(self, entity, attrname, index):
       
   170         """generates a random date (format is 'yyyy-mm-dd')"""
       
   171         base = timedelta(randint(1, 365))
       
   172         return self._constrained_generate(entity, attrname, base, timedelta(days=1), index)
       
   173 
       
   174     def generate_time(self, entity, attrname, index):
       
   175         """generates a random time (format is ' HH:MM')"""
       
   176         return time(11, index%60) #'11:%02d' % (index % 60)
       
   177 
       
   178     generate_tztime = generate_time # XXX implementation should add a timezone
       
   179 
       
   180     def generate_bytes(self, entity, attrname, index, format=None):
       
   181         fakefile = Binary(("%s%s" % (attrname, index)).encode('ascii'))
       
   182         fakefile.filename = u"file_%s" % attrname
       
   183         return fakefile
       
   184 
       
   185     def generate_boolean(self, entity, attrname, index):
       
   186         """generates a consistent value for 'attrname' if it's a boolean"""
       
   187         return index % 2 == 0
       
   188 
       
   189     def _constrained_generate(self, entity, attrname, base, step, index):
       
   190         choosed = self.get_choice(entity, attrname)
       
   191         if choosed is not None:
       
   192             return choosed
       
   193         # ensure index > 0
       
   194         index += 1
       
   195         minvalue, maxvalue = self.get_bounds(entity, attrname)
       
   196         if maxvalue is None:
       
   197             if minvalue is not None:
       
   198                 base = max(minvalue, base)
       
   199             maxvalue = base + index * step
       
   200         if minvalue is None:
       
   201             minvalue = maxvalue - (index * step) # i.e. randint(-index, 0)
       
   202         return choice(list(custom_range(minvalue, maxvalue, step)))
       
   203 
       
   204     def _actual_boundary(self, entity, attrname, boundary):
       
   205         if isinstance(boundary, Attribute):
       
   206             # ensure we've a value for this attribute
       
   207             entity[attrname] = None # infinite loop safety belt
       
   208             if not boundary.attr in entity:
       
   209                 self.generate_attribute_value(entity, boundary.attr)
       
   210             boundary = actual_value(boundary, entity)
       
   211         return boundary
       
   212 
       
   213     def get_bounds(self, entity, attrname):
       
   214         minvalue = maxvalue = None
       
   215         for cst in self.eschema.rdef(attrname).constraints:
       
   216             if isinstance(cst, IntervalBoundConstraint):
       
   217                 minvalue = self._actual_boundary(entity, attrname, cst.minvalue)
       
   218                 maxvalue = self._actual_boundary(entity, attrname, cst.maxvalue)
       
   219             elif isinstance(cst, BoundaryConstraint):
       
   220                 if cst.operator[0] == '<':
       
   221                     maxvalue = self._actual_boundary(entity, attrname, cst.boundary)
       
   222                 else:
       
   223                     minvalue = self._actual_boundary(entity, attrname, cst.boundary)
       
   224         return minvalue, maxvalue
       
   225 
       
   226     def get_choice(self, entity, attrname):
       
   227         """generates a consistent value for 'attrname' if it has some static
       
   228         vocabulary set, else return None.
       
   229         """
       
   230         for cst in self.eschema.rdef(attrname).constraints:
       
   231             if isinstance(cst, StaticVocabularyConstraint):
       
   232                 return text_type(choice(cst.vocabulary()))
       
   233         return None
       
   234 
       
   235     # XXX nothing to do here
       
   236     def generate_Any_data_format(self, entity, index, **kwargs):
       
   237         # data_format attribute of File has no vocabulary constraint, we
       
   238         # need this method else stupid values will be set which make mtconverter
       
   239         # raise exception
       
   240         return u'application/octet-stream'
       
   241 
       
   242     def generate_Any_content_format(self, entity, index, **kwargs):
       
   243         # content_format attribute of EmailPart has no vocabulary constraint, we
       
   244         # need this method else stupid values will be set which make mtconverter
       
   245         # raise exception
       
   246         return u'text/plain'
       
   247 
       
   248     def generate_CWDataImport_log(self, entity, index, **kwargs):
       
   249         # content_format attribute of EmailPart has no vocabulary constraint, we
       
   250         # need this method else stupid values will be set which make mtconverter
       
   251         # raise exception
       
   252         logs =  [u'%s\t%s\t%s\t%s<br/>' % (logging.ERROR, 'http://url.com?arg1=hop&arg2=hip',
       
   253                                            1, xml_escape('hjoio&oio"'))]
       
   254         return u'<br/>'.join(logs)
       
   255 
       
   256 
       
   257 class autoextend(type):
       
   258     def __new__(mcs, name, bases, classdict):
       
   259         for attrname, attrvalue in classdict.items():
       
   260             if callable(attrvalue):
       
   261                 if attrname.startswith('generate_') and \
       
   262                        len(inspect.getargspec(attrvalue).args) < 2:
       
   263                     raise TypeError('generate_xxx must accept at least 1 argument')
       
   264                 setattr(_ValueGenerator, attrname, attrvalue)
       
   265         return type.__new__(mcs, name, bases, classdict)
       
   266 
       
   267 
       
   268 @add_metaclass(autoextend)
       
   269 class ValueGenerator(_ValueGenerator):
       
   270     pass
       
   271 
       
   272 
       
   273 def _default_choice_func(etype, attrname):
       
   274     """default choice_func for insert_entity_queries"""
       
   275     return None
       
   276 
       
   277 def insert_entity_queries(etype, schema, vreg, entity_num,
       
   278                           choice_func=_default_choice_func):
       
   279     """returns a list of 'add entity' queries (couples query, args)
       
   280     :type etype: str
       
   281     :param etype: the entity's type
       
   282 
       
   283     :type schema: cubicweb.schema.Schema
       
   284     :param schema: the instance schema
       
   285 
       
   286     :type entity_num: int
       
   287     :param entity_num: the number of entities to insert
       
   288 
       
   289     XXX FIXME: choice_func is here for *historical* reasons, it should
       
   290                probably replaced by a nicer way to specify choices
       
   291     :type choice_func: function
       
   292     :param choice_func: a function that takes an entity type, an attrname and
       
   293                         returns acceptable values for this attribute
       
   294     """
       
   295     queries = []
       
   296     for index in range(entity_num):
       
   297         restrictions = []
       
   298         args = {}
       
   299         for attrname, value in make_entity(etype, schema, vreg, index, choice_func).items():
       
   300             restrictions.append('X %s %%(%s)s' % (attrname, attrname))
       
   301             args[attrname] = value
       
   302         if restrictions:
       
   303             queries.append(('INSERT %s X: %s' % (etype, ', '.join(restrictions)),
       
   304                             args))
       
   305             assert not 'eid' in args, args
       
   306         else:
       
   307             queries.append(('INSERT %s X' % etype, {}))
       
   308     return queries
       
   309 
       
   310 
       
   311 def make_entity(etype, schema, vreg, index=0, choice_func=_default_choice_func,
       
   312                 form=False):
       
   313     """generates a random entity and returns it as a dict
       
   314 
       
   315     by default, generate an entity to be inserted in the repository
       
   316     elif form, generate an form dictionary to be given to a web controller
       
   317     """
       
   318     eschema = schema.eschema(etype)
       
   319     valgen = ValueGenerator(eschema, choice_func)
       
   320     entity = attrdict()
       
   321     # preprocessing to deal with _format fields
       
   322     attributes = []
       
   323     relatedfields = {}
       
   324     for rschema, attrschema in eschema.attribute_definitions():
       
   325         attrname = rschema.type
       
   326         if attrname == 'eid':
       
   327             # don't specify eids !
       
   328             continue
       
   329         if attrname.endswith('_format') and attrname[:-7] in eschema.subject_relations():
       
   330             relatedfields[attrname[:-7]] = attrschema
       
   331         else:
       
   332             attributes.append((attrname, attrschema))
       
   333     for attrname, attrschema in attributes:
       
   334         if attrname in relatedfields:
       
   335             # first generate a format and record it
       
   336             format = valgen.generate_attribute_value(entity, attrname + '_format', index)
       
   337             # then a value coherent with this format
       
   338             value = valgen.generate_attribute_value(entity, attrname, index, format=format)
       
   339         else:
       
   340             value = valgen.generate_attribute_value(entity, attrname, index)
       
   341         if form: # need to encode values
       
   342             if attrschema.type == 'Bytes':
       
   343                 # twisted way
       
   344                 fakefile = value
       
   345                 filename = value.filename
       
   346                 value = (filename, u"text/plain", fakefile)
       
   347             elif attrschema.type == 'Date':
       
   348                 value = value.strftime(vreg.property_value('ui.date-format'))
       
   349             elif attrschema.type == 'Datetime':
       
   350                 value = value.strftime(vreg.property_value('ui.datetime-format'))
       
   351             elif attrschema.type == 'Time':
       
   352                 value = value.strftime(vreg.property_value('ui.time-format'))
       
   353             elif attrschema.type == 'Float':
       
   354                 fmt = vreg.property_value('ui.float-format')
       
   355                 value = fmt % value
       
   356             else:
       
   357                 value = text_type(value)
       
   358     return entity
       
   359 
       
   360 
       
   361 
       
   362 def select(constraints, cnx, selectvar='O', objtype=None):
       
   363     """returns list of eids matching <constraints>
       
   364 
       
   365     <selectvar> should be either 'O' or 'S' to match schema definitions
       
   366     """
       
   367     try:
       
   368         rql = 'Any %s WHERE %s' % (selectvar, constraints)
       
   369         if objtype:
       
   370             rql += ', %s is %s' % (selectvar, objtype)
       
   371         rset = cnx.execute(rql)
       
   372     except Exception:
       
   373         print("could restrict eid_list with given constraints (%r)" % constraints)
       
   374         return []
       
   375     return set(eid for eid, in rset.rows)
       
   376 
       
   377 
       
   378 
       
   379 def make_relations_queries(schema, edict, cnx, ignored_relations=(),
       
   380                            existingrels=None):
       
   381     """returns a list of generated RQL queries for relations
       
   382     :param schema: The instance schema
       
   383 
       
   384     :param e_dict: mapping between etypes and eids
       
   385 
       
   386     :param ignored_relations: list of relations to ignore (i.e. don't try
       
   387                               to generate insert queries for these relations)
       
   388     """
       
   389     gen = RelationsQueriesGenerator(schema, cnx, existingrels)
       
   390     return gen.compute_queries(edict, ignored_relations)
       
   391 
       
   392 def composite_relation(rschema):
       
   393     for obj in rschema.objects():
       
   394         if obj.rdef(rschema, 'object', takefirst=True).composite == 'subject':
       
   395             return True
       
   396     for obj in rschema.subjects():
       
   397         if obj.rdef(rschema, 'subject', takefirst=True).composite == 'object':
       
   398             return True
       
   399     return False
       
   400 
       
   401 class RelationsQueriesGenerator(object):
       
   402     rql_tmpl = 'SET S %s O WHERE S eid %%(subjeid)s, O eid %%(objeid)s'
       
   403     def __init__(self, schema, cnx, existing=None):
       
   404         self.schema = schema
       
   405         self.cnx = cnx
       
   406         self.existingrels = existing or {}
       
   407 
       
   408     def compute_queries(self, edict, ignored_relations):
       
   409         queries = []
       
   410         #   1/ skip final relations and explictly ignored relations
       
   411         rels = sorted([rschema for rschema in self.schema.relations()
       
   412                        if not (rschema.final or rschema in ignored_relations)],
       
   413                       key=lambda x:not composite_relation(x))
       
   414         # for each relation
       
   415         #   2/ take each possible couple (subj, obj)
       
   416         #   3/ analyze cardinality of relation
       
   417         #      a/ if relation is mandatory, insert one relation
       
   418         #      b/ else insert N relations where N is the mininum
       
   419         #         of 20 and the number of existing targetable entities
       
   420         for rschema in rels:
       
   421             sym = set()
       
   422             sedict = deepcopy(edict)
       
   423             oedict = deepcopy(edict)
       
   424             delayed = []
       
   425             # for each couple (subjschema, objschema), insert relations
       
   426             for subj, obj in rschema.rdefs:
       
   427                 sym.add( (subj, obj) )
       
   428                 if rschema.symmetric and (obj, subj) in sym:
       
   429                     continue
       
   430                 subjcard, objcard = rschema.rdef(subj, obj).cardinality
       
   431                 # process mandatory relations first
       
   432                 if subjcard in '1+' or objcard in '1+' or composite_relation(rschema):
       
   433                     for query, args in self.make_relation_queries(sedict, oedict,
       
   434                                                           rschema, subj, obj):
       
   435                         yield query, args
       
   436                 else:
       
   437                     delayed.append( (subj, obj) )
       
   438             for subj, obj in delayed:
       
   439                 for query, args in self.make_relation_queries(sedict, oedict, rschema,
       
   440                                                               subj, obj):
       
   441                     yield query, args
       
   442 
       
   443     def qargs(self, subjeids, objeids, subjcard, objcard, subjeid, objeid):
       
   444         if subjcard in '?1+':
       
   445             subjeids.remove(subjeid)
       
   446         if objcard in '?1+':
       
   447             objeids.remove(objeid)
       
   448         return {'subjeid' : subjeid, 'objeid' : objeid}
       
   449 
       
   450     def make_relation_queries(self, sedict, oedict, rschema, subj, obj):
       
   451         rdef = rschema.rdef(subj, obj)
       
   452         subjcard, objcard = rdef.cardinality
       
   453         subjeids = sedict.get(subj, frozenset())
       
   454         used = self.existingrels[rschema.type]
       
   455         preexisting_subjrels = set(subj for subj, obj in used)
       
   456         preexisting_objrels = set(obj for subj, obj in used)
       
   457         # if there are constraints, only select appropriate objeids
       
   458         q = self.rql_tmpl % rschema.type
       
   459         constraints = [c for c in rdef.constraints
       
   460                        if isinstance(c, RQLConstraint)]
       
   461         if constraints:
       
   462             restrictions = ', '.join(c.expression for c in constraints)
       
   463             q += ', %s' % restrictions
       
   464             # restrict object eids if possible
       
   465             # XXX the attempt to restrict below in completely wrong
       
   466             # disabling it for now
       
   467             objeids = select(restrictions, self.cnx, objtype=obj)
       
   468         else:
       
   469             objeids = oedict.get(obj, frozenset())
       
   470         if subjcard in '?1' or objcard in '?1':
       
   471             for subjeid, objeid in used:
       
   472                 if subjcard in '?1' and subjeid in subjeids:
       
   473                     subjeids.remove(subjeid)
       
   474                     # XXX why?
       
   475                     #if objeid in objeids:
       
   476                     #    objeids.remove(objeid)
       
   477                 if objcard in '?1' and objeid in objeids:
       
   478                     objeids.remove(objeid)
       
   479                     # XXX why?
       
   480                     #if subjeid in subjeids:
       
   481                     #    subjeids.remove(subjeid)
       
   482         if not subjeids:
       
   483             check_card_satisfied(objcard, objeids, subj, rschema, obj)
       
   484             return
       
   485         if not objeids:
       
   486             check_card_satisfied(subjcard, subjeids, subj, rschema, obj)
       
   487             return
       
   488         if subjcard in '?1+':
       
   489             for subjeid in tuple(subjeids):
       
   490                 # do not insert relation if this entity already has a relation
       
   491                 if subjeid in preexisting_subjrels:
       
   492                     continue
       
   493                 objeid = choose_eid(objeids, subjeid)
       
   494                 if objeid is None or (subjeid, objeid) in used:
       
   495                     continue
       
   496                 yield q, self.qargs(subjeids, objeids, subjcard, objcard,
       
   497                                     subjeid, objeid)
       
   498                 used.add( (subjeid, objeid) )
       
   499                 if not objeids:
       
   500                     check_card_satisfied(subjcard, subjeids, subj, rschema, obj)
       
   501                     break
       
   502         elif objcard in '?1+':
       
   503             for objeid in tuple(objeids):
       
   504                 # do not insert relation if this entity already has a relation
       
   505                 if objeid in preexisting_objrels:
       
   506                     continue
       
   507                 subjeid = choose_eid(subjeids, objeid)
       
   508                 if subjeid is None or (subjeid, objeid) in used:
       
   509                     continue
       
   510                 yield q, self.qargs(subjeids, objeids, subjcard, objcard,
       
   511                                     subjeid, objeid)
       
   512                 used.add( (subjeid, objeid) )
       
   513                 if not subjeids:
       
   514                     check_card_satisfied(objcard, objeids, subj, rschema, obj)
       
   515                     break
       
   516         else:
       
   517             # FIXME: 20 should be read from config
       
   518             subjeidsiter = [choice(tuple(subjeids)) for i in range(min(len(subjeids), 20))]
       
   519             objeidsiter = [choice(tuple(objeids)) for i in range(min(len(objeids), 20))]
       
   520             for subjeid, objeid in zip(subjeidsiter, objeidsiter):
       
   521                 if subjeid != objeid and not (subjeid, objeid) in used:
       
   522                     used.add( (subjeid, objeid) )
       
   523                     yield q, self.qargs(subjeids, objeids, subjcard, objcard,
       
   524                                         subjeid, objeid)
       
   525 
       
   526 def check_card_satisfied(card, remaining, subj, rschema, obj):
       
   527     if card in '1+' and remaining:
       
   528         raise Exception("can't satisfy cardinality %s for relation %s %s %s" %
       
   529                         (card, subj, rschema, obj))
       
   530 
       
   531 
       
   532 def choose_eid(values, avoid):
       
   533     values = tuple(values)
       
   534     if len(values) == 1 and values[0] == avoid:
       
   535         return None
       
   536     objeid = choice(values)
       
   537     while objeid == avoid: # avoid infinite recursion like in X comment X
       
   538         objeid = choice(values)
       
   539     return objeid
       
   540 
       
   541 
       
   542 
       
   543 # UTILITIES FUNCS ##############################################################
       
   544 def make_tel(num_tel):
       
   545     """takes an integer, converts is as a string and inserts
       
   546     white spaces each 2 chars (french notation)
       
   547     """
       
   548     num_list = list(str(num_tel))
       
   549     for index in (6, 4, 2):
       
   550         num_list.insert(index, ' ')
       
   551 
       
   552     return ''.join(num_list)
       
   553 
       
   554 
       
   555 def numlen(number):
       
   556     """returns the number's length"""
       
   557     return len(str(number))