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)) |
|