├── .gitignore ├── LambdaServer └── __init__.py ├── README.md ├── bin └── bstpy ├── event.json ├── example └── TestSkill.py ├── setup.py └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /venv 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 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 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # PyCharm 62 | .idea 63 | -------------------------------------------------------------------------------- /LambdaServer/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from http.server import BaseHTTPRequestHandler, HTTPServer 4 | from importlib import import_module, reload 5 | import os 6 | import sys 7 | import traceback 8 | import json 9 | import pkg_resources # part of setuptools 10 | 11 | sys.path.append('.') 12 | 13 | __author__ = 'OpenDog' 14 | __description__ = 'A local http server that exposes an AWS lambda.' 15 | 16 | version = pkg_resources.require("LambdaServer")[0].version 17 | 18 | 19 | def main(argv): 20 | import time 21 | import getopt 22 | 23 | server_port = 10000 24 | timezone = 'UTC' 25 | 26 | usage = 'Usage: bstpy -p -t ' 27 | 28 | if len(argv) == 0: 29 | print(usage) 30 | sys.exit(2) 31 | 32 | lambda_path = argv[0] 33 | 34 | # Don't start with options 35 | 36 | if lambda_path.startswith("-"): 37 | print(usage) 38 | sys.exit(2) 39 | 40 | try: 41 | opts, args = getopt.getopt(argv[1:], "hp:t:v", ['help', 'port=', 'timezone=', 'version']) 42 | except getopt.GetoptError as err: 43 | print(str(err)) 44 | print(usage) 45 | sys.exit(2) 46 | 47 | for opt, arg in opts: 48 | if opt in ("-h", "--help"): 49 | print(usage) 50 | sys.exit() 51 | elif opt in ("-p", "--port"): 52 | try: 53 | server_port=int(arg) 54 | except ValueError: 55 | print("Invalid port: {}".format(server_port)) 56 | print(usage) 57 | sys.exit(3) 58 | elif opt in ("-t", "--timezone"): 59 | timezone = arg 60 | elif opt in ("-v", "--version"): 61 | print("Python Lambda Server {}".format(version)) 62 | sys.exit() 63 | else: 64 | assert False, "unhandled option" 65 | 66 | os.environ['TZ'] = timezone 67 | time.tzset() 68 | 69 | if lambda_path == '': 70 | print("Lambda path is mandatory!") 71 | print(usage) 72 | sys.exit() 73 | else: 74 | print("Lambda path: {}".format(lambda_path)) 75 | 76 | print("Current time is {}".format(time.strftime('%X %x %Z'))) 77 | 78 | run(lambda_path, port=server_port) 79 | 80 | 81 | def run(lambda_path, server_class=HTTPServer, port=10000): 82 | class LambdaContext: 83 | def __init__(self, function_name): 84 | self.function_name = function_name 85 | 86 | @staticmethod 87 | def get_remaining_time_in_millis(): 88 | return 999999 89 | 90 | function_name = 'your-lambda-function' 91 | function_version = '2.0-SNAPSHOT' 92 | invoked_function_arn = 'arn-long-aws-id' 93 | memory_limit_in_mb = '128' 94 | aws_request_id = 'aws-request-test-id' 95 | log_group_name = 'log-group-bst' 96 | log_stream_name = 'bst-stream' 97 | identity = None 98 | client_context = None 99 | 100 | def import_lambda(path): 101 | try: 102 | # Parse path into module and function name. 103 | path = str(path) 104 | 105 | if '/' in path or '\\' in path: 106 | raise ValueError() 107 | 108 | spath = path.split('.') 109 | module = '.'.join(spath[:-1]) 110 | function = spath[-1] 111 | 112 | # Import the module and get the function. 113 | import_module(module) 114 | 115 | # Comes from the cache if we don't reload 116 | reload(sys.modules[module]) 117 | 118 | return getattr(sys.modules[module], function) 119 | 120 | except (AttributeError, TypeError) as e: 121 | print("\nOops! There was a problem finding your function.\n") 122 | raise e 123 | except ImportError: 124 | print("\nOops! There was problem loading your module. Is the module visible from your PYTHONPATH?\n") 125 | sys.exit(1) 126 | except ValueError: 127 | print("\nOops! It seems you pointed the tool to Python file. This argument must be\n" 128 | "a module path, in the form of [module 1].[module 2...n].[function]\n" + 129 | "Also make sure the module is seen from your PYTHONPATH.\n") 130 | sys.exit(1) 131 | 132 | class S(BaseHTTPRequestHandler): 133 | def _set_headers(self, output_string): 134 | self.send_response(200) 135 | self.send_header('Content-Type', 'application/json') 136 | self.send_header('Content-Length', str(len(output_string))) 137 | self.end_headers() 138 | 139 | def do_POST(self): 140 | data_string = self.rfile.read(int(self.headers['Content-Length'])) 141 | 142 | print("==> {}".format(data_string)) 143 | 144 | data = json.loads(data_string) 145 | 146 | # noinspection PyBroadException 147 | try: 148 | context = LambdaContext(lambda_path) 149 | handler = import_lambda(lambda_path) 150 | r = handler(data, context) 151 | except Exception as e: 152 | print("\nOops! There was a problem in your lambda function: {}\n".format(lambda_path)) 153 | print(traceback.format_exc()) 154 | r = { 155 | 'error': str(e) 156 | } 157 | 158 | return_value = json.dumps(r, indent=4 * ' ') 159 | self._set_headers(return_value); 160 | 161 | self.wfile.write(bytes(return_value, encoding='utf8')) 162 | #self.wfile.close() 163 | 164 | print("<=== {}".format(return_value)) 165 | 166 | return 167 | 168 | server_address = ('', port) 169 | httpd = server_class(server_address, S) 170 | print("Starting httpd on port " + str(port)) 171 | httpd.serve_forever() 172 | 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bstpy 2 | 3 | **Python AWS Lambda Server** 4 | 5 | ## Recommended Uses 6 | Use `bstpy` to expose an AWS Lambda as an http endpoint locally. It provides a Python "harness" that you can use to wrap your 7 | function and run/analyze it. 8 | 9 | - Development 10 | - Run your lambda functions instantly locally, without packaging and sending to AWS. 11 | - Shorten your feedback loop on lambda executions. 12 | 13 | - Testing 14 | - Use it with other bespoken tools, like the bst Alexa emulator or bst proxy, to instantly test your Alexa skill. 15 | 16 | ## Features 17 | Present: 18 | - Run an AWS-compatible lambda function as an http endpoint. 19 | - Automatically reloads your skill so you can see your changes without restarting it. 20 | 21 | 22 | Planned: 23 | - accessing AWS resources 24 | - picking up any libraries present in ``./lib`` directory of the project 25 | - context from file 26 | 27 | ## Installation 28 | 1. `git clone` [the bstpy repo](https://github.com/bespoken/bstpy.git) 29 | 2. Install it with `pip install -e bstpy` (you might need to sudo this command if you're using your system Python instead of a virtualenv or similar) 30 | 31 | 32 | ## Usage 33 | 34 | ``` 35 | $ bstpy --help 36 | Usage: bstpy -p -t 37 | ``` 38 | 39 | The only mandatory paramater is the first parameter, the lambda path. 40 | 41 | Use -p (--port) to listen on another port. 42 | 43 | Use -t (--timezone) to specify the timezone you want to run on. The default is UTC. 44 | 45 | ``` 46 | $ bstpy foo -t US/Eastern 47 | ``` 48 | 49 | Yo can also use -h (--help) for help and -v (--version) for version. 50 | 51 | ## Quick Start 52 | 53 | ### Try the example lambda in the project 54 | 55 | From the example folder, run: 56 | `bstpy TestSkill.example_handler` 57 | 58 | You should see output similar to the following: 59 | ``` 60 | Lambda path: foo 61 | Current time is 14:04:42 10/17/16 UTC 62 | Starting httpd on port 10000 63 | ``` 64 | 65 | Note: This is the simplest way to call a handler in a Python file. 66 | Alternatively you can specify a module and a function like this: Module1[.Module2][.Module3].handler_function, 67 | just make sure the module is seen from your Python path. The script will add your current folder (.) to the path (your welcome). 68 | 69 | From another terminal window in the project root folder, run: 70 | `./test.sh` 71 | 72 | Note: You need to have curl installed for this. Of course you can use your own favorites, like Postman. 73 | 74 | #### What's happening? 75 | 76 | In this example, `bstpy`: 77 | 1. Loads the `example_handler` function from the `TestSkill` module (file in this case). 78 | 1. Starts a simple http server on port 10000 and starts listening for POST request with json bodies (events) 79 | 1. Curl will post the content of the event.json file 80 | 1. LambdaServer calls the the lambda handler 81 | 1. Returns the resulting json 82 | -------------------------------------------------------------------------------- /bin/bstpy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import LambdaServer 3 | from sys import argv 4 | 5 | if __name__ == '__main__': 6 | LambdaServer.main(argv[1:]) 7 | -------------------------------------------------------------------------------- /event.json: -------------------------------------------------------------------------------- 1 | { 2 | "session": { 3 | "sessionId": "SessionId.6ab325dd-xxxx-xxxx-aee5-456cd330932a", 4 | "application": { 5 | "applicationId": "amzn1.echo-sdk-ams.app.bd304b90-xxxx-xxxx-xxxx-xxxxd4772bab" 6 | }, 7 | "attributes": {}, 8 | "user": { 9 | "userId": "amzn1.ask.account.XXXXXX" 10 | }, 11 | "new": true 12 | }, 13 | "request": { 14 | "type": "IntentRequest", 15 | "requestId": "EdwRequestId.b851ed18-2ca8-xxxx-xxxx-cca3f2b521e4", 16 | "timestamp": "2016-07-05T15:27:34Z", 17 | "intent": { 18 | "name": "GetTrainTimes", 19 | "slots": { 20 | "Station": { 21 | "name": "Station", 22 | "value": "Balboa Park" 23 | } 24 | } 25 | }, 26 | "locale": "en-US" 27 | }, 28 | "version": "1.0" 29 | } 30 | 31 | -------------------------------------------------------------------------------- /example/TestSkill.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | 4 | def example_handler(event, context): 5 | 6 | # Your awesome skill code goes here 7 | 8 | try: 9 | session_id = event['session']['sessionId'] 10 | except KeyError as e: 11 | session_id = 'NO_SESSION' 12 | 13 | memory = context.memory_limit_in_mb 14 | 15 | return { 16 | 'version': '1.0', 17 | 'response': { 18 | 'outputSpeech': { 19 | 'text': 'BST tools are the best!', 20 | 'type': 'PlainText' 21 | }, 22 | 'shouldEndSession': False, 23 | 'reprompt': { 24 | 'outputSpeech': { 25 | 'text': 'BST tools are the best!', 26 | 'type': 'PlainText' 27 | } 28 | }, 29 | 'card': { 30 | 'content': 'BST Python Server', 31 | 'type': 'Simple', 32 | 'title': 'BST Tools' 33 | } 34 | }, 35 | 'sessionAttributes': { 36 | 'lastSession': session_id, 37 | 'memory': memory 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | 4 | setup( 5 | name='LambdaServer', 6 | version='0.3', 7 | packages=['LambdaServer'], 8 | scripts=['bin/bstpy'], 9 | url='https://github.com/bespoken/bstpy', 10 | license='MIT', 11 | author='OpenDog', 12 | author_email='bela@xappmedia.com', 13 | description='Python http server to expose an AWS lambda', 14 | install_requires=[ 15 | 16 | ], 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | curl -i -H "Accept: application/json" -X POST -d @event.json http://localhost:10000 4 | 5 | --------------------------------------------------------------------------------