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

1. Dejavu now adds methods to FixedPoint? for pickling if not present.
2. Seleciton of defaultStore deferred to storage() call.
3. New shelve StorageManager? + tests.
4. New zoo test application.
5. New SM.create_database methods for storeado.

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 # You can use this arena instance if you are
544 # deploying a single application per process.
545 # Otherwise, you should create your own instance
546 # per application.
547 dejavuarena = Arena()
548
549 def _synapses_func(key, farClass, farKey):
550     """Produce a new synapses() function to be bound to a Unit class."""
551     def synapses(self, expr=None):
552         """Recall associated '%(farname)s' Units.
553         
554         %(farname)s Units will be recalled if their %(farkey)s matches
555         self.%(key)s. However, if self.%(key)s is None,
556         no Units will be recalled.
557         """ % {'farname': farClass.__name__,
558                'farkey': farKey,
559                'key': key,
560                }
561         value = getattr(self, key)
562         if value is None:
563             return iter([])
564        
565         # kwargs won't take unicode keys
566         f = logic.filter(**{str(farKey): value})
567         if expr is not None:
568             f += expr
569         return self.sandbox.recall(farClass, f)
570     return synapses
571
572
573 ###########################################################################
574 ##                                                                       ##
575 ##                              Sandboxes                                ##
576 ##                                                                       ##
577 ###########################################################################
578
579
580 class Sandbox(object):
581     """Data sandbox for Dejavu arenas.
582     
583     Each consumer (that is, each UI process) maintains a Sandbox for
584     managing Units. Sandboxes populate themselves with Units on a lazy
585     basis, allowing UI code to request data as it's needed. However, once
586     obtained, such Units are persisted (usually for the lifetime of the
587     thread); this important detail means that multiple requests for the
588     same Units result in multiple references to the same objects, rather
589     than multiple objects. Sandboxes are basically what Fowler calls
590     Identity Maps.
591     
592     The *REALLY* important thing to understand if you're customizing this
593     is that Sandboxes won't survive sharing across threads--DON'T TRY IT.
594     If you need to share unit data across requests, use or make an SM which
595     persists the data, and chain it with another, more normal SM.
596     
597     _cache(), _caches, and _stores are private for a reason--don't access
598     them from interface code--tell the Sandbox to do it for you.
599     """
600    
601     def __init__(self, arena):
602         self.arena = arena
603         self._caches = {}
604    
605     def memorize(self, unit):
606         """Attach a unit to this sandbox so that it will persist."""
607         cls = unit.__class__
608         unit.sandbox = self
609        
610         # Ask the store to accept the unit, assigning it an ID if
611         # necessary. The store should also call unit.cleanse()
612         # if it saves the whole unit state on this call.
613         store = self.arena.storage(cls)
614         if store:
615             store.reserve(unit)
616        
617         # Insert the unit into the cache.
618         self._cache(cls)[unit.ID] = unit
619        
620         # Do this at the end of the func, since most on_memorize
621         # will want to have an ID when called.
622         if hasattr(unit, "on_memorize"):
623             unit.on_memorize()
624    
625     def forget(self, unit):
626         """Destroy the unit, both in the cache and storage."""
627         cls = unit.__class__
628         store = self.arena.storage(cls)
629         if store:
630             store.destroy(unit)
631        
632         del self._cache(cls)[unit.ID]
633        
634         if hasattr(unit, "on_forget"):
635             unit.on_forget()
636        
637         unit.sandbox = None
638    
639     def recall(self, cls, expr=None, *args):
640         """recall(cls, expr=None) -> Recall units of cls which match expr.
641         
642         If additional args are supplied:
643         
644         1) They shall be of the form: (cls, expr, cls, expr, cls, [expr]).
645             The final expr is optional.
646         2) Each such secondary cls/expr pair will be recalled; however,
647             only those secondary Units which are associated with Units
648             in the primary set will be returned.
649         3) Instead of single Units, the yielded value will be a tuple of
650             Units, in the same order as the cls args were supplied. This
651             facilitates consumer code like:
652             
653                 for invoice, price in sandbox.recall(Invoice, f, Price):
654                     deal_with(invoice)
655                     deal_with(price)
656             
657         4) If any secondary Units are found, all combinations of those
658             Units, together with each primary Unit, will be returned.
659         5) If no secondary Units are recalled, then a token tuple will be
660             returned of the form (primary_unit, None). For example:
661             
662                 for invoice, price in sandbox.recall(Invoice, f, Price):
663                     if price is None:
664                         handle_no_prices(invoice)
665             
666         """
667        
668         store = self.arena.storage(cls)
669        
670         if args:
671             # Deal with multiple class/expr pairs.
672            
673             # Format extra class/expr pairs more rigorously
674             pairs = []
675             args = list(args)
676             while args:
677                 c = args.pop(0)
678                 if self.arena.storage(c) is not store:
679                     raise ValueError(u"recall() does not currently support "
680                                      u"multiple classes in disparate stores.")
681                 if args:
682                     e = args.pop(0)
683                 else:
684                     e = None
685                 pairs.append((c, e))
686            
687             # Don't try any in-memory techniques, just flush it all,
688             # and ask the SM to give us what we want.
689             self.flush(cls)
690             for c, e in pairs:
691                 self.flush(c)
692             for units in store.recall(cls, expr, pairs):
693                 confirmed = True
694                 for unit in units:
695                     if unit is not None:
696                         ID = unit.ID
697                         cache = self._cache(unit.__class__)
698                         if ID not in cache:
699                             cache[ID] = unit
700                         unit.sandbox = self
701                     if hasattr(unit, 'on_recall'):
702                         try:
703                             unit.on_recall()
704                         except UnrecallableError:
705                             confirmed = False
706                 if confirmed:
707                     yield units
708             raise StopIteration
709        
710         # Run through our cache first.
711         cache = self._cache(cls)
712         skip_cache = False
713        
714         if expr:
715             fc = expr.func.func_code
716             if (fc.co_code == '|\x00\x00i\x01\x00d\x01\x00j\x02\x00S'
717                 and fc.co_names[-1] == 'ID'):
718                 # Special-case the scenario where one Unit is
719                 # expected and called by ID. We should be able
720                 # to save a database hit.
721                 expectedID = fc.co_consts[-1]
722                 if cache.has_key(expectedID):
723                     unit = cache[expectedID]
724                     # Do NOT call on_recall here. That should be called
725                     # only at the Sandbox-SM boundary.
726                     yield unit
727                     raise StopIteration
728                 else:
729                     skip_cache = True
730        
731         if not skip_cache:
732             for unit in cache.itervalues():
733                 if expr is None or expr.evaluate(unit):
734                     # Do NOT call on_recall here. That should be called
735                     # only at the Sandbox-SM boundary.
736                     yield unit
737        
738         # Query Storage.
739         if store:
740             for unit in store.recall(cls, expr):
741                 ID = unit.ID
742                 # Very important that we check for existing unit, as its
743                 # state may have changed in memory but not in storage.
744                 if ID not in cache:
745                     cache[ID] = unit
746                     unit.sandbox = self
747                     confirmed = True
748                     if hasattr(unit, 'on_recall'):
749                         try:
750                             unit.on_recall()
751                         except UnrecallableError:
752                             confirmed = False
753                     if confirmed:
754                         yield unit
755    
756     def unit(self, cls, **kwargs):
757         """Recall a single Unit, else None.
758         
759         **kwargs will be combined into an Expression via logic.filter.
760             The first Unit matching that expression is returned; if no
761             Units match, None is returned.
762         
763         If you need a single Unit which matches a more complex
764             expression, use recall().next().
765         """
766         expr = None
767         if kwargs:
768             expr = logic.filter(**kwargs)
769         try:
770             return self.recall(cls, expr).next()
771         except StopIteration:
772             return None
773    
774     def distinct(self, cls, attrs, expr=None):
775         """Recall distinct Unit property values.
776         
777         If only one attribute is specified, a list of values will be returned.
778         If more than one attribute is specified, a zipped list will be returned.
779         
780         Notice that you can also use this function as a count() function
781         (in fact it's the only way to do it) by using attrs = ['ID'].
782         """
783         seen = {}
784         cache = self._cache(cls)
785         for unit in cache.itervalues():
786             if expr is None or expr.evaluate(unit):
787                 row = tuple([getattr(unit, attr) for attr in attrs])
788                 if row not in seen:
789                     seen[row] = None
790        
791         store = self.arena.storage(cls)
792         if store:
793             for row in store.distinct(cls, attrs, expr):
794                 if row not in seen:
795                     seen[row] = None
796        
797         seen = seen.keys()
798         seen.sort()
799         if len(attrs) == 1:
800             seen = [x[0] for x in seen]
801         return seen
802    
803     def count(self, cls, expr):
804         return len(self.distinct(cls, ['ID'], expr))
805    
806     ####################################
807     ##        Cache Management        ##
808     ####################################
809    
810     def _cache(self, cls):
811         """Return the cache for the specified class.
812         
813         This base class creates a new cache for each cls per request.
814         """
815         if cls not in self._caches:
816             self._caches[cls] = {}
817         return self._caches[cls]
818    
819     def purge(self, cls):
820         del self._caches[cls]
821    
822     def flush(self, cls):
823         """flush(cls) -> Repress all units of the specified class."""
824         cache = self._cache(cls)
825         store = self.arena.storage(cls)
826         while cache:
827             id, unit = cache.popitem()
828            
829             if hasattr(unit, "on_repress"):
830                 unit.on_repress()
831            
832             if store and unit.dirty():
833                 store.save(unit)
834    
835     def flush_all(self):
836         """flush_all() -> repress() all units."""
837         for cls in self._caches.iterkeys():
838             self.flush(cls)
839    
840     def repress(self, unit):
841         """repress(unit) -> Remove unit from cache (but don't destroy)."""
842         if hasattr(unit, "on_repress"):
843             unit.on_repress()
844        
845         cls = unit.__class__
846         store = self.arena.storage(cls)
847         if store and unit.dirty():
848             store.save(unit)
849        
850         del self._cache(cls)[unit.ID]
851
852
853 ###########################################################################
854 ##                                                                       ##
855 ##                               Errors                                  ##
856 ##                                                                       ##
857 ###########################################################################
858
859
860 class DejavuError(Exception):
861     """Base class for errors which occur within Dejavu."""
862     def __init__(self, *args):
863         self.args = args
864    
865     def __str__(self):
866         return u'\n'.join([unicode(eachArg) for eachArg in self.args])
867
868 class AssociationError(DejavuError):
869     """Exception raised when a Unit association fails."""
870     pass
871
872 class UnrecallableError(DejavuError):
873     """Exception raised when a Unit was sought but not recalled."""
874     pass
875
876
877 ###########################################################################
878 ##                                                                       ##
879 ##                           Logic functions                             ##
880 ##                                                                       ##
881 ###########################################################################
882
883
884 def icontains(a, b):
885     """Case-insensitive test b in a. Note the operand order."""
886     if a is None or b is None:
887         return False
888     return b.lower() in a.lower()
889
890 def icontainedby(a, b):
891     """Case-insensitive test a in b. Note the operand order."""
892     if a is None or b is None:
893         return False
894     return a.lower() in b.lower()
895
896 def istartswith(a, b):
897     """True if a starts with b (case-insensitive), False otherwise."""
898     if a is None or b is None:
899         return False
900     return a.lower().startswith(b.lower())
901
902 def iendswith(a, b):
903     """True if a ends with b (case-insensitive), False otherwise."""
904     if a is None or b is None:
905         return False
906     return a.lower().endswith(b.lower())
907
908 def ieq(a, b):
909     """True if a == b (case-insensitive), False otherwise."""
910     if a is None or b is None:
911         return False
912     return (a.lower() == b.lower())
913
914 def year(value):
915     """The year attribute of a date."""
916     if isinstance(value, (datetime.date, datetime.datetime)):
917         return value.year
918     else:
919         return None
920
921 def now():
922     """Late-bound datetime.datetime.now(). Taint this when early binding."""
923     return datetime.datetime.now()
924 now.bind_late = True
925
926 def today():
927     """Late-bound datetime.date.today(). Taint this when early binding."""
928     return datetime.date.today()
929 today.bind_late = True
930
931 def iscurrentweek(value):
932     """If value is in the current week, return True, else False."""
933     if isinstance(value, (datetime.date, datetime.datetime)):
934         return datetime.date.today().strftime('%W%Y') == value.strftime('%W%Y')
935     else:
936         return False
937 iscurrentweek.bind_late = True
938
939 # Inject these functions into the logic module's globals.
940 class _Empty(object): pass
941 _d = _Empty()
942 for _name in ['icontains', 'icontainedby', 'istartswith', 'iendswith',
943               'ieq', 'year', 'now', 'today', 'iscurrentweek']:
944     setattr(_d, _name, globals()[_name])
945 logic.dejavu = _d
Note: See TracBrowser for help on using the browser.