././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1694499879.5074608 zope.sqlalchemy-3.1/0000755000076600000240000000000014500002050014160 5ustar00m.howitzstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/CHANGES.rst0000644000076600000240000002040014500002047015764 0ustar00m.howitzstaffChanges ======= 3.1 (2023-09-12) ---------------- - Fix ``psycopg.errors.OperationalError.sqlstate`` can be ``None``. (`#81 `_) 3.0 (2023-06-01) ---------------- - Add support for SQLAlchemy 2.0 and for new psycopg v3 backend. (`#79 `_) **Breaking Changes** - No longer allow calling ``session.commit()`` within a manual nested database transaction (a savepoint). If you want to use savepoints directly in code that is not aware of ``transaction.savepoint()`` with ``session.begin_nested()`` then use the savepoint returned by the function to commit just the nested transaction i.e. ``savepoint = session.begin_nested(); savepoint.commit()`` or use it as a context manager i.e. ``with session.begin_nested():``. (`for details see #79 `_) 2.0 (2023-02-06) ---------------- - Drop support for Python 2.7, 3.5, 3.6. - Drop support for ``SQLAlchemy < 1.1`` (`#65 `_) - Add support for Python 3.10, 3.11. 1.6 (2021-09-06) ---------------- - Add support for Python 2.7 on SQLAlchemy 1.4. (`#71 `_) 1.5 (2021-07-14) ---------------- - Call ``mark_changed`` also on the ``do_orm_execute`` event if the operation is an insert, update or delete. This is SQLAlchemy >= 1.4 only, as it introduced that event. (`#67 `_) - Fixup get transaction. There was regression introduced in 1.4. (`#66 `_) 1.4 (2021-04-26) ---------------- - Add ``mark_changed`` and ``join_transaction`` methods to ``ZopeTransactionEvents``. (`#46 `_) - Reduce DeprecationWarnings with SQLAlchemy 1.4 and require at least SQLAlchemy >= 0.9. (`#54 `_) - Add support for SQLAlchemy 1.4. (`#58 `_) - Prevent using an SQLAlchemy 1.4 version with broken flush support. (`#57 `_) 1.3 (2020-02-17) ---------------- * ``.datamanager.register()`` now returns the ``ZopeTransactionEvents`` instance which was used to register the events. This allows to change its parameters afterwards. (`#40 `_) * Add preliminary support for Python 3.9a3. 1.2 (2019-10-17) ---------------- **Breaking Changes** * Drop support for Python 3.4. * Add support for Python 3.7 and 3.8. * Fix deprecation warnings for the event system. We already used it in general but still leveraged the old extension mechanism in some places. (`#31 `_) To make things clearer we renamed the ``ZopeTransactionExtension`` class to ``ZopeTransactionEvents``. Existing code using the 'register' version stays compatible. **Upgrade from 1.1** Your old code like this: .. code-block:: python from zope.sqlalchemy import ZopeTransactionExtension DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension(), **options)) becomes: .. code-block:: python from zope.sqlalchemy import register DBSession = scoped_session(sessionmaker(**options)) register(DBSession) 1.1 (2019-01-03) ---------------- * Add support to MySQL using pymysql. 1.0 (2018-01-31) ---------------- * Add support for Python 3.4 up to 3.6. * Support SQLAlchemy 1.2. * Drop support for Python 2.6, 3.2 and 3.3. * Drop support for transaction < 1.6.0. * Fix hazard that could cause SQLAlchemy session not to be committed when transaction is committed in rare situations. (`#23 `_) 0.7.7 (2016-06-23) ------------------ * Support SQLAlchemy 1.1. (`#15 `_) 0.7.6 (2015-03-20) ------------------ * Make version check in register compatible with prereleases. 0.7.5 (2014-06-17) ------------------ * Ensure mapped objects are expired following a ``transaction.commit()`` when no database commit was required. (`#8 `_) 0.7.4 (2014-01-06) ------------------ * Allow ``session.commit()`` on nested transactions to facilitate integration of existing code that might not use ``transaction.savepoint()``. (`#1 `_) * Add a new function zope.sqlalchemy.register(), which replaces the direct use of ZopeTransactionExtension to make use of the newer SQLAlchemy event system to establish instrumentation on the given Session instance/class/factory. Requires at least SQLAlchemy 0.7. (`#4 `_) * Fix `keep_session=True` doesn't work when a transaction is joined by flush and other manngers bug. (`#5 `_) 0.7.3 (2013-09-25) ------------------ * Prevent the ``Session`` object from getting into a "wedged" state if joining a transaction fails. With thread scoped sessions that are reused this can cause persistent errors requiring a server restart. (`#2 `_) 0.7.2 (2013-02-19) ------------------ * Make life-time of sessions configurable. Specify `keep_session=True` when setting up the SA extension. * Python 3.3 compatibility. 0.7.1 (2012-05-19) ------------------ * Use ``@implementer`` as a class decorator instead of ``implements()`` at class scope for compatibility with ``zope.interface`` 4.0. This requires ``zope.interface`` >= 3.6.0. 0.7 (2011-12-06) ---------------- * Python 3.2 compatibility. 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/CONTRIBUTING.md0000644000076600000240000000144514500002047016423 0ustar00m.howitzstaff # Contributing to zopefoundation projects The projects under the zopefoundation GitHub organization are open source and welcome contributions in different forms: * bug reports * code improvements and bug fixes * documentation improvements * pull request reviews For any changes in the repository besides trivial typo fixes you are required to sign the contributor agreement. See https://www.zope.dev/developer/becoming-a-committer.html for details. Please visit our [Developer Guidelines](https://www.zope.dev/developer/guidelines.html) if you'd like to contribute code changes and our [guidelines for reporting bugs](https://www.zope.dev/developer/reporting-bugs.html) if you want to file a bug report. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/COPYRIGHT.txt0000644000076600000240000000004014500002047016271 0ustar00m.howitzstaffZope Foundation and Contributors././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/CREDITS.rst0000644000076600000240000000052614500002047016020 0ustar00m.howitzstaffzope.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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/LICENSE.txt0000644000076600000240000000402614500002047016013 0ustar00m.howitzstaffZope 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/MANIFEST.in0000644000076600000240000000057714500002047015735 0ustar00m.howitzstaff# Generated from: # https://github.com/zopefoundation/meta/tree/master/config/zope-product include *.md include *.rst include *.txt include buildout.cfg include tox.ini recursive-include src *.py include github_actions.cfg include github_actions20.cfg include mysql.cfg include oracle.cfg include postgres.cfg include postgres20.cfg include pysqlite.cfg recursive-include src *.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1694499879.5075448 zope.sqlalchemy-3.1/PKG-INFO0000644000076600000240000004362114500002050015263 0ustar00m.howitzstaffMetadata-Version: 2.1 Name: zope.sqlalchemy Version: 3.1 Summary: Minimal Zope/SQLAlchemy transaction integration Home-page: https://github.com/zopefoundation/zope.sqlalchemy Author: Laurence Rowe Author-email: laurence@lrowe.co.uk License: ZPL 2.1 Keywords: zope zope3 sqlalchemy Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Pyramid Classifier: Framework :: Zope :: 3 Classifier: Framework :: Zope :: 5 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.7 Provides-Extra: test License-File: LICENSE.txt *************** 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`_ (used by `Turbogears 2`_ and other systems). This package is also used by `pyramid_tm`_ (an add-on of the `Pyramid`_) web framework. You need to understand `SQLAlchemy`_ and the `Zope transaction manager`_ for this package and this README to make any sense. .. _repoze.tm2: https://repozetm2.readthedocs.io/en/latest/ .. _pyramid_tm: https://docs.pylonsproject.org/projects/pyramid_tm/en/latest/ .. _Pyramid: https://pylonsproject.org/ .. _Turbogears 2: https://turbogears.org/ .. _SQLAlchemy: https://sqlalchemy.org/docs/ .. _Zope transaction manager: https://www.zodb.org/en/latest/#transactions Running the tests ================= This package is distributed as a buildout. Using your desired python run: $ python bootstrap.py $ ./bin/buildout 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 Usage in short ============== The integration between Zope transactions and the SQLAlchemy event system is done using the ``register()`` function on the session factory class. .. code-block:: python from zope.sqlalchemy import register from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session engine = sqlalchemy.create_engine("postgresql://scott:tiger@localhost/test") DBSession = scoped_session(sessionmaker(bind=engine)) register(DBSession) Instantiated sessions commits and rollbacks will now be integrated with Zope transactions. .. code-block:: python import transaction from sqlalchemy.sql import text session = DBSession() result = session.execute(text("DELETE FROM objects WHERE id=:id"), {"id": 2}) row = result.fetchone() transaction.commit() Full Example ============ This example is lifted directly from the SQLAlchemy declarative documentation. First the necessary imports. >>> from sqlalchemy import * >>> from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker, relationship >>> from sqlalchemy.sql import text >>> from zope.sqlalchemy import register >>> 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 = relationship("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) >>> 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 register >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE)) 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() >>> register(session) >>> session.query(User).all() [] We can now create a new user and commit the changes using Zope's transaction machinery, 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(text('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] >>> str(bob.name) '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 [
] >>> str(bob.addresses[0].email) '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] >>> str(bob.addresses[0].email) '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().where(users.c.name=='bob'), {'name': 'ben'}) >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session) >>> transaction.commit() >>> session = Session() >>> str(session.query(User).all()[0].name) 'ben' >>> transaction.abort() If this is a problem you may register the events and tell them to place the session in the 'changed' state initially. >>> Session.remove() >>> register(Session, 'changed') >>> session = Session() >>> conn = session.connection() >>> conn.execute(users.update().where(users.c.name=='ben'), {'name': 'bob'}) >>> transaction.commit() >>> session = Session() >>> str(session.query(User).all()[0].name) 'bob' >>> transaction.abort() The `mark_changed` function accepts a kwarg for `keep_session` which defaults to `False` and is unaware of the registered extensions `keep_session` configuration. If you intend for `keep_session` to be True, you can specify it explicitly: >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session, keep_session=True) >>> transaction.commit() You can also use a configured extension to preserve this argument: >>> sessionExtension = register(session, keep_session=True) >>> sessionExtension.mark_changed(session) >>> transaction.commit() Long-lasting session scopes --------------------------- The default behaviour of the transaction integration is to close the session after a commit. You can tell by trying to access an object after committing: >>> bob = session.query(User).all()[0] >>> transaction.commit() >>> bob.name Traceback (most recent call last): sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed... To support cases where a session needs to last longer than a transaction (useful in test suites) you can specify to keep a session when registering the events: >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE)) >>> register(Session, keep_session=True) >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.name = 'bobby' >>> transaction.commit() >>> bob.name 'bobby' The session must then be closed manually: >>> session.close() Development version =================== `GIT version `_ Changes ======= 3.1 (2023-09-12) ---------------- - Fix ``psycopg.errors.OperationalError.sqlstate`` can be ``None``. (`#81 `_) 3.0 (2023-06-01) ---------------- - Add support for SQLAlchemy 2.0 and for new psycopg v3 backend. (`#79 `_) **Breaking Changes** - No longer allow calling ``session.commit()`` within a manual nested database transaction (a savepoint). If you want to use savepoints directly in code that is not aware of ``transaction.savepoint()`` with ``session.begin_nested()`` then use the savepoint returned by the function to commit just the nested transaction i.e. ``savepoint = session.begin_nested(); savepoint.commit()`` or use it as a context manager i.e. ``with session.begin_nested():``. (`for details see #79 `_) 2.0 (2023-02-06) ---------------- - Drop support for Python 2.7, 3.5, 3.6. - Drop support for ``SQLAlchemy < 1.1`` (`#65 `_) - Add support for Python 3.10, 3.11. 1.6 (2021-09-06) ---------------- - Add support for Python 2.7 on SQLAlchemy 1.4. (`#71 `_) 1.5 (2021-07-14) ---------------- - Call ``mark_changed`` also on the ``do_orm_execute`` event if the operation is an insert, update or delete. This is SQLAlchemy >= 1.4 only, as it introduced that event. (`#67 `_) - Fixup get transaction. There was regression introduced in 1.4. (`#66 `_) 1.4 (2021-04-26) ---------------- - Add ``mark_changed`` and ``join_transaction`` methods to ``ZopeTransactionEvents``. (`#46 `_) - Reduce DeprecationWarnings with SQLAlchemy 1.4 and require at least SQLAlchemy >= 0.9. (`#54 `_) - Add support for SQLAlchemy 1.4. (`#58 `_) - Prevent using an SQLAlchemy 1.4 version with broken flush support. (`#57 `_) 1.3 (2020-02-17) ---------------- * ``.datamanager.register()`` now returns the ``ZopeTransactionEvents`` instance which was used to register the events. This allows to change its parameters afterwards. (`#40 `_) * Add preliminary support for Python 3.9a3. 1.2 (2019-10-17) ---------------- **Breaking Changes** * Drop support for Python 3.4. * Add support for Python 3.7 and 3.8. * Fix deprecation warnings for the event system. We already used it in general but still leveraged the old extension mechanism in some places. (`#31 `_) To make things clearer we renamed the ``ZopeTransactionExtension`` class to ``ZopeTransactionEvents``. Existing code using the 'register' version stays compatible. **Upgrade from 1.1** Your old code like this: .. code-block:: python from zope.sqlalchemy import ZopeTransactionExtension DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension(), **options)) becomes: .. code-block:: python from zope.sqlalchemy import register DBSession = scoped_session(sessionmaker(**options)) register(DBSession) 1.1 (2019-01-03) ---------------- * Add support to MySQL using pymysql. 1.0 (2018-01-31) ---------------- * Add support for Python 3.4 up to 3.6. * Support SQLAlchemy 1.2. * Drop support for Python 2.6, 3.2 and 3.3. * Drop support for transaction < 1.6.0. * Fix hazard that could cause SQLAlchemy session not to be committed when transaction is committed in rare situations. (`#23 `_) 0.7.7 (2016-06-23) ------------------ * Support SQLAlchemy 1.1. (`#15 `_) 0.7.6 (2015-03-20) ------------------ * Make version check in register compatible with prereleases. 0.7.5 (2014-06-17) ------------------ * Ensure mapped objects are expired following a ``transaction.commit()`` when no database commit was required. (`#8 `_) 0.7.4 (2014-01-06) ------------------ * Allow ``session.commit()`` on nested transactions to facilitate integration of existing code that might not use ``transaction.savepoint()``. (`#1 `_) * Add a new function zope.sqlalchemy.register(), which replaces the direct use of ZopeTransactionExtension to make use of the newer SQLAlchemy event system to establish instrumentation on the given Session instance/class/factory. Requires at least SQLAlchemy 0.7. (`#4 `_) * Fix `keep_session=True` doesn't work when a transaction is joined by flush and other manngers bug. (`#5 `_) 0.7.3 (2013-09-25) ------------------ * Prevent the ``Session`` object from getting into a "wedged" state if joining a transaction fails. With thread scoped sessions that are reused this can cause persistent errors requiring a server restart. (`#2 `_) 0.7.2 (2013-02-19) ------------------ * Make life-time of sessions configurable. Specify `keep_session=True` when setting up the SA extension. * Python 3.3 compatibility. 0.7.1 (2012-05-19) ------------------ * Use ``@implementer`` as a class decorator instead of ``implements()`` at class scope for compatibility with ``zope.interface`` 4.0. This requires ``zope.interface`` >= 3.6.0. 0.7 (2011-12-06) ---------------- * Python 3.2 compatibility. 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/README.rst0000644000076600000240000000041014500002047015650 0ustar00m.howitzstaff|Build status|_ .. |Build status| image:: https://github.com/zopefoundation/zope.sqlalchemy/actions/workflows/tests.yml/badge.svg .. _Build status: https://github.com/zopefoundation/zope.sqlalchemy/actions/workflows/tests.yml See src/zope/sqlalchemy/README.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/buildout.cfg0000644000076600000240000000041114500002047016472 0ustar00m.howitzstaff[buildout] develop = . parts = test scripts [test] recipe = zc.recipe.testrunner eggs = zope.sqlalchemy [test] defaults = ['--auto-color', '-s', 'zope.sqlalchemy'] [scripts] recipe = zc.recipe.egg eggs = ${test:eggs} collective.checkdocs interpreter = py ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/github_actions.cfg0000644000076600000240000000036514500002047017655 0ustar00m.howitzstaff# This config is intended for the use of github actions as the ident # auth-method does not work well with containers. [buildout] extends = postgres.cfg [pgenv] TEST_DSN = postgresql+psycopg2://postgres:postgres@localhost/zope_sqlalchemy_tests ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/github_actions20.cfg0000644000076600000240000000052314500002047020013 0ustar00m.howitzstaff# This config is intended for the use of github actions as the ident # auth-method does not work well with containers. [buildout] extends = postgres20.cfg [pgenv] TEST_DSN = postgresql+psycopg2://postgres:postgres@localhost/zope_sqlalchemy_tests [pgenv3] TEST_DSN = postgresql+psycopg://postgres:postgres@localhost/zope_sqlalchemy_tests ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/mysql.cfg0000644000076600000240000000042214500002047016012 0ustar00m.howitzstaff# bin/buildout -c mysql.cfg # mysql -u root -e "create database zope_sqlalchemy_tests" [buildout] extends = buildout.cfg parts += testmysql [test] eggs += pymysql [testmysql] <= test environment = mysqlenv [mysqlenv] TEST_DSN = mysql+pymysql:///zope_sqlalchemy_tests ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/oracle.cfg0000644000076600000240000000151514500002047016116 0ustar00m.howitzstaff# 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 [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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/postgres.cfg0000644000076600000240000000125714500002047016522 0ustar00m.howitzstaff# 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/ parts += testpg testpg2 [testpg] <= test eggs += psycopg2 environment = pgenv [testpg2] <= testpg environment = pgenv2 [scripts] eggs += psycopg2 [pgenv] TEST_DSN = postgresql+psycopg2:///zope_sqlalchemy_tests [pgenv2] <= pgenv TEST_TWOPHASE=True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/postgres20.cfg0000644000076600000240000000121414500002047016655 0ustar00m.howitzstaff# 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 = postgres.cfg parts += testpg3 testpg32 [testpg3] <= test eggs += psycopg[c] environment = pgenv3 [testpg32] <= testpg3 environment = pgenv32 [scripts] eggs += psycopg[c] [pgenv3] TEST_DSN = postgresql+psycopg:///zope_sqlalchemy_tests [pgenv32] <= pgenv3 TEST_TWOPHASE=True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/pysqlite.cfg0000644000076600000240000000066414500002047016527 0ustar00m.howitzstaff# See: https://code.google.com/p/pysqlite-static-env/ [buildout] extends = buildout.cfg parts = pysqlite test scripts versions = versions [versions] pysqlite = 2.6.3-static-env-savepoints [test] eggs += pysqlite [pysqlite] recipe = zc.recipe.egg:custom environment = pysqlite-env find-links = http://pysqlite-static-env.googlecode.com/files/pysqlite-2.6.3-static-env-savepoints.tar.gz [pysqlite-env] STATICBUILD = true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1694499879.5078597 zope.sqlalchemy-3.1/setup.cfg0000644000076600000240000000073114500002050016002 0ustar00m.howitzstaff[bdist_wheel] universal = 0 [flake8] doctests = 1 no-accept-encodings = True htmldir = parts/flake8 [check-manifest] ignore = .editorconfig .meta.toml [isort] force_single_line = True combine_as_imports = True sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER known_third_party = six, docutils, pkg_resources, pytz known_zope = known_first_party = default_section = ZOPE line_length = 79 lines_after_imports = 2 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/setup.py0000644000076600000240000000361214500002047015702 0ustar00m.howitzstaffimport os.path from setuptools import find_packages from setuptools import setup tests_require = ['zope.testing'] setup( name='zope.sqlalchemy', version='3.1', packages=find_packages('src'), package_dir={'': 'src'}, include_package_data=True, zip_safe=False, namespace_packages=['zope'], test_suite='zope.sqlalchemy.tests.test_suite', author='Laurence Rowe', author_email='laurence@lrowe.co.uk', url='https://github.com/zopefoundation/zope.sqlalchemy', description="Minimal Zope/SQLAlchemy transaction integration", long_description=( open(os.path.join('src', 'zope', 'sqlalchemy', 'README.rst')).read() + "\n\n" + open('CHANGES.rst').read()), license='ZPL 2.1', keywords='zope zope3 sqlalchemy', classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Pyramid", "Framework :: Zope :: 3", "Framework :: Zope :: 5", "Intended Audience :: Developers", "License :: OSI Approved :: Zope Public License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database", "Topic :: Software Development :: Libraries :: Python Modules", ], python_requires='>=3.7', install_requires=[ 'packaging', 'setuptools', 'SQLAlchemy>=1.1,!=1.4.0,!=1.4.1,!=1.4.2,!=1.4.3,!=1.4.4,!=1.4.5,!=1.4.6', # noqa: E501 line too long 'transaction>=1.6.0', 'zope.interface>=3.6.0', ], extras_require={'test': tests_require}, tests_require=tests_require, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1694499879.5011308 zope.sqlalchemy-3.1/src/0000755000076600000240000000000014500002050014747 5ustar00m.howitzstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1694499879.5044868 zope.sqlalchemy-3.1/src/zope/0000755000076600000240000000000014500002050015724 5ustar00m.howitzstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope/__init__.py0000644000076600000240000000007014500002047020040 0ustar00m.howitzstaff__import__('pkg_resources').declare_namespace(__name__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1694499879.5072708 zope.sqlalchemy-3.1/src/zope/sqlalchemy/0000755000076600000240000000000014500002050020066 5ustar00m.howitzstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope/sqlalchemy/README.rst0000644000076600000240000002104314500002047021563 0ustar00m.howitzstaff*************** 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`_ (used by `Turbogears 2`_ and other systems). This package is also used by `pyramid_tm`_ (an add-on of the `Pyramid`_) web framework. You need to understand `SQLAlchemy`_ and the `Zope transaction manager`_ for this package and this README to make any sense. .. _repoze.tm2: https://repozetm2.readthedocs.io/en/latest/ .. _pyramid_tm: https://docs.pylonsproject.org/projects/pyramid_tm/en/latest/ .. _Pyramid: https://pylonsproject.org/ .. _Turbogears 2: https://turbogears.org/ .. _SQLAlchemy: https://sqlalchemy.org/docs/ .. _Zope transaction manager: https://www.zodb.org/en/latest/#transactions Running the tests ================= This package is distributed as a buildout. Using your desired python run: $ python bootstrap.py $ ./bin/buildout 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 Usage in short ============== The integration between Zope transactions and the SQLAlchemy event system is done using the ``register()`` function on the session factory class. .. code-block:: python from zope.sqlalchemy import register from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session engine = sqlalchemy.create_engine("postgresql://scott:tiger@localhost/test") DBSession = scoped_session(sessionmaker(bind=engine)) register(DBSession) Instantiated sessions commits and rollbacks will now be integrated with Zope transactions. .. code-block:: python import transaction from sqlalchemy.sql import text session = DBSession() result = session.execute(text("DELETE FROM objects WHERE id=:id"), {"id": 2}) row = result.fetchone() transaction.commit() Full Example ============ This example is lifted directly from the SQLAlchemy declarative documentation. First the necessary imports. >>> from sqlalchemy import * >>> from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker, relationship >>> from sqlalchemy.sql import text >>> from zope.sqlalchemy import register >>> 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 = relationship("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) >>> 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 register >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE)) 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() >>> register(session) >>> session.query(User).all() [] We can now create a new user and commit the changes using Zope's transaction machinery, 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(text('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] >>> str(bob.name) '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 [
] >>> str(bob.addresses[0].email) '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] >>> str(bob.addresses[0].email) '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().where(users.c.name=='bob'), {'name': 'ben'}) >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session) >>> transaction.commit() >>> session = Session() >>> str(session.query(User).all()[0].name) 'ben' >>> transaction.abort() If this is a problem you may register the events and tell them to place the session in the 'changed' state initially. >>> Session.remove() >>> register(Session, 'changed') >>> session = Session() >>> conn = session.connection() >>> conn.execute(users.update().where(users.c.name=='ben'), {'name': 'bob'}) >>> transaction.commit() >>> session = Session() >>> str(session.query(User).all()[0].name) 'bob' >>> transaction.abort() The `mark_changed` function accepts a kwarg for `keep_session` which defaults to `False` and is unaware of the registered extensions `keep_session` configuration. If you intend for `keep_session` to be True, you can specify it explicitly: >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session, keep_session=True) >>> transaction.commit() You can also use a configured extension to preserve this argument: >>> sessionExtension = register(session, keep_session=True) >>> sessionExtension.mark_changed(session) >>> transaction.commit() Long-lasting session scopes --------------------------- The default behaviour of the transaction integration is to close the session after a commit. You can tell by trying to access an object after committing: >>> bob = session.query(User).all()[0] >>> transaction.commit() >>> bob.name Traceback (most recent call last): sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed... To support cases where a session needs to last longer than a transaction (useful in test suites) you can specify to keep a session when registering the events: >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE)) >>> register(Session, keep_session=True) >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.name = 'bobby' >>> transaction.commit() >>> bob.name 'bobby' The session must then be closed manually: >>> session.close() Development version =================== `GIT version `_ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope/sqlalchemy/__init__.py0000644000076600000240000000163614500002047022213 0ustar00m.howitzstaff############################################################################## # # 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 # ############################################################################## from zope.sqlalchemy.datamanager import ZopeTransactionEvents from zope.sqlalchemy.datamanager import mark_changed from zope.sqlalchemy.datamanager import register invalidate = mark_changed __all__ = [ 'ZopeTransactionEvents', 'invalidate', 'mark_changed', 'register', ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope/sqlalchemy/datamanager.py0000644000076600000240000003166714500002047022727 0ustar00m.howitzstaff############################################################################## # # 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 # ############################################################################## from weakref import WeakKeyDictionary import transaction as zope_transaction from packaging.version import Version as parse_version from sqlalchemy import __version__ as sqlalchemy_version from sqlalchemy.engine.base import Engine from sqlalchemy.exc import DBAPIError from sqlalchemy.orm.exc import ConcurrentModificationError from transaction._transaction import Status as ZopeStatus from transaction.interfaces import IDataManagerSavepoint from transaction.interfaces import ISavepointDataManager from zope.interface import implementer _retryable_errors = [] try: import psycopg2.extensions except ImportError: pass else: _retryable_errors.append( (psycopg2.extensions.TransactionRollbackError, None)) # Error Class 40: Transaction Rollback, for details # see https://www.psycopg.org/psycopg3/docs/api/errors.html try: import psycopg.errors except ImportError: pass else: _retryable_errors.append( (psycopg.errors.OperationalError, lambda e: e.sqlstate and e.sqlstate.startswith('40')) ) # 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) ) # 1213: Deadlock found when trying to get lock; try restarting transaction try: import pymysql except ImportError: pass else: _retryable_errors.append( (pymysql.err.OperationalError, lambda e: e.args[0] == 1213) ) # 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 # session joined to transaction, no writes allowed. STATUS_READONLY = "readonly" STATUS_INVALIDATED = STATUS_CHANGED # BBB NO_SAVEPOINT_SUPPORT = {"sqlite"} _SESSION_STATE = WeakKeyDictionary() # a mapping of session -> status # This is thread safe because you are using scoped sessions SA_GE_14 = parse_version(sqlalchemy_version) >= parse_version('1.4.0') # # The two variants of the DataManager. # @implementer(ISavepointDataManager) class SessionDataManager: """Integrate a top level sqlalchemy session transaction into a zope transaction. One phase variant. """ def __init__( self, session, status, transaction_manager, keep_session=False): self.transaction_manager = transaction_manager if SA_GE_14: root_transaction = session.get_transaction() or session.begin() else: # Support both SQLAlchemy 1.0 and 1.1 # https://github.com/zopefoundation/zope.sqlalchemy/issues/15 _iterate_parents = ( getattr(session.transaction, "_iterate_self_and_parents", None) or session.transaction._iterate_parents ) root_transaction = _iterate_parents()[-1] self.tx = root_transaction self.session = session transaction_manager.get().join(self) _SESSION_STATE[session] = status self.state = "init" self.keep_session = keep_session def _finish(self, final_state): assert self.tx is not None session = self.session del _SESSION_STATE[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 if not self.keep_session: session.close() else: session.expire_all() 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[self.session] if status is not STATUS_INVALIDATED: session = self.session if session.expire_on_commit: session.expire_all() 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 != "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 { engine.url.drivername for engine in self.tx._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): # we may not have voted, and been aborted already if self.tx is not None: self.tx.rollback() self._finish("aborted commit") def sortKey(self): # Sort normally return "sqlalchemy.twophase:%d" % id(self.tx) @implementer(IDataManagerSavepoint) class SessionSavepoint: 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. self.transaction.rollback() def join_transaction( session, initial_state=STATUS_ACTIVE, transaction_manager=zope_transaction.manager, keep_session=False, ): """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 ZopeTransactionEvents can be used to ensure that this is called automatically after session write operations. """ if _SESSION_STATE.get(session, None) is None: if session.twophase: DataManager = TwoPhaseSessionDataManager else: DataManager = SessionDataManager DataManager( session, initial_state, transaction_manager, keep_session=keep_session ) def mark_changed( session, transaction_manager=zope_transaction.manager, keep_session=False ): """Mark a session as needing to be committed. """ assert ( _SESSION_STATE.get(session, None) is not STATUS_READONLY ), "Session already registered as read only" join_transaction(session, STATUS_CHANGED, transaction_manager, keep_session) _SESSION_STATE[session] = STATUS_CHANGED class ZopeTransactionEvents: """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, keep_session=False, ): if initial_state == "invalidated": initial_state = STATUS_CHANGED # BBB self.initial_state = initial_state self.transaction_manager = transaction_manager self.keep_session = keep_session def after_begin(self, session, transaction, connection): join_transaction( session, self.initial_state, self.transaction_manager, self.keep_session ) def after_attach(self, session, instance): join_transaction( session, self.initial_state, self.transaction_manager, self.keep_session ) def after_flush(self, session, flush_context): mark_changed(session, self.transaction_manager, self.keep_session) def after_bulk_update(self, update_context): mark_changed(update_context.session, self.transaction_manager, self.keep_session) def after_bulk_delete(self, delete_context): mark_changed(delete_context.session, self.transaction_manager, self.keep_session) def before_commit(self, session): in_nested_transaction = ( session.in_nested_transaction() if SA_GE_14 # support sqlalchemy 1.3 and below else session.transaction.nested ) assert ( in_nested_transaction or self.transaction_manager.get().status == ZopeStatus.COMMITTING ), "Transaction must be committed using the transaction manager" def do_orm_execute(self, execute_state): dml = any((execute_state.is_update, execute_state.is_insert, execute_state.is_delete)) if execute_state.is_orm_statement and dml: mark_changed(execute_state.session, self.transaction_manager, self.keep_session) def mark_changed(self, session): """Developer interface to `mark_changed` that preserves the extension's active configuration. """ mark_changed(session, self.transaction_manager, self.keep_session) def join_transaction(self, session): """Developer interface to `join_transaction` that preserves the extension's active configuration. """ join_transaction( session, self.initial_state, self.transaction_manager, self.keep_session ) def register( session, initial_state=STATUS_ACTIVE, transaction_manager=zope_transaction.manager, keep_session=False, ): """Register ZopeTransaction listener events on the given Session or Session factory/class. This function requires at least SQLAlchemy 0.7 and makes use of the newer sqlalchemy.event package in order to register event listeners on the given Session. The session argument here may be a Session class or subclass, a sessionmaker or scoped_session instance, or a specific Session instance. Event listening will be specific to the scope of the type of argument passed, including specificity to its subclass as well as its identity. It returns the instance of ZopeTransactionEvents those methods where used to register the event listeners. """ from sqlalchemy import event ext = ZopeTransactionEvents( initial_state=initial_state, transaction_manager=transaction_manager, keep_session=keep_session, ) event.listen(session, "after_begin", ext.after_begin) event.listen(session, "after_attach", ext.after_attach) event.listen(session, "after_flush", ext.after_flush) event.listen(session, "after_bulk_update", ext.after_bulk_update) event.listen(session, "after_bulk_delete", ext.after_bulk_delete) event.listen(session, "before_commit", ext.before_commit) if SA_GE_14: event.listen(session, "do_orm_execute", ext.do_orm_execute) return ext ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope/sqlalchemy/tests.py0000644000076600000240000007104114500002047021613 0ustar00m.howitzstaff############################################################################## # # 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 # ############################################################################## # Inspiration from z3c.sqlalchemy/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 threading import time import unittest import sqlalchemy as sa import transaction from packaging.version import Version as parse_version from sqlalchemy import __version__ as sqlalchemy_version from sqlalchemy import exc from sqlalchemy import orm from sqlalchemy import sql from transaction._transaction import Status as ZopeStatus from transaction.interfaces import TransactionFailedError from zope.sqlalchemy import datamanager as tx from zope.sqlalchemy import mark_changed SA_GE_20 = parse_version(sqlalchemy_version) >= parse_version('2.0.0') TEST_TWOPHASE = bool(os.environ.get("TEST_TWOPHASE")) TEST_DSN = os.environ.get("TEST_DSN", "sqlite:///:memory:") class SimpleModel: def __init__(self, **kw): for k, v in kw.items(): setattr(self, k, v) def asDict(self): return {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) # See https://code.google.com/p/pysqlite-static-env/ HAS_PATCHED_PYSQLITE = False if engine.url.drivername == "sqlite": try: from pysqlite2.dbapi2 import Connection except ImportError: pass else: if hasattr(Connection, "operation_needs_transaction_callback"): HAS_PATCHED_PYSQLITE = True if HAS_PATCHED_PYSQLITE: from sqlalchemy import event from zope.sqlalchemy.datamanager import NO_SAVEPOINT_SUPPORT NO_SAVEPOINT_SUPPORT.remove("sqlite") @event.listens_for(engine, "connect") def connect(dbapi_connection, connection_record): dbapi_connection.operation_needs_transaction_callback = lambda x: True Session = orm.scoped_session(orm.sessionmaker( bind=engine, twophase=TEST_TWOPHASE)) tx.register(Session) UnboundSession = orm.scoped_session(orm.sessionmaker(twophase=TEST_TWOPHASE)) tx.register(UnboundSession) EventSession = orm.scoped_session( orm.sessionmaker(bind=engine, twophase=TEST_TWOPHASE)) tx.register(EventSession) KeepSession = orm.scoped_session( orm.sessionmaker(bind=engine, twophase=TEST_TWOPHASE)) tx.register(KeepSession, keep_session=True) metadata = sa.MetaData() # best to use unbound metadata test_users = sa.Table( "test_users", metadata, sa.Column("id", sa.Integer, primary_key=True), # mssql cannot do equality on a text type sa.Column("firstname", sa.VARCHAR(255)), 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",)), ) if SA_GE_20: # bound metadata does no longer exist in SQLAlchemy 2.0 test_one = sa.Table( "test_one", metadata, sa.Column("id", sa.Integer, primary_key=True) ) test_two = sa.Table( "test_two", metadata, sa.Column("id", sa.Integer, primary_key=True) ) else: engine2 = sa.create_engine(TEST_DSN) 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. if SA_GE_20: mapper_reg = orm.registry() mapper = mapper_reg.map_imperatively else: mapper = orm.mapper m1 = mapper( User, test_users, properties={ "skills": orm.relationship( Skill, primaryjoin=( test_users.columns["id"] == test_skills.columns["user_id"] ), ) }, ) m2 = mapper(Skill, test_skills) m3 = mapper(TestOne, test_one) m4 = mapper(TestTwo, test_two) return [m1, m2, m3, m4] class DummyException(Exception): pass class DummyTargetRaised(DummyException): pass class DummyTargetResult(DummyException): pass class DummyDataManager: 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 as 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) # a connection which bypasses the session/transaction machinery self.conn = engine.connect() def tearDown(self): transaction.abort() metadata.drop_all(engine) orm.clear_mappers() self.conn.close() def testMarkUnknownSession(self): import zope.sqlalchemy.datamanager DummyDataManager(key="dummy.first") session = Session() mark_changed(session) self.assertTrue(session in zope.sqlalchemy.datamanager._SESSION_STATE) def testAbortBeforeCommit(self): # Simulate what happens in a conflict error DummyDataManager(key="dummy.first") session = Session() conn = session.connection() mark_changed(session) try: # Thus we could fail in commit transaction.commit() except: # noqa: E722 do not use bare 'except' # But abort must succeed (and rollback the base connection) transaction.abort() pass # Or the next transaction will not be able to start! transaction.begin() session = Session() conn = session.connection() conn.execute(sql.text("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: # noqa: E722 do not use bare 'except' # But abort must succed (and actually rollback the base connection) transaction.abort() pass # Or the next transaction will not be able to start! transaction.begin() session = Session() conn = session.connection() conn.execute(sql.text("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 machinery if SA_GE_20: stmt = sql.select(*test_users.columns).order_by("id") else: stmt = sql.select(test_users.columns).order_by("id") conn = session.connection() results = conn.execute(stmt) self.assertEqual( results.fetchall(), [(1, "udo", "juergens"), (2, "heino", "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.assertFalse( [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.assertTrue( [r for r in t._resources if isinstance(r, tx.SessionDataManager)], "Not joined transaction", ) def testTransactionJoiningUsingRegister(self): transaction.abort() # clean slate t = transaction.get() self.assertFalse( [r for r in t._resources if isinstance(r, tx.SessionDataManager)], "Joined transaction too early", ) session = EventSession() session.add(User(id=1, firstname="udo", lastname="juergens")) t = transaction.get() self.assertTrue( [r for r in t._resources if isinstance(r, tx.SessionDataManager)], "Not joined transaction", ) def testSavepoint(self): use_savepoint = engine.url.drivername not in tx.NO_SAVEPOINT_SUPPORT t = transaction.get() session = Session() query = session.query(User) self.assertFalse(query.all(), "Users table should be empty") 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.assertTrue(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.assertTrue(len(query.all()) == 2, "Users table should have two rows") s2.rollback() self.assertTrue(len(query.all()) == 1, "Users table should have one row") s1.rollback() self.assertFalse(query.all(), "Users table should be empty") def testRollbackAttributes(self): use_savepoint = engine.url.drivername not in tx.NO_SAVEPOINT_SUPPORT if not use_savepoint: self.skipTest('No savepoint support') t = transaction.get() session = Session() query = session.query(User) self.assertFalse(query.all(), "Users table should be empty") 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() 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) machinery with self.conn.begin(): results = self.conn.execute(test_users.select()) self.assertEqual(len(results.fetchall()), 2) def testCommitWithSavepoint(self): if engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT: self.skipTest('No savepoint support') 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() t.savepoint() session.delete(rows[1]) session.flush() transaction.commit() # bypass the session machinery with self.conn.begin(): results = self.conn.execute(test_users.select()) self.assertEqual(len(results.fetchall()), 1) def testSessionSavepointCommitAllowed(self): # Existing code might use nested transactions if engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT: self.skipTest('No save point support') session = Session() session.add(User(id=1, firstname="udo", lastname="juergens")) savepoint = session.begin_nested() session.add(User(id=2, firstname="heino", lastname="n/a")) savepoint.commit() transaction.commit() def testSessionCommitDisallowed(self): session = Session() session.add(User(id=1, firstname="udo", lastname="juergens")) self.assertRaises(AssertionError, session.commit) def testTwoPhase(self): session = Session() if not session.twophase: self.skipTest('No two phase transaction support') 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 as e: result = e.args[0] except DummyTargetRaised as 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 as 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() with self.conn.begin(): results = self.conn.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() with self.conn.begin(): results = self.conn.execute( test_users.select().where(test_users.c.lastname == "smith") ) self.assertEqual(len(results.fetchall()), 2) def testBulkDeleteUsingRegister(self): session = EventSession() session.add(User(id=1, firstname="udo", lastname="juergens")) session.add(User(id=2, firstname="heino", lastname="n/a")) transaction.commit() session = EventSession() session.query(User).delete() transaction.commit() with self.conn.begin(): results = self.conn.execute(test_users.select()) self.assertEqual(len(results.fetchall()), 0) def testBulkUpdateUsingRegister(self): session = EventSession() session.add(User(id=1, firstname="udo", lastname="juergens")) session.add(User(id=2, firstname="heino", lastname="n/a")) transaction.commit() session = EventSession() session.query(User).update(dict(lastname="smith")) transaction.commit() with self.conn.begin(): results = self.conn.execute( test_users.select().where(test_users.c.lastname == "smith") ) self.assertEqual(len(results.fetchall()), 2) def testFailedJoin(self): # When a join is issued while the transaction is in COMMITFAILED, the # session is never closed and the session id stays in _SESSION_STATE, # which means the session won't be joined in the future either. This # causes the session to stay open forever, potentially accumulating # data, but never issuing a commit. dummy = DummyDataManager(key="dummy.first") transaction.get().join(dummy) try: transaction.commit() except DummyException: # Commit raised an error, we are now in COMMITFAILED pass self.assertEqual(transaction.get().status, ZopeStatus.COMMITFAILED) session = Session() # try to interact with the session while the transaction is still # in COMMITFAILED self.assertRaises(TransactionFailedError, session.query(User).all) transaction.abort() # start a new transaction everything should be ok now transaction.begin() session = Session() self.assertEqual([], session.query(User).all()) session.add(User(id=1, firstname="udo", lastname="juergens")) # abort transaction, session should be closed without commit transaction.abort() self.assertEqual([], session.query(User).all()) def testKeepSession(self): session = KeepSession() try: with transaction.manager: session.add(User(id=1, firstname="foo", lastname="bar")) if SA_GE_20: user = session.get(User, 1) else: user = session.query(User).get(1) # if the keep_session works correctly, this transaction will not # close the session after commit with transaction.manager: user.firstname = "super" session.flush() # make sure the session is still attached to user self.assertEqual(user.firstname, "super") finally: # KeepSession does not rollback on transaction abort session.rollback() def testExpireAll(self): session = Session() session.add(User(id=1, firstname="udo", lastname="juergens")) transaction.commit() session = Session() if SA_GE_20: instance = session.get(User, 1) else: instance = session.query(User).get(1) transaction.commit() # No work, session.close() self.assertEqual(sa.inspect(instance).expired, True) 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. self.e1 = sa.create_engine(TEST_DSN) self.e2 = sa.create_engine(TEST_DSN) self.s1 = orm.sessionmaker(bind=self.e1, twophase=TEST_TWOPHASE)() tx.register(self.s1, transaction_manager=self.tm1) self.s2 = orm.sessionmaker(bind=self.e2, twophase=TEST_TWOPHASE)() tx.register(self.s2, transaction_manager=self.tm2) 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() # ensure any open connections on the temporary engines get closed # if we don't do this we get a `ResourceWarning` in psycopg v3 self.e1.dispose() self.e2.dispose() self.e1 = None self.e2 = None self.s1 = None self.s2 = None 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.assertTrue( len(s1.query(User).all()) == 1, "Users table should have one row" ) tm2.begin() self.assertTrue( len(s2.query(User).all()) == 1, "Users table should have one row" ) s1.query(User).delete() if SA_GE_20: user = s2.get(User, 1) else: user = s2.query(User).get(1) user.lastname = "smith" tm1.commit() raised = False try: s2.flush() except orm.exc.ConcurrentModificationError as e: # This error is thrown when the number of updated rows is not as # expected raised = True self.assertTrue(tm2._retryable(type(e), e), "Error should be retryable") self.assertTrue(raised, "Did not raise expected error") 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.assertTrue( len(s1.query(User).all()) == 1, "Users table should have one row" ) tm2.begin() s2.connection().execute(sql.text( "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE" )) self.assertTrue( 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: if SA_GE_20: s2.query(User).with_for_update().filter(User.id == 1).one() else: s2.query(User).with_for_update().get(1) except exc.DBAPIError as e: # This error wraps the underlying DBAPI module error, some of which # are retryable raised = True retryable = tm2._retryable(type(e), e) self.assertTrue(retryable, "Error should be retryable") self.assertTrue(raised, "Did not raise expected error") thread.join() # well, we must have joined by now class MultipleEngineTests(unittest.TestCase): def setUp(self): if SA_GE_20: self.skipTest( 'Bound metadata is not supported in SQLAlchemy 2.0' ) 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(): import doctest from unittest import TestSuite from unittest import makeSuite 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)) # examples in docs are only correct for SQLAlchemy >=1.4 if parse_version(sqlalchemy_version) >= parse_version('1.4.0'): suite.addTest( doctest.DocFileSuite( "README.rst", optionflags=optionflags, tearDown=tearDownReadMe, globs={"TEST_DSN": TEST_DSN, "TEST_TWOPHASE": TEST_TWOPHASE}, ) ) return suite ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1694499879.5063004 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/0000755000076600000240000000000014500002050021557 5ustar00m.howitzstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/PKG-INFO0000644000076600000240000004362114500002047022670 0ustar00m.howitzstaffMetadata-Version: 2.1 Name: zope.sqlalchemy Version: 3.1 Summary: Minimal Zope/SQLAlchemy transaction integration Home-page: https://github.com/zopefoundation/zope.sqlalchemy Author: Laurence Rowe Author-email: laurence@lrowe.co.uk License: ZPL 2.1 Keywords: zope zope3 sqlalchemy Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Pyramid Classifier: Framework :: Zope :: 3 Classifier: Framework :: Zope :: 5 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.7 Provides-Extra: test License-File: LICENSE.txt *************** 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`_ (used by `Turbogears 2`_ and other systems). This package is also used by `pyramid_tm`_ (an add-on of the `Pyramid`_) web framework. You need to understand `SQLAlchemy`_ and the `Zope transaction manager`_ for this package and this README to make any sense. .. _repoze.tm2: https://repozetm2.readthedocs.io/en/latest/ .. _pyramid_tm: https://docs.pylonsproject.org/projects/pyramid_tm/en/latest/ .. _Pyramid: https://pylonsproject.org/ .. _Turbogears 2: https://turbogears.org/ .. _SQLAlchemy: https://sqlalchemy.org/docs/ .. _Zope transaction manager: https://www.zodb.org/en/latest/#transactions Running the tests ================= This package is distributed as a buildout. Using your desired python run: $ python bootstrap.py $ ./bin/buildout 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 Usage in short ============== The integration between Zope transactions and the SQLAlchemy event system is done using the ``register()`` function on the session factory class. .. code-block:: python from zope.sqlalchemy import register from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session engine = sqlalchemy.create_engine("postgresql://scott:tiger@localhost/test") DBSession = scoped_session(sessionmaker(bind=engine)) register(DBSession) Instantiated sessions commits and rollbacks will now be integrated with Zope transactions. .. code-block:: python import transaction from sqlalchemy.sql import text session = DBSession() result = session.execute(text("DELETE FROM objects WHERE id=:id"), {"id": 2}) row = result.fetchone() transaction.commit() Full Example ============ This example is lifted directly from the SQLAlchemy declarative documentation. First the necessary imports. >>> from sqlalchemy import * >>> from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker, relationship >>> from sqlalchemy.sql import text >>> from zope.sqlalchemy import register >>> 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 = relationship("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) >>> 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 register >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE)) 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() >>> register(session) >>> session.query(User).all() [] We can now create a new user and commit the changes using Zope's transaction machinery, 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(text('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] >>> str(bob.name) '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 [
] >>> str(bob.addresses[0].email) '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] >>> str(bob.addresses[0].email) '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().where(users.c.name=='bob'), {'name': 'ben'}) >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session) >>> transaction.commit() >>> session = Session() >>> str(session.query(User).all()[0].name) 'ben' >>> transaction.abort() If this is a problem you may register the events and tell them to place the session in the 'changed' state initially. >>> Session.remove() >>> register(Session, 'changed') >>> session = Session() >>> conn = session.connection() >>> conn.execute(users.update().where(users.c.name=='ben'), {'name': 'bob'}) >>> transaction.commit() >>> session = Session() >>> str(session.query(User).all()[0].name) 'bob' >>> transaction.abort() The `mark_changed` function accepts a kwarg for `keep_session` which defaults to `False` and is unaware of the registered extensions `keep_session` configuration. If you intend for `keep_session` to be True, you can specify it explicitly: >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session, keep_session=True) >>> transaction.commit() You can also use a configured extension to preserve this argument: >>> sessionExtension = register(session, keep_session=True) >>> sessionExtension.mark_changed(session) >>> transaction.commit() Long-lasting session scopes --------------------------- The default behaviour of the transaction integration is to close the session after a commit. You can tell by trying to access an object after committing: >>> bob = session.query(User).all()[0] >>> transaction.commit() >>> bob.name Traceback (most recent call last): sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed... To support cases where a session needs to last longer than a transaction (useful in test suites) you can specify to keep a session when registering the events: >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE)) >>> register(Session, keep_session=True) >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.name = 'bobby' >>> transaction.commit() >>> bob.name 'bobby' The session must then be closed manually: >>> session.close() Development version =================== `GIT version `_ Changes ======= 3.1 (2023-09-12) ---------------- - Fix ``psycopg.errors.OperationalError.sqlstate`` can be ``None``. (`#81 `_) 3.0 (2023-06-01) ---------------- - Add support for SQLAlchemy 2.0 and for new psycopg v3 backend. (`#79 `_) **Breaking Changes** - No longer allow calling ``session.commit()`` within a manual nested database transaction (a savepoint). If you want to use savepoints directly in code that is not aware of ``transaction.savepoint()`` with ``session.begin_nested()`` then use the savepoint returned by the function to commit just the nested transaction i.e. ``savepoint = session.begin_nested(); savepoint.commit()`` or use it as a context manager i.e. ``with session.begin_nested():``. (`for details see #79 `_) 2.0 (2023-02-06) ---------------- - Drop support for Python 2.7, 3.5, 3.6. - Drop support for ``SQLAlchemy < 1.1`` (`#65 `_) - Add support for Python 3.10, 3.11. 1.6 (2021-09-06) ---------------- - Add support for Python 2.7 on SQLAlchemy 1.4. (`#71 `_) 1.5 (2021-07-14) ---------------- - Call ``mark_changed`` also on the ``do_orm_execute`` event if the operation is an insert, update or delete. This is SQLAlchemy >= 1.4 only, as it introduced that event. (`#67 `_) - Fixup get transaction. There was regression introduced in 1.4. (`#66 `_) 1.4 (2021-04-26) ---------------- - Add ``mark_changed`` and ``join_transaction`` methods to ``ZopeTransactionEvents``. (`#46 `_) - Reduce DeprecationWarnings with SQLAlchemy 1.4 and require at least SQLAlchemy >= 0.9. (`#54 `_) - Add support for SQLAlchemy 1.4. (`#58 `_) - Prevent using an SQLAlchemy 1.4 version with broken flush support. (`#57 `_) 1.3 (2020-02-17) ---------------- * ``.datamanager.register()`` now returns the ``ZopeTransactionEvents`` instance which was used to register the events. This allows to change its parameters afterwards. (`#40 `_) * Add preliminary support for Python 3.9a3. 1.2 (2019-10-17) ---------------- **Breaking Changes** * Drop support for Python 3.4. * Add support for Python 3.7 and 3.8. * Fix deprecation warnings for the event system. We already used it in general but still leveraged the old extension mechanism in some places. (`#31 `_) To make things clearer we renamed the ``ZopeTransactionExtension`` class to ``ZopeTransactionEvents``. Existing code using the 'register' version stays compatible. **Upgrade from 1.1** Your old code like this: .. code-block:: python from zope.sqlalchemy import ZopeTransactionExtension DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension(), **options)) becomes: .. code-block:: python from zope.sqlalchemy import register DBSession = scoped_session(sessionmaker(**options)) register(DBSession) 1.1 (2019-01-03) ---------------- * Add support to MySQL using pymysql. 1.0 (2018-01-31) ---------------- * Add support for Python 3.4 up to 3.6. * Support SQLAlchemy 1.2. * Drop support for Python 2.6, 3.2 and 3.3. * Drop support for transaction < 1.6.0. * Fix hazard that could cause SQLAlchemy session not to be committed when transaction is committed in rare situations. (`#23 `_) 0.7.7 (2016-06-23) ------------------ * Support SQLAlchemy 1.1. (`#15 `_) 0.7.6 (2015-03-20) ------------------ * Make version check in register compatible with prereleases. 0.7.5 (2014-06-17) ------------------ * Ensure mapped objects are expired following a ``transaction.commit()`` when no database commit was required. (`#8 `_) 0.7.4 (2014-01-06) ------------------ * Allow ``session.commit()`` on nested transactions to facilitate integration of existing code that might not use ``transaction.savepoint()``. (`#1 `_) * Add a new function zope.sqlalchemy.register(), which replaces the direct use of ZopeTransactionExtension to make use of the newer SQLAlchemy event system to establish instrumentation on the given Session instance/class/factory. Requires at least SQLAlchemy 0.7. (`#4 `_) * Fix `keep_session=True` doesn't work when a transaction is joined by flush and other manngers bug. (`#5 `_) 0.7.3 (2013-09-25) ------------------ * Prevent the ``Session`` object from getting into a "wedged" state if joining a transaction fails. With thread scoped sessions that are reused this can cause persistent errors requiring a server restart. (`#2 `_) 0.7.2 (2013-02-19) ------------------ * Make life-time of sessions configurable. Specify `keep_session=True` when setting up the SA extension. * Python 3.3 compatibility. 0.7.1 (2012-05-19) ------------------ * Use ``@implementer`` as a class decorator instead of ``implements()`` at class scope for compatibility with ``zope.interface`` 4.0. This requires ``zope.interface`` >= 3.6.0. 0.7 (2011-12-06) ---------------- * Python 3.2 compatibility. 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/SOURCES.txt0000644000076600000240000000125614500002047023455 0ustar00m.howitzstaffCHANGES.rst CONTRIBUTING.md COPYRIGHT.txt CREDITS.rst LICENSE.txt MANIFEST.in README.rst buildout.cfg github_actions.cfg github_actions20.cfg mysql.cfg oracle.cfg postgres.cfg postgres20.cfg pysqlite.cfg setup.cfg setup.py tox.ini 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.rst src/zope/sqlalchemy/__init__.py src/zope/sqlalchemy/datamanager.py src/zope/sqlalchemy/tests.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/dependency_links.txt0000644000076600000240000000000114500002047025633 0ustar00m.howitzstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/namespace_packages.txt0000644000076600000240000000000514500002047026113 0ustar00m.howitzstaffzope ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/not-zip-safe0000644000076600000240000000000114500002047024013 0ustar00m.howitzstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/requires.txt0000644000076600000240000000023314500002047024163 0ustar00m.howitzstaffpackaging setuptools SQLAlchemy!=1.4.0,!=1.4.1,!=1.4.2,!=1.4.3,!=1.4.4,!=1.4.5,!=1.4.6,>=1.1 transaction>=1.6.0 zope.interface>=3.6.0 [test] zope.testing ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/src/zope.sqlalchemy.egg-info/top_level.txt0000644000076600000240000000000514500002047024312 0ustar00m.howitzstaffzope ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1694499879.0 zope.sqlalchemy-3.1/tox.ini0000644000076600000240000000604514500002047015506 0ustar00m.howitzstaff# Generated from: # https://github.com/zopefoundation/meta/tree/master/config/zope-product [tox] minversion = 3.18 envlist = lint py37 py38 py39 py310 py311 coverage py{37,38,39}-sqlalchemy11 py{37,38,39,310}-sqlalchemy{12,13} py{37,38,39,310,311}-sqlalchemy{14,20} [testenv] skip_install = true deps = zc.buildout >= 3.0.1 wheel > 0.37 sqlalchemy11: SQLAlchemy==1.1.* sqlalchemy12: SQLAlchemy==1.2.* sqlalchemy13: SQLAlchemy==1.3.* sqlalchemy14: SQLAlchemy==1.4.* sqlalchemy20: SQLAlchemy==2.0.* commands_pre = !sqlalchemy20: sh -c 'if [ '{env:CI:false}' = 'true' ]; then {envbindir}/buildout -nc {toxinidir}/github_actions.cfg buildout:directory={envdir} buildout:develop={toxinidir} ; fi' !sqlalchemy20: sh -c 'if [ '{env:CI:false}' != 'true' ]; then {envbindir}/buildout -nc {toxinidir}/postgres.cfg buildout:directory={envdir} buildout:develop={toxinidir} ; fi' sqlalchemy20: sh -c 'if [ '{env:CI:false}' = 'true' ]; then {envbindir}/buildout -nc {toxinidir}/github_actions20.cfg buildout:directory={envdir} buildout:develop={toxinidir} ; fi' sqlalchemy20: sh -c 'if [ '{env:CI:false}' != 'true' ]; then {envbindir}/buildout -nc {toxinidir}/postgres20.cfg buildout:directory={envdir} buildout:develop={toxinidir} ; fi' commands = {envbindir}/test {posargs:-cv} sh -c 'if [ '{env:TEST_PG:{env:CI:false}}' = 'true' ]; then {envbindir}/testpg {posargs:-cv} ; fi' sh -c 'if [ '{env:TEST_PG:{env:CI:false}}' = 'true' ]; then {envbindir}/testpg2 {posargs:-cv} ; fi' sqlalchemy20: sh -c 'if [ '{env:TEST_PG:{env:CI:false}}' = 'true' ]; then {envbindir}/testpg3 {posargs:-cv} ; fi' sqlalchemy20: sh -c 'if [ '{env:TEST_PG:{env:CI:false}}' = 'true' ]; then {envbindir}/testpg32 {posargs:-cv} ; fi' passenv = CI TEST_PG allowlist_externals = sh [testenv:lint] basepython = python3 commands_pre = mkdir -p {toxinidir}/parts/flake8 allowlist_externals = mkdir commands = isort --check-only --diff {toxinidir}/src {toxinidir}/setup.py flake8 {toxinidir}/src {toxinidir}/setup.py check-manifest check-python-versions deps = check-manifest check-python-versions flake8 isort # Useful flake8 plugins that are Python and Plone specific: flake8-coding flake8-debugger mccabe [testenv:isort-apply] basepython = python3 commands_pre = deps = isort commands = isort {toxinidir}/src {toxinidir}/setup.py [] [testenv:coverage] basepython = python3 skip_install = true allowlist_externals = {[testenv]allowlist_externals} mkdir deps = {[testenv]deps} coverage commands = mkdir -p {toxinidir}/parts/htmlcov coverage run {envdir}/bin/test {posargs:-cv} coverage html coverage report -m --fail-under=65 [coverage:run] branch = True source = zope.sqlalchemy [coverage:report] precision = 2 exclude_lines = pragma: no cover pragma: nocover except ImportError: raise NotImplementedError if __name__ == '__main__': self.fail raise AssertionError [coverage:html] directory = parts/htmlcov