diff --git a/CHANGES.txt b/CHANGES.txt index 516930d..4a2f458 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,16 @@ +1.2.0 (unreleased) +------------------ + + - now using zope.sqlalchemy for ZODB transaction integration + + - internal class renaming + + - remove PythonBaseWrapper, there is only *one* ZopeWrapper + + - requires SQLAlchemy 0.4.6 or higher + + - requires zope.sqlalchemy 0.1 or higher + 1.1.5 (08.05.2008) ------------------ diff --git a/README.txt b/README.txt index cffcc29..6579d05 100644 --- a/README.txt +++ b/README.txt @@ -22,7 +22,6 @@ What z3c.sqlalchemy does not do and won't do: - no support for Zope 3 schemas - no support for Archetypes schemas - z3c.sqlachemy just tries to provide you with the basic functionalities you need to write SQLAlchemy-based applications with Zope 2/3. Higher-level functionalities like integration with Archetypes/Zope 3 schemas are subject to @@ -33,7 +32,8 @@ Requirements: ============= - Zope 2.8+, Zope 3.X -- SQLAlchemy 0.4.0 or higher (no support for SQLAlchemy 0.3) +- SQLAlchemy 0.4.6 or higher (no support for SQLAlchemy 0.3) +- zope.sqlalchemy 0.1.0 or higher - Python 2.4+ diff --git a/buildout.cfg b/buildout.cfg index b4e0375..3437168 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -1,29 +1,7 @@ [buildout] -parts = plone zope2 instance -eggs = -develop = - -[plone] -recipe = plone.recipe.plone - -[zope2] -recipe = plone.recipe.zope2install -url = ${plone:zope2-url} - -[instance] -recipe = plone.recipe.zope2instance -zope2-location = ${zope2:location} -user = admin:admin -http-port = 8080 -debug-mode = on -verbose-security = on -eggs = - ${buildout:eggs} - ${plone:eggs} -zcml = - -products = - ${plone:products} - ${buildout:directory}/products - +parts = test +develop = . +[test] +recipe = zc.recipe.testrunner +eggs = z3c.sqlalchemy [test] diff --git a/setup.py b/setup.py index ed4cd7d..ccfb2ef 100644 --- a/setup.py +++ b/setup.py @@ -7,10 +7,8 @@ ########################################################################## -import os from setuptools import setup, find_packages - CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -48,7 +46,8 @@ zip_safe=True, namespace_packages=['z3c'], install_requires=['setuptools', - 'SQLAlchemy>=0.4.0', + 'SQLAlchemy>=0.4.6', + 'zope.sqlalchemy', # 'zope.component==3.3', # 'zope.interface==3.3', # 'zope.schema==3.3', diff --git a/src/z3c/sqlalchemy/base.py b/src/z3c/sqlalchemy/base.py index 289edfc..cad711c 100644 --- a/src/z3c/sqlalchemy/base.py +++ b/src/z3c/sqlalchemy/base.py @@ -6,51 +6,22 @@ # and ZOPYX Ltd. & Co. KG, Tuebingen, Germany ########################################################################## -import random -import threading - -import sqlalchemy -from sqlalchemy.engine.url import make_url -from sqlalchemy.orm import sessionmaker - from zope.interface import implements from zope.component import getUtility from zope.component.interfaces import ComponentLookupError -from z3c.sqlalchemy.interfaces import ISQLAlchemyWrapper, IModelProvider from z3c.sqlalchemy.model import Model from z3c.sqlalchemy.mapper import LazyMapperCollection +from z3c.sqlalchemy.interfaces import ISQLAlchemyWrapper, IModelProvider -import transaction -from transaction.interfaces import ISavepointDataManager, IDataManagerSavepoint - - -class SynchronizedThreadCache(object): - - def __init__(self): - self.lock = threading.Lock() - self.cache = threading.local() - - def set(self, id, d): - self.lock.acquire() - setattr(self.cache, id, d) - self.lock.release() - - def get(self, id): - self.lock.acquire() - result = getattr(self.cache, id, None) - self.lock.release() - return result - - def remove(self, id): - self.lock.acquire() - if hasattr(self.cache, id): - delattr(self.cache, id) - self.lock.release() +from sqlalchemy import create_engine, MetaData +from sqlalchemy.engine.url import make_url +from sqlalchemy.orm import scoped_session, sessionmaker, relation +from zope.sqlalchemy import ZopeTransactionExtension -class BaseWrapper(object): +class ZopeWrapper(object): implements(ISQLAlchemyWrapper) @@ -82,7 +53,6 @@ def __init__(self, dsn, model=None, transactional=True, engine_options={}, sessi self.session_options = session_options self._model = None self._createEngine() - self._id = str(random.random()) # used as unique key for session/connection cache if model: @@ -117,13 +87,23 @@ def __init__(self, dsn, model=None, transactional=True, engine_options={}, sessi @property def metadata(self): if not hasattr(self, '_v_metadata'): - self._v_metadata = sqlalchemy.MetaData(self._engine) + self._v_metadata = MetaData(self._engine) return self._v_metadata @property def session(self): + """ Return thread-local session """ return self._sessionmaker() + @property + def connection(self): + """ Return underlying connection """ + session = self.session + # Return the ConnectionFairy + return session.connection().connection + # instead of the raw connection + #return session.connection().connection.connection + def registerMapper(self, mapper, name): self._mappers.registerMapper(mapper, name) @@ -144,152 +124,10 @@ def model(self): return self._model def _createEngine(self): - self._engine = sqlalchemy.create_engine(self.dsn, **self.engine_options) - self._sessionmaker = sqlalchemy.orm.sessionmaker(bind=self._engine, - autoflush=True, - transactional=True, - **self.session_options) - - -connection_cache = SynchronizedThreadCache() - - -class SessionDataManager(object): - """ Wraps session into transaction context of Zope """ - - implements(ISavepointDataManager) - - def __init__(self, connection, session, id, transactional=True): - - self.connection = connection - self.session = session - self.transactional = True - self._id = id - self.transaction = None - if self.transactional: - self.transaction = connection.begin() - - def abort(self, trans): - - try: - if self.transaction is not None: - self.transaction.rollback() - # DM: done in "_cleanup" (similar untidy code at other places as well) -## self.session.clear() -## connection_cache.remove(self._id) - finally: - # ensure '_cleanup' is called even when 'rollback' causes an exception - self._cleanup() - - def _flush(self): - - # check if the session contains something flushable - if self.session.new or self.session.deleted or self.session.dirty: - - # Check if a session-bound transaction has been created so far. - # If not, create a new transaction -# if self.transaction is None: -# self.transaction = connection.begin() - - # Flush - self.session.flush() - - def commit(self, trans): - self._flush() - - def tpc_begin(self, trans): - pass - - def tpc_vote(self, trans): - self._flush() - - def tpc_finish(self, trans): - - if self.transaction is not None: - self.transaction.commit() - - self.session.clear() - self._cleanup() - - - # DM: no need to duplicate this code (identical to "abort") -## def tpc_abort(self, trans): -## if self.transaction is not None: -## self.transaction.rollback() -## self._cleanup() - tpc_abort = abort - - def sortKey(self): - return 'z3c.sqlalchemy_' + str(id(self)) - - def _cleanup(self): - self.session.clear() - if self.connection: - self.connection.close() - self.connection = None - connection_cache.remove(self._id) - # DM: maybe, we should set "transaction" to "None"? - - def savepoint(self): - """ return a dummy savepoint """ - return AlchemySavepoint() - - - -# taken from z3c.zalchemy - -class AlchemySavepoint(object): - """A dummy saveoint """ - - implements(IDataManagerSavepoint) - - def __init__(self): - pass - - def rollback(self): - pass - - - -class ZopeBaseWrapper(BaseWrapper): - """ A wrapper to be used from within Zope. It connects - the session with the transaction management of Zope. - """ - - - def __getOrCreateConnectionCacheItem(self, cache_id): - - cache_item = connection_cache.get(cache_id) - - # return cached session if we are within the same transaction - # and same thread - if cache_item is not None: - return cache_item - - # no cached session, let's create a new one - connection = self.engine.connect() - session = sessionmaker(connection)() - - # register a DataManager with the current transaction - transaction.get().join(SessionDataManager(connection, session, self._id)) - - # update thread-local cache - cache_item = dict(connection=connection, session=session) - connection_cache.set(self._id, cache_item) - return cache_item - - - @property - def session(self): - """ Return a (cached) session object for the current transaction """ - return self.__getOrCreateConnectionCacheItem(self._id)['session'] - - - @property - def connection(self): - """ This property is _private_ and only intented to be used - by SQLAlchemyDA and therefore it is not part of the - public API. - """ - - return self.__getOrCreateConnectionCacheItem(self._id)['connection'] + self._engine = create_engine(self.dsn, **self.engine_options) + self._sessionmaker = scoped_session(sessionmaker(bind=self._engine, + transactional=True, + autoflush=True, + extension=ZopeTransactionExtension(), + **self.session_options + )) diff --git a/src/z3c/sqlalchemy/doc/api.pdf b/src/z3c/sqlalchemy/doc/api.pdf deleted file mode 100644 index 10c3fc2..0000000 Binary files a/src/z3c/sqlalchemy/doc/api.pdf and /dev/null differ diff --git a/src/z3c/sqlalchemy/postgres.py b/src/z3c/sqlalchemy/postgres.py index c771fd8..ba0fafd 100644 --- a/src/z3c/sqlalchemy/postgres.py +++ b/src/z3c/sqlalchemy/postgres.py @@ -15,7 +15,7 @@ from zope.interface import implements from z3c.sqlalchemy.interfaces import ISQLAlchemyWrapper -from z3c.sqlalchemy.base import BaseWrapper, ZopeBaseWrapper +from z3c.sqlalchemy.base import ZopeWrapper _cache = threading.local() # module-level cache @@ -68,12 +68,7 @@ def findDependentTables(self, schema='public', ignoreErrors=False): return _cache.ref_mapping -class PythonPostgresWrapper(BaseWrapper, PostgresMixin): - """ Wrapper to be used with Python with extended - Postgres functionality. - """ - -class ZopePostgresWrapper(ZopeBaseWrapper, PostgresMixin): +class ZopePostgresWrapper(ZopeWrapper, PostgresMixin): """ A wrapper to be used from within Zope. It connects the session with the transaction management of Zope. """ diff --git a/src/z3c/sqlalchemy/tests/testSQLAlchemy.py b/src/z3c/sqlalchemy/tests/testSQLAlchemy.py index 85c94d3..eb90bad 100644 --- a/src/z3c/sqlalchemy/tests/testSQLAlchemy.py +++ b/src/z3c/sqlalchemy/tests/testSQLAlchemy.py @@ -14,7 +14,6 @@ """ import os -import unittest import sqlalchemy from sqlalchemy import MetaData, Integer, String, Column, Table @@ -22,13 +21,13 @@ from zope.interface.verify import verifyClass from z3c.sqlalchemy.interfaces import ISQLAlchemyWrapper, IModel -from z3c.sqlalchemy.postgres import PythonPostgresWrapper, ZopePostgresWrapper -from z3c.sqlalchemy.base import BaseWrapper +from z3c.sqlalchemy.postgres import ZopePostgresWrapper from z3c.sqlalchemy.mapper import MappedClassBase from z3c.sqlalchemy import createSAWrapper, Model, registerSAWrapper, getSAWrapper +from Testing.ZopeTestCase import ZopeTestCase -class WrapperTests(unittest.TestCase): +class WrapperTests(ZopeTestCase): def setUp(self): @@ -38,25 +37,17 @@ def setUp(self): users = Table('users', metadata, Column('id', Integer, primary_key=True), - Column('firstname', String), - Column('lastname', String)) + Column('firstname', String(255)), + Column('lastname', String(255))) skill = Table('skills', metadata, - Column('id', Integer, primary_key=True), + Column('user_id', Integer, primary_key=True), Column('user_id', Integer), - Column('name', String)) + Column('name', String(255))) + metadata.drop_all() metadata.create_all() - - def testIFaceBaseWrapper (self): - verifyClass(ISQLAlchemyWrapper , BaseWrapper) - - - def testIFacePythonPostgres(self): - verifyClass(ISQLAlchemyWrapper , PythonPostgresWrapper) - - def testIFaceZopePostgres(self): verifyClass(ISQLAlchemyWrapper , ZopePostgresWrapper) @@ -77,7 +68,7 @@ def testSimplePopulation(self): session.save(User(id=1, firstname='udo', lastname='juergens')) session.save(User(id=2, firstname='heino', lastname='n/a')) session.flush() - + rows = session.query(User).order_by(User.c.id).all() self.assertEqual(len(rows), 2) row1 = rows[0] @@ -173,12 +164,38 @@ def getModel(md): User = db.getMapper('users') session = db.session session.save(User(id=1,firstname='foo', lastname='bar')) - + session.flush() user = session.query(User).filter_by(firstname='foo')[0] Skill = user.getMapper('skills') user.skills.append(Skill(id=1, name='Zope')) session.flush() + def testCheckConnection(self): + """ Check access to low-level connection """ + db = createSAWrapper(self.dsn) + conn = db.connection + cursor = conn.cursor() + cursor.execute('select * from users') + rows = cursor.fetchall() + self.assertEqual(len(rows), 0) + + def testConnectionPlusSession(self): + """ Check access to low-level connection """ + db = createSAWrapper(self.dsn) + + User = db.getMapper('users') + session = db.session + session.save(User(id=1, firstname='udo', lastname='juergens')) + session.save(User(id=2, firstname='heino', lastname='n/a')) + session.flush() + + conn = db.connection + cursor = conn.cursor() + cursor.execute('select * from users') + rows = cursor.fetchall() + self.assertEqual(len(rows), 2) + + def test_suite(): from unittest import TestSuite, makeSuite suite = TestSuite() diff --git a/src/z3c/sqlalchemy/util.py b/src/z3c/sqlalchemy/util.py index 7f0a661..3aa7c11 100644 --- a/src/z3c/sqlalchemy/util.py +++ b/src/z3c/sqlalchemy/util.py @@ -14,21 +14,19 @@ from sqlalchemy.engine.url import make_url -from zope.component import getService, getGlobalServices, getUtilitiesFor, getUtility -from zope.component.utility import GlobalUtilityService +from zope.component import getUtilitiesFor, getUtility from zope.component.interfaces import IUtilityService, ComponentLookupError -from zope.component.servicenames import Utilities from z3c.sqlalchemy.interfaces import ISQLAlchemyWrapper -from z3c.sqlalchemy.postgres import ZopePostgresWrapper, PythonPostgresWrapper -from z3c.sqlalchemy.base import BaseWrapper, ZopeBaseWrapper +from z3c.sqlalchemy.postgres import ZopePostgresWrapper +from z3c.sqlalchemy.base import ZopeWrapper __all__ = ('createSQLAlchemyWrapper', 'registerSQLAlchemyWrapper', 'allRegisteredSQLAlchemyWrappers', 'getSQLAlchemyWrapper', 'createSAWrapper', 'registerSAWrapper', 'allRegisteredSAWrappers', 'getSAWrapper', 'allSAWrapperNames') registeredWrappers = {} -def createSAWrapper(dsn, model=None, forZope=False, name=None, transactional=True, +def createSAWrapper(dsn, model=None, name=None, transactional=True, engine_options={}, session_options={}, **kw): """ Convenience method to generate a wrapper for a DSN and a model. This method hides all database related magic from the user. @@ -39,9 +37,6 @@ def createSAWrapper(dsn, model=None, forZope=False, name=None, transactional=Tru a named utility implementing IModelProvider or a method/callable returning an instance of model.Model. - 'forZope' - set this to True in order to obtain a Zope-transaction-aware - wrapper. - 'transactional' - True|False, only used for SQLAlchemyDA *don't change it* 'name' can be set to register the wrapper automatically in order @@ -57,10 +52,10 @@ def createSAWrapper(dsn, model=None, forZope=False, name=None, transactional=Tru url = make_url(dsn) driver = url.drivername - klass = forZope and ZopeBaseWrapper or BaseWrapper + klass = ZopeWrapper if driver == 'postgres': - klass = forZope and ZopePostgresWrapper or PythonPostgresWrapper + klass = ZopePostgresWrapper wrapper = klass(dsn, model, transactional=transactional,