pax_global_header00006660000000000000000000000064126222453510014514gustar00rootroot0000000000000052 comment=057501dedc0f65bff5f2119756fb580136ea8bf5 django-celery-transactions-0.3.6/000077500000000000000000000000001262224535100167535ustar00rootroot00000000000000django-celery-transactions-0.3.6/.gitignore000066400000000000000000000000361262224535100207420ustar00rootroot00000000000000build dist *.egg-info *.pyc *~django-celery-transactions-0.3.6/.travis.yml000066400000000000000000000012661262224535100210710ustar00rootroot00000000000000language: python python: - "2.7" - "3.3" - "3.4" services: mysql env: - DJANGO="django==1.8.5" DB="mysql" - DJANGO="django==1.7.10" DB="mysql" - DJANGO="django==1.6.11" DB="mysql" install: - pip install $DJANGO - pip install celery==3.1.18 - pip install django-celery==3.1.16 - pip install coveralls - pip install mysqlclient before_script: - mysql -e 'create database testdb;' script: - coverage run --source=djcelery_transactions runtests.py - coverage run --source=djcelery_transactions runtests-djcelery.py after_success: coveralls notifications: email: recipients: - nicolas.grasset@gmail.com on_success: change on_failure: change django-celery-transactions-0.3.6/AUTHORS000066400000000000000000000004071262224535100200240ustar00rootroot00000000000000Bradley Ayers Nicolas Delaby Chris Doble Bryce Drennan Michael Fladischer Nicolas Grasset Nils Lundquist Marcin Nowak Jakub Paczkowski Tom Playford Nic Pottier Peter Sheats Zachary Mott django-celery-transactions-0.3.6/CHANGELOG000066400000000000000000000175101262224535100201710ustar00rootroot00000000000000Changelog ========= 0.3.6 (2015-11-15) ------------------------ - Tests shared_task. [Nicolas Grasset] - Add a replacement shared_task decorator. [Zachary B. Mott] - Update Readme.md: Django and Celery coverage. [Nicolas Grasset] Thanks @jakob-o for pointing this out! 0.3.5 (2015-10-07) ------------------------ - Handle nested transactions differently to account for tests and live code. [Nicolas Grasset] 0.3.4 (2015-10-07) ------------------ - Fix nested condition. [Nicolas Grasset] 0.3.3 (2015-10-06) ------------------ - TestCase support for Django 1.6 and 1.7. [Nicolas Grasset] - Nested transactions. Django 1.8 TestCase is different. [Nicolas Grasset] - Check for nested transactions. [Nicolas Grasset] - Merge pull request #11 from frewsxcv/patch-2. [Nicolas Grasset] Update CHANGELOG 0.3.2 release date - Update CHANGELOG 0.3.2 release date. [Corey Farwell] 0.3.2 (2015-06-03) ------------------ - Merge pull request #10 from fellowshipofone/django-18. [Nicolas Grasset] Django 1.8 support - Drop support for Django 1.5. [Nicolas Grasset] - Travis test with MySQL backend. Fixes #7. [Nicolas Grasset] #7 is not “fixed”, but if the tests pass, I’ll need more details about the issue - Pkg 0.3.2 change log. [Nicolas Grasset] - Pkg: Drop test support for Django 1.5. [Nicolas Grasset] - Trigger tasks in inner savepoint commits. [Nicolas Grasset] This is a rollback of https://github.com/fellowshipofone/django-celery-transactions/commit/c3d 2b704101ed90b0d6eebec0f0b8acc9ff1b617 by @sheats but I think it is cleaner this way, and it is required for new Django 1.8 tests since they are nested in atomic blocks - Django 1.8 extra not compatible with TransactionTestCase. [Nicolas Grasset] - Make Django 1.8 code update compatible with 1.5. [Nicolas Grasset] - Pkg: Update from Django 1.8 source. [Nicolas Grasset] - Pkg: Cleanup test structure for extra 1.8 test. [Nicolas Grasset] - [tests] Check Django 1.8 logic. [Nicolas Grasset] 0.3.1 (2015-04-18) ------------------ Changes ~~~~~~~ - Removed support for Django <= 1.3. [Nicolas Grasset] Other ~~~~~ - 0.3.1 PyPi upload issues. Fixes #9. [Nicolas Grasset] - Pkg: Fix PyPi file. [Nicolas Grasset] 0.3.0 (2015-03-14) ------------------ - [travis] Target specific Django releases. [Nicolas Grasset] - 0.3.0 Release for Python 3. Fixes #8. [Nicolas Grasset] - [travis-ci] Test newest minor releases of Django. [Nicolas Grasset] - [travis-ci] Test Python 3.3 and 3.4. [Nicolas Grasset] - [python 3] Fixed issues on Python 3. [Nicolas Grasset] - [settings] Add MIDDLEWARE_CLASSES for Django 1.7 default. [Nicolas Grasset] - Added coveralls support. [Nicolas Grasset] - Update Travis environment matrix for Django 1.7.1. [Nicolas Grasset] 0.2.0 (2014-11-09) ------------------ - Merge pull request #5 from fellowshipofone/unify-releases. [Nicolas Grasset] Unify releases - Small readability change for README. [Nicolas Grasset] - Update AUTHORS after merge. [Nicolas Grasset] - Merge branch 'brycedrennan-master' into unify-releases. [Nicolas Grasset] - Merge branch 'master' of github.com:brycedrennan/django-celery- transactions into brycedrennan-master. [Nicolas Grasset] Conflicts: README.md djcelery_transactions/__init__.py tests/tests.py - Merge remote-tracking branch 'fellowshipofone/master' [Bryce] Conflicts: djcelery_transactions/__init__.py - Merge remote-tracking branch '10to8/master' [Bryce] Conflicts: djcelery_transactions/__init__.py - Merge remote-tracking branch 'fladi/asyncresult-not-returned' [U -CircleUp-Alpha\Bryce] - Heads up that task.delay() returns None, not AsyncResult. [Michael Fladischer] - Merge remote-tracking branch 'fladi/celery-3.x' [U-CircleUp- Alpha\Bryce] - Use celery.app package instead of deprecated celery.task. [Michael Fladischer] - Merge remote-tracking branch 'ticosax/fix-regression' [U-CircleUp- Alpha\Bryce] - Fix regression for django.db.transaction.managed. [Nicolas Delaby] - Add syntax highlighting. [Corey Farwell] - Fix eager_transaction. [Nicolas Grasset] - Update AUTHORS. [Nicolas Grasset] - New setting CELERY_EAGER_TRANSACTION. [Nicolas Grasset] - Merge branch 'clearcare-master' into unify-releases. [Nicolas Grasset] - Merge branch 'master' of github.com:clearcare/django-celery- transactions into clearcare-master. [Nicolas Grasset] Conflicts: djcelery_transactions/__init__.py - Looks like you only need djcelery to run tests, so install it if you're going to run tests. But I'd rather not have it installed if we don't need it. [Peter Sheats] - Need to honor CELERY_ALWAYS_EAGER here too. [Peter Sheats] - We don't want tasks to get fired off during inner savepoint commits -- only when the parent transaction is committed. [Peter Sheats] - Update setup file for version 0.2.0. [Nicolas Grasset] - Update README file. [Nicolas Grasset] - Django 1.7, use default caches. [Nicolas Grasset] - Use django cache instead of global variable to fix issue from Django 1.6. [Nicolas Grasset] - TestRunner, use DiscoverRunner from Django 1.6. [Nicolas Grasset] - Make both test settings more similar. [Nicolas Grasset] - Only test Django 2.7 for now. [Nicolas Grasset] - Update email, copied from django-redis earlier. [Nicolas Grasset] - Travis-ci. [Nicolas Grasset] - Fix broken tests. Works on Django < 1.6. [Nicolas Grasset] - Merge pull request #3 from fellowshipofone/django-1.6. [Nicolas Grasset] Django 1.6 support - Fixed the tests for 1.6, CELERY_EAGER now working differently. [Nicolas Grasset] - Merge github.com:nlundquist/django-celery-transactions into django-1.6. [Nicolas Grasset] Conflicts: .gitignore djcelery_transactions/__init__.py djcelery_transactions/transaction_signals.py - Adding Batches subclass. [Nils Lundquist] - Celery 3.1 compatibility. [Jakub Paczkowski] - Bring django celery transactions to 1.6.5 version. [Nic Pottier] - Only queue tasks for later if we are in an atomic block. [Nic Pottier] - Modifications for django 1.6, serious overhaul. [Nic Pottier] - Deal with case when there are no args. [Nic Pottier] - Do not queue celeryt asks if always eager is on. [Nic Pottier] - Simpler CELERY_ALWAYS_EAGER support. [Nicolas Grasset] - Change CELERY_ALWAYS_EAGER fix. [Nicolas Grasset] - Ignore .idea files. [Nicolas Grasset] - Merge pull request #1 from 10to8/master. [Nicolas Grasset] Fix for CELERY_ALWAYS_EAGER setting - Update __init__.py. [Tom Playford] - After_transaction support. [Tom Playford] - Fix for CELERY_ALWAYS_EAGER setting. [Tom Playford] - Merge pull request #5 from nicolas-DH/add-test-and-fix-bug. [Bradley Ayers] Add test and fix bug - Ignore build directory. [Nicolas Delaby] - Add minimal test suite to check any regression and prove bug fixing. [Nicolas Delaby] - BUG Fix. [Nicolas Delaby] If transactions is not mark as dirty in case of rollback, celery queue is still consumed - Get rid of extra argument after_transaction passed to apply_async. [Nicolas Delaby] - Add support for prior versions of Django 1.3. [Nicolas Delaby] - Reorganise import for pep8 compliancy. [Nicolas Delaby] - Be function signature agnostic. [Nicolas Delaby] Make sure we can follow any changes in transaction API - Assign and return in same time. [Nicolas Delaby] - Push task to broker in the same order they have appeared. [Nicolas Delaby] - Alphabetise AUTHORS to standardise it for future contributors. [Bradley Ayers] 0.1.3 (2012-10-15) ------------------ - Reduce version requirements, bump to v0.1.3. [Chris Doble] - Merge pull request #2 from marcinn/master. [Chris Doble] Python 2.5 compatibility. - Compatibility with python 2.5. [Marcin Nowak] - Improve README. [Chris Doble] - Fix task queue creation. [Chris Doble] - Use transaction signals instead of request signals. [Chris Doble] - Use signal handlers instead of middleware. [Chris Doble] - Initial commit. [Chris Doble] django-celery-transactions-0.3.6/LICENSE000066400000000000000000000027471262224535100177720ustar00rootroot00000000000000Copyright (c) 2012, Chris Doble All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 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 OWNER OR CONTRIBUTORS 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. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. django-celery-transactions-0.3.6/MANIFEST.in000066400000000000000000000000621262224535100205070ustar00rootroot00000000000000include AUTHORS include README.md include LICENSE django-celery-transactions-0.3.6/README000066400000000000000000000000171262224535100176310ustar00rootroot00000000000000See README.md. django-celery-transactions-0.3.6/README.md000066400000000000000000000074321262224535100202400ustar00rootroot00000000000000# django-celery-transactions [![Travis](https://img.shields.io/travis/fellowshipofone/django-celery-transactions.svg?style=flat)][2] [![Version](https://img.shields.io/pypi/v/django-celery-transactions.svg?style=flat)][3] [![Downloads](https://img.shields.io/pypi/dm/django-celery-transactions.svg?style=flat)][4] [![Coverage Status](https://coveralls.io/repos/fellowshipofone/django-celery-transactions/badge.png?branch=master)][5] django-celery-transactions holds on to Celery tasks until the current database transaction is committed, avoiding potential race conditions as described in Celery's [user guide][1]. Send tasks from signal handlers without fear! ## Django and Celery coverage * Django 1.6, 1.7 and 1.8 are supported and tested. 1.9 will likely require refactoring. * Celery 3.0.x and 3.1.x are supported, but the tests are running with 3.1.18 ## Features * If the transaction is rolled back, the tasks are discarded. Django's transaction middleware does this if an exception is raised. * If transactions aren't being managed, tasks are sent as normal. This means that sending tasks from within Django's shell will work as expected, as will the various transaction decorators `commit_manually`, `commit_on_success`, etc. ## Installation & Use 1. Install django-celery-transactions from PyPI: ```sh $ pip install django-celery-transactions ``` 2. Use the patched decorator to create your tasks: ```python from djcelery_transactions import task from models import Model @task def print_model(model_pk): print Model.objects.get(pk=model_pk) ``` 3. Then use them as normal: ```python from django.db import transaction from models import Model from tasks import print_model # This task will be sent after the transaction is committed. This works # from anywhere in the managed transaction block (e.g. signal handlers). def view(request): model = Model.objects.create(...) print_model.delay(model.pk) # This task will not be sent (it is discarded) as the transaction # middleware rolls the transaction back when it catches the exception. def failing_view(request, model_pk): print_model.delay(model_pk) raise Exception() # This task will be sent immediately as transactions aren't being # managed and it is assumed that the author knows what they're doing. @transaction.commit_manually def manual_view(request, model_pk): print_model.delay(model_pk) transaction.commit() ``` ## Caveats Due to the task being sent after the current transaction has been commited, the `PostTransactionTask` provided in this package does not return an `celery.result.AsyncResult` as the original celery `Task` does. Thus, `print_model.delay(model_pk)` simply returns `None`. In order to track the task later on, the `task_id` can be predefined in the `apply_async` method: from celery.utils import uuid u = uuid() print_model.apply_async((model_pk), {}, task_id=u) ## Compatibility with CELERY_ALWAYS_EAGER There are 2 main reasons for `CELERY_ALWAYS_EAGER`: 1. Running task synchronously and returning `EagerResult`, Celery's [user guide][1] 2. Being able to run code (often tests) without a celery broker. For this second reason, the intended behavior will often conflict with transactions handling, which is why you should then also use `CELERY_EAGER_TRANSACTION` CELERY_ALWAYS_EAGER = True CELERY_EAGER_TRANSACTION = True ## Run test suite ```sh $ python setup.py test ``` [1]: http://celery.readthedocs.org/en/latest/userguide/tasks.html#database-transactions [2]: https://travis-ci.org/fellowshipofone/django-celery-transactions [3]: https://pypi.python.org/pypi/django-celery-transactions [4]: https://pypi.python.org/pypi/django-celery-transactions [5]: https://coveralls.io/r/fellowshipofone/django-celery-transactions?branch=master django-celery-transactions-0.3.6/djcelery_transactions/000077500000000000000000000000001262224535100233445ustar00rootroot00000000000000django-celery-transactions-0.3.6/djcelery_transactions/__init__.py000066400000000000000000000135141262224535100254610ustar00rootroot00000000000000# coding=utf-8 from functools import partial import threading from celery import current_app from celery import current_app, Task from celery import task as base_task, shared_task as base_shared_task from celery.contrib.batches import Batches import django from django.conf import settings from django.db import transaction from django.db.transaction import get_connection, atomic import djcelery_transactions.transaction_signals # Thread-local data (task queue). _thread_data = threading.local() def _get_task_queue(): """Returns the calling thread's task queue.""" return _thread_data.__dict__.setdefault("task_queue", []) class PostTransactionTask(Task): """A task whose execution is delayed until after the current transaction. The task's fate depends on the outcome of the current transaction. If it's committed or no changes are made in the transaction block, the task is sent as normal. If it's rolled back, the task is discarded. If transactions aren't being managed when ``apply_asyc()`` is called (if you're in the Django shell, for example) or the ``after_transaction`` keyword argument is ``False``, the task will A replacement decorator is provided: .. code-block:: python from djcelery_transactions import task @task def example(pk): print "Hooray, the transaction has been committed!" """ abstract = True def original_apply_async(self, *args, **kwargs): """Shortcut method to reach real implementation of celery.Task.apply_sync """ return super(PostTransactionTask, self).apply_async(*args, **kwargs) def apply_async(self, *args, **kwargs): # Delay the task unless the client requested otherwise or transactions # aren't being managed (i.e. the signal handlers won't send the task). celery_eager = _get_celery_settings('CELERY_ALWAYS_EAGER') # New setting to run eager task post transaction # defaults to `not CELERY_ALWAYS_EAGER` eager_transaction = _get_celery_settings('CELERY_EAGER_TRANSACTION', not celery_eager) if django.VERSION < (1, 6): if transaction.is_managed() and eager_transaction: if not transaction.is_dirty(): # Always mark the transaction as dirty # because we push task in queue that must be fired or discarded if 'using' in kwargs: transaction.set_dirty(using=kwargs['using']) else: transaction.set_dirty() _get_task_queue().append((self, args, kwargs)) else: apply_async_orig = super(PostTransactionTask, self).apply_async return apply_async_orig(*args, **kwargs) else: connection = get_connection() if connection.in_atomic_block and eager_transaction: _get_task_queue().append((self, args, kwargs)) else: return self.original_apply_async(*args, **kwargs) class PostTransactionBatches(Batches): """A batch of tasks whose queuing is delayed until after the current transaction. """ abstract = True def original_apply_async(self, *args, **kwargs): """Shortcut method to reach real implementation of celery.Task.apply_sync """ return super(PostTransactionBatches, self).apply_async(*args, **kwargs) def apply_async(self, *args, **kwargs): # Delay the task unless the client requested otherwise or transactions # aren't being managed (i.e. the signal handlers won't send the task). celery_eager = _get_celery_settings('CELERY_ALWAYS_EAGER') # New setting to run eager task post transaction # defaults to `not CELERY_ALWAYS_EAGER` eager_transaction = _get_celery_settings('CELERY_EAGER_TRANSACTION', not celery_eager) connection = get_connection() if connection.in_atomic_block and eager_transaction: _get_task_queue().append((self, args, kwargs)) else: return self.original_apply_async(*args, **kwargs) def _discard_tasks(**kwargs): """Discards all delayed Celery tasks. Called after a transaction is rolled back.""" _get_task_queue()[:] = [] def _send_tasks(**kwargs): """Sends all delayed Celery tasks. Called after a transaction is committed or we leave a transaction management block in which no changes were made (effectively a commit). """ # Detect test mode through CELERY_ALWAYS_EAGER settings # We assume all celery transactions tests on 1.8+ are running with TestCase, otherwise we'd get atomic exceptions celery_eager = _get_celery_settings('CELERY_ALWAYS_EAGER') # If we detect higher up nested atomic block, continue connection = get_connection() if django.VERSION >= (1, 8): min_number_transactions = 1 if celery_eager else 0 if (not celery_eager and connection.in_atomic_block) or len(connection.savepoint_ids) > min_number_transactions: return elif connection.in_atomic_block: return queue = _get_task_queue() while queue: tsk, args, kwargs = queue.pop(0) tsk.original_apply_async(*args, **kwargs) # Replacement decorators. task = partial(base_task, base=PostTransactionTask) shared_task = partial(base_shared_task, base=PostTransactionTask) # Hook the signal handlers up. transaction.signals.post_commit.connect(_send_tasks) transaction.signals.post_rollback.connect(_discard_tasks) def _get_celery_settings(setting, default=False): """ Returns CELERY setting :param setting: :param default: :return: """ return any(getattr(obj, setting, default) for obj in (current_app.conf, settings)) django-celery-transactions-0.3.6/djcelery_transactions/transaction_signals.py000066400000000000000000000137701262224535100277730ustar00rootroot00000000000000# coding=utf-8 """Adds signals to ``django.db.transaction``. Signals are monkey patched into ``django.db.transaction``: * ``post_commit``: sent after a transaction is committed. If no changes were made in the transaction block, nothing is committed and this won't be sent. * ``post_rollback``: sent after a transaction is rolled back. * ``post_transaction_management``: sent after leaving transaction management. This signal isn't posted if a ``TransactionManagementError`` is raised. .. code-block:: python import djcelery_transactions.transaction_signals def _post_commit(**kwargs): print "The transaction has been committed!" django.db.transaction.signals.post_commit.connect(_post_commit) This code was inspired by Grégoire Cachet's implementation of similar functionality, which can be found on GitHub: https://gist.github.com/247844 .. warning:: This module must be imported before you attempt to use the signals. """ from functools import partial import django from django.db import ( DEFAULT_DB_ALIAS, DatabaseError, ProgrammingError, Error, connections, ) from django.dispatch import Signal from django.db.transaction import get_connection from django.db import transaction class TransactionSignals(object): """A container for the transaction signals.""" def __init__(self): self.post_commit = Signal() self.post_rollback = Signal() # Access as django.db.transaction.signals. transaction.signals = TransactionSignals() __original__exit__ = transaction.Atomic.__exit__ def __patched__exit__(self, exc_type, exc_value, traceback): connection = get_connection(self.using) if connection.savepoint_ids: sid = connection.savepoint_ids.pop() else: # Prematurely unset this flag to allow using commit or rollback. connection.in_atomic_block = False try: if connection.closed_in_transaction: # The database will perform a rollback by itself. # Wait until we exit the outermost block. pass elif exc_type is None and not connection.needs_rollback: if connection.in_atomic_block: # Release savepoint if there is one if sid is not None: try: connection.savepoint_commit(sid) transaction.signals.post_commit.send(None) except DatabaseError: try: connection.savepoint_rollback(sid) transaction.signals.post_rollback.send(None) # The savepoint won't be reused. Release it to # minimize overhead for the database server. connection.savepoint_commit(sid) transaction.signals.post_commit.send(None) except Error: # If rolling back to a savepoint fails, mark for # rollback at a higher level and avoid shadowing # the original exception. connection.needs_rollback = True raise else: # Commit transaction try: connection.commit() transaction.signals.post_commit.send(None) except DatabaseError: try: connection.rollback() transaction.signals.post_rollback.send(None) except Error: # An error during rollback means that something # went wrong with the connection. Drop it. connection.close() raise else: # This flag will be set to True again if there isn't a savepoint # allowing to perform the rollback at this level. connection.needs_rollback = False if connection.in_atomic_block: # Roll back to savepoint if there is one, mark for rollback # otherwise. if sid is None: connection.needs_rollback = True else: try: connection.savepoint_rollback(sid) transaction.signals.post_rollback.send(None) # The savepoint won't be reused. Release it to # minimize overhead for the database server. connection.savepoint_commit(sid) transaction.signals.post_commit.send(None) except Error: # If rolling back to a savepoint fails, mark for # rollback at a higher level and avoid shadowing # the original exception. connection.needs_rollback = True else: # Roll back transaction try: connection.rollback() transaction.signals.post_rollback.send(None) except Error: # An error during rollback means that something # went wrong with the connection. Drop it. connection.close() finally: # Outermost block exit when autocommit was enabled. if not connection.in_atomic_block: if connection.closed_in_transaction: connection.connection = None elif connection.features.autocommits_when_autocommit_is_off: connection.autocommit = True else: connection.set_autocommit(True) # Outermost block exit when autocommit was disabled. elif not connection.savepoint_ids and not connection.commit_on_exit: if connection.closed_in_transaction: connection.connection = None else: connection.in_atomic_block = False # Monkey patch that shit transaction.Atomic.__exit__ = __patched__exit__django-celery-transactions-0.3.6/runtests-djcelery.py000066400000000000000000000031231262224535100230120ustar00rootroot00000000000000# -*- coding: utf-8 -*- import django from django.conf import settings from django.core.management import call_command import os, sys os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") sys.path.insert(0, 'tests') def runtests(): if not settings.configured: # Choose database for settings DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' } } test_db = os.environ.get('DB', 'sqlite') if test_db == 'mysql': DATABASES['default'].update({ 'ENGINE': 'django.db.backends.mysql', 'NAME': 'testdb', 'USER': 'root', }) # Configure test environment settings.configure( DATABASES=DATABASES, INSTALLED_APPS=( 'djcelery_transactions', 'tests.test', ), ROOT_URLCONF='tests.urls', LANGUAGES=( ('en', 'English'), ), MIDDLEWARE_CLASSES=(), CELERY_EAGER_TRANSACTION = True, TEST_RUNNER = 'djcelery.contrib.test_runner.CeleryTestSuiteRunner' ) from celery import current_app current_app.conf.CELERY_ALWAYS_EAGER = True current_app.conf.CELERY_EAGER_PROPAGATES_EXCEPTIONS = True if django.VERSION >= (1, 7): django.setup() failures = call_command( 'test', 'tests', interactive=False, failfast=False, verbosity=1) sys.exit(bool(failures)) if __name__ == '__main__': runtests() django-celery-transactions-0.3.6/runtests.py000066400000000000000000000033021262224535100212120ustar00rootroot00000000000000# -*- coding: utf-8 -*- import django from django.conf import settings from django.core.management import call_command import os, sys os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") sys.path.insert(0, 'tests') def runtests(): if not settings.configured: # Choose database for settings DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' } } test_db = os.environ.get('DB', 'sqlite') if test_db == 'mysql': DATABASES['default'].update({ 'ENGINE': 'django.db.backends.mysql', 'NAME': 'testdb', 'USER': 'root', }) TEST_RUNNER = 'django.test.runner.DiscoverRunner' # Configure test environment settings.configure( DATABASES=DATABASES, INSTALLED_APPS=( 'djcelery_transactions', 'tests.test', ), ROOT_URLCONF='tests.urls', LANGUAGES=( ('en', 'English'), ), MIDDLEWARE_CLASSES=(), CELERY_ALWAYS_EAGER = True, CELERY_EAGER_PROPAGATES_EXCEPTIONS = True, CELERY_EAGER_TRANSACTION = True, TEST_RUNNER=TEST_RUNNER ) from celery import current_app current_app.conf.CELERY_ALWAYS_EAGER = True current_app.conf.CELERY_EAGER_PROPAGATES_EXCEPTIONS = True if django.VERSION >= (1, 7): django.setup() failures = call_command( 'test', 'tests', interactive=False, failfast=False, verbosity=1) sys.exit(bool(failures)) if __name__ == '__main__': runtests() django-celery-transactions-0.3.6/setup.py000066400000000000000000000015601262224535100204670ustar00rootroot00000000000000# coding=utf-8 import os import sys from setuptools import setup, Command, find_packages setup( name="django-celery-transactions", version="0.3.6", description="Django transaction support for Celery tasks.", long_description="See https://github.com/fellowshipofone/django-celery-transactions", author="Nicolas Grasset", author_email="nicolas.grasset@gmail.com", url="https://github.com/fellowshipofone/django-celery-transactions", license="Simplified BSD", packages=["djcelery_transactions"], install_requires=[ "celery>=2.2.7", "Django>=1.2.4", ], classifiers=[ "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Database", ], ) django-celery-transactions-0.3.6/tests.settings000066400000000000000000000000001262224535100216650ustar00rootroot00000000000000django-celery-transactions-0.3.6/tests/000077500000000000000000000000001262224535100201155ustar00rootroot00000000000000django-celery-transactions-0.3.6/tests/__init__.py000066400000000000000000000000001262224535100222140ustar00rootroot00000000000000django-celery-transactions-0.3.6/tests/test/000077500000000000000000000000001262224535100210745ustar00rootroot00000000000000django-celery-transactions-0.3.6/tests/test/__init__.py000066400000000000000000000000001262224535100231730ustar00rootroot00000000000000django-celery-transactions-0.3.6/tests/test/models.py000066400000000000000000000003261262224535100227320ustar00rootroot00000000000000from django.db import models class Plants(models.Model): name = models.CharField(max_length=120) class Trees(models.Model): name = models.CharField(max_length=120) plant = models.ForeignKey(Plants) django-celery-transactions-0.3.6/tests/test/tests.py000066400000000000000000000103271262224535100226130ustar00rootroot00000000000000import django, os from djcelery_transactions import task, shared_task if django.VERSION < (1, 8): from django.test import TransactionTestCase as TestCaseForTests # commit() was disabled in TestCase prior to 1.8 else: from django.test import TestCase as TestCaseForTests # Django 1.8 now works just fine with TestCase from .models import Trees, Plants if django.VERSION < (1, 7): from django.core.cache import cache else: from django.core.cache import caches cache = caches['default'] if django.VERSION >= (1, 6): from django.db.transaction import atomic else: from django.db import transaction atomic = transaction.commit_on_success @task def my_task(): cache.set('my_global', 42) @task def my_model_task(): Plants.objects.create(name='Oak') @shared_task def my_shared_task(): cache.set('my_global', 42) try: from celery.registry import tasks tasks.register(my_task) except: from celery import task as base_task, current_app, Task current_app.registry.register(my_task) class SpecificException(Exception): pass class DjangoCeleryTestCase(TestCaseForTests): """Test djcelery transaction safe task manager """ def setUp(self): super(DjangoCeleryTestCase, self).setUp() self.task = my_task def tearDown(self): cache.delete('my_global') def test_committed_transaction_fire_task(self): """Check that task is consumed when no exception happens """ @atomic def do_something(): self.task.delay() do_something() self.assertEqual(cache.get('my_global'), 42) def test_committed_nested_transaction_fire_task(self): """Check that task is consumed when no exception happens """ @atomic def do_something(): @atomic def nested_do_something(): self.task.delay() nested_do_something() do_something() self.assertEqual(cache.get('my_global'), 42) def test_rollbacked_transaction_discard_task(self): """Check that task is not consumed when exception happens """ @atomic def do_something(): self.task.delay() raise SpecificException try: do_something() except SpecificException: self.assertIsNone(cache.get('my_global')) else: self.fail('Exception not raised') def test_via_api(self): r = self.client.get('/test_api/') self.assertEqual(r.status_code, 200) self.assertEqual(cache.get('my_global'), 42) def test_django_db_transaction_managed(self): """ Check that django.db.transaction.managed is not affected by monkey-patching """ if django.VERSION >= (1, 6): self.skipTest('Django 1.6 does not need this test') from django.db import transaction self.assertFalse(transaction.is_managed()) transaction.enter_transaction_management() try: transaction.managed() self.assertTrue(transaction.is_managed()) finally: transaction.leave_transaction_management() def test_multiple_models(self): """Check that task is consumed when no exception happens """ self.assertEqual(Plants.objects.count(), 0) @atomic def do_something(): my_model_task.delay() do_something() self.assertEqual(Plants.objects.count(), 1) Trees.objects.create(name='Grey Oak', plant=Plants.objects.get(name='Oak')) def test_nested_atomic_blocks(self): """Check that task is consumed only after last transaction """ @atomic def do_something(): self.task.delay() self.assertIsNone(cache.get('my_global')) @atomic def do_something_outside(): do_something() self.assertIsNone(cache.get('my_global')) do_something_outside() self.assertEqual(cache.get('my_global'), 42) class SharedTaskTestCase(DjangoCeleryTestCase): """Test djcelery transaction safe task manager """ def setUp(self): super(SharedTaskTestCase, self).setUp() self.task = my_shared_task django-celery-transactions-0.3.6/tests/test/views.py000066400000000000000000000004771262224535100226130ustar00rootroot00000000000000from django.http import HttpResponse from .tests import my_task import django if django.VERSION > (1,6): from django.db.transaction import atomic else: from django.db import transaction atomic = transaction.commit_on_success @atomic def test_api(request): my_task.delay() return HttpResponse('ok')django-celery-transactions-0.3.6/tests/urls.py000066400000000000000000000002031262224535100214470ustar00rootroot00000000000000from django.conf.urls import patterns from tests.test import views urlpatterns = patterns('', (r'^test_api', views.test_api) )