Package: asgi-lifespan
asgi-lifespanProgrammatically send startup/shutdownlifespanevents intoASGIapplications. When used in combination with an ASGI-capable HTTP client such asHTTPX, this allows mocking or testing ASGI applications without having to spin up an ASGI server.FeaturesSend lifespan events to an ASGI app usingLifespanManager.Support forasyncioandtrio.Fully type-annotated.100% test coverage.Installationpipinstall'asgi-lifespan==2.*'Usageasgi-lifespanprovides aLifespanManagerto programmatically send ASGI lifespan events into an ASGI app. This can be used to programmatically startup/shutdown an ASGI app without having to spin up an ASGI server.LifespanManagercan run on eitherasyncioortrio, and will auto-detect the async library in use.Basic usage# example.pyfromcontextlibimportasynccontextmanagerfromasgi_lifespanimportLifespanManagerfromstarlette.applicationsimportStarlette# Example lifespan-capable ASGI app. Any ASGI app that supports# the lifespan protocol will do, e.g. FastAPI, Quart, Responder, ...@asynccontextmanagerasyncdeflifespan(app):print("Starting up!")yieldprint("Shutting down!")app=Starlette(lifespan=lifespan)asyncdefmain():asyncwithLifespanManager(app)asmanager:print("We're in!")# On asyncio:importasyncio;asyncio.run(main())# On trio:# import trio; trio.run(main)Output:$pythonexample.pyStarting up!We're in!Shutting down!Sending lifespan events for testingThe example below demonstrates how to useasgi-lifespanin conjunction withHTTPXandpytestin order to send test requests into an ASGI app.Install dependencies:pip install asgi-lifespan httpx starlette pytest pytest-asyncioTest script:# test_app.pyfromcontextlibimportasynccontextmanagerimporthttpximportpytestimportpytest_asynciofromasgi_lifespanimportLifespanManagerfromstarlette.applicationsimportStarlettefromstarlette.responsesimportPlainTextResponsefromstarlette.routingimportRoute@pytest_asyncio.fixtureasyncdefapp():@asynccontextmanagerasyncdeflifespan(app):print("Starting up")yieldprint("Shutting down")asyncdefhome(request):returnPlainTextResponse("Hello, world!")app=Starlette(routes=[Route("/",home)],lifespan=lifespan,)asyncwithLifespanManager(app)asmanager:print("We're in!")yieldmanager.app@pytest_asyncio.fixtureasyncdefclient(app):asyncwithhttpx.AsyncClient(app=app,base_url="http://app.io")asclient:print("Client is ready")yieldclient@pytest.mark.asyncioasyncdeftest_home(client):print("Testing")response=awaitclient.get("/")assertresponse.status_code==200assertresponse.text=="Hello, world!"print("OK")Run the test suite:$pytest-stest_app.py======================= test session starts =======================test_app.py Starting upWe're in!Client is readyTestingOK.Shutting down======================= 1 passed in 0.88s =======================Accessing stateLifespanManagerprovisions alifespan statewhich persists data from the lifespan cycle for use in request/response handling.For your app to be aware of it, be sure to usemanager.appinstead of theappitself when inside the context manager.For example if using HTTPX as an async test client:asyncwithLifespanManager(app)asmanager:asyncwithhttpx.AsyncClient(app=manager.app)asclient:...API ReferenceLifespanManagerdef__init__(self,app:Callable,startup_timeout:Optional[float]=5,shutdown_timeout:Optional[float]=5,)Anasynchronous context managerthat starts up an ASGI app on enter and shuts it down on exit.More precisely:On enter, start alifespanrequest toappin the background, then send thelifespan.startupevent and wait for the application to sendlifespan.startup.complete.On exit, send thelifespan.shutdownevent and wait for the application to sendlifespan.shutdown.complete.If an exception occurs during startup, shutdown, or in the body of theasync withblock, it bubbles up and no shutdown is performed.ExampleasyncwithLifespanManager(app)asmanager:# 'app' was started up....# 'app' was shut down.Parametersapp(Callable): an ASGI application.startup_timeout(Optional[float], defaults to 5): maximum number of seconds to wait for the application to startup. UseNonefor no timeout.shutdown_timeout(Optional[float], defaults to 5): maximum number of seconds to wait for the application to shutdown. UseNonefor no timeout.Yieldsmanager(LifespanManager): theLifespanManageritself. In case you uselifespan state, useasync with LifespanManager(app) as manager: ...then accessmanager.appto get a reference to the state-aware app.RaisesLifespanNotSupported: 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 thelifespan.startupASGI event, unsupported lifespan protocol is detected in two situations:The application calledsend()before callingreceive()for the first time.The application raised an exception during startup before making its first call toreceive(). For example, this may be because the application failed on a statement such asassert scope["type"] == "http".TimeoutError: if startup or shutdown timed out.Exception: any exception raised by the application (during startup, shutdown, or within theasync withbody) that does not indicate it does not support the lifespan protocol.LicenseMITChangelogAll notable changes to this project will be documented in this file.The format is based onKeep a Changelog.2.1.0 - 2023-03-28AddedAdd support for lifespan state. (Pull #59)2.0.0 - 2022-11-11RemovedDrop support for Python 3.6. (Pull #55)AddedAdd official support for Python 3.11. (Pull #55)Add official support for Python 3.9 and 3.10. (Pull #46 - Thanks @euri10)FixedEnsure compatibility with mypy 0.990+, which madeno_implicit_optionalthe default. (Pull #53 - Thanks @AllSeeingEyeTolledEweSew)1.0.1 - 2020-06-08FixedUpdate development status to5 - Production/Stable. (Pull #32)1.0.0 - 2020-02-02RemovedDropLifespanandLifespanMiddleware. Please use Starlette's built-in lifespan capabilities instead. (Pull #27)FixedUsesniffiofor auto-detecting the async environment. (Pull #28)Enforce 100% test coverage on CI. (Pull #29)ChangedEnforce importing from the top-level package by switching to private internal modules. (Pull #26)0.6.0 - 2019-11-29ChangedMoveLifespanto thelifespanmodule. (Pull #21)RefactorLifespanManagerto drop dependency onasynccontextmanageron 3.6. (Pull #20)0.5.0 - 2019-11-29Enter Beta development status.RemovedRemovecuriosupport. (Pull #18)AddedShip binary distributions (wheels) alongside source distributions.ChangedUse custom concurrency backends instead ofanyiofor asyncio and trio support. (Pull #18)0.4.2 - 2019-10-06FixedEnsurepy.typedis bundled with the package so that type checkers can detect type annotations. (Pull #16)0.4.1 - 2019-09-29FixedImprove error handling inLifespanManager(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 callssend()before calling having calledreceive()at least once.0.4.0 - 2019-09-29Enter Alpha development status.0.3.1 - 2019-09-29AddedAdd configurable timeouts toLifespanManager. (Pull #10)0.3.0 - 2019-09-29AddedAddLifespanManagerfor sending lifespan events into an ASGI app. (Pull #5)0.2.0 - 2019-09-28AddedAddLifespanMiddleware, an ASGI middleware to add lifespan support to an ASGI app. (Pull #9)0.1.0 - 2019-09-28AddedAddLifespan, an ASGI app implementing the lifespan protocol with event handler registration support. (Pull #7)0.0.2 - 2019-09-28FixedInstallation from PyPI used to fail due to missingMANIFEST.in.0.0.1 - 2019-09-28AddedEmpty package.