mirror of https://github.com/pallets/flask.git
				
				
				
			add celery example
This commit is contained in:
		
							parent
							
								
									dca8cf013b
								
							
						
					
					
						commit
						3f195248dc
					
				|  | @ -14,6 +14,10 @@ Celery itself. | |||
| .. _Celery: https://celery.readthedocs.io | ||||
| .. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html | ||||
| 
 | ||||
| The Flask repository contains `an example <https://github.com/pallets/flask/tree/main/examples/celery>`_ | ||||
| based on the information on this page, which also shows how to use JavaScript to submit | ||||
| tasks and poll for progress and results. | ||||
| 
 | ||||
| 
 | ||||
| Install | ||||
| ------- | ||||
|  | @ -209,6 +213,9 @@ Now you can start the task using the first route, then poll for the result using | |||
| second route. This keeps the Flask request workers from being blocked waiting for tasks | ||||
| to finish. | ||||
| 
 | ||||
| The Flask repository contains `an example <https://github.com/pallets/flask/tree/main/examples/celery>`_ | ||||
| using JavaScript to submit tasks and poll for progress and results. | ||||
| 
 | ||||
| 
 | ||||
| Passing Data to Tasks | ||||
| --------------------- | ||||
|  |  | |||
|  | @ -0,0 +1,27 @@ | |||
| Background Tasks with Celery | ||||
| ============================ | ||||
| 
 | ||||
| This example shows how to configure Celery with Flask, how to set up an API for | ||||
| submitting tasks and polling results, and how to use that API with JavaScript. See | ||||
| [Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). | ||||
| 
 | ||||
| From this directory, create a virtualenv and install the application into it. Then run a | ||||
| Celery worker. | ||||
| 
 | ||||
| ```shell | ||||
| $ python3 -m venv .venv | ||||
| $ . ./.venv/bin/activate | ||||
| $ pip install -r requirements.txt && pip install -e . | ||||
| $ celery -A make_celery worker --loglevel INFO | ||||
| ``` | ||||
| 
 | ||||
| In a separate terminal, activate the virtualenv and run the Flask development server. | ||||
| 
 | ||||
| ```shell | ||||
| $ . ./.venv/bin/activate | ||||
| $ flask -A task_app --debug run | ||||
| ``` | ||||
| 
 | ||||
| Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling | ||||
| requests in the browser dev tools and the Flask logs. You can see the tasks submitting | ||||
| and completing in the Celery logs. | ||||
|  | @ -0,0 +1,4 @@ | |||
| from task_app import create_app | ||||
| 
 | ||||
| flask_app = create_app() | ||||
| celery_app = flask_app.extensions["celery"] | ||||
|  | @ -0,0 +1,11 @@ | |||
| [project] | ||||
| name = "flask-example-celery" | ||||
| version = "1.0.0" | ||||
| description = "Example Flask application with Celery background tasks." | ||||
| readme = "README.md" | ||||
| requires-python = ">=3.7" | ||||
| dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"] | ||||
| 
 | ||||
| [build-system] | ||||
| requires = ["setuptools"] | ||||
| build-backend = "setuptools.build_meta" | ||||
|  | @ -0,0 +1,56 @@ | |||
| # | ||||
| # This file is autogenerated by pip-compile with Python 3.10 | ||||
| # by the following command: | ||||
| # | ||||
| #    pip-compile pyproject.toml | ||||
| # | ||||
| amqp==5.1.1 | ||||
|     # via kombu | ||||
| async-timeout==4.0.2 | ||||
|     # via redis | ||||
| billiard==3.6.4.0 | ||||
|     # via celery | ||||
| celery[redis]==5.2.7 | ||||
|     # via flask-example-celery (pyproject.toml) | ||||
| click==8.1.3 | ||||
|     # via | ||||
|     #   celery | ||||
|     #   click-didyoumean | ||||
|     #   click-plugins | ||||
|     #   click-repl | ||||
|     #   flask | ||||
| click-didyoumean==0.3.0 | ||||
|     # via celery | ||||
| click-plugins==1.1.1 | ||||
|     # via celery | ||||
| click-repl==0.2.0 | ||||
|     # via celery | ||||
| flask==2.2.2 | ||||
|     # via flask-example-celery (pyproject.toml) | ||||
| itsdangerous==2.1.2 | ||||
|     # via flask | ||||
| jinja2==3.1.2 | ||||
|     # via flask | ||||
| kombu==5.2.4 | ||||
|     # via celery | ||||
| markupsafe==2.1.2 | ||||
|     # via | ||||
|     #   jinja2 | ||||
|     #   werkzeug | ||||
| prompt-toolkit==3.0.36 | ||||
|     # via click-repl | ||||
| pytz==2022.7.1 | ||||
|     # via celery | ||||
| redis==4.5.1 | ||||
|     # via celery | ||||
| six==1.16.0 | ||||
|     # via click-repl | ||||
| vine==5.0.0 | ||||
|     # via | ||||
|     #   amqp | ||||
|     #   celery | ||||
|     #   kombu | ||||
| wcwidth==0.2.6 | ||||
|     # via prompt-toolkit | ||||
| werkzeug==2.2.2 | ||||
|     # via flask | ||||
|  | @ -0,0 +1,39 @@ | |||
| from celery import Celery | ||||
| from celery import Task | ||||
| from flask import Flask | ||||
| from flask import render_template | ||||
| 
 | ||||
| 
 | ||||
| def create_app() -> Flask: | ||||
|     app = Flask(__name__) | ||||
|     app.config.from_mapping( | ||||
|         CELERY=dict( | ||||
|             broker_url="redis://localhost", | ||||
|             result_backend="redis://localhost", | ||||
|             task_ignore_result=True, | ||||
|         ), | ||||
|     ) | ||||
|     app.config.from_prefixed_env() | ||||
|     celery_init_app(app) | ||||
| 
 | ||||
|     @app.route("/") | ||||
|     def index() -> str: | ||||
|         return render_template("index.html") | ||||
| 
 | ||||
|     from . import views | ||||
| 
 | ||||
|     app.register_blueprint(views.bp) | ||||
|     return app | ||||
| 
 | ||||
| 
 | ||||
| def celery_init_app(app: Flask) -> Celery: | ||||
|     class FlaskTask(Task): | ||||
|         def __call__(self, *args: object, **kwargs: object) -> object: | ||||
|             with app.app_context(): | ||||
|                 return self.run(*args, **kwargs) | ||||
| 
 | ||||
|     celery_app = Celery(app.name, task_cls=FlaskTask) | ||||
|     celery_app.config_from_object(app.config["CELERY"]) | ||||
|     celery_app.set_default() | ||||
|     app.extensions["celery"] = celery_app | ||||
|     return celery_app | ||||
|  | @ -0,0 +1,23 @@ | |||
| import time | ||||
| 
 | ||||
| from celery import shared_task | ||||
| from celery import Task | ||||
| 
 | ||||
| 
 | ||||
| @shared_task(ignore_result=False) | ||||
| def add(a: int, b: int) -> int: | ||||
|     return a + b | ||||
| 
 | ||||
| 
 | ||||
| @shared_task() | ||||
| def block() -> None: | ||||
|     time.sleep(5) | ||||
| 
 | ||||
| 
 | ||||
| @shared_task(bind=True, ignore_result=False) | ||||
| def process(self: Task, total: int) -> object: | ||||
|     for i in range(total): | ||||
|         self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) | ||||
|         time.sleep(1) | ||||
| 
 | ||||
|     return {"current": total, "total": total} | ||||
|  | @ -0,0 +1,108 @@ | |||
| <!doctype html> | ||||
| <html> | ||||
| <head> | ||||
|   <meta charset=UTF-8> | ||||
|   <title>Celery Example</title> | ||||
| </head> | ||||
| <body> | ||||
| <h2>Celery Example</h2> | ||||
| Execute background tasks with Celery. Submits tasks and shows results using JavaScript. | ||||
| 
 | ||||
| <hr> | ||||
| <h4>Add</h4> | ||||
| <p>Start a task to add two numbers, then poll for the result. | ||||
| <form id=add method=post action="{{ url_for("tasks.add") }}"> | ||||
|   <label>A <input type=number name=a value=4></label><br> | ||||
|   <label>B <input type=number name=b value=2></label><br> | ||||
|   <input type=submit> | ||||
| </form> | ||||
| <p>Result: <span id=add-result></span></p> | ||||
| 
 | ||||
| <hr> | ||||
| <h4>Block</h4> | ||||
| <p>Start a task that takes 5 seconds. However, the response will return immediately. | ||||
| <form id=block method=post action="{{ url_for("tasks.block") }}"> | ||||
|   <input type=submit> | ||||
| </form> | ||||
| <p id=block-result></p> | ||||
| 
 | ||||
| <hr> | ||||
| <h4>Process</h4> | ||||
| <p>Start a task that counts, waiting one second each time, showing progress. | ||||
| <form id=process method=post action="{{ url_for("tasks.process") }}"> | ||||
|   <label>Total <input type=number name=total value="10"></label><br> | ||||
|   <input type=submit> | ||||
| </form> | ||||
| <p id=process-result></p> | ||||
| 
 | ||||
| <script> | ||||
|   const taskForm = (formName, doPoll, report) => { | ||||
|     document.forms[formName].addEventListener("submit", (event) => { | ||||
|       event.preventDefault() | ||||
|       fetch(event.target.action, { | ||||
|         method: "POST", | ||||
|         body: new FormData(event.target) | ||||
|       }) | ||||
|         .then(response => response.json()) | ||||
|         .then(data => { | ||||
|           report(null) | ||||
| 
 | ||||
|           const poll = () => { | ||||
|             fetch(`/tasks/result/${data["result_id"]}`) | ||||
|               .then(response => response.json()) | ||||
|               .then(data => { | ||||
|                 report(data) | ||||
| 
 | ||||
|                 if (!data["ready"]) { | ||||
|                   setTimeout(poll, 500) | ||||
|                 } else if (!data["successful"]) { | ||||
|                   console.error(formName, data) | ||||
|                 } | ||||
|               }) | ||||
|           } | ||||
| 
 | ||||
|           if (doPoll) { | ||||
|             poll() | ||||
|           } | ||||
|         }) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   taskForm("add", true, data => { | ||||
|     const el = document.getElementById("add-result") | ||||
| 
 | ||||
|     if (data === null) { | ||||
|       el.innerText = "submitted" | ||||
|     } else if (!data["ready"]) { | ||||
|       el.innerText = "waiting" | ||||
|     } else if (!data["successful"]) { | ||||
|       el.innerText = "error, check console" | ||||
|     } else { | ||||
|       el.innerText = data["value"] | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   taskForm("block", false, data => { | ||||
|     document.getElementById("block-result").innerText = ( | ||||
|       "request finished, check celery log to see task finish in 5 seconds" | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   taskForm("process", true, data => { | ||||
|     const el = document.getElementById("process-result") | ||||
| 
 | ||||
|     if (data === null) { | ||||
|       el.innerText = "submitted" | ||||
|     } else if (!data["ready"]) { | ||||
|       el.innerText = `${data["value"]["current"]} / ${data["value"]["total"]}` | ||||
|     } else if (!data["successful"]) { | ||||
|       el.innerText = "error, check console" | ||||
|     } else { | ||||
|       el.innerText = "✅ done" | ||||
|     } | ||||
|     console.log(data) | ||||
|   }) | ||||
| 
 | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
|  | @ -0,0 +1,38 @@ | |||
| from celery.result import AsyncResult | ||||
| from flask import Blueprint | ||||
| from flask import request | ||||
| 
 | ||||
| from . import tasks | ||||
| 
 | ||||
| bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||||
| 
 | ||||
| 
 | ||||
| @bp.get("/result/<id>") | ||||
| def result(id: str) -> dict[str, object]: | ||||
|     result = AsyncResult(id) | ||||
|     ready = result.ready() | ||||
|     return { | ||||
|         "ready": ready, | ||||
|         "successful": result.successful() if ready else None, | ||||
|         "value": result.get() if ready else result.result, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @bp.post("/add") | ||||
| def add() -> dict[str, object]: | ||||
|     a = request.form.get("a", type=int) | ||||
|     b = request.form.get("b", type=int) | ||||
|     result = tasks.add.delay(a, b) | ||||
|     return {"result_id": result.id} | ||||
| 
 | ||||
| 
 | ||||
| @bp.post("/block") | ||||
| def block() -> dict[str, object]: | ||||
|     result = tasks.block.delay() | ||||
|     return {"result_id": result.id} | ||||
| 
 | ||||
| 
 | ||||
| @bp.post("/process") | ||||
| def process() -> dict[str, object]: | ||||
|     result = tasks.process.delay(total=request.form.get("total", type=int)) | ||||
|     return {"result_id": result.id} | ||||
		Loading…
	
		Reference in New Issue