├── .gitignore ├── .gitlab-ci.yml ├── LICENSE ├── README.md ├── aws_lambda_wsgi └── __init__.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | #ide 107 | .idea/ 108 | .vscode/ 109 | 110 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: python:3.6-stretch 2 | 3 | before_script: 4 | - python -V # Print out python version for debugging 5 | - pip install --upgrade setuptools twine wheel 6 | 7 | build: 8 | stage: build 9 | script: 10 | - python3 setup.py sdist bdist_wheel 11 | artifacts: 12 | paths: 13 | - dist/aws_lambda_wsgi* 14 | 15 | deploy to pypi: 16 | stage: deploy 17 | only: 18 | - /^v([0-9]{1,2}\.)+[0-9]{1,3}$/ 19 | environment: 20 | name: production 21 | url: https://pypi.org/project/aws-lambda-wsgi/ 22 | script: 23 | - twine upload dist/* 24 | when: on_success 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 TruckPad / labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aws-lambda-wsgi 2 | =============== 3 | 4 | A WSGI adapter for AWS API Gateway/Lambda Proxy Integration 5 | ----------------------------------------------------------- 6 | 7 | AWS-Lambda-WSGI allows you to use WSGI-compatible middleware and frameworks like Bottle, Django and Flask with the [AWS API Gateway/Lambda proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html). 8 | 9 | Based on [awsgi, by Matthew Wedgwood](https://github.com/slank/awsgi). 10 | 11 | Installation 12 | ------------ 13 | 14 | `aws_lambda_wsgi` is available from PyPI as `aws_lambda_wsgi`: 15 | 16 | ``` 17 | pip install aws_lambda_wsgi 18 | ``` 19 | 20 | Example 21 | ------- 22 | 23 | ```python 24 | import aws_lambda_wsgi 25 | from bottle import Bottle 26 | 27 | app = Bottle() 28 | 29 | 30 | @app.route('/') 31 | def index(): 32 | return {'message': 'OK'} 33 | 34 | 35 | def lambda_handler(event, context): 36 | return aws_lambda_wsgi.response(app, event, context) 37 | ``` -------------------------------------------------------------------------------- /aws_lambda_wsgi/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import sys 3 | from io import StringIO, BytesIO 4 | 5 | try: 6 | # Python 3 7 | from urllib.parse import urlencode 8 | 9 | # Convert bytes to str, if required 10 | def convert_str(s): 11 | try: 12 | return s.decode('utf-8') 13 | except UnicodeDecodeError: 14 | return s 15 | except: 16 | # Python 2 17 | from urllib import urlencode 18 | 19 | # No conversion required 20 | def convert_str(s): 21 | return s 22 | 23 | 24 | def response(app, event, context): 25 | sr = StartResponse() 26 | output = app(environ(event, context), sr) 27 | return sr.response(output) 28 | 29 | 30 | class StartResponse: 31 | def __init__(self): 32 | self.status = 500 33 | self.headers = [] 34 | self.body = StringIO() 35 | 36 | def __call__(self, status, headers, exc_info=None): 37 | self.status = status.split()[0] 38 | self.headers[:] = headers 39 | return self.body.write 40 | 41 | def response(self, output): 42 | headers = dict(self.headers) 43 | if headers.get('Content-Type') in ['image/png', 'image/gif', 'application/octet-stream']: 44 | is_base64 = True 45 | body = base64.b64encode(b''.join(output)).decode('ascii') 46 | else: 47 | is_base64 = False 48 | body = self.body.getvalue() + ''.join(map(convert_str, output)) 49 | return { 50 | 'statusCode': str(self.status), 51 | 'headers': headers, 52 | 'body': body, 53 | 'isBase64Encoded': is_base64 54 | } 55 | 56 | 57 | def environ(event, context): 58 | body = b'' 59 | str_body = event.get('body') 60 | if str_body: 61 | body = bytes(str_body, 'utf-8') 62 | environ = { 63 | 'REQUEST_METHOD': event['httpMethod'], 64 | 'SCRIPT_NAME': '', 65 | 'PATH_INFO': event['path'], 66 | 'QUERY_STRING': urlencode(event['queryStringParameters'] or {}), 67 | 'REMOTE_ADDR': '127.0.0.1', 68 | 'CONTENT_LENGTH': str(len(body) or ''), 69 | 'HTTP': 'on', 70 | 'SERVER_PROTOCOL': 'HTTP/1.1', 71 | 'wsgi.version': (1, 0), 72 | 'wsgi.input': BytesIO(body), 73 | 'wsgi.errors': sys.stderr, 74 | 'wsgi.multithread': False, 75 | 'wsgi.multiprocess': False, 76 | 'wsgi.run_once': False, 77 | } 78 | headers = event.get('headers', {}) 79 | if headers: 80 | for k, v in headers.items(): 81 | k = k.upper().replace('-', '_') 82 | 83 | if k == 'CONTENT_TYPE': 84 | environ['CONTENT_TYPE'] = v 85 | elif k == 'HOST': 86 | environ['SERVER_NAME'] = v 87 | elif k == 'X_FORWARDED_FOR': 88 | environ['REMOTE_ADDR'] = v.split(', ')[0] 89 | elif k == 'X_FORWARDED_PROTO': 90 | environ['wsgi.url_scheme'] = v 91 | elif k == 'X_FORWARDED_PORT': 92 | environ['SERVER_PORT'] = v 93 | 94 | environ['HTTP_' + k] = v 95 | 96 | return environ 97 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truckpad/aws-lambda-wsgi/695279c6efac66d70968e13dc3b3d1d7e2323269/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import subprocess 6 | 7 | from setuptools import setup 8 | 9 | 10 | # Utility function to read the README file. 11 | # Used for the long_description. It's nice, because now 1) we have a top level 12 | # README file and 2) it's easier to type in the README file than to put a raw 13 | # string in below ... 14 | def read(fname): 15 | return open(os.path.join(os.path.dirname(__file__), fname), encoding='utf-8').read() 16 | 17 | 18 | def version(): 19 | git_version = None 20 | try: 21 | git_tag = subprocess.check_output(['git', 'describe', '--tags']) 22 | if git_tag: 23 | git_version = git_tag.strip()[1:].decode('utf-8') 24 | except: 25 | pass 26 | if not git_version: 27 | git_version = 'SNAPSHOT' 28 | return git_version 29 | 30 | 31 | setup( 32 | name='aws_lambda_wsgi', 33 | version=version(), 34 | packages=['aws_lambda_wsgi'], 35 | description='AWS Lambda WSGI - WSGI adapter for AWS API Gateway/Lambda Proxy Integration', 36 | long_description=read('README.md'), 37 | long_description_content_type="text/markdown", 38 | author='Marcos Araujo Sobrinho', 39 | author_email='marcos.sobrinho@truckpad.com.br', 40 | url='https://github.com/truckpad/aws-lambda-wsgi', 41 | install_requires=read('requirements.txt').strip().split('\n'), 42 | classifiers=[ 43 | 'Development Status :: 2 - Pre-Alpha', 44 | 'Operating System :: POSIX', 45 | 'Programming Language :: Python :: 3', 46 | 'Topic :: Internet :: WWW/HTTP' 47 | ] 48 | ) 49 | --------------------------------------------------------------------------------