{ "info": { "author": "Florimond Manca", "author_email": "florimond.manca@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8" ], "description": "# asgi-lifespan\n\n[![Build Status](https://travis-ci.com/florimondmanca/asgi-lifespan.svg?branch=master)](https://travis-ci.com/florimondmanca/asgi-lifespan)\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\nModular components for adding [lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) support to [ASGI] apps and libraries.\n\n**Note**: This project is in an \"alpha\" stage.\n\n[asgi]: https://asgi.readthedocs.io\n\n**Contents**\n\n- [Features](#features)\n- [Installation](#installation)\n- [Usage](#usage)\n - [Adding lifespan to ASGI apps](#adding-lifespan-to-asgi-apps)\n - [Sending lifespan events](#sending-lifespan-events)\n- [API Reference](#api-reference)\n - [`Lifespan`](#lifespan)\n - [`LifespanManager`](#lifespanmanager)\n - [`LifespanMiddleware`](#lifespanmiddleware)\n\n## Features\n\n- Create a lifespan-capable ASGI app with event handler registration support using `Lifespan`.\n- Add lifespan support to an ASGI app using `LifespanMiddleware`.\n- Send lifespan events to an ASGI app (e.g. for testing) using `LifespanManager`.\n- Support for [asyncio], [trio] and [curio] (provided by [anyio]).\n- Fully type-annotated.\n- 100% test coverage.\n\n[asyncio]: https://docs.python.org/3/library/asyncio\n[trio]: https://anyio.readthedocs.io/en/latest/\n[curio]: https://anyio.readthedocs.io/en/latest/\n[anyio]: https://anyio.readthedocs.io\n\n## Installation\n\n```bash\npip install asgi-lifespan\n```\n\n## Usage\n\n### Adding lifespan to ASGI apps\n\n```python\nfrom asgi_lifespan import Lifespan, LifespanMiddleware\n\n\n# 'Lifespan' is a standalone ASGI app.\n# It implements the lifespan protocol,\n# and allows registering lifespan event handlers.\n\nlifespan = Lifespan()\n\n\n@lifespan.on_event(\"startup\")\nasync def startup():\n print(\"Starting up...\")\n\n\n@lifespan.on_event(\"shutdown\")\nasync def shutdown():\n print(\"Shutting down...\")\n\n\n# Sync event handlers and an imperative syntax are supported too.\n\n\ndef more_shutdown():\n print(\"Bye!\")\n\n\nlifespan.add_event_handler(\"shutdown\", more_shutdown)\n\n\n# Example ASGI app. We're using a \"Hello, world\" application here,\n# but any ASGI-compliant callable will do.\n\nasync def app(scope, receive, send):\n assert scope[\"type\"] == \"http\"\n output = b\"Hello, World!\"\n headers = [\n (b\"content-type\", \"text/plain\"),\n (b\"content-length\", str(len(output)))\n ]\n await send(\n {\"type\": \"http.response.start\", \"status\": 200, \"headers\": headers}\n )\n await send({\"type\": \"http.response.body\", \"body\": output})\n\n\n# 'LifespanMiddleware' returns an ASGI app.\n# It forwards lifespan requests to 'lifespan',\n# and anything else goes to 'app'.\n\napp = LifespanMiddleware(app, lifespan=lifespan)\n```\n\nSave this script as `app.py`. You can serve this application with an ASGI server such as [uvicorn]:\n\n[uvicorn]: https://www.uvicorn.org/\n\n```bash\nuvicorn app:app\n```\n\nYou should get the following output:\n\n```console\nINFO: Started server process [2407]\nINFO: Waiting for application startup.\nStarting up...\nINFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\n```\n\nStop the server using `Ctrl+C`, and you should get the following output:\n\n```console\nINFO: Shutting down\nINFO: Waiting for application shutdown.\nShutting down...\nBye!\nINFO: Finished server process [2407]\n```\n\n### Sending lifespan events\n\nTo 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].\n\n[httpx]: https://www.encode.io/httpx/\n\n```python\nfrom asgi_lifespan import Lifespan, LifespanManager\n\n\n# Example lifespan-capable ASGI app.\n# (Doesn't need to be a `Lifespan` instance.\n# Any ASGI app implementing the lifespan protocol will do.)\n\napp = Lifespan()\n\n\n@app.on_event(\"startup\")\nasync def startup():\n print(\"Starting up...\")\n\n\n@app.on_event(\"shutdown\")\nasync def shutdown():\n print(\"Shutting down...\")\n\n\nasync def main():\n async with LifespanManager(app):\n print(\"We're in!\")\n # Maybe make some requests to 'app'\n # using an ASGI-capable test client here?\n```\n\n> **Note**: if `LifespanManager` detects that the lifespan protocol isn't supported, a `LifespanNotSupported` exception is raised.\n\nSave this script as `main.py`. You can run it with any of the supported async libraries:\n\n```python\n# Add one of these at the bottom of 'main.py'.\n\nimport asyncio\nasyncio.run(main())\n\nimport trio\ntrio.run(main)\n\nimport curio\ncurio.run(main)\n```\n\nRun `$ python main.py` in your terminal, and you should get the following output:\n\n```console\nStarting up...\nWe're in!\nShutting down...\n```\n\n## API Reference\n\n### `Lifespan`\n\n```python\ndef __init__(self, on_startup: Callable = None, on_shutdown: Callable = None)\n```\n\nA standalone ASGI app that implements the lifespan protocol and supports registering event handlers.\n\n**Example**\n\n```python\nlifespan = Lifespan()\n```\n\n**Parameters**\n\n- `on_startup` (`Callable`): an optional initial startup event handler.\n- `on_shutdown` (`Callable`): an optional initial shutdown event handler.\n\n#### `add_event_handler`\n\n```python\ndef add_event_handler(self, event_type: str, func: Callable[[], None]) -> None\n```\n\nRegister a callback to be called when the application starts up or shuts down.\n\nImperative version of [`.on_event()`](#on_event).\n\n**Example**\n\n```python\nasync def on_startup():\n ...\n\nlifespan.add_event_handler(\"startup\", on_startup)\n```\n\n**Parameters**\n\n- `event_type` (`str`): one of `\"startup\"` or `\"shutdown\"`.\n- `func` (`Callable`): a callback. Can be sync or async.\n\n#### `on_event`\n\n```python\ndef on_event(self, event_type: str) -> Callable[[], None]\n```\n\nRegister a callback to be called when the application starts up or shuts down.\n\nDecorator version of [`.add_event_handler()`](#add_event_handler).\n\n**Example**\n\n```python\n@lifespan.on_event(\"startup\")\nasync def on_startup():\n ...\n```\n\n**Parameters**\n\n- `event_type` (`str`): one of `\"startup\"` or `\"shutdown\"`.\n\n#### `__call__`\n\n```python\nasync def __call__(self, scope: dict, receive: Callable, send: Callable) -> None\n```\n\nASGI 3 implementation.\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 `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):\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**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### `LifespanMiddleware`\n\n```python\ndef __init__(self, app: Callable, lifespan: Callable)\n```\n\nAn ASGI middleware that forwards \"lifespan\" requests to `lifespan` and anything else to `app`.\n\n**Example**\n\n```python\napp = LifespanMiddleware(app, lifespan=lifespan)\n```\n\nThis is roughly equivalent to:\n\n```python\ndefault = app\n\nasync def app(scope, receive, send):\n if scope[\"type\"] == \"lifespan\":\n await lifespan(scope, receive, send)\n else:\n await default(scope, receive, send)\n```\n\n**Parameters**\n\n- `app` (`Callable`): an ASGI application to be wrapped by the middleware.\n- `lifespan` (`Callable`): an ASGI application.\n - Can be a [`Lifespan`](#lifespan) instance, but that is not mandatory.\n - This will only be given `\"lifespan\"` ASGI scope types, so it is safe (and recommended) to use `assert scope[\"type\"] == \"lifespan\"` in custom implementations.\n\n#### `__call__`\n\n```python\nasync def __call__(self, scope: dict, receive: Callable, send: Callable) -> None\n```\n\nASGI 3 implementation.\n\n## License\n\nMIT\n\n\n# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n## Unreleased\n\n## 0.4.2 (October 6, 2019)\n\n### Fixed\n\n- Ensure `py.typed` is bundled with the package so that type checkers can detect type annotations. (Pull #16)\n\n## 0.4.1 (September 29, 2019)\n\n### Fixed\n\n- Improve error handling in `LifespanManager` (Pull #11):\n - Exceptions raised in the context manager body or during shutdown are now properly propagated.\n - Unsupported lifespan is now also detected when the app calls `send()` before calling having called `receive()` at least once.\n\n## 0.4.0 (September 29, 2019)\n\n- Enter Alpha development status.\n\n## 0.3.1 (September 29, 2019)\n\n### Added\n\n- Add configurable timeouts to `LifespanManager`. (Pull #10)\n\n## 0.3.0 (September 29, 2019)\n\n### Added\n\n- Add `LifespanManager` for sending lifespan events into an ASGI app. (Pull #5)\n\n## 0.2.0 (September 28, 2019)\n\n### Added\n\n- Add `LifespanMiddleware`, an ASGI middleware to add lifespan support to an ASGI app. (Pull #9)\n\n## 0.1.0 (September 28, 2019)\n\n### Added\n\n- Add `Lifespan`, an ASGI app implementing the lifespan protocol with event handler registration support. (Pull #7)\n\n## 0.0.2 (September 28, 2019)\n\n### Fixed\n\n- Installation from PyPI used to fail due to missing `MANIFEST.in`.\n\n## 0.0.1 (September 28, 2019)\n\n### Added\n\n- Empty package.", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/florimondmanca/asgi-lifespan", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "asgi-lifespan", "package_url": "https://pypi.org/project/asgi-lifespan/", "platform": "", "project_url": "https://pypi.org/project/asgi-lifespan/", "project_urls": { "Homepage": "https://github.com/florimondmanca/asgi-lifespan" }, "release_url": "https://pypi.org/project/asgi-lifespan/0.4.2/", "requires_dist": null, "requires_python": ">=3.6", "summary": "Lifespan protocol support for ASGI apps and libraries.", "version": "0.4.2" }, "last_serial": 5936083, "releases": { "0.0.1": [ { "comment_text": "", "digests": { "md5": "612fc468278d18ea18c4af7ea3ec6d6d", "sha256": "e970d0b21696fcebda2ad7c3151ee1f3105c6ce4e976cec2fae8501f3b85f523" }, "downloads": -1, "filename": "asgi-lifespan-0.0.1.tar.gz", "has_sig": false, "md5_digest": "612fc468278d18ea18c4af7ea3ec6d6d", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 4108, "upload_time": "2019-09-28T12:55:44", "url": "https://files.pythonhosted.org/packages/4d/4f/1d49ee3124924f7f0d5f47ffa1284a4f79c1f3c3832cc040000e214aa0e1/asgi-lifespan-0.0.1.tar.gz" } ], "0.0.2": [ { "comment_text": "", "digests": { "md5": "392791f487a6c61dc50e7f659984e6f5", "sha256": "b2743c5caa7690ca207cbf616f32ed047dc1580923b50a1bf9c1dcdb9a5dbbba" }, "downloads": -1, "filename": "asgi-lifespan-0.0.2.tar.gz", "has_sig": false, "md5_digest": "392791f487a6c61dc50e7f659984e6f5", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 4927, "upload_time": "2019-09-28T12:59:45", "url": "https://files.pythonhosted.org/packages/cd/ac/140b800d5280342d5a09e29c2a62c6e63738bfc884270f93ba5ba2a84c83/asgi-lifespan-0.0.2.tar.gz" } ], "0.1.0": [ { "comment_text": "", "digests": { "md5": "52c6156e26fc53b974aa54b9256b6f75", "sha256": "0ee51a5cdee5ba3e3ad18379355b9bfc5f9e425043bf7f338e10c67b118d17c1" }, "downloads": -1, "filename": "asgi-lifespan-0.1.0.tar.gz", "has_sig": false, "md5_digest": "52c6156e26fc53b974aa54b9256b6f75", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 6048, "upload_time": "2019-09-28T18:35:08", "url": "https://files.pythonhosted.org/packages/cf/92/25f8aaf365835cbfa65980e87d1506ebb0f03f1974c2fb090dd1bc4dbe43/asgi-lifespan-0.1.0.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "b6c6bc8e1fa100c0498c6a66690fd017", "sha256": "b683710fd768c9a52a526bd49fb846dc6c6c623fb3f3f6aa54b243fa491b5ed2" }, "downloads": -1, "filename": "asgi-lifespan-0.2.0.tar.gz", "has_sig": false, "md5_digest": "b6c6bc8e1fa100c0498c6a66690fd017", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 6668, "upload_time": "2019-09-28T20:28:16", "url": "https://files.pythonhosted.org/packages/bd/8d/bbb8e0bc6101a63a41bf759f1b24418042d63b7948c8e0e183ddb783604c/asgi-lifespan-0.2.0.tar.gz" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "6e18fc6455a95f93e83a033f7adc2c80", "sha256": "80d6c50dd9081b39189f0e661c07f50cae73394fcb37924e67fc814a8ec2f183" }, "downloads": -1, "filename": "asgi-lifespan-0.3.0.tar.gz", "has_sig": false, "md5_digest": "6e18fc6455a95f93e83a033f7adc2c80", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 9498, "upload_time": "2019-09-28T22:19:42", "url": "https://files.pythonhosted.org/packages/6a/35/137628321659640bdefe9290c22941590d915ae283b0bfcf6463d9bc7f9c/asgi-lifespan-0.3.0.tar.gz" } ], "0.3.1": [ { "comment_text": "", "digests": { "md5": "11e6869f519f9655e868cfa2397f9b55", "sha256": "e163f1de8650d2dbbbbb683797cc91f3df66cc009f01e1ec8595e3b2be4a7c00" }, "downloads": -1, "filename": "asgi-lifespan-0.3.1.tar.gz", "has_sig": false, "md5_digest": "11e6869f519f9655e868cfa2397f9b55", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 9905, "upload_time": "2019-09-28T22:58:55", "url": "https://files.pythonhosted.org/packages/ad/66/92d8cb4a73cecaa0ba00b379953a40d4c7acfb24110302b0d4b020ea74d5/asgi-lifespan-0.3.1.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "814ed665909ddd87cabcb955dce907f0", "sha256": "28b487d59538338b0d9861764441540fa030c22da7e1b5f0ae672485265032ef" }, "downloads": -1, "filename": "asgi-lifespan-0.4.0.tar.gz", "has_sig": false, "md5_digest": "814ed665909ddd87cabcb955dce907f0", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 9979, "upload_time": "2019-09-28T23:06:42", "url": "https://files.pythonhosted.org/packages/16/27/a35bed5558192663d074bfcdd1b45b2b7bc383aeeefabd92d009cfea4f75/asgi-lifespan-0.4.0.tar.gz" } ], "0.4.1": [ { "comment_text": "", "digests": { "md5": "fadd97d00d64c442330753a6daefaf1a", "sha256": "f22cf83121313f3eafe8220fa2e86d8e35f9cdd0ef564f77882e360484ddf1db" }, "downloads": -1, "filename": "asgi-lifespan-0.4.1.tar.gz", "has_sig": false, "md5_digest": "fadd97d00d64c442330753a6daefaf1a", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 10405, "upload_time": "2019-09-28T23:24:30", "url": "https://files.pythonhosted.org/packages/e0/d9/10b99ab8d76b6cc5608a4d10c55d1b160a61ccaec3498cc890f8b1bd684d/asgi-lifespan-0.4.1.tar.gz" } ], "0.4.2": [ { "comment_text": "", "digests": { "md5": "448ff277e9fceac1ad8e6d488a1e5e9e", "sha256": "6ae6cf8d35e7b8d1cee878ed749898c47eb191d392a5fe3a4c71586dd3cec4fe" }, "downloads": -1, "filename": "asgi-lifespan-0.4.2.tar.gz", "has_sig": false, "md5_digest": "448ff277e9fceac1ad8e6d488a1e5e9e", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 10559, "upload_time": "2019-10-06T21:38:25", "url": "https://files.pythonhosted.org/packages/5b/9c/bf6c9bc2b347daeb6108e516ac73a422a544e9d3002892d4a28f3d44b2b8/asgi-lifespan-0.4.2.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "448ff277e9fceac1ad8e6d488a1e5e9e", "sha256": "6ae6cf8d35e7b8d1cee878ed749898c47eb191d392a5fe3a4c71586dd3cec4fe" }, "downloads": -1, "filename": "asgi-lifespan-0.4.2.tar.gz", "has_sig": false, "md5_digest": "448ff277e9fceac1ad8e6d488a1e5e9e", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 10559, "upload_time": "2019-10-06T21:38:25", "url": "https://files.pythonhosted.org/packages/5b/9c/bf6c9bc2b347daeb6108e516ac73a422a544e9d3002892d4a28f3d44b2b8/asgi-lifespan-0.4.2.tar.gz" } ] }