]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:sparkles: Add support for BackgroundTasks parameters (#103)
authorSebastián Ramírez <tiangolo@gmail.com>
Sun, 24 Mar 2019 19:33:35 +0000 (23:33 +0400)
committerGitHub <noreply@github.com>
Sun, 24 Mar 2019 19:33:35 +0000 (23:33 +0400)
* :sparkles: Add support for BackgroundTasks parameters

* :bug: Fix type declaration in dependencies

* :bug: Fix coverage of util in tests

docs/src/background_tasks/tutorial001.py [new file with mode: 0644]
docs/src/background_tasks/tutorial002.py [new file with mode: 0644]
docs/tutorial/background-tasks.md [new file with mode: 0644]
fastapi/__init__.py
fastapi/dependencies/models.py
fastapi/dependencies/utils.py
fastapi/routing.py
mkdocs.yml
tests/test_tutorial/test_background_tasks/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_background_tasks/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_background_tasks/test_tutorial002.py [new file with mode: 0644]

diff --git a/docs/src/background_tasks/tutorial001.py b/docs/src/background_tasks/tutorial001.py
new file mode 100644 (file)
index 0000000..1720a74
--- /dev/null
@@ -0,0 +1,15 @@
+from fastapi import BackgroundTasks, FastAPI
+
+app = FastAPI()
+
+
+def write_notification(email: str, message=""):
+    with open("log.txt", mode="w") as email_file:
+        content = f"notification for {email}: {message}"
+        email_file.write(content)
+
+
+@app.post("/send-notification/{email}")
+async def send_notification(email: str, background_tasks: BackgroundTasks):
+    background_tasks.add_task(write_notification, email, message="some notification")
+    return {"message": "Notification sent in the background"}
diff --git a/docs/src/background_tasks/tutorial002.py b/docs/src/background_tasks/tutorial002.py
new file mode 100644 (file)
index 0000000..9fe7370
--- /dev/null
@@ -0,0 +1,24 @@
+from fastapi import BackgroundTasks, Depends, FastAPI
+
+app = FastAPI()
+
+
+def write_log(message: str):
+    with open("log.txt", mode="a") as log:
+        log.write(message)
+
+
+def get_query(background_tasks: BackgroundTasks, q: str = None):
+    if q:
+        message = f"found query: {q}\n"
+        background_tasks.add_task(write_log, message)
+    return q
+
+
+@app.post("/send-notification/{email}")
+async def send_notification(
+    email: str, background_tasks: BackgroundTasks, q: str = Depends(get_query)
+):
+    message = f"message to {email}\n"
+    background_tasks.add_task(write_log, message)
+    return {"message": "Message sent"}
diff --git a/docs/tutorial/background-tasks.md b/docs/tutorial/background-tasks.md
new file mode 100644 (file)
index 0000000..5764dc9
--- /dev/null
@@ -0,0 +1,86 @@
+You can define background tasks to be run *after* returning a response.
+
+This is useful for operations that need to happen after a request, but that the client doesn't really have to be waiting for the operation to complete before receiving his response.
+
+This includes, for example:
+
+* Email notifications sent after performing an action:
+    * As connecting to an email server and sending an email tends to be "slow" (several seconds), you can return the response right away and send the email notification in the background.
+* Processing data:
+    * For example, let's say you receive a file that must go through a slow process, you can return a response of "Accepted" (HTTP 202) and process it in the background.
+
+## Using `BackgroundTasks`
+
+First, import `BackgroundTasks` and define a parameter in your *path operation function* with a type declaration of `BackgroundTasks`:
+
+```Python hl_lines="1 13"
+{!./src/background_tasks/tutorial001.py!}
+```
+
+**FastAPI** will create the object of type `BackgroundTasks` for you and pass it as that parameter.
+
+!!! tip
+    You declare a parameter of `BackgroundTasks` and use it in a very similar way as to when <a href="/tutorial/using-request-directly/" target="_blank">using the `Request` directly</a>.
+
+
+## Create a task function
+
+Create a function to be run as the background task.
+
+It is just a standard function that can receive parameters.
+
+It can be an `async def` or normal `def` function, **FastAPI** will know how to handle it correctly.
+
+In this case, the task function will write to a file (simulating sending an email). 
+
+And as the write operation doesn't use `async` and `await`, we define the function with normal `def`:
+
+```Python hl_lines="6 7 8 9"
+{!./src/background_tasks/tutorial001.py!}
+```
+
+## Add the background task
+
+Inside of your *path operation function*, pass your task function to the *background tasks* object with the method `.add_task()`:
+
+```Python hl_lines="14"
+{!./src/background_tasks/tutorial001.py!}
+```
+
+`.add_task()` receives as arguments:
+
+* A task function to be run in the background (`write_notification`).
+* Any sequence of arguments that should be passed to the task function in order (`email`).
+* Any keyword arguments that should be passed to the task function (`message="some notification"`).
+
+## Dependency Injection
+
+Using `BackgroundTasks` also works with the dependency injection system, you can declare a parameter of type `BackgroundTasks` at multiple levels: in a *path operation function*, in a dependency (dependable), in a sub-dependency, etc.
+
+**FastAPI** knows what to do in each case and how to re-use the same object, so that all the background tasks are merged together and are run in the background afterwards:
+
+```Python hl_lines="11 14 20 23"
+{!./src/background_tasks/tutorial002.py!}
+```
+
+In this example, the messages will be written to the `log.txt` file *after* the response is sent.
+
+If there was a query in the request, it will be written to the log in a background task.
+
+And then another background task generated at the *path operation function* will write a message using the `email` path parameter.
+
+## Technical Details
+
+The class `BackgroundTasks` comes directly from <a href="https://www.starlette.io/background/" target="_blank">`starlette.background`</a>.
+
+It is imported/included directly into FastAPI so that you can import it from `fastapi` and avoid accidentally importing the alternative `BackgroundTask` (without the `s` at the end) from `starlette.background`.
+
+By only using `BackgroundTasks` (and not `BackgroundTask`), it's then possible to use it as a *path operation function* parameter and have **FastAPI** handle the rest for you, just like when using the `Request` object directly.
+
+It's still possible to use `BackgroundTask` alone in FastAPI, but you have to create the object in your code and return a Starlette `Response` including it.
+
+You can see more details in <a href="https://www.starlette.io/background/" target="_blank">Starlette's official docs for Background Tasks</a>.
+
+## Recap
+
+Import and use `BackgroundTasks` with parameters in *path operation functions* and dependencies to add background tasks.
index 2db3339dbcc80abebbc6ab84f4a8e21015c16dde..6affb37e1c57636c263abb06101f2b7fb1368a17 100644 (file)
@@ -2,6 +2,8 @@
 
 __version__ = "0.9.1"
 
+from starlette.background import BackgroundTasks
+
 from .applications import FastAPI
 from .datastructures import UploadFile
 from .exceptions import HTTPException
index 748fe4a9eb990ae2657f3097cd36a876633607fc..d7fbf853d1ba5bcb2cf34c3efc4b775f99d1413e 100644 (file)
@@ -26,6 +26,7 @@ class Dependant:
         name: str = None,
         call: Callable = None,
         request_param_name: str = None,
+        background_tasks_param_name: str = None,
     ) -> None:
         self.path_params = path_params or []
         self.query_params = query_params or []
@@ -35,5 +36,6 @@ class Dependant:
         self.dependencies = dependencies or []
         self.security_requirements = security_schemes or []
         self.request_param_name = request_param_name
+        self.background_tasks_param_name = background_tasks_param_name
         self.name = name
         self.call = call
index 5c1f42632afc23d2a731bbb8c690facebb647258..157a685c463cd4777a411e5ef650205eac817ad8 100644 (file)
@@ -3,7 +3,18 @@ import inspect
 from copy import deepcopy
 from datetime import date, datetime, time, timedelta
 from decimal import Decimal
-from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type, Union
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    List,
+    Mapping,
+    Optional,
+    Sequence,
+    Tuple,
+    Type,
+    Union,
+)
 from uuid import UUID
 
 from fastapi import params
@@ -16,6 +27,7 @@ from pydantic.errors import MissingError
 from pydantic.fields import Field, Required, Shape
 from pydantic.schema import get_annotation_from_schema
 from pydantic.utils import lenient_issubclass
+from starlette.background import BackgroundTasks
 from starlette.concurrency import run_in_threadpool
 from starlette.datastructures import UploadFile
 from starlette.requests import Headers, QueryParams, Request
@@ -125,6 +137,8 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
             )
         elif lenient_issubclass(param.annotation, Request):
             dependant.request_param_name = param_name
+        elif lenient_issubclass(param.annotation, BackgroundTasks):
+            dependant.background_tasks_param_name = param_name
         elif not isinstance(param.default, params.Depends):
             add_param_to_body_fields(param=param, dependant=dependant)
     return dependant
@@ -215,13 +229,20 @@ def is_coroutine_callable(call: Callable) -> bool:
 
 
 async def solve_dependencies(
-    *, request: Request, dependant: Dependant, body: Dict[str, Any] = None
-) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
+    *,
+    request: Request,
+    dependant: Dependant,
+    body: Dict[str, Any] = None,
+    background_tasks: BackgroundTasks = None,
+) -> Tuple[Dict[str, Any], List[ErrorWrapper], Optional[BackgroundTasks]]:
     values: Dict[str, Any] = {}
     errors: List[ErrorWrapper] = []
     for sub_dependant in dependant.dependencies:
-        sub_values, sub_errors = await solve_dependencies(
-            request=request, dependant=sub_dependant, body=body
+        sub_values, sub_errors, background_tasks = await solve_dependencies(
+            request=request,
+            dependant=sub_dependant,
+            body=body,
+            background_tasks=background_tasks,
         )
         if sub_errors:
             errors.extend(sub_errors)
@@ -258,7 +279,11 @@ async def solve_dependencies(
         errors.extend(body_errors)
     if dependant.request_param_name:
         values[dependant.request_param_name] = request
-    return values, errors
+    if dependant.background_tasks_param_name:
+        if background_tasks is None:
+            background_tasks = BackgroundTasks()
+        values[dependant.background_tasks_param_name] = background_tasks
+    return values, errors, background_tasks
 
 
 def request_params_to_args(
index 67619bda57e93960b5c2dbcf24b1a012df6d9906..493590af7002e2e3d4977f3ed66e0cb8f1455c4e 100644 (file)
@@ -68,7 +68,7 @@ def get_app(
             raise HTTPException(
                 status_code=400, detail="There was an error parsing the body"
             )
-        values, errors = await solve_dependencies(
+        values, errors, background_tasks = await solve_dependencies(
             request=request, dependant=dependant, body=body
         )
         if errors:
@@ -83,11 +83,17 @@ def get_app(
             else:
                 raw_response = await run_in_threadpool(dependant.call, **values)
             if isinstance(raw_response, Response):
+                if raw_response.background is None:
+                    raw_response.background = background_tasks
                 return raw_response
             response_data = serialize_response(
                 field=response_field, response=raw_response
             )
-            return content_type(content=response_data, status_code=status_code)
+            return content_type(
+                content=response_data,
+                status_code=status_code,
+                background=background_tasks,
+            )
 
     return app
 
index 0e4e91c4ab8fbedb55dbe6810c0142caa5c42456..1eed39a8c9467b36711f4b417817d706fd87d9cc 100644 (file)
@@ -59,6 +59,7 @@ nav:
         - SQL (Relational) Databases: 'tutorial/sql-databases.md'
         - NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
         - Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
+        - Background Tasks: 'tutorial/background-tasks.md'
         - Sub Applications - Behind a Proxy: 'tutorial/sub-applications-proxy.md'
         - Application Configuration: 'tutorial/application-configuration.md'
         - GraphQL: 'tutorial/graphql.md'
diff --git a/tests/test_tutorial/test_background_tasks/__init__.py b/tests/test_tutorial/test_background_tasks/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial001.py b/tests/test_tutorial/test_background_tasks/test_tutorial001.py
new file mode 100644 (file)
index 0000000..f86f7f9
--- /dev/null
@@ -0,0 +1,19 @@
+import os
+from pathlib import Path
+
+from starlette.testclient import TestClient
+
+from background_tasks.tutorial001 import app
+
+client = TestClient(app)
+
+
+def test():
+    log = Path("log.txt")
+    if log.is_file():
+        os.remove(log)  # pragma: no cover
+    response = client.post("/send-notification/foo@example.com")
+    assert response.status_code == 200
+    assert response.json() == {"message": "Notification sent in the background"}
+    with open("./log.txt") as f:
+        assert "notification for foo@example.com: some notification" in f.read()
diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002.py b/tests/test_tutorial/test_background_tasks/test_tutorial002.py
new file mode 100644 (file)
index 0000000..69c8b7f
--- /dev/null
@@ -0,0 +1,19 @@
+import os
+from pathlib import Path
+
+from starlette.testclient import TestClient
+
+from background_tasks.tutorial002 import app
+
+client = TestClient(app)
+
+
+def test():
+    log = Path("log.txt")
+    if log.is_file():
+        os.remove(log)  # pragma: no cover
+    response = client.post("/send-notification/foo@example.com?q=some-query")
+    assert response.status_code == 200
+    assert response.json() == {"message": "Message sent"}
+    with open("./log.txt") as f:
+        assert "found query: some-query\nmessage to foo@example.com" in f.read()