Giter Club home page Giter Club logo

lambda-decorators's Introduction

🐍λ✨ - lambda_decorators

Version Docs Build SayThanks

A collection of useful decorators for making AWS Lambda handlers

lambda_decorators is a collection of useful decorators for writing Python handlers for AWS Lambda. They allow you to avoid boiler plate for common things such as CORS headers, JSON serialization, etc.

Quick example

# handler.py

from lambda_decorators import json_http_resp, load_json_body

@json_http_resp
@load_json_body
def handler(event, context):
    return {'hello': event['body']['name']}

When deployed to Lambda behind API Gateway and cURL'd:

$ curl -d '{"name": "world"}' https://example.execute-api.us-east-1.amazonaws.com/dev/hello
{"hello": "world"}

Install

If you are using the serverless framework I recommend using serverless-python-requirements

sls plugin install -n serverless-python-requirements
echo lambda-decorators >> requirements.txt

Or if using some other deployment method to AWS Lambda you can just download the entire module because it's only one file.

curl -O https://raw.githubusercontent.com/dschep/lambda-decorators/master/lambda_decorators.py

Included Decorators:

lambda_decorators includes the following decorators to avoid boilerplate for common usecases when using AWS Lambda with Python.

See each individual decorators for specific usage details and the example for some more use cases. This library is also meant to serve as an example for how to write decorators for use as lambda middleware. See the recipes page for some more niche examples of using decorators as middleware for lambda.

Writing your own

lambda_decorators includes utilities to make building your own decorators easier. The before, after, and on_exception decorators can be applied to your own functions to turn them into decorators for your handlers. For example:

import logging
from lambda_decorators import before

@before
def log_event(event, context):
    logging.debug(event)
    return event, context

@log_event
def handler(event, context):
    return {}

And if you want to make a decorator that provides two or more of before/after/on_exception functionality, you can use LambdaDecorator:

import logging
from lambda_decorators import LambdaDecorator

class log_everything(LambdaDecorator):
    def before(event, context):
        logging.debug(event, context)
        return event, context
    def after(retval):
        logging.debug(retval)
        return retval
    def on_exception(exception):
        logging.debug(exception)
        return {'statusCode': 500}

@log_everything
def handler(event, context):
    return {}

Why

Initially, I was inspired by middy which I like using in JavaScript. So naturally, I thought I'd like to have something similar in Python too. But then as I thought about it more, it seemed that when thinking of functions as the compute unit, when using python, decorators pretty much are middleware! So instead of building a middleware engine and a few middlewares, I just built a few useful decorators and utilities to build them.


Full API Documentation

lambda-decorators's People

Contributors

dschep avatar gliptak avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

lambda-decorators's Issues

module initialization error: importlib_metadata

I'm trying to use the json_http_resp and json_schema_validator decorators on a lambda function running on Python 3.6 (and version 0.3.0 of lambda-decorators). It works great locally and in my docker container, but when I try to actually run it on AWS, I get the following error message on function initialization if I try to import the lambda-decorators module:

module initialization error: importlib_metadata

Unfortunately AWS doesn't give much more than this. If I remove the lambda-decorators import, everything works fine. Has this been seen before?

pass parameters to decorators

Hi is there a way to define a decorator with paramaters like the CORS one.

Many thanks for help and thoughts, not sure if i am just not understanding the syntax to use or its not possible,

I have two use cases.

  1. On exception be able to define a friendly message per handler for error responses
@on_exception
def handle_errors(exception,friendlyMesssage="Something went wrong"):
    print(exception)
    return {'statusCode': 500, 'body':friendlyMesssage}

@handle_errors(friendlyMesssage="Failed to get something")
def getSomething(event, context):
   #do something
  1. On before - i am writing a middleware to implenent RBAC (role based access control) and want o provide the list of permissions required to call the function
@before
def rbac(event, context,permissions=[]):
    print(f'required permissions {" ".join(str(x) for x in permissions)}')
    # actually do the validation

@rbac(permissions=["get:dogs","put:badgers"])
def getSomething(event, context):
   #do something

Pass event and context variables to after and on_exception decorators

It doesn't cost anything and would enable a wider range of use cases. For example:

  1. To determine whether the response body may be gzip-encoded in an after callback, the event object needs to be inspected (to get the request header "Accept-Encoding")
  2. To pass state from the before to the after callback, the context variable could be used; e.g. to measure the time it took to process the request.

There's an initial proof-of-concept in jonaswitt@1ed4697 that works well for me.

For backwards compatibility, at least with the included decorators, it tries to figure out how many arguments the decorated function takes. I believe this could be extended to LambdaDecorator subclasses in order to avoid breaking existing user's code.
It uses getargspec() which works in Python 2 and 3 but is deprecated in Python 3. I haven't found a similar function that would work in both Python 2 and 3.

What do you think about this approach? I'd be happy to turn this into a PR (w/ updated documentation) if you agree with the general direction.

JSON Schema validation decorators

Add request/response JSON Schema validation like middy has https://github.com/middyjs/middy/blob/master/docs/middlewares.md#validator

Potential blocker: This will most likely depend on a 3rd party library and currently lambda_decorators can be installed via downloading the one python file, which is helpful because AWS Lambda does not make including dependencies easy (of course unles syou're using serverless-python-requirements 😀 )

Likely mitigation: wrap imports of 3rd party stuff in try/except ImportError and only define those decorators if the dependency is met.

Trouble installing

I'm attempting to install in a python 3.6 venv. In a pipenv shell I'm running pipenv install and get the following stack trace (see below).

I'm wondering if this is more likely an issue with my local environment or the lambda-decorators install script.

Pipfile.lock not found, creating…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Traceback (most recent call last):
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/bin/pipenv", line 11, in <module>
    load_entry_point('pipenv==8.3.2', 'console_scripts', 'pipenv')()
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/vendor/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/vendor/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/vendor/click/core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/vendor/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/vendor/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/cli.py", line 1866, in install
    do_init(dev=dev, allow_global=system, ignore_pipfile=ignore_pipfile, system=system, skip_lock=skip_lock, verbose=verbose, concurrent=concurrent, deploy=deploy, pre=pre)
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/cli.py", line 1330, in do_init
    do_lock(system=system, pre=pre)
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/cli.py", line 1121, in do_lock
    pre=pre
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/utils.py", line 461, in resolve_deps
    resolved_tree.update(resolver.resolve(max_rounds=PIPENV_MAX_ROUNDS))
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/piptools/resolver.py", line 102, in resolve
    has_changed, best_matches = self._resolve_one_round()
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/piptools/resolver.py", line 200, in _resolve_one_round
    for dep in self._iter_dependencies(best_match):
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/piptools/resolver.py", line 278, in _iter_dependencies
    for dependency in self.repository.get_dependencies(ireq):
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/piptools/repositories/pypi.py", line 153, in get_dependencies
    result = reqset._prepare_file(self.finder, ireq)
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/pip/req/req_set.py", line 639, in _prepare_file
    abstract_dist.prep_for_dist()
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/pip/req/req_set.py", line 134, in prep_for_dist
    self.req_to_install.run_egg_info()
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/pip/req/req_install.py", line 438, in run_egg_info
    command_desc='python setup.py egg_info')
  File "/usr/local/Cellar/pipenv/8.3.2/libexec/lib/python2.7/site-packages/pipenv/patched/pip/utils/__init__.py", line 707, in call_subprocess
    % (command_desc, proc.returncode, cwd))
pip.exceptions.InstallationError: Command "python setup.py egg_info" failed with error code 1 in /var/folders/64/0m79fk4566n19z0t6stcjgxm0000gn/T/tmpeqCh9Qbuild/functools32/

Here is my Pipfile:

[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true


[dev-packages]

pytest = "==3.0.7"
responses = "==0.8.1"


[packages]

click = ">=6.0"
requests = "*"
"boto3" = "*"
"lambda-decorators" = "*"

Deprecation of `asyncio.get_event_loop()` if there is no running event loop

Hi! Thanks for writing and maintaining this module! 😄

The Python 3.10 docs for asyncio.get_event_loop() note:

Deprecated since version 3.10: Deprecation warning is emitted if there is no running event loop. In future Python releases, this function will be an alias of get_running_loop().

It seems @async_handler will fail with RuntimeError: no running event loop in that case. Do you think this correct?

Perhaps a solution is to effectively move the deprecated functionality into the module itself. I created a PR for this.

Running tests raises exceptions without fake context

Running a handler in a test environment raises exceptions. Just FYI that mocking aws context is required. Maybe that is obvious? Feel free to close if there is no other way around this.

@wraps(handler)
    def wrapper(event, context):
>       if context.aws_request_id in seen_request_ids:
E       AttributeError: 'dict' object has no attribute 'aws_request_id'

Multipe domain & wildcard support for CORS

Enable usage like this, list of domains & support wildcards in strings:

    >>> @cors_headers(origin=['https://example.com', 'https://*.example.com'])
    ... def hello_custom_origin(example, context):
    ...     return {'body': 'foobar'}
    >>> hello_custom_origin({'headers': {'Host': 'https://foobar.example.com'}}, object())
    {'body': 'foobar', 'headers': {'Access-Control-Allow-Origin': 'https://foobar.example.com'}

Add simple before/after/on_exception decorators

Add handlers for building some simple decorators

Examples (Of course you wouldn't make some of these since there are builtin version)

@before(lambda event, context: json.loads(event['body']), context)
def handler(decoded_body, context):
    pass

@after(lambda response: json.dumps(response))
def handler(decoded_body, context):
    pass

@on_exception(lambda exception, context: {'statusCode': 200, 'body': str(exception)})
def handler(event, context):
    raise Exception('ruh roh')

`load_json_body` is triggered only when `event["body"]` is a string. Is there a good reason for it?

Because I only see reasons against it:

  1. Simply because a JSON can be passed also as bytes - hence json.loads() method accepts types of bytes and bytearray as well.
  2. If I decide to use this decorator on a handler it means I expect a JSON in the body, therefore I'd like it to return a 400 even when the input is not a string. This could be skipped it the body is already a dictionary.

I understand changing the logic to start acting on non-strings is most likely breaking. If a maintainer gives a green light, I can create a PR.

Also, I am curious of other opinions.

Use custom JSONEncoder / JSONDecoder on json decorators

When pulling data from a database (using PynamoDB, for example) we are often left with a "model" object which can not be directly serialized with json, unless a custom JSONEncoder is written. Normally we pass this class in to the json.dumps() function.

It would be extremely useful to pass this special Encoder into the dump_json_body and similar decorators

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.