Repository: encode/starlette

Issue:
Background tasks
* Add a `background=...` keyword argument to all responses. It should be either `None` or a coroutine with no arguments.
* The response `__call__` should run the background coroutine if it exists (after sending the response).
* Provide a `BackgroundTask()` class that wraps a callable and some arguments. This can then be used to pass a function *or* coroutine with args/kwargs to a response.

```python
class BackgroundTask:
    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs

    async def __call__(self):
        if asyncio.iscoroutine(self.func):
            await self.func(*self.args, **self.kwargs)
        else:
            self.func(*self.args, **self.kwargs)
```

Then we can do...

```
return Response(..., background=BackgroundTask(my_func, a=123)
```

Naming might need some thinking about - not sure that conflicting with asyncio's `Task` is ideal.

This'd be a good feature to get in, since it's a low amount of work, but provides functionality that WSGI is unable to do.


Discussion:
Interesting. I look forward to trying this out.
Going along with your example above, it'd be simple to just..
```python
async def __call__(self, receive: Receive, send: Send) -> None:
        await send(
            {
                "type": "http.response.start",
                "status": self.status_code,
                "headers": self.raw_headers,
            }
        )
        await send({"type": "http.response.body", "body": self.body})
        if self.background:
            asyncio.ensure_future(self.background())
```
Or is that too naive a solution?
@alexbotello That's exactly right, yup.

(To be precise we'd use `if self.background is not None`)
I played around with this some yesterday. And while this does indeed execute the background task, it also blocks any other incoming requests. 
Any ideas how to circumvent that?
> it also blocks any other incoming requests.

Are you using `time.sleep(...)` in testing or some other synchronous code?

> 
> ...or some other synchronous code?

Ah, yes, I was testing with a synchronous function. In that case a different tack will be needed.
Something like...
```python
loop = asyncio.get_event_loop()
future = await loop.run_in_executor(None, self.background)
```
This seems to work for synchronous cases. So `Response.__call__` could look like...
```python
if self.background is not None:
    task = self.background
    if task.is_coroutine():
        future = asyncio.ensure_future(task())
    else:
        loop = asyncio.get_event_loop()
        future = await loop.run_in_executor(None, task.func)
```



Resolving patch:
diff --git a/starlette/background.py b/starlette/background.py
new file mode 100644
--- /dev/null
+++ b/starlette/background.py
@@ -0,0 +1,17 @@
+import asyncio
+import functools
+
+
+class BackgroundTask:
+    def __init__(self, func, *args, **kwargs):
+        self.func = func
+        self.args = args
+        self.kwargs = kwargs
+
+    async def __call__(self):
+        if asyncio.iscoroutinefunction(self.func):
+            await asyncio.ensure_future(self.func(*self.args, **self.kwargs))
+        else:
+            fn = functools.partial(self.func, *self.args, **self.kwargs)
+            loop = asyncio.get_event_loop()
+            await loop.run_in_executor(None, fn)
diff --git a/starlette/responses.py b/starlette/responses.py
--- a/starlette/responses.py
+++ b/starlette/responses.py
@@ -1,5 +1,6 @@
 from email.utils import formatdate
 from mimetypes import guess_type
+from starlette.background import BackgroundTask
 from starlette.datastructures import MutableHeaders
 from starlette.types import Receive, Send
 from urllib.parse import quote_plus
@@ -41,11 +42,13 @@ def __init__(
         status_code: int = 200,
         headers: dict = None,
         media_type: str = None,
+        background: BackgroundTask = None,
     ) -> None:
         self.body = self.render(content)
         self.status_code = status_code
         if media_type is not None:
             self.media_type = media_type
+        self.background = background
         self.init_headers(headers)
 
     def render(self, content: typing.Any) -> bytes:
@@ -96,6 +99,9 @@ async def __call__(self, receive: Receive, send: Send) -> None:
         )
         await send({"type": "http.response.body", "body": self.body})
 
+        if self.background is not None:
+            await self.background()
+
 
 class HTMLResponse(Response):
     media_type = "text/html"


Test patch:
diff --git a/tests/test_background.py b/tests/test_background.py
new file mode 100644
--- /dev/null
+++ b/tests/test_background.py
@@ -0,0 +1,52 @@
+from starlette.responses import Response
+from starlette.background import BackgroundTask
+from starlette.testclient import TestClient
+import asyncio
+
+
+def test_async_task():
+    async def async_task():
+        count = 0
+        for num in range(3):
+            count += 1
+            await asyncio.sleep(1)
+        return count
+
+    task = BackgroundTask(async_task)
+
+    def app(scope):
+        async def asgi(receive, send):
+            response = Response(
+                "task initiated", media_type="text/plain", background=task
+            )
+            await response(receive, send)
+
+        return asgi
+
+    client = TestClient(app)
+    response = client.get("/")
+    assert response.text == "task initiated"
+
+
+def test_sync_task():
+    def sync_task():
+        num = 500
+        count = 0
+        while count != num:
+            count += 1
+        return count
+
+    task = BackgroundTask(sync_task)
+
+    def app(scope):
+        async def asgi(receive, send):
+            response = Response(
+                "task initiated", media_type="text/plain", background=task
+            )
+            await response(receive, send)
+
+        return asgi
+
+    client = TestClient(app)
+    response = client.get("/")
+    assert response.text == "task initiated"