diff --git a/CHANGELOG.md b/CHANGELOG.md index e1128ce44e9..f139fc3d21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Utilities**: Add new `Validator` utility to validate inbound events and responses using JSON Schema + ## [1.5.0] - 2020-09-04 ### Added diff --git a/Makefile b/Makefile index 20da3040bb9..2b841f362c7 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ target: dev: pip install --upgrade pip poetry pre-commit - poetry install + poetry install --extras "jmespath" pre-commit install dev-docs: diff --git a/aws_lambda_powertools/utilities/validation/__init__.py b/aws_lambda_powertools/utilities/validation/__init__.py new file mode 100644 index 00000000000..94706e3214d --- /dev/null +++ b/aws_lambda_powertools/utilities/validation/__init__.py @@ -0,0 +1,14 @@ +""" +Simple validator to enforce incoming/outgoing event conforms with JSON Schema +""" + +from .exceptions import InvalidEnvelopeExpressionError, InvalidSchemaFormatError, SchemaValidationError +from .validator import validate, validator + +__all__ = [ + "validate", + "validator", + "InvalidSchemaFormatError", + "SchemaValidationError", + "InvalidEnvelopeExpressionError", +] diff --git a/aws_lambda_powertools/utilities/validation/base.py b/aws_lambda_powertools/utilities/validation/base.py new file mode 100644 index 00000000000..eab7f89064d --- /dev/null +++ b/aws_lambda_powertools/utilities/validation/base.py @@ -0,0 +1,65 @@ +import logging +from typing import Any, Dict + +import fastjsonschema +import jmespath +from jmespath.exceptions import LexerError + +from .exceptions import InvalidEnvelopeExpressionError, InvalidSchemaFormatError, SchemaValidationError +from .jmespath_functions import PowertoolsFunctions + +logger = logging.getLogger(__name__) + + +def validate_data_against_schema(data: Dict, schema: Dict): + """Validate dict data against given JSON Schema + + Parameters + ---------- + data : Dict + Data set to be validated + schema : Dict + JSON Schema to validate against + + Raises + ------ + SchemaValidationError + When schema validation fails against data set + InvalidSchemaFormatError + When JSON schema provided is invalid + """ + try: + fastjsonschema.validate(definition=schema, data=data) + except fastjsonschema.JsonSchemaException as e: + message = f"Failed schema validation. Error: {e.message}, Path: {e.path}, Data: {e.value}" # noqa: B306, E501 + raise SchemaValidationError(message) + except (TypeError, AttributeError) as e: + raise InvalidSchemaFormatError(f"Schema received: {schema}. Error: {e}") + + +def unwrap_event_from_envelope(data: Dict, envelope: str, jmespath_options: Dict) -> Any: + """Searches data using JMESPath expression + + Parameters + ---------- + data : Dict + Data set to be filtered + envelope : str + JMESPath expression to filter data against + jmespath_options : Dict + Alternative JMESPath options to be included when filtering expr + + Returns + ------- + Any + Data found using JMESPath expression given in envelope + """ + if not jmespath_options: + jmespath_options = {"custom_functions": PowertoolsFunctions()} + + try: + logger.debug(f"Envelope detected: {envelope}. JMESPath options: {jmespath_options}") + return jmespath.search(envelope, data, options=jmespath.Options(**jmespath_options)) + except (LexerError, TypeError, UnicodeError) as e: + message = f"Failed to unwrap event from envelope using expression. Error: {e} Exp: {envelope}, Data: {data}" # noqa: B306, E501 + raise InvalidEnvelopeExpressionError(message) diff --git a/aws_lambda_powertools/utilities/validation/envelopes.py b/aws_lambda_powertools/utilities/validation/envelopes.py new file mode 100644 index 00000000000..7bc84fce614 --- /dev/null +++ b/aws_lambda_powertools/utilities/validation/envelopes.py @@ -0,0 +1,10 @@ +"""Built-in envelopes""" + +API_GATEWAY_REST = "powertools_json(body)" +API_GATEWAY_HTTP = API_GATEWAY_REST +SQS = "Records[*].powertools_json(body)" +SNS = "Records[0].Sns.Message | powertools_json(@)" +EVENTBRIDGE = "detail" +CLOUDWATCH_EVENTS_SCHEDULED = EVENTBRIDGE +KINESIS_DATA_STREAM = "Records[*].kinesis.powertools_json(powertools_base64(data))" +CLOUDWATCH_LOGS = "awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]" diff --git a/aws_lambda_powertools/utilities/validation/exceptions.py b/aws_lambda_powertools/utilities/validation/exceptions.py new file mode 100644 index 00000000000..6b51fe5ca28 --- /dev/null +++ b/aws_lambda_powertools/utilities/validation/exceptions.py @@ -0,0 +1,14 @@ +class SchemaValidationError(Exception): + """When serialization fail schema validation""" + + pass + + +class InvalidSchemaFormatError(Exception): + """When JSON Schema is in invalid format""" + + pass + + +class InvalidEnvelopeExpressionError(Exception): + """When JMESPath fails to parse expression""" diff --git a/aws_lambda_powertools/utilities/validation/jmespath_functions.py b/aws_lambda_powertools/utilities/validation/jmespath_functions.py new file mode 100644 index 00000000000..b23ab477d6b --- /dev/null +++ b/aws_lambda_powertools/utilities/validation/jmespath_functions.py @@ -0,0 +1,22 @@ +import base64 +import gzip +import json + +import jmespath + + +class PowertoolsFunctions(jmespath.functions.Functions): + @jmespath.functions.signature({"types": ["string"]}) + def _func_powertools_json(self, value): + return json.loads(value) + + @jmespath.functions.signature({"types": ["string"]}) + def _func_powertools_base64(self, value): + return base64.b64decode(value).decode() + + @jmespath.functions.signature({"types": ["string"]}) + def _func_powertools_base64_gzip(self, value): + encoded = base64.b64decode(value) + uncompressed = gzip.decompress(encoded) + + return uncompressed.decode() diff --git a/aws_lambda_powertools/utilities/validation/validator.py b/aws_lambda_powertools/utilities/validation/validator.py new file mode 100644 index 00000000000..c404e90f55a --- /dev/null +++ b/aws_lambda_powertools/utilities/validation/validator.py @@ -0,0 +1,204 @@ +import logging +from typing import Any, Callable, Dict, Union + +from ...middleware_factory import lambda_handler_decorator +from .base import unwrap_event_from_envelope, validate_data_against_schema + +logger = logging.getLogger(__name__) + + +@lambda_handler_decorator +def validator( + handler: Callable, + event: Union[Dict, str], + context: Any, + inbound_schema: Dict = None, + outbound_schema: Dict = None, + envelope: str = None, + jmespath_options: Dict = None, +) -> Any: + """Lambda handler decorator to validate incoming/outbound data using a JSON Schema + + Example + ------- + + **Validate incoming event** + + from aws_lambda_powertools.utilities.validation import validator + + @validator(inbound_schema=json_schema_dict) + def handler(event, context): + return event + + **Validate incoming and outgoing event** + + from aws_lambda_powertools.utilities.validation import validator + + @validator(inbound_schema=json_schema_dict, outbound_schema=response_json_schema_dict) + def handler(event, context): + return event + + **Unwrap event before validating against actual payload - using built-in envelopes** + + from aws_lambda_powertools.utilities.validation import validator, envelopes + + @validator(inbound_schema=json_schema_dict, envelope=envelopes.API_GATEWAY_REST) + def handler(event, context): + return event + + **Unwrap event before validating against actual payload - using custom JMESPath expression** + + from aws_lambda_powertools.utilities.validation import validator + + @validator(inbound_schema=json_schema_dict, envelope="payload[*].my_data") + def handler(event, context): + return event + + **Unwrap and deserialize JSON string event before validating against actual payload - using built-in functions** + + from aws_lambda_powertools.utilities.validation import validator + + @validator(inbound_schema=json_schema_dict, envelope="Records[*].powertools_json(body)") + def handler(event, context): + return event + + **Unwrap, decode base64 and deserialize JSON string event before validating against actual payload - using built-in functions** # noqa: E501 + + from aws_lambda_powertools.utilities.validation import validator + + @validator(inbound_schema=json_schema_dict, envelope="Records[*].kinesis.powertools_json(powertools_base64(data))") + def handler(event, context): + return event + + **Unwrap, decompress ZIP archive and deserialize JSON string event before validating against actual payload - using built-in functions** # noqa: E501 + + from aws_lambda_powertools.utilities.validation import validator + + @validator(inbound_schema=json_schema_dict, envelope="awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]") + def handler(event, context): + return event + + Parameters + ---------- + handler : Callable + Method to annotate on + event : Dict + Lambda event to be validated + context : Any + Lambda context object + inbound_schema : Dict + JSON Schema to validate incoming event + outbound_schema : Dict + JSON Schema to validate outbound event + envelope : Dict + JMESPath expression to filter data against + jmespath_options : Dict + Alternative JMESPath options to be included when filtering expr + + Returns + ------- + Any + Lambda handler response + + Raises + ------ + SchemaValidationError + When schema validation fails against data set + InvalidSchemaFormatError + When JSON schema provided is invalid + InvalidEnvelopeExpressionError + When JMESPath expression to unwrap event is invalid + """ + if envelope: + event = unwrap_event_from_envelope(data=event, envelope=envelope, jmespath_options=jmespath_options) + + if inbound_schema: + logger.debug("Validating inbound event") + validate_data_against_schema(data=event, schema=inbound_schema) + + response = handler(event, context) + + if outbound_schema: + logger.debug("Validating outbound event") + validate_data_against_schema(data=response, schema=outbound_schema) + + return response + + +def validate(event: Dict, schema: Dict = None, envelope: str = None, jmespath_options: Dict = None): + """Standalone function to validate event data using a JSON Schema + + Typically used when you need more control over the validation process. + + **Validate event** + + from aws_lambda_powertools.utilities.validation import validate + + def handler(event, context): + validate(event=event, schema=json_schema_dict) + return event + + **Unwrap event before validating against actual payload - using built-in envelopes** + + from aws_lambda_powertools.utilities.validation import validate, envelopes + + def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope=envelopes.API_GATEWAY_REST) + return event + + **Unwrap event before validating against actual payload - using custom JMESPath expression** + + from aws_lambda_powertools.utilities.validation import validate + + def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="payload[*].my_data") + return event + + **Unwrap and deserialize JSON string event before validating against actual payload - using built-in functions** + + from aws_lambda_powertools.utilities.validation import validate + + def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="Records[*].powertools_json(body)") + return event + + **Unwrap, decode base64 and deserialize JSON string event before validating against actual payload - using built-in functions** + + from aws_lambda_powertools.utilities.validation import validate + + def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="Records[*].kinesis.powertools_json(powertools_base64(data))") + return event + + **Unwrap, decompress ZIP archive and deserialize JSON string event before validating against actual payload - using built-in functions** # noqa: E501 + + from aws_lambda_powertools.utilities.validation import validate + + def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]") + return event + + Parameters + ---------- + event : Dict + Lambda event to be validated + schema : Dict + JSON Schema to validate incoming event + envelope : Dict + JMESPath expression to filter data against + jmespath_options : Dict + Alternative JMESPath options to be included when filtering expr + + Raises + ------ + SchemaValidationError + When schema validation fails against data set + InvalidSchemaFormatError + When JSON schema provided is invalid + InvalidEnvelopeExpressionError + When JMESPath expression to unwrap event is invalid + """ + if envelope: + event = unwrap_event_from_envelope(data=event, envelope=envelope, jmespath_options=jmespath_options) + + validate_data_against_schema(data=event, schema=schema) diff --git a/docs/content/utilities/validation.mdx b/docs/content/utilities/validation.mdx new file mode 100644 index 00000000000..74b762a096e --- /dev/null +++ b/docs/content/utilities/validation.mdx @@ -0,0 +1,236 @@ +--- +title: Validation +description: Utility +--- + + +import Note from "../../src/components/Note" + +This utility provides JSON Schema validation for events and responses, including JMESPath support to unwrap events before validation. + +**Key features** + +* Validate incoming event and response +* JMESPath support to unwrap events before validation applies +* Built-in envelopes to unwrap popular event sources payloads + +## Validating events + +You can validate inbound and outbound events using `validator` decorator. + +You can also use the standalone `validate` function, if you want more control over the validation process such as handling a validation error. + +We support any JSONSchema draft supported by [fastjsonschema](https://horejsek.github.io/python-fastjsonschema/) library. + + + Both validator decorator and validate standalone function expects your JSON Schema to be + a dictionary, not a filename. + + + +### Validator decorator + +**Validator** decorator is typically used to validate either inbound or functions' response. + +It will fail fast with `SchemaValidationError` exception if event or response doesn't conform with given JSON Schema. + +```python:title=validator_decorator.py +from aws_lambda_powertools.utilities.validation import validator + +json_schema_dict = {..} +response_json_schema_dict = {..} + +@validator(inbound_schema=json_schema_dict, outbound_schema=response_json_schema_dict) +def handler(event, context): + return event +``` + +**NOTE**: It's not a requirement to validate both inbound and outbound schemas - You can either use one, or both. + +### Validate function + +**Validate** standalone function is typically used within the Lambda handler, or any other methods that perform data validation. + +You can also gracefully handle schema validation errors by catching `SchemaValidationError` exception. + +```python:title=validator_decorator.py +from aws_lambda_powertools.utilities.validation import validate +from aws_lambda_powertools.utilities.validation.exceptions import SchemaValidationError + +json_schema_dict = {..} + +def handler(event, context): + try: + validate(event=event, schema=json_schema_dict) + except SchemaValidationError as e: + # do something before re-raising + raise + + return event +``` + +## Unwrapping events prior to validation + +You might want to validate only a portion of your event - This is where the `envelope` parameter is for. + +Envelopes are [JMESPath expressions](https://jmespath.org/tutorial.html) to extract a portion of JSON you want before applying JSON Schema validation. + +Here is a sample custom EventBridge event, where we only validate what's inside the `detail` key: + +```json:title=sample_wrapped_event.json +{ + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-east-1", + "resources": ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"], + "detail": {"message": "hello hello", "username": "blah blah"}, // highlight-line +} +``` + +Here is how you'd use the `envelope` parameter to extract the payload inside the `detail` key before validating: + +```python:title=unwrapping_events.py +from aws_lambda_powertools.utilities.validation import validator, validate + +json_schema_dict = {..} + +@validator(inbound_schema=json_schema_dict, envelope="detail") # highlight-line +def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="detail") # highlight-line + return event +``` + +This is quite powerful because you can use JMESPath Query language to extract records from [arrays, slice and dice](https://jmespath.org/tutorial.html#list-and-slice-projections), to [pipe expressions](https://jmespath.org/tutorial.html#pipe-expressions) and [function expressions](https://jmespath.org/tutorial.html#functions), where you'd extract what you need before validating the actual payload. + +## Built-in envelopes + +This utility comes with built-in envelopes to easily extract the payload from popular event sources. + +```python:title=unwrapping_popular_event_sources.py +from aws_lambda_powertools.utilities.validation import envelopes, validate, validator + +json_schema_dict = {..} + +@validator(inbound_schema=json_schema_dict, envelope=envelopes.EVENTBRIDGE) # highlight-line +def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope=envelopes.EVENTBRIDGE) # highlight-line + return event +``` + +Here is a handy table with built-in envelopes along with their JMESPath expressions in case you want to build your own. + +Envelope name | JMESPath expression +------------------------------------------------- | --------------------------------------------------------------------------------- +**API_GATEWAY_REST** | "powertools_json(body)" +**API_GATEWAY_HTTP** | "powertools_json(body)" +**SQS** | "Records[*].powertools_json(body)" +**SNS** | "Records[0].Sns.Message | powertools_json(@)" +**EVENTBRIDGE** | "detail" +**CLOUDWATCH_EVENTS_SCHEDULED** | "detail" +**KINESIS_DATA_STREAM** | "Records[*].kinesis.powertools_json(powertools_base64(data))" +**CLOUDWATCH_LOGS** | "awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]" + +## Built-in JMESPath functions + +You might have events or responses that contain non-encoded JSON, where you need to decode before validating them. + +You can use our built-in JMESPath functions within your expressions to do exactly that to decode JSON Strings, base64, and uncompress gzip data. + + + We use these for built-in envelopes to easily to decode and unwrap events from sources like Kinesis, CloudWatch Logs, etc. + + +### powertools_json function + +Use `powertools_json` function to decode any JSON String. + +This sample will decode the value within the `data` key into a valid JSON before we can validate it. + +```python:title=powertools_json_jmespath_function.py +from aws_lambda_powertools.utilities.validation import validate + +json_schema_dict = {..} +sample_event = { + 'data': '{"payload": {"message": "hello hello", "username": "blah blah"}}' +} + +def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="powertools_json(data)") # highlight-line + return event + +handler(event=sample_event, context={}) +``` + +### powertools_base64 function + +Use `powertools_base64` function to decode any base64 data. + +This sample will decode the base64 value within the `data` key, and decode the JSON string into a valid JSON before we can validate it. + +```python:title=powertools_json_jmespath_function.py +from aws_lambda_powertools.utilities.validation import validate + +json_schema_dict = {..} +sample_event = { + "data": "eyJtZXNzYWdlIjogImhlbGxvIGhlbGxvIiwgInVzZXJuYW1lIjogImJsYWggYmxhaCJ9=" +} + +def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="powertools_json(powertools_base64(data))") # highlight-line + return event + +handler(event=sample_event, context={}) +``` + +### powertools_base64_gzip function + +Use `powertools_base64_gzip` function to decompress and decode base64 data. + +This sample will decompress and decode base64 data, then use JMESPath pipeline expression to pass the result for decoding its JSON string. + +```python:title=powertools_json_jmespath_function.py +from aws_lambda_powertools.utilities.validation import validate + +json_schema_dict = {..} +sample_event = { + "data": "H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==" +} + +def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="powertools_base64_gzip(data) | powertools_json(@)") # highlight-line + return event + +handler(event=sample_event, context={}) +``` + +## Bring your own JMESPath function + + + This should only be used for advanced use cases where you have special formats not covered by the built-in functions. +

+ This will replace all provided built-in functions such as `powertools_json`, so you will no longer be able to use them. +
+ +For special binary formats that you want to decode before applying JSON Schema validation, you can bring your own [JMESPath function](https://github.com/jmespath/jmespath.py#custom-functions) and any additional option via `jmespath_options` param. + +```python:title=custom_jmespath_function +from aws_lambda_powertools.utilities.validation import validate +from jmespath import functions + +json_schema_dict = {..} + +class CustomFunctions(functions.Functions): + + @functions.signature({'types': ['string']}) + def _func_special_decoder(self, s): + return my_custom_decoder_logic(s) + +custom_jmespath_options = {"custom_functions": CustomFunctions()} + +def handler(event, context): + validate(event=event, schema=json_schema_dict, envelope="", jmespath_options=**custom_jmespath_options) # highlight-line + return event +``` diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js index a4286e0d55f..171499c3a22 100644 --- a/docs/gatsby-config.js +++ b/docs/gatsby-config.js @@ -34,6 +34,7 @@ module.exports = { 'utilities/parameters', 'utilities/batch', 'utilities/typing', + 'utilities/validation' ], }, navConfig: { diff --git a/poetry.lock b/poetry.lock index e7b7cdff1db..303b2447966 100644 --- a/poetry.lock +++ b/poetry.lock @@ -104,13 +104,12 @@ description = "Classes Without Boilerplate" name = "attrs" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.3.0" +version = "20.1.0" [package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] [[package]] category = "main" @@ -218,10 +217,11 @@ version = "7.1.2" [[package]] category = "dev" description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\" or platform_system == \"Windows\" or python_version > \"3.4\"" name = "colorama" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.4.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" [[package]] category = "dev" @@ -229,7 +229,7 @@ description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2" +version = "5.2.1" [package.dependencies] [package.dependencies.toml] @@ -270,7 +270,7 @@ description = "Fastest Python implementation of JSON schema" name = "fastjsonschema" optional = false python-versions = "*" -version = "2.14.4" +version = "2.14.5" [package.extras] devel = ["colorama", "jsonschema", "json-spec", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] @@ -589,7 +589,7 @@ description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.5" -version = "8.4.0" +version = "8.5.0" [[package]] category = "dev" @@ -733,7 +733,7 @@ description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.0" +version = "2.10.1" [package.dependencies] coverage = ">=4.4" @@ -781,14 +781,17 @@ description = "Code Metrics in Python" name = "radon" optional = false python-versions = "*" -version = "4.1.0" +version = "4.2.0" [package.dependencies] -colorama = "0.4.1" flake8-polyfill = "*" future = "*" mando = ">=0.6,<0.7" +[package.dependencies.colorama] +python = ">=3.5" +version = ">=0.4.1" + [[package]] category = "dev" description = "Alternative regular expression module, to replace re." @@ -848,7 +851,7 @@ description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false python-versions = ">=3.6" -version = "3.1.0" +version = "3.2.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" @@ -892,7 +895,7 @@ description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4.2" +version = "3.7.4.3" [[package]] category = "main" @@ -900,7 +903,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and name = "urllib3" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.9" +version = "1.25.10" [package.extras] brotli = ["brotlipy (>=0.6.0)"] @@ -942,15 +945,20 @@ description = "Yet another URL library" name = "yarl" optional = false python-versions = ">=3.5" -version = "1.4.2" +version = "1.5.1" [package.dependencies] idna = ">=2.0" multidict = ">=4.0" +[package.dependencies.typing-extensions] +python = "<3.8" +version = ">=3.7.4" + [[package]] category = "main" description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" name = "zipp" optional = false python-versions = ">=3.6" @@ -960,8 +968,11 @@ version = "3.1.0" docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] +[extras] +jmespath = ["jmespath"] + [metadata] -content-hash = "18607a712e4a4a05de7350ecbcf26327a4fb45bb8609dc7f3d19b7610c2faafc" +content-hash = "73a725bb90970d6a99d39eb2fc833937e4576f5fe729d60e9b26d505e08a6ea0" lock-version = "1.0" python-versions = "^3.6" @@ -1005,8 +1016,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, + {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, + {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, ] aws-xray-sdk = [ {file = "aws-xray-sdk-2.6.0.tar.gz", hash = "sha256:abf5b90f740e1f402e23414c9670e59cb9772e235e271fef2bce62b9100cbc77"}, @@ -1041,44 +1052,44 @@ click = [ {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ - {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, - {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] coverage = [ - {file = "coverage-5.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a"}, - {file = "coverage-5.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10"}, - {file = "coverage-5.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62"}, - {file = "coverage-5.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613"}, - {file = "coverage-5.2-cp27-cp27m-win32.whl", hash = "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4"}, - {file = "coverage-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a"}, - {file = "coverage-5.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70"}, - {file = "coverage-5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee"}, - {file = "coverage-5.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b"}, - {file = "coverage-5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913"}, - {file = "coverage-5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c"}, - {file = "coverage-5.2-cp35-cp35m-win32.whl", hash = "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b"}, - {file = "coverage-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e"}, - {file = "coverage-5.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0"}, - {file = "coverage-5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f"}, - {file = "coverage-5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405"}, - {file = "coverage-5.2-cp36-cp36m-win32.whl", hash = "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40"}, - {file = "coverage-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e"}, - {file = "coverage-5.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6"}, - {file = "coverage-5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1"}, - {file = "coverage-5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d"}, - {file = "coverage-5.2-cp37-cp37m-win32.whl", hash = "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec"}, - {file = "coverage-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703"}, - {file = "coverage-5.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032"}, - {file = "coverage-5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d"}, - {file = "coverage-5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e"}, - {file = "coverage-5.2-cp38-cp38-win32.whl", hash = "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7"}, - {file = "coverage-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"}, - {file = "coverage-5.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d"}, - {file = "coverage-5.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d"}, - {file = "coverage-5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c"}, - {file = "coverage-5.2-cp39-cp39-win32.whl", hash = "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c"}, - {file = "coverage-5.2-cp39-cp39-win_amd64.whl", hash = "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2"}, - {file = "coverage-5.2.tar.gz", hash = "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404"}, + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, + {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, + {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, + {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, + {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, + {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, + {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, + {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, + {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, + {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, + {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, + {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, + {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, + {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, + {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, + {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, + {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, + {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, + {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, ] dataclasses = [ {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, @@ -1093,8 +1104,8 @@ eradicate = [ {file = "eradicate-1.0.tar.gz", hash = "sha256:4ffda82aae6fd49dfffa777a857cb758d77502a1f2e0f54c9ac5155a39d2d01a"}, ] fastjsonschema = [ - {file = "fastjsonschema-2.14.4-py3-none-any.whl", hash = "sha256:02a39b518077cc73c1a537f27776527dc6c1e5012d530eb8ac0d1062efbabff7"}, - {file = "fastjsonschema-2.14.4.tar.gz", hash = "sha256:7292cde54f1c30172f78557509ad4cb152f374087fc844bd113a83e2ac494dd6"}, + {file = "fastjsonschema-2.14.5-py3-none-any.whl", hash = "sha256:467593c61f5ba8307205a3536313a774b37df91c9a937c5267c11aee5256e77e"}, + {file = "fastjsonschema-2.14.5.tar.gz", hash = "sha256:afbc235655f06356e46caa80190512e4d9222abfaca856041be5a74c665fa094"}, ] flake8 = [ {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, @@ -1223,8 +1234,8 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ - {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, - {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, + {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, + {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, ] multidict = [ {file = "multidict-4.7.6-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000"}, @@ -1288,8 +1299,8 @@ pytest-asyncio = [ {file = "pytest-asyncio-0.12.0.tar.gz", hash = "sha256:475bd2f3dc0bc11d2463656b3cbaafdbec5a47b47508ea0b329ee693040eebd2"}, ] pytest-cov = [ - {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, - {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, + {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, + {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, ] pytest-mock = [ {file = "pytest-mock-2.0.0.tar.gz", hash = "sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f"}, @@ -1313,8 +1324,8 @@ pyyaml = [ {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] radon = [ - {file = "radon-4.1.0-py2.py3-none-any.whl", hash = "sha256:0c18111ec6cfe7f664bf9db6c51586714ac8c6d9741542706df8a85aca39b99a"}, - {file = "radon-4.1.0.tar.gz", hash = "sha256:56082c52206db45027d4a73612e1b21663c4cc2be3760fee769d966fd7efdd6d"}, + {file = "radon-4.2.0-py2.py3-none-any.whl", hash = "sha256:215e42c8748b5ca8ddf7c061831600b9e73e9c48770a81eeaaeeb066697aee15"}, + {file = "radon-4.2.0.tar.gz", hash = "sha256:b73f6f469c15c9616e0f7ce12080a9ecdee9f2335bdbb5ccea1f2bae26e8d20d"}, ] regex = [ {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, @@ -1356,8 +1367,8 @@ smmap = [ {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, ] stevedore = [ - {file = "stevedore-3.1.0-py3-none-any.whl", hash = "sha256:9fb12884b510fdc25f8a883bb390b8ff82f67863fb360891a33135bcb2ce8c54"}, - {file = "stevedore-3.1.0.tar.gz", hash = "sha256:79270bd5fb4a052e76932e9fef6e19afa77090c4000f2680eb8c2e887d2e6e36"}, + {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, + {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, ] testfixtures = [ {file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"}, @@ -1391,13 +1402,13 @@ typed-ast = [ {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, - {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, - {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] urllib3 = [ - {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, - {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, + {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, + {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1411,23 +1422,23 @@ xenon = [ {file = "xenon-0.7.0.tar.gz", hash = "sha256:5e6433c9297d965bf666256a0a030b6e13660ab87680220c4eb07241f101625b"}, ] yarl = [ - {file = "yarl-1.4.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b"}, - {file = "yarl-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1"}, - {file = "yarl-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080"}, - {file = "yarl-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a"}, - {file = "yarl-1.4.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f"}, - {file = "yarl-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea"}, - {file = "yarl-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb"}, - {file = "yarl-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70"}, - {file = "yarl-1.4.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d"}, - {file = "yarl-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce"}, - {file = "yarl-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"}, - {file = "yarl-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce"}, - {file = "yarl-1.4.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b"}, - {file = "yarl-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae"}, - {file = "yarl-1.4.2-cp38-cp38-win32.whl", hash = "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462"}, - {file = "yarl-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6"}, - {file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"}, + {file = "yarl-1.5.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb"}, + {file = "yarl-1.5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593"}, + {file = "yarl-1.5.1-cp35-cp35m-win32.whl", hash = "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409"}, + {file = "yarl-1.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317"}, + {file = "yarl-1.5.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511"}, + {file = "yarl-1.5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e"}, + {file = "yarl-1.5.1-cp36-cp36m-win32.whl", hash = "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f"}, + {file = "yarl-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2"}, + {file = "yarl-1.5.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a"}, + {file = "yarl-1.5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8"}, + {file = "yarl-1.5.1-cp37-cp37m-win32.whl", hash = "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8"}, + {file = "yarl-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d"}, + {file = "yarl-1.5.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02"}, + {file = "yarl-1.5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a"}, + {file = "yarl-1.5.1-cp38-cp38-win32.whl", hash = "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6"}, + {file = "yarl-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692"}, + {file = "yarl-1.5.1.tar.gz", hash = "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6"}, ] zipp = [ {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, diff --git a/pyproject.toml b/pyproject.toml index 1fc075e8382..7a5f6866816 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,9 @@ license = "MIT-0" [tool.poetry.dependencies] python = "^3.6" aws-xray-sdk = "^2.5.0" -fastjsonschema = "~=2.14.4" +fastjsonschema = "^2.14.5" boto3 = "^1.12" +jmespath = "^0.10.0" [tool.poetry.dev-dependencies] coverage = {extras = ["toml"], version = "^5.0.3"} diff --git a/tests/functional/validator/__init__.py b/tests/functional/validator/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/validator/conftest.py b/tests/functional/validator/conftest.py new file mode 100644 index 00000000000..5c154b5aab4 --- /dev/null +++ b/tests/functional/validator/conftest.py @@ -0,0 +1,358 @@ +import json + +import pytest + + +@pytest.fixture +def schema(): + return { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"message": "hello world", "username": "lessa"}], + "required": ["message", "username"], + "properties": { + "message": { + "$id": "#/properties/message", + "type": "string", + "title": "The message", + "examples": ["hello world"], + }, + "username": { + "$id": "#/properties/username", + "type": "string", + "title": "The username", + "examples": ["lessa"], + }, + }, + } + + +@pytest.fixture +def schema_array(): + return { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "array", + "title": "Sample schema", + "description": "Sample JSON Schema for dummy data in an array", + "examples": [[{"username": "lessa", "message": "hello world"}]], + "additionalItems": True, + "items": { + "$id": "#/items", + "anyOf": [ + { + "$id": "#/items/anyOf/0", + "type": "object", + "description": "Dummy data in an array", + "required": ["message", "username"], + "properties": { + "message": { + "$id": "#/items/anyOf/0/properties/message", + "type": "string", + "title": "The message", + "examples": ["hello world"], + }, + "username": { + "$id": "#/items/anyOf/0/properties/usernam", + "type": "string", + "title": "The username", + "examples": ["lessa"], + }, + }, + } + ], + }, + } + + +@pytest.fixture +def schema_response(): + return { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample outgoing schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"statusCode": 200, "body": "response"}], + "required": ["statusCode", "body"], + "properties": { + "statusCode": {"$id": "#/properties/statusCode", "type": "integer", "title": "The statusCode"}, + "body": {"$id": "#/properties/body", "type": "string", "title": "The response"}, + }, + } + + +@pytest.fixture +def raw_event(): + return {"message": "hello hello", "username": "blah blah"} + + +@pytest.fixture +def wrapped_event(): + return {"data": {"payload": {"message": "hello hello", "username": "blah blah"}}} + + +@pytest.fixture +def wrapped_event_json_string(): + return {"data": json.dumps({"payload": {"message": "hello hello", "username": "blah blah"}})} + + +@pytest.fixture +def wrapped_event_base64_json_string(): + return {"data": "eyJtZXNzYWdlIjogImhlbGxvIGhlbGxvIiwgInVzZXJuYW1lIjogImJsYWggYmxhaCJ9="} + + +@pytest.fixture +def raw_response(): + return {"statusCode": 200, "body": "response"} + + +@pytest.fixture +def apigateway_event(): + return { + "body": '{"message": "hello world", "username": "lessa"}', + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": True, + "queryStringParameters": {"foo": "bar"}, + "multiValueQueryStringParameters": {"foo": ["bar"]}, + "pathParameters": {"proxy": "/path/to/resource"}, + "stageVariables": {"baz": "qux"}, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https", + }, + "multiValueHeaders": { + "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], + "Accept-Encoding": ["gzip, deflate, sdch"], + "Accept-Language": ["en-US,en;q=0.8"], + "Cache-Control": ["max-age=0"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["0123456789.execute-api.us-east-1.amazonaws.com"], + "Upgrade-Insecure-Requests": ["1"], + "User-Agent": ["Custom User Agent String"], + "Via": ["1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="], + "X-Forwarded-For": ["127.0.0.1, 127.0.0.2"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"], + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1", + }, + } + + +@pytest.fixture +def sns_event(): + return { + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:us-east-1::ExampleTopic", + "Sns": { + "Type": "Notification", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:ExampleTopic", + "Subject": "example subject", + "Message": '{"message": "hello world", "username": "lessa"}', + "Timestamp": "1970-01-01T00:00:00.000Z", + "SignatureVersion": "1", + "Signature": "EXAMPLE", + "SigningCertUrl": "EXAMPLE", + "UnsubscribeUrl": "EXAMPLE", + "MessageAttributes": { + "Test": {"Type": "String", "Value": "TestString"}, + "TestBinary": {"Type": "Binary", "Value": "TestBinary"}, + }, + }, + } + ] + } + + +@pytest.fixture +def kinesis_event(): + return { + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJtZXNzYWdlIjogImhlbGxvIGhlbGxvIiwgInVzZXJuYW1lIjogImJsYWggYmxhaCJ9=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600.0, + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "us-east-1", + } + ] + } + + +@pytest.fixture +def eventbridge_event(): + return { + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-east-1", + "resources": ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"], + "detail": {"message": "hello hello", "username": "blah blah"}, + } + + +@pytest.fixture +def sqs_event(): + return { + "Records": [ + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": '{"message": "hello world", "username": "lessa"}', + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001", + }, + "messageAttributes": {}, + "md5OfBody": "7b270e59b47ff90a553787216d55d91d", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1", + }, + ] + } + + +@pytest.fixture +def cloudwatch_logs_event(): + return { + "awslogs": { + "data": "H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==" # noqa: E501 + } + } + + +@pytest.fixture +def cloudwatch_logs_schema(): + return { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "array", + "title": "Sample schema", + "description": "Sample JSON Schema for CloudWatch Logs logEvents using structured dummy data", + "examples": [ + [ + { + "id": "eventId1", + "message": {"username": "lessa", "message": "hello world"}, + "timestamp": 1440442987000, + }, + { + "id": "eventId2", + "message": {"username": "dummy", "message": "hello world"}, + "timestamp": 1440442987001, + }, + ] + ], + "additionalItems": True, + "items": { + "$id": "#/items", + "anyOf": [ + { + "$id": "#/items/anyOf/0", + "type": "object", + "title": "The first anyOf schema", + "description": "Actual log data found in CloudWatch Logs logEvents key", + "required": ["id", "message", "timestamp"], + "properties": { + "id": { + "$id": "#/items/anyOf/0/properties/id", + "type": "string", + "title": "The id schema", + "description": "Unique identifier for log event", + "default": "", + "examples": ["eventId1"], + }, + "message": { + "$id": "#/items/anyOf/0/properties/message", + "type": "object", + "title": "The message schema", + "description": "Log data captured in CloudWatch Logs", + "default": {}, + "examples": [{"username": "lessa", "message": "hello world"}], + "required": ["username", "message"], + "properties": { + "username": { + "$id": "#/items/anyOf/0/properties/message/properties/username", + "type": "string", + "title": "The username", + "examples": ["lessa"], + }, + "message": { + "$id": "#/items/anyOf/0/properties/message/properties/message", + "type": "string", + "title": "The message", + "examples": ["hello world"], + }, + }, + "additionalProperties": True, + }, + "timestamp": { + "$id": "#/items/anyOf/0/properties/timestamp", + "type": "integer", + "title": "The timestamp schema", + "description": "Log event epoch timestamp in milliseconds", + "default": 0, + "examples": [1440442987000], + }, + }, + } + ], + }, + } diff --git a/tests/functional/validator/test_validator.py b/tests/functional/validator/test_validator.py new file mode 100644 index 00000000000..7d2a8465529 --- /dev/null +++ b/tests/functional/validator/test_validator.py @@ -0,0 +1,123 @@ +import jmespath +import pytest +from jmespath import functions + +from aws_lambda_powertools.utilities.validation import envelopes, exceptions, validate, validator + + +def test_validate_raw_event(schema, raw_event): + validate(event=raw_event, schema=schema) + + +def test_validate_wrapped_event_raw_envelope(schema, wrapped_event): + validate(event=wrapped_event, schema=schema, envelope="data.payload") + + +def test_validate_json_string_envelope(schema, wrapped_event_json_string): + validate(event=wrapped_event_json_string, schema=schema, envelope="powertools_json(data).payload") + + +def test_validate_base64_string_envelope(schema, wrapped_event_base64_json_string): + validate(event=wrapped_event_base64_json_string, schema=schema, envelope="powertools_json(powertools_base64(data))") + + +def test_validate_event_does_not_conform_with_schema(schema): + with pytest.raises(exceptions.SchemaValidationError): + validate(event={"message": "hello_world"}, schema=schema) + + +def test_validate_json_string_no_envelope(schema, wrapped_event_json_string): + # WHEN data key contains a JSON String + with pytest.raises(exceptions.SchemaValidationError, match=".*data must be object"): + validate(event=wrapped_event_json_string, schema=schema, envelope="data.payload") + + +def test_validate_invalid_schema_format(raw_event): + with pytest.raises(exceptions.InvalidSchemaFormatError): + validate(event=raw_event, schema="schema.json") + + +def test_validate_invalid_envelope_expression(schema, wrapped_event): + with pytest.raises(exceptions.InvalidEnvelopeExpressionError): + validate(event=wrapped_event, schema=schema, envelope=True) + + +def test_validate_invalid_event(schema): + b64_event = "eyJtZXNzYWdlIjogImhlbGxvIGhlbGxvIiwgInVzZXJuYW1lIjogImJsYWggYmxhaCJ9=" + with pytest.raises(exceptions.SchemaValidationError): + validate(event=b64_event, schema=schema) + + +def test_apigateway_envelope(schema, apigateway_event): + # Payload v1 and v2 remains consistent where the payload is (body) + validate(event=apigateway_event, schema=schema, envelope=envelopes.API_GATEWAY_REST) + validate(event=apigateway_event, schema=schema, envelope=envelopes.API_GATEWAY_HTTP) + + +def test_sqs_envelope(sqs_event, schema_array): + validate(event=sqs_event, schema=schema_array, envelope=envelopes.SQS) + + +def test_sns_envelope(schema, sns_event): + validate(event=sns_event, schema=schema, envelope=envelopes.SNS) + + +def test_eventbridge_envelope(schema, eventbridge_event): + validate(event=eventbridge_event, schema=schema, envelope=envelopes.EVENTBRIDGE) + + +def test_kinesis_data_stream_envelope(schema_array, kinesis_event): + validate(event=kinesis_event, schema=schema_array, envelope=envelopes.KINESIS_DATA_STREAM) + + +def test_cloudwatch_logs_envelope(cloudwatch_logs_schema, cloudwatch_logs_event): + validate(event=cloudwatch_logs_event, schema=cloudwatch_logs_schema, envelope=envelopes.CLOUDWATCH_LOGS) + + +def test_validator_incoming(schema, raw_event): + @validator(inbound_schema=schema) + def lambda_handler(evt, context): + pass + + lambda_handler(raw_event, {}) + + +def test_validator_outgoing(schema_response, raw_response): + @validator(outbound_schema=schema_response) + def lambda_handler(evt, context): + return raw_response + + lambda_handler({}, {}) + + +def test_validator_incoming_and_outgoing(schema, schema_response, raw_event, raw_response): + @validator(inbound_schema=schema, outbound_schema=schema_response) + def lambda_handler(evt, context): + return raw_response + + lambda_handler(raw_event, {}) + + +def test_validator_propagates_exception(schema, raw_event, schema_response): + @validator(inbound_schema=schema, outbound_schema=schema_response) + def lambda_handler(evt, context): + raise ValueError("Bubble up") + + with pytest.raises(ValueError): + lambda_handler(raw_event, {}) + + +def test_custom_jmespath_function_overrides_builtin_functions(schema, wrapped_event_json_string): + class CustomFunctions(functions.Functions): + @functions.signature({"types": ["string"]}) + def _func_echo_decoder(self, value): + return value + + jmespath_opts = {"custom_functions": CustomFunctions()} + with pytest.raises(jmespath.exceptions.UnknownFunctionError, match="Unknown function: powertools_json()"): + validate( + event=wrapped_event_json_string, + schema=schema, + envelope="powertools_json(data).payload", + jmespath_options=jmespath_opts, + )