Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/trunk/__init__.py

Revision 26 (checked in by fumanchu, 9 years ago)

1. Use error trapping instead of asserts.
2. Bug in engines--can't .copy() a list, use [:]

Line 
1 import ConfigParser
2 import datetime
3
4
5 from dejavu.containers import *
6 from dejavu.analysis import *
7 from dejavu.readme import *
8 from dejavu import logic
9 import xray
10
11
12 ###########################################################################
13 ##                                                                       ##
14 ##                                 Units                                 ##
15 ##                                                                       ##
16 ###########################################################################
17
18
19 # All Units currently must possess an 'ID' UnitProperty. The sequencing of
20 # IDs depends upon their type and the particular needs of the class. Pick
21 # one of these UnitSequencers to fit your subclass.
22
23 # At the moment, no ID sequences are allowed None as a value, since this
24 # signals a Unit which needs to be sequenced when memorized. In addition,
25 # you should aim to create new sequencers which generate IDs that obey
26 # the builtin max() and min() functions.
27
28 class UnitSequencerNull(object):
29     """A null sequencer for Unit IDs. Sequencing will error.
30     
31     In many cases, ID values simply have no algorithmic sequence;
32     for example, a set of Employee Units might use Social Security
33     Numbers for IDs (which you should never, ever do ;).
34     
35     In other cases, sequencing will be best handled by custom algorithms
36     within application code; that is, the job of abstracting the sequence
37     logic would not be worth the effort.
38     """
39    
40     def __init__(self, type=unicode):
41         self.type = type
42    
43     def next(self, sequence):
44         raise StopIteration("No sequence defined.")
45
46
47 class UnitSequencerInteger(object):
48     """A sequencer for Unit IDs, where id[i+1] == id[i] + 1."""
49    
50     def __init__(self, type=int, initial=1):
51         self.type = type
52         self.initial = initial
53    
54     def next(self, sequence):
55         if sequence:
56             m = max(sequence)
57             if m is not None:
58                 return m + 1
59         return self.initial
60
61
62 class UnitSequencerUnicode(object):
63     """A sequencer for Unit IDs, where e.g. next(['abc']) == 'abd'."""
64    
65     def __init__(self, type=unicode, width=6,
66                  range="abcdefghijklmnopqrstuvwxyz"):
67         self.type = type
68         self.width = width
69         self.range = range
70    
71     def next(self, sequence):
72         r = self.range
73         if sequence:
74             maxid = max(sequence)
75             if len(maxid) != self.width:
76                 raise OverflowError("'%s' is not of width %s." %
77                                     (maxid, self.width))
78             for i in range(self.width - 1, -1, -1):
79                 pos = r.index(maxid[i]) + 1
80                 if pos >= len(r) or pos < 0:
81                     maxid = maxid[:i] + r[0] + maxid[i+1:]
82                 else:
83                     maxid = maxid[:i] + r[pos] + maxid[i+1:]
84                     break
85             else:
86                 raise OverflowError("Next ID exceeds width %s." % self.width)
87             return maxid
88         return r[0] * self.width
89
90
91 class UnitProperty(object):
92     """Data descriptor for Unit data which will persist in storage.
93     
94     pre, post: Override these with functions to provide custom behaviors
95         upon attribute modification. They are not called if sandbox is None.
96         If you need a behavior which fires regardless, you should override
97         __set__. They also are not called if the new value matches the
98         existing value. Again, override __set__ if you need an exception.
99     
100     hints: A dictionary which provides named hints to Storage Managers
101         concerning the nature of the data. A common use, for example,
102         is to inform Managers that would usually store unicode strings
103         as strings of length 255, that a particular value should be
104         a larger object; this is done with a 'Size' mapping, such as:
105         hints = {u'Size': 0}, where 0 implies no limit. Canonical storage
106         hint names and implementation details may be found in storage.py
107         documentation.
108     """
109    
110     pre = None
111     post = None
112    
113     def __init__(self, key, type=unicode, index=False, hints={}):
114         self.key = key
115         self.type = type
116         self.index = index
117         self.hints = hints
118    
119     def __get__(self, unit, unitclass=None):
120         if unit is None:
121             # When calling on the class instead of an instance...
122             return self
123         else:
124             return unit._properties[self.key]
125    
126     def __set__(self, unit, value):
127         if self.coerce:
128             value = self.coerce(unit, value)
129        
130         oldvalue = unit._properties[self.key]
131         # This test is expensive, but it saves us a lot of
132         # unnecessary save() operations later on.
133         if value != oldvalue:
134             if unit.sandbox and self.pre:
135                 self.pre(unit, value)
136             unit.dirty = True
137             unit._properties[self.key] = value
138             if unit.sandbox and self.post:
139                 self.post(unit, value)
140    
141     def coerce(self, unit, value):
142         if value is not None and not isinstance(value, self.type):
143             # Try to coerce the value.
144             try:
145                 value = self.type(value)
146             except Exception, x:
147                 x.args += (value, type(value))
148                 raise x
149         return value
150    
151     def __delete__(self, unit):
152         raise AttributeError("Unit Properties may not be deleted.")
153
154
155 class MetaUnit(type):
156     def __init__(cls, name, bases, dct):
157         cls._associations = {}
158        
159         # Make a copy of the parent class' _properties keys, and store
160         # it in the _properties attribute of this subclass. In this
161         # manner, Unit Properties should propagate down to subclasses,
162         # but not back up to superclasses.
163         props = dict.fromkeys(cls._properties.keys())
164        
165         # Now grab any new UnitProperties defined in this class.
166         # Overwrite any properties defined in superclasses.
167         for key, val in dct.iteritems():
168             if isinstance(val, UnitProperty):
169                 props[key] = val
170        
171         cls._properties = props
172
173
174 class Unit(object):
175     """A generic object, the building-block of Dejavu.
176     
177     These are purposefully lightweight, relying on Sandboxes to cache
178     them, which in turn rely on Storage Managers to load and save them.
179     
180     They maintain their own "schema" via UnitProperty objects, so that the
181     Storage Managers don't need to know every detail about every Unit.
182     Storage Managers for simple databases, for example, will simply create
183     a single flat table for each unit type. If you write a custom Storage
184     Manager, you can do as you like; the only place you might run into a
185     problem is if you write a custom Storage Manager for custom Unit types,
186     because the knowledge between the two is indeterminate. For example,
187     if we provide a standard StorageManagerForLotusNotes, and you create
188     custom Units which interface with it, you should probably subclass and
189     extend our StorageManagerForLotusNotes with some custom storage logic.
190     
191     sandbox: The sandbox in which the Unit "lives". Also serves as a flag
192         indicating whether this Unit has finished the initial creation
193         process. While sandbox is None, pre and post descriptor functions
194         will not be called.
195         
196         Sandboxes receive Units during recall() and memorize();
197         these processes should set the sandbox attribute.
198     
199     dirty: a flag indicating whether elements in the _properties dictionary
200         have been modified. This flag is used by Sandboxes to optimize
201         forget(): they do not ask Storage Managers to save data for Units
202         which have not been modified. Because SM's may cache Units, no code
203         should set this flag other than UnitProperty.__set__ and SM's.
204     """
205    
206     __metaclass__ = MetaUnit
207     _properties = {}
208     sequencer = UnitSequencerInteger()
209    
210     def __init__(self, **kwargs):
211         # Copy the class _properties dict into self, setting
212         # each value to None.
213         self._properties = dict.fromkeys(self.__class__._properties.keys())
214        
215         self.sandbox = None
216         self.dirty = False
217        
218         for k, v in kwargs.iteritems():
219             setattr(self, k, v)
220    
221     def set_property(cls, key, type=unicode, index=False,
222                      descriptor=UnitProperty):
223         """Set a Unit Property for cls."""
224         setattr(cls, key, descriptor(key, type, index))
225         cls._properties[key] = None
226     set_property = classmethod(set_property)
227    
228     def set_properties(cls, types={}, descriptor=UnitProperty):
229         """Set Unit Properties for cls."""
230         for key, type in types.items():
231             cls.set_property(key, type, False, descriptor)
232     set_properties = classmethod(set_properties)
233    
234     def indices(cls):
235         """cls.indices() -> tuple of names of indexed UnitProperties."""
236         product = []
237         for key in cls.properties():
238             try:
239                 if getattr(cls, key).index:
240                     product.append(key)
241             except AttributeError, x:
242                 x.args += (cls, key)
243                 raise x
244         return tuple(product)
245     indices = classmethod(indices)
246    
247     def properties(cls):
248         """cls.properties() -> list of UnitProperty names."""
249         return cls._properties.iterkeys()
250     properties = classmethod(properties)
251    
252     def property_type(cls, key):
253         """cls.property_type(key) -> type of the given UnitProperty."""
254         # Retrieving from the class gives us
255         # the UnitProperty object, not its value.
256         return getattr(cls, key).type
257     property_type = classmethod(property_type)
258    
259     def adjust(self, **values):
260         """adjust(**values) -> Set UnitProperties by key, value pairs."""
261         for key, val in values.iteritems():
262             setattr(self, key, val)
263    
264     def repress(self):
265         """repress() -> Remove this Unit from memory (do not destroy)."""
266         self.sandbox.repress(self)
267    
268     def forget(self):
269         """forget() -> Destroy this Unit."""
270         self.sandbox.forget(self)
271    
272     def __copy__(self):
273         newUnit = self.__class__()
274         for key in self.__class__.properties():
275             if key != u'ID':
276                 newUnit._properties[key] = self._properties[key]
277         newUnit.ID = None
278         newUnit.sandbox = None
279         return newUnit
280    
281     def __getstate__(self):
282         return (self._properties, self.dirty)
283    
284     def __setstate__(self, state):
285         self.sandbox = None
286         self._properties, self.dirty = state
287    
288     def add(self, unit):
289         """add(unit) -> Auto-create a relationship between self and unit."""
290         try:
291             key, farKey = self.__class__._associations[unit.__class__]
292         except KeyError:
293             raise AssociationError("'%s' is not associated with '%s'"
294                                    % (self.__class__, unit.__class__))
295        
296         nearval = getattr(self, key)
297         farval = getattr(unit, farKey)
298         if nearval is None:
299             if farval is None:
300                 raise AssociationError("At least one Unit key must be set.")
301             else:
302                 setattr(self, key, farval)
303         else:
304             # If far key is already set, it will simply be overwritten.
305             setattr(unit, farKey, nearval)
306
307
308 # The default ID type is int. If you wish to use a different type for
309 # the ID's of a subclass of Unit, just overwrite ID, e.g.:
310 #     ID = UnitProperty('ID', unicode, index=True)
311 #       or
312 #     UnitSubclass.set_property('ID', unicode, index=True)
313 #       or even
314 #     UnitSubclass.ID.type = unicode
315 # You will probably also want to override Unit.sequencer for the class.
316 Unit.set_property(u'ID', int, index=True)
317
318
319
320 ###########################################################################
321 ##                                                                       ##
322 ##                                Arenas                                 ##
323 ##                                                                       ##
324 ###########################################################################
325
326
327 class Arena(object):
328     """A namespace/workspace for a Dejavu application."""
329    
330     def __init__(self):
331         self.defaultStore = None
332         self.stores = {}
333         self.roster = Prism('name', 'cls', 'store')
334         self.associations = Graph()
335         self.engine_functions = {}
336    
337     def load(self, configFileName):
338         """Load StorageManagers."""
339         parser = ConfigParser.ConfigParser()
340         # Make names case-sensitive by overriding optionxform.
341         parser.optionxform = unicode
342         parser.read(configFileName)
343        
344         stores = []
345         for section in parser.sections():
346             opts = dict(parser.items(section))
347             stores.append((int(opts.get("Load Order", "0")), section, opts))
348         stores.sort()
349        
350         for order, name, options in stores:
351             storage_mgr_class = xray.classes(options[u'Class'])
352             store = storage_mgr_class(name, self, options)
353            
354             unitClasses = []
355             for x in options.get('Units', '').split(","):
356                 clsname = x.strip()
357                 if clsname:
358                     unitClasses.append(clsname)
359            
360             self.add_store(name, store, unitClasses)
361    
362     def add_store(self, name, store, unitClasses=[]):
363         """Register a StorageManager."""
364         self.stores[name] = store
365        
366         # Fill Roster, a Prism of class-associated data.
367         if unitClasses:
368             for clsname in unitClasses:
369                 if clsname:
370                     self.roster.add(name=clsname, cls=None, store=store)
371         else:
372             self.defaultStore = store
373    
374     def shutdown(self):
375         """Shutdown the arena."""
376         # Tell all stores to shut down.
377         stores = [(x.shutdownOrder, x) for x in self.stores.itervalues()]
378         stores.sort()
379         for order, store in stores:
380             store.shutdown()
381    
382     def new_sandbox(self):
383         return Sandbox(self)
384    
385     ###########################################
386     ##        Unit Class Registration        ##
387     ###########################################
388    
389     def register(self, cls):
390         """register(cls) -> Assert that Units of class 'cls' will be handled."""
391         try:
392             row = self.roster.row_number(name=cls.__name__)
393         except ValueError:
394             self.roster.add(name=cls.__name__, cls=cls, store=self.defaultStore)
395         else:
396             # We left cls == None in _load(). Set it now.
397             self.roster.facets['cls'][row] = cls
398    
399     def class_by_name(self, classname):
400         return self.roster.cls(name=classname)
401    
402     def storage(self, cls):
403         return self.roster.store(cls=cls)
404    
405     def create_storage(self, cls):
406         self.storage(cls).create_storage(cls)
407    
408     def associate(self, cls, key, farClass, farKey,
409                   nearFactory=None, farFactory=None):
410         """Associate one Unit class with another by relating attributes.
411         
412         cls, key: The 'near' class and its key.
413         farClass, farKey: the 'far' class and its key.
414         
415         Far Units will be recalled if their farKey matches cls.key.
416         However, if cls.key is empty or None, no Units will be recalled.
417         """
418        
419         # Disallow overwriting of existing attributes.
420         if hasattr(cls, farClass.__name__):
421             raise AssociationError(u"%s already has an attribute named '%s'."
422                                    % (cls, farClass.__name__))
423         if hasattr(farClass, cls.__name__):
424             raise AssociationError(u"%s already has an attribute named '%s'."
425                                    % (farClass, cls.__name__))
426        
427         # Assert that both classes are registered.
428         self.register(cls)
429         self.register(farClass)
430        
431         # Add a method to cls which retrieves farClass synapses
432         if nearFactory is None:
433             nearFactory = _synapses_func
434         func = nearFactory(key, farClass, farKey)
435         setattr(cls, farClass.__name__, func)
436         cls._associations[farClass] = (key, farKey)
437        
438         # Add a method to farClass which retrieves cls synapses
439         if farFactory is None:
440             farFactory = _synapses_func
441         func = farFactory(farKey, cls, key)
442         setattr(farClass, cls.__name__, func)
443         farClass._associations[cls] = (farKey, key)
444        
445         # Register the association(s) in an undirected graph.
446         self.associations.connect(cls, farClass)
447
448
449 # You can use this arena instance if you are
450 # deploying a single application per process.
451 # Otherwise, you should create your own instance
452 # per application.
453 dejavuarena = Arena()
454
455 def _synapses_func(key, farClass, farKey):
456     """Produce a new synapses() function to be bound to a Unit class."""
457     def synapses(self, expr=None):
458         """Recall associated '%(farname)s' Units.
459         
460         %(farname)s Units will be recalled if their %(farkey)s matches
461         self.%(key)s. However, if self.%(key)s is None,
462         no Units will be recalled.
463         """ % {'farname': farClass.__name__,
464                'farkey': farKey,
465                'key': key,
466                }
467         value = getattr(self, key)
468         if value is None:
469             return iter([])
470        
471         # kwargs won't take unicode keys
472         f = logic.filter(**{str(farKey): value})
473         if expr is not None:
474             f += expr
475         return self.sandbox.recall(farClass, f)
476     return synapses
477
478
479 ###########################################################################
480 ##                                                                       ##
481 ##                              Sandboxes                                ##
482 ##                                                                       ##
483 ###########################################################################
484
485
486 class Sandbox(object):
487     """Data sandbox for Dejavu arenas.
488     
489     Each consumer (that is, each UI process) maintains a Sandbox for
490     managing Units. Sandboxes populate themselves with Units on a lazy
491     basis, allowing UI code to request data as it's needed. However, once
492     obtained, such Units are persisted (usually for the lifetime of the
493     thread); this important detail means that multiple requests for the
494     same Units result in multiple references to the same objects, rather
495     than multiple objects. Sandboxes are basically what Fowler calls
496     Identity Maps.
497     
498     The *REALLY* important thing to understand if you're customizing this
499     is that Sandboxes won't survive sharing across threads--DON'T TRY IT.
500     If you need to share unit data across requests, use or make an SM which
501     persists the data, and chain it with another, more normal SM.
502     
503     _cache(), _caches, and _stores are private for a reason--don't access
504     them from interface code--tell the Sandbox to do it for you.
505     """
506    
507     def __init__(self, arena):
508         self.arena = arena
509         self._caches = {}
510    
511     def memorize(self, unit):
512         """Attach a unit to this sandbox so that it will persist."""
513         cls = unit.__class__
514         unit.sandbox = self
515        
516         # Ask the store to accept the unit, assigning it an ID if
517         # necessary. The store should also set unit.dirty to False
518         # if it saves the whole unit state on this call or not.
519         store = self.arena.storage(cls)
520         if store:
521             store.reserve(unit)
522        
523         # Insert the unit into the cache.
524         self._cache(cls)[unit.ID] = unit
525        
526         # Do this at the end of the func, since most on_memorize
527         # will want to have an ID when called.
528         if hasattr(unit, "on_memorize"):
529             unit.on_memorize()
530    
531     def forget(self, unit):
532         """Destroy the unit, both in the cache and storage."""
533         cls = unit.__class__
534         store = self.arena.storage(cls)
535         if store:
536             store.destroy(unit)
537        
538         del self._cache(cls)[unit.ID]
539        
540         if hasattr(unit, "on_forget"):
541             unit.on_forget()
542        
543         unit.sandbox = None
544    
545     def recall(self, cls, expr=None, *args):
546         """recall(cls, expr=None) -> Recall units of cls which match expr.
547         
548         If additional args are supplied:
549         
550         1) They shall be of the form: (cls, expr, cls, expr, cls, [expr]).
551             The final expr is optional.
552         2) Each such secondary cls/expr pair will be recalled; however,
553             only those secondary Units which are associated with Units
554             in the primary set will be returned.
555         3) Instead of single Units, the yielded value will be a tuple of
556             Units, in the same order as the cls args were supplied. This
557             facilitates consumer code like:
558             
559                 for invoice, price in sandbox.recall(Invoice, f, Price):
560                     deal_with(invoice)
561                     deal_with(price)
562             
563         4) If any secondary Units are found, all combinations of those
564             Units, together with each primary Unit, will be returned.
565         5) If no secondary Units are recalled, then a token tuple will be
566             returned of the form (primary_unit, None). For example:
567             
568                 for invoice, price in sandbox.recall(Invoice, f, Price):
569                     if price is None:
570                         handle_no_prices(invoice)
571             
572         """
573        
574         store = self.arena.storage(cls)
575        
576         if args:
577             # Deal with multiple class/expr pairs.
578            
579             # Format extra class/expr pairs more rigorously
580             pairs = []
581             args = list(args)
582             while args:
583                 c = args.pop(0)
584                 if self.arena.storage(c) is not store:
585                     raise ValueError(u"recall() does not currently support "
586                                      u"multiple classes in disparate stores.")
587                 if args:
588                     e = args.pop(0)
589                 else:
590                     e = None
591                 pairs.append((c, e))
592            
593             # Don't try any in-memory techniques, just flush it all,
594             # and ask the SM to give us what we want.
595             self.flush(cls)
596             for c, e in pairs:
597                 self.flush(c)
598             for units in store.recall(cls, expr, pairs):
599                 confirmed = True
600                 for unit in units:
601                     if unit is not None:
602                         ID = unit.ID
603                         cache = self._cache(unit.__class__)
604                         if ID not in cache:
605                             cache[ID] = unit
606                         unit.sandbox = self
607                     if hasattr(unit, 'on_recall'):
608                         try:
609                             unit.on_recall()
610                         except UnrecallableError:
611                             confirmed = False
612                 if confirmed:
613                     yield units
614             raise StopIteration
615        
616         # Run through our cache first.
617         cache = self._cache(cls)
618         skip_cache = False
619        
620         if expr:
621             fc = expr.func.func_code
622             if (fc.co_code == '|\x00\x00i\x01\x00d\x01\x00j\x02\x00S'
623                 and fc.co_names[-1] == 'ID'):
624                 # Special-case the scenario where one Unit is
625                 # expected and called by ID. We should be able
626                 # to save a database hit.
627                 expectedID = fc.co_consts[-1]
628                 if cache.has_key(expectedID):
629                     unit = cache[expectedID]
630                     # Do NOT call on_recall here. That should be called
631                     # only at the Sandbox-SM boundary.
632                     yield unit
633                     raise StopIteration
634                 else:
635                     skip_cache = True
636        
637         if not skip_cache:
638             for unit in cache.itervalues():
639                 if expr is None or expr.evaluate(unit):
640                     # Do NOT call on_recall here. That should be called
641                     # only at the Sandbox-SM boundary.
642                     yield unit
643        
644         # Query Storage.
645         if store:
646             for unit in store.recall(cls, expr):
647                 ID = unit.ID
648                 # Very important that we check for existing unit, as its
649                 # state may have changed in memory but not in storage.
650                 if ID not in cache:
651                     cache[ID] = unit
652                     unit.sandbox = self
653                     confirmed = True
654                     if hasattr(unit, 'on_recall'):
655                         try:
656                             unit.on_recall()
657                         except UnrecallableError:
658                             confirmed = False
659                     if confirmed:
660                         yield unit
661    
662     def unit(self, cls, **kwargs):
663         """Recall a single Unit, else None.
664         
665         **kwargs will be combined into an Expression via logic.filter.
666             The first Unit matching that expression is returned; if no
667             Units match, None is returned.
668         
669         If you need a single Unit which matches a more complex
670             expression, use recall().next().
671         """
672         expr = None
673         if kwargs:
674             expr = logic.filter(**kwargs)
675         try:
676             return self.recall(cls, expr).next()
677         except StopIteration:
678             return None
679    
680     def distinct(self, cls, attrs, expr=None):
681         """Recall distinct Unit property values.
682         
683         If only one attribute is specified, a list of values will be returned.
684         If more than one attribute is specified, a zipped list will be returned.
685         
686         Notice that you can also use this function as a count() function
687         (in fact it's the only way to do it) by using attrs = ['ID'].
688         """
689         seen = {}
690         cache = self._cache(cls)
691         for unit in cache.itervalues():
692             if expr is None or expr.evaluate(unit):
693                 row = tuple([getattr(unit, attr) for attr in attrs])
694                 if row not in seen:
695                     seen[row] = None
696        
697         store = self.arena.storage(cls)
698         if store:
699             for row in store.distinct(cls, attrs, expr):
700                 if row not in seen:
701                     seen[row] = None
702        
703         seen = seen.keys()
704         seen.sort()
705         if len(attrs) == 1:
706             seen = [x[0] for x in seen]
707         return seen
708    
709     def count(self, cls, expr):
710         return len(self.distinct(cls, ['ID'], expr))
711    
712     ####################################
713     ##        Cache Management        ##
714     ####################################
715    
716     def _cache(self, cls):
717         """Return the cache for the specified class.
718         
719         This base class creates a new cache for each cls per request.
720         """
721         if cls not in self._caches:
722             self._caches[cls] = {}
723         return self._caches[cls]
724    
725     def purge(self, cls):
726         del self._caches[cls]
727    
728     def flush(self, cls):
729         """flush(cls) -> Repress all units of the specified class."""
730         cache = self._cache(cls)
731         store = self.arena.storage(cls)
732         while cache:
733             id, unit = cache.popitem()
734            
735             if hasattr(unit, "on_repress"):
736                 unit.on_repress()
737            
738             if store and unit.dirty:
739                 store.save(unit)
740    
741     def flush_all(self):
742         """flush_all() -> repress() all units."""
743         for cls in self._caches.iterkeys():
744             self.flush(cls)
745    
746     def repress(self, unit):
747         """repress(unit) -> Remove unit from cache (but don't destroy)."""
748         if hasattr(unit, "on_repress"):
749             unit.on_repress()
750        
751         cls = unit.__class__
752         store = self.arena.storage(cls)
753         if store and unit.dirty:
754             store.save(unit)
755        
756         del self._cache(cls)[unit.ID]
757
758
759 ###########################################################################
760 ##                                                                       ##
761 ##                               Errors                                  ##
762 ##                                                                       ##
763 ###########################################################################
764
765
766 class DejavuError(Exception):
767     """Base class for errors which occur within Dejavu."""
768     def __init__(self, *args):
769         self.args = args
770    
771     def __str__(self):
772         return u'\n'.join([unicode(eachArg) for eachArg in self.args])
773
774 class AssociationError(DejavuError):
775     """Exception raised when a Unit association fails."""
776     pass
777
778 class UnrecallableError(DejavuError):
779     """Exception raised when a Unit was sought but not recalled."""
780     pass
781
782
783 ###########################################################################
784 ##                                                                       ##
785 ##                           Logic functions                             ##
786 ##                                                                       ##
787 ###########################################################################
788
789
790 def icontains(a, b):
791     """Case-insensitive test b in a. Note the operand order."""
792     if a is None or b is None:
793         return False
794     return b.lower() in a.lower()
795
796 def icontainedby(a, b):
797     """Case-insensitive test a in b. Note the operand order."""
798     if a is None or b is None:
799         return False
800     return a.lower() in b.lower()
801
802 def istartswith(a, b):
803     """True if a starts with b (case-insensitive), False otherwise."""
804     if a is None or b is None:
805         return False
806     return a.lower().startswith(b.lower())
807
808 def iendswith(a, b):
809     """True if a ends with b (case-insensitive), False otherwise."""
810     if a is None or b is None:
811         return False
812     return a.lower().endswith(b.lower())
813
814 def ieq(a, b):
815     """True if a == b (case-insensitive), False otherwise."""
816     if a is None or b is None:
817         return False
818     return (a.lower() == b.lower())
819
820 def year(value):
821     """The year attribute of a date."""
822     if isinstance(value, (datetime.date, datetime.datetime)):
823         return value.year
824     else:
825         return None
826
827 def now():
828     """Late-bound datetime.datetime.now(). Taint this when early binding."""
829     return datetime.datetime.now()
830 now.bind_late = True
831
832 def today():
833     """Late-bound datetime.date.today(). Taint this when early binding."""
834     return datetime.date.today()
835 today.bind_late = True
836
837 def iscurrentweek(value):
838     """If value is in the current week, return True, else False."""
839     if isinstance(value, (datetime.date, datetime.datetime)):
840         return datetime.date.today().strftime('%W%Y') == value.strftime('%W%Y')
841     else:
842         return False
843 iscurrentweek.bind_late = True
844
845 # Inject these functions into the logic module's globals.
846 class _Empty(object): pass
847 _d = _Empty()
848 for _name in ['icontains', 'icontainedby', 'istartswith', 'iendswith',
849               'ieq', 'year', 'now', 'today', 'iscurrentweek']:
850     setattr(_d, _name, globals()[_name])
851 logic.dejavu = _d
Note: See TracBrowser for help on using the browser.