web-dev-qa-db-fra.com

Flask avec create_app, SQLAlchemy et Celery

Je me bats vraiment pour obtenir la bonne configuration pour Flask, SQLAlchemy et Celery. J'ai longuement cherché et essayé différentes approches, rien ne semble vraiment fonctionner. Soit j'ai raté le contexte de l'application, soit je ne peux pas exécuter les travailleurs, soit il y a d'autres problèmes. La structure est très générale afin que je puisse construire une application plus grande.

J'utilise: Flacon 0.10.1, SQLAlchemy 1.0, Celery 3.1.13, ma configuration actuelle est la suivante:

app/__ init__.py

#Empty

app/config.py

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config:

    @staticmethod
    def init_app(app):
        pass

class LocalConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = r"sqlite:///" + os.path.join(basedir, 
                                 "data-dev.sqlite")
    CELERY_BROKER_URL = 'amqp://guest:guest@localhost:5672//'


config = {
    "local": LocalConfig}

app/exstensions.py

from flask.ext.sqlalchemy import SQLAlchemy
from celery import Celery

db = SQLAlchemy()
celery = Celery()

app/factory.py

from extensions import db, celery
from flask import Flask
from flask import g
from config import config

def create_before_request(app):
    def before_request():
        g.db = db
    return before_request


def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])

    db.init_app(app)
    celery.config_from_object(config)

    # Register the blueprints

    # Add the before request handler
    app.before_request(create_before_request(app))
    return app

app/manage.py

from factory import create_app

app = create_app("local")

from flask import render_template
from flask import request

@app.route('/test', methods=['POST'])
def task_simple():
    import tasks
    tasks.do_some_stuff.delay()
    return ""

if __== "__main__":
    app.run()

app/models.py

from extensions import db

class User(db.Model):
    __table= "user"

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(128), unique=True, nullable=False)

app/tasks.py

from extensions import celery
from celery.signals import task_prerun
from flask import g, current_app


@task_prerun.connect
def close_session(*args, **kwargs):
    with current_app.app_context():
       # use g.db
       print g

@celery.task()
def do_some_stuff():
    with current_app.app_context():
       # use g.db
       print g

Dans l'application de dossier:

  • démarrer le serveur Web de développement avec: python.exe manage.py
  • démarrer les ouvriers avec: celery.exe worker -A tasks

Je reçois une erreur d'importation qui n'a aucun sens pour moi. Devrais-je structurer l'application différemment? À la fin, je pense que je veux une configuration assez basique, par exemple. En utilisant Flask avec le modèle d'usine, vous pouvez utiliser l'extension Flask-SQLAlchmey et disposer d'un ouvrier ayant besoin d'accéder à la base de données.

Toute aide est grandement appréciée.

Le traçage est exécuté lors du démarrage du céleri.

Traceback (most recent call last):

  File "[PATH]\scripts\celery-script.py", line 9, in <module>
    load_entry_point('celery==3.1.13', 'console_scripts', 'celery')()

  File "[PATH]\lib\site-packages\celery\__main__.py", line 30, in main
    main()

  File "[PATH]\lib\site-packages\celery\bin\celery.py", line 81, in main
    cmd.execute_from_commandline(argv)

  File "[PATH]\lib\site-packages\celery\bin\celery.py", line 769, in execute_from_commandline
    super(CeleryCommand, self).execute_from_commandline(argv)))

  File "[PATH]\lib\site-packages\celery\bin\base.py", line 305, in execute_from_commandline
    argv = self.setup_app_from_commandline(argv)

  File "[PATH]\lib\site-packages\celery\bin\base.py", line 473, in setup_app_from_commandline
    user_preload = Tuple(self.app.user_options['preload'] or ())
AttributeError: 'Flask' object has no attribute 'user_options'

UPDATEJe change le code en fonction de la suggestion dans le commentaire. L'agent s'ouvre maintenant mais testez-le avec une requête get à http://127.0.0.1:5000/test. Je reçois le suivi suivant:

Traceback (most recent call last):
  File "[PATH]\lib\site-packages\celery\app\trace.py", line 230, in trace_task
    args=args, kwargs=kwargs)

  File "[PATH]\lib\site-packages\celery\utils\dispatch\signal.py", line 166, in send
    response = receiver(signal=self, sender=sender, \**named)

  File "[PATH]\app\stackoverflow\tasks.py", line 7, in close_session
    with current_app.app_context():

  File "[PATH]\lib\site-packages\werkzeug\local.py", line 338, in __getattr__
    return getattr(self._get_current_object(), name)

  File "[PATH]\lib\site-packages\werkzeug\local.py", line 297, in _get_current_object
    return self.__local()

  File "[PATH]\lib\site-packages\flask\globals.py", line 34, in _find_app
    raise RuntimeError('working outside of application context')
RuntimeError: working outside of application context exc, exc_info.traceback)))

UPDATEEn fonction des commentaires de Marteen, j'ai changé le code. La version de travail actuelle se trouve sous: https://Gist.github.com/anonymous/fa47834db2f4f3b8b257 . Toute autre amélioration ou suggestion est la bienvenue.

16
foobar

J'étais parti avec le conseil current_app.

Votre objet céleri doit avoir accès au contexte de l'application. J'ai trouvé des informations en ligne sur la création d'un objet Celery avec une fonction factory. L'exemple ci-dessous est testé sans courtier de messages.

#factory.py
from celery import Celery
from config import config

def create_celery_app(app=None):
    app = app or create_app(config)
    celery = Celery(__name__, broker=app.config['CELERY_BROKER_URL'])
    celery.conf.update(app.config)
    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
    return celery

et dans tasks.py:

#tasks.py
from factory import create_celery_app
from celery.signals import task_prerun
from flask import g

celery = create_celery_app()

@task_prerun.connect
def celery_prerun(*args, **kwargs):
    #print g
    with celery.app.app_context():
    #   # use g.db
       print g

@celery.task()
def do_some_stuff():
    with celery.app.app_context():
        # use g.db
        g.user = "test"
        print g.user

Quelques liens:

Modèle de flacon pour la création d'une instance de céleri avec fonction d'usine

Application utilisant à la fois une fabrique d'applications et du céleri

Source de la fabrique de cette application.py

Source pour application tasks.py

17
Maarten

Voici une solution qui fonctionne avec le modèle de fabrique d’applications de flacon et crée également une tâche de céleri avec un contexte, sans avoir à utiliser explicitement app.app_context() dans les tâches. Dans mon application, il est vraiment difficile d'obtenir cet objet d'application tout en évitant les importations circulaires, mais cela résout le problème. Cela convient également à la dernière version 4.2 du céleri au moment de la rédaction.

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

Donc base est le package d'application principal dans cet exemple. Dans le base/__init__.py, nous créons l'instance de céleri comme suit:

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

Le fichier base/app.py contient la fabrique d’applications de flacon create_app et notez la init_celery(app, celery) qu’il contient:

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

Passer au contenu de base/runcelery.py:

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)

Ensuite, le fichier base/celeryconfig.py (à titre d'exemple):

# -*- 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)
#    },
# }

Définissez maintenant init_celery dans le fichier base/utility/celery_util.py:

# -*- 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

Pour les travailleurs dans 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

@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() as Task has app 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) 

@celery_app.task
def do_some_stuff():
    print(g)

Ensuite, vous devez démarrer le batteur de céleri et le travailleur de céleri dans deux invites de commande différentes à partir de dans le dossier repo_name.

Dans une invite de commande, faites un celery -A base.runcelery:celery beat et l'autre celery -A base.runcelery:celery worker.

Ensuite, exécutez votre tâche qui nécessitait le contexte de la bouteille. Devrait marcher.

2
Bob Jordan