![Tests](https://github.com/JGoutin/aio_lambda_api/workflows/tests/badge.svg)
[![codecov](https://codecov.io/gh/JGoutin/aio_lambda_api/branch/main/graph/badge.svg?token=52KXxxDFhx)](https://codecov.io/gh/JGoutin/aio_lambda_api)
[![PyPI](https://img.shields.io/pypi/v/aio_lambda_api.svg)](https://pypi.org/project/aio_lambda_api)
A lightweight AsyncIO HTTP API for serverless functions like AWS lambda.
Features:
* Asyncio in AWS lambda.
* FastAPI inspired routing, parameters and exception handling.
* Detailed JSON formatted access log.
* X-Request-ID header support (Including in logs).
* Configurable request timeout.
* Optional input validation using Pydantic.
* Optional JSON serialization/deserialization speedup with Orjson.
* Optional speedups using accelerated libraries (Like UVloop and ORJson).
Supported backends:
* AWS Lambda
* Request/responses are in API Gateway format (Only the format is required,
but can be triggered without using the API Gateway service).
* Support batches or requests with AWS SQS queue, AWS SNS or AWS MQ.
* JSON access logs works well with AWS Cloudwatch Insight.
Not supported yet:
* Routes with variables (Like `"/items/{item_id}"`).
* Query strings.
* Pydantic models as response or request body.
* More backends.
* AWS Lambda backend: AWS SSM parameter store helper.
## Usage
### Usage with AWS lambda
Function code example (`app.py`):
```python
from aio_lambda_api import Handler
handler = Handler()
@handler.get("/")
def read_root():
return {"Hello": "World"}
```
AWS lambda function handler must be configured to `app.handler`.
### Routing
The `aio_lambda_api.Handler` class provides decorators to configure routes for each
HTTP method:
* `Handler.get()`: GET.
* `Handler.head()`: HEAD.
* `Handler.post()`: POST.
* `Handler.put()`: PUT.
* `Handler.patch()`: PATCH.
* `Handler.delete()`: DELETE.
* `Handler.options()`: OPTIONS.
For all decorators, the first arguments is the HTTP path and is required.
The decorated function is executed when the defined HTTP path and method matches.
By default, the body of the request is parsed as JSON and injected in the function as
arguments.
If Pydantic is installed, parameters are validated against arguments types annotations.
The decorated function must return a JSON serializable object or `None`. If the function
returns `None`, the returned status code is automatically set to `204`.
### Exception handling
It is possible to trigger a response using the `aio_lambda_api.HTTPException` as follow:
```python
from aio_lambda_api import Handler, HTTPException
handler = Handler()
items = {"foo": "The Foo Wrestlers"}
@handler.get("/item")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
```
If an exception is risen in routes functions, the behavior is the following:
* `aio_lambda_api.HTTPException`: Converted to HTTP response with the
specified body and returns code.
* `pydantic.ValidationError`: Converted to 422 HTTP error response with Pydantic error
details as body.
* Other exceptions: Reraised. Callers using the Lambda API will
be able to analyse the error like any other Python lambda error
(With `errorType`, `errorMessage` and `stackTrace`)
Callers using an HTTP endpoint/API gateway will receive a simple 500 error with no
details.
### Customizing responses
#### Return code
It is possible to select the return code (when no exception occurs) using the
`status_code` argument. If not specified `200` is used.
```python
from aio_lambda_api import Handler
handler = Handler()
@handler.get("/", status_code=201)
def read_root():
return {"Hello": "World"}
```
#### Headers
It is possible to configure headers by using the `Response` object from arguments:
```python
from aio_lambda_api import Handler, Response
handler = Handler()
@handler.get("/")
async def read_item(response: Response):
response.headers["Cache-Control"] = "no-cache"
return {"Hello": "World"}
```
#### Custom response
It is possible to fully configure the response by returning the `Response` object.
```python
from aio_lambda_api import Handler, Response
handler = Handler()
@handler.get("/")
async def read_item():
return Response(
status_code=202,
media_type="application/octet-stream",
content=b"helloworld"
)
```
The default `Response` class accept `str` or `bytes` as content.
The `JSONResponse` object is also available, it is the default response object when not
explicitly set.
It is possible to create a subclass of `Response` class to have a custom behavior. The
`Response.render` method is responsible for the serialization of the response.
Note: If a response class returns a `bytes` content after `Response.render`, this
content will be base64 encoded automatically in the API Gateway compatible response
returned.
### Accessing Request data
It is possible to access request data by using the `Request` object from arguments:
```python
from aio_lambda_api import Handler, Request
handler = Handler()
@handler.get("/")
async def read_item(request: Request):
user_agent = request.headers["user-agent"]
return {"Hello": user_agent}
```
Note: All headers keys are lowercase in the `Request` object.
### Logging
An access log is automatically generated. This access log is in JSON format (`dict` in
the code). With AWS lambda, the logs will appear with other lambda logs in Cloudwatch
logs. The JSON format make them very easy to query in Cloudwatch Insight.
All request and all exceptions from routes functions are logged in the access log
(Including reraised 500 errors.)
When raising `aio_lambda_api.HTTPException`, it is possible to show extra information
on the logs using the `error_detail` arguments (This will be shown in logs but will not
be visible by the client in the response).
The logger dict can be accessed from any routes functions using the
`aio_lambda_api.get_logger` function. This can be used to add custom logs entries. All
log entries must be JSON serializable.
Defaults log fields:
* `error_detail`: `error_detail` argument value of `aio_lambda_api.HTTPException`.
* `execution_time_ms`: Execution time in ms of the route function.
* `level`: Logging level (`info`, `warning`, `error`, `critical`).
* `method`: HTTP method of the request.
* `path`: HTTP path of the request.
* `request_id`: `X-Request-Id` header is present else AWS lambda `requestId`.
* `server`: Server running the lambda. This is the ID of the first lambda function call,
so this value will not change if lambda reuse the same context in another function
call.
* `status_code`: HTTP status code of the response.
### Async initialization
In AWS lambda the asyncio context is limited to the routes functions.
But, the `aio_lambda_api.Handler` class provides methods to run async function outside
routes functions:
* `Handler.run_async`: Runs an async function and returns the result.
* `Handler.enter_async_context`: Initialize an async contextmanager and returns the
initialized object. The Context manager is also attached to the
`aio_lambda_api.Handler` exit stack (And will be exited with the handler; note that
there is no guarantee that this is executed with AWS lambda).
```python
from aio_lambda_api import Handler
from database import Database
handler = Handler()
# Initialize a database connection outside routes functions
# AWS lambda will keep this value cached between runs
async def init_database():
db = Database()
await db.connect()
return db
DB = handler.run_async(init_database())
# Variable can then be used normally from routes functions
@handler.get("/user")
def get_fron_db():
return await DB.select("*")
```
### Configuration
#### Settings
These settings are passed to the handle with environment variables.
* `FUNCTION_TIMEOUT`: The route function call timeout in seconds.
Available as `aio_lambda_api.settings.FUNCTION_TIMEOUT`. Default to 30s.
* `CONNECTION_TIMEOUT`: Global connection timeout in seconds.
Available as `aio_lambda_api.settings.CONNECTION_TIMEOUT`.
Also used in `aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Default to 5s.
* `READ_TIMEOUT`: Global read timeout in seconds.
Available as `aio_lambda_api.settings.READ_TIMEOUT`.
Also used in `aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Default to 15s.
* `BOTO_PARAMETER_VALIDATION`: If set enable `boto3` input validation in
`aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Disabled by default to improve
performance.
* `BOTO_MAX_POOL_CONNECTIONS`: `boto3` `max_pool_connections` in
`aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Default to 100.
#### AWS utilities
##### Botocore default config.
A `botocore.client.Config` is provided by
`aio_lambda_api.backends.aws_lambda.Backend.botocore_config()` and can be used with
`aioboto3` clients and resources.
```python
import aioboto3
from aio_lambda_api.backends.aws_lambda import Backend
session = aioboto3.Session()
async with session.resource("s3", config=Backend.botocore_config()) as s3:
pass
```
When using `aio_lambda_api.backends.aws_lambda.Backend.botocore_config()`,
botocore/aiobotocore is configured to use orjson is available to speed up JSON
serialization/deserialization.
If the `speedups` extra is installed, aiohttp is installed with its own speedups extra.
Since aiobotocore and aioboto3 rely on aiohttp, this will also improve their
performance.
## Installation
### Minimal installation:
```bash
pip install aio-lambda-api
```
### Installations with extras:
Multiple extra are provided
```bash
pip install aio-lambda-api[all]
```
* `all`: Install all extras.
* `aws`: Install AWS SDK (`aioboto3`).
* `validation`: Install input validation dependencies (`pydantic`).
* `speedups`: Input performance speedups dependencies (`uvloop`, `orjson`).
Raw data
{
"_id": null,
"home_page": "https://github.com/JGoutin/aio_lambda_api",
"name": "aio-lambda-api",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.9,<4.0",
"maintainer_email": "",
"keywords": "aws,lambda,http,API,asyncio",
"author": "JGoutin",
"author_email": "",
"download_url": "https://files.pythonhosted.org/packages/cf/60/ec76ced3cf9e15fac3a28764b74f32eb7214b2fede4a62643d9ab4348150/aio_lambda_api-0.3.0.tar.gz",
"platform": null,
"description": "![Tests](https://github.com/JGoutin/aio_lambda_api/workflows/tests/badge.svg)\n[![codecov](https://codecov.io/gh/JGoutin/aio_lambda_api/branch/main/graph/badge.svg?token=52KXxxDFhx)](https://codecov.io/gh/JGoutin/aio_lambda_api)\n[![PyPI](https://img.shields.io/pypi/v/aio_lambda_api.svg)](https://pypi.org/project/aio_lambda_api)\n\nA lightweight AsyncIO HTTP API for serverless functions like AWS lambda.\n\nFeatures:\n* Asyncio in AWS lambda.\n* FastAPI inspired routing, parameters and exception handling.\n* Detailed JSON formatted access log.\n* X-Request-ID header support (Including in logs).\n* Configurable request timeout.\n* Optional input validation using Pydantic.\n* Optional JSON serialization/deserialization speedup with Orjson.\n* Optional speedups using accelerated libraries (Like UVloop and ORJson).\n\nSupported backends:\n* AWS Lambda \n * Request/responses are in API Gateway format (Only the format is required, \n but can be triggered without using the API Gateway service).\n * Support batches or requests with AWS SQS queue, AWS SNS or AWS MQ.\n * JSON access logs works well with AWS Cloudwatch Insight.\n\nNot supported yet:\n* Routes with variables (Like `\"/items/{item_id}\"`).\n* Query strings.\n* Pydantic models as response or request body.\n* More backends.\n* AWS Lambda backend: AWS SSM parameter store helper.\n\n## Usage\n\n### Usage with AWS lambda\n\nFunction code example (`app.py`):\n```python\nfrom aio_lambda_api import Handler\n\nhandler = Handler()\n\n@handler.get(\"/\")\ndef read_root():\n return {\"Hello\": \"World\"}\n```\nAWS lambda function handler must be configured to `app.handler`.\n\n### Routing\n\nThe `aio_lambda_api.Handler` class provides decorators to configure routes for each \nHTTP method:\n* `Handler.get()`: GET.\n* `Handler.head()`: HEAD.\n* `Handler.post()`: POST.\n* `Handler.put()`: PUT.\n* `Handler.patch()`: PATCH.\n* `Handler.delete()`: DELETE.\n* `Handler.options()`: OPTIONS.\n\nFor all decorators, the first arguments is the HTTP path and is required.\n\nThe decorated function is executed when the defined HTTP path and method matches.\n\nBy default, the body of the request is parsed as JSON and injected in the function as \narguments.\nIf Pydantic is installed, parameters are validated against arguments types annotations.\n\nThe decorated function must return a JSON serializable object or `None`. If the function\nreturns `None`, the returned status code is automatically set to `204`.\n\n### Exception handling\n\nIt is possible to trigger a response using the `aio_lambda_api.HTTPException` as follow:\n\n```python\nfrom aio_lambda_api import Handler, HTTPException\n\nhandler = Handler()\n\nitems = {\"foo\": \"The Foo Wrestlers\"}\n\n@handler.get(\"/item\")\nasync def read_item(item_id: str):\n if item_id not in items:\n raise HTTPException(status_code=404, detail=\"Item not found\")\n return {\"item\": items[item_id]}\n```\n\nIf an exception is risen in routes functions, the behavior is the following:\n* `aio_lambda_api.HTTPException`: Converted to HTTP response with the \n specified body and returns code.\n* `pydantic.ValidationError`: Converted to 422 HTTP error response with Pydantic error \n details as body.\n* Other exceptions: Reraised. Callers using the Lambda API will\n be able to analyse the error like any other Python lambda error \n (With `errorType`, `errorMessage` and `stackTrace`)\n Callers using an HTTP endpoint/API gateway will receive a simple 500 error with no\n details.\n\n### Customizing responses\n\n#### Return code\n\nIt is possible to select the return code (when no exception occurs) using the\n`status_code` argument. If not specified `200` is used.\n\n```python\nfrom aio_lambda_api import Handler\n\nhandler = Handler()\n\n@handler.get(\"/\", status_code=201)\ndef read_root():\n return {\"Hello\": \"World\"}\n```\n\n#### Headers\n\nIt is possible to configure headers by using the `Response` object from arguments:\n\n```python\nfrom aio_lambda_api import Handler, Response\n\nhandler = Handler()\n\n@handler.get(\"/\")\nasync def read_item(response: Response):\n response.headers[\"Cache-Control\"] = \"no-cache\"\n\n return {\"Hello\": \"World\"}\n```\n\n#### Custom response\n\nIt is possible to fully configure the response by returning the `Response` object.\n\n```python\nfrom aio_lambda_api import Handler, Response\n\nhandler = Handler()\n\n@handler.get(\"/\")\nasync def read_item():\n return Response(\n status_code=202,\n media_type=\"application/octet-stream\",\n content=b\"helloworld\"\n )\n```\n\nThe default `Response` class accept `str` or `bytes` as content.\n\nThe `JSONResponse` object is also available, it is the default response object when not \nexplicitly set.\n\nIt is possible to create a subclass of `Response` class to have a custom behavior. The\n`Response.render` method is responsible for the serialization of the response.\n\nNote: If a response class returns a `bytes` content after `Response.render`, this\ncontent will be base64 encoded automatically in the API Gateway compatible response\nreturned.\n\n### Accessing Request data\n\nIt is possible to access request data by using the `Request` object from arguments:\n\n```python\nfrom aio_lambda_api import Handler, Request\n\nhandler = Handler()\n\n@handler.get(\"/\")\nasync def read_item(request: Request):\n user_agent = request.headers[\"user-agent\"]\n return {\"Hello\": user_agent}\n```\n\nNote: All headers keys are lowercase in the `Request` object.\n\n### Logging\n\nAn access log is automatically generated. This access log is in JSON format (`dict` in \nthe code). With AWS lambda, the logs will appear with other lambda logs in Cloudwatch \nlogs. The JSON format make them very easy to query in Cloudwatch Insight.\n\nAll request and all exceptions from routes functions are logged in the access log \n(Including reraised 500 errors.)\n\nWhen raising `aio_lambda_api.HTTPException`, it is possible to show extra information\non the logs using the `error_detail` arguments (This will be shown in logs but will not\nbe visible by the client in the response).\n\nThe logger dict can be accessed from any routes functions using the\n`aio_lambda_api.get_logger` function. This can be used to add custom logs entries. All\nlog entries must be JSON serializable.\n\nDefaults log fields:\n* `error_detail`: `error_detail` argument value of `aio_lambda_api.HTTPException`.\n* `execution_time_ms`: Execution time in ms of the route function.\n* `level`: Logging level (`info`, `warning`, `error`, `critical`).\n* `method`: HTTP method of the request.\n* `path`: HTTP path of the request.\n* `request_id`: `X-Request-Id` header is present else AWS lambda `requestId`.\n* `server`: Server running the lambda. This is the ID of the first lambda function call,\n so this value will not change if lambda reuse the same context in another function \n call.\n* `status_code`: HTTP status code of the response.\n\n### Async initialization\n\nIn AWS lambda the asyncio context is limited to the routes functions.\n\nBut, the `aio_lambda_api.Handler` class provides methods to run async function outside \nroutes functions:\n* `Handler.run_async`: Runs an async function and returns the result.\n* `Handler.enter_async_context`: Initialize an async contextmanager and returns the\n initialized object. The Context manager is also attached to the \n `aio_lambda_api.Handler` exit stack (And will be exited with the handler; note that \n there is no guarantee that this is executed with AWS lambda).\n\n```python\nfrom aio_lambda_api import Handler\nfrom database import Database\n\nhandler = Handler()\n\n# Initialize a database connection outside routes functions\n# AWS lambda will keep this value cached between runs\n\nasync def init_database():\n db = Database()\n await db.connect()\n return db\n\nDB = handler.run_async(init_database())\n\n# Variable can then be used normally from routes functions\n\n@handler.get(\"/user\")\ndef get_fron_db():\n return await DB.select(\"*\")\n\n```\n\n### Configuration \n\n#### Settings\n\nThese settings are passed to the handle with environment variables.\n\n* `FUNCTION_TIMEOUT`: The route function call timeout in seconds. \n Available as `aio_lambda_api.settings.FUNCTION_TIMEOUT`. Default to 30s.\n* `CONNECTION_TIMEOUT`: Global connection timeout in seconds.\n Available as `aio_lambda_api.settings.CONNECTION_TIMEOUT`.\n Also used in `aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Default to 5s.\n* `READ_TIMEOUT`: Global read timeout in seconds.\n Available as `aio_lambda_api.settings.READ_TIMEOUT`.\n Also used in `aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Default to 15s.\n* `BOTO_PARAMETER_VALIDATION`: If set enable `boto3` input validation in \n `aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Disabled by default to improve \n performance.\n* `BOTO_MAX_POOL_CONNECTIONS`: `boto3` `max_pool_connections` in \n `aio_lambda_api.aws.BOTO_CLIENT_CONFIG`. Default to 100.\n\n#### AWS utilities\n\n##### Botocore default config.\n\nA `botocore.client.Config` is provided by \n`aio_lambda_api.backends.aws_lambda.Backend.botocore_config()` and can be used with\n`aioboto3` clients and resources.\n\n```python\nimport aioboto3\nfrom aio_lambda_api.backends.aws_lambda import Backend\n\nsession = aioboto3.Session()\nasync with session.resource(\"s3\", config=Backend.botocore_config()) as s3:\n pass\n```\n\nWhen using `aio_lambda_api.backends.aws_lambda.Backend.botocore_config()`, \nbotocore/aiobotocore is configured to use orjson is available to speed up JSON \nserialization/deserialization.\n\nIf the `speedups` extra is installed, aiohttp is installed with its own speedups extra.\nSince aiobotocore and aioboto3 rely on aiohttp, this will also improve their \nperformance.\n\n## Installation\n\n### Minimal installation:\n```bash\npip install aio-lambda-api\n```\n\n### Installations with extras:\n\nMultiple extra are provided\n\n```bash\npip install aio-lambda-api[all]\n```\n\n* `all`: Install all extras.\n* `aws`: Install AWS SDK (`aioboto3`).\n* `validation`: Install input validation dependencies (`pydantic`).\n* `speedups`: Input performance speedups dependencies (`uvloop`, `orjson`).\n",
"bugtrack_url": null,
"license": "BSD-2-Clause",
"summary": "Simple AsyncIO AWS lambda HTTP API",
"version": "0.3.0",
"split_keywords": [
"aws",
"lambda",
"http",
"api",
"asyncio"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "579e523f7b3918f6a1e09ab2d3cfde98091ddccb3c003ef98fb73c8e8710b369",
"md5": "8a2303a22d42cbc139208a55b4a37853",
"sha256": "533cbca4d6f9494f7df28e1d0cc3f9c343937214a9aab16898b8a2dbb83591b3"
},
"downloads": -1,
"filename": "aio_lambda_api-0.3.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "8a2303a22d42cbc139208a55b4a37853",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9,<4.0",
"size": 18525,
"upload_time": "2023-02-01T16:50:57",
"upload_time_iso_8601": "2023-02-01T16:50:57.496779Z",
"url": "https://files.pythonhosted.org/packages/57/9e/523f7b3918f6a1e09ab2d3cfde98091ddccb3c003ef98fb73c8e8710b369/aio_lambda_api-0.3.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "cf60ec76ced3cf9e15fac3a28764b74f32eb7214b2fede4a62643d9ab4348150",
"md5": "a6c4ecc29e4454e28cde6e90396c336a",
"sha256": "077bd476d364395d7e8ff2d6989dccb8d104e6534425c9eb13602b6aaa804f2b"
},
"downloads": -1,
"filename": "aio_lambda_api-0.3.0.tar.gz",
"has_sig": false,
"md5_digest": "a6c4ecc29e4454e28cde6e90396c336a",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9,<4.0",
"size": 19068,
"upload_time": "2023-02-01T16:50:59",
"upload_time_iso_8601": "2023-02-01T16:50:59.171264Z",
"url": "https://files.pythonhosted.org/packages/cf/60/ec76ced3cf9e15fac3a28764b74f32eb7214b2fede4a62643d9ab4348150/aio_lambda_api-0.3.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-02-01 16:50:59",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "JGoutin",
"github_project": "aio_lambda_api",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "aio-lambda-api"
}