[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