Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

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

root/trunk/arenas.py

Revision 101 (checked in by fumanchu, 8 years ago)

Fix for #20 (awareness of to-one or to-many).

  1. Magic Unit relation methods may now return a single Unit (or None) if they are to-one. To-many methods still return a list.
  2. dejavu.associate is replaced by Unit.associate, one_to_many, one_to_one, and many_to_one.
  3. Unit.first is removed.
  4. One-way associations are now possible via dejavu.ToOne? and .ToMany?. Customization of relation descriptors is easier (override UnitAssociation?.related).
  5. Unit._associations dict has changed protocol.
  6. Lots of module breakouts to make reading easier.
  • Property svn:eol-style set to native
Line 
1
2 import ConfigParser
3
4 from containers import Graph as _Graph
5 import logic as _logic
6
7 # logging flags (see Arena.logflags)
8 LOGSQL = 4
9 LOGCONN = 8
10
11 LOGMEMORIZE = 128
12 LOGRECALL = 256
13 LOGVIEW = 512
14 LOGREPRESS = 1024
15 LOGFORGET = 2048
16 LOGSANDBOX = LOGMEMORIZE | LOGRECALL | LOGVIEW | LOGREPRESS | LOGFORGET
17
18
19 class Arena(object):
20     """Arena(). A namespace/workspace for a Dejavu application."""
21    
22     def __init__(self):
23         self.defaultStore = None
24         self.stores = {}
25         self._registered_classes = {}
26         self.associations = _Graph(directed=False)
27         self.engine_functions = {}
28         self.logflags = 0
29    
30     def log(self, message, flag):
31         """Default logger (writes to stdout). Feel free to replace."""
32         if flag & self.logflags:
33             print message
34    
35     def load(self, configFileName):
36         """Load StorageManagers."""
37         parser = ConfigParser.ConfigParser()
38         # Make names case-sensitive by overriding optionxform.
39         parser.optionxform = unicode
40         parser.read(configFileName)
41        
42         stores = []
43         for section in parser.sections():
44             opts = dict(parser.items(section))
45             stores.append((int(opts.get("Load Order", "0")), section, opts))
46         stores.sort()
47        
48         for order, name, options in stores:
49             self.add_store(name, options[u'Class'], options)
50    
51     def add_store(self, name, store, options=None):
52         """add_store(name, store, options=None). Register a StorageManager.
53         
54         The 'store' argument may be the name of a Storage Manager class;
55         if so, it must be importable (that is, it must have the full dotted
56         package name).
57         """
58        
59         if isinstance(store, basestring):
60             import xray
61             store = xray.classes(store)(name, self, options or {})
62        
63         self.stores[name] = store
64         if not store.classnames:
65             # This store has no "classnames" list, which signals that it
66             # handles all classes which are not handled by other stores.
67             self.defaultStore = store
68         return store
69    
70     def remove_store(self, name):
71         if name in self.stores:
72             store = self.stores[name]
73            
74             # Disassociate all registered classes with this store.
75             for c in self._registered_classes.keys():
76                 if self._registered_classes[c] is store:
77                     self._registered_classes[c] = None
78            
79             del self.stores[name]
80    
81     def shutdown(self):
82         """Shutdown the arena."""
83         # Tell all stores to shut down.
84         stores = [(v.shutdownOrder, v, k) for k, v in self.stores.iteritems()]
85         stores.sort()
86         for order, store, name in stores:
87             store.shutdown()
88    
89     def new_sandbox(self):
90         return Sandbox(self)
91    
92     ###########################################
93     ##        Unit Class Registration        ##
94     ###########################################
95    
96     def register(self, cls):
97         """register(cls) -> Assert that Units of class 'cls' will be handled."""
98         # We must allow modules to register classes before any stores have
99         # been added, but not overwrite a store which has already been found.
100         if cls not in self._registered_classes:
101             self._registered_classes[cls] = None
102        
103         # Register any association(s) in an undirected graph.
104         for ua in cls._associations.itervalues():
105             self.associations.connect(cls, ua.farClass)
106    
107     def register_all(self, globals):
108         import dejavu
109         for obj in globals.itervalues():
110             if isinstance(obj, type) and issubclass(obj, dejavu.Unit):
111                 self.register(obj)
112    
113     def class_by_name(self, classname):
114         for cls in self._registered_classes:
115             if cls.__name__ == classname:
116                 return cls
117         raise KeyError("No registered class found for '%s'." % classname)
118    
119     def storage(self, cls):
120         found = self._registered_classes.get(cls)
121        
122         if found:
123             return found
124        
125         # Search all stores for the class name.
126         clsname = cls.__name__
127         for store in self.stores.itervalues():
128             if clsname in store.classnames:
129                 found = store
130                 break
131         found = found or self.defaultStore
132         if found is None:
133             raise KeyError("No store found for '%s' and no "
134                            "default store." % clsname)
135        
136         self._registered_classes[cls] = found
137         return found
138    
139     def create_storage(self, cls):
140         """create_storage(cls). Create storage space for cls."""
141         self.storage(cls).create_storage(cls)
142    
143     def migrate_class(self, cls, new_store):
144         """migrate_class(cls, new_store). Copy all units of cls to new_store."""
145         new_store.create_storage(cls)
146         for unit in self.new_sandbox().xrecall(cls):
147             new_store.reserve(unit)
148             new_store.save(unit, True)
149    
150     def migrate(self, new_store, old_store=None, copy_only=False):
151         """migrate(new_store, old_store=None). Copy all units (of old_store) to new_store."""
152         for cls in self._registered_classes:
153             store = self.storage(cls)
154             if old_store is None or old_store is store:
155                 self.migrate_class(cls, new_store)
156                 if not copy_only:
157                     self._registered_classes[cls] = new_store
158
159
160 ###########################################################################
161 ##                                                                       ##
162 ##                              Sandboxes                                ##
163 ##                                                                       ##
164 ###########################################################################
165
166
167 class Sandbox(object):
168     """Sandbox(arena). Data sandbox for Dejavu arenas.
169     
170     Each consumer (that is, each UI process) maintains a Sandbox for
171     managing Units. Sandboxes populate themselves with Units on a lazy
172     basis, allowing UI code to request data as it's needed. However, once
173     obtained, such Units are persisted (usually for the lifetime of the
174     thread); this important detail means that multiple requests for the
175     same Units result in multiple references to the same objects, rather
176     than multiple objects. Sandboxes are basically what Fowler calls
177     Identity Maps.
178     
179     The *REALLY* important thing to understand if you're customizing this
180     is that Sandboxes won't survive sharing across threads--DON'T TRY IT.
181     If you need to share unit data across requests, use or make an SM which
182     persists the data, and chain it with another, more normal SM.
183     
184     _cache(), _caches, and _stores are private for a reason--don't access
185     them from interface code--tell the Sandbox to do it for you.
186     """
187    
188     def __init__(self, arena):
189         self.arena = arena
190         self._caches = {}
191    
192     def memorize(self, unit):
193         """memorize(unit). Persist unit in storage."""
194         cls = unit.__class__
195         unit.sandbox = self
196        
197         # Ask the store to accept the unit, assigning it an ID if
198         # necessary. The store should also call unit.cleanse()
199         # if it saves the whole unit state on this call.
200         self.arena.storage(cls).reserve(unit)
201        
202         # Insert the unit into the cache.
203         self._cache(cls)[unit.ID] = unit
204         self.arena.log("MEMORIZE %s: %s" % (cls.__name__, unit.ID), LOGMEMORIZE)
205        
206         # Do this at the end of the func, since most on_memorize
207         # will want to have an ID when called.
208         if hasattr(unit, "on_memorize"):
209             unit.on_memorize()
210    
211     def forget(self, unit):
212         """Destroy unit, both in the cache and storage."""
213         cls = unit.__class__
214        
215         self.arena.log("FORGET %s: %s" % (cls.__name__, unit.ID), LOGFORGET)
216         self.arena.storage(cls).destroy(unit)
217        
218         del self._cache(cls)[unit.ID]
219        
220         if hasattr(unit, "on_forget"):
221             unit.on_forget()
222        
223         unit.sandbox = None
224    
225     def xrecall(self, cls, expr=None):
226         """Iterator over units of cls which match expr."""
227        
228         self.arena.log("RECALL %s: %s" % (cls.__name__, expr), LOGRECALL)
229        
230         cache = self._cache(cls)
231        
232         # Special-case the scenario where one Unit is expected and called
233         # by ID. We should be able to save a database hit.
234         if expr:
235             fc = expr.func.func_code
236             if (fc.co_code == '|\x00\x00i\x01\x00d\x01\x00j\x02\x00S' and
237                 fc.co_names[-1] == 'ID'):
238                 ID = fc.co_consts[-1]
239                 unit = cache.get(ID)
240                 if unit is not None:
241                     # Do NOT call on_recall here. That should be called
242                     # only at the Sandbox-SM boundary.
243                     yield unit
244                     raise StopIteration
245        
246         # Query Cache and Storage.
247         for unit in self.arena.storage(cls).recall(cls, expr):
248             ID = unit.ID
249             # Very important that we check for existing unit, as its
250             # state may have changed in memory but not in storage.
251             # Make sure the cache lookup and get happens atomically.
252             existing = cache.get(ID)
253             if existing:
254                 yield existing
255             else:
256                 unit.sandbox = self
257                 confirmed = True
258                 cache[ID] = unit
259                 if hasattr(unit, 'on_recall'):
260                     try:
261                         unit.on_recall()
262                     except UnrecallableError:
263                         confirmed = False
264                 if confirmed:
265                     yield unit
266    
267     def recall(self, cls, expr=None):
268         """List of units of class 'cls' which match expr."""
269         return [x for x in self.xrecall(cls, expr)]
270    
271     def multirecall(self, *pairs):
272         """multirecall((cls1, expr1), ...) -> [[unit, ...], [unit, ...], ...]
273         Recall units of each cls which match each expr.
274         
275         Units of each additional cls/expr pair will be recalled; however,
276         only those Units with associations to Units in the PRIMARY set will
277         be returned. For you database guys, it's a set of inner joins,
278         ALL of which are between the FIRST set and the subsequent set(s).
279         
280         Instead of single Units, each yielded value will be a tuple of
281         Units, in the same order as the cls args were supplied. This
282         facilitates unpacking in iterative consumer code like:
283         
284         for invoice, price in sandbox.multirecall(Invoice, f, Price, None):
285             deal_with(invoice)
286             deal_with(price)
287         """
288        
289         self.arena.log("RECALL %s" % ", ".join(["(%s: %s)" % (c.__name__, e)
290                                                 for c, e in pairs]),
291                                                 LOGRECALL)
292         store = self.arena.storage(pairs[0][0])
293         for c, e in pairs:
294             if self.arena.storage(c) is not store:
295                 raise ValueError(u"multirecall() does not support multiple"
296                                  u" classes in disparate stores.")
297        
298         # This is broken. If a filter expr is supplied, then the store may
299         # not return rows which our cache would, and those won't be included
300         # in the resultset. If you're using multirecall with no expr's, or
301         # in read-only scripts, it should be OK for now. But if you mutate
302         # Units and then call multirecall, expect inconsistent results.
303         for unitset in store.multirecall(*pairs):
304             confirmed = True
305             for index in xrange(len(unitset)):
306                 unit = unitset[index]
307                 ID = unit.ID
308                 cache = self._cache(unit.__class__)
309                 if ID in cache:
310                     # Keep the unit which is in our cache!
311                     unitset[index] = cache[ID]
312                 else:
313                     cache[ID] = unit
314                     unit.sandbox = self
315                     if hasattr(unit, 'on_recall'):
316                         try:
317                             unit.on_recall()
318                         except UnrecallableError:
319                             confirmed = False
320                             break
321             if confirmed:
322                 yield unitset
323    
324     def unit(self, cls, **kwargs):
325         """unit(cls, **kwargs) -> A single matching Unit, else None.
326         
327         **kwargs will be combined into an Expression via logic.filter.
328             The first Unit matching that expression is returned; if no
329             Units match, None is returned.
330         
331         If you need a single Unit which matches a more complex
332             expression, use recall()[0] or xrecall().next().
333         """
334         expr = None
335         if kwargs:
336             expr = _logic.filter(**kwargs)
337         try:
338             return self.xrecall(cls, expr).next()
339         except StopIteration:
340             return None
341    
342     def view(self, cls, attrs, expr=None):
343         """view(cls, attrs, expr=None) -> Iterator of all Property tuples."""
344         self.arena.log("VIEW %s [%s]: %s" % (cls.__name__, attrs, expr), LOGVIEW)
345        
346         cache = self._cache(cls)
347        
348         for unit in cache.itervalues():
349             if expr is None or expr.evaluate(unit):
350                 yield tuple([getattr(unit, attr) for attr in attrs])
351        
352         # Add the ID attribute if not present. This is necessary to
353         # avoid duplicating objects which are already in our cache.
354         fields = list(attrs)
355         if "ID" not in fields:
356             fields.append("ID")
357         index_of_id = fields.index("ID")
358        
359         for row in self.arena.storage(cls).view(cls, fields, expr):
360             if row[index_of_id] not in cache:
361                 if "ID" not in attrs:
362                     # Remove the ID column from the tuple.
363                     row = row[:len(row) - 1]
364                 yield row
365    
366     def distinct(self, cls, attrs, expr=None):
367         """distinct(cls, attrs, expr=None) -> List of distinct Property tuples.
368         
369         If only one attribute is specified, a list of values will be returned.
370         If more than one attribute is specified, a zipped list will be returned.
371         
372         Notice that you can also use this function as a count() function
373         (in fact it's the only way to do it) by using attrs = ['ID'].
374         """
375         self.arena.log("DISTINCT %s [%s]: %s" % (cls.__name__, attrs, expr), LOGVIEW)
376        
377         seen = {}
378         cache = self._cache(cls)
379         for unit in cache.itervalues():
380             if expr is None or expr.evaluate(unit):
381                 row = tuple([getattr(unit, attr) for attr in attrs])
382                 if row not in seen:
383                     seen[row] = None
384        
385         for row in self.arena.storage(cls).distinct(cls, attrs, expr):
386             if row not in seen:
387                 seen[row] = None
388        
389         seen = seen.keys()
390         seen.sort()
391         if len(attrs) == 1:
392             seen = [x[0] for x in seen]
393         return seen
394    
395     def count(self, cls, expr):
396         """count(cls, expr) -> Number of Units of class 'cls'."""
397         return len(self.distinct(cls, ['ID'], expr))
398    
399     ####################################
400     ##        Cache Management        ##
401     ####################################
402    
403     def _cache(self, cls):
404         """cache(cls). Return the cache for the specified class.
405         
406         This base class creates a new cache for each cls per request.
407         """
408         if cls not in self._caches:
409             self._caches[cls] = {}
410         return self._caches[cls]
411    
412     def purge(self, cls):
413         """purge(cls). Drop all cached Units of class 'cls'. Do not save."""
414         del self._caches[cls]
415    
416     def repress(self, unit):
417         """repress(unit). Remove unit from cache (but don't destroy)."""
418         cls = unit.__class__
419         self.arena.log("REPRESS %s: %s" % (cls.__name__, unit.ID), LOGREPRESS)
420        
421         if hasattr(unit, "on_repress"):
422             unit.on_repress()
423        
424         # Save after on_repress in case on_repress modified the unit.
425         self.arena.storage(cls).save(unit)
426        
427         del self._cache(cls)[unit.ID]
428    
429     def flush_all(self):
430         """flush_all(). Repress all units."""
431        
432         for cls in self._caches.keys():
433             # Call all on_repress methods first! There are truly horrible
434             # interdependency chains in most on_repress methods, and
435             # it's best to resolve them all at once BEFORE flushing
436             # any units from the cache.
437             # Note we use values instead of itervalues, since the
438             # cache may change size during iteration.
439             for unit in self._cache(cls).values():
440                 if hasattr(unit, "on_repress"):
441                     unit.on_repress()
442        
443         for cls in self._caches.keys():
444             cache = self._cache(cls)
445             store = self.arena.storage(cls)
446             while cache:
447                 unitid, unit = cache.popitem()
448                 self.arena.log("REPRESS %s: %s" % (cls.__name__, unitid), LOGREPRESS)
449                 store.save(unit)
450
Note: See TracBrowser for help on using the browser.