zope.sqlalchemy-0.6.1/ 000755 000765 000120 00000000000 11512115253 014536 5 ustar 00ldr admin 000000 000000 zope.sqlalchemy-0.6.1/bootstrap.py 000644 000765 000120 00000003366 11421133757 017145 0 ustar 00ldr admin 000000 000000 ##############################################################################
#
# Copyright (c) 2006 Zope Foundation 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 71258 2006-11-21 22:22:48Z jim $
"""
import os, shutil, sys, tempfile, urllib2
tmpeggs = tempfile.mkdtemp()
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)
zope.sqlalchemy-0.6.1/._buildout.cfg 000644 000765 000120 00000000272 11403177554 017277 0 ustar 00ldr admin 000000 000000 Mac OS X 2 ˆ º ATTR –× º ˜ " ˜ " com.macromates.caret {
column = 0;
line = 14;
} zope.sqlalchemy-0.6.1/buildout.cfg 000644 000765 000120 00000000416 11403177554 017062 0 ustar 00ldr admin 000000 000000 [buildout]
develop = .
parts = test scripts
allow-hosts = pypi.python.org
[test]
recipe = zc.recipe.testrunner
eggs = zope.sqlalchemy [test]
defaults = ['--auto-color']
[scripts]
recipe = zc.recipe.egg
eggs =
${test:eggs}
collective.checkdocs
interpreter = py
zope.sqlalchemy-0.6.1/._CHANGES.txt 000644 000765 000120 00000000271 11512115131 016557 0 ustar 00ldr admin 000000 000000 Mac OS X 2 ‡ ¹ ATTR f! ¹ ˜ ! ˜ ! com.macromates.caret {
column = 0;
line = 5;
} zope.sqlalchemy-0.6.1/CHANGES.txt 000644 000765 000120 00000004472 11512115131 016351 0 ustar 00ldr admin 000000 000000 Changes
=======
0.6.1 (2011-01-08)
------------------
* Update datamanager.mark_changed to handle sessions which have not yet logged
a (ORM) query.
0.6 (2010-07-24)
----------------
* Implement should_retry for sqlalchemy.orm.exc.ConcurrentModificationError
and serialization errors from PostgreSQL and Oracle.
(Specify transaction>=1.1 to use this functionality.)
* Include license files.
* Add ``transaction_manager`` attribute to data managers for compliance with
IDataManager interface.
0.5 (2010-06-07)
----------------
* Remove redundant session.flush() / session.clear() on savepoint operations.
These were only needed with SQLAlchemy 0.4.x.
* SQLAlchemy 0.6.x support. Require SQLAlchemy >= 0.5.1.
* Add support for running ``python setup.py test``.
* Pull in pysqlite explicitly as a test dependency.
* Setup sqlalchemy mappers in test setup and clear them in tear down. This
makes the tests more robust and clears up the global state after. It
caused the tests to fail when other tests in the same run called
clear_mappers.
0.4 (2009-01-20)
----------------
Bugs fixed:
* Only raise errors in tpc_abort if we have committed.
* Remove the session id from the SESSION_STATE just before we de-reference the
session (i.e. all work is already successfuly completed). This fixes cases
where the transaction commit failed but SESSION_STATE was already cleared. In
those cases, the transaction was wedeged as abort would always error. This
happened on PostgreSQL where invalid SQL was used and the error caught.
* Call session.flush() unconditionally in tpc_begin.
* Change error message on session.commit() to be friendlier to non zope users.
Feature changes:
* Support for bulk update and delete with SQLAlchemy 0.5.1
0.3 (2008-07-29)
----------------
Bugs fixed:
* New objects added to a session did not cause a transaction join, so were not
committed at the end of the transaction unless the database was accessed.
SQLAlchemy 0.4.7 or 0.5beta3 now required.
Feature changes:
* For correctness and consistency with ZODB, renamed the function 'invalidate'
to 'mark_changed' and the status 'invalidated' to 'changed'.
0.2 (2008-06-28)
----------------
Feature changes:
* Updated to support SQLAlchemy 0.5. (0.4.6 is still supported).
0.1 (2008-05-15)
----------------
* Initial public release.
zope.sqlalchemy-0.6.1/COPYRIGHT.txt 000644 000765 000120 00000000040 11421133757 016651 0 ustar 00ldr admin 000000 000000 Zope Foundation and Contributors zope.sqlalchemy-0.6.1/CREDITS.txt 000644 000765 000120 00000000526 11120253246 016377 0 ustar 00ldr admin 000000 000000 zope.sqlalchemy credits
***********************
* Laurence Rowe - creator and main developer
* Martijn Faassen - updated to work with SQLAlchemy 0.5
Also thanks to Michael Bayer for help with integration in SQLAlchemy
and of course SQLAlchemy itself, as well as the many Zope developers
who worked on Zope/SQLAlchemy integration projects.
zope.sqlalchemy-0.6.1/LICENSE.txt 000644 000765 000120 00000004026 11421133757 016373 0 ustar 00ldr admin 000000 000000 Zope Public License (ZPL) Version 2.1
A copyright notice accompanies this license document that identifies the
copyright holders.
This license has been certified as open source. It has also been designated as
GPL compatible by the Free Software Foundation (FSF).
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions in source code must retain the accompanying copyright
notice, this list of conditions, and the following disclaimer.
2. Redistributions in binary form must reproduce the accompanying copyright
notice, this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Names of the copyright holders must not be used to endorse or promote
products derived from this software without prior written permission from the
copyright holders.
4. The right to distribute this software or to use it for any purpose does not
give you the right to use Servicemarks (sm) or Trademarks (tm) of the
copyright
holders. Use of them is covered by separate agreement with the copyright
holders.
5. If any files are modified, you must cause the modified files to carry
prominent notices stating that you changed the files and the date of any
change.
Disclaimer
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
zope.sqlalchemy-0.6.1/oracle.cfg 000644 000765 000120 00000001557 11503171204 016472 0 ustar 00ldr admin 000000 000000 # To run the oracle tests I use the oracle developer days VirtualBox image:
# http://www.oracle.com/technology/software/products/virtualbox/appliances/index.html
# For cx_Oracle to build, download instantclient basiclite and sdk from:
# http://www.oracle.com/technology/software/tech/oci/instantclient/index.html
[buildout]
extends = buildout.cfg
# extends = postgres.cfg
parts += python-oracle cx_Oracle testora
python = python-oracle
allow-hosts += *.sourceforge.net
[python-oracle]
recipe = gocept.cxoracle
instant-client = ${buildout:directory}/instantclient-basiclite-10.2.0.4.0-macosx-x64.zip
instant-sdk = instantclient-sdk-10.2.0.4.0-macosx-x64.zip
[cx_Oracle]
recipe = zc.recipe.egg:custom
egg = cx_Oracle
[test]
eggs += cx_Oracle
[testora]
<= test
environment = oraenv
[scripts]
eggs += cx_Oracle
[oraenv]
TEST_DSN = oracle://system:oracle@192.168.56.101/orcl
zope.sqlalchemy-0.6.1/PKG-INFO 000644 000765 000120 00000025167 11512115253 015646 0 ustar 00ldr admin 000000 000000 Metadata-Version: 1.0
Name: zope.sqlalchemy
Version: 0.6.1
Summary: Minimal Zope/SQLAlchemy transaction integration
Home-page: http://pypi.python.org/pypi/zope.sqlalchemy
Author: Laurence Rowe
Author-email: laurence@lrowe.co.uk
License: ZPL 2.1
Description: ***************
zope.sqlalchemy
***************
.. contents::
:local:
Introduction
============
The aim of this package is to unify the plethora of existing packages
integrating SQLAlchemy with Zope's transaction management. As such it seeks
only to provide a data manager and makes no attempt to define a `zopeish` way
to configure engines.
For WSGI applications, Zope style automatic transaction management is
available with `repoze.tm2`_, a part of `Repoze BFG`_ and `Turbogears 2`_.
You need to understand `SQLAlchemy`_ for this package and this README to make
any sense.
.. _repoze.tm2: http://docs.repoze.org/tm2/
.. _Repoze BFG: http://bfg.repoze.org/
.. _Turbogears 2: http://turbogears.org/
.. _SQLAlchemy: http://sqlalchemy.org/docs/
Running the tests
=================
This package is distributed as a buildout. Using your desired python run:
$ python bootstrap.py
This will download the dependent packages and setup the test script, which may
be run with:
$ ./bin/test
or with the standard setuptools test command:
$ ./bin/py setup.py test
To enable testing with your own database set the TEST_DSN environment variable
to your sqlalchemy database dsn. Two-phase commit behaviour may be tested by
setting the TEST_TWOPHASE variable to a non empty string. e.g:
$ TEST_DSN=postgres://test:test@localhost/test TEST_TWOPHASE=True bin/test
Example
=======
This example is lifted directly from the SQLAlchemy declarative documentation.
First the necessary imports.
>>> from sqlalchemy import *
>>> from sqlalchemy.ext.declarative import declarative_base
>>> from sqlalchemy.orm import scoped_session, sessionmaker, relation
>>> from zope.sqlalchemy import ZopeTransactionExtension
>>> import transaction
Now to define the mapper classes.
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = 'test_users'
... id = Column('id', Integer, primary_key=True)
... name = Column('name', String(50))
... addresses = relation("Address", backref="user")
>>> class Address(Base):
... __tablename__ = 'test_addresses'
... id = Column('id', Integer, primary_key=True)
... email = Column('email', String(50))
... user_id = Column('user_id', Integer, ForeignKey('test_users.id'))
Create an engine and setup the tables. Note that for this example to work a
recent version of sqlite/pysqlite is required. 3.4.0 seems to be sufficient.
>>> engine = create_engine(TEST_DSN, convert_unicode=True)
>>> Base.metadata.create_all(engine)
Now to create the session itself. As zope is a threaded web server we must use
scoped sessions. Zope and SQLAlchemy sessions are tied together by using the
ZopeTransactionExtension from this package.
>>> Session = scoped_session(sessionmaker(bind=engine,
... twophase=TEST_TWOPHASE, extension=ZopeTransactionExtension()))
Call the scoped session factory to retrieve a session. You may call this as
many times as you like within a transaction and you will always retrieve the
same session. At present there are no users in the database.
>>> session = Session()
>>> session.query(User).all()
[]
We can now create a new user and commit the changes using Zope's transaction
machinary, just as Zope's publisher would.
>>> session.add(User(id=1, name='bob'))
>>> transaction.commit()
Engine level connections are outside the scope of the transaction integration.
>>> engine.connect().execute('SELECT * FROM test_users').fetchall()
[(1, ...'bob')]
A new transaction requires a new session. Let's add an address.
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.name
u'bob'
>>> bob.addresses
[]
>>> bob.addresses.append(Address(id=1, email='bob@bob.bob'))
>>> transaction.commit()
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.addresses
[
]
>>> bob.addresses[0].email
u'bob@bob.bob'
>>> bob.addresses[0].email = 'wrong@wrong'
To rollback a transaction, use transaction.abort().
>>> transaction.abort()
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.addresses[0].email
u'bob@bob.bob'
>>> transaction.abort()
By default, zope.sqlalchemy puts sessions in an 'active' state when they are
first used. ORM write operations automatically move the session into a
'changed' state. This avoids unnecessary database commits. Sometimes it
is necessary to interact with the database directly through SQL. It is not
possible to guess whether such an operation is a read or a write. Therefore we
must manually mark the session as changed when manual SQL statements write
to the DB.
>>> session = Session()
>>> conn = session.connection()
>>> users = Base.metadata.tables['test_users']
>>> conn.execute(users.update(users.c.name=='bob'), name='ben')
>>> from zope.sqlalchemy import mark_changed
>>> mark_changed(session)
>>> transaction.commit()
>>> session = Session()
>>> session.query(User).all()[0].name
u'ben'
>>> transaction.abort()
If this is a problem you may tell the extension to place the session in the
'changed' state initially.
>>> Session.configure(extension=ZopeTransactionExtension('changed'))
>>> Session.remove()
>>> session = Session()
>>> conn = session.connection()
>>> conn.execute(users.update(users.c.name=='ben'), name='bob')
>>> transaction.commit()
>>> session = Session()
>>> session.query(User).all()[0].name
u'bob'
>>> transaction.abort()
Development version
===================
`SVN version `_
Changes
=======
0.6.1 (2011-01-08)
------------------
* Update datamanager.mark_changed to handle sessions which have not yet logged
a (ORM) query.
0.6 (2010-07-24)
----------------
* Implement should_retry for sqlalchemy.orm.exc.ConcurrentModificationError
and serialization errors from PostgreSQL and Oracle.
(Specify transaction>=1.1 to use this functionality.)
* Include license files.
* Add ``transaction_manager`` attribute to data managers for compliance with
IDataManager interface.
0.5 (2010-06-07)
----------------
* Remove redundant session.flush() / session.clear() on savepoint operations.
These were only needed with SQLAlchemy 0.4.x.
* SQLAlchemy 0.6.x support. Require SQLAlchemy >= 0.5.1.
* Add support for running ``python setup.py test``.
* Pull in pysqlite explicitly as a test dependency.
* Setup sqlalchemy mappers in test setup and clear them in tear down. This
makes the tests more robust and clears up the global state after. It
caused the tests to fail when other tests in the same run called
clear_mappers.
0.4 (2009-01-20)
----------------
Bugs fixed:
* Only raise errors in tpc_abort if we have committed.
* Remove the session id from the SESSION_STATE just before we de-reference the
session (i.e. all work is already successfuly completed). This fixes cases
where the transaction commit failed but SESSION_STATE was already cleared. In
those cases, the transaction was wedeged as abort would always error. This
happened on PostgreSQL where invalid SQL was used and the error caught.
* Call session.flush() unconditionally in tpc_begin.
* Change error message on session.commit() to be friendlier to non zope users.
Feature changes:
* Support for bulk update and delete with SQLAlchemy 0.5.1
0.3 (2008-07-29)
----------------
Bugs fixed:
* New objects added to a session did not cause a transaction join, so were not
committed at the end of the transaction unless the database was accessed.
SQLAlchemy 0.4.7 or 0.5beta3 now required.
Feature changes:
* For correctness and consistency with ZODB, renamed the function 'invalidate'
to 'mark_changed' and the status 'invalidated' to 'changed'.
0.2 (2008-06-28)
----------------
Feature changes:
* Updated to support SQLAlchemy 0.5. (0.4.6 is still supported).
0.1 (2008-05-15)
----------------
* Initial public release.
Keywords: zope zope3 sqlalchemy
Platform: UNKNOWN
Classifier: Framework :: Zope3
Classifier: Programming Language :: Python
Classifier: License :: OSI Approved :: Zope Public License
Classifier: Topic :: Software Development :: Libraries :: Python Modules
zope.sqlalchemy-0.6.1/postgres.cfg 000644 000765 000120 00000001145 11503171204 017064 0 ustar 00ldr admin 000000 000000 # PATH=/opt/local/lib/postgresql90/bin:$PATH bin/buildout -c postgres.cfg
# sudo -u postgres /opt/local/lib/postgresql90/bin/createdb zope_sqlalchemy_tests
# sudo -u postgres /opt/local/lib/postgresql90/bin/createuser -s
# sudo -u postgres /opt/local/lib/postgresql90/bin/postgres -D /opt/local/var/db/postgresql90/defaultdb -d 1
[buildout]
extends = buildout.cfg
find-links = http://initd.org/pub/software/psycopg/
allow-hosts += initd.org
parts += testpg
[test]
eggs += psycopg2
[testpg]
<= test
environment = pgenv
[scripts]
eggs += psycopg2
[pgenv]
TEST_DSN = postgres:///zope_sqlalchemy_tests
zope.sqlalchemy-0.6.1/._README.txt 000644 000765 000120 00000000271 11403200443 016444 0 ustar 00ldr admin 000000 000000 Mac OS X 2 ‡ ¹ ATTR –Ñ ¹ ˜ ! ˜ ! com.macromates.caret {
column = 0;
line = 1;
} zope.sqlalchemy-0.6.1/README.txt 000644 000765 000120 00000000043 11403200443 016224 0 ustar 00ldr admin 000000 000000 See src/zope/sqlalchemy/README.txt
zope.sqlalchemy-0.6.1/setup.cfg 000644 000765 000120 00000000073 11512115253 016357 0 ustar 00ldr admin 000000 000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
zope.sqlalchemy-0.6.1/._setup.py 000644 000765 000120 00000000272 11512115005 016461 0 ustar 00ldr admin 000000 000000 Mac OS X 2 ˆ º ATTR f º ˜ " ˜ " com.macromates.caret {
column = 18;
line = 5;
} zope.sqlalchemy-0.6.1/setup.py 000644 000765 000120 00000002263 11512115005 016246 0 ustar 00ldr admin 000000 000000 import os.path
from setuptools import setup, find_packages
setup(
name='zope.sqlalchemy',
version='0.6.1', # Remember to update __version__ in __init__.py
packages=find_packages('src'),
package_dir = {'':'src'},
include_package_data=True,
zip_safe=False,
namespace_packages=['zope'],
test_suite='zope.sqlalchemy',
author='Laurence Rowe',
author_email='laurence@lrowe.co.uk',
url='http://pypi.python.org/pypi/zope.sqlalchemy',
description="Minimal Zope/SQLAlchemy transaction integration",
long_description=open(os.path.join('src', 'zope', 'sqlalchemy', 'README.txt')).read() + "\n\n" +
open('CHANGES.txt').read(),
license='ZPL 2.1',
keywords='zope zope3 sqlalchemy',
classifiers=[
"Framework :: Zope3",
"Programming Language :: Python",
"License :: OSI Approved :: Zope Public License",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=[
'setuptools',
'SQLAlchemy>=0.5.1',
'transaction',
'zope.interface',
],
extras_require={
'test': [
'pysqlite',
]
},
)
zope.sqlalchemy-0.6.1/src/ 000755 000765 000120 00000000000 11512115253 015325 5 ustar 00ldr admin 000000 000000 zope.sqlalchemy-0.6.1/src/zope/ 000755 000765 000120 00000000000 11512115253 016302 5 ustar 00ldr admin 000000 000000 zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/ 000755 000765 000120 00000000000 11512115253 022135 5 ustar 00ldr admin 000000 000000 zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/dependency_links.txt 000644 000765 000120 00000000001 11512115252 026202 0 ustar 00ldr admin 000000 000000
zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/namespace_packages.txt 000644 000765 000120 00000000005 11512115252 026462 0 ustar 00ldr admin 000000 000000 zope
zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/not-zip-safe 000644 000765 000120 00000000001 11216432702 024365 0 ustar 00ldr admin 000000 000000
zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/PKG-INFO 000644 000765 000120 00000025167 11512115252 023244 0 ustar 00ldr admin 000000 000000 Metadata-Version: 1.0
Name: zope.sqlalchemy
Version: 0.6.1
Summary: Minimal Zope/SQLAlchemy transaction integration
Home-page: http://pypi.python.org/pypi/zope.sqlalchemy
Author: Laurence Rowe
Author-email: laurence@lrowe.co.uk
License: ZPL 2.1
Description: ***************
zope.sqlalchemy
***************
.. contents::
:local:
Introduction
============
The aim of this package is to unify the plethora of existing packages
integrating SQLAlchemy with Zope's transaction management. As such it seeks
only to provide a data manager and makes no attempt to define a `zopeish` way
to configure engines.
For WSGI applications, Zope style automatic transaction management is
available with `repoze.tm2`_, a part of `Repoze BFG`_ and `Turbogears 2`_.
You need to understand `SQLAlchemy`_ for this package and this README to make
any sense.
.. _repoze.tm2: http://docs.repoze.org/tm2/
.. _Repoze BFG: http://bfg.repoze.org/
.. _Turbogears 2: http://turbogears.org/
.. _SQLAlchemy: http://sqlalchemy.org/docs/
Running the tests
=================
This package is distributed as a buildout. Using your desired python run:
$ python bootstrap.py
This will download the dependent packages and setup the test script, which may
be run with:
$ ./bin/test
or with the standard setuptools test command:
$ ./bin/py setup.py test
To enable testing with your own database set the TEST_DSN environment variable
to your sqlalchemy database dsn. Two-phase commit behaviour may be tested by
setting the TEST_TWOPHASE variable to a non empty string. e.g:
$ TEST_DSN=postgres://test:test@localhost/test TEST_TWOPHASE=True bin/test
Example
=======
This example is lifted directly from the SQLAlchemy declarative documentation.
First the necessary imports.
>>> from sqlalchemy import *
>>> from sqlalchemy.ext.declarative import declarative_base
>>> from sqlalchemy.orm import scoped_session, sessionmaker, relation
>>> from zope.sqlalchemy import ZopeTransactionExtension
>>> import transaction
Now to define the mapper classes.
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = 'test_users'
... id = Column('id', Integer, primary_key=True)
... name = Column('name', String(50))
... addresses = relation("Address", backref="user")
>>> class Address(Base):
... __tablename__ = 'test_addresses'
... id = Column('id', Integer, primary_key=True)
... email = Column('email', String(50))
... user_id = Column('user_id', Integer, ForeignKey('test_users.id'))
Create an engine and setup the tables. Note that for this example to work a
recent version of sqlite/pysqlite is required. 3.4.0 seems to be sufficient.
>>> engine = create_engine(TEST_DSN, convert_unicode=True)
>>> Base.metadata.create_all(engine)
Now to create the session itself. As zope is a threaded web server we must use
scoped sessions. Zope and SQLAlchemy sessions are tied together by using the
ZopeTransactionExtension from this package.
>>> Session = scoped_session(sessionmaker(bind=engine,
... twophase=TEST_TWOPHASE, extension=ZopeTransactionExtension()))
Call the scoped session factory to retrieve a session. You may call this as
many times as you like within a transaction and you will always retrieve the
same session. At present there are no users in the database.
>>> session = Session()
>>> session.query(User).all()
[]
We can now create a new user and commit the changes using Zope's transaction
machinary, just as Zope's publisher would.
>>> session.add(User(id=1, name='bob'))
>>> transaction.commit()
Engine level connections are outside the scope of the transaction integration.
>>> engine.connect().execute('SELECT * FROM test_users').fetchall()
[(1, ...'bob')]
A new transaction requires a new session. Let's add an address.
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.name
u'bob'
>>> bob.addresses
[]
>>> bob.addresses.append(Address(id=1, email='bob@bob.bob'))
>>> transaction.commit()
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.addresses
[]
>>> bob.addresses[0].email
u'bob@bob.bob'
>>> bob.addresses[0].email = 'wrong@wrong'
To rollback a transaction, use transaction.abort().
>>> transaction.abort()
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.addresses[0].email
u'bob@bob.bob'
>>> transaction.abort()
By default, zope.sqlalchemy puts sessions in an 'active' state when they are
first used. ORM write operations automatically move the session into a
'changed' state. This avoids unnecessary database commits. Sometimes it
is necessary to interact with the database directly through SQL. It is not
possible to guess whether such an operation is a read or a write. Therefore we
must manually mark the session as changed when manual SQL statements write
to the DB.
>>> session = Session()
>>> conn = session.connection()
>>> users = Base.metadata.tables['test_users']
>>> conn.execute(users.update(users.c.name=='bob'), name='ben')
>>> from zope.sqlalchemy import mark_changed
>>> mark_changed(session)
>>> transaction.commit()
>>> session = Session()
>>> session.query(User).all()[0].name
u'ben'
>>> transaction.abort()
If this is a problem you may tell the extension to place the session in the
'changed' state initially.
>>> Session.configure(extension=ZopeTransactionExtension('changed'))
>>> Session.remove()
>>> session = Session()
>>> conn = session.connection()
>>> conn.execute(users.update(users.c.name=='ben'), name='bob')
>>> transaction.commit()
>>> session = Session()
>>> session.query(User).all()[0].name
u'bob'
>>> transaction.abort()
Development version
===================
`SVN version `_
Changes
=======
0.6.1 (2011-01-08)
------------------
* Update datamanager.mark_changed to handle sessions which have not yet logged
a (ORM) query.
0.6 (2010-07-24)
----------------
* Implement should_retry for sqlalchemy.orm.exc.ConcurrentModificationError
and serialization errors from PostgreSQL and Oracle.
(Specify transaction>=1.1 to use this functionality.)
* Include license files.
* Add ``transaction_manager`` attribute to data managers for compliance with
IDataManager interface.
0.5 (2010-06-07)
----------------
* Remove redundant session.flush() / session.clear() on savepoint operations.
These were only needed with SQLAlchemy 0.4.x.
* SQLAlchemy 0.6.x support. Require SQLAlchemy >= 0.5.1.
* Add support for running ``python setup.py test``.
* Pull in pysqlite explicitly as a test dependency.
* Setup sqlalchemy mappers in test setup and clear them in tear down. This
makes the tests more robust and clears up the global state after. It
caused the tests to fail when other tests in the same run called
clear_mappers.
0.4 (2009-01-20)
----------------
Bugs fixed:
* Only raise errors in tpc_abort if we have committed.
* Remove the session id from the SESSION_STATE just before we de-reference the
session (i.e. all work is already successfuly completed). This fixes cases
where the transaction commit failed but SESSION_STATE was already cleared. In
those cases, the transaction was wedeged as abort would always error. This
happened on PostgreSQL where invalid SQL was used and the error caught.
* Call session.flush() unconditionally in tpc_begin.
* Change error message on session.commit() to be friendlier to non zope users.
Feature changes:
* Support for bulk update and delete with SQLAlchemy 0.5.1
0.3 (2008-07-29)
----------------
Bugs fixed:
* New objects added to a session did not cause a transaction join, so were not
committed at the end of the transaction unless the database was accessed.
SQLAlchemy 0.4.7 or 0.5beta3 now required.
Feature changes:
* For correctness and consistency with ZODB, renamed the function 'invalidate'
to 'mark_changed' and the status 'invalidated' to 'changed'.
0.2 (2008-06-28)
----------------
Feature changes:
* Updated to support SQLAlchemy 0.5. (0.4.6 is still supported).
0.1 (2008-05-15)
----------------
* Initial public release.
Keywords: zope zope3 sqlalchemy
Platform: UNKNOWN
Classifier: Framework :: Zope3
Classifier: Programming Language :: Python
Classifier: License :: OSI Approved :: Zope Public License
Classifier: Topic :: Software Development :: Libraries :: Python Modules
zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/requires.txt 000644 000765 000120 00000000110 11512115252 024524 0 ustar 00ldr admin 000000 000000 setuptools
SQLAlchemy>=0.5.1
transaction
zope.interface
[test]
pysqlite zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/SOURCES.txt 000644 000765 000120 00000001077 11512115253 024026 0 ustar 00ldr admin 000000 000000 CHANGES.txt
COPYRIGHT.txt
CREDITS.txt
LICENSE.txt
README.txt
bootstrap.py
buildout.cfg
oracle.cfg
postgres.cfg
setup.py
src/zope/__init__.py
src/zope.sqlalchemy.egg-info/PKG-INFO
src/zope.sqlalchemy.egg-info/SOURCES.txt
src/zope.sqlalchemy.egg-info/dependency_links.txt
src/zope.sqlalchemy.egg-info/namespace_packages.txt
src/zope.sqlalchemy.egg-info/not-zip-safe
src/zope.sqlalchemy.egg-info/requires.txt
src/zope.sqlalchemy.egg-info/top_level.txt
src/zope/sqlalchemy/README.txt
src/zope/sqlalchemy/__init__.py
src/zope/sqlalchemy/datamanager.py
src/zope/sqlalchemy/tests.py zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/top_level.txt 000644 000765 000120 00000000005 11512115252 024661 0 ustar 00ldr admin 000000 000000 zope
zope.sqlalchemy-0.6.1/src/zope/__init__.py 000644 000765 000120 00000000364 11120253246 020416 0 ustar 00ldr admin 000000 000000 # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/ 000755 000765 000120 00000000000 11512115253 020444 5 ustar 00ldr admin 000000 000000 zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/.___init__.py 000644 000765 000120 00000000273 11512115015 022770 0 ustar 00ldr admin 000000 000000 Mac OS X 2 ‰ » ATTR fø » ˜ # ˜ # com.macromates.caret {
column = 20;
line = 14;
} zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/__init__.py 000644 000765 000120 00000001354 11512115015 022554 0 ustar 00ldr admin 000000 000000 ##############################################################################
#
# Copyright (c) 2008 Zope Foundation 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
#
##############################################################################
__version__ = '0.6.1'
from datamanager import ZopeTransactionExtension, mark_changed
invalidate = mark_changed
zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/._datamanager.py 000644 000765 000120 00000000273 11512114670 023503 0 ustar 00ldr admin 000000 000000 Mac OS X 2 ‰ » ATTR f » ˜ # ˜ # com.macromates.caret {
column = 8;
line = 190;
} zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/datamanager.py 000644 000765 000120 00000021210 11512114670 023260 0 ustar 00ldr admin 000000 000000 ##############################################################################
#
# Copyright (c) 2008 Zope Foundation 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
#
##############################################################################
import transaction as zope_transaction
from zope.interface import implements
from transaction.interfaces import ISavepointDataManager, IDataManagerSavepoint
from transaction._transaction import Status as ZopeStatus
from sqlalchemy.orm.exc import ConcurrentModificationError
from sqlalchemy.exc import DBAPIError
from sqlalchemy.orm.session import SessionExtension
from sqlalchemy.engine.base import Engine
_retryable_errors = []
try:
import psycopg2.extensions
except ImportError:
pass
else:
_retryable_errors.append((psycopg2.extensions.TransactionRollbackError, None))
# ORA-08177: can't serialize access for this transaction
try:
import cx_Oracle
except ImportError:
pass
else:
_retryable_errors.append((cx_Oracle.DatabaseError, lambda e: e.args[0].code==8177))
# The status of the session is stored on the connection info
STATUS_ACTIVE = 'active' # session joined to transaction, writes allowed.
STATUS_CHANGED = 'changed' # data has been written
STATUS_READONLY = 'readonly' # session joined to transaction, no writes allowed.
STATUS_INVALIDATED = STATUS_CHANGED # BBB
NO_SAVEPOINT_SUPPORT = set(['sqlite'])
_SESSION_STATE = {} # a mapping of id(session) -> status
# This is thread safe because you are using scoped sessions
#
# The two variants of the DataManager.
#
class SessionDataManager(object):
"""Integrate a top level sqlalchemy session transaction into a zope transaction
One phase variant.
"""
implements(ISavepointDataManager)
def __init__(self, session, status, transaction_manager):
self.transaction_manager = transaction_manager
self.tx = session.transaction._iterate_parents()[-1]
self.session = session
_SESSION_STATE[id(session)] = status
self.state = 'init'
def _finish(self, final_state):
assert self.tx is not None
session = self.session
del _SESSION_STATE[id(self.session)]
self.tx = self.session = None
self.state = final_state
# closing the session is the last thing we do. If it fails the
# transactions don't get wedged and the error propagates
session.close()
def abort(self, trans):
if self.tx is not None: # there may have been no work to do
self._finish('aborted')
def tpc_begin(self, trans):
self.session.flush()
def commit(self, trans):
status = _SESSION_STATE[id(self.session)]
if status is not STATUS_INVALIDATED:
self._finish('no work')
def tpc_vote(self, trans):
# for a one phase data manager commit last in tpc_vote
if self.tx is not None: # there may have been no work to do
self.tx.commit()
self._finish('committed')
def tpc_finish(self, trans):
pass
def tpc_abort(self, trans):
assert self.state is not 'committed'
def sortKey(self):
# Try to sort last, so that we vote last - we may commit in tpc_vote(),
# which allows Zope to roll back its transaction if the RDBMS
# threw a conflict error.
return "~sqlalchemy:%d" % id(self.tx)
@property
def savepoint(self):
"""Savepoints are only supported when all connections support subtransactions
"""
# ATT: the following check is weak since the savepoint capability
# of a RDBMS also depends on its version. E.g. Postgres 7.X does not
# support savepoints but Postgres is whitelisted independent of its
# version. Possibly additional version information should be taken
# into account (ajung)
if set(engine.url.drivername
for engine in self.session.transaction._connections.keys()
if isinstance(engine, Engine)
).intersection(NO_SAVEPOINT_SUPPORT):
raise AttributeError('savepoint')
return self._savepoint
def _savepoint(self):
return SessionSavepoint(self.session)
def should_retry(self, error):
if isinstance(error, ConcurrentModificationError):
return True
if isinstance(error, DBAPIError):
orig = error.orig
for error_type, test in _retryable_errors:
if isinstance(orig, error_type):
if test is None:
return True
if test(orig):
return True
class TwoPhaseSessionDataManager(SessionDataManager):
"""Two phase variant.
"""
def tpc_vote(self, trans):
if self.tx is not None: # there may have been no work to do
self.tx.prepare()
self.state = 'voted'
def tpc_finish(self, trans):
if self.tx is not None:
self.tx.commit()
self._finish('committed')
def tpc_abort(self, trans):
if self.tx is not None: # we may not have voted, and been aborted already
self.tx.rollback()
self._finish('aborted commit')
def sortKey(self):
# Sort normally
return "sqlalchemy.twophase:%d" % id(self.tx)
class SessionSavepoint:
implements(IDataManagerSavepoint)
def __init__(self, session):
self.session = session
self.transaction = session.begin_nested()
def rollback(self):
# no need to check validity, sqlalchemy should raise an exception. I think.
self.transaction.rollback()
def join_transaction(session, initial_state=STATUS_ACTIVE, transaction_manager=zope_transaction.manager):
"""Join a session to a transaction using the appropriate datamanager.
It is safe to call this multiple times, if the session is already joined
then it just returns.
`initial_state` is either STATUS_ACTIVE, STATUS_INVALIDATED or STATUS_READONLY
If using the default initial status of STATUS_ACTIVE, you must ensure that
mark_changed(session) is called when data is written to the database.
The ZopeTransactionExtesion SessionExtension can be used to ensure that this is
called automatically after session write operations.
"""
if _SESSION_STATE.get(id(session), None) is None:
if session.twophase:
DataManager = TwoPhaseSessionDataManager
else:
DataManager = SessionDataManager
transaction_manager.get().join(DataManager(session, initial_state, transaction_manager))
def mark_changed(session, transaction_manager=zope_transaction.manager):
"""Mark a session as needing to be committed.
"""
session_id = id(session)
assert _SESSION_STATE.get(session_id, None) is not STATUS_READONLY, "Session already registered as read only"
join_transaction(session, STATUS_CHANGED, transaction_manager)
_SESSION_STATE[session_id] = STATUS_CHANGED
class ZopeTransactionExtension(SessionExtension):
"""Record that a flush has occurred on a session's connection. This allows
the DataManager to rollback rather than commit on read only transactions.
"""
def __init__(self, initial_state=STATUS_ACTIVE, transaction_manager=zope_transaction.manager):
if initial_state=='invalidated': initial_state = STATUS_CHANGED #BBB
SessionExtension.__init__(self)
self.initial_state = initial_state
self.transaction_manager = transaction_manager
def after_begin(self, session, transaction, connection):
join_transaction(session, self.initial_state, self.transaction_manager)
def after_attach(self, session, instance):
join_transaction(session, self.initial_state, self.transaction_manager)
def after_flush(self, session, flush_context):
mark_changed(session, self.transaction_manager)
def after_bulk_update(self, session, query, query_context, result):
mark_changed(session, self.transaction_manager)
def after_bulk_delete(self, session, query, query_context, result):
mark_changed(session, self.transaction_manager)
def before_commit(self, session):
assert self.transaction_manager.get().status == ZopeStatus.COMMITTING, "Transaction must be committed using the transaction manager"
zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/README.txt 000644 000765 000120 00000013370 11503171204 022144 0 ustar 00ldr admin 000000 000000 ***************
zope.sqlalchemy
***************
.. contents::
:local:
Introduction
============
The aim of this package is to unify the plethora of existing packages
integrating SQLAlchemy with Zope's transaction management. As such it seeks
only to provide a data manager and makes no attempt to define a `zopeish` way
to configure engines.
For WSGI applications, Zope style automatic transaction management is
available with `repoze.tm2`_, a part of `Repoze BFG`_ and `Turbogears 2`_.
You need to understand `SQLAlchemy`_ for this package and this README to make
any sense.
.. _repoze.tm2: http://docs.repoze.org/tm2/
.. _Repoze BFG: http://bfg.repoze.org/
.. _Turbogears 2: http://turbogears.org/
.. _SQLAlchemy: http://sqlalchemy.org/docs/
Running the tests
=================
This package is distributed as a buildout. Using your desired python run:
$ python bootstrap.py
This will download the dependent packages and setup the test script, which may
be run with:
$ ./bin/test
or with the standard setuptools test command:
$ ./bin/py setup.py test
To enable testing with your own database set the TEST_DSN environment variable
to your sqlalchemy database dsn. Two-phase commit behaviour may be tested by
setting the TEST_TWOPHASE variable to a non empty string. e.g:
$ TEST_DSN=postgres://test:test@localhost/test TEST_TWOPHASE=True bin/test
Example
=======
This example is lifted directly from the SQLAlchemy declarative documentation.
First the necessary imports.
>>> from sqlalchemy import *
>>> from sqlalchemy.ext.declarative import declarative_base
>>> from sqlalchemy.orm import scoped_session, sessionmaker, relation
>>> from zope.sqlalchemy import ZopeTransactionExtension
>>> import transaction
Now to define the mapper classes.
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = 'test_users'
... id = Column('id', Integer, primary_key=True)
... name = Column('name', String(50))
... addresses = relation("Address", backref="user")
>>> class Address(Base):
... __tablename__ = 'test_addresses'
... id = Column('id', Integer, primary_key=True)
... email = Column('email', String(50))
... user_id = Column('user_id', Integer, ForeignKey('test_users.id'))
Create an engine and setup the tables. Note that for this example to work a
recent version of sqlite/pysqlite is required. 3.4.0 seems to be sufficient.
>>> engine = create_engine(TEST_DSN, convert_unicode=True)
>>> Base.metadata.create_all(engine)
Now to create the session itself. As zope is a threaded web server we must use
scoped sessions. Zope and SQLAlchemy sessions are tied together by using the
ZopeTransactionExtension from this package.
>>> Session = scoped_session(sessionmaker(bind=engine,
... twophase=TEST_TWOPHASE, extension=ZopeTransactionExtension()))
Call the scoped session factory to retrieve a session. You may call this as
many times as you like within a transaction and you will always retrieve the
same session. At present there are no users in the database.
>>> session = Session()
>>> session.query(User).all()
[]
We can now create a new user and commit the changes using Zope's transaction
machinary, just as Zope's publisher would.
>>> session.add(User(id=1, name='bob'))
>>> transaction.commit()
Engine level connections are outside the scope of the transaction integration.
>>> engine.connect().execute('SELECT * FROM test_users').fetchall()
[(1, ...'bob')]
A new transaction requires a new session. Let's add an address.
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.name
u'bob'
>>> bob.addresses
[]
>>> bob.addresses.append(Address(id=1, email='bob@bob.bob'))
>>> transaction.commit()
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.addresses
[]
>>> bob.addresses[0].email
u'bob@bob.bob'
>>> bob.addresses[0].email = 'wrong@wrong'
To rollback a transaction, use transaction.abort().
>>> transaction.abort()
>>> session = Session()
>>> bob = session.query(User).all()[0]
>>> bob.addresses[0].email
u'bob@bob.bob'
>>> transaction.abort()
By default, zope.sqlalchemy puts sessions in an 'active' state when they are
first used. ORM write operations automatically move the session into a
'changed' state. This avoids unnecessary database commits. Sometimes it
is necessary to interact with the database directly through SQL. It is not
possible to guess whether such an operation is a read or a write. Therefore we
must manually mark the session as changed when manual SQL statements write
to the DB.
>>> session = Session()
>>> conn = session.connection()
>>> users = Base.metadata.tables['test_users']
>>> conn.execute(users.update(users.c.name=='bob'), name='ben')
>>> from zope.sqlalchemy import mark_changed
>>> mark_changed(session)
>>> transaction.commit()
>>> session = Session()
>>> session.query(User).all()[0].name
u'ben'
>>> transaction.abort()
If this is a problem you may tell the extension to place the session in the
'changed' state initially.
>>> Session.configure(extension=ZopeTransactionExtension('changed'))
>>> Session.remove()
>>> session = Session()
>>> conn = session.connection()
>>> conn.execute(users.update(users.c.name=='ben'), name='bob')
>>> transaction.commit()
>>> session = Session()
>>> session.query(User).all()[0].name
u'bob'
>>> transaction.abort()
Development version
===================
`SVN version `_
zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/tests.py 000644 000765 000120 00000051252 11512077640 022174 0 ustar 00ldr admin 000000 000000 ##############################################################################
#
# Copyright (c) 2008 Zope Foundation 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
#
##############################################################################
# Much inspiration from z3c.sqlalchemy/trunk/src/z3c/sqlalchemy/tests/testSQLAlchemy.py
#
# You may want to run the tests with your database. To do so set the environment variable
# TEST_DSN to the connection url. e.g.:
# export TEST_DSN=postgres://plone:plone@localhost/test
# export TEST_DSN=mssql://plone:plone@/test?dsn=mydsn
#
# To test in twophase commit mode export TEST_TWOPHASE=True
#
# NOTE: The sqlite that ships with Mac OS X 10.4 is buggy. Install a newer version (3.5.6)
# and rebuild pysqlite2 against it.
import os
import unittest
import transaction
import threading
import time
import sqlalchemy as sa
from sqlalchemy import orm, sql, exc
from zope.sqlalchemy import datamanager as tx
from zope.sqlalchemy import mark_changed
TEST_TWOPHASE = bool(os.environ.get('TEST_TWOPHASE'))
TEST_DSN = os.environ.get('TEST_DSN', 'sqlite:///:memory:')
class SimpleModel(object):
def __init__(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
def asDict(self):
return dict((k, v) for k, v in self.__dict__.items() if not k.startswith('_'))
class User(SimpleModel): pass
class Skill(SimpleModel): pass
engine = sa.create_engine(TEST_DSN)
engine2 = sa.create_engine(TEST_DSN)
Session = orm.scoped_session(orm.sessionmaker(
bind=engine,
extension=tx.ZopeTransactionExtension(),
twophase=TEST_TWOPHASE,
))
UnboundSession = orm.scoped_session(orm.sessionmaker(
extension=tx.ZopeTransactionExtension(),
twophase=TEST_TWOPHASE,
))
metadata = sa.MetaData() # best to use unbound metadata
test_users = sa.Table('test_users', metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('firstname', sa.VARCHAR(255)), # mssql cannot do equality on a text type
sa.Column('lastname', sa.VARCHAR(255)),
)
test_skills = sa.Table('test_skills', metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('user_id', sa.Integer),
sa.Column('name', sa.VARCHAR(255)),
sa.ForeignKeyConstraint(('user_id',), ('test_users.id',)),
)
bound_metadata1 = sa.MetaData(engine)
bound_metadata2 = sa.MetaData(engine2)
test_one = sa.Table('test_one', bound_metadata1, sa.Column('id', sa.Integer, primary_key=True))
test_two = sa.Table('test_two', bound_metadata2, sa.Column('id', sa.Integer, primary_key=True))
class TestOne(SimpleModel): pass
class TestTwo(SimpleModel): pass
def setup_mappers():
orm.clear_mappers()
# Other tests can clear mappers by calling clear_mappers(),
# be more robust by setting up mappers in the test setup.
m1 = orm.mapper(User, test_users,
properties = {'skills': orm.relation(Skill,
primaryjoin=test_users.columns['id']==test_skills.columns['user_id']),
})
m2 = orm.mapper(Skill, test_skills)
m3 = orm.mapper(TestOne, test_one)
m4 = orm.mapper(TestTwo, test_two)
return [m1, m2, m3, m4]
class DummyException(Exception):
pass
class DummyTargetRaised(DummyException):
pass
class DummyTargetResult(DummyException):
pass
class DummyDataManager(object):
def __init__(self, key, target=None, args=(), kwargs={}):
self.key = key
self.target = target
self.args = args
self.kwargs = kwargs
def abort(self, trans):
pass
def tpc_begin(self, trans):
pass
def commit(self, trans):
pass
def tpc_vote(self, trans):
if self.target is not None:
try:
result = self.target(*self.args, **self.kwargs)
except Exception, e:
raise DummyTargetRaised(e)
raise DummyTargetResult(result)
else:
raise DummyException('DummyDataManager cannot commit')
def tpc_finish(self, trans):
pass
def tpc_abort(self, trans):
pass
def sortKey(self):
return self.key
class ZopeSQLAlchemyTests(unittest.TestCase):
def setUp(self):
self.mappers = setup_mappers()
metadata.drop_all(engine)
metadata.create_all(engine)
def tearDown(self):
transaction.abort()
metadata.drop_all(engine)
orm.clear_mappers()
def testMarkUnknownSession(self):
import zope.sqlalchemy.datamanager
dummy = DummyDataManager(key='dummy.first')
session = Session()
mark_changed(session)
self.assertTrue(id(session) in zope.sqlalchemy.datamanager._SESSION_STATE)
def testAbortBeforeCommit(self):
# Simulate what happens in a conflict error
dummy = DummyDataManager(key='dummy.first')
session = Session()
conn = session.connection()
mark_changed(session)
try:
# Thus we could fail in commit
transaction.commit()
except:
# But abort must succed (and actually rollback the base connection)
transaction.abort()
pass
# Or the next transaction the next transaction will not be able to start!
transaction.begin()
session = Session()
conn = session.connection()
conn.execute("SELECT 1 FROM test_users")
mark_changed(session)
transaction.commit()
def testAbortAfterCommit(self):
# This is a regression test which used to wedge the transaction
# machinery when using PostgreSQL (and perhaps other) connections.
# Basically, if a commit failed, there was no way to abort the
# transaction. Leaving the transaction wedged.
transaction.begin()
session = Session()
conn = session.connection()
# At least PostgresSQL requires a rollback after invalid SQL is executed
self.assertRaises(Exception, conn.execute, "BAD SQL SYNTAX")
mark_changed(session)
try:
# Thus we could fail in commit
transaction.commit()
except:
# But abort must succed (and actually rollback the base connection)
transaction.abort()
pass
# Or the next transaction the next transaction will not be able to start!
transaction.begin()
session = Session()
conn = session.connection()
conn.execute("SELECT 1 FROM test_users")
mark_changed(session)
transaction.commit()
def testSimplePopulation(self):
session = Session()
query = session.query(User)
rows = query.all()
self.assertEqual(len(rows), 0)
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
session.flush()
rows = query.order_by(User.id).all()
self.assertEqual(len(rows), 2)
row1 = rows[0]
d = row1.asDict()
self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1})
# bypass the session machinary
stmt = sql.select(test_users.columns).order_by('id')
conn = session.connection()
results = conn.execute(stmt)
self.assertEqual(results.fetchall(), [(1, u'udo', u'juergens'), (2, u'heino', u'n/a')])
def testRelations(self):
session = Session()
session.add(User(id=1,firstname='foo', lastname='bar'))
user = session.query(User).filter_by(firstname='foo')[0]
user.skills.append(Skill(id=1, name='Zope'))
session.flush()
def testTransactionJoining(self):
transaction.abort() # clean slate
t = transaction.get()
self.failIf([r for r in t._resources if isinstance(r, tx.SessionDataManager)],
"Joined transaction too early")
session = Session()
session.add(User(id=1, firstname='udo', lastname='juergens'))
t = transaction.get()
# Expect this to fail with SQLAlchemy 0.4
self.failUnless([r for r in t._resources if isinstance(r, tx.SessionDataManager)],
"Not joined transaction")
transaction.abort()
conn = Session().connection()
self.failUnless([r for r in t._resources if isinstance(r, tx.SessionDataManager)],
"Not joined transaction")
def testSavepoint(self):
use_savepoint = not engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT
t = transaction.get()
session = Session()
query = session.query(User)
self.failIf(query.all(), "Users table should be empty")
s0 = t.savepoint(optimistic=True) # this should always work
if not use_savepoint:
self.assertRaises(TypeError, t.savepoint)
return # sqlite databases do not support savepoints
s1 = t.savepoint()
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.flush()
self.failUnless(len(query.all())==1, "Users table should have one row")
s2 = t.savepoint()
session.add(User(id=2, firstname='heino', lastname='n/a'))
session.flush()
self.failUnless(len(query.all())==2, "Users table should have two rows")
s2.rollback()
self.failUnless(len(query.all())==1, "Users table should have one row")
s1.rollback()
self.failIf(query.all(), "Users table should be empty")
def testRollbackAttributes(self):
use_savepoint = not engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT
if not use_savepoint:
return # sqlite databases do not support savepoints
t = transaction.get()
session = Session()
query = session.query(User)
self.failIf(query.all(), "Users table should be empty")
s1 = t.savepoint()
user = User(id=1, firstname='udo', lastname='juergens')
session.add(user)
session.flush()
s2 = t.savepoint()
user.firstname='heino'
session.flush()
s2.rollback()
self.assertEqual(user.firstname, 'udo', "User firstname attribute should have been rolled back")
def testCommit(self):
session = Session()
use_savepoint = not engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT
query = session.query(User)
rows = query.all()
self.assertEqual(len(rows), 0)
transaction.commit() # test a none modifying transaction works
session = Session()
query = session.query(User)
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
session.flush()
rows = query.order_by(User.id).all()
self.assertEqual(len(rows), 2)
transaction.abort() # test that the abort really aborts
session = Session()
query = session.query(User)
rows = query.order_by(User.id).all()
self.assertEqual(len(rows), 0)
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
session.flush()
rows = query.order_by(User.id).all()
row1 = rows[0]
d = row1.asDict()
self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1})
transaction.commit()
rows = query.order_by(User.id).all()
self.assertEqual(len(rows), 2)
row1 = rows[0]
d = row1.asDict()
self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1})
# bypass the session (and transaction) machinary
results = engine.connect().execute(test_users.select())
self.assertEqual(len(results.fetchall()), 2)
def testCommitWithSavepoint(self):
if engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT:
return
session = Session()
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
session.flush()
transaction.commit()
session = Session()
query = session.query(User)
# lets just test that savepoints don't affect commits
t = transaction.get()
rows = query.order_by(User.id).all()
s1 = t.savepoint()
session.delete(rows[1])
session.flush()
transaction.commit()
# bypass the session machinary
results = engine.connect().execute(test_users.select())
self.assertEqual(len(results.fetchall()), 1)
def testTwoPhase(self):
session = Session()
if not session.twophase:
return
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
session.flush()
transaction.commit()
# Test that we clean up after a tpc_abort
t = transaction.get()
def target():
return engine.connect().recover_twophase()
dummy = DummyDataManager(key='~~~dummy.last', target=target)
t.join(dummy)
session = Session()
query = session.query(User)
rows = query.all()
session.delete(rows[0])
session.flush()
result = None
try:
t.commit()
except DummyTargetResult, e:
result = e.args[0]
except DummyTargetRaised, e:
raise e.args[0]
self.assertEqual(len(result), 1, "Should have been one prepared transaction when dummy aborted")
transaction.begin()
self.assertEqual(len(engine.connect().recover_twophase()), 0, "Test no outstanding prepared transactions")
def testThread(self):
transaction.abort()
global thread_error
thread_error = None
def target():
try:
session = Session()
metadata.drop_all(engine)
metadata.create_all(engine)
query = session.query(User)
rows = query.all()
self.assertEqual(len(rows), 0)
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
session.flush()
rows = query.order_by(User.id).all()
self.assertEqual(len(rows), 2)
row1 = rows[0]
d = row1.asDict()
self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1})
except Exception, err:
global thread_error
thread_error = err
transaction.abort()
thread = threading.Thread(target=target)
thread.start()
thread.join()
if thread_error is not None:
raise thread_error # reraise in current thread
def testBulkDelete(self):
session = Session()
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
transaction.commit()
session = Session()
session.query(User).delete()
transaction.commit()
results = engine.connect().execute(test_users.select())
self.assertEqual(len(results.fetchall()), 0)
def testBulkUpdate(self):
session = Session()
session.add(User(id=1, firstname='udo', lastname='juergens'))
session.add(User(id=2, firstname='heino', lastname='n/a'))
transaction.commit()
session = Session()
session.query(User).update(dict(lastname="smith"))
transaction.commit()
results = engine.connect().execute(test_users.select(test_users.c.lastname=="smith"))
self.assertEqual(len(results.fetchall()), 2)
class RetryTests(unittest.TestCase):
def setUp(self):
self.mappers = setup_mappers()
metadata.drop_all(engine)
metadata.create_all(engine)
self.tm1 = transaction.TransactionManager()
self.tm2 = transaction.TransactionManager()
# With psycopg2 you might supply isolation_level='SERIALIZABLE' here,
# unfortunately that is not supported by cx_Oracle.
e1 = sa.create_engine(TEST_DSN)
e2 = sa.create_engine(TEST_DSN)
self.s1 = orm.sessionmaker(
bind=e1,
extension=tx.ZopeTransactionExtension(transaction_manager=self.tm1),
twophase=TEST_TWOPHASE,
)()
self.s2 = orm.sessionmaker(
bind=e2,
extension=tx.ZopeTransactionExtension(transaction_manager=self.tm2),
twophase=TEST_TWOPHASE,
)()
self.tm1.begin()
self.s1.add(User(id=1, firstname='udo', lastname='juergens'))
self.tm1.commit()
def tearDown(self):
self.tm1.abort()
self.tm2.abort()
metadata.drop_all(engine)
orm.clear_mappers()
def testRetry(self):
# sqlite is unable to run this test as the databse is locked
tm1, tm2, s1, s2 = self.tm1, self.tm2, self.s1, self.s2
# make sure we actually start a session.
tm1.begin()
self.failUnless(len(s1.query(User).all())==1, "Users table should have one row")
tm2.begin()
self.failUnless(len(s2.query(User).all())==1, "Users table should have one row")
s1.query(User).delete()
user = s2.query(User).get(1)
user.lastname = u'smith'
tm1.commit()
raised = False
try:
s2.flush()
except orm.exc.ConcurrentModificationError, e:
# This error is thrown when the number of updated rows is not as expected
raised = True
self.failUnless(raised, "Did not raise expected error")
self.failUnless(tm2._retryable(type(e), e), "Error should be retryable")
def testRetryThread(self):
tm1, tm2, s1, s2 = self.tm1, self.tm2, self.s1, self.s2
# make sure we actually start a session.
tm1.begin()
self.failUnless(len(s1.query(User).all())==1, "Users table should have one row")
tm2.begin()
s2.connection().execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
self.failUnless(len(s2.query(User).all())==1, "Users table should have one row")
s1.query(User).delete()
raised = False
def target():
time.sleep(0.2)
tm1.commit()
thread = threading.Thread(target=target)
thread.start()
try:
user = s2.query(User).with_lockmode('update').get(1)
except exc.DBAPIError, e:
# This error wraps the underlying DBAPI module error, some of which are retryable
raised = True
self.failUnless(raised, "Did not raise expected error")
self.failUnless(tm2._retryable(type(e), e), "Error should be retryable")
thread.join() # well, we must have joined by now
class MultipleEngineTests(unittest.TestCase):
def setUp(self):
self.mappers = setup_mappers()
bound_metadata1.drop_all()
bound_metadata1.create_all()
bound_metadata2.drop_all()
bound_metadata2.create_all()
def tearDown(self):
transaction.abort()
bound_metadata1.drop_all()
bound_metadata2.drop_all()
orm.clear_mappers()
def testTwoEngines(self):
session = UnboundSession()
session.add(TestOne(id=1))
session.add(TestTwo(id=2))
session.flush()
transaction.commit()
session = UnboundSession()
rows = session.query(TestOne).all()
self.assertEqual(len(rows), 1)
rows = session.query(TestTwo).all()
self.assertEqual(len(rows), 1)
def tearDownReadMe(test):
Base = test.globs['Base']
engine = test.globs['engine']
Base.metadata.drop_all(engine)
def test_suite():
from unittest import TestSuite, makeSuite
import doctest
optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
suite = TestSuite()
suite.addTest(makeSuite(ZopeSQLAlchemyTests))
suite.addTest(makeSuite(MultipleEngineTests))
if TEST_DSN.startswith('postgres') or TEST_DSN.startswith('oracle'):
suite.addTest(makeSuite(RetryTests))
suite.addTest(doctest.DocFileSuite('README.txt', optionflags=optionflags, tearDown=tearDownReadMe,
globs={'TEST_DSN': TEST_DSN, 'TEST_TWOPHASE': TEST_TWOPHASE}))
return suite