Skip to content

Commit 3f19524

Browse files
committed
add celery example
1 parent dca8cf0 commit 3f19524

File tree

9 files changed

+313
-0
lines changed

9 files changed

+313
-0
lines changed

docs/patterns/celery.rst

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Celery itself.
1414
.. _Celery: https://celery.readthedocs.io
1515
.. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html
1616

17+
The Flask repository contains `an example <https://github.com/pallets/flask/tree/main/examples/celery>`_
18+
based on the information on this page, which also shows how to use JavaScript to submit
19+
tasks and poll for progress and results.
20+
1721

1822
Install
1923
-------
@@ -209,6 +213,9 @@ Now you can start the task using the first route, then poll for the result using
209213
second route. This keeps the Flask request workers from being blocked waiting for tasks
210214
to finish.
211215

216+
The Flask repository contains `an example <https://github.com/pallets/flask/tree/main/examples/celery>`_
217+
using JavaScript to submit tasks and poll for progress and results.
218+
212219

213220
Passing Data to Tasks
214221
---------------------

examples/celery/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Background Tasks with Celery
2+
============================
3+
4+
This example shows how to configure Celery with Flask, how to set up an API for
5+
submitting tasks and polling results, and how to use that API with JavaScript. See
6+
[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/).
7+
8+
From this directory, create a virtualenv and install the application into it. Then run a
9+
Celery worker.
10+
11+
```shell
12+
$ python3 -m venv .venv
13+
$ . ./.venv/bin/activate
14+
$ pip install -r requirements.txt && pip install -e .
15+
$ celery -A make_celery worker --loglevel INFO
16+
```
17+
18+
In a separate terminal, activate the virtualenv and run the Flask development server.
19+
20+
```shell
21+
$ . ./.venv/bin/activate
22+
$ flask -A task_app --debug run
23+
```
24+
25+
Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling
26+
requests in the browser dev tools and the Flask logs. You can see the tasks submitting
27+
and completing in the Celery logs.

examples/celery/make_celery.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from task_app import create_app
2+
3+
flask_app = create_app()
4+
celery_app = flask_app.extensions["celery"]

examples/celery/pyproject.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "flask-example-celery"
3+
version = "1.0.0"
4+
description = "Example Flask application with Celery background tasks."
5+
readme = "README.md"
6+
requires-python = ">=3.7"
7+
dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"]
8+
9+
[build-system]
10+
requires = ["setuptools"]
11+
build-backend = "setuptools.build_meta"

examples/celery/requirements.txt

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.10
3+
# by the following command:
4+
#
5+
# pip-compile pyproject.toml
6+
#
7+
amqp==5.1.1
8+
# via kombu
9+
async-timeout==4.0.2
10+
# via redis
11+
billiard==3.6.4.0
12+
# via celery
13+
celery[redis]==5.2.7
14+
# via flask-example-celery (pyproject.toml)
15+
click==8.1.3
16+
# via
17+
# celery
18+
# click-didyoumean
19+
# click-plugins
20+
# click-repl
21+
# flask
22+
click-didyoumean==0.3.0
23+
# via celery
24+
click-plugins==1.1.1
25+
# via celery
26+
click-repl==0.2.0
27+
# via celery
28+
flask==2.2.2
29+
# via flask-example-celery (pyproject.toml)
30+
itsdangerous==2.1.2
31+
# via flask
32+
jinja2==3.1.2
33+
# via flask
34+
kombu==5.2.4
35+
# via celery
36+
markupsafe==2.1.2
37+
# via
38+
# jinja2
39+
# werkzeug
40+
prompt-toolkit==3.0.36
41+
# via click-repl
42+
pytz==2022.7.1
43+
# via celery
44+
redis==4.5.1
45+
# via celery
46+
six==1.16.0
47+
# via click-repl
48+
vine==5.0.0
49+
# via
50+
# amqp
51+
# celery
52+
# kombu
53+
wcwidth==0.2.6
54+
# via prompt-toolkit
55+
werkzeug==2.2.2
56+
# via flask
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from celery import Celery
2+
from celery import Task
3+
from flask import Flask
4+
from flask import render_template
5+
6+
7+
def create_app() -> Flask:
8+
app = Flask(__name__)
9+
app.config.from_mapping(
10+
CELERY=dict(
11+
broker_url="redis://localhost",
12+
result_backend="redis://localhost",
13+
task_ignore_result=True,
14+
),
15+
)
16+
app.config.from_prefixed_env()
17+
celery_init_app(app)
18+
19+
@app.route("/")
20+
def index() -> str:
21+
return render_template("index.html")
22+
23+
from . import views
24+
25+
app.register_blueprint(views.bp)
26+
return app
27+
28+
29+
def celery_init_app(app: Flask) -> Celery:
30+
class FlaskTask(Task):
31+
def __call__(self, *args: object, **kwargs: object) -> object:
32+
with app.app_context():
33+
return self.run(*args, **kwargs)
34+
35+
celery_app = Celery(app.name, task_cls=FlaskTask)
36+
celery_app.config_from_object(app.config["CELERY"])
37+
celery_app.set_default()
38+
app.extensions["celery"] = celery_app
39+
return celery_app

examples/celery/src/task_app/tasks.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import time
2+
3+
from celery import shared_task
4+
from celery import Task
5+
6+
7+
@shared_task(ignore_result=False)
8+
def add(a: int, b: int) -> int:
9+
return a + b
10+
11+
12+
@shared_task()
13+
def block() -> None:
14+
time.sleep(5)
15+
16+
17+
@shared_task(bind=True, ignore_result=False)
18+
def process(self: Task, total: int) -> object:
19+
for i in range(total):
20+
self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total})
21+
time.sleep(1)
22+
23+
return {"current": total, "total": total}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset=UTF-8>
5+
<title>Celery Example</title>
6+
</head>
7+
<body>
8+
<h2>Celery Example</h2>
9+
Execute background tasks with Celery. Submits tasks and shows results using JavaScript.
10+
11+
<hr>
12+
<h4>Add</h4>
13+
<p>Start a task to add two numbers, then poll for the result.
14+
<form id=add method=post action="{{ url_for("tasks.add") }}">
15+
<label>A <input type=number name=a value=4></label><br>
16+
<label>B <input type=number name=b value=2></label><br>
17+
<input type=submit>
18+
</form>
19+
<p>Result: <span id=add-result></span></p>
20+
21+
<hr>
22+
<h4>Block</h4>
23+
<p>Start a task that takes 5 seconds. However, the response will return immediately.
24+
<form id=block method=post action="{{ url_for("tasks.block") }}">
25+
<input type=submit>
26+
</form>
27+
<p id=block-result></p>
28+
29+
<hr>
30+
<h4>Process</h4>
31+
<p>Start a task that counts, waiting one second each time, showing progress.
32+
<form id=process method=post action="{{ url_for("tasks.process") }}">
33+
<label>Total <input type=number name=total value="10"></label><br>
34+
<input type=submit>
35+
</form>
36+
<p id=process-result></p>
37+
38+
<script>
39+
const taskForm = (formName, doPoll, report) => {
40+
document.forms[formName].addEventListener("submit", (event) => {
41+
event.preventDefault()
42+
fetch(event.target.action, {
43+
method: "POST",
44+
body: new FormData(event.target)
45+
})
46+
.then(response => response.json())
47+
.then(data => {
48+
report(null)
49+
50+
const poll = () => {
51+
fetch(`/tasks/result/${data["result_id"]}`)
52+
.then(response => response.json())
53+
.then(data => {
54+
report(data)
55+
56+
if (!data["ready"]) {
57+
setTimeout(poll, 500)
58+
} else if (!data["successful"]) {
59+
console.error(formName, data)
60+
}
61+
})
62+
}
63+
64+
if (doPoll) {
65+
poll()
66+
}
67+
})
68+
})
69+
}
70+
71+
taskForm("add", true, data => {
72+
const el = document.getElementById("add-result")
73+
74+
if (data === null) {
75+
el.innerText = "submitted"
76+
} else if (!data["ready"]) {
77+
el.innerText = "waiting"
78+
} else if (!data["successful"]) {
79+
el.innerText = "error, check console"
80+
} else {
81+
el.innerText = data["value"]
82+
}
83+
})
84+
85+
taskForm("block", false, data => {
86+
document.getElementById("block-result").innerText = (
87+
"request finished, check celery log to see task finish in 5 seconds"
88+
)
89+
})
90+
91+
taskForm("process", true, data => {
92+
const el = document.getElementById("process-result")
93+
94+
if (data === null) {
95+
el.innerText = "submitted"
96+
} else if (!data["ready"]) {
97+
el.innerText = `${data["value"]["current"]} / ${data["value"]["total"]}`
98+
} else if (!data["successful"]) {
99+
el.innerText = "error, check console"
100+
} else {
101+
el.innerText = "✅ done"
102+
}
103+
console.log(data)
104+
})
105+
106+
</script>
107+
</body>
108+
</html>

examples/celery/src/task_app/views.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from celery.result import AsyncResult
2+
from flask import Blueprint
3+
from flask import request
4+
5+
from . import tasks
6+
7+
bp = Blueprint("tasks", __name__, url_prefix="/tasks")
8+
9+
10+
@bp.get("/result/<id>")
11+
def result(id: str) -> dict[str, object]:
12+
result = AsyncResult(id)
13+
ready = result.ready()
14+
return {
15+
"ready": ready,
16+
"successful": result.successful() if ready else None,
17+
"value": result.get() if ready else result.result,
18+
}
19+
20+
21+
@bp.post("/add")
22+
def add() -> dict[str, object]:
23+
a = request.form.get("a", type=int)
24+
b = request.form.get("b", type=int)
25+
result = tasks.add.delay(a, b)
26+
return {"result_id": result.id}
27+
28+
29+
@bp.post("/block")
30+
def block() -> dict[str, object]:
31+
result = tasks.block.delay()
32+
return {"result_id": result.id}
33+
34+
35+
@bp.post("/process")
36+
def process() -> dict[str, object]:
37+
result = tasks.process.delay(total=request.form.get("total", type=int))
38+
return {"result_id": result.id}

0 commit comments

Comments
 (0)