Python Flask with celery out of application context

celery, flask, python

Solution

Here is a solution which works with the flask application factory pattern and also creates celery task with context, without needing to use `app.app_context()`. It is really tricky to get that app while avoiding circular imports, but this solves it. This is for celery 4.2 which is the latest at the time of writing.

Structure:

repo_name/
    manage.py
    base/
    base/__init__.py
    base/app.py
    base/runcelery.py
    base/celeryconfig.py
    base/utility/celery_util.py
    base/tasks/workers.py

So `base` is the main application package in this example. In the `base/__init__.py` we create the celery instance as below:

from celery import Celery
celery = Celery('base', config_source='base.celeryconfig')

The `base/app.py` file contains the flask app factory `create_app` and note the `init_celery(app, celery)` it contains:

from base import celery
from base.utility.celery_util import init_celery

def create_app(config_obj):
    """An application factory, as explained here:
    http://flask.pocoo.org/docs/patterns/appfactories/.
    :param config_object: The configuration object to use.
    """
    app = Flask('base')
    app.config.from_object(config_obj)
    init_celery(app, celery=celery)
    register_extensions(app)
    register_blueprints(app)
    register_errorhandlers(app)
    register_app_context_processors(app)
    return app

Moving on to `base/runcelery.py` contents:

from flask.helpers import get_debug_flag
from base.settings import DevConfig, ProdConfig
from base import celery
from base.app import create_app
from base.utility.celery_util import init_celery
CONFIG = DevConfig if get_debug_flag() else ProdConfig
app = create_app(CONFIG)
init_celery(app, celery)

Next, the `base/celeryconfig.py` file (as an example):

# -*- coding: utf-8 -*-
"""
Configure Celery. See the configuration guide at ->
http://docs.celeryproject.org/en/master/userguide/configuration.html#configuration
"""

## Broker settings.
broker_url = 'pyamqp://guest:guest@localhost:5672//'
broker_heartbeat=0

# List of modules to import when the Celery worker starts.
imports = ('base.tasks.workers',)

## Using the database to store task state and results.
result_backend = 'rpc'
#result_persistent = False

accept_content = ['json', 'application/text']

result_serializer = 'json'
timezone = "UTC"

# define periodic tasks / cron here
# beat_schedule = {
#    'add-every-10-seconds': {
#        'task': 'workers.add_together',
#        'schedule': 10.0,
#        'args': (16, 16)
#    },
# }

Now define the init_celery in the `base/utility/celery_util.py` file:

# -*- coding: utf-8 -*-

def init_celery(app, celery):
    """Add flask app context to celery.Task"""
    TaskBase = celery.Task
    class ContextTask(TaskBase):
        abstract = True
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return TaskBase.__call__(self, *args, **kwargs)
    celery.Task = ContextTask

For the workers in `base/tasks/workers.py`:

from base import celery as celery_app
from flask_security.utils import config_value, send_mail
from base.bp.users.models.user_models import User
from base.extensions import mail # this is the flask-mail

@celery_app.task
def send_async_email(msg):
    """Background task to send an email with Flask-mail."""
    #with app.app_context():
    mail.send(msg)

@celery_app.task
def send_welcome_email(email, user_id, confirmation_link):
    """Background task to send a welcome email with flask-security's mail.
    You don't need to use with app.app_context() here. Task has context.
    """
    user = User.query.filter_by(id=user_id).first()
    print(f'sending user {user} a welcome email')
    send_mail(config_value('EMAIL_SUBJECT_REGISTER'),
              email,
              'welcome', user=user,
              confirmation_link=confirmation_link) 

Then, you need to start the celery beat and celery worker in two different cmd prompts from inside the `repo_name` folder.

In one cmd prompt do a `celery -A base.runcelery:celery beat` and the other `celery -A base.runcelery:celery worker`.

Then, run through your task that needed the flask context. Should work.

Problem

I am building a website using python Flask. Everything is going good and now I am trying to implement celery. That was going good as well until I tried to send an email using flask-mail from celery. Now I am getting an "working outside of application context" error. full traceback is ``` Traceback (most recent call last): File "/usr/lib/python2.7/site-packages/celery/task/trace.py", line 228, in trace_task R = retval = fun(*args, **kwargs) File "/usr/lib/python2.7/site-packages/celery/task/trace.py", line 415, in __protected_call__ return self.run(*args, **kwargs) File "/home/ryan/www/CG-Website/src/util/mail.py", line 28, in send_forgot_email msg = Message("Recover your Crusade Gaming Account") File "/usr/lib/python2.7/site-packages/flask_mail.py", line 178, in __init__ sender = current_app.config.get("DEFAULT_MAIL_SENDER") File "/usr/lib/python2.7/site-packages/werkzeug/local.py", line 336, in __getattr__ return getattr(self._get_current_object(), name) File "/usr/lib/python2.7/site-packages/werkzeug/local.py", line 295, in _get_current_object return self.__local() File "/usr/lib/python2.7/site-packages/flask/globals.py", line 26, in _find_app raise RuntimeError('working outside of application context') RuntimeError: working outside of application context ``` This is my mail function: ``` @celery.task def send_forgot_email(email, ref): global mail msg = Message("Recover your Crusade Gaming Account") msg.recipients = [email] msg.sender = "Crusade Gaming stuff@cg.com" msg.html = \ """ Hello Person,<br/> You have requested your password be reset. <a href="{0}" >Click here recover your account</a> or copy and paste this link in to your browser: {0} <br /> If you did not request that your password be reset, please ignore this. """.format(url_for('account.forgot', ref=ref, _external=True)) mail.send(msg) ``` This is my celery file: ``` from __future__ import absolute_import from celery import Celery celery = Celery('src.tasks', broker='amqp://', include=['src.util.mail']) if __name__ == "__main__": celery.start() ```

Original source