How to preserve Flask app context across Celery and SQLAlchemy - python

I'm building trying to learn Flask with a proof of concept Flask app, that takes a JSON payload, and uses SQLAlchemy to write it to a DB. I'm using celery to manage the write tasks.
The app is structured
|-app.py
|-project
|-__init__.py
|-celery_utils.py
|-config.py
|-users
|-__init_.py
|-models.py
|-tasks.py
app.py builds the flask app and celery instance.
app.py
from project import create_app, ext_celery
app = create_app()
celery = ext_celery.celery
#app.route("/")
def alive():
return "alive"
/project/__init__.py is the application factory for the flask app. It instantiates the extensions, links everything together, and registers the blueprints.
/project/init.py
import os
from flask import Flask
from flask_celeryext import FlaskCeleryExt
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from project.celery_utils import make_celery
from project.config import config
# instantiate extensions
db = SQLAlchemy()
migrate = Migrate()
ext_celery = FlaskCeleryExt(create_celery_app=make_celery)
def create_app(config_name=None):
if config_name is None:
config_name = os.environ.get("FLASK_CONFIG", "development")
# instantiate the app
app = Flask(__name__)
# set config
app.config.from_object(config[config_name])
# set up extensions
db.init_app(app)
migrate.init_app(app, db)
ext_celery.init_app(app)
# register blueprints
from project.users import users_blueprint
app.register_blueprint(users_blueprint)
# shell context for flask cli
#app.shell_context_processor
def ctx():
return {"app": app, "db": db}
return app
/project/celery_utils.py manages the creation of the celery instances
/project/celery_utils.py
from celery import current_app as current_celery_app
def make_celery(app):
celery = current_celery_app
celery.config_from_object(app.config, namespace="CELERY")
return celery
In the users dir, I'm trying to manage the creation of a basic user with celery task management.
'/project/users/init.py` is where I create the blueprints and routes.
/project/users/init.py
from flask import Blueprint, request, jsonify
from .tasks import divide, post_to_db
users_blueprint = Blueprint("users", __name__, url_prefix="/users", template_folder="templates")
from . import models, tasks
#users_blueprint.route('/users', methods=['POST'])
def users():
request_data = request.get_json()
task = post_to_db.delay(request_data)
response = {"id": task.task_id,
"status": task.status,
}
return jsonify(response)
#users_blueprint.route('/responses', methods=['GET'])
def responses():
request_data = request.get_json()
result = AsyncResult(id=request_data['id'])
response = result.get()
return jsonify(response)
/project/users/models.py is a simple User model - however, it does manage to successfully remain in the context of the flask app if created from the flask app cli.
/project/users/models.py
from project import db
class User(db.Model):
"""model for the user object"""
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(128), unique=True, nullable=False)
email = db.Column(db.String(128), unique=True, nullable=False)
def __init__(self, username, email, *args, **kwargs):
self.username = username
self.email = email
Finally, /project/users/tasks.py is where I handle the celery tasks for this dir.
/project/users/tasks.py
from celery import shared_task
from .models import User
from project import db
#shared_task()
def post_to_db(payload):
print("made it here")
user = User(**payload)
db.session.add(user)
db.session.commit()
db.session.close()
return True
The modules work, but as soon as I wire it all up and hit the endpoint with a JSON payload, I get the error message:
RuntimeError: No application found. Either work inside a view function or push an application context. ...
I have tried to preserve the app context in tasks.py by:
...
from project import db, ext_celery
#ext_celery.shared_task()
def post_to_db(payload):
...
...
from project import db, ext_celery
#ext_celery.task()
def post_to_db(payload):
...
These error with: TypeError: exceptions must derive from BaseException
I've tried pushing the app context
...
from project import db
from app import app
#shared_task()
def post_to_db(payload):
with app.app_context():
...
This also errors with: TypeError: exceptions must derive from BaseException
I've tried importing celery from the app itself
...
from project import db
from app import celery
#celery.task()
def post_to_db(payload):
...
This also errors with: TypeError: exceptions must derive from BaseException
Any suggestions gratefully received. There's a final piece of the puzzle I'm missing, and it's very frustrating.

With thanks to snakecharmerb
I had to add ContextTask to the make_celery() function in /project/celery_utils.py
from celery import current_app as current_celery_app
def make_celery(app):
celery = current_celery_app
celery.config_from_object(app.config, namespace="CELERY")
class ContextTask(celery.Task):
def __call__(self, *args, **kwargs):
with app.app_context():
return self.run(*args, **kwargs)
celery.Task = ContextTask
return celery
And then a few tweaks in /project/users/tasks.py
from celery import shared_task
from .models import User
from project import db
#shared_task()
def post_to_db(payload):
user = User(**payload)
db.session.add(user)
db.session.commit()
db.session.close()
return True
Now I can see the user in the database, and my message queue is progressing as expected.

Related

APScheduler RuntimeError: No application found

I have a problem. I have IP addresses and with APScheduler I try to ping them every 10 seconds and update my database. For APScheduler I understand that I need to use with app.app_context() but the problem is that I don't know where to place it and wherever I have tried to place it, it raise: RuntimeError: No application found and fails to update the database
init.py:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from os import path
db = SQLAlchemy()
DB_NAME = "database.db"
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hjshjhdjah kjshkjdhjs'
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_NAME}'
db.init_app(app)
from .views import views
from .auth import auth
app.register_blueprint(views, url_prefix='/')
app.register_blueprint(auth, url_prefix='/')
from .models import Servers
create_database(app)
return app
def create_database(app):
if not path.exists('websiteflaskupdatedd/' + DB_NAME):
db.create_all(app=app)
print('Created Database!')
auth.py:
from flask import Blueprint, render_template, request, flash, redirect, url_for, escape, session
from .models import Servers, ping
from . import db, create_app
from apscheduler.schedulers.background import BackgroundScheduler
#auth.route('/servers', methods = ['POST', 'GET'])
def servers():
if request.method == 'POST':
server_ip = request.form.get("server_ip")
new_server = Servers(server_ip = server_ip)
try:
db.session.add(new_server)
db.session.commit()
return redirect("/servers")
except:
return "There was an error adding your server"
else:
if not Servers.query.get(1) == None:
servers = Servers.query.order_by(Servers.date_created)
return render_template('servers.html', servers=servers)
def update_up_status():
with create_app().app_context():
server_status_column = Servers.query.filter(Servers.server_status.in_(["0","1"]))
for server in server_status_column:
server.server_status = ping(server.server_ip)
db.session.commit()
scheduler = BackgroundScheduler()
if not scheduler.running:
scheduler.add_job(func=update_up_status, trigger="interval", seconds=10)
scheduler.start()
models.py:
from . import db
from datetime import datetime
import platform
import subprocess
def ping(host):
cmd = ['ping', '-w', "1", "-n", '1', host]
return subprocess.call(cmd) == 0
class Servers(db.Model):
id = db.Column(db.Integer, primary_key=True)
server_ip = db.Column(db.String(12), nullable=False, unique=True)
server_status = db.Column(db.String(12), nullable=False)
date_created = db.Column(db.DateTime, default= datetime.now)
Looks like you don't have your application running.
I use this setup regularly (flask + apscheduler) and have a main.py file with the following (including the necessary module imports from your other files):
if __name__ == "__main__":
scheduler = BackgroundScheduler()
scheduler.add_job(func=update_up_status, trigger="interval", seconds=10)
scheduler.start()
app.run(host="0.0.0.0", debug=True)
The background scheduler works within the background of another application and will not be blocked by the main thread which in this case is the flask application. As opposed to the blocking scheduler which will block the main thread and cannot be run inside of a another application.
Just like this.
def insertArticles(articles):
from app import db
from app.models.articles import Article
from app import scheduler
# 解决 flask apscheduler RuntimeError: No application found
with scheduler.app.app_context():
for article in articles:
# 字典 解包,直接插入字典
new_article = Article(**article)
db.session.add(new_article)
db.session.flush()
db.session.commit()
with scheduler.app.app_context() is the important line.

Peewee error on model.create() [ Proxy ]

Hello.
When i'm trying to create new record in model, i'm getting
AttributeError: 'dict' object has no attribute 'compiler'
Possible, that i'm getting this error because using Proxy class, but can't find mistake in my code.
init.py
from flask import Flask
from flask_peewee.db import Proxy
from flask_bootstrap import Bootstrap
from app.config import config_select
db = Proxy()
bootstrap = Bootstrap()
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config_select[config_name])
config_select[config_name].init_app(app)
db.initialize(config_select[config_name].DATABASE)
bootstrap.init_app(app)
#register blueprints
from app.blueprint.main import main_bp as main_blueprint
app.register_blueprint(main_blueprint)
return app
models.py
from peewee import *
from app import db
class BaseModel(Model):
class Meta:
database = db
class User(BaseModel):
id = PrimaryKeyField()
username = CharField(default='test', max_length=20)
class Meta:
db_table = 'users'
MODELS_LIST = [User]
Database was created by manager 'generate_db_tables' command
import os
from app import create_app
from flask_script import Manager, Shell
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
def connect_db():
from peewee import SqliteDatabase
db_lite = SqliteDatabase(app.config['DATABASE']['name'])
return db_lite
#manager.command
def generate_db_tables():
from app.models.models import MODELS_LIST
db_lite = connect_db()
for model in MODELS_LIST:
db_lite.create_table(model, safe=True)
print("Db tables created")
and the view function:
from flask import render_template, session, \
redirect, url_for, current_app, flash, request, make_response
from . import main_bp
from .forms import NameForm
from app.models.models import User
#main_bp.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
User.create(username=form.name.data) # <<<< Here is error
return url_for('.index')
return render_template('index.html', form=form)
Before i started to use Proxy class everything worked like a charm.
Seems, that i pass dict
DATABASE = {
'name': 'test.db',
'engine': 'peewee.SqliteDatabase'
}
to db.initialize()
instead of string name of database
DATABASE_INFO = 'test.db'
DATABASE = peewee.SqliteDatabase(DATABASE_INFO)
db.initialize(DATABASE)

use db without requiring app.app_context()- Flask

I have an app like this:
myapp/app/init.py:
import sqlite3
from contextlib import closing
from flask import Flask, g
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
# from app.models import db
from database import db
application = Flask(__name__)
application.config.from_object('config')
application.debug = True
db.init_app(application)
login_manager = LoginManager()
login_manager.init_app(application)
from app import views
myapp/database.py:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
myapp/app/models.py:
from database import db
from app import application
class CRUDMixin(object):
...
def delete(self, commit=True):
"""Remove the record from the database."""
with application.app_context():
db.session.delete(self)
return commit and db.session.commit()
class Model(CRUDMixin, db.Model):
"""Base model class that includes CRUD convenience methods."""
__abstract__ = True
def __init__(self, **kwargs):
db.Model.__init__(self, **kwargs)
class User(Model):
"""
:param str email: email address of user
:param str password: encrypted password for the user
"""
__tablename__ = 'users'
email = db.Column(db.String, primary_key=True)
password = db.Column(db.String)
authenticated = db.Column(db.Boolean, default=False)
def is_active(self):
"""True, as all users are active."""
return True
def get_id(self):
"""Return the email address to satisfy Flask-Login's requirements."""
return self.email
def is_authenticated(self):
"""Return True if the user is authenticated."""
return self.authenticated
def is_anonymous(self):
"""False, as anonymous users aren't supported."""
return False
The project I tried to structure after did not require with application.app_context() in the Model helper class. I cannot see any significant differences between my setup and its, and yet without with application.app_context() all over anything related to db I get the usual application not registered on db error. When everything you see in app/models.py and database.py was in app/__init__.py, it worked without requiring any with application.app_context() and I could import db raw in the shell like from myapp.app import db and it worked as is. What can I do to quiet the application not registered on db complaint but be able to use db easily without needing app_context, but still keep a proper directory structure where everything isn't jammed into init? Thank you
Flask-Script gives you a shell.
If you want to do it without Flask-Script, you must set the application context. A normal Python shell doesn't know how to setup your context.
It is easy to mimic the Flask-Script shell.
Create a shell.py file:
from app import application
ctx = application.app_context()
ctx.push()
Run it with python -i and use db with your app context already defined:
$ python -i shell.py
>>> from app import db
>>> db.create_all()

Flask app context for sqlalchemy

I am working on a small rest api in flask. Api has route that registers a request and spawn separate thread to run in background. Here is the code:
def dostuff(scriptname):
new_thread = threading.Thread(target=executescript,args=(scriptname,))
new_thread.start()
Thread starts but it errors out when I try to insert into db from executescript function. It complains about db object not registered with the app.
I am dynamically creating my app (with api as Blueprint).
Here is the structure of the app
-run.py ## runner script
-config
-development.py
-prod.py
-app
-__init__.py
- auth.py
- api_v1
- __init__.py
- routes.py
- models.py
here is my runner script run.py :
from app import create_app, db
if __name__ == '__main__':
app = create_app(os.environ.get('FLASK_CONFIG', 'development'))
with app.app_context():
db.create_all()
app.run()
Here is the code from app/__init__.py which creates the app:
from flask import Flask, jsonify, g
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app(config_name):
"""Create an application instance."""
app = Flask(__name__)
# apply configuration
cfg = os.path.join(os.getcwd(), 'config', config_name + '.py')
app.config.from_pyfile(cfg)
# initialize extensions
db.init_app(app)
# register blueprints
from .api_v1 import api as api_blueprint
app.register_blueprint(api_blueprint, url_prefix='/api/')
return app
All I need to know is how do I extend app context in routes.py. I can not import app there directly and if I do the following, I get RuntimeError: working outside of application context
def executescript(scriptname):
with current_app.app_context():
test_run = Testrun(pid=989, exit_status=988,comments="Test TestStarted")
db.session.add(test_run)
db.session.commit()
You're running a background task in a different thread that doesn't have the application context. You should pass the app object to the background worker. Miguel Grinberg gives an example of this here:
from threading import Thread
from app import app
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
Alternatively (and probably the best solution) would be to actually set up a thread-local scoped SQLAlchemy session instead of relying on Flask-SQLAlchemy's request context.
>>> from sqlalchemy.orm import scoped_session
>>> from sqlalchemy.orm import sessionmaker
>>> session_factory = sessionmaker(bind=some_engine)
>>> Session = scoped_session(session_factory)

How to use Flask-SQLAlchemy in a Celery task

I recently switch to Celery 3.0. Before that I was using Flask-Celery in order to integrate Celery with Flask. Although it had many issues like hiding some powerful Celery functionalities but it allowed me to use the full context of Flask app and especially Flask-SQLAlchemy.
In my background tasks I am processing data and the SQLAlchemy ORM to store the data. The maintainer of Flask-Celery has dropped support of the plugin. The plugin was pickling the Flask instance in the task so I could have full access to SQLAlchemy.
I am trying to replicate this behavior in my tasks.py file but with no success. Do you have any hints on how to achieve this?
Update: We've since started using a better way to handle application teardown and set up on a per-task basis, based on the pattern described in the more recent flask documentation.
extensions.py
import flask
from flask.ext.sqlalchemy import SQLAlchemy
from celery import Celery
class FlaskCelery(Celery):
def __init__(self, *args, **kwargs):
super(FlaskCelery, self).__init__(*args, **kwargs)
self.patch_task()
if 'app' in kwargs:
self.init_app(kwargs['app'])
def patch_task(self):
TaskBase = self.Task
_celery = self
class ContextTask(TaskBase):
abstract = True
def __call__(self, *args, **kwargs):
if flask.has_app_context():
return TaskBase.__call__(self, *args, **kwargs)
else:
with _celery.app.app_context():
return TaskBase.__call__(self, *args, **kwargs)
self.Task = ContextTask
def init_app(self, app):
self.app = app
self.config_from_object(app.config)
celery = FlaskCelery()
db = SQLAlchemy()
app.py
from flask import Flask
from extensions import celery, db
def create_app():
app = Flask()
#configure/initialize all your extensions
db.init_app(app)
celery.init_app(app)
return app
Once you've set up your app this way, you can run and use celery without having to explicitly run it from within an application context, as all your tasks will automatically be run in an application context if necessary, and you don't have to explicitly worry about post-task teardown, which is an important issue to manage (see other responses below).
Troubleshooting
Those who keep getting with _celery.app.app_context(): AttributeError: 'FlaskCelery' object has no attribute 'app' make sure to:
Keep the celery import at the app.py file level. Avoid:
app.py
from flask import Flask
def create_app():
app = Flask()
initiliaze_extensions(app)
return app
def initiliaze_extensions(app):
from extensions import celery, db # DOOMED! Keep celery import at the FILE level
db.init_app(app)
celery.init_app(app)
Start you celery workers BEFORE you flask run and use
celery worker -A app:celery -l info -f celery.log
Note the app:celery, i.e. loading from app.py.
You can still import from extensions to decorate tasks, i.e. from extensions import celery.
Old answer below, still works, but not as clean a solution
I prefer to run all of celery within the application context by creating a separate file that invokes celery.start() with the application's context. This means your tasks file doesn't have to be littered with context setup and teardowns. It also lends itself well to the flask 'application factory' pattern.
extensions.py
from from flask.ext.sqlalchemy import SQLAlchemy
from celery import Celery
db = SQLAlchemy()
celery = Celery()
tasks.py
from extensions import celery, db
from flask.globals import current_app
from celery.signals import task_postrun
#celery.task
def do_some_stuff():
current_app.logger.info("I have the application context")
#you can now use the db object from extensions
#task_postrun.connect
def close_session(*args, **kwargs):
# Flask SQLAlchemy will automatically create new sessions for you from
# a scoped session factory, given that we are maintaining the same app
# context, this ensures tasks have a fresh session (e.g. session errors
# won't propagate across tasks)
db.session.remove()
app.py
from extensions import celery, db
def create_app():
app = Flask()
#configure/initialize all your extensions
db.init_app(app)
celery.config_from_object(app.config)
return app
RunCelery.py
from app import create_app
from extensions import celery
app = create_app()
if __name__ == '__main__':
with app.app_context():
celery.start()
In your tasks.py file do the following:
from main import create_app
app = create_app()
celery = Celery(__name__)
celery.add_defaults(lambda: app.config)
#celery.task
def create_facet(project_id, **kwargs):
with app.test_request_context():
# your code
I used Paul Gibbs' answer with two differences. Instead of task_postrun I used worker_process_init. And instead of .remove() I used db.session.expire_all().
I'm not 100% sure, but from what I understand the way this works is when Celery creates a worker process, all inherited/shared db sessions will be expired, and SQLAlchemy will create new sessions on demand unique to that worker process.
So far it seems to have fixed my problem. With Paul's solution, when one worker finished and removed the session, another worker using the same session was still running its query, so db.session.remove() closed the connection while it was being used, giving me a "Lost connection to MySQL server during query" exception.
Thanks Paul for steering me in the right direction!
Nevermind that didn't work. I ended up having an argument in my Flask app factory to not run db.init_app(app) if Celery was calling it. Instead the workers will call it after Celery forks them. I now see several connections in my MySQL processlist.
from extensions import db
from celery.signals import worker_process_init
from flask import current_app
#worker_process_init.connect
def celery_worker_init_db(**_):
db.init_app(current_app)
from flask import Flask
from werkzeug.utils import import_string
from celery.signals import worker_process_init, celeryd_init
from flask_celery import Celery
from src.app import config_from_env, create_app
celery = Celery()
def get_celery_conf():
config = import_string('src.settings')
config = {k: getattr(config, k) for k in dir(config) if k.isupper()}
config['BROKER_URL'] = config['CELERY_BROKER_URL']
return config
#celeryd_init.connect
def init_celeryd(conf=None, **kwargs):
conf.update(get_celery_conf())
#worker_process_init.connect
def init_celery_flask_app(**kwargs):
app = create_app()
app.app_context().push()
Update celery config at celeryd init
Use your flask app factory to inititalize all flask extensions, including SQLAlchemy extension.
By doing this, we are able to maintain database connection per-worker.
If you want to run your task under flask context, you can subclass Task.__call__:
class SmartTask(Task):
abstract = True
def __call__(self, *_args, **_kwargs):
with self.app.flask_app.app_context():
with self.app.flask_app.test_request_context():
result = super(SmartTask, self).__call__(*_args, **_kwargs)
return result
class SmartCelery(Celery):
def init_app(self, app):
super(SmartCelery, self).init_app(app)
self.Task = SmartTask

Categories