DUNE-DAQ
DUNE Trigger and Data Acquisition software
Loading...
Searching...
No Matches
dal.py
Go to the documentation of this file.
1"""Contains the base class for DAL types and auxiliary methods.
2
3This module defines the PyDALBase class that is used as the base type for all
4DAL classes. A few utilities are also available.
5"""
6# This variable holds the global list of DAL classes ever generated for the
7# current python session. This speeds up generation and makes python resolve
8# types in the correct, expected way.
9__dal__ = {}
10
11# Homogeneous comparison between strings and regular expressions
12
13
14def __strcmp__(v1, v2):
15 return v1 == v2
16
17
18def __recmp__(pat, v):
19 return pat.match(v) is not None
20
21
22def prettyprint_cardinality(not_null, multivalue):
23 """Returns a nice string representation for an object cardinality"""
24 if not_null:
25 if multivalue:
26 return '1..*'
27 else:
28 return '1..1'
29
30 else:
31 if multivalue:
32 return '0..*'
33 else:
34 return '0..1'
35
36
38 """Prints the range of an attribute in a nice way"""
39 to_print = list(attr['range']) # copy
40 for k in range(len(to_print)):
41 if isinstance(to_print[k], tuple):
42 to_print[k] = '..'.join([str(i) for i in to_print[k]])
43 return ', '.join([str(i) for i in to_print])
44
45
46def prettyprint_doc(entry):
47 """Pretty prints a schema Cache entry, to be used by __doc__ strings"""
48 from .schema import oks_types
49
50 akeys = list(entry['attribute'].keys())
51 akeys.sort()
52 retval = ' Attributes:'
53 if len(akeys) == 0:
54 retval += ' None'
55 retval += '\n'
56 for k in akeys:
57 retval += ' - "' + k + '": ' + \
58 entry['attribute'][k]['description'].strip() + '\n'
59 retval += ' oks-type: ' + entry['attribute'][k]['type'] + '\n'
60 retval += ' cardinality: ' + \
61 prettyprint_cardinality(entry['attribute'][k]['not-null'],
62 entry['attribute'][k]['multivalue']) + '\n'
63 if entry['attribute'][k]['range']:
64 retval += ' range: ' + \
65 prettyprint_range(entry['attribute'][k]) + '\n'
66 if entry['attribute'][k]['init-value']:
67 retval += ' initial value: ' + \
68 str(entry['attribute'][k]['init-value']) + '\n'
69
70 rkeys = list(entry['relation'].keys())
71 rkeys.sort()
72 retval += '\n Relationships:'
73 if len(rkeys) == 0:
74 retval += ' None'
75 retval += '\n'
76 for k in rkeys:
77 retval += ' - "' + k + '": ' + \
78 entry['relation'][k]['description'].strip() + '\n'
79 retval += ' oks-class: ' + entry['relation'][k]['type'] + '\n'
80 retval += ' cardinality: ' + \
81 prettyprint_cardinality(entry['relation'][k]['not-null'],
82 entry['relation'][k]['multivalue']) + '\n'
83 retval += ' aggregated: ' + \
84 str(entry['relation'][k]['aggregation']) + '\n'
85 return retval[:-1]
86
87
88class DalBase(object):
89 """This class is used to represent any DAL object in the system. """
90
91 # This will keep track of the dal objects that gets updated
92 _updated = set()
93
94 @staticmethod
95 def updated():
96 """Returns a set of DAL objects that were modified in this DB session
97 """
98 return set(DalBase._updated)
99
100 @staticmethod
102 """Reset the set keeping track of modified DAL objects
103 """
104 DalBase._updated.clear()
105
106 def __init__(self, id, **kwargs):
107 """Constructs an object by setting its id (UID in OKS jargon) at least.
108
109 This method will initialize an object of the DalBase type, by setting
110 its internal properties (with schema cross-checking where it is
111 possible). The user should at least set the object's id, which at this
112 moment is not checked for uniqueness.
113
114 Keyword arguments:
115
116 id -- This is the unique identifier (per database) that the user wants
117 to assign to this object. This identifier will be used as the OKS
118 identifier when and if this object is ever serialized in an OKS
119 database.
120
121 **kwargs -- This is a set of attributes and relationships that must
122 exist in the associated DAL class that inherits from this base.
123 """
124 from . import dalproperty
125 prop = property(dalproperty._return_attribute('id', self, id),
126 dalproperty._assign_attribute('id'))
127 setattr(self.__class__, 'id', prop)
128 self.__reset_identity__()
129
130 self.__touched__ = [] # optimization
131 for k, v in kwargs.items():
132 setattr(self, k, v)
133
135 self.__fullname__ = '%s@%s' % (self.id, self.className())
136 self.__hashvalue__ = hash(self.__fullname__)
137
138 def className(self):
139 return self.__class__.pyclassName()
140
141 def isDalType(self, val):
142 cmp = __strcmp__
143 if hasattr(val, 'match'):
144 cmp = __recmp__
145 return True in [cmp(val, k) for k in self.__class__.__okstypes__]
146
147 def oksTypes(self):
148 return self.__class__.pyoksTypes()
149
150 def fullName(self):
151 return self.__fullname__
152
153 def copy(self, other):
154 """Copies attributes and relationships from the other component.
155
156 This will copy whatever relevant attributes and relationships from
157 another component into myself. The implemented algorithm starts by
158 iterating on my own schema and looking for the counter part on the
159 other class's schema, only matching values are copied. This is useful
160 to copy values from base class objects or templated class objects.
161
162 Arguments:
163
164 other -- This is the other dal object you are trying to copy.
165 """
166 for k, v in self.__schema__['attribute'].items():
167 if k not in other.__schema__['attribute']:
168 continue
169 setattr(self, k, getattr(other, k))
170 for k, v in self.__schema__['relation'].items():
171 if k not in other.__schema__['relation']:
172 continue
173 obj = getattr(other, k)
174 try:
175 setattr(self, k, list(obj))
176 except TypeError:
177 setattr(self, k, obj)
178
179 def rename(self, new_name):
180 """
181 Rename the DAL object to a new name.
182
183 This will store the old name in a hidden attribute and when
184 Configuration.update_dal() is called we use it to check
185 if there is an existing object with that name in the database.
186
187 If yes, we call the underlying ConfigObject.rename() method
188 transparently. If the old name does not exist in the database,
189 nothing special is done.
190
191 This is the only 'official' way to rename an object on the
192 DAL level. Just changing the 'id' attribute will not have
193 the same effect.
194 """
195 if self.id == new_name:
196 return
197
198 if not hasattr(self, '__old_id'):
199 setattr(self, '__old_id', getattr(self, 'id'))
200
201 self.id = new_name
202
203 def __repr__(self):
204 """Returns a nice representation of this object."""
205 return "<%s>" % (self.__fullname__)
206
207 def __str__(self):
208 """Returns human readable information about the object."""
209
210 retval = "%s(id='%s'" % (self.className(), self.id)
211
212 for a, v in self.__schema__['attribute'].items():
213 retval += ',\n %s = ' % a
214 if hasattr(self, a):
215 retval += str(getattr(self, a))
216 else:
217 retval += 'None'
218 if v['init-value']:
219 retval += ", # defaults to '%s'" % v['init-value']
220 else:
221 if v['not-null']:
222 retval += ', # MUST be set, there is not default!'
223
224 for r, v in self.__schema__['relation'].items():
225 retval += ',\n %s = ' % r
226 rel = None
227 if hasattr(self, r):
228 rel = getattr(self, r)
229
230 if rel is None:
231 retval += "<unset>"
232 elif isinstance(rel, list):
233 retval += str([repr(k) for k in getattr(self, r)])
234 retval += ']'
235 else:
236 retval += repr(getattr(self, r))
237
238 if retval[-2:] == ',\n':
239 retval = retval[:-2]
240 retval += ')'
241
242 return retval
243
244 def __eq__(self, other):
245 """True is the 2 objects have the same class and ID."""
246 return self.__hashvalue__ == hash(other)
247
248 def __ne__(self, other):
249 """True if the 2 objects *not* have the same class and ID."""
250 return self.__hashvalue__ != hash(other)
251
252 def __gt__(self, other):
253 """True if the object is greater than the other alphabetically."""
254 if self.className() == other.className():
255 return self.id > other.id
256 return self.className() > other.className()
257
258 def __lt__(self, other):
259 """True if the class is smaller than the other. """
260 if self.className() == other.className():
261 return self.id < other.id
262 return (self.className() < other.className())
263
264 def __ge__(self, other):
265 """True if the object is greater or equal than the other
266 alphabetically.
267 """
268 return (self > other) or (self == other)
269
270 def __le__(self, other):
271 """Returns True if the class is smaller or equal than the other. """
272 return (self < other) or (self == other)
273
274 def __hash__(self):
275 """This method is meant to be used to allow DAL objects as map keys."""
276 return self.__hashvalue__
277
278 def __getall__(self, comp=None):
279 """Get all relations, includding a link to myself"""
280 top = False
281 if not comp:
282 top = True
283 comp = {}
284
285 if self.__fullname__ in comp:
286 return
287
288 comp[self.__fullname__] = self
289 for r in list(self.__schema__['relation'].keys()):
290
291 if not getattr(self, r):
292 continue
293
294 if isinstance(getattr(self, r), list):
295 for k in getattr(self, r):
296 k.__getall__(comp)
297 else:
298 getattr(self, r).__getall__(comp)
299
300 if top:
301 return comp
302
303 def get(self, className, idVal=None, lookBaseClasses=False):
304 """Get components in the object based on class name and/or id.
305
306 This method runs trough the components of its relationships and
307 returns a sorted list (sorting based on class name and object ID)
308 containing references to all components that match the search criteria.
309
310 Keyword Parameters (may be named):
311
312 className -- The name of the class to look for. Should be a string
313
314 idVal -- The id of the object to look for. If not set (or set to None),
315 the search will be based only on the class name. If set, it must be
316 either a string or an object that defines a match() method (such as a
317 regular expression).
318
319 lookBaseClasses -- If True and parameter to be search is a class, the
320 method will look also through the base classes names, so if value =
321 Application, for instance the method will return all objects of class
322 Application or that inherit from the Application class.
323
324 Returns a list with all the components that matched the search
325 criteria, if idVal is not set or is a type that defines a match()
326 method such as a regular expression. Otherwise (if it is a string)
327 returns a single object, if any is found following the criterias for
328 className and a exact idVal match.
329 """
330 retval = []
331
332 cmp_class = __strcmp__
333 if hasattr(className, 'match'):
334 cmp_class = __recmp__
335
336 for v in self.__getall__().values():
337 if cmp_class(className, v.__class__.__name__) or \
338 (lookBaseClasses and v.isDalType(className)):
339 if idVal:
340 if type(idVal) == str:
341 if idVal == v.id:
342 return v
343 else:
344 continue
345
346 # if idVal is set and is not a string,
347 # we just go brute force...
348 if idVal.match(v.id):
349 retval.append(v)
350 else:
351 # if idVal is not set and we are sure the class matched,
352 # just append
353 retval.append(v)
354
355 if isinstance(idVal, str):
356 raise KeyError('Did not find %s@%s under %s' %
357 (idVal, className, self.fullName()))
358 return retval
359
360 def __getattr__(self, par):
361 """Returns a given attribute or relationship.
362
363 This method returns an attribute or relationship from the current
364 object, or throws an AttributeError if no such thing exists. It sets
365 the field touched, so it does not get called twice.
366 """
367
368 if par in self.__schema__['attribute']:
369 if self.__schema__['attribute'][par]['init-value']:
370 setattr(self, par, self.__schema__[
371 'attribute'][par]['init-value'])
372 else:
373 if self.__schema__['attribute'][par]['multivalue']:
374 setattr(self, par, [])
375 else:
376 return None # in this case, does not set anything
377 return getattr(self, par)
378
379 elif par in self.__schema__['relation']:
380 if self.__schema__['relation'][par]['multivalue']:
381 setattr(self, par, [])
382 else:
383 return None
384 return getattr(self, par)
385
386 raise AttributeError("'%s' object has no attribute/relation '%s'" %
387 (self.className(), par))
388
389 def setattr_nocheck(self, par, val):
390 """Sets an attribute by-passing the built-in type check."""
391
392 self.__dict__[par] = val
393 if par in list(self.__schema__['relation'].keys()):
394 self.__touched__.append(par)
395 return val
396
397 def __setattr__(self, par, val):
398 """Sets an object attribute or relationship.
399
400 This method overrides the default setattr method, so it can apply
401 existence and type verification on class attributes. If the attribute
402 to be set starts with '__', or the passed value is None,
403 no verification is performed. If the value to set an attribute is a
404 list, the type verification is performed in every component of that
405 list.
406
407 N.B.: This method takes a reference to the object being passed. It does
408 not copy the value, so, if you do a.b = c, and then you apply changes
409 to 'c', these changes will be also applied to 'a.b'.
410
411 Parameters:
412
413 par -- The name of the parameter (attribute or relationship)
414
415 val -- The value that will be attributed to 'par'.
416
417 Raises AttributeError if the parameter does not exist.
418
419 Raises ValueError if the value you passed cannot be coerced to a
420 compatible OKS python type for the attribute or relationship you are
421 trying to set.
422 """
423 from .schema import coerce, check_relation, check_cardinality
424 from . import dalproperty
425
426 # no checks for control parameters
427 if par[0:2] == '__':
428 self.__dict__[par] = val
429 return
430
431 # and for the id it is special
432 if par == 'id':
433 if isinstance(val, str):
434 prop = getattr(self.__class__, par)
435 prop.__set__(self, val)
436 self.__reset_identity__()
437 return
438 else:
439 raise ValueError(
440 'The "id" attribute of a DAL object must be a string')
441
442 if par in list(self.__schema__['attribute'].keys()):
443
444 # If val is None, skip checks
445 if val is None:
446 self.__dict__[par] = val
447
448 try:
449 if val is not None:
450 check_cardinality(val, self.__schema__['attribute'][par])
451 if self.__schema__['attribute'][par]['multivalue']:
452 result = \
453 [coerce(v, self.__schema__['attribute'][par])
454 for v in val]
455 else:
456 result = coerce(val, self.__schema__['attribute'][par])
457 else:
458 result = val
459
460 try:
461 prop = getattr(self.__class__, par)
462
463 except AttributeError:
464
465 prop = property(dalproperty._return_attribute(par),
466 dalproperty._assign_attribute(par))
467 setattr(self.__class__, par, prop)
468
469 prop.__set__(self, result)
470
471 except ValueError as e:
472 raise ValueError('Problems setting attribute "%s" '
473 'at object %s: %s' %
474 (par, self.fullName(), str(e)))
475
476 elif par in list(self.__schema__['relation'].keys()):
477
478 try:
479 # If val is None, skip checks
480 if val is not None:
481 check_cardinality(val, self.__schema__['relation'][par])
482
483 tmpval = \
484 val if self.__schema__[
485 'relation'][par]['multivalue'] else [val]
486 for v in tmpval:
487 check_relation(v, self.__schema__['relation'][par])
488
489 try:
490 prop = getattr(self.__class__, par)
491
492 except AttributeError:
493
494 multi = self.__schema__['relation'][par]['multivalue']
495 prop = property(
496 dalproperty._return_relation(par, multi=multi),
497 dalproperty._assign_relation(par))
498 setattr(self.__class__, par, prop)
499
500 prop.__set__(self, val)
501 self.__touched__.append(par)
502
503 except ValueError as e:
504 raise ValueError('Problems setting relation "%s" at '
505 'object %s: %s' %
506 (par, self.fullName(), str(e)))
507
508 else:
509 raise AttributeError('Parameter "%s" is not ' % par +
510 'part of class "%s" or any of its '
511 'parent classes' %
512 (self.className()))
513
514
515class DalType(type):
516 """
517 This class is the metaclass that every DAL class will be created with.
518
519 DalType is a metaclass (something like a C++ template) that allows us to
520 create DAL classes without having to go through the 'exec' burden all the
521 time and being, therefore, much faster than that mechanism. The idea is
522 that we create DAL types everytime we see a new class and archive this in a
523 cache, together with the configuration object. Everytime an object of a
524 certain OKS type is needed by the user, we make use of the generated class
525 living in that cache to make it a new DAL object.
526
527 The DAL type consistency checks are limited by the amount of generic
528 functionality one can extract by looking at the C++ Configuration class.
529
530 The work here is modelled after the old PyDALBase implementation that used
531 to live in the "genconffwk" package.
532 """
533
534 def __init__(cls, name, bases, dct):
535 """Class constructor.
536
537 Keyword Parameters:
538
539 cls -- This is a pointer to the class being constructed
540
541 name -- The name that the class will have
542
543 bases -- These are the classes, objects of the new generated type will
544 inherit from. It is useful in our context, to express the OKS
545 inheritance relations between the classes.
546
547 dct -- This is a dictionary that will contain mappings between
548 methods/attributes of the newly generated class and values or methods
549 that will be bound to it. The dictionary should contain a pointer to
550 the class schema, and that should be called '__schema__'.
551 """
552 # set all types as expected by the DAL
553 alltypes = [name]
554 for b in bases:
555 if hasattr(b, 'pyoksTypes'):
556 for t in b.pyoksTypes():
557 if t not in alltypes:
558 alltypes.append(t)
559
560 super(DalType, cls).__init__(name, bases, dct)
561 cls.__okstypes__ = alltypes
562
563 def pyclassName(cls):
564 """Returns this class name"""
565 return cls.__name__
566
567 def pyoksTypes(cls):
568 """Returns a join of this class and base class names"""
569 return cls.__okstypes__
570
571
573 """Returns a map with classes in a module, the key is the class name."""
574
575 # assesses all classes from other modules, correlate with names
576 map = {}
577 for k in dir(m):
578 if k.find('__') == 0:
579 continue
580 map[k] = getattr(m, k)
581 return map
582
583
584def generate(configuration, other_dals=[]):
585 """Generates the DAL python access layer for the configuration passed.
586
587 This method will generate the python DAL access layer for all classes
588 declared through the conffwk.Configuration object passed. If this file
589 includes other schemas, the classes for those schemas will also be
590 generated, unless, classes with matching names are passed through the
591 "other_dals" parameters.
592
593 This method will re-use classes generated in other calls to this method,
594 either directly (in DAL binding to a python module) or while you created
595 Configuration type objects. So, you can call this as many times as you want
596 without incurring in much overhead.
597
598 Keyword parameters:
599
600 configuration -- The conffwk.Configuration object that you want the prepare
601 the DAL for.
602
603 other_dals -- This is a list of classes that contain other DALs that should
604 be considered for the inheritance structure of the classes that are going
605 to be generated here. These classes will not be regenerated. This parameter
606 can be either a list of modules or classes that won't be regenerated, but
607 re-used by this generation method.
608
609 Returns the DAL classes you asked for.
610 """
611 from types import ModuleType as module
612
613 klasses = []
614
615 other_classes = {}
616 for k in other_dals:
617 if isinstance(k, module):
618 other_classes.update(get_classes(k))
619 else:
620 other_classes[k.pyclassName()] = k
621
622 # we can save a few loops here, we order by number of bases
623 to_generate = {}
624 for k in configuration.classes():
625 if k in other_classes:
626 continue
627
628 N = len(configuration.superclasses(k))
629 if N in to_generate:
630 to_generate[N].append(k)
631 else:
632 to_generate[N] = [k]
633
634 ordered = []
635 run_order = list(to_generate.keys())
636 run_order.sort()
637 for k in run_order:
638 ordered += to_generate[k]
639
640 # generate what we need to
641 while ordered:
642 next = ordered[0] # gets the first one, no matter what it is
643
644 # if I generated this before, just re-use the class,
645 # so python can check the types in the way the user expects
646 if next in __dal__:
647 klasses.append(__dal__[next])
648 other_classes[next] = __dal__[next]
649
650 # else, I need to generate a brand new class here and
651 # add it to my __dal__
652 else:
653 bases = configuration.superclasses(next)
654 bases = [other_classes.get(k, None) for k in bases]
655 bases.append(DalBase)
656 if None in bases: # cannot yet generate for this one, rotate
657 ordered.append(next)
658 else: # we can generate this one now
659 klasses.append(DalType(next, tuple(bases),
660 {'__schema__':
661 configuration.__schema__[next]}))
662 # so we can use this next time
663 other_classes[next] = klasses[-1]
664 klasses[-1].__doc__ = prettyprint_doc(
665 configuration.__schema__[next])
666 __dal__[next] = klasses[-1]
667
668 del ordered[0]
669
670 return klasses
671
672
673def module(name, schema, other_dals=[], backend='oksconflibs', db=None):
674 """Creates a new python module with the OKS schema files passed as
675 parameter.
676
677 This method creates a new module for the user, using the schema files
678 passed as parameter. Classes from other DALs are not re-created, but just
679 re-used. This is an example usage:
680
681 import conffwk.dal
682 dal = conffwk.dal.module('dal', 'dal/schema/core.schema.xml')
683 DFdal = conffwk.dal.module('DFdal', 'DFConfiguration/schema/df.schema.xml',
684 [dal])
685
686 This will generate two python dals in the current context. One that binds
687 everything available in the first schema file and a second one that binds
688 everything else defined in the DF OKS schema file.
689
690 Keyword parameters:
691
692 name -- The name of the python module to create. It should match the name
693 of the variable you are attributing to, but it is not strictly required by
694 the python interpreter, just a good practice.
695
696 schema -- This is a list of OKS schema files that should be considered. You
697 can also pass OKS datafiles to this one, which actually includes the schema
698 files you want to have a DAL for. It will just work.
699
700 other_dals -- This is a list of other DAL modules that I'll not regenerate,
701 and which classes will *not* make part of the returned module. In fact,
702 this parameter is only used to restrict the amount of output classes since
703 once class is generated internally, it is not regenerated a second time.
704 In other words creating twice the same DAL implies in almost no overhead.
705
706 backend -- This is the OKS backend to use when retrieving the schemas. By
707 default it is set to 'oksconflibs', which is what we
708 """
709 import types
710 from .Configuration import Configuration
711
712 retval = types.ModuleType(name)
713 if isinstance(schema, str):
714 schema = [schema]
715 for s in schema:
716 if db is None:
717 db = Configuration(backend + ':' + s)
718 else:
719 db.load(s)
720 db.__core_init__()
721
722 for k in generate(db, other_dals):
723 retval.__dict__[k.pyclassName()] = k
724 return retval
setattr_nocheck(self, par, val)
Definition dal.py:389
copy(self, other)
Definition dal.py:153
__getattr__(self, par)
Definition dal.py:360
__setattr__(self, par, val)
Definition dal.py:397
__init__(self, id, **kwargs)
Definition dal.py:106
__reset_identity__(self)
Definition dal.py:134
__ne__(self, other)
Definition dal.py:248
__ge__(self, other)
Definition dal.py:264
__eq__(self, other)
Definition dal.py:244
isDalType(self, val)
Definition dal.py:141
__getall__(self, comp=None)
Definition dal.py:278
className(self)
Definition dal.py:138
__le__(self, other)
Definition dal.py:270
get(self, className, idVal=None, lookBaseClasses=False)
Definition dal.py:303
__lt__(self, other)
Definition dal.py:258
rename(self, new_name)
Definition dal.py:179
__gt__(self, other)
Definition dal.py:252
__init__(cls, name, bases, dct)
Definition dal.py:534
get_classes(m)
Definition dal.py:572
prettyprint_doc(entry)
Definition dal.py:46
__strcmp__(v1, v2)
Definition dal.py:14
module(name, schema, other_dals=[], backend='oksconflibs', db=None)
Definition dal.py:673
__recmp__(pat, v)
Definition dal.py:18
prettyprint_range(attr)
Definition dal.py:37
prettyprint_cardinality(not_null, multivalue)
Definition dal.py:22