Code Monkey home page Code Monkey logo

python-lambdarest's Introduction

test Latest Version Python Support

lambdarest Buy Me A Coffee

Python routing mini-framework for AWS Lambda with optional JSON-schema validation.

Features

  • lambda_handler function constructor with built-in dispatcher
  • Decorator to register functions to handle HTTP methods
  • Optional JSON-schema input validation using same decorator

Merchandise ❤️

You can support the development by buying a wonderful cup or buy coffee for the maintainers cup

lambdarest mug

External articles / tutorials

Other articles? add them here

Installation

Install the package from PyPI using pip:

$ pip install lambdarest

Getting Started

This module helps you to handle different HTTP methods in your AWS Lambda.

from lambdarest import lambda_handler

@lambda_handler.handle("get")
def my_own_get(event):
    return {"this": "will be json dumped"}

##### TEST #####

input_event = {
    "body": '{}',
    "httpMethod": "GET",
    "resource": "/"
}
result = lambda_handler(event=input_event)
assert result == {"body": '{"this": "will be json dumped"}', "statusCode": 200, "headers":{}}

Documentation

See docs for documentation and examples covering amongst:

Anormal unittest behaviour with lambda_handler singleton

Because of python unittests leaky test-cases it seems like you shall beware of this issue when using the singleton lambda_handler in a multiple test-case scenario.

Tests

Use the following commands to install requirements and run test-suite:

$ pip install -e ".[dev]"
$ black tests/ lambdarest/
$ rm -f test_readme.py
$ pytest --doctest-modules -vvv

For more info see Contributing...

Changelog

See HISTORY.md

Contributors

Thanks for contributing!

@sphaugh, @amacks, @jacksgt, @mkreg, @aphexer, @nabrosimoff, @elviejokike, @eduardomourar, @devgrok, @AlbertoTrindade, @paddie, @svdgraaf, @simongarnier, @martinbuberl, @adamelmore, @sloev

Wanna contribute?

And by the way, we have a Code Of Friendlyhood!

FAQ

python-lambdarest's People

Contributors

adamdottv avatar albertotrindade avatar amacks avatar aphexer avatar bjornbnielsen avatar devgrok avatar eduardomourar avatar elviejokike avatar getglad avatar jacksgt avatar martinbuberl avatar mkreg avatar nabrosimoff avatar paddie avatar simongarnier avatar sloev avatar svdgraaf avatar tmo-trustpilot avatar tprobots 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

Watchers

 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

python-lambdarest's Issues

Issue with custom domain

I've hit an issue when using a custom domain implementation.

When api gateway is setup with a subpath (eg my-custom-domain.com/v1/[resource]), the request will be different, and contain the /v1/ as part of the request, eg:

{
	'resource': '/foobar',
	'path': '/v1/foobar',
	'httpMethod': 'GET',
	'headers': {
		'Accept': 'application/json',
		'Accept-Encoding': 'gzip, deflate, br',
		'Accept-Language': 'en-US,en;q=0.9,nl;q=0.8',
		'CloudFront-Forwarded-Proto': 'https',
		'CloudFront-Is-Desktop-Viewer': 'true',
		'CloudFront-Is-Mobile-Viewer': 'false',
		'CloudFront-Is-SmartTV-Viewer': 'false',
		'CloudFront-Is-Tablet-Viewer': 'false',
		'CloudFront-Viewer-Country': 'NL',
		'Host': 'api.foobar.com',
		'Referer': 'https://api.foobar.com/v1/docs/',
		'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36',
		'Via': '2.0 64150aec025f89247bcddad7c210f6cb.cloudfront.net (CloudFront)',
		'X-Amz-Cf-Id': 'jbPh8cdSTAyR6SgVM8txu9WS8PphI29OP2hmQOZ0IvJ0ssW8fcQJbQ==',
		'x-amz-date': '20180406T135655Z',
		'x-amz-security-token': '',
		'X-Amzn-Trace-Id': 'Root=1-5ac77ca7-111b160ce4b39b7608425954',
		'X-Forwarded-For': '185.40.99.134, 54.239.167.86',
		'X-Forwarded-Port': '443',
		'X-Forwarded-Proto': 'https'
	},
	'queryStringParameters': None,
	'pathParameters': None,
	'stageVariables': None,
	'requestContext': {
		'resourceId': 'x5thtq',
		'resourcePath': '/foobar',
		'httpMethod': 'GET',
		'extendedRequestId': 'E7BqIHrWjoEFjQg=',
		'requestTime': '06/Apr/2018:13:56:55 +0000',
		'path': '/v1/foobar',
		'accountId': '618537606105',
		'protocol': 'HTTP/1.1',
		'stage': 'production',
		'requestTimeEpoch': 1523023015228,
		'requestId': '5d943842-39a2-11e8-837d-d57bd205b928',
		'identity': {
			'cognitoIdentityPoolId': None,
			'accountId': '618537606105',
			'cognitoIdentityId': None,
			'caller': 'AROAJ4QGV6BHJ4WTSGWQK:AssumeRoleSession',
			'sourceIp': '185.40.99.134',
			'accessKey': 'ASIAIPZBIISXIHIAC4EA',
			'cognitoAuthenticationType': None,
			'cognitoAuthenticationProvider': None,
			'userArn': 'arn:aws:sts::1234:assumed-role/awefsdasdfcom/AssumeRoleSession',
			'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36',
			'user': 'AROAJ4QGV6BHJ4WTSGWQK:AssumeRoleSession'
		},
		'apiId': 'dil4b1umf1'
	},
	'body': None,
	'isBase64Encoded': False
}

I'll see if I can make a PR and fix this

Trouble installing with pip

When running the install command "pip install lambdarest" I get an error

ERROR: Command errored out with exit status 1:
command: /backend/api_lambda/bin/python3 -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/65/jpxwzrxj68xb7lp74c2_yf8hrp6vql/T/pip-install-wor4aeh7/lambdarest/setup.py'"'"'; file='"'"'/private/var/folders/65/jpxwzrxj68xb7lp74c2_yf8hrp6vql/T/pip-install-wor4aeh7/lambdarest/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(file);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, file, '"'"'exec'"'"'))' egg_info --egg-base /private/var/folders/65/jpxwzrxj68xb7lp74c2_yf8hrp6vql/T/pip-install-wor4aeh7/lambdarest/pip-egg-info
cwd: /private/var/folders/65/jpxwzrxj68xb7lp74c2_yf8hrp6vql/T/pip-install-wor4aeh7/lambdarest/
Complete output (5 lines):
Traceback (most recent call last):
File "", line 1, in
File "/private/var/folders/65/jpxwzrxj68xb7lp74c2_yf8hrp6vql/T/pip-install-wor4aeh7/lambdarest/setup.py", line 9, in
history = open("HISTORY.md").read()
FileNotFoundError: [Errno 2] No such file or directory: 'HISTORY.md'
----------------------------------------
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.

Is there an issue with the setup.py script? Is this a known issue?

isBase64_encoded only handled if application_load_balancer is True

Currently, the isBase64_encoded is handled and passed further only if the application_load_balancer is set to True.

By changing

# if body is None, remove the key
if response.get("body") == None:
    response.pop("body")

if application_load_balancer: 

to

# if body is None, remove the key
if response.get("body") == None:
    response.pop("body")

if self.isBase64_encoded:
    response["isBase64Encoded"] = self.isBase64_encoded

if application_load_balancer: 

in __init__.py it would work OK.

lambdarest returns stacktrace if not run under "lambda proxy"

we need to catch if event is not compliant with lambda proxy
returning stacktrace is not acceptable

{
  "stackTrace": [
    [
      "/var/task/lambdarest/__init__.py",
      65,
      "inner_lambda_handler",
      "method_name = event[\"httpMethod\"].lower()"
    ]
  ],
  "errorType": "KeyError",
  "errorMessage": "'httpMethod'"
}

[Feature Request] ALB Custom Scopes

issue / feature request

I have a need to perform fine-grained permission checking on a Lambda function behind a ALB. I have extended lambdarest to allow for the existing Scopes functionality to also verify scopes passed from an ALB as it currently does from API Gateway. I would like to provide this code into the core code base - hence wanting to ensure you would be happy to accept it before I raise a PR.

Unlike custom authorizers in API GW, ALB's do not provide a "scope" attribute or claim that is standardized in the event object - I have allowed the developer to specify the "scope" claim that is used (in the call to create_lambda_handler) and then have added code to extract and verify the scopes from the x-amzn-oidc-data header when the request is parsed.

Parsing the ALB header requires the use of several external libraries (base64, jwt, requests) as the header data needs to be decoded, the signature checked (hence the ALB public key fetched). Hence this is not just an addition of a few lines of Python but requires the base dependency to be extended.

I think this could be useful to the wider users of this library - but wanted ton confirm before raising a PR

Thanks!

Example Helper Class to verify ALB Scopes

class ALBScopes:
  def __init__(self, scope_param):
    self.scope_param = scope_param
    self.alb_public_keys={}

  def __get_public_key(self, kid):
    url = 'https://public-keys.auth.elb.' + \
      os.environ['AWS_REGION'] + '.amazonaws.com/' + kid
    req = requests.get(url)
    return req.text

  def __verify_oidc_data(self, data):
    jwt_headers = data.split('.')[0]
    decoded_jwt_headers = base64.b64decode(jwt_headers)
    decoded_json = json.loads(decoded_jwt_headers)
    key_id = decoded_json['kid']
    if decoded_json['kid'] not in self.alb_public_keys:
      self.alb_public_keys[key_id] = self.__get_public_key(key_id)
    return jwt.decode(data, self.alb_public_keys[key_id], algorithms=['ES256'])

  def get_scopes(self, event):
    encoded_data = event.get('headers', {}).get('x-amzn-oidc-data', None)
    data = self.__verify_oidc_data(encoded_data)
    if isinstance(data.get(self.scope_param, None), list):
      return data.get(self.scope_param, None)
    return []

[BUG] multiValueHeaders not being sent back (ALB)

It looks like ALB needs support for multiValueHeaders as part of the return JSON for things like Content-Type, headers does not seem to be properly passed through the ALB. I added a multiValueHeader element by copying and modifying headers in the to_json and inner_lambda_handler and it seems to work as expected and the ALB properly interprets. I can submit a PR, but I cannot vouch for the quality of the code, however it does seem to work

diff --git a/lambdarest/__init__.py b/lambdarest/__init__.py
index d8cd80f..8079e97 100755
--- a/lambdarest/__init__.py
+++ b/lambdarest/__init__.py
@@ -22,10 +22,11 @@ class Response(object):
     if no headers are specified, empty dict is returned
     """
 
-    def __init__(self, body=None, status_code=None, headers=None):
+    def __init__(self, body=None, status_code=None, headers=None, multiValueHeaders=None):
         self.body = body
         self.status_code = status_code
         self.headers = headers
+        self.multiValueHeaders = multiValueHeaders
         self.status_code_description = None
         self.isBase64_encoded = False
 
@@ -42,8 +43,12 @@ class Response(object):
             if do_json_dumps
             else self.body,
             "statusCode": status_code,
-            "headers": self.headers or {},
         }
+        ## handle multiValueHeaders if defined, default to headers
+        if (self.multiValueHeaders == None) :
+            response["headers"] = self.headers or {}
+        else:
+            response["multiValueHeaders"] = self.multiValueHeaders
         # if body is None, remove the key
         if response.get("body") == None:
             response.pop("body")
@@ -235,21 +240,22 @@ def create_lambda_handler(
                             raise ValueError("Response tuple has more than 3 items")
 
                         # Unpack the tuple, missing items will be defaulted
-                        body, status_code, headers = response + (None,) * (
-                            3 - response_len
+                        body, status_code, headers, multiValueHeaders = response + (None,) * (
+                            4 - response_len
                         )
 
                     elif isinstance(response, dict) and all(
-                        key in ["body", "statusCode", "headers"]
+                        key in ["body", "statusCode", "headers", "multiValueHeaders"]
                         for key in response.keys()
                     ):
                         body = response.get("body")
                         status_code = response.get("statusCode") or status_code
                         headers = response.get("headers") or headers
+                        multiValueHeaders = response.get("multiValueHeaders") or multiValueHeaders
 
                     else:  # if response is string, int, etc.
                         body = response
-                    response = Response(body, status_code, headers)
+                    response = Response(body, status_code, headers, multiValueHeaders)
                 return response.to_json(
                     encoder=json_encoder,
                     application_load_balancer=application_load_balancer,

json_load query params

put json.load'ed post body into event["json"]["body"] and store json.load'ed queryStringParameters into event["json"]["query"]

strange behaviour when unittesting with singleton lambda handler

when unittesting code like the following

from lambdarest import lambda_handler

@lambda_handler.handle("get")
def my_own_get(event):
    return {"this": "will be json dumped"}

The lambda_handler singleton will stay instantiated between testcases since the module is not torn down completely (unittest does this to be able to later on gather the test results see https://chrisbailey.blogs.ilrt.org/2013/05/19/pythons-leaky-testcase-aka-hidden-gotchas-using-self/)

This means that the same lambda_handler instance will be used for all testcases, them being in different files or not. Which again means that the internal lookup http_methods will be scrampled.

A fix for this is to either put your import statements locally in your test functions like:

class test(unittest.TestCase):
    def test_this(self):
        from my_module import lambda_handler
        lambda_handler.handle("get")

or stop using the singleton and instantiate your own like:

from lambdarest import create_lambda_handler
lambda_handler = create_lambda_handler()

Versions prior to v13 on PyPI

Hello,
Our project has a dependency on "lambdarest==12.0.0" (which I do realize is old now) but was wondering if you were planning on publishing the previous versions of lambdarest on PyPI?

Thanks

better JSON schema validation error logging

right now for example:

WARNING:root:[get][400]: Schema[properties][query][additionalProperties] with value Additional properties are not allowed ('query' was unexpected)

would be nice with title of the schema.

If supported, add clearer documentation for headers

Hi @svdgraaf, Can you maybe briefly write an update to the readme mentioning how to setup my APIs to respond with different headers? For example, I need to respond with the 'Access-Control-Allow-Origin' header due to CORS restrictions, and I'm struggling to find the way to do that. Aside from that, am loving the ease of use. Thanks for your contributions!

more complex routing

Yes og course. It was very nondescript :-)

I think it would be great if the handler supported both of the following
syntaxes:

Normal behaviour, implicitly routing /:

@lambda_handler.handle('get')
def get_this():pass

And more complex routings like

@lambda_handler.handle('get', path='/foobar')
def get_this_foobar():pass

Invalid JSON causes Lambda function to 500

If a user sets up lambdarest as in many of the examples (i.e. they don't wrap the whole thing in a try/catch), requests that POST invalid JSON crash the function, and will receive 500 errors. I am willing to write a patch for this once we reach a consensus of what should happen. My suggestion would be to return a 400 error.
https://github.com/sloev/python-lambdarest/blob/master/lambdarest/__init__.py#L365-L372

I've written a test demonstrating the server error:
master...gswalden:master

fix github releases

github releases are broken, have not been updated since april 14th, pypi releases are pushed manually and work fine.

we need automated gh releases tagged with pr name etc

[Question/BUG] Query Parameters converted to float

issue / feature request

I Would like to understand the rationale of converting any query parameter that is a series of digits to floats. I am building an API that relies on Account ID's being based which are digits, these are needed in string form and python-lambdarest uses the float() function to convert all strings that are numeric, without the ability to override this behaviour from what i can tell.

Is there a reason for this - HTTP is a string based protocol afaik, hence converting to float seems somewhat arbitrary and if needed something that could be done in business logic vs the underlying library which is parsing the API Gateway JSON payload.

Thanks!

[Guestbook] 2021 status of Lambdarest, call to all users!

Hi all Lambdarest users, i hope you got through 2020 alive and that 2021 will be a great year for you all.

Its 2021 and i have officially not used Lambdarest in production for nearly 3 years, so as maintainer of this project it means a lot with user feedback to keep having a sense of meaning maintaining it, so here goes... ;-)

If you have the time i would really like you all to tell me about:

  1. Some stories about how you use Lambdarest
  2. What problems Lambdarest has solved for you
  3. What features you miss in Lambdarest
  4. What you think of the quality of the repo (code, docs, test etc)
  5. Are you satisfied with how the repo is being maintained? interactions like issues, PR's etc
  6. What is the future for Lambdarest? still has a place?

Thanks for taking the time, and i wish you all a great year and good health 🤗

❗ if you wanna support the development of this library you can do so here buymeacoffee.com/sloev

@andyfase, @amacks, @jacksgt, @mkreg, @aphexer, @nabrosimoff, @elviejokike, @eduardomourar, @devgro, @svdgraaf, @simongarnier, @adamelmore

Default behaviour when the path parameters is not provided

First of all, I want to thank you guys for this piece of code. I was looking for something to bind functions to HTTP methods when using AWS lambdas and was about to write it myself, but lambdarest came to the rescue.

However, I'm experiencing issues with how the binding behaves when you don't provide the path parameters. The way I see it, the behavior should be to match any path, not "/". This would allow matching on event with path parameters(eg: "/your/object/{id}"). Correct me if I'm wrong, but currently, it is impossible to match path with path parameters, because the path will be different on each event.

To support my use case, I wrote a decorator to hijack the event and set its path to "/" before calling the handler on it.

I already have an idea of how to implement this, I could submit a pull request if you think that this behavior is a good idea.

Just some thought, keep up the great work!

support for multiValueQueryStringParameters

Hey @sloev Indeed this is just for arrays, or multi-value query string parameters. Actually i wonder if this change can be made at the same time as potentially adding support for the multiValueQueryStringParameters field within the event object. this field is supported for both API Gateway and ALB and its actually something I looked for in terms of support initially vs comma-seperation. This would allow for foo&bar&foo=bar2 type query strings.

In this way comma-seperation float conversion can be left as is, and multiValueQueryStringParameters support can be added that either does not perform type casting or attempts to use the schema definition (if it exists) to convert to the appropriate type (string, integer, float)

I'm happy to have a go at adding support for multiValueQueryStringParameters if you feel it's something you would accept.

Originally posted by @andyfase in #69 (comment)

[ENHANCEMENT] Support Lambda Function URLs

I've been using lambdarest for years and tried the latest version with Lambda Function URLs, which will not work without a bit of work (notably, the field names in the event are slightly different than what the code expects). I recognize this project is currently unmaintained, but wanted to introduce this enhancement as a notice to other users should they consider trying this themselves.

Sample event from Lambda Function URL request

{
  "version": "2.0",
  "routeKey": "$default",
  "rawPath": "/my/path",
  "rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
  "cookies": [
    "cookie1",
    "cookie2"
  ],
  "headers": {
    "header1": "value1",
    "header2": "value1,value2"
  },
  "queryStringParameters": {
    "parameter1": "value1,value2",
    "parameter2": "value"
  },
  "requestContext": {
    "accountId": "123456789012",
    "apiId": "<urlid>",
    "authentication": null,
    "authorizer": {
        "iam": {
                "accessKey": "AKIA...",
                "accountId": "111122223333",
                "callerId": "AIDA...",
                "cognitoIdentity": null,
                "principalOrgId": null,
                "userArn": "arn:aws:iam::111122223333:user/example-user",
                "userId": "AIDA..."
        }
    },
    "domainName": "<url-id>.lambda-url.us-west-2.on.aws",
    "domainPrefix": "<url-id>",
    "http": {
      "method": "POST",
      "path": "/my/path",
      "protocol": "HTTP/1.1",
      "sourceIp": "123.123.123.123",
      "userAgent": "agent"
    },
    "requestId": "id",
    "routeKey": "$default",
    "stage": "$default",
    "time": "12/Mar/2020:19:03:58 +0000",
    "timeEpoch": 1583348638390
  },
  "body": "Hello from client!",
  "pathParameters": null,
  "isBase64Encoded": false,
  "stageVariables": null
}

Exception not re-raised in catchall except block

I discovered that create_lambda_handler() decorator is catching any exception thrown by the handler, logging the error and then returning a 500 response.

Usually when catching all exception, one would re-raise the exception so it would bubble up and code registering the handler could do something useful with the exception. In my case, I want to capture such exception using a decorator provided by the sentry SDK(raven) to get visibility on errors in my code.

Your solution works great in the case where you get your stack trace in cloud watch or something but is not very flexible if you want to get them outside of your cloud provider.

I'm curious about your opinion, I've already made the required change and associated test in a branch on my fork.

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.