Skip to content

Commit

Permalink
Include atomic.py in the docs
Browse files Browse the repository at this point in the history
  • Loading branch information
epandurski committed Apr 2, 2019
1 parent 29fcbea commit eb5fda5
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 24 deletions.
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Version 0.3.7
- Split implementation into two files: `signalbus.py` and `utils.py`
- Renamed `cli.py` to `signalbus_cli.py`
- Added `.circleci` directory
- Added `atomic.py` module, still undocumented
- Added `atomic.py` module
- Added new tests


Expand Down
11 changes: 11 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,14 @@ Mixins

.. autoclass:: SignalBusMixin
:members:


.. autoclass:: AtomicProceduresMixin
:members:


Exceptions
``````````

.. autoclass:: DBSerializationError
:members:
2 changes: 2 additions & 0 deletions flask_signalbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
from flask_signalbus.atomic import ( # noqa: F401
AtomicProceduresMixin,
)

from flask_signalbus.utils import DBSerializationError # noqa: F401
44 changes: 23 additions & 21 deletions flask_signalbus/atomic.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
class _ModelUtilitiesMixin(object):
@classmethod
def get_instance(cls, instance_or_pk):
"""Return an instance in `db.session` when given any instance or a primary key."""
"""Return an instance in ``db.session`` when given any instance or a primary key."""

if isinstance(instance_or_pk, cls):
if instance_or_pk in cls._flask_signalbus_sa.session:
Expand All @@ -29,7 +29,7 @@ def get_instance(cls, instance_or_pk):

@classmethod
def lock_instance(cls, instance_or_pk, read=False):
"""Return a locked instance in `db.session` when given any instance or a primary key."""
"""Return a locked instance in ``db.session`` when given any instance or a primary key."""

mapper = inspect(cls)
pk_attrs = [mapper.get_property_by_column(c).class_attribute for c in mapper.primary_key]
Expand All @@ -48,7 +48,8 @@ def get_pk_values(cls, instance_or_pk):


class AtomicProceduresMixin(object):
"""Adds utility functions to :class:`~flask_sqlalchemy.SQLAlchemy` and the declarative base.
"""A **mixin class** that adds utility functions to
:class:`~flask_sqlalchemy.SQLAlchemy` and the declarative base.
For example::
Expand Down Expand Up @@ -82,14 +83,14 @@ def atomic(self, func):
Example::
@atomic
@db.atomic
def f():
write_to_db('a message')
return 'OK'
assert f() == 'OK'
This code defines the function `f`, which is wrapped in an
This code defines the function ``f``, which is wrapped in an
atomic block. Wrapping a function in an atomic block gives two
guarantees:
Expand Down Expand Up @@ -129,50 +130,51 @@ def wrapper(*args, **kwargs):

return wrapper

def execute_atomic(self, __func, *args, **kwargs):
"""A decorator that executes a function in an atomic block.
def execute_atomic(self, _fn, *args, **kwargs):
"""A decorator that executes a function in an atomic block (see :meth:`atomic`).
Example::
@execute_atomic
@db.execute_atomic
def result():
write_to_db('a message')
return 'OK'
assert result == 'OK'
This code defines *and executes* the function `result` in an
atomic block. At the end, the name `result` holds the value
This code defines *and executes* the function ``result`` in an
atomic block. At the end, the name ``result`` holds the value
returned from the function.
Note: `execute_atomic` can be called with more that one
Note: :meth:`execute_atomic` can be called with more that one
argument. The extra arguments will be passed to the function
given as a first argument. Example::
result = execute_atomic(write_to_db, 'a message')
"""

return self.atomic(__func)(*args, **kwargs)
return self.atomic(_fn)(*args, **kwargs)

@contextmanager
def retry_on_integrity_error(self):
"""Re-raise `IntegrityError` as `DBSerializationError`.
"""Re-raise :class:`~sqlalchemy.exc.IntegrityError` as `DBSerializationError`.
This is mainly useful to handle race conditions in atomic
blocks. For example, even if prior to INSERT we verify that there
is no existing row with the given primary key, we still may get an
`IntegrityError` if another transaction have insterted it in the
meantime. But if we do::
blocks. For example, even if prior to INSERT we verify that
there is no existing row with the given primary key, we still
may get an :class:`~sqlalchemy.exc.IntegrityError` if another
transaction have insterted it in the meantime. But if we do::
with db.retry_on_integrity_error():
db.session.add(instance)
then if the before-mentioned race condition occurs,
`DBSerializationError` will be raised instead of `IntegrityError`,
so that the transaction will be retried (by the atomic block), and
this time our prior-to-INSERT check will correctly detect a
primary key collision.
`DBSerializationError` will be raised instead of
:class:`~sqlalchemy.exc.IntegrityError`, so that the
transaction will be retried (by the atomic block), and this
time our prior-to-INSERT check will correctly detect a primary
key collision.
Note: `retry_on_integrity_error` triggers a session flush.
Expand Down
4 changes: 2 additions & 2 deletions flask_signalbus/signalbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def _raise_error_if_not_signal_model(model):


class SignalBusMixin(object):
"""A **mixin class** that can be used to extend the
`flask_sqlalchemy.SQLAlchemy` class to handle signals.
"""A **mixin class** that can be used to extend
:class:`~flask_sqlalchemy.SQLAlchemy` to handle signals.
For example::
Expand Down

0 comments on commit eb5fda5

Please sign in to comment.