aio-lambda-api


Nameaio-lambda-api JSON
Version 0.3.0 PyPI version JSON
download
home_pagehttps://github.com/JGoutin/aio_lambda_api
SummarySimple AsyncIO AWS lambda HTTP API
upload_time2023-02-01 16:50:59
maintainer
docs_urlNone
authorJGoutin
requires_python>=3.9,<4.0
licenseBSD-2-Clause
keywords aws lambda http api asyncio
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            ![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"
}
        
Elapsed time: 0.08584s