[Checkins] SVN: z3c.dobbin/ Initial import.
Malthe Borch
mborch at gmail.com
Sun May 18 10:05:45 EDT 2008
Log message for revision 86822:
Initial import.
Changed:
A z3c.dobbin/
A z3c.dobbin/trunk/
A z3c.dobbin/trunk/README.txt
A z3c.dobbin/trunk/bootstrap.py
A z3c.dobbin/trunk/buildout.cfg
A z3c.dobbin/trunk/setup.py
A z3c.dobbin/trunk/src/
A z3c.dobbin/trunk/src/z3c/
A z3c.dobbin/trunk/src/z3c/__init__.py
A z3c.dobbin/trunk/src/z3c/dobbin/
A z3c.dobbin/trunk/src/z3c/dobbin/README.txt
A z3c.dobbin/trunk/src/z3c/dobbin/__init__.py
A z3c.dobbin/trunk/src/z3c/dobbin/bootstrap.py
A z3c.dobbin/trunk/src/z3c/dobbin/configure.zcml
A z3c.dobbin/trunk/src/z3c/dobbin/factory.py
A z3c.dobbin/trunk/src/z3c/dobbin/interfaces.py
A z3c.dobbin/trunk/src/z3c/dobbin/mapper.py
A z3c.dobbin/trunk/src/z3c/dobbin/relations.py
A z3c.dobbin/trunk/src/z3c/dobbin/session.py
A z3c.dobbin/trunk/src/z3c/dobbin/testing.py
A z3c.dobbin/trunk/src/z3c/dobbin/tests.py
-=-
Added: z3c.dobbin/trunk/README.txt
===================================================================
--- z3c.dobbin/trunk/README.txt (rev 0)
+++ z3c.dobbin/trunk/README.txt 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,26 @@
+Overview
+========
+
+Dobbin is a relational database abstraction layer supporting a
+semi-transparent object persistance model.
+
+It relies on descriptive attribute and field declarations based on
+zope.interface and zope.schema.
+
+Tables are created automatically with a 1:1 correspondence to an
+interface with no inheritance (minimal interface). As such, objects
+are modelled as a join between the interfaces it implements.
+
+Authors
+-------
+
+This package was designed and implemented by Malthe Borch, Stefan
+Eletzhofer. It's licensed as ZPL.
+
+Todo
+----
+
+* Containers
+* Dictionaries (zope.schema.Dict)
+* Polymorphic relations (zope.interface.Attribute)
+
Added: z3c.dobbin/trunk/bootstrap.py
===================================================================
--- z3c.dobbin/trunk/bootstrap.py (rev 0)
+++ z3c.dobbin/trunk/bootstrap.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,56 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id: bootstrap.py 77225 2007-06-29 09:20:13Z dobe $
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+try:
+ import pkg_resources
+except ImportError:
+ ez = {}
+ exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+ ).read() in ez
+ ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+ import pkg_resources
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+if sys.platform == 'win32':
+ cmd = '"%s"' % cmd # work around spawn lamosity on windows
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+ os.P_WAIT, sys.executable, sys.executable,
+ '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
+ dict(os.environ,
+ PYTHONPATH=
+ ws.find(pkg_resources.Requirement.parse('setuptools')).location
+ ),
+ ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)
+
Added: z3c.dobbin/trunk/buildout.cfg
===================================================================
--- z3c.dobbin/trunk/buildout.cfg (rev 0)
+++ z3c.dobbin/trunk/buildout.cfg 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,7 @@
+[buildout]
+develop = .
+parts = test
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = z3c.dobbin [test]
\ No newline at end of file
Added: z3c.dobbin/trunk/setup.py
===================================================================
--- z3c.dobbin/trunk/setup.py (rev 0)
+++ z3c.dobbin/trunk/setup.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,47 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from setuptools import setup, find_packages
+
+setup(name='z3c.dobbin',
+ version='0.1',
+ license='ZPL',
+ author = "Malthe Borch, Stefan Eletzhofer and the Zope Community",
+ author_email = "zope-dev at zope.org",
+ description="Relational object persistance framework",
+ long_description=open('README.txt').read()+open('src/z3c/dobbin/README.txt').read(),
+ keywords='',
+ classifiers=['Programming Language :: Python',
+ 'Environment :: Web Environment',
+ 'Framework :: Zope3',
+ ],
+ packages=find_packages('src'),
+ package_dir = {'': 'src'},
+ namespace_packages=['z3c'],
+ include_package_data=True,
+ zip_safe=True,
+ extras_require = dict(
+ test = [
+ 'zope.app.testing',
+ ],
+ ),
+ install_requires = [ 'setuptools',
+ 'zope.interface',
+ 'zope.schema',
+ 'zope.component',
+ 'zope.dottedname',
+ 'ore.alchemist',
+ 'ZODB3',
+ 'SQLAlchemy'],
+ )
Added: z3c.dobbin/trunk/src/z3c/__init__.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/__init__.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/__init__.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,7 @@
+# this is a namespace package
+try:
+ import pkg_resources
+ pkg_resources.declare_namespace(__name__)
+except ImportError:
+ import pkgutil
+ __path__ = pkgutil.extend_path(__path__, __name__)
Added: z3c.dobbin/trunk/src/z3c/dobbin/README.txt
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/README.txt (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/README.txt 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,327 @@
+Developer documentation
+=======================
+
+Dobbin creates ORM mappers based on class specification. Columns are
+infered from interface schema fields and attributes, and a class may
+be provided as the mapper metatype.
+
+Interface mapping
+-----------------
+
+An mapper adapter is provided.
+
+ >>> from z3c.dobbin.mapper import getMapper
+ >>> component.provideAdapter(getMapper)
+
+We begin with a database session.
+
+ >>> import ore.alchemist
+ >>> session = ore.alchemist.Session()
+
+Define a schema interface:
+
+ >>> class IAlbum(interface.Interface):
+ ... artist = schema.TextLine(
+ ... title=u"Artist",
+ ... default=u"")
+ ...
+ ... title = schema.TextLine(
+ ... title=u"Title",
+ ... default=u"")
+
+We can then fabricate an instance that implements this interface by
+using the ``create`` method.
+
+ >>> from z3c.dobbin.factory import create
+ >>> album = create(IAlbum)
+
+Set attributes.
+
+ >>> album.artist = "The Beach Boys"
+ >>> album.title = u"Pet Sounds"
+
+Interface inheritance is supported. For instance, a vinyl record is a
+particular type of album.
+
+ >>> class IVinyl(IAlbum):
+ ... rpm = schema.Int(
+ ... title=u"RPM",
+ ... default=33)
+
+ >>> vinyl = create(IVinyl)
+
+What actually happens on the database side is that columns are mapped
+to the interface that they provide.
+
+Let's demonstrate that the mapper instance actually implements the
+defined fields.
+
+ >>> vinyl.artist = "Diana Ross and The Supremes"
+ >>> vinyl.title = "Taking Care of Business"
+ >>> vinyl.rpm = 45
+
+Or a compact disc.
+
+ >>> class ICompactDisc(IAlbum):
+ ... year = schema.Int(title=u"Year")
+
+ >>> cd = create(ICompactDisc)
+
+Let's pick a more recent Diana Ross, to fit the format.
+
+ >>> cd.artist = "Diana Ross"
+ >>> cd.title = "The Great American Songbook"
+ >>> cd.year = 2005
+
+To verify that we've actually inserted objects to the database, we
+commit the transacation, thus flushing the current session.
+
+ >>> import transaction
+ >>> transaction.commit()
+
+We get a reference to the database metadata object, to locate each
+underlying table.
+
+ >>> from ore.alchemist.interfaces import IDatabaseEngine
+ >>> engine = component.getUtility(IDatabaseEngine)
+ >>> metadata = engine.metadata
+
+Tables are given a name based on the dotted path of the interface they
+describe. A utility method is provided to create a proper table name
+for an interface.
+
+ >>> from z3c.dobbin.mapper import encode
+
+Verify tables for ``IVinyl``, ``IAlbum`` and ``ICompactDisc``.
+
+ >>> session.bind = metadata.bind
+ >>> session.execute(metadata.tables[encode(IVinyl)].select()).fetchall()
+ [(2, 45)]
+
+ >>> session.execute(metadata.tables[encode(IAlbum)].select()).fetchall()
+ [(1, u'The Great American Songbook', u'Diana Ross'),
+ (2, u'Taking Care of Business', u'Diana Ross and The Supremes'),
+ (3, u'Pet Sounds', u'The Beach Boys')]
+
+ >>> session.execute(metadata.tables[encode(ICompactDisc)].select()).fetchall()
+ [(1, 2005)]
+
+Now we'll create a mapper based on a concrete class. We'll let the
+class implement the interface that describes the attributes we want to
+store, but also provides a custom method.
+
+ >>> class Vinyl(object):
+ ... interface.implements(IVinyl)
+ ...
+ ... artist = title = u""
+ ... rpm = 33
+ ...
+ ... def __repr__(self):
+ ... return "<Vinyl %s: %s (@ %d RPM)>" % \
+ ... (self.artist, self.title, self.rpm)
+
+Although the symbols we define in this test report that they're
+available from the ``__builtin__`` module, they really aren't.
+
+We'll manually add these symbols.
+
+ >>> import __builtin__
+ >>> __builtin__.IVinyl = IVinyl
+ >>> __builtin__.Vinyl = Vinyl
+
+Create an instance using the ``create`` factory.
+
+ >>> vinyl = create(Vinyl)
+
+Verify that we've instantiated and instance of our class.
+
+ >>> isinstance(vinyl, Vinyl)
+ True
+
+Copy the attributes from the Diana Ross vinyl record.
+
+ >>> diana = session.query(IVinyl.__mapper__).select_by(
+ ... IAlbum.__mapper__.c.id==2)[0]
+
+ >>> vinyl.artist = diana.artist
+ >>> vinyl.title = diana.title
+ >>> vinyl.rpm = diana.rpm
+
+Verify that the methods on our ``Vinyl``-class are available on the mapper.
+
+ >>> repr(vinyl)
+ '<Vinyl Diana Ross and The Supremes: Taking Care of Business (@ 45 RPM)>'
+
+Relations
+---------
+
+Most people have a favourite record.
+
+ >>> class IFavorite(interface.Interface):
+ ... item = schema.Object(title=u"Item", schema=IVinyl)
+
+Let's make our Diana Ross record a favorite.
+
+ >>> favorite = create(IFavorite)
+ >>> favorite.item = vinyl
+ >>> favorite.item
+ <Vinyl Diana Ross and The Supremes: Taking Care of Business (@ 45 RPM)>
+
+Get back the object.
+
+ >>> favorite = session.query(IFavorite.__mapper__).select_by(
+ ... IFavorite.__mapper__.c.spec==IFavorite.__mapper__.__spec__)[0]
+
+When we retrieve the related items, it's automatically reconstructed
+to match the specification to which it was associated.
+
+ >>> favorite.item
+ <Vinyl Diana Ross and The Supremes: Taking Care of Business (@ 45 RPM)>
+
+We can create relations to objects that are not mapped. Let's model an
+accessory item.
+
+ >>> class IAccessory(interface.Interface):
+ ... name = schema.TextLine(title=u"Name of accessory")
+
+ >>> class Accessory(object):
+ ... interface.implements(IAccessory)
+ ...
+ ... def __repr__(self):
+ ... return "<Accessory '%s'>" % self.name
+
+If we now instantiate an accessory and assign it as a favorite item,
+we'll implicitly create a mapper from the class specification and
+insert it into the database.
+
+ >>> cleaner = Accessory()
+ >>> cleaner.name = "Record cleaner"
+
+Set up relation.
+
+ >>> favorite.item = cleaner
+
+Let's try and get back our record cleaner item.
+
+ >>> __builtin__.Accessory = Accessory
+ >>> favorite.item
+ <Accessory 'Record cleaner'>
+
+Within the same transaction, the relation will return the original
+object, maintaining integrity.
+
+ >>> favorite.item is cleaner
+ True
+
+Internally, this is done by setting an attribute on the original
+object that points to the database item, and maintaining a list of
+pending objects on the current database session:
+
+ >>> cleaner._d_uuid in session._d_pending
+ True
+
+However, once we commit the transaction, the relation is no longer
+attached to the relation source, and the correct data will be
+persisted in the database.
+
+ >>> cleaner.name = u"CD cleaner"
+ >>> transaction.commit()
+ >>> favorite.item.name
+ u'CD cleaner'
+
+This behavior should work well in a request-response type environment,
+where the request will typically end with a commit.
+
+Collections
+-----------
+
+Let's set up a record collection as a list.
+
+ >>> class ICollection(interface.Interface):
+ ... records = schema.List(
+ ... title=u"Records",
+ ... value_type=schema.Object(schema=IAlbum)
+ ... )
+
+ >>> collection = create(ICollection)
+
+Add the Diana Ross record, and save the collection to the session.
+
+ >>> collection.records.append(diana)
+
+We can get our collection back.
+
+ >>> collection = session.query(ICollection.__mapper__).select_by(
+ ... ICollection.__mapper__.c.spec==ICollection.__mapper__.__spec__)[0]
+
+Let's verify that we've stored the Diana Ross record.
+
+ >>> record = collection.records[0]
+
+ >>> record.artist, record.title
+ (u'Diana Ross and The Supremes', u'Taking Care of Business')
+
+Now let's try and add another record.
+
+ >>> collection.records.append(vinyl)
+ >>> another_record = collection.records[1]
+
+They're different.
+
+ >>> record.uuid != another_record.uuid
+ True
+
+We can remove items.
+
+ >>> collection.records.remove(vinyl)
+ >>> len(collection.records) == 1
+ True
+
+And extend.
+
+ >>> collection.records.extend((vinyl,))
+ >>> len(collection.records) == 2
+ True
+
+Items can appear twice in the list.
+
+ >>> collection.records.append(vinyl)
+ >>> len(collection.records) == 3
+ True
+
+We can add concrete instances to collections.
+
+ >>> vinyl = Vinyl()
+ >>> collection.records.append(vinyl)
+ >>> len(collection.records) == 4
+ True
+
+And remove them, too.
+
+ >>> collection.records.remove(vinyl)
+ >>> len(collection.records) == 3
+ True
+
+Known limitations
+-----------------
+
+Certain names are disallowed, and will be ignored when constructing
+the mapper.
+
+ >>> class IKnownLimitations(interface.Interface):
+ ... __name__ = schema.TextLine()
+
+ >>> from z3c.dobbin.interfaces import IMapper
+
+ >>> mapper = IMapper(IKnownLimitations)
+ >>> '__name__' in mapper.c
+ False
+
+Cleanup
+-------
+
+Commit session.
+
+ >>> transaction.commit()
+
+
Added: z3c.dobbin/trunk/src/z3c/dobbin/__init__.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/__init__.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/__init__.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1 @@
+#
Added: z3c.dobbin/trunk/src/z3c/dobbin/bootstrap.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/bootstrap.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/bootstrap.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,78 @@
+from zope import component
+
+import sqlalchemy as rdb
+from sqlalchemy import orm
+
+from ore.alchemist.interfaces import IDatabaseEngine
+
+from interfaces import IMapped
+
+import relations
+
+def bootstrapDatabaseEngine(event):
+ engine = component.getUtility(IDatabaseEngine)
+ engine.metadata = metadata = rdb.MetaData(engine)
+ setUp(metadata)
+
+def setUp(metadata):
+ soup(metadata)
+ catalog(metadata)
+ relation(metadata)
+ metadata.create_all()
+
+class Soup(object):
+ pass
+
+def soup(metadata):
+ table = rdb.Table(
+ 'soup',
+ metadata,
+ rdb.Column('id', rdb.Integer, primary_key=True, autoincrement=True),
+ rdb.Column('uuid', rdb.String(length=32), unique=True, index=True),
+ rdb.Column('spec', rdb.String, index=True),
+ )
+
+ orm.mapper(Soup, table)
+
+ return table
+
+def catalog(metadata):
+ return rdb.Table(
+ 'catalog',
+ metadata,
+ rdb.Column('id', rdb.Integer, primary_key=True, autoincrement=True),
+ rdb.Column('left', rdb.String(length=32), rdb.ForeignKey("soup.uuid"), index=True),
+ rdb.Column('right', rdb.String(length=32), rdb.ForeignKey("soup.uuid")),
+ rdb.Column('name', rdb.String))
+
+class Relation(object):
+ def _get_source(self):
+ return relations.lookup(self.left)
+
+ def _set_source(self, item):
+ self.left = item.uuid
+
+ def _get_target(self):
+ return relations.lookup(self.right)
+
+ def _set_target(self, item):
+ if not IMapped.providedBy(item):
+ item = relations.persist(item)
+
+ self.right = item.uuid
+
+ source = property(_get_source, _set_source)
+ target = property(_get_target, _set_target)
+
+def relation(metadata):
+ table = rdb.Table(
+ 'relation',
+ metadata,
+ rdb.Column('id', rdb.Integer, primary_key=True, autoincrement=True),
+ rdb.Column('left', rdb.String(length=32), rdb.ForeignKey("soup.uuid"), index=True),
+ rdb.Column('right', rdb.String(length=32), rdb.ForeignKey("soup.uuid")),
+ rdb.Column('order', rdb.Integer, nullable=False))
+
+ orm.mapper(Relation, table)
+
+ return table
Added: z3c.dobbin/trunk/src/z3c/dobbin/configure.zcml
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/configure.zcml (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/configure.zcml 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,5 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+ <adapter factory=".mapper.getMapper" />
+
+</configure>
Added: z3c.dobbin/trunk/src/z3c/dobbin/factory.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/factory.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/factory.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,23 @@
+from interfaces import IMapper
+from zope.interface.declarations import _normalizeargs
+
+from uuid import uuid1
+
+from ore.alchemist import Session
+
+def create(spec):
+ # set up mapper
+ mapper = IMapper(spec)
+
+ # create instance
+ instance = mapper()
+
+ # set soup attributes
+ instance.uuid = uuid1().hex
+ instance.spec = mapper.__spec__
+
+ # save to session
+ session = Session()
+ session.save(instance)
+
+ return instance
Added: z3c.dobbin/trunk/src/z3c/dobbin/interfaces.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/interfaces.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/interfaces.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,8 @@
+from zope import interface
+
+class IMapped(interface.Interface):
+ __mapper__ = interface.Attribute(
+ """ORM mapper.""")
+
+class IMapper(interface.Interface):
+ """An ORM mapper for a particular specification."""
Added: z3c.dobbin/trunk/src/z3c/dobbin/mapper.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/mapper.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/mapper.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,340 @@
+from zope import interface
+from zope import schema
+from zope import component
+
+from zope.dottedname.resolve import resolve
+
+from interfaces import IMapper
+from interfaces import IMapped
+
+import sqlalchemy as rdb
+from sqlalchemy import orm
+
+from sqlalchemy.orm.attributes import proxied_attribute_factory
+
+from ore.alchemist.interfaces import IDatabaseEngine
+from ore.alchemist import Session
+from ore.alchemist.zs2sa import FieldTranslator
+from ore.alchemist.zs2sa import StringTranslator
+
+import bootstrap
+import relations
+
+import factory
+
+from itertools import chain
+
+import types
+
+class RelationProperty(property):
+ def __init__(self, field):
+ self.field = field
+ self.name = field.__name__+'_relation'
+ property.__init__(self, self.get, self.set)
+
+ def get(kls, instance):
+ item = getattr(instance, kls.name)
+ return relations.lookup(item.uuid)
+
+ def set(kls, instance, value):
+ if not IMapped.providedBy(value):
+ value = relations.persist(value)
+
+ setattr(instance, kls.name, value)
+
+class RelationList(object):
+ __emulates__ = None
+
+ def __init__(self):
+ self.data = []
+
+ @property
+ def adapter(self):
+ return self._sa_adapter
+
+ @orm.collections.collection.internally_instrumented
+ @orm.collections.collection.appender
+ def append(self, item, _sa_initiator=None):
+ if not IMapped.providedBy(item):
+ item = relations.persist(item)
+
+ self.adapter.fire_append_event(item, _sa_initiator)
+
+ relation = self._create_relation(item)
+ self.data.append(relation)
+
+ @orm.collections.collection.internally_instrumented
+ @orm.collections.collection.remover
+ def remove(self, item, _sa_initiator=None):
+ if IMapped.providedBy(item):
+ uuid = item.uuid
+ else:
+ uuid = item._d_uuid
+
+ for relation in self.data:
+ if relation.right == uuid:
+ # fire remove event on target
+ target = relations.lookup(uuid, ignore_cache=True)
+ self.adapter.fire_remove_event(target, _sa_initiator)
+
+ # remove reference to relation
+ self.data.remove(relation)
+
+ # delete from database
+ session = Session()
+ session.delete(relation)
+
+ return
+
+ return ValueError("Not in list: %s" % item)
+
+ def extend(self, items):
+ map(self.append, items)
+
+ @orm.collections.collection.iterator
+ def __iter__(self):
+ return iter(self.data)
+
+ def __len__(self):
+ return len(self.data)
+
+ def __getitem__(self, index):
+ return self.data[index].target
+
+ def __setitem__(self, index, value):
+ return NotImplementedError("Setting items at an index is not implemented.")
+
+ def _create_relation(self, item):
+ relation = bootstrap.Relation()
+
+ relation.target = item
+ relation.order = len(self.data)
+
+ session = Session()
+ session.save(relation)
+
+ return relation
+
+class ObjectTranslator(object):
+ def __init__(self, column_type=None):
+ self.column_type = column_type
+
+ def __call__(self, field, metadata):
+ return rdb.Column(
+ field.__name__+'_uuid', rdb.String(length=32), nullable=False)
+
+class ObjectProperty(object):
+ """Object property.
+
+ We're not checking type here, because we'll only be creating
+ relations to items that are joined with the soup.
+ """
+
+ def __call__(self, field, column, metadata):
+ relation = RelationProperty(field)
+
+ return {
+ field.__name__: relation,
+ relation.name: orm.relation(
+ bootstrap.Soup,
+ primaryjoin=(field.schema.__mapper__.c.uuid==column),
+ foreign_keys=[column],
+ enable_typechecks=False,
+ lazy=True)
+ }
+
+class ListProperty(object):
+ """A list property.
+
+ Model the schema.List
+ """
+
+ def __call__(self, field, column, metadata):
+ relation_table = metadata.tables['relation']
+ soup_table = metadata.tables['soup']
+
+ return {
+ field.__name__: orm.relation(
+ bootstrap.Relation,
+ #secondary=relation_table,
+ primaryjoin=soup_table.c.uuid==relation_table.c.left,
+ #secondaryjoin=relation_table.c.target==soup_table.c.uuid,
+ #foreign_keys=[relation_table.c.source],
+ collection_class=RelationList,
+ enable_typechecks=False)
+ }
+
+class DictProperty(object):
+ """A dict property.
+
+ In SQLAlchemy, we need to model the following two defintion types:
+
+ schema.Dict(
+ value_type=schema.Object(
+ schema=ISomeInterface)
+ )
+
+ schema.Dict(
+ value_type=schema.Set(
+ value_schema.Object(
+ schema=ISomeInterface)
+ )
+ )
+
+ Reference:
+
+ http://blog.discorporate.us/2008/02/sqlalchemy-partitioned-collections-1
+ http://www.sqlalchemy.org/docs/04/mappers.html#advdatamapping_relation_collections
+
+ """
+
+ def __call__(self, field, column, metadata):
+ #
+ #
+ # TODO: Return column definition
+
+ pass
+
+fieldmap = {
+ schema.ASCII: StringTranslator(),
+ schema.ASCIILine: StringTranslator(),
+ schema.Bool: FieldTranslator(rdb.BOOLEAN),
+ schema.Bytes: FieldTranslator(rdb.BLOB),
+ schema.Bytes: FieldTranslator(rdb.BLOB),
+ schema.Choice: StringTranslator(),
+ schema.Date: FieldTranslator(rdb.DATE),
+ schema.Dict: (ObjectTranslator(), DictProperty()),
+ schema.DottedName: StringTranslator(),
+ schema.Float: FieldTranslator(rdb.Float),
+ schema.Id: StringTranslator(),
+ schema.Int: FieldTranslator(rdb.Integer),
+ schema.List: (None, ListProperty()),
+ schema.Object: (ObjectTranslator(), ObjectProperty()),
+ schema.Password: StringTranslator(),
+ schema.SourceText: StringTranslator(),
+ schema.Text: StringTranslator(),
+ schema.TextLine: StringTranslator(),
+ schema.URI: StringTranslator(),
+ interface.interface.Method: None,
+}
+
+def decode(name):
+ return resolve(name.replace(':', '.'))
+
+def encode(iface):
+ return iface.__identifier__.replace('.', ':')
+
+def expand(iface):
+ yield iface
+
+ for spec in iface.getBases():
+ for iface in expand(spec):
+ yield iface
+
+ at interface.implementer(IMapper)
+ at component.adapter(interface.Interface)
+def getMapper(spec):
+ """Return a mapper for the specification."""
+ if not callable(spec):
+ raise TypeError("Create called for non-factory", spec)
+
+ if IMapped.providedBy(spec):
+ return spec.__mapper__
+
+ return createMapper(spec)
+
+def createMapper(spec):
+ """Create a mapper for the specification."""
+
+ engine = component.getUtility(IDatabaseEngine)
+ metadata = engine.metadata
+
+ # expand specification
+ if interface.interfaces.IInterface.providedBy(spec):
+ ifaces = set([spec.get(name).interface for name in schema.getFields(spec)])
+ kls = object
+ else:
+ implemented = interface.implementedBy(spec)
+ fields = chain(*[schema.getFields(iface) for iface in implemented])
+ ifaces = set([implemented.get(name).interface for name in fields])
+ kls = spec
+
+ # create joined table
+ soup_table = table = metadata.tables['soup']
+ properties = {}
+
+ for (t, p) in (getTable(iface, metadata) for iface in ifaces):
+ table = rdb.join(table, t, onclause=(t.c.id==soup_table.c.id))
+ properties.update(p)
+
+ class Mapper(kls):
+ interface.implements(IMapped, *ifaces)
+
+ __spec__ = '%s.%s' % (spec.__module__, spec.__name__)
+
+ # set class representation method if not defined
+ if not isinstance(Mapper.__repr__, types.MethodType):
+ def __repr__(self):
+ return "<Mapper (%s.%s) at %s>" % \
+ (spec.__module__, spec.__name__, hex(id(self)))
+
+ Mapper.__repr__ = __repr__
+
+ # set ``property``-derived properties directly on the mapper
+ for name, prop in properties.items():
+ if isinstance(prop, property):
+ del properties[name]
+ setattr(Mapper, name, prop)
+
+ orm.mapper(Mapper, table, properties=properties)
+
+ spec.__mapper__ = Mapper
+ interface.alsoProvides(spec, IMapped)
+
+ return Mapper
+
+def removeMapper(spec):
+ del spec.mapper
+ interface.noLongerProvides(spec, IMapped)
+
+def getTable(iface, metadata):
+ columns = []
+ properties = {}
+
+ for field in map(lambda key: iface[key], iface.names()):
+ property_factory = None
+
+ # ignores
+ if field.__name__ in ('__name__',):
+ continue
+
+ try:
+ column_factory, property_factory = fieldmap[type(field)]
+ except TypeError:
+ column_factory = fieldmap[type(field)]
+ except KeyError:
+ # raise NotImplementedError("Field type unsupported (%s)." % field)
+ continue
+
+ if column_factory is not None:
+ column = column_factory(field, metadata)
+ columns.append(column)
+ else:
+ column = None
+
+ if property_factory is not None:
+ props = property_factory(field, column, metadata)
+ properties.update(props)
+
+ kw = dict(useexisting=True)
+
+ table = rdb.Table(
+ encode(iface),
+ metadata,
+ rdb.Column('id', rdb.Integer, rdb.ForeignKey("soup.id"), primary_key=True),
+ *columns,
+ **kw)
+
+ metadata.create_all(checkfirst=True)
+
+ return table, properties
Added: z3c.dobbin/trunk/src/z3c/dobbin/relations.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/relations.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/relations.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,61 @@
+from zope import interface
+
+from interfaces import IMapper
+from interfaces import IMapped
+
+from session import getTransactionManager
+
+from zope.dottedname.resolve import resolve
+from ore.alchemist import Session
+
+import factory
+import bootstrap
+
+def lookup(uuid, ignore_cache=False):
+ session = Session()
+ item = session.query(bootstrap.Soup).select_by(uuid=uuid)[0]
+
+ # try to acquire relation target from session
+ if not ignore_cache:
+ try:
+ return session._d_pending[item.uuid]
+ except (AttributeError, KeyError):
+ pass
+
+ # build item
+ return build(item.spec, item.uuid)
+
+def build(spec, uuid):
+ kls = resolve(spec)
+ mapper = IMapper(kls)
+
+ session = Session()
+ return session.query(mapper).select_by(uuid=uuid)[0]
+
+def persist(item):
+ # create instance
+ instance = factory.create(item.__class__)
+
+ # assign uuid to item
+ item._d_uuid = instance.uuid
+
+ # hook into transaction
+ try:
+ manager = item._d_manager
+ except AttributeError:
+ manager = item._d_manager = getTransactionManager(item)
+
+ manager.register()
+
+ # update attributes
+ update(instance, item)
+
+ return instance
+
+def update(instance, item):
+ # set attributes
+ for iface in interface.providedBy(item):
+ for name in iface.names():
+ value = getattr(item, name)
+ setattr(instance, name, value)
+
Added: z3c.dobbin/trunk/src/z3c/dobbin/session.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/session.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/session.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,77 @@
+from zope import interface
+
+from transaction.interfaces import ISavepointDataManager
+from transaction import get as getTransaction
+
+from ore.alchemist import Session
+
+import relations
+
+class Savepoint:
+ """Transaction savepoint."""
+
+ def rollback(self):
+ raise NotImplementedError("Rollbacks are not implemented.")
+
+class TransactionManager(object):
+ """Transaction manager for the database session.
+
+ This is used to synchronize relations to concrete items.
+ """
+
+ interface.implements(ISavepointDataManager)
+
+ def __init__(self, obj):
+ self.registered = False
+ self.vote = False
+ self.obj = obj
+
+ session = Session()
+
+ try:
+ session._d_pending[obj._d_uuid] = obj
+ except AttributeError:
+ session._d_pending = {obj._d_uuid: obj}
+
+ def register(self):
+ if not self.registered:
+ getTransaction().join(self)
+ self.registered = True
+
+ def savepoint(self):
+ return Savepoint()
+
+ def tpc_begin(self, transaction):
+ pass
+
+ def commit(self, transaction):
+ obj = self.obj
+ uuid = obj._d_uuid
+
+ # unset pending state
+ session = Session()
+ del session._d_pending[uuid]
+
+ # build instance
+ instance = relations.lookup(uuid)
+
+ # update attributes
+ relations.update(instance, obj)
+
+ def tpc_vote(self, transaction):
+ pass
+
+ def tpc_finish(self, transaction):
+ self.registered = False
+
+ def tpc_abort(self, transaction):
+ raise NotImplemented("Abort not implemented.")
+ self.registered = False
+
+ abort = tpc_abort
+
+ def sortKey(self):
+ return id(self)
+
+def getTransactionManager(obj):
+ return TransactionManager(obj)
Added: z3c.dobbin/trunk/src/z3c/dobbin/testing.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/testing.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/testing.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,24 @@
+from zope import component
+
+import z3c.dobbin.bootstrap
+import sqlalchemy as rdb
+
+from sqlalchemy import orm
+
+from ore.alchemist import Session
+from ore.alchemist.interfaces import IDatabaseEngine
+
+metadata = rdb.MetaData()
+
+def setUp(test):
+ test._engine = rdb.create_engine('sqlite:///:memory:')
+
+ # register database engine
+ component.provideUtility(test._engine, IDatabaseEngine)
+
+ # bootstrap database engine
+ z3c.dobbin.bootstrap.bootstrapDatabaseEngine(None)
+
+def tearDown(test):
+ del test._engine
+ del metadata._bind
Added: z3c.dobbin/trunk/src/z3c/dobbin/tests.py
===================================================================
--- z3c.dobbin/trunk/src/z3c/dobbin/tests.py (rev 0)
+++ z3c.dobbin/trunk/src/z3c/dobbin/tests.py 2008-05-18 14:05:43 UTC (rev 86822)
@@ -0,0 +1,38 @@
+import unittest
+import doctest
+
+from zope import interface
+from zope import schema
+from zope import component
+
+from zope.app.testing import setup
+from zope.testing.doctestunit import DocFileSuite
+
+import transaction
+
+import testing
+
+def setUp(test):
+ setup.placefulSetUp()
+ testing.setUp(test)
+
+def tearDown(test):
+ setup.placefulTearDown()
+ testing.tearDown(test)
+
+def test_suite():
+ globs = dict(
+ interface=interface,
+ component=component,
+ schema=schema)
+
+ return unittest.TestSuite((
+ DocFileSuite('README.txt',
+ setUp=setUp, tearDown=tearDown,
+ globs=globs,
+ optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+ ),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
More information about the Checkins
mailing list