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:
parent
aab4dec27e
commit
d72d6f8c7c
6
docs/api/db.rst
Normal file
6
docs/api/db.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
``tailbone.db``
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. automodule:: tailbone.db
|
||||||
|
:members:
|
|
@ -44,6 +44,7 @@ Package API:
|
||||||
|
|
||||||
api/api/batch/core
|
api/api/batch/core
|
||||||
api/api/batch/ordering
|
api/api/batch/ordering
|
||||||
|
api/db
|
||||||
api/diffs
|
api/diffs
|
||||||
api/forms
|
api/forms
|
||||||
api/forms.widgets
|
api/forms.widgets
|
||||||
|
|
|
@ -61,7 +61,7 @@ install_requires =
|
||||||
transaction
|
transaction
|
||||||
waitress
|
waitress
|
||||||
WebHelpers2
|
WebHelpers2
|
||||||
zope.sqlalchemy
|
zope.sqlalchemy>=1.5
|
||||||
|
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
|
|
159
tailbone/db.py
159
tailbone/db.py
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -21,14 +21,13 @@
|
||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
"""
|
"""
|
||||||
Database Stuff
|
Database sessions etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from zope.sqlalchemy import datamanager
|
from zope.sqlalchemy import datamanager
|
||||||
import sqlalchemy_continuum as continuum
|
import sqlalchemy_continuum as continuum
|
||||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
from pkg_resources import get_distribution, parse_version
|
|
||||||
|
|
||||||
from rattail.db import SessionBase
|
from rattail.db import SessionBase
|
||||||
from rattail.db.continuum import versioning_manager
|
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)
|
# empty dict for now, this must populated on app startup (if needed)
|
||||||
ExtraTrainwreckSessions = {}
|
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):
|
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.
|
One phase variant.
|
||||||
|
|
||||||
.. note::
|
.. 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):
|
def tpc_vote(self, trans):
|
||||||
|
""" """
|
||||||
# for a one phase data manager commit last in tpc_vote
|
# 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
|
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')
|
self._finish('committed')
|
||||||
|
|
||||||
|
|
||||||
def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transaction_manager=datamanager.zope_transaction.manager, keep_session=False):
|
def join_transaction(
|
||||||
"""Join a session to a transaction using the appropriate datamanager.
|
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
|
It is safe to call this multiple times, if the session is already
|
||||||
then it just returns.
|
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
|
If using the default initial status of STATUS_ACTIVE, you must
|
||||||
mark_changed(session) is called when data is written to the database.
|
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
|
The ZopeTransactionExtesion SessionExtension can be used to ensure
|
||||||
called automatically after session write operations.
|
that this is called automatically after session write operations.
|
||||||
|
|
||||||
.. note::
|
.. 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.
|
# the upstream internals of this function has changed a little over time.
|
||||||
# unfortunately for us, that means we must include each variant here.
|
# 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 datamanager._SESSION_STATE.get(session, None) is None:
|
||||||
if session.twophase:
|
if session.twophase:
|
||||||
DataManager = datamanager.TwoPhaseSessionDataManager
|
DataManager = datamanager.TwoPhaseSessionDataManager
|
||||||
|
@ -100,98 +117,79 @@ def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transacti
|
||||||
DataManager = TailboneSessionDataManager
|
DataManager = TailboneSessionDataManager
|
||||||
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
|
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):
|
class ZopeTransactionEvents(datamanager.ZopeTransactionEvents):
|
||||||
"""
|
"""
|
||||||
Record that a flush has occurred on a session's
|
Record that a flush has occurred on a session's connection. This
|
||||||
connection. This allows the DataManager to rollback rather
|
allows the DataManager to rollback rather than commit on read only
|
||||||
than commit on read only transactions.
|
transactions.
|
||||||
|
|
||||||
.. note::
|
.. 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):
|
def after_begin(self, session, transaction, connection):
|
||||||
|
""" """
|
||||||
join_transaction(session, self.initial_state,
|
join_transaction(session, self.initial_state,
|
||||||
self.transaction_manager, self.keep_session)
|
self.transaction_manager, self.keep_session)
|
||||||
|
|
||||||
def after_attach(self, session, instance):
|
def after_attach(self, session, instance):
|
||||||
|
""" """
|
||||||
join_transaction(session, self.initial_state,
|
join_transaction(session, self.initial_state,
|
||||||
self.transaction_manager, self.keep_session)
|
self.transaction_manager, self.keep_session)
|
||||||
|
|
||||||
def join_transaction(self, session):
|
def join_transaction(self, session):
|
||||||
|
""" """
|
||||||
join_transaction(session, self.initial_state,
|
join_transaction(session, self.initial_state,
|
||||||
self.transaction_manager, self.keep_session)
|
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
|
Register ZopeTransaction listener events on the given Session or
|
||||||
connection. This allows the DataManager to rollback rather
|
Session factory/class.
|
||||||
than commit on read only transactions.
|
|
||||||
|
|
||||||
.. note::
|
This function requires at least SQLAlchemy 0.7 and makes use of
|
||||||
This class is copied from upstream, and tweaked so that our
|
the newer sqlalchemy.event package in order to register event
|
||||||
custom :func:`join_transaction()` will be used.
|
listeners on the given 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 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.
|
|
||||||
|
|
||||||
The session argument here may be a Session class or subclass, a
|
The session argument here may be a Session class or subclass, a
|
||||||
sessionmaker or scoped_session instance, or a specific Session instance.
|
sessionmaker or scoped_session instance, or a specific Session
|
||||||
Event listening will be specific to the scope of the type of argument
|
instance. Event listening will be specific to the scope of the
|
||||||
passed, including specificity to its subclass as well as its identity.
|
type of argument passed, including specificity to its subclass as
|
||||||
|
well as its identity.
|
||||||
|
|
||||||
.. note::
|
.. 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
|
from sqlalchemy import event
|
||||||
|
|
||||||
if zope_sqlalchemy_version_parsed >= parse_version('1.2'): # 1.2+
|
|
||||||
|
|
||||||
ext = ZopeTransactionEvents(
|
ext = ZopeTransactionEvents(
|
||||||
initial_state=initial_state,
|
initial_state=initial_state,
|
||||||
transaction_manager=transaction_manager,
|
transaction_manager=transaction_manager,
|
||||||
keep_session=keep_session,
|
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_begin", ext.after_begin)
|
||||||
event.listen(session, "after_attach", ext.after_attach)
|
event.listen(session, "after_attach", ext.after_attach)
|
||||||
event.listen(session, "after_flush", ext.after_flush)
|
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, "after_bulk_delete", ext.after_bulk_delete)
|
||||||
event.listen(session, "before_commit", ext.before_commit)
|
event.listen(session, "before_commit", ext.before_commit)
|
||||||
|
|
||||||
if zope_sqlalchemy_version_parsed >= parse_version('1.5'): # 1.5+
|
|
||||||
if datamanager.SA_GE_14:
|
if datamanager.SA_GE_14:
|
||||||
event.listen(session, "do_orm_execute", ext.do_orm_execute)
|
event.listen(session, "do_orm_execute", ext.do_orm_execute)
|
||||||
|
|
||||||
|
|
7
tests/test_db.py
Normal file
7
tests/test_db.py
Normal 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
|
||||||
|
|
Loading…
Reference in a new issue