fix: require zope.sqlalchemy >= 1.5

so we can do away with some old cruft, since latest zope.sqlalchemy is
3.1 from 2023-09-12
This commit is contained in:
Lance Edgar 2024-07-02 11:14:03 -05:00
parent aab4dec27e
commit d72d6f8c7c
5 changed files with 129 additions and 118 deletions

6
docs/api/db.rst Normal file
View file

@ -0,0 +1,6 @@
``tailbone.db``
===============
.. automodule:: tailbone.db
:members:

View file

@ -44,6 +44,7 @@ Package API:
api/api/batch/core
api/api/batch/ordering
api/db
api/diffs
api/forms
api/forms.widgets

View file

@ -61,7 +61,7 @@ install_requires =
transaction
waitress
WebHelpers2
zope.sqlalchemy
zope.sqlalchemy>=1.5
[options.packages.find]

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -21,14 +21,13 @@
#
################################################################################
"""
Database Stuff
Database sessions etc.
"""
import sqlalchemy as sa
from zope.sqlalchemy import datamanager
import sqlalchemy_continuum as continuum
from sqlalchemy.orm import sessionmaker, scoped_session
from pkg_resources import get_distribution, parse_version
from rattail.db import SessionBase
from rattail.db.continuum import versioning_manager
@ -43,23 +42,28 @@ TrainwreckSession = scoped_session(sessionmaker())
# empty dict for now, this must populated on app startup (if needed)
ExtraTrainwreckSessions = {}
# some of the logic below may need to vary somewhat, based on which version of
# zope.sqlalchemy we have installed
zope_sqlalchemy_version = get_distribution('zope.sqlalchemy').version
zope_sqlalchemy_version_parsed = parse_version(zope_sqlalchemy_version)
class TailboneSessionDataManager(datamanager.SessionDataManager):
"""Integrate a top level sqlalchemy session transaction into a zope transaction
"""
Integrate a top level sqlalchemy session transaction into a zope
transaction
One phase variant.
.. note::
This class appears to be necessary in order for the Continuum
integration to work alongside the Zope transaction integration.
This class appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It subclasses
``zope.sqlalchemy.datamanager.SessionDataManager`` but injects
some SQLAlchemy-Continuum logic within :meth:`tpc_vote()`, and
is sort of monkey-patched into the mix.
"""
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
@ -71,28 +75,41 @@ class TailboneSessionDataManager(datamanager.SessionDataManager):
self._finish('committed')
def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transaction_manager=datamanager.zope_transaction.manager, keep_session=False):
"""Join a session to a transaction using the appropriate datamanager.
def join_transaction(
session,
initial_state=datamanager.STATUS_ACTIVE,
transaction_manager=datamanager.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.
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
`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.
If using the default initial status of STATUS_ACTIVE, you must
ensure that mark_changed(session) is called when data is written
to the database.
The ZopeTransactionExtesion SessionExtension can be used to ensure that this is
called automatically after session write operations.
The ZopeTransactionExtesion SessionExtension can be used to ensure
that this is called automatically after session write operations.
.. note::
This function is copied from upstream, and tweaked so that our custom
:class:`TailboneSessionDataManager` will be used.
This function appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It overrides ``zope.sqlalchemy.datamanager.join_transaction()``
to ensure the custom :class:`TailboneSessionDataManager` is
used, and is sort of monkey-patched into the mix.
"""
# the upstream internals of this function has changed a little over time.
# unfortunately for us, that means we must include each variant here.
if zope_sqlalchemy_version_parsed >= parse_version('1.1'): # 1.1+
if datamanager._SESSION_STATE.get(session, None) is None:
if session.twophase:
DataManager = datamanager.TwoPhaseSessionDataManager
@ -100,98 +117,79 @@ def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transacti
DataManager = TailboneSessionDataManager
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
else: # pre-1.1
if datamanager._SESSION_STATE.get(id(session), None) is None:
if session.twophase:
DataManager = datamanager.TwoPhaseSessionDataManager
else:
DataManager = TailboneSessionDataManager
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
if zope_sqlalchemy_version_parsed >= parse_version('1.2'): # 1.2+
class ZopeTransactionEvents(datamanager.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.
Record that a flush has occurred on a session's connection. This
allows the DataManager to rollback rather than commit on read only
transactions.
.. note::
This class is copied from upstream, and tweaked so that our
custom :func:`join_transaction()` will be used.
This class appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It subclasses
``zope.sqlalchemy.datamanager.ZopeTransactionEvents`` but
overrides various methods to ensure the custom
:func:`join_transaction()` is called, and is sort of
monkey-patched into the mix.
"""
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 join_transaction(self, session):
""" """
join_transaction(session, self.initial_state,
self.transaction_manager, self.keep_session)
else: # pre-1.2
class ZopeTransactionExtension(datamanager.ZopeTransactionExtension):
def register(
session,
initial_state=datamanager.STATUS_ACTIVE,
transaction_manager=datamanager.zope_transaction.manager,
keep_session=False,
):
"""
Record that a flush has occurred on a session's
connection. This allows the DataManager to rollback rather
than commit on read only transactions.
Register ZopeTransaction listener events on the given Session or
Session factory/class.
.. note::
This class is copied from upstream, and tweaked so that our
custom :func:`join_transaction()` will be used.
"""
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 register(session, initial_state=datamanager.STATUS_ACTIVE,
transaction_manager=datamanager.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.
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.
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.
.. note::
This function is copied from upstream, and tweaked so that our custom
:class:`ZopeTransactionExtension` will be used.
This function appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It overrides ``zope.sqlalchemy.datamanager.regsiter()`` to
ensure the custom :class:`ZopeTransactionEvents` is used.
"""
from sqlalchemy import event
if zope_sqlalchemy_version_parsed >= parse_version('1.2'): # 1.2+
ext = ZopeTransactionEvents(
initial_state=initial_state,
transaction_manager=transaction_manager,
keep_session=keep_session,
)
else: # pre-1.2
ext = ZopeTransactionExtension(
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)
@ -199,7 +197,6 @@ def register(session, initial_state=datamanager.STATUS_ACTIVE,
event.listen(session, "after_bulk_delete", ext.after_bulk_delete)
event.listen(session, "before_commit", ext.before_commit)
if zope_sqlalchemy_version_parsed >= parse_version('1.5'): # 1.5+
if datamanager.SA_GE_14:
event.listen(session, "do_orm_execute", ext.do_orm_execute)

7
tests/test_db.py Normal file
View file

@ -0,0 +1,7 @@
# -*- coding: utf-8; -*-
# TODO: add real tests at some point but this at least gives us basic
# coverage when running this "test" module alone
from tailbone import db