lambda + SNS enhancements (#1048)
* updates - support lambda messages from SNS - run lambda in docker container * decode output * populate timeout * simplify * whoops * skeletons of cloudwatchlogs * impl filter log streams * fix logging * PEP fixes * PEP fixes * fix reset * fix reset * add new endpoint * fix region name * add docker * try to fix tests * try to fix travis issue with boto * fix escaping in urls * fix environment variables * fix PEP * more pep * switch back to precise * another fix attempt * fix typo * fix lambda invoke * fix more unittests * work on getting this to work in new scheme * fix py2 * fix error * fix tests when running in server mode * more lambda fixes * try running with latest docker adapted from aiodocker * switch to docker python client * pep fixes * switch to docker volume * fix unittest * fix invoke from sns * fix zip2tar * add hack impl for get_function with zip * try fix * fix for py < 3.6 * add volume refcount * try to fix travis * docker test * fix yaml * try fix * update endpoints * fix * another attempt * try again * fix recursive import * refactor fix * revert changes with better fix * more reverts * wait for service to come up * add back detached mode * sleep and add another exception type * put this back for logging * put back with note * whoops :) * docker in docker! * fix invalid url * hopefully last fix! * fix lambda regions * fix protocol * travis!!!! * just run lambda test for now * use one print * fix escaping * another attempt * yet another * re-enable all tests * fixes * fix for py2 * revert change * fix for py2.7 * fix output ordering * remove this given there's a new unittest that covers it * changes based on review - add skeleton logs test file - switch to docker image that matches test env - fix mock_logs import * add readme entry
This commit is contained in:
parent
ca8ce8705b
commit
9008b85299
21 changed files with 836 additions and 167 deletions
|
|
@ -1,34 +1,150 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
import docker.errors
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except:
|
||||
from io import StringIO
|
||||
import uuid
|
||||
import functools
|
||||
import tarfile
|
||||
import calendar
|
||||
import threading
|
||||
import traceback
|
||||
import requests.adapters
|
||||
|
||||
import boto.awslambda
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.utils import unix_time_millis
|
||||
from moto.s3.models import s3_backend
|
||||
from moto.logs.models import logs_backends
|
||||
from moto.s3.exceptions import MissingBucket, MissingKey
|
||||
from moto import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
from tempfile import TemporaryDirectory
|
||||
except ImportError:
|
||||
from backports.tempfile import TemporaryDirectory
|
||||
|
||||
|
||||
_stderr_regex = re.compile(r'START|END|REPORT RequestId: .*')
|
||||
_orig_adapter_send = requests.adapters.HTTPAdapter.send
|
||||
|
||||
|
||||
def zip2tar(zip_bytes):
|
||||
with TemporaryDirectory() as td:
|
||||
tarname = os.path.join(td, 'data.tar')
|
||||
timeshift = int((datetime.datetime.now() -
|
||||
datetime.datetime.utcnow()).total_seconds())
|
||||
with zipfile.ZipFile(io.BytesIO(zip_bytes), 'r') as zipf, \
|
||||
tarfile.TarFile(tarname, 'w') as tarf:
|
||||
for zipinfo in zipf.infolist():
|
||||
if zipinfo.filename[-1] == '/': # is_dir() is py3.6+
|
||||
continue
|
||||
|
||||
tarinfo = tarfile.TarInfo(name=zipinfo.filename)
|
||||
tarinfo.size = zipinfo.file_size
|
||||
tarinfo.mtime = calendar.timegm(zipinfo.date_time) - timeshift
|
||||
infile = zipf.open(zipinfo.filename)
|
||||
tarf.addfile(tarinfo, infile)
|
||||
|
||||
with open(tarname, 'rb') as f:
|
||||
tar_data = f.read()
|
||||
return tar_data
|
||||
|
||||
|
||||
class _VolumeRefCount:
|
||||
__slots__ = "refcount", "volume"
|
||||
|
||||
def __init__(self, refcount, volume):
|
||||
self.refcount = refcount
|
||||
self.volume = volume
|
||||
|
||||
|
||||
class _DockerDataVolumeContext:
|
||||
_data_vol_map = defaultdict(lambda: _VolumeRefCount(0, None)) # {sha256: _VolumeRefCount}
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __init__(self, lambda_func):
|
||||
self._lambda_func = lambda_func
|
||||
self._vol_ref = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._vol_ref.volume.name
|
||||
|
||||
def __enter__(self):
|
||||
# See if volume is already known
|
||||
with self.__class__._lock:
|
||||
self._vol_ref = self.__class__._data_vol_map[self._lambda_func.code_sha_256]
|
||||
self._vol_ref.refcount += 1
|
||||
if self._vol_ref.refcount > 1:
|
||||
return self
|
||||
|
||||
# See if the volume already exists
|
||||
for vol in self._lambda_func.docker_client.volumes.list():
|
||||
if vol.name == self._lambda_func.code_sha_256:
|
||||
self._vol_ref.volume = vol
|
||||
return self
|
||||
|
||||
# It doesn't exist so we need to create it
|
||||
self._vol_ref.volume = self._lambda_func.docker_client.volumes.create(self._lambda_func.code_sha_256)
|
||||
container = self._lambda_func.docker_client.containers.run('alpine', 'sleep 100', volumes={self.name: '/tmp/data'}, detach=True)
|
||||
try:
|
||||
tar_bytes = zip2tar(self._lambda_func.code_bytes)
|
||||
container.put_archive('/tmp/data', tar_bytes)
|
||||
finally:
|
||||
container.remove(force=True)
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
with self.__class__._lock:
|
||||
self._vol_ref.refcount -= 1
|
||||
if self._vol_ref.refcount == 0:
|
||||
try:
|
||||
self._vol_ref.volume.remove()
|
||||
except docker.errors.APIError as e:
|
||||
if e.status_code != 409:
|
||||
raise
|
||||
|
||||
raise # multiple processes trying to use same volume?
|
||||
|
||||
|
||||
class LambdaFunction(BaseModel):
|
||||
|
||||
def __init__(self, spec, validate_s3=True):
|
||||
def __init__(self, spec, region, validate_s3=True):
|
||||
# required
|
||||
self.region = region
|
||||
self.code = spec['Code']
|
||||
self.function_name = spec['FunctionName']
|
||||
self.handler = spec['Handler']
|
||||
self.role = spec['Role']
|
||||
self.run_time = spec['Runtime']
|
||||
self.logs_backend = logs_backends[self.region]
|
||||
self.environment_vars = spec.get('Environment', {}).get('Variables', {})
|
||||
self.docker_client = docker.from_env()
|
||||
|
||||
# Unfortunately mocking replaces this method w/o fallback enabled, so we
|
||||
# need to replace it if we detect it's been mocked
|
||||
if requests.adapters.HTTPAdapter.send != _orig_adapter_send:
|
||||
_orig_get_adapter = self.docker_client.api.get_adapter
|
||||
|
||||
def replace_adapter_send(*args, **kwargs):
|
||||
adapter = _orig_get_adapter(*args, **kwargs)
|
||||
|
||||
if isinstance(adapter, requests.adapters.HTTPAdapter):
|
||||
adapter.send = functools.partial(_orig_adapter_send, adapter)
|
||||
return adapter
|
||||
self.docker_client.api.get_adapter = replace_adapter_send
|
||||
|
||||
# optional
|
||||
self.description = spec.get('Description', '')
|
||||
|
|
@ -36,13 +152,18 @@ class LambdaFunction(BaseModel):
|
|||
self.publish = spec.get('Publish', False) # this is ignored currently
|
||||
self.timeout = spec.get('Timeout', 3)
|
||||
|
||||
self.logs_group_name = '/aws/lambda/{}'.format(self.function_name)
|
||||
self.logs_backend.ensure_log_group(self.logs_group_name, [])
|
||||
|
||||
# this isn't finished yet. it needs to find out the VpcId value
|
||||
self._vpc_config = spec.get(
|
||||
'VpcConfig', {'SubnetIds': [], 'SecurityGroupIds': []})
|
||||
|
||||
# auto-generated
|
||||
self.version = '$LATEST'
|
||||
self.last_modified = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.last_modified = datetime.datetime.utcnow().strftime(
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if 'ZipFile' in self.code:
|
||||
# more hackery to handle unicode/bytes/str in python3 and python2 -
|
||||
# argh!
|
||||
|
|
@ -52,12 +173,13 @@ class LambdaFunction(BaseModel):
|
|||
except Exception:
|
||||
to_unzip_code = base64.b64decode(self.code['ZipFile'])
|
||||
|
||||
zbuffer = io.BytesIO()
|
||||
zbuffer.write(to_unzip_code)
|
||||
zip_file = zipfile.ZipFile(zbuffer, 'r', zipfile.ZIP_DEFLATED)
|
||||
self.code = zip_file.read("".join(zip_file.namelist()))
|
||||
self.code_bytes = to_unzip_code
|
||||
self.code_size = len(to_unzip_code)
|
||||
self.code_sha_256 = hashlib.sha256(to_unzip_code).hexdigest()
|
||||
|
||||
# TODO: we should be putting this in a lambda bucket
|
||||
self.code['UUID'] = str(uuid.uuid4())
|
||||
self.code['S3Key'] = '{}-{}'.format(self.function_name, self.code['UUID'])
|
||||
else:
|
||||
# validate s3 bucket and key
|
||||
key = None
|
||||
|
|
@ -76,10 +198,12 @@ class LambdaFunction(BaseModel):
|
|||
"InvalidParameterValueException",
|
||||
"Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist.")
|
||||
if key:
|
||||
self.code_bytes = key.value
|
||||
self.code_size = key.size
|
||||
self.code_sha_256 = hashlib.sha256(key.value).hexdigest()
|
||||
self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format(
|
||||
self.function_name)
|
||||
|
||||
self.function_arn = 'arn:aws:lambda:{}:123456789012:function:{}'.format(
|
||||
self.region, self.function_name)
|
||||
|
||||
self.tags = dict()
|
||||
|
||||
|
|
@ -94,7 +218,7 @@ class LambdaFunction(BaseModel):
|
|||
return json.dumps(self.get_configuration())
|
||||
|
||||
def get_configuration(self):
|
||||
return {
|
||||
config = {
|
||||
"CodeSha256": self.code_sha_256,
|
||||
"CodeSize": self.code_size,
|
||||
"Description": self.description,
|
||||
|
|
@ -110,70 +234,105 @@ class LambdaFunction(BaseModel):
|
|||
"VpcConfig": self.vpc_config,
|
||||
}
|
||||
|
||||
def get_code(self):
|
||||
if isinstance(self.code, dict):
|
||||
return {
|
||||
"Code": {
|
||||
"Location": "s3://lambda-functions.aws.amazon.com/{0}".format(self.code['S3Key']),
|
||||
"RepositoryType": "S3"
|
||||
},
|
||||
"Configuration": self.get_configuration(),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"Configuration": self.get_configuration(),
|
||||
if self.environment_vars:
|
||||
config['Environment'] = {
|
||||
'Variables': self.environment_vars
|
||||
}
|
||||
|
||||
def convert(self, s):
|
||||
return config
|
||||
|
||||
def get_code(self):
|
||||
return {
|
||||
"Code": {
|
||||
"Location": "s3://awslambda-{0}-tasks.s3-{0}.amazonaws.com/{1}".format(self.region, self.code['S3Key']),
|
||||
"RepositoryType": "S3"
|
||||
},
|
||||
"Configuration": self.get_configuration(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def convert(s):
|
||||
try:
|
||||
return str(s, encoding='utf-8')
|
||||
except:
|
||||
return s
|
||||
|
||||
def is_json(self, test_str):
|
||||
@staticmethod
|
||||
def is_json(test_str):
|
||||
try:
|
||||
response = json.loads(test_str)
|
||||
except:
|
||||
response = test_str
|
||||
return response
|
||||
|
||||
def _invoke_lambda(self, code, event={}, context={}):
|
||||
# TO DO: context not yet implemented
|
||||
try:
|
||||
mycode = "\n".join(['import json',
|
||||
self.convert(self.code),
|
||||
self.convert('print(json.dumps(lambda_handler(%s, %s)))' % (self.is_json(self.convert(event)), context))])
|
||||
def _invoke_lambda(self, code, event=None, context=None):
|
||||
# TODO: context not yet implemented
|
||||
if event is None:
|
||||
event = dict()
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
except Exception as ex:
|
||||
print("Exception %s", ex)
|
||||
|
||||
errored = False
|
||||
try:
|
||||
original_stdout = sys.stdout
|
||||
original_stderr = sys.stderr
|
||||
codeOut = StringIO()
|
||||
codeErr = StringIO()
|
||||
sys.stdout = codeOut
|
||||
sys.stderr = codeErr
|
||||
exec(mycode)
|
||||
exec_err = codeErr.getvalue()
|
||||
exec_out = codeOut.getvalue()
|
||||
result = self.convert(exec_out.strip())
|
||||
if exec_err:
|
||||
result = "\n".join([exec_out.strip(), self.convert(exec_err)])
|
||||
except Exception as ex:
|
||||
errored = True
|
||||
result = '%s\n\n\nException %s' % (mycode, ex)
|
||||
finally:
|
||||
codeErr.close()
|
||||
codeOut.close()
|
||||
sys.stdout = original_stdout
|
||||
sys.stderr = original_stderr
|
||||
return self.convert(result), errored
|
||||
# TODO: I believe we can keep the container running and feed events as needed
|
||||
# also need to hook it up to the other services so it can make kws/s3 etc calls
|
||||
# Should get invoke_id /RequestId from invovation
|
||||
env_vars = {
|
||||
"AWS_LAMBDA_FUNCTION_TIMEOUT": self.timeout,
|
||||
"AWS_LAMBDA_FUNCTION_NAME": self.function_name,
|
||||
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE": self.memory_size,
|
||||
"AWS_LAMBDA_FUNCTION_VERSION": self.version,
|
||||
"AWS_REGION": self.region,
|
||||
}
|
||||
|
||||
env_vars.update(self.environment_vars)
|
||||
|
||||
container = output = exit_code = None
|
||||
with _DockerDataVolumeContext(self) as data_vol:
|
||||
try:
|
||||
run_kwargs = dict(links={'motoserver': 'motoserver'}) if settings.TEST_SERVER_MODE else {}
|
||||
container = self.docker_client.containers.run(
|
||||
"lambci/lambda:{}".format(self.run_time),
|
||||
[self.handler, json.dumps(event)], remove=False,
|
||||
mem_limit="{}m".format(self.memory_size),
|
||||
volumes=["{}:/var/task".format(data_vol.name)], environment=env_vars, detach=True, **run_kwargs)
|
||||
finally:
|
||||
if container:
|
||||
exit_code = container.wait()
|
||||
output = container.logs(stdout=False, stderr=True)
|
||||
output += container.logs(stdout=True, stderr=False)
|
||||
container.remove()
|
||||
|
||||
output = output.decode('utf-8')
|
||||
|
||||
# Send output to "logs" backend
|
||||
invoke_id = uuid.uuid4().hex
|
||||
log_stream_name = "{date.year}/{date.month:02d}/{date.day:02d}/[{version}]{invoke_id}".format(
|
||||
date=datetime.datetime.utcnow(), version=self.version, invoke_id=invoke_id
|
||||
)
|
||||
|
||||
self.logs_backend.create_log_stream(self.logs_group_name, log_stream_name)
|
||||
|
||||
log_events = [{'timestamp': unix_time_millis(), "message": line}
|
||||
for line in output.splitlines()]
|
||||
self.logs_backend.put_log_events(self.logs_group_name, log_stream_name, log_events, None)
|
||||
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
'lambda invoke failed output: {}'.format(output))
|
||||
|
||||
# strip out RequestId lines
|
||||
output = os.linesep.join([line for line in self.convert(output).splitlines() if not _stderr_regex.match(line)])
|
||||
return output, False
|
||||
except BaseException as e:
|
||||
traceback.print_exc()
|
||||
return "error running lambda: {}".format(e), True
|
||||
|
||||
def invoke(self, body, request_headers, response_headers):
|
||||
payload = dict()
|
||||
|
||||
if body:
|
||||
body = json.loads(body)
|
||||
|
||||
# Get the invocation type:
|
||||
res, errored = self._invoke_lambda(code=self.code, event=body)
|
||||
if request_headers.get("x-amz-invocation-type") == "RequestResponse":
|
||||
|
|
@ -189,7 +348,8 @@ class LambdaFunction(BaseModel):
|
|||
return result
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json,
|
||||
region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
|
||||
# required
|
||||
|
|
@ -212,17 +372,19 @@ class LambdaFunction(BaseModel):
|
|||
# this snippet converts this plaintext code to a proper base64-encoded ZIP file.
|
||||
if 'ZipFile' in properties['Code']:
|
||||
spec['Code']['ZipFile'] = base64.b64encode(
|
||||
cls._create_zipfile_from_plaintext_code(spec['Code']['ZipFile']))
|
||||
cls._create_zipfile_from_plaintext_code(
|
||||
spec['Code']['ZipFile']))
|
||||
|
||||
backend = lambda_backends[region_name]
|
||||
fn = backend.create_function(spec)
|
||||
return fn
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
from moto.cloudformation.exceptions import \
|
||||
UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
region = 'us-east-1'
|
||||
return 'arn:aws:lambda:{0}:123456789012:function:{1}'.format(region, self.function_name)
|
||||
return 'arn:aws:lambda:{0}:123456789012:function:{1}'.format(
|
||||
self.region, self.function_name)
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -236,7 +398,6 @@ class LambdaFunction(BaseModel):
|
|||
|
||||
|
||||
class EventSourceMapping(BaseModel):
|
||||
|
||||
def __init__(self, spec):
|
||||
# required
|
||||
self.function_name = spec['FunctionName']
|
||||
|
|
@ -246,10 +407,12 @@ class EventSourceMapping(BaseModel):
|
|||
# optional
|
||||
self.batch_size = spec.get('BatchSize', 100)
|
||||
self.enabled = spec.get('Enabled', True)
|
||||
self.starting_position_timestamp = spec.get('StartingPositionTimestamp', None)
|
||||
self.starting_position_timestamp = spec.get('StartingPositionTimestamp',
|
||||
None)
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json,
|
||||
region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
spec = {
|
||||
'FunctionName': properties['FunctionName'],
|
||||
|
|
@ -264,12 +427,12 @@ class EventSourceMapping(BaseModel):
|
|||
|
||||
|
||||
class LambdaVersion(BaseModel):
|
||||
|
||||
def __init__(self, spec):
|
||||
self.version = spec['Version']
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json,
|
||||
region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
spec = {
|
||||
'Version': properties.get('Version')
|
||||
|
|
@ -278,9 +441,14 @@ class LambdaVersion(BaseModel):
|
|||
|
||||
|
||||
class LambdaBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, region_name):
|
||||
self._functions = {}
|
||||
self.region_name = region_name
|
||||
|
||||
def reset(self):
|
||||
region_name = self.region_name
|
||||
self.__dict__ = {}
|
||||
self.__init__(region_name)
|
||||
|
||||
def has_function(self, function_name):
|
||||
return function_name in self._functions
|
||||
|
|
@ -289,7 +457,7 @@ class LambdaBackend(BaseBackend):
|
|||
return self.get_function_by_arn(function_arn) is not None
|
||||
|
||||
def create_function(self, spec):
|
||||
fn = LambdaFunction(spec)
|
||||
fn = LambdaFunction(spec, self.region_name)
|
||||
self._functions[fn.function_name] = fn
|
||||
return fn
|
||||
|
||||
|
|
@ -308,6 +476,42 @@ class LambdaBackend(BaseBackend):
|
|||
def list_functions(self):
|
||||
return self._functions.values()
|
||||
|
||||
def send_message(self, function_name, message):
|
||||
event = {
|
||||
"Records": [
|
||||
{
|
||||
"EventVersion": "1.0",
|
||||
"EventSubscriptionArn": "arn:aws:sns:EXAMPLE",
|
||||
"EventSource": "aws:sns",
|
||||
"Sns": {
|
||||
"SignatureVersion": "1",
|
||||
"Timestamp": "1970-01-01T00:00:00.000Z",
|
||||
"Signature": "EXAMPLE",
|
||||
"SigningCertUrl": "EXAMPLE",
|
||||
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
|
||||
"Message": message,
|
||||
"MessageAttributes": {
|
||||
"Test": {
|
||||
"Type": "String",
|
||||
"Value": "TestString"
|
||||
},
|
||||
"TestBinary": {
|
||||
"Type": "Binary",
|
||||
"Value": "TestBinary"
|
||||
}
|
||||
},
|
||||
"Type": "Notification",
|
||||
"UnsubscribeUrl": "EXAMPLE",
|
||||
"TopicArn": "arn:aws:sns:EXAMPLE",
|
||||
"Subject": "TestInvoke"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
self._functions[function_name].invoke(json.dumps(event), {}, {})
|
||||
pass
|
||||
|
||||
def list_tags(self, resource):
|
||||
return self.get_function_by_arn(resource).tags
|
||||
|
||||
|
|
@ -328,10 +532,8 @@ def do_validate_s3():
|
|||
return os.environ.get('VALIDATE_LAMBDA_S3', '') in ['', '1', 'true']
|
||||
|
||||
|
||||
lambda_backends = {}
|
||||
for region in boto.awslambda.regions():
|
||||
lambda_backends[region.name] = LambdaBackend()
|
||||
|
||||
# Handle us forgotten regions, unless Lambda truly only runs out of US and
|
||||
for region in ['ap-southeast-2']:
|
||||
lambda_backends[region] = LambdaBackend()
|
||||
lambda_backends = {_region.name: LambdaBackend(_region.name)
|
||||
for _region in boto.awslambda.regions()}
|
||||
|
||||
lambda_backends['ap-southeast-2'] = LambdaBackend('ap-southeast-2')
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ response = LambdaResponse()
|
|||
|
||||
url_paths = {
|
||||
'{0}/(?P<api_version>[^/]+)/functions/?$': response.root,
|
||||
'{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/?$': response.function,
|
||||
'{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/invocations/?$': response.invoke,
|
||||
'{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/invoke-async/?$': response.invoke_async,
|
||||
'{0}/(?P<api_version>[^/]+)/tags/(?P<resource_arn>.+)': response.tag
|
||||
r'{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/?$': response.function,
|
||||
r'{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/invocations/?$': response.invoke,
|
||||
r'{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/invoke-async/?$': response.invoke_async,
|
||||
r'{0}/(?P<api_version>[^/]+)/tags/(?P<resource_arn>.+)': response.tag
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue