mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
196 lines
5.3 KiB
Python
Executable File
196 lines
5.3 KiB
Python
Executable File
#!/usr/bin/env python2.7
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import StringIO
|
|
import traceback
|
|
import contextlib
|
|
import imp
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog='run_handler',
|
|
description='Runs a Lambda entry point (handler) with an optional event',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--event', dest='event',
|
|
type=json.loads,
|
|
help=("The event that will be deserialized and passed to the function. "
|
|
"This has to be valid JSON, and will be deserialized into a "
|
|
"Python dictionary before your handler is invoked")
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--handler-path', dest='handler_path',
|
|
help=("File path to the handler, e.g. `lib/function.py`")
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--handler-function', dest='handler_function',
|
|
default='lambda_handler',
|
|
help=("File path to the handler")
|
|
)
|
|
|
|
|
|
class FakeLambdaContext(object):
|
|
def __init__(self, name='Fake', version='LATEST'):
|
|
self.name = name
|
|
self.version = version
|
|
|
|
@property
|
|
def get_remaining_time_in_millis(self):
|
|
return 10000
|
|
|
|
@property
|
|
def function_name(self):
|
|
return self.name
|
|
|
|
@property
|
|
def function_version(self):
|
|
return self.version
|
|
|
|
@property
|
|
def invoked_function_arn(self):
|
|
return 'arn:aws:lambda:serverless:' + self.name
|
|
|
|
@property
|
|
def memory_limit_in_mb(self):
|
|
return 1024
|
|
|
|
@property
|
|
def aws_request_id(self):
|
|
return '1234567890'
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def preserve_value(namespace, name):
|
|
"""A context manager to restore a binding to its prior value
|
|
|
|
At the beginning of the block, `__enter__`, the value specified is
|
|
saved, and is restored when `__exit__` is called on the contextmanager
|
|
|
|
namespace (object): Some object with a binding
|
|
name (string): The name of the binding to be preserved.
|
|
"""
|
|
saved_value = getattr(namespace, name)
|
|
yield
|
|
setattr(namespace, name, saved_value)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def capture_fds(stdout=None, stderr=None):
|
|
"""Replace stdout and stderr with a different file handle.
|
|
|
|
Call with no arguments to just ignore stdout or stderr.
|
|
"""
|
|
orig_stdout, orig_stderr = sys.stdout, sys.stderr
|
|
orig_stdout.flush()
|
|
orig_stderr.flush()
|
|
|
|
temp_stdout = stdout or StringIO.StringIO()
|
|
temp_stderr = stderr or StringIO.StringIO()
|
|
sys.stdout, sys.stderr = temp_stdout, temp_stderr
|
|
|
|
yield
|
|
|
|
sys.stdout = orig_stdout
|
|
sys.stderr = orig_stderr
|
|
|
|
temp_stdout.flush()
|
|
temp_stdout.seek(0)
|
|
temp_stderr.flush()
|
|
temp_stderr.seek(0)
|
|
|
|
|
|
def make_module_from_file(module_name, module_filepath):
|
|
"""Make a new module object from the source code in specified file.
|
|
|
|
:param module_name: Desired name (must be valid import name)
|
|
:param module_filepath: The filesystem path with the Python source
|
|
:return: A loaded module
|
|
|
|
The Python import mechanism is not used. No cached bytecode
|
|
file is created, and no entry is placed in `sys.modules`.
|
|
"""
|
|
py_source_open_mode = 'U'
|
|
py_source_description = (".py", py_source_open_mode, imp.PY_SOURCE)
|
|
|
|
with open(module_filepath, py_source_open_mode) as module_file:
|
|
with preserve_value(sys, 'dont_write_bytecode'):
|
|
sys.dont_write_bytecode = True
|
|
module = imp.load_module(
|
|
module_name,
|
|
module_file,
|
|
module_filepath,
|
|
py_source_description
|
|
)
|
|
return module
|
|
|
|
|
|
def bail_out(code=99):
|
|
output = {
|
|
'success': False,
|
|
'exception': traceback.format_exception(*sys.exc_info()),
|
|
}
|
|
print(json.dumps(output))
|
|
sys.exit(code)
|
|
|
|
|
|
def import_program_as_module(handler_file):
|
|
"""Import module from `handler_file` and return it to be used.
|
|
|
|
Since we don't want to clutter up the filesystem, we're going to turn
|
|
off bytecode generation (.pyc file creation)
|
|
"""
|
|
module = make_module_from_file('lambda_handler', handler_file)
|
|
sys.modules['lambda_handler'] = module
|
|
|
|
return module
|
|
|
|
|
|
def run_with_context(handler, function_path, event=None):
|
|
function = getattr(handler, function_path)
|
|
return function(event or {}, FakeLambdaContext())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
args = parser.parse_args(sys.argv[1:])
|
|
path = os.path.expanduser(args.handler_path)
|
|
if not os.path.isfile(path):
|
|
message = (u'There is no such file "{}". --handler-path must be a '
|
|
u'Python file'.format(path))
|
|
print(json.dumps({"success": False, "exception": message}))
|
|
sys.exit(100)
|
|
|
|
try:
|
|
handler = import_program_as_module(path)
|
|
except Exception as e:
|
|
bail_out()
|
|
|
|
stdout, stderr = StringIO.StringIO(), StringIO.StringIO()
|
|
output = {}
|
|
with capture_fds(stdout, stderr):
|
|
try:
|
|
result = run_with_context(handler, args.handler_function, args.event)
|
|
output['result'] = result
|
|
except Exception as e:
|
|
message = u'Failure running handler function {f} from file {file}:\n{tb}'
|
|
output['exception'] = message.format(
|
|
f=args.handler_function,
|
|
file=path,
|
|
tb=traceback.format_exception(*sys.exc_info()),
|
|
)
|
|
output['success'] = False
|
|
else:
|
|
output['success'] = True
|
|
output.update({
|
|
'stdout': stdout.read(),
|
|
'stderr': stderr.read(),
|
|
})
|
|
|
|
print(json.dumps(output))
|