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

Allow Unit properties to be set in Unit.init() by **kwargs.

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