#!/usr/bin/env python #@+leo #@+node:0::@file metakitplus.py #@+body #@@first #@@language python """ metakitplus.py (Viewing/editing the source code with Leo - U{http://leo.sf.net} - is highly advisable) Metakit Plus is a convenience and enhancement wrapper for Metakit for Python (U{http://www.equi4.com/metakit/python.html}). This module wraps the metakit classes, and provides a set of enhancement methods for these classes in a way that makes Metakit much more easily extensible. The derived classes here are trivial to subclass. Homepage of this module is U{http://www.freenet.org.nz/python/metakit} This module was written by David McNab, david at freenet dot org dot nz Copyright (C) 2003, David McNab. Released under the GNU General Public License (refer U{http://gnu.org}). No warranty, yada yada. """ #@+others #@+node:1::globals #@+body version = "0.1.0" #@-body #@-node:1::globals #@+node:2::imports #@+body try: import warnings warnings.filterwarnings('ignore', 'Python C API version mismatch', RuntimeWarning, ) except: pass import metakit #@-body #@-node:2::imports #@+node:3::METAKIT WRAPPERS #@+node:1::class view #@+body class view: """ Convenience wrapper for metakit views """ #@+others #@+node:1::__init__ #@+body def __init__(self, v=None): """ Constructor for wrapped view class RTFM Metakit for more info """ if v == None: # invoking metakit.view() self._view = metakit.view() elif repr(type(v)) in ["", "", ""]: # Wrapping an existing view self._view = v self._view1 = None self._view2 = None self._method = None self._args = None elif isinstance(v, view): while isinstance(v, view): v = v._view self._view = v self._view1 = None self._view2 = None self._method = None self._args = None else: print "Suss: arg type = %s" % type(v) raise Exception("Cannot wrap non-view object") #@-body #@-node:1::__init__ #@+node:2::Wrapper Methods #@+body # # Functions to support access to attributes and subscripted items # def __getattr__(self, attr): """ Supports attribute access for our view subclass """ # Assume anything unknown is an attribute of the actual view object #print "view: seeking attribute '%s'" % attr if attr in ['fieldNames','fields','columns']: return self._fieldNames() else: return getattr(self._view, attr) def __getitem__(self, idx): """ Support subscripting of our view subclass """ # if subscripted, apply the subscript to the wrapped view object return self._view[idx] def __setitem__(self, idx, val): """ Support subscripting and cell assignment for our view subclass """ # set the indexed item of the wrapped view # unwrap row objects while isinstance(val, row): val = val._row self._view[idx] = val def __len__(self): """ Number of rows in this group """ return len(self._view) def __getslice__(self, fromidx, toidx): """ Supports slice retrieval for our view subclass """ return self.__class__(self._view[fromidx:toidx]) def __setslice__(self, fromidx, toidx, newslice): """ Supports slice assignment for our view subclass """ self._view[fromidx:toidx] = newslice # # Wrap existing view methods # def filter(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.filter, args, kw)) newview._view1 = self return newview def indices(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.indices, args, kw)) newview._view1 = self return newview def copy(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.copy, args, kw)) newview._view1 = self return newview def select(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.select, args, kw)) newview._view1 = self return newview def sort(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.sort, args, kw)) newview._view1 = self return newview def sortrev(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.sortrev, args, kw)) newview._view1 = self return newview def project(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.project, args, kw)) newview._view1 = self return newview def flatten(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.flatten, args, kw)) newview._view1 = self return newview def join(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ args = list(args) view2 = args[0] while isinstance(args[0], view): args[0] = args[0]._view newview = self.__class__(apply(self._view.join, args, kw)) newview._view1 = self newview._view2 = view2 newview._method = self._view.join newview._args = args return newview def find(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.find, args, kw)) newview._view1 = self return newview def search(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.search, args, kw)) newview._view1 = self return newview def unique(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.unique, args, kw)) newview._view1 = self return newview def union(self, v): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ while isinstance(v, view): v = v._view nweview = self.__class__(self._view.union(v)) newview._view1 = self newview._view2 = v return newview def intersect(self, v): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ while isinstance(v, view): v = v._view newview = self.__class__(self._view.intersect(v)) newview._view1 = self newview._view2 = v return newview def different(self, v): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ while isinstance(v, view): v = v._view newview = self.__class__(self._view.different(v)) newview._view1 = self newview._view2 = v return newview def minus(self, v): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ while isinstance(v, view): v = v._view newview = self.__class__(self._view.minus(v)) newview._view1 = self newview._view2 = v return newview def remapwith(self, v): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ while isinstance(v, view): v = v._view newview = self.__class__(self._view.remapwith(v)) newview._view1 = self newview._view2 = v return newview def pair(self, v): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ while isinstance(v, view): v = v._view newview = self.__class__(self._view.pair(v)) newview._view1 = self newview._view2 = v return newview def rename(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.rename, args, kw)) newview._view1 = self return newview def product(self, v): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ while isinstance(v, view): v = v._view newview = self.__class__(self._view.product(v)) newview._view1 = self newview._view2 = v return newview def groupby(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.groupby, args, kw)) newview._view1 = self return newview def counts(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.counts, args, kw)) newview._view1 = self return newview def hash(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.hash, args, kw)) newview._view1 = self return newview def blocked(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.blocked, args, kw)) newview._view1 = self return newview def ordered(self, *args, **kw): """ RTFM - U{http://www.equi4.com/metakit/python.html} """ newview = self.__class__(apply(self._view.ordered, args, kw)) newview._view1 = self return newview #@-body #@-node:2::Wrapper Methods #@+node:3::New Convenience Methods #@+body # # Now toss in a whole swathe of convenience methods # #@-body #@+node:1::fieldNames #@+body def _fieldNames(v): struct = v.structure() fields = [] for f in struct: fields.append(f.name) return fields #@-body #@-node:1::fieldNames #@+node:2::getRowAsDict #@+body def getRowAsDict(self, idx): """ Returns a dict with keys,values in row """ fields = self.fieldNames row = self[idx] d = {} for f in fields: d[f] = getattr(row, f) return d #@-body #@-node:2::getRowAsDict #@+node:3::getRowAsList #@+body def getRowAsList(self, idx): """ return the column 'colname' of each row, as a list """ row = self[idx] fields = [] for fld in self.fields: fields.append(getattr(row, fld)) return fields #@-body #@-node:3::getRowAsList #@+node:4::getColAsList #@+body def getColAsList(self, colname): """ return the column 'colname' of each row, as a list """ if colname not in self.columns: raise Exception("Column '%s' does not exist in this view" % colname) cols = [] for r in self._view: cols.append(getattr(r, colname)) return cols #@-body #@-node:4::getColAsList #@+node:5::getFiltered #@+body def getFiltered(self, func): """ shorthand for view.remapwith(view.filter(func)) Returns a view whose rows satisfy 'func' """ return self.remapwith(self.filter(func)) #@-body #@-node:5::getFiltered #@+node:6::onlyWhen #@+body def onlyWhen(self, func): """ Synonym for 'getFiltered' Returns a view that contains only rows satisfying func """ return self.remapwith(self.filter(func)) #@-body #@-node:6::onlyWhen #@+node:7::hasRowsWhen #@+body def hasRowsWhen(self, func): """ Returns 1 if the table has any rows satisfying 'func', or 0 if not """ return len(self.getFiltered(func)) > 0 #@-body #@-node:7::hasRowsWhen #@+node:8::filterInPlace #@+body def filterInPlace(self, func): """ Removes from this view all rows which do not satisfy func. Func should accept one argument - the row to test - and should return 1 if the row should be kept, or 0 if it should be discarded This function is opposite to L{removeWhen} """ self[:] = self.getFiltered(func) #@-body #@-node:8::filterInPlace #@+node:9::setWhen #@+body def setWhen(self, func, values=None, **kw): """ viewSetWhen(view, func, values) When func(row) returns true, set fields in view to those in dict 'values' Example: - Change Mary Pike's surname to married name of 'Fisher':: myview.setWhen(lambda r: r.first=='Mary' and r.last!='Pike', {'last':'Fisher'}) - This roughly corresponds to SQL syntax like:: UPDATE myview SET last='Fisher' WHERE first='Mary' AND last='Pike' """ v = {} if values: v.update(values) v.update(kw) if self._view1 or self._view2: if self._view1: apply(self._view1.setWhen, (func, v)) if self._view1: apply(self._view2.setWhen, (func, v)) else: subset = self.getFiltered(func) subset.map(rowSetFunc(v)) #@-body #@-node:9::setWhen #@+node:10::removeWhen #@+body def removeWhen(self, func): """ Shorthand for view.filter... view.remove Args: - func - a lambda func accepting row which returns 1 if should delete that row Returns: - the same view, with the desired rows deleted Example: - get rid of all records for 'Mary Smith':: myview.removeWhen(lambda r: r.firstname == 'Mary' and r.lastname != 'Smith') - corresponding SQL syntax would be:: DELETE FROM myview WHERE firstname='Mary' AND lastname='Smith' """ if self._view1 or self._view2: if self._view1: self._view1.removeWhen(func) if self._view2: self._view2.removeWhen(func) if self._method: self._view = apply(self._method, self._args) else: self.remove(self._view.filter(func)) #@-body #@-node:10::removeWhen #@+node:11::purgeDuplicates #@+body def purgeDuplicates(self): self[:] = self.unique() #@-body #@-node:11::purgeDuplicates #@+node:12::dump #@+body def dump(self): dump(self) #@-body #@-node:12::dump #@-node:3::New Convenience Methods #@-others #@-body #@-node:1::class view #@+node:2::class storage #@+body class storage: """ Convenience wrapper for metakit 'storage' class """ #@+others #@+node:1::Wrapper methods #@+body # # Wrapper methods to make this look/feel/smell like a real metakit storage object # def __init__(self, *args, **kw): """ Creates a metakit-plus 'storage' class, which wraps metakit's storage objects. If you want to subclass Metakit Plus's 'view' class, you'll have to invoke this constructor with the keyword arg "viewclass=myNewViewClass". Once you've done this, you'll find that all views derived from your storage object will be in your own view class, as will be any views you derive from these views. If you don't specify a view class in this way, the view class used for creating views from this storage object will be the 'view' class in this module. """ self._db = apply(metakit.storage, args) self._viewClass = kw.get('viewclass', view) def __getattr__(self, attr): try: return getattr(self._db, attr) except: # the special attribute 'views' lists the names of existing views if attr in ['views', 'tables']: return self.listViews() # a little bit of magic to allow opening a view via an attrib ref # example - 'db.tablename' -> 'db.view(tablename)' views = self.listViews() if attr in views: return self.view(attr) else: raise Exception("Attempted to retrieve nonexistent view '%s'" % attr) def __getitem__(self, item): """ Allows tables to be retrieved with dbobj['tablename'] """ return self.__getattr__(item) # Here, intercept the normal storage methods to return a wrapped view def getas(self, *args, **kw): """ You can use the keyword arg 'viewclass=myNewViewClass' to 'cast' the created/modified view into a separate view class. If you don't do this, the view class will be the one with which the storage object was created. """ viewclass = kw.get('viewclass', self._viewClass) return viewclass(apply(self._db.getas, args)) def view(self, *args, **kw): """ You can use the keyword arg 'viewclass=myNewViewClass' to 'cast' the created/modified view into a separate view class. If you don't do this, the view class will be the one with which the storage object was created. """ viewclass = kw.get('viewclass', self._viewClass) return viewclass(apply(self._db.view, args)) def contents(self, *args, **kw): """ You can use the keyword arg 'viewclass=myNewViewClass' to 'cast' the created/modified view into a separate view class. If you don't do this, the view class will be the one with which the storage object was created. """ viewclass = kw.get('viewclass', self._viewClass) return viewclass(apply(self._db.contents, args)) #@-body #@-node:1::Wrapper methods #@+node:2::New Convenience Methods #@+node:1::dump #@+body def dump(self): dump(self) #@-body #@-node:1::dump #@+node:2::listViews #@+body def listViews(self): s = self.contents().structure() views = [] for item in s: views.append(item.name) return views #@-body #@-node:2::listViews #@-node:2::New Convenience Methods #@-others #@-body #@-node:2::class storage #@+node:3::rowMatchFunc #@+body def rowMatchFunc(values=None, **kw): """ Creates and returns a function object, which takes a row, and tests against the given values If, for all given, each corresponding column in row matches, returns 1, otherwise returns 0. Arguments: - values - a dict of values to match (optional) Keywords: - the values to match Examples:: f = rowMatchFunc({'first':'Fred', 'last':'Flintstone'}) f = rowMatchFunc(first='Fred', last='Flintstone') """ class rowMatchFactory: def __init__(self, values=None, **kw): v = {} if values: v.update(values) v.update(kw) self.values = v #print "rowMatchFactory: matchdict=", v def do_it(self, row): for k,v in self.values.items(): #print "Comparing field '%s': want '%s', have '%s'" % ( # k, v, getattr(row, k, v)) if getattr(row, k, v) != v: return 0 return 1 return apply(rowMatchFactory, (values,), kw).do_it #@-body #@-node:3::rowMatchFunc #@+node:4::rowSetFunc #@+body def rowSetFunc(values=None, **kw): """ rowSetFunc(row, values) - Createa a row updating function. Returns a function which, when called with row, for each key in values, set the corresponding column in row """ class rowSetFactory: def __init__(self, values=None, **kw): v = {} if values: v.update(values) v.update(kw) self.values = v def do_it(self, row): for k,v in self.values.items(): setattr(row, k, v) return apply(rowSetFactory, (values,), kw).do_it #@-body #@-node:4::rowSetFunc #@-node:3::METAKIT WRAPPERS #@+node:4::UTILITY FUNCS #@+node:1::dump #@+body def dump(thing): """ Dumps out a voew or storage object in human-readable text Arguments: - thing - the thing to dump - must be a 'view' or 'storage' object (or subclass), or native Metakit view or storage object. """ if repr(type(thing)) == "" or isinstance(thing, storage): # regard as a 'storage' type tables = thing.contents().structure() for table in tables: print "Table '%s'" % table.name fields = thing.view(table.name).structure() for f in fields: print " %3d %s %s" % (f.id, f.type, f.name) else: # regard as a 'view' or 'derived view' type metakit.dump(thing) #@-body #@-node:1::dump #@+node:2::property #@+body def property(*args, **kw): return apply(metakit.property, args, kw) #@-body #@-node:2::property #@+node:3::wrap #@+body def wrap(*args, **kw): return apply(metakit.wrap, args, kw) #@-body #@-node:3::wrap #@-node:4::UTILITY FUNCS #@-others #@-body #@-node:0::@file metakitplus.py #@-leo