From 9ff44c8d74ba9f44e42086f02fa32c6f967e107a Mon Sep 17 00:00:00 2001 From: aciesielczyk Date: Wed, 25 May 2022 20:39:42 +0200 Subject: [PATCH] add support for HTTP API Gateway --- README.rst | 14 +-- example.py | 23 +++++ flask_lambda_http.py | 111 ++++++++++++++++++++++++ flask_lambda.py => flask_lambda_rest.py | 9 +- setup.py | 2 +- 5 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 example.py create mode 100644 flask_lambda_http.py rename flask_lambda.py => flask_lambda_rest.py (91%) diff --git a/README.rst b/README.rst index c439ebd..d821b8f 100644 --- a/README.rst +++ b/README.rst @@ -13,23 +13,26 @@ Installation Usage ----- -This module works pretty much just like Flask. This allows you to run and develop this applicaiton locally just like you would in Flask. When ready deploy to Lambda, and configure the handler as:: +This module works pretty much just like Flask. This allows you to run and develop this application locally just like you would in Flask. When ready deploy to Lambda, and configure the handler as:: my_python_file.app +1) Keep in mind to set up correctly lambda handler: + In this case handler: my_python_file.app + This options is available under Runtime settings -> handler + Here is an example of what ``my_python_file.py`` would look like:: - from flask_lambda import FlaskLambda + from flask_lambda_rest import FlaskLambdaRest # or FlaskLambdaHttp - app = FlaskLambda(__name__) + app = FlaskLambdaRest(__name__) # FlaskLambdaHttp @app.route('/foo', methods=['GET', 'POST']) def foo(): data = { 'form': request.form.copy(), - 'args': request.args.copy(), - 'json': request.json + 'args': request.args.copy() } return ( json.dumps(data, indent=4, sort_keys=True), @@ -41,6 +44,7 @@ Here is an example of what ``my_python_file.py`` would look like:: if __name__ == '__main__': app.run(debug=True) +2) Code above should work if you configure REST Api Gateway -> for HTTP Api Gateway use FlaskLambdaHttp class Flask-RESTful ------------- diff --git a/example.py b/example.py new file mode 100644 index 0000000..e585b76 --- /dev/null +++ b/example.py @@ -0,0 +1,23 @@ +from flask_lambda_http import FlaskLambdaHttp +from flask_lambda_rest import FlaskLambdaRest +from flask import request +import json + +app = FlaskLambdaHttp(__name__) + + +@app.route('foo', methods=['POST', 'GET']) +def foo(): + data = { + 'form': request.form.copy(), + 'args': request.args.copy() + } + return ( + json.dumps(data, indent=4, sort_keys=True), + 200, + {'Content-Type': 'application/json'} + ) + + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/flask_lambda_http.py b/flask_lambda_http.py new file mode 100644 index 0000000..06551ab --- /dev/null +++ b/flask_lambda_http.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Matt Martz +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + +from flask import Flask + +try: + from cStringIO import StringIO +except ImportError: + try: + from StringIO import StringIO + except ImportError: + from io import StringIO + +try: # werkzeug <= 2.0.3 + from werkzeug.wrappers import BaseRequest +except: # werkzeug > 2.1 + from werkzeug.wrappers import Request as BaseRequest # issue fixed by joranbeasley + + +__version__ = '0.0.4' + + +def make_environ(event): + environ = {} + + for hdr_name, hdr_value in event['headers'].items(): + hdr_name = hdr_name.replace('-', '_').upper() + if hdr_name in ['CONTENT_TYPE', 'CONTENT_LENGTH']: + environ[hdr_name] = hdr_value + continue + + http_hdr_name = 'HTTP_%s' % hdr_name + environ[http_hdr_name] = hdr_value + + qs = event.get('queryStringParameters', "") + + environ['REQUEST_METHOD'] = event['requestContext']['http']['method'] + environ['PATH_INFO'] = event['rawPath'] + environ['QUERY_STRING'] = urlencode(qs) if qs else '' + environ['REMOTE_ADDR'] = event['requestContext']['http']['sourceIp'] + environ['HOST'] = '%(HTTP_HOST)s:%(HTTP_X_FORWARDED_PORT)s' % environ + environ['SCRIPT_NAME'] = '' + environ['SERVER_NAME'] = 'localhost:5000' + + environ['SERVER_PORT'] = environ['HTTP_X_FORWARDED_PORT'] + environ['SERVER_PROTOCOL'] = 'HTTP/1.1' + + environ['CONTENT_LENGTH'] = str( + len(event['body']) if event['body'] else '' + ) + + environ['wsgi.url_scheme'] = environ['HTTP_X_FORWARDED_PROTO'] + environ['wsgi.input'] = StringIO(event['body'] or '') + environ['wsgi.version'] = (1, 0) + environ['wsgi.errors'] = sys.stderr + environ['wsgi.multithread'] = False + environ['wsgi.run_once'] = True + environ['wsgi.multiprocess'] = False + + BaseRequest(environ) + + return environ + + +class LambdaResponse(object): + def __init__(self): + self.status = None + self.response_headers = None + + def start_response(self, status, response_headers, exc_info=None): + self.status = int(status[:3]) + self.response_headers = dict(response_headers) + + +class FlaskLambdaHttp(Flask): + def __call__(self, event, context): + if not event.get('requestContext', {}).get('http', None): + return super(FlaskLambdaHttp, self).__call__(event, context) + + response = LambdaResponse() + + body = next(self.wsgi_app( + make_environ(event), + response.start_response + )) + + return { + 'statusCode': response.status, + 'headers': response.response_headers, + 'body': body + } diff --git a/flask_lambda.py b/flask_lambda_rest.py similarity index 91% rename from flask_lambda.py rename to flask_lambda_rest.py index 1a57560..1d564ed 100644 --- a/flask_lambda.py +++ b/flask_lambda_rest.py @@ -31,7 +31,10 @@ except ImportError: from io import StringIO -from werkzeug.wrappers import BaseRequest +try: # werkzeug <= 2.0.3 + from werkzeug.wrappers import BaseRequest +except: # werkzeug > 2.1 + from werkzeug.wrappers import Request as BaseRequest # issue fixed by joranbeasley __version__ = '0.0.4' @@ -88,13 +91,13 @@ def start_response(self, status, response_headers, exc_info=None): self.response_headers = dict(response_headers) -class FlaskLambda(Flask): +class FlaskLambdaRest(Flask): def __call__(self, event, context): if 'httpMethod' not in event: # In this "context" `event` is `environ` and # `context` is `start_response`, meaning the request didn't # occur via API Gateway and Lambda - return super(FlaskLambda, self).__call__(event, context) + return super(FlaskLambdaRest, self).__call__(event, context) response = LambdaResponse() diff --git a/setup.py b/setup.py index 9c44fd5..70bfa80 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ def find_version(*file_paths): setup( name='flask-lambda', - version=find_version('flask_lambda.py'), + version=find_version('flask_lambda_rest.py'), description=('Python module to make Flask compatible with AWS Lambda for ' 'creating RESTful applications'), long_description=long_description,