{"id":13634515,"url":"https://github.com/florimondmanca/asgi-lifespan","last_synced_at":"2025-05-16T02:07:16.987Z","repository":{"id":45596311,"uuid":"211390375","full_name":"florimondmanca/asgi-lifespan","owner":"florimondmanca","description":"Programmatic startup/shutdown of ASGI apps.","archived":false,"fork":false,"pushed_at":"2023-10-30T15:31:42.000Z","size":128,"stargazers_count":227,"open_issues_count":3,"forks_count":12,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-05-10T15:18:07.920Z","etag":null,"topics":["asgi","async","python"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/asgi-lifespan","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/florimondmanca.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-09-27T19:42:10.000Z","updated_at":"2025-05-02T20:42:24.000Z","dependencies_parsed_at":"2024-01-16T23:26:24.984Z","dependency_job_id":"ba6b1390-1172-481f-90fa-9f47a0f387d4","html_url":"https://github.com/florimondmanca/asgi-lifespan","commit_stats":{"total_commits":87,"total_committers":7,"mean_commits":"12.428571428571429","dds":"0.13793103448275867","last_synced_commit":"fcb318f2f1c41302f73464daa74478289d6731f4"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/florimondmanca%2Fasgi-lifespan","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/florimondmanca%2Fasgi-lifespan/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/florimondmanca%2Fasgi-lifespan/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/florimondmanca%2Fasgi-lifespan/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/florimondmanca","download_url":"https://codeload.github.com/florimondmanca/asgi-lifespan/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254453652,"owners_count":22073617,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asgi","async","python"],"created_at":"2024-08-01T23:01:06.711Z","updated_at":"2025-05-16T02:07:16.956Z","avatar_url":"https://github.com/florimondmanca.png","language":"Python","funding_links":[],"categories":["Testing","Others"],"sub_categories":["Tutorials"],"readme":"# asgi-lifespan\n\n[![Build Status](https://dev.azure.com/florimondmanca/public/_apis/build/status/florimondmanca.asgi-lifespan?branchName=master)](https://dev.azure.com/florimondmanca/public/_build?definitionId=12)\n[![Coverage](https://codecov.io/gh/florimondmanca/asgi-lifespan/branch/master/graph/badge.svg)](https://codecov.io/gh/florimondmanca/asgi-lifespan)\n[![Package version](https://badge.fury.io/py/asgi-lifespan.svg)](https://pypi.org/project/asgi-lifespan)\n\nProgrammatically send startup/shutdown [lifespan](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) events into [ASGI](https://asgi.readthedocs.io) applications. When used in combination with an ASGI-capable HTTP client such as [HTTPX](https://www.python-httpx.org), this allows mocking or testing ASGI applications without having to spin up an ASGI server.\n\n## Features\n\n- Send lifespan events to an ASGI app using `LifespanManager`.\n- Support for [`asyncio`](https://docs.python.org/3/library/asyncio) and [`trio`](https://trio.readthedocs.io).\n- Fully type-annotated.\n- 100% test coverage.\n\n## Installation\n\n```bash\npip install 'asgi-lifespan==2.*'\n```\n\n## Usage\n\n`asgi-lifespan` provides a `LifespanManager` to 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.\n\n`LifespanManager` can run on either `asyncio` or `trio`, and will auto-detect the async library in use.\n\n### Basic usage\n\n```python\n# example.py\nfrom contextlib import asynccontextmanager\nfrom asgi_lifespan import LifespanManager\nfrom starlette.applications import Starlette\n\n# Example lifespan-capable ASGI app. Any ASGI app that supports\n# the lifespan protocol will do, e.g. FastAPI, Quart, Responder, ...\n\n@asynccontextmanager\nasync def lifespan(app):\n    print(\"Starting up!\")\n    yield\n    print(\"Shutting down!\")\n\napp = Starlette(lifespan=lifespan)\n\nasync def main():\n    async with LifespanManager(app) as manager:\n        print(\"We're in!\")\n\n# On asyncio:\nimport asyncio; asyncio.run(main())\n\n# On trio:\n# import trio; trio.run(main)\n```\n\nOutput:\n\n```console\n$ python example.py\nStarting up!\nWe're in!\nShutting down!\n```\n\n### Sending lifespan events for testing\n\nThe example below demonstrates how to use `asgi-lifespan` in conjunction with [HTTPX](https://www.python-httpx.org) and `pytest` in order to send test requests into an ASGI app.\n\n- Install dependencies:\n\n```\npip install asgi-lifespan httpx starlette pytest pytest-asyncio\n```\n\n- Test script:\n\n```python\n# test_app.py\nfrom contextlib import asynccontextmanager\nimport httpx\nimport pytest\nimport pytest_asyncio\nfrom asgi_lifespan import LifespanManager\nfrom starlette.applications import Starlette\nfrom starlette.responses import PlainTextResponse\nfrom starlette.routing import Route\n\n\n@pytest_asyncio.fixture\nasync def app():\n    @asynccontextmanager\n    async def lifespan(app):\n        print(\"Starting up\")\n        yield\n        print(\"Shutting down\")\n\n    async def home(request):\n        return PlainTextResponse(\"Hello, world!\")\n\n    app = Starlette(\n        routes=[Route(\"/\", home)],\n        lifespan=lifespan,\n    )\n\n    async with LifespanManager(app) as manager:\n        print(\"We're in!\")\n        yield manager.app\n\n\n@pytest_asyncio.fixture\nasync def client(app):\n    async with httpx.AsyncClient(app=app, base_url=\"http://app.io\") as client:\n        print(\"Client is ready\")\n        yield client\n\n\n@pytest.mark.asyncio\nasync def test_home(client):\n    print(\"Testing\")\n    response = await client.get(\"/\")\n    assert response.status_code == 200\n    assert response.text == \"Hello, world!\"\n    print(\"OK\")\n```\n\n- Run the test suite:\n\n```console\n$ pytest -s test_app.py\n======================= test session starts =======================\n\ntest_app.py Starting up\nWe're in!\nClient is ready\nTesting\nOK\n.Shutting down\n\n======================= 1 passed in 0.88s =======================\n```\n\n### Accessing state\n\n`LifespanManager` provisions a [lifespan state](https://asgi.readthedocs.io/en/latest/specs/lifespan.html#lifespan-state) which persists data from the lifespan cycle for use in request/response handling.\n\nFor your app to be aware of it, be sure to use `manager.app` instead of the `app` itself when inside the context manager.\n\nFor example if using HTTPX as an async test client:\n\n```python\nasync with LifespanManager(app) as manager:\n    async with httpx.AsyncClient(app=manager.app) as client:\n        ...\n```\n\n## API Reference\n\n### `LifespanManager`\n\n```python\ndef __init__(\n    self,\n    app: Callable,\n    startup_timeout: Optional[float] = 5,\n    shutdown_timeout: Optional[float] = 5,\n)\n```\n\nAn [asynchronous context manager](https://docs.python.org/3/reference/datamodel.html#async-context-managers) that starts up an ASGI app on enter and shuts it down on exit.\n\nMore precisely:\n\n- 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`.\n- On exit, send the `lifespan.shutdown` event and wait for the application to send `lifespan.shutdown.complete`.\n- If an exception occurs during startup, shutdown, or in the body of the `async with` block, it bubbles up and no shutdown is performed.\n\n**Example**\n\n```python\nasync with LifespanManager(app) as manager:\n    # 'app' was started up.\n    ...\n\n# 'app' was shut down.\n```\n\n**Parameters**\n\n- `app` (`Callable`): an ASGI application.\n- `startup_timeout` (`Optional[float]`, defaults to 5): maximum number of seconds to wait for the application to startup. Use `None` for no timeout.\n- `shutdown_timeout` (`Optional[float]`, defaults to 5): maximum number of seconds to wait for the application to shutdown. Use `None` for no timeout.\n\n**Yields**\n\n- `manager` (`LifespanManager`): the `LifespanManager` itself. In case you use [lifespan state](https://asgi.readthedocs.io/en/latest/specs/lifespan.html#lifespan-state), use `async with LifespanManager(app) as manager: ...` then access `manager.app` to get a reference to the state-aware app.\n\n**Raises**\n\n- `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:\n  - The application called `send()` before calling `receive()` for the first time.\n  - 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\"`.\n- `TimeoutError`: if startup or shutdown timed out.\n- `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.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflorimondmanca%2Fasgi-lifespan","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflorimondmanca%2Fasgi-lifespan","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflorimondmanca%2Fasgi-lifespan/lists"}