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() ```