Skip to main content

Lifespan protocol support for ASGI apps and libraries.

Project description

asgi-lifespan

Build Status Coverage Package version

Modular components for adding lifespan protocol support to ASGI apps and libraries.

Contents

Features

  • Create a lifespan-capable ASGI app with event handler registration support using Lifespan.
  • Add lifespan support to an ASGI app using LifespanMiddleware.
  • Send lifespan events to an ASGI app (e.g. for testing) using LifespanManager.
  • Support for asyncio and trio.
  • No hard dependencies.
  • Fully type-annotated.
  • 100% test coverage.

Installation

pip install asgi-lifespan

Usage

Adding lifespan to ASGI apps

from asgi_lifespan import Lifespan, LifespanMiddleware


# 'Lifespan' is a standalone ASGI app.
# It implements the lifespan protocol,
# and allows registering lifespan event handlers.

lifespan = Lifespan()


@lifespan.on_event("startup")
async def startup():
    print("Starting up...")


@lifespan.on_event("shutdown")
async def shutdown():
    print("Shutting down...")


# Sync event handlers and an imperative syntax are supported too.


def more_shutdown():
    print("Bye!")


lifespan.add_event_handler("shutdown", more_shutdown)


# Example ASGI app. We're using a "Hello, world" application here,
# but any ASGI-compliant callable will do.

async def app(scope, receive, send):
    assert scope["type"] == "http"
    output = b"Hello, World!"
    headers = [
        (b"content-type", "text/plain"),
        (b"content-length", str(len(output)))
    ]
    await send(
        {"type": "http.response.start", "status": 200, "headers": headers}
    )
    await send({"type": "http.response.body", "body": output})


# 'LifespanMiddleware' returns an ASGI app.
# It forwards lifespan requests to 'lifespan',
# and anything else goes to 'app'.

app = LifespanMiddleware(app, lifespan=lifespan)

Save this script as app.py. You can serve this application with an ASGI server such as uvicorn:

uvicorn app:app

You should get the following output:

INFO: Started server process [2407]
INFO: Waiting for application startup.
Starting up...
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Stop the server using Ctrl+C, and you should get the following output:

INFO: Shutting down
INFO: Waiting for application shutdown.
Shutting down...
Bye!
INFO: Finished server process [2407]

Sending lifespan events

To programmatically send ASGI lifespan events to an ASGI app, use LifespanManager. This is particularly useful for testing and/or making requests using an ASGI-capable HTTP client such as HTTPX.

from asgi_lifespan import Lifespan, LifespanManager


# Example lifespan-capable ASGI app.
# (Doesn't need to be a `Lifespan` instance.
# Any ASGI app implementing the lifespan protocol will do.)

app = Lifespan()


@app.on_event("startup")
async def startup():
    print("Starting up...")


@app.on_event("shutdown")
async def shutdown():
    print("Shutting down...")


async def main():
    async with LifespanManager(app):
        print("We're in!")
        # Maybe make some requests to 'app'
        # using an ASGI-capable test client here?

Note: if LifespanManager detects that the lifespan protocol isn't supported, a LifespanNotSupported exception is raised.

Save this script as main.py. You can run it with any of the supported async libraries:

# Add one of these at the bottom of 'main.py'.
# asgi-lifespan will auto-detect the async library in use.

import asyncio
asyncio.run(main())

import trio
trio.run(main)

Run $ python main.py in your terminal, and you should get the following output:

Starting up...
We're in!
Shutting down...

API Reference

Lifespan

def __init__(self, on_startup: Callable = None, on_shutdown: Callable = None)

A standalone ASGI app that implements the lifespan protocol and supports registering event handlers.

Example

lifespan = Lifespan()

Parameters

  • on_startup (Callable): an optional initial startup event handler.
  • on_shutdown (Callable): an optional initial shutdown event handler.

add_event_handler

def add_event_handler(self, event_type: str, func: Callable[[], None]) -> None

Register a callback to be called when the application starts up or shuts down.

Imperative version of .on_event().

Example

async def on_startup():
    ...

lifespan.add_event_handler("startup", on_startup)

Parameters

  • event_type (str): one of "startup" or "shutdown".
  • func (Callable): a callback. Can be sync or async.

on_event

def on_event(self, event_type: str) -> Callable[[], None]

Register a callback to be called when the application starts up or shuts down.

Decorator version of .add_event_handler().

Example

@lifespan.on_event("startup")
async def on_startup():
    ...

Parameters

  • event_type (str): one of "startup" or "shutdown".

__call__

async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None

ASGI 3 implementation.

LifespanManager

def __init__(
    self,
    app: Callable,
    startup_timeout: Optional[float] = 5,
    shutdown_timeout: Optional[float] = 5,
)

An asynchronous context manager that starts up an ASGI app on enter and shuts it down on exit.

More precisely:

  • On enter, start a lifespan request to app in the background, then send the lifespan.startup event and wait for the application to send lifespan.startup.complete.
  • On exit, send lifespan.shutdown event and wait for the application to send lifespan.shutdown.complete.
  • If an exception occurs during startup, shutdown, or in the body of the async with block, it bubbles up and no shutdown is performed.

Example

async with LifespanManager(app):
    # 'app' was started up.
    ...

# 'app' was shut down.

Parameters

  • app (Callable): an ASGI application.
  • startup_timeout (Optional[float], defaults to 5): maximum number of seconds to wait for the application to startup. Use None for no timeout.
  • shutdown_timeout (Optional[float], defaults to 5): maximum number of seconds to wait for the application to shutdown. Use None for no timeout.

Raises

  • LifespanNotSupported: if the application does not seem to support the lifespan protocol. Based on the rationale that if the app supported the lifespan protocol then it would successfully receive the lifespan.startup ASGI event, unsupported lifespan protocol is detected in two situations:
    • The application called send() before calling receive() for the first time.
    • The application raised an exception during startup before making its first call to receive(). For example, this may be because the application failed on a statement such as assert scope["type"] == "http".
  • TimeoutError: if startup or shutdown timed out.
  • Exception: any exception raised by the application (during startup, shutdown, or within the async with body) that does not indicate it does not support the lifespan protocol.

LifespanMiddleware

def __init__(self, app: Callable, lifespan: Callable)

An ASGI middleware that forwards "lifespan" requests to lifespan and anything else to app.

Example

app = LifespanMiddleware(app, lifespan=lifespan)

This is roughly equivalent to:

default = app

async def app(scope, receive, send):
    if scope["type"] == "lifespan":
        await lifespan(scope, receive, send)
    else:
        await default(scope, receive, send)

Parameters

  • app (Callable): an ASGI application to be wrapped by the middleware.
  • lifespan (Callable): an ASGI application.
    • Can be a Lifespan instance, but that is not mandatory.
    • This will only be given "lifespan" ASGI scope types, so it is safe (and recommended) to use assert scope["type"] == "lifespan" in custom implementations.

__call__

async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None

ASGI 3 implementation.

License

MIT

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

Unreleased

0.5.0 (November 29, 2019)

  • Enter Beta development status.

Removed

  • Remove curio support. (Pull #18)

Added

  • Ship binary distributions (wheels) alongside source distributions.

Changed

  • Use custom concurrency backends instead of anyio for asyncio and trio support. (Pull #18)

0.4.2 (October 6, 2019)

Fixed

  • Ensure py.typed is bundled with the package so that type checkers can detect type annotations. (Pull #16)

0.4.1 (September 29, 2019)

Fixed

  • Improve error handling in LifespanManager (Pull #11):
    • Exceptions raised in the context manager body or during shutdown are now properly propagated.
    • Unsupported lifespan is now also detected when the app calls send() before calling having called receive() at least once.

0.4.0 (September 29, 2019)

  • Enter Alpha development status.

0.3.1 (September 29, 2019)

Added

  • Add configurable timeouts to LifespanManager. (Pull #10)

0.3.0 (September 29, 2019)

Added

  • Add LifespanManager for sending lifespan events into an ASGI app. (Pull #5)

0.2.0 (September 28, 2019)

Added

  • Add LifespanMiddleware, an ASGI middleware to add lifespan support to an ASGI app. (Pull #9)

0.1.0 (September 28, 2019)

Added

  • Add Lifespan, an ASGI app implementing the lifespan protocol with event handler registration support. (Pull #7)

0.0.2 (September 28, 2019)

Fixed

  • Installation from PyPI used to fail due to missing MANIFEST.in.

0.0.1 (September 28, 2019)

Added

  • Empty package.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

asgi-lifespan-0.5.0.tar.gz (14.7 kB view details)

Uploaded Source

Built Distribution

asgi_lifespan-0.5.0-py3-none-any.whl (12.5 kB view details)

Uploaded Python 3

File details

Details for the file asgi-lifespan-0.5.0.tar.gz.

File metadata

  • Download URL: asgi-lifespan-0.5.0.tar.gz
  • Upload date:
  • Size: 14.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.2.0 requests-toolbelt/0.9.1 tqdm/4.39.0 CPython/3.8.0

File hashes

Hashes for asgi-lifespan-0.5.0.tar.gz
Algorithm Hash digest
SHA256 63f1c4b9c50483b549740a07e6d74eb86c322c74c8da861ef88531cc26cdd645
MD5 febbbfa835e7a031658a2fc601c00617
BLAKE2b-256 62b7aea72177669bac07d68c00e91675ace8d43cf84f28cec589d2f68c908cc8

See more details on using hashes here.

File details

Details for the file asgi_lifespan-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: asgi_lifespan-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 12.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.2.0 requests-toolbelt/0.9.1 tqdm/4.39.0 CPython/3.8.0

File hashes

Hashes for asgi_lifespan-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9bb89d6c54256c07e5cd15ddb7a88e2bf76d3f9c7540a3a8a20c1de1f7b891b8
MD5 6bf7a189ad0699dd4d4dd73513540311
BLAKE2b-256 3854b8a3f896cc19c02d67102dc4507cf9458b04da21bd72cccedab4baf7baa4

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page