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 37 (checked in by fumanchu, 8 years ago)

Added zoo.py, a test application.

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