Python Flask with celery out of application context

匿名 (未验证) 提交于 2019-12-03 03:04:01

问题:

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

回答1:

Flask-mail needs the Flask application context to work correctly. Instantiate the app object on the celery side and use app.app_context like this:

with app.app_context():     celery.start() 


回答2:

In your mail.py file, import your "app" and "mail" objects. Then, use request context. Do something like this:

from whateverpackagename import app from whateverpackagename import mail  @celery.task def send_forgot_email(email, ref):     with app.test_request_context():         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) 


回答3:

I don't have any points, so I couldn't upvote @codegeek's above answer, so I decided to write my own since my search for an issue like this was helped by this question/answer: I've just had some success trying to tackle a similar problem in a python/flask/celery scenario. Even though your error was from trying to use mail while my error was around trying to use url_for in a celery task, I suspect the two were related to the same problem and that you would have had errors stemming from the use of url_for if you had tried to use that before mail.

With no context of the app present in a celery task (even after including an import app from my_app_module) I was getting errors, too. You'll need to perform the mail operation in the context of the app:

from module_containing_my_app_and_mail import app, mail    # Flask app, Flask mail from flask.ext.mail import Message    # Message class  @celery.task def send_forgot_email(email, ref):     with app.app_context():    # This is the important bit!         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) 

If anyone is interested, my solution for the problem of using url_for in celery tasks can be found here



回答4:

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. The Celery version I'm using is 4.2rc4 from master.

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//'  # 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 base.extensions import mail 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.



易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!