diff --git a/.travis.yml b/.travis.yml index 2a7fe3b18..a9ce519c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ install: - "pip install setuptools --upgrade; pip install -r test_requirements.txt; pip install -e git+https://github.com/django/django-contrib-comments.git#egg=django-contrib-comments; python setup.py install" # command to run tests env: - - TESYCASE=tests/tests_docs.py + - TESTCASE=tests/tests_docs.py - TESTCASE=tests/test_handler.py - TESTCASE=tests/tests_middleware.py - TESTCASE=tests/tests_placebo.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dcbf6532..883f2b3e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Zappa Changelog +## 0.47.0 +* Support for SQS events +* Added test to enforce running of doctoc +* Add support for running django as a WSGI app (for NewRelic and others) +* Updates AWS regions for lambda and API Gateway +* Fix support for gcloud and other packages with slim_handler +* Add --disable-keep-open to zappa tail +* Dependency updates +* Fix pyenv invocation +* Add custom base_path stripping support +* Multiple documentation fixes and improvements +* first iteration of a documented deploy policy + +## 0.46.2 +* hotfix for creating virtual environments + ## 0.46.1 * Hotfix for pipenv support (pip >10.0.1) * Adds AWS GovCloud support! diff --git a/README.md b/README.md index c1c53d2d4..cc45c1633 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ - [IAM Policy](#iam-policy) - [API Gateway Lambda Authorizers](#api-gateway-lambda-authorizers) - [Cognito User Pool Authorizer](#cognito-user-pool-authorizer) + - [API Gateway Resource Policy](#api-gateway-resource-policy) - [Setting Environment Variables](#setting-environment-variables) - [Local Environment Variables](#local-environment-variables) - [Remote AWS Environment Variables](#remote-aws-environment-variables) @@ -156,7 +157,7 @@ This will automatically detect your application type (Flask/Django - Pyramid use // The name of your stage "dev": { // The name of your S3 bucket - "s3_bucket": "lmbda", + "s3_bucket": "lambda", // The modular python path to your WSGI application function. // In Flask and Bottle, this is your 'app' object. @@ -174,7 +175,7 @@ or for Django: ```javascript { "dev": { // The name of your stage - "s3_bucket": "lmbda", // The name of your S3 bucket + "s3_bucket": "lambda", // The name of your S3 bucket "django_settings": "your_project.settings" // The python path to your Django settings. } } @@ -460,7 +461,7 @@ However, it's now far easier to use Route 53-based DNS authentication, which wil ## Executing in Response to AWS Events -Similarly, you can have your functions execute in response to events that happen in the AWS ecosystem, such as S3 uploads, DynamoDB entries, Kinesis streams, and SNS messages. +Similarly, you can have your functions execute in response to events that happen in the AWS ecosystem, such as S3 uploads, DynamoDB entries, Kinesis streams, SNS messages, and SQS queues. In your *zappa_settings.json* file, define your [event sources](http://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html) and the function you wish to execute. For instance, this will execute `your_module.process_upload_function` in response to new objects in your `my-bucket` S3 bucket. Note that `process_upload_function` must accept `event` and `context` parameters. @@ -558,6 +559,21 @@ Optionally you can add [SNS message filters](http://docs.aws.amazon.com/sns/late ] ``` +[SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) is also pulling messages from a stream. At this time, [only "Standard" queues can trigger lambda events, not "FIFO" queues](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html). Read the AWS Documentation carefully since Lambda calls the SQS DeleteMessage API on your behalf once your function completes successfully. + +```javascript + "events": [ + { + "function": "your_module.process_messages", + "event_source": { + "arn": "arn:aws:sqs:us-east-1:12341234:your-queue-name-arn", + "batch_size": 10, // Max: 10. Use 1 to trigger immediate processing + "enabled": true // Default is false + } + } + ] +``` + For configuring Lex Bot's intent triggered events: ```javascript "bot_events": [ @@ -805,6 +821,7 @@ to change Zappa's behavior. Use these at your own risk! "apigateway_description": "My funky application!", // Define a custom description for the API Gateway console. Default None. "assume_policy": "my_assume_policy.json", // optional, IAM assume policy JSON file "attach_policy": "my_attach_policy.json", // optional, IAM attach policy JSON file + "apigateway_policy": "my_apigateway_policy.json", // optional, API Gateway resource policy JSON file "async_source": "sns", // Source of async tasks. Defaults to "lambda" "async_resources": true, // Create the SNS topic and DynamoDB table to use. Defaults to true. "async_response_table": "your_dynamodb_table_name", // the DynamoDB table name to use for captured async responses; defaults to None (can't capture) @@ -906,7 +923,7 @@ to change Zappa's behavior. Use these at your own risk! "Key": "Value", // Example Key and value "Key2": "Value2", }, - "timeout_seconds": 30, // Maximum lifespan for the Lambda function (default 30, max 300.) + "timeout_seconds": 30, // Maximum lifespan for the Lambda function (default 30, max 900.) "touch": true, // GET the production URL upon initial deployment (default True) "touch_path": "/", // The endpoint path to GET when checking the initial deployment (default "/") "use_precompiled_packages": true, // If possible, use C-extension packages which have been pre-compiled for AWS Lambda. Default true. @@ -1049,6 +1066,31 @@ You can also use AWS Cognito User Pool Authorizer by adding: } ``` +#### API Gateway Resource Policy + +You can also use API Gateway Resource Policies. Example of IP Whitelisting: + +```javascript +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "execute-api:Invoke", + "Resource": "execute-api:/*", + "Condition": { + "IpAddress": { + "aws:SourceIp": [ + "1.2.3.4/32" + ] + } + } + } + ] +} +``` + ### Setting Environment Variables #### Local Environment Variables diff --git a/requirements.txt b/requirements.txt index ed83440b8..5b73ae8f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ argcomplete==1.9.3 -base58==1.0.0 botocore>=1.7.19 boto3>=1.4.7 docutils>=0.12 @@ -15,6 +14,8 @@ python-dateutil>=2.6.1, <2.7.0 python-slugify==1.2.4 PyYAML==3.13 requests>=2.10.0 +# requests has issues with urllib3 1.24 +urllib3<=1.23 six>=1.11.0 toml>=0.9.4 tqdm==4.19.1 diff --git a/test_requirements.txt b/test_requirements.txt index 6a8df5713..f2f720714 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -7,3 +7,6 @@ mock>=2.0.0 nose>=1.3.7 nose-timer==0.6.0 placebo>=0.8.1 +# requests has issues with urllib3 1.24 +# and if we install 1.24 here python setup.py install keeps both versions +urllib3<=1.23 diff --git a/test_settings.py b/test_settings.py index eb9c68915..8aa104943 100644 --- a/test_settings.py +++ b/test_settings.py @@ -19,7 +19,8 @@ 'arn:aws:s3:1': 'test_settings.aws_s3_event', 'arn:aws:sns:1': 'test_settings.aws_sns_event', 'arn:aws:dynamodb:1': 'test_settings.aws_dynamodb_event', - 'arn:aws:kinesis:1': 'test_settings.aws_kinesis_event' + 'arn:aws:kinesis:1': 'test_settings.aws_kinesis_event', + 'arn:aws:sqs:1': 'test_settings.aws_sqs_event' } ENVIRONMENT_VARIABLES={'testenv': 'envtest'} @@ -54,9 +55,14 @@ def aws_kinesis_event(event, content): return "AWS KINESIS EVENT" +def aws_sqs_event(event, content): + return "AWS SQS EVENT" + + def authorizer_event(event, content): return "AUTHORIZER_EVENT" def command(): print("command") + diff --git a/tests/tests.py b/tests/tests.py index d575809e5..176c8e68e 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1463,6 +1463,7 @@ def test_get_domain_respects_route53_setting(self, client, template): # And that the route53 path still works zappa_core.route53.list_hosted_zones.return_value = { + 'IsTruncated': False, 'HostedZones': [ { 'Id': 'somezone' @@ -1483,6 +1484,70 @@ def test_get_domain_respects_route53_setting(self, client, template): zappa_core.route53.list_resource_record_sets.assert_called_once_with( HostedZoneId='somezone') + @mock.patch('botocore.client') + def test_get_all_zones_normal_case(self, client): + zappa_core = Zappa( + boto_session=mock.Mock(), + profile_name="test", + aws_region="test", + load_credentials=False + ) + zappa_core.route53 = mock.Mock() + + # Check that it handle the normal case + zappa_core.route53.list_hosted_zones.return_value = { + 'IsTruncated': False, + 'HostedZones': [ + { + 'Id': 'somezone' + } + ] + } + + zones = zappa_core.get_all_zones() + zappa_core.route53.list_hosted_zones.assert_called_with(MaxItems='100') + self.assertListEqual(zones['HostedZones'], [{'Id': 'somezone'}]) + + @mock.patch('botocore.client') + def test_get_all_zones_two_pages(self, client): + zappa_core = Zappa( + boto_session=mock.Mock(), + profile_name="test", + aws_region="test", + load_credentials=False + ) + zappa_core.route53 = mock.Mock() + + # Check that it handle the normal case + zappa_core.route53.list_hosted_zones.side_effect = [ + { + 'IsTruncated': True, + 'HostedZones': [ + { + 'Id': 'zone1' + } + ], + 'NextMarker': "101" + }, + { + 'IsTruncated': False, + 'HostedZones': [ + { + 'Id': 'zone2' + } + ] + } + ] + + zones = zappa_core.get_all_zones() + zappa_core.route53.list_hosted_zones.assert_has_calls( + [ + mock.call(MaxItems='100'), + mock.call(MaxItems='100', Marker='101'), + ] + ) + self.assertListEqual(zones['HostedZones'], [{'Id': 'zone1'}, {'Id': 'zone2'}]) + ## # Django ## diff --git a/tests/tests_placebo.py b/tests/tests_placebo.py index 1bea096e7..6ac4c5425 100644 --- a/tests/tests_placebo.py +++ b/tests/tests_placebo.py @@ -376,6 +376,29 @@ def test_handler(self, session): } self.assertEqual("AWS KINESIS EVENT", lh.handler(event, None)) + # Test AWS SQS event + event = { + u"Records": [ + { + u"messageId": u"c80e8021-a70a-42c7-a470-796e1186f753", + u"receiptHandle": u"AQEBJQ+/u6NsnT5t8Q/VbVxgdUl4TMKZ5FqhksRdIQvLBhwNvADoBxYSOVeCBXdnS9P+erlTtwEALHsnBXynkfPLH3BOUqmgzP25U8kl8eHzq6RAlzrSOfTO8ox9dcp6GLmW33YjO3zkq5VRYyQlJgLCiAZUpY2D4UQcE5D1Vm8RoKfbE+xtVaOctYeINjaQJ1u3mWx9T7tork3uAlOe1uyFjCWU5aPX/1OHhWCGi2EPPZj6vchNqDOJC/Y2k1gkivqCjz1CZl6FlZ7UVPOx3AMoszPuOYZ+Nuqpx2uCE2MHTtMHD8PVjlsWirt56oUr6JPp9aRGo6bitPIOmi4dX0FmuMKD6u/JnuZCp+AXtJVTmSHS8IXt/twsKU7A+fiMK01NtD5msNgVPoe9JbFtlGwvTQ==", + u"body": u"{\"foo\":\"bar\"}", + u"attributes": { + u"ApproximateReceiveCount": u"3", + u"SentTimestamp": u"1529104986221", + u"SenderId": u"594035263019", + u"ApproximateFirstReceiveTimestamp": u"1529104986230" + }, + u"messageAttributes": {}, + u"md5OfBody": u"9bb58f26192e4ba00f01e2e7b136bbd8", + u"eventSource": u"aws:sqs", + u"eventSourceARN": u"arn:aws:sqs:1", + u"awsRegion": u"us-east-1" + } + ] + } + self.assertEqual("AWS SQS EVENT", lh.handler(event, None)) + # Test Authorizer event event = {u'authorizationToken': u'hubtoken1', u'methodArn': u'arn:aws:execute-api:us-west-2:1234:xxxxx/dev/GET/v1/endpoint/param', u'type': u'TOKEN'} self.assertEqual("AUTHORIZER_EVENT", lh.handler(event, None)) diff --git a/zappa/__init__.py b/zappa/__init__.py index 13bcf0a7b..44a4eb09c 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -11,4 +11,4 @@ 'Zappa (and AWS Lambda) support the following versions of Python: {}'.format(formatted_supported_versions) raise RuntimeError(err_msg) -__version__ = '0.46.2' +__version__ = '0.47.0' diff --git a/zappa/cli.py b/zappa/cli.py index a8171430f..d65ef8c54 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -52,6 +52,7 @@ CUSTOM_SETTINGS = [ + 'apigateway_policy', 'assume_policy', 'attach_policy', 'aws_region', @@ -661,7 +662,8 @@ def template(self, lambda_arn, role_arn, output=None, json=False): iam_authorization=self.iam_authorization, authorizer=self.authorizer, cors_options=self.cors, - description=self.apigateway_description + description=self.apigateway_description, + policy=self.apigateway_policy ) if not output: @@ -1018,6 +1020,9 @@ def update(self, source_zip=None, no_upload=False): if endpoint_url and 'https://' not in endpoint_url: endpoint_url = 'https://' + endpoint_url + if self.base_path: + endpoint_url += '/' + self.base_path + deployed_string = "Your updated Zappa deployment is " + click.style("live", fg='green', bold=True) + "!" if self.use_apigateway: deployed_string = deployed_string + ": " + click.style("{}".format(endpoint_url), bold=True) @@ -1459,8 +1464,11 @@ def tabular_print(title, value): # There literally isn't a better way to do this. # AWS provides no way to tie a APIGW domain name to its Lambda function. domain_url = self.stage_config.get('domain', None) + base_path = self.stage_config.get('base_path', None) if domain_url: status_dict["Domain URL"] = 'https://' + domain_url + if base_path: + status_dict["Domain URL"] += '/' + base_path else: status_dict["Domain URL"] = "None Supplied" @@ -2005,6 +2013,7 @@ def load_settings(self, settings_file=None, session=None): self.profile_name = self.stage_config.get('profile_name', None) self.log_level = self.stage_config.get('log_level', "DEBUG") self.domain = self.stage_config.get('domain', None) + self.base_path = self.stage_config.get('base_path', None) self.timeout_seconds = self.stage_config.get('timeout_seconds', 30) dead_letter_arn = self.stage_config.get('dead_letter_arn', '') self.dead_letter_config = {'TargetArn': dead_letter_arn} if dead_letter_arn else {} @@ -2271,6 +2280,11 @@ def create_package(self, output=None): else: settings_s = settings_s + "DOMAIN=None\n" + if self.base_path: + settings_s = settings_s + "BASE_PATH='{0!s}'\n".format((self.base_path)) + else: + settings_s = settings_s + "BASE_PATH=None\n" + # Pass through remote config bucket and path if self.remote_env: settings_s = settings_s + "REMOTE_ENV='{0!s}'\n".format( diff --git a/zappa/core.py b/zappa/core.py index 14d779348..512c72710 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -201,7 +201,7 @@ # Related: https://github.com/Miserlou/Zappa/pull/581 ZIP_EXCLUDES = [ '*.exe', '*.DS_Store', '*.Python', '*.git', '.git/*', '*.zip', '*.tar.gz', - '*.hg', '*.egg-info', 'pip', 'docutils*', 'setuputils*', '__pycache__/*' + '*.hg', 'pip', 'docutils*', 'setuputils*', '__pycache__/*' ] ## @@ -225,6 +225,7 @@ class Zappa(object): extra_permissions = None assume_policy = ASSUME_POLICY attach_policy = ATTACH_POLICY + apigateway_policy = None cloudwatch_log_levels = ['OFF', 'ERROR', 'INFO'] xray_tracing = False @@ -1291,6 +1292,8 @@ def create_api_gateway_routes( self, endpoint = troposphere.apigateway.EndpointConfiguration() endpoint.Types = ["REGIONAL"] restapi.EndpointConfiguration = endpoint + if self.apigateway_policy: + restapi.Policy = json.loads(self.apigateway_policy) self.cf_template.add_resource(restapi) root_id = troposphere.GetAtt(restapi, 'RootResourceId') @@ -2164,7 +2167,7 @@ def update_domain_name(self, "path" : "/certificateArn", "value" : certificate_arn} ]) - + def update_domain_base_path_mapping(self, domain_name, lambda_name, stage, base_path): """ Update domain base path mapping on API Gateway if it was changed @@ -2182,9 +2185,9 @@ def update_domain_base_path_mapping(self, domain_name, lambda_name, stage, base_ self.apigateway_client.update_base_path_mapping(domainName=domain_name, basePath=base_path_mapping['basePath'], patchOperations=[ - {"op" : "replace", - "path" : "/basePath", - "value" : base_path} + {"op" : "replace", + "path" : "/basePath", + "value" : '' if base_path is None else base_path} ]) if not found: self.apigateway_client.create_base_path_mapping( @@ -2194,6 +2197,17 @@ def update_domain_base_path_mapping(self, domain_name, lambda_name, stage, base_ stage=stage ) + def get_all_zones(self): + """Same behaviour of list_host_zones, but transparently handling pagination.""" + zones = {'HostedZones': []} + + new_zones = self.route53.list_hosted_zones(MaxItems='100') + while new_zones['IsTruncated']: + zones['HostedZones'] += new_zones['HostedZones'] + new_zones = self.route53.list_hosted_zones(Marker=new_zones['NextMarker'], MaxItems='100') + + zones['HostedZones'] += new_zones['HostedZones'] + return zones def get_domain_name(self, domain_name, route53=True): """ @@ -2212,7 +2226,7 @@ def get_domain_name(self, domain_name, route53=True): return True try: - zones = self.route53.list_hosted_zones() + zones = self.get_all_zones() for zone in zones['HostedZones']: records = self.route53.list_resource_record_sets(HostedZoneId=zone['Id']) for record in records['ResourceRecordSets']: @@ -2367,10 +2381,10 @@ def schedule_events(self, lambda_arn, lambda_name, events, default=True): http://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html """ - # The two stream sources - DynamoDB and Kinesis - are working differently than the other services (pull vs push) + # The stream sources - DynamoDB, Kinesis and SQS - are working differently than the other services (pull vs push) # and do not require event permissions. They do require additional permissions on the Lambda roles though. # http://docs.aws.amazon.com/lambda/latest/dg/lambda-api-permissions-ref.html - pull_services = ['dynamodb', 'kinesis'] + pull_services = ['dynamodb', 'kinesis', 'sqs'] # XXX: Not available in Lambda yet. # We probably want to execute the latest code. @@ -2615,7 +2629,10 @@ def unschedule_events(self, events, lambda_arn=None, lambda_name=None, excluded_ function, self.boto_session ) - print("Removed event " + name + " (" + str(event_source['events']) + ").") + print("Removed event {}{}.".format( + name, + " ({})".format(str(event_source['events'])) if 'events' in event_source else '') + ) ### # Async / SNS @@ -2811,7 +2828,7 @@ def get_hosted_zone_id_for_domain(self, domain): Get the Hosted Zone ID for a given domain. """ - all_zones = self.route53.list_hosted_zones() + all_zones = self.get_all_zones() return self.get_best_match_zone(all_zones, domain) @staticmethod diff --git a/zappa/handler.py b/zappa/handler.py index 5bfcc4318..c860901a3 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -294,7 +294,7 @@ def get_function_for_aws_event(self, record): """ Get the associated function to execute for a triggered AWS event - Support S3, SNS, DynamoDB and kinesis events + Support S3, SNS, DynamoDB, kinesis and SQS events """ if 's3' in record: if ':' in record['s3']['configurationId']: @@ -311,6 +311,8 @@ def get_function_for_aws_event(self, record): arn = record['Sns'].get('TopicArn') elif 'dynamodb' in record or 'kinesis' in record: arn = record.get('eventSourceARN') + elif 'eventSource' in record and record.get('eventSource') == 'aws:sqs': + arn = record.get('eventSourceARN') elif 's3' in record: arn = record['s3']['bucket']['arn'] @@ -494,10 +496,13 @@ def handler(self, event, context): # API stage script_name = '/' + settings.API_STAGE + base_path = getattr(settings, 'BASE_PATH', None) + # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, + base_path=base_path, trailing_slash=self.trailing_slash, binary_support=settings.BINARY_SUPPORT, context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS diff --git a/zappa/utilities.py b/zappa/utilities.py index e13188c42..93624b14c 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -1,9 +1,11 @@ +import botocore import calendar import datetime import durationpy import fnmatch import io import json +import logging import os import re import shutil @@ -17,6 +19,8 @@ else: from urllib.parse import urlparse +LOG = logging.getLogger(__name__) + ## # Settings / Packaging ## @@ -199,6 +203,7 @@ def get_event_source(event_source, lambda_arn, target_function, boto_session, dr """ import kappa.function import kappa.restapi + import kappa.event_source.base import kappa.event_source.dynamodb_stream import kappa.event_source.kinesis import kappa.event_source.s3 @@ -216,6 +221,103 @@ class PseudoFunction(object): def __init__(self): return + # Mostly adapted from kappa - will probably be replaced by kappa support + class SqsEventSource(kappa.event_source.base.EventSource): + + def __init__(self, context, config): + super(SqsEventSource, self).__init__(context, config) + self._lambda = kappa.awsclient.create_client( + 'lambda', context.session) + + def _get_uuid(self, function): + uuid = None + response = self._lambda.call( + 'list_event_source_mappings', + FunctionName=function.name, + EventSourceArn=self.arn) + LOG.debug(response) + if len(response['EventSourceMappings']) > 0: + uuid = response['EventSourceMappings'][0]['UUID'] + return uuid + + def add(self, function): + try: + response = self._lambda.call( + 'create_event_source_mapping', + FunctionName=function.name, + EventSourceArn=self.arn, + BatchSize=self.batch_size, + Enabled=self.enabled + ) + LOG.debug(response) + except Exception: + LOG.exception('Unable to add event source') + + def enable(self, function): + self._config['enabled'] = True + try: + response = self._lambda.call( + 'update_event_source_mapping', + UUID=self._get_uuid(function), + Enabled=self.enabled + ) + LOG.debug(response) + except Exception: + LOG.exception('Unable to enable event source') + + def disable(self, function): + self._config['enabled'] = False + try: + response = self._lambda.call( + 'update_event_source_mapping', + FunctionName=function.name, + Enabled=self.enabled + ) + LOG.debug(response) + except Exception: + LOG.exception('Unable to disable event source') + + def update(self, function): + response = None + uuid = self._get_uuid(function) + if uuid: + try: + response = self._lambda.call( + 'update_event_source_mapping', + BatchSize=self.batch_size, + Enabled=self.enabled, + FunctionName=function.arn) + LOG.debug(response) + except Exception: + LOG.exception('Unable to update event source') + + def remove(self, function): + response = None + uuid = self._get_uuid(function) + if uuid: + response = self._lambda.call( + 'delete_event_source_mapping', + UUID=uuid) + LOG.debug(response) + return response + + def status(self, function): + response = None + LOG.debug('getting status for event source %s', self.arn) + uuid = self._get_uuid(function) + if uuid: + try: + response = self._lambda.call( + 'get_event_source_mapping', + UUID=self._get_uuid(function)) + LOG.debug(response) + except botocore.exceptions.ClientError: + LOG.debug('event source %s does not exist', self.arn) + response = None + else: + LOG.debug('No UUID for event source %s', self.arn) + return response + class ExtendedSnsEventSource(kappa.event_source.sns.SNSEventSource): @property def filters(self): @@ -245,6 +347,7 @@ def add(self, function): 'kinesis': kappa.event_source.kinesis.KinesisEventSource, 's3': kappa.event_source.s3.S3EventSource, 'sns': ExtendedSnsEventSource, + 'sqs': SqsEventSource, 'events': kappa.event_source.cloudwatch.CloudWatchEventSource } @@ -421,4 +524,4 @@ def titlecase_keys(d): """ Takes a dict with keys of type str and returns a new dict with all keys titlecased. """ - return {k.title(): v for k, v in d.items()} \ No newline at end of file + return {k.title(): v for k, v in d.items()} diff --git a/zappa/wsgi.py b/zappa/wsgi.py index c1f93a1fb..1cd2edbfd 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -32,6 +32,7 @@ def create_wsgi_request(event_info, script_name=None, trailing_slash=True, binary_support=False, + base_path=None, context_header_mappings={} ): """ @@ -87,6 +88,11 @@ def create_wsgi_request(event_info, headers = titlecase_keys(headers) path = urls.url_unquote(event_info['path']) + if base_path: + script_name = '/' + base_path + + if path.startswith(script_name): + path = path[len(script_name):] if query: query_string = urlencode(query)